ai-c/pages/companion-chat/companion-chat.js
2026-02-02 18:21:32 +08:00

752 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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)
// 转换为pxrpx * 屏幕宽度 / 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 })
}
})