// pages/companion-chat/companion-chat.js - 陪聊师列表/陪聊聊天页面 // 对接后端API const api = require('../../utils/api') const { getPageAssets } = require('../../utils/assets') const app = getApp() Page({ data: { statusBarHeight: 44, navBarHeight: 44, totalNavHeight: 88, stickyHeaderHeight: 150, isSticky: false, stickyThreshold: 0, loading: false, showCategoryGrid: true, categoryImageLoadCount: 0, categoryImageErrorCount: 0, // 页面素材 bannerImage: 'https://ai-c.maimanji.com/images/Header-banner.png', categoryImages: [ 'https://ai-c.maimanji.com/images/pb01.png', 'https://ai-c.maimanji.com/images/pb02.png', 'https://ai-c.maimanji.com/images/pb03.png', 'https://ai-c.maimanji.com/images/pb04.png' ], consultButtonImage: '/images/btn-text-consult.png', giftIcon: '/images/icon-gift.png', locationIcon: '/images/icon-location.png', // 页面模式: list(陪聊师列表) / chat(聊天) mode: 'list', // 列表模式数据 searchKeyword: '', counselorList: [], filters: { status: '', gender: '', sortBy: 'rating', specialty: '' }, // 筛选条显示文本 filterLabels: { sort: '排序', gender: '性别', specialty: '类型', filter: '筛选' }, page: 1, hasMore: true, // 聊天模式数据 orderId: '', companionId: '', companionName: '', messages: [], inputText: '', serviceEndTime: null, remainingTime: '', // 未读消息数 totalUnread: 0, // 电话倾诉指南弹窗 showGuidePopup: false, guideData: { title: '电话倾诉指南', subtitle: '让沟通更有效', steps: [ { number: 1, title: '选择合适的倾诉师', description: '根据您的需求,浏览倾诉师的擅长方向、服务经验和用户评价,选择最适合您的专业倾诉师。' }, { number: 2, title: '预约通话时间', description: '选择您方便的时间段进行预约,确保有充足的时间进行深入交流,建议每次通话30-60分钟。' }, { number: 3, title: '准备倾诉内容', description: '提前整理您想要倾诉的问题或困惑,这样能让沟通更有针对性和效果。' }, { number: 4, title: '保持真诚开放', description: '在安全私密的环境中,真诚地表达您的感受和想法。倾诉师会为您保密,请放心倾诉。' } ], tips: { title: '温馨提示', content: '首次通话建议先试听,了解倾诉师的沟通风格。如遇紧急情况,请及时拨打心理危机热线或就医。' } }, auditStatus: 0 }, async onLoad(options) { const systemInfo = wx.getSystemInfoSync() const statusBarHeight = systemInfo.statusBarHeight || 44 const menuButton = wx.getMenuButtonBoundingClientRect() const navBarHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2 const totalNavHeight = statusBarHeight + navBarHeight // 计算固定头部高度(搜索框 + 筛选条) const stickyHeaderHeight = statusBarHeight + 90 // 计算吸顶触发阈值:banner高度(400rpx) + 红包条(约80rpx) + 分类按钮(约230rpx) + 标题行(约80rpx) // 转换为px:rpx * 屏幕宽度 / 750 const screenWidth = systemInfo.screenWidth const stickyThreshold = (400 + 80 + 230 + 80) * screenWidth / 750 this.setData({ statusBarHeight, navBarHeight, totalNavHeight, stickyHeaderHeight, stickyThreshold }) // 加载页面素材 await this.loadPageAssets() // 判断页面模式 if (options.orderId) { // 聊天模式 this.setData({ mode: 'chat', orderId: options.orderId, companionId: options.companionId, companionName: decodeURIComponent(options.name || '') }) this.loadChatHistory() } else { // 列表模式 this.loadCounselorList() } }, onShow() { wx.hideTabBar({ animation: false }) const app = getApp() this.setData({ auditStatus: app.globalData.auditStatus }) this.loadUnreadCount() }, onUnload() { // 清除计时器 if (this.timer) { clearInterval(this.timer) } }, /** * 页面滚动监听 - 控制吸顶效果 */ onPageScroll(e) { const scrollTop = e.detail.scrollTop const { stickyThreshold, isSticky } = this.data // 添加一定的缓冲区,避免在临界点频繁切换 const buffer = 10 // 10px缓冲区 // 当滚动超过阈值+缓冲时显示吸顶,低于阈值-缓冲时隐藏 let shouldSticky = isSticky if (scrollTop > stickyThreshold + buffer) { shouldSticky = true } else if (scrollTop < stickyThreshold - buffer) { shouldSticky = false } if (shouldSticky !== isSticky) { this.setData({ isSticky: shouldSticky }) } }, /** * 加载页面素材配置 */ async loadPageAssets() { try { const assets = await getPageAssets(); if (assets) { this.setData({ bannerImage: assets.banners.companion_banner, categoryImages: [ assets.entries.entry_1, assets.entries.entry_2, assets.entries.entry_3, assets.entries.entry_4 ], consultButtonImage: assets.icons.consult_button, giftIcon: assets.icons.gift, locationIcon: assets.icons.location }); } } catch (error) { console.error('加载页面素材失败:', error); // 使用默认值,已在 data 中定义 } }, /** * 加载未读消息数 */ async loadUnreadCount() { if (!app.globalData.isLoggedIn) { this.setData({ totalUnread: 0 }) return } try { const res = await api.chat.getConversations() if (res.success && res.data) { const totalUnread = res.data.reduce((sum, conv) => sum + (conv.unread_count || 0), 0) this.setData({ totalUnread }) } else { this.setData({ totalUnread: 0 }) } } catch (err) { console.log('获取未读消息数失败', err) this.setData({ totalUnread: 0 }) } }, // ==================== 列表模式 ==================== /** * 加载陪聊师列表 */ async loadCounselorList() { this.setData({ loading: true, page: 1 }) try { const params = { page: 1, pageSize: 20, ...this.data.filters } // 过滤掉空值参数 Object.keys(params).forEach(key => { if (params[key] === '' || params[key] === undefined || params[key] === null) { delete params[key] } }) if (this.data.searchKeyword.trim()) { params.keyword = this.data.searchKeyword.trim() } console.log('请求陪聊师列表,参数:', params) const res = await api.companion.getList(params) console.log('陪聊师列表响应:', res) // 兼容两种返回格式 let list = [] if (res.success && res.data) { // 格式1: { success: true, data: { list: [...] } } // 格式2: { success: true, data: [...] } list = Array.isArray(res.data) ? res.data : (res.data.list || []) } if (list.length > 0) { const transformedList = list.map(c => this.transformCounselor(c)) this.setData({ counselorList: transformedList, hasMore: list.length >= 20, loading: false }) console.log('更新列表成功,数量:', transformedList.length) } else { // API没有数据时使用模拟数据 console.log('API返回空数据,使用模拟数据') this.setData({ counselorList: this.getMockCounselorList(), loading: false }) } } catch (err) { console.error('加载陪聊师列表失败', err) // 加载失败时使用模拟数据 this.setData({ counselorList: this.getMockCounselorList(), loading: false }) } }, /** * 获取模拟陪聊师数据 */ getMockCounselorList() { return [ { id: 'c001', name: '林心怡', avatar: '', avatarColor: '#e8b4d8', avatarColorEnd: '#c984cd', type: '文字/语音', age: '28岁', education: '心理学硕士', training: '国家二级心理咨询师', certification: '情感咨询专家认证', quote: '每一次倾诉,都是心灵的释放', tags: ['情感倾诉', '婚姻家庭', '亲密关系'], serviceCount: 1286, repeatCount: 423, rating: 4.96, location: '北京', online: true }, { id: 'c002', name: '张明辉', avatar: '', avatarColor: '#a8d8ea', avatarColorEnd: '#6bb3d9', type: '文字/语音', age: '35岁', education: '应用心理学博士', training: '高级心理咨询师', certification: '职场心理专家', quote: '用专业的态度,温暖每一颗心', tags: ['职场压力', '人际关系', '情绪管理'], serviceCount: 2156, repeatCount: 687, rating: 4.92, location: '上海', online: true }, { id: 'c003', name: '王雨萱', avatar: '', avatarColor: '#f8c8dc', avatarColorEnd: '#e89bb8', type: '文字/语音', age: '26岁', education: '心理学学士', training: '情感咨询师', certification: '青年心理辅导员', quote: '倾听你的故事,陪伴你的成长', tags: ['恋爱指导', '分手挽回', '单身脱单'], serviceCount: 856, repeatCount: 298, rating: 4.89, location: '深圳', online: false }, { id: 'c004', name: '李思远', avatar: '', avatarColor: '#b8d4e3', avatarColorEnd: '#8ab4cf', type: '文字/语音', age: '42岁', education: '临床心理学硕士', training: '资深心理治疗师', certification: '家庭治疗师认证', quote: '专业倾听,用心陪伴每一刻', tags: ['婚姻危机', '家庭矛盾', '亲子教育'], serviceCount: 3421, repeatCount: 1156, rating: 4.98, location: '广州', online: true }, { id: 'c005', name: '陈晓琳', avatar: '', avatarColor: '#d4b8e8', avatarColorEnd: '#b088d4', type: '文字/语音', age: '31岁', education: '发展心理学硕士', training: '心理咨询师', certification: '情绪管理专家', quote: '让每一次对话都充满温暖', tags: ['焦虑抑郁', '情绪调节', '自我成长'], serviceCount: 1567, repeatCount: 512, rating: 4.94, location: '杭州', online: true }, { id: 'c006', name: '赵文博', avatar: '', avatarColor: '#a8e6cf', avatarColorEnd: '#7bc9a6', type: '文字/语音', age: '38岁', education: '社会心理学博士', training: '高级心理顾问', certification: '企业EAP咨询师', quote: '理性分析,感性陪伴', tags: ['职业规划', '压力管理', '领导力'], serviceCount: 1892, repeatCount: 634, rating: 4.91, location: '成都', online: false } ] }, /** * 转换陪聊师数据格式 */ transformCounselor(data) { const config = require('../../config/index') // 处理地址,只显示城市名 let location = data.location || data.city || '' if (location) { // 如果地址包含多个部分(如"北京 北京市 朝阳区"),只取第一个城市名 // 或者如果是"XX市"格式,去掉"市"字 const parts = location.split(/[\s,,]+/).filter(p => p.trim()) if (parts.length > 0) { location = parts[0].replace(/[省市区县]$/, '') || parts[0] } } // 处理在线状态 - 后端返回 onlineStatus 或 status const onlineStatus = data.onlineStatus || data.online_status || data.status || 'offline' const isOnline = onlineStatus === 'online' const isBusy = onlineStatus === 'busy' // 处理头像URL - 如果是相对路径,拼接完整域名 let avatar = data.avatar || '' if (avatar && avatar.startsWith('/')) { // 从 API_BASE_URL 提取域名(去掉 /api 后缀) const baseUrl = config.API_BASE_URL.replace(/\/api$/, '') avatar = baseUrl + avatar } return { id: data.id, name: data.name || data.displayName || data.nickname, avatar: avatar, type: data.service_type || '文字/语音', age: data.age_group || data.age || '', education: data.education || '', training: data.training || data.certification || '', certification: data.certification || '', quote: data.quote || data.bio || data.introduction || '', tags: data.tags || data.specialties || [], serviceCount: data.serviceCount || data.service_count || data.totalOrders || 0, repeatCount: data.repeatCount || data.repeat_count || 0, rating: data.rating || 5.0, location: location, online: isOnline, onlineStatus: onlineStatus, statusText: data.statusText || (isOnline ? '在线' : isBusy ? '忙碌中' : '离线'), // 等级和价格信息 levelCode: data.levelCode || 'junior', levelName: data.levelName || '初级', textPrice: data.textPrice || data.pricePerMinute || 0.5, voicePrice: data.voicePrice || 1 } }, onSearchInput(e) { this.setData({ searchKeyword: e.detail.value }) }, /** * 搜索 */ onSearch() { this.loadCounselorList() }, onCategoryTap(e) { const type = e.currentTarget.dataset.type wx.showToast({ title: type, icon: 'none' }) }, /** * 分类图片加载成功 */ onCategoryImageLoad() { const count = this.data.categoryImageLoadCount + 1 this.setData({ categoryImageLoadCount: count }) }, /** * 分类图片加载失败 - 如果全部失败则隐藏区域 */ onCategoryImageError() { const errorCount = this.data.categoryImageErrorCount + 1 this.setData({ categoryImageErrorCount: errorCount }) // 如果4张图片都加载失败,隐藏整个分类区域 if (errorCount >= 4) { this.setData({ showCategoryGrid: false }) } }, onFilterTap(e) { const filter = e.currentTarget.dataset.filter console.log('筛选点击:', filter) const options = this.getFilterOptions(filter) console.log('筛选选项:', options) if (!options || options.length === 0) { wx.showToast({ title: '暂无筛选选项', icon: 'none' }) return } wx.showActionSheet({ itemList: options, success: (res) => { console.log('选择了:', res.tapIndex, options[res.tapIndex]) const value = this.getFilterValue(filter, res.tapIndex) const label = options[res.tapIndex] // 更新筛选值 - 映射筛选类型到正确的参数名 // sort -> sortBy, filter -> status, 其他保持不变 const filterKeyMap = { sort: 'sortBy', filter: 'status', // 筛选选项对应后端的 status 参数 gender: 'gender', specialty: 'specialty' } const filterKey = filterKeyMap[filter] || filter // 更新筛选标签显示 const defaultLabels = { sort: '排序', gender: '性别', specialty: '类型', filter: '筛选' } const displayLabel = res.tapIndex === 0 ? defaultLabels[filter] : label console.log('更新筛选参数:', filterKey, '=', value) this.setData({ [`filters.${filterKey}`]: value, [`filterLabels.${filter}`]: displayLabel }) this.loadCounselorList() }, fail: (err) => { console.log('取消选择或失败:', err) } }) }, getFilterOptions(filter) { const options = { sort: ['综合排序', '好评优先', '服务人次', '价格从低到高'], gender: ['不限', '男', '女'], specialty: ['不限', '情感倾诉', '婚姻家庭', '职场压力', '亲子关系'], filter: ['在线优先', '有空闲', '全部'] } return options[filter] || [] }, getFilterValue(filter, index) { const values = { sort: ['', 'rating', 'service_count', 'price'], gender: ['', 'male', 'female'], specialty: ['', 'emotion', 'marriage', 'work', 'family'], filter: ['online', 'available', ''] } return (values[filter] || [])[index] || '' }, async onGuideClick() { // 显示弹窗 this.setData({ showGuidePopup: true }) // 从后台获取协议内容 try { const res = await api.agreement.get('phone-guide') if (res.success && res.data && res.data.content) { // 解析JSON内容 let content = res.data.content if (typeof content === 'string') { try { content = JSON.parse(content) } catch (e) { console.log('协议内容不是JSON格式') return } } // 更新弹窗数据 this.setData({ 'guideData.subtitle': content.subtitle || '让沟通更有效', 'guideData.steps': content.steps || this.data.guideData.steps, 'guideData.tips': content.tips || this.data.guideData.tips }) } } catch (err) { console.log('获取协议内容失败,使用默认内容', err) } }, /** * 关闭指南弹窗 */ onCloseGuidePopup() { this.setData({ showGuidePopup: false }) }, /** * 阻止弹窗内容区域的点击事件冒泡 */ preventClose() { // 空函数,阻止事件冒泡 }, onCounselorTap(e) { const id = e.currentTarget.dataset.id wx.navigateTo({ url: `/pages/counselor-detail/counselor-detail?id=${id}` }) }, /** * 头像加载失败时,清空avatar字段让其显示占位符 */ onAvatarError(e) { const index = e.currentTarget.dataset.index if (index !== undefined) { this.setData({ [`counselorList[${index}].avatar`]: '' }) } }, onTrialListen(e) { const id = e.currentTarget.dataset.id wx.showToast({ title: '正在播放试听...', icon: 'none' }) }, onConsult(e) { const id = e.currentTarget.dataset.id wx.navigateTo({ url: `/pages/counselor-detail/counselor-detail?id=${id}` }) }, // ==================== 聊天模式 ==================== /** * 加载聊天历史 */ async loadChatHistory() { this.setData({ loading: true }) try { const res = await api.chat.getChatHistory(this.data.orderId, { page: 1, limit: 50 }) if (res.success && res.data) { const messages = (res.data || []).map(msg => ({ id: msg.id, content: msg.content, isMe: msg.sender_type === 'user', time: this.formatTime(msg.created_at), type: msg.message_type || 'text' })) this.setData({ messages, loading: false }) this.scrollToBottom() } else { this.setData({ loading: false }) } } catch (err) { console.error('加载聊天历史失败', err) this.setData({ loading: false }) } }, /** * 发送消息 */ async sendMessage() { const { inputText, orderId } = this.data if (!inputText.trim()) return const messageText = inputText.trim() // 添加到消息列表 const newMessage = { id: Date.now(), content: messageText, isMe: true, time: this.formatTime(new Date()), type: 'text' } this.setData({ messages: [...this.data.messages, newMessage], inputText: '' }) this.scrollToBottom() try { await api.companion.sendMessage({ order_id: orderId, message: messageText }) } catch (err) { console.error('发送消息失败', err) wx.showToast({ title: '发送失败', icon: 'none' }) } }, onInput(e) { this.setData({ inputText: e.detail.value }) }, /** * 格式化时间 */ formatTime(dateStr) { const date = new Date(dateStr) const hour = String(date.getHours()).padStart(2, '0') const minute = String(date.getMinutes()).padStart(2, '0') return `${hour}:${minute}` }, scrollToBottom() { // 滚动到底部 }, onBack() { wx.navigateBack() }, // Tab bar navigation - 需要登录的页面检查登录状态 switchTab(e) { const path = e.currentTarget.dataset.path const app = getApp() // 消息和我的页面需要登录 if (path === '/pages/chat/chat' || path === '/pages/profile/profile') { if (!app.globalData.isLoggedIn) { wx.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent(path) }) return } } wx.switchTab({ url: path }) } })