1172 lines
32 KiB
JavaScript
1172 lines
32 KiB
JavaScript
// pages/counselor-detail/counselor-detail.js - 陪聊师详情页面
|
||
// 对接后端API
|
||
|
||
const api = require('../../utils/api')
|
||
const app = getApp()
|
||
|
||
Page({
|
||
data: {
|
||
statusBarHeight: 44,
|
||
navBarHeight: 44,
|
||
totalNavHeight: 88, // 总导航栏高度(状态栏+导航内容)
|
||
menuButtonTop: 0,
|
||
menuButtonHeight: 32,
|
||
loading: true,
|
||
counselor: null,
|
||
messages: [],
|
||
currentTime: '',
|
||
inputText: '',
|
||
inputFocus: false,
|
||
// 语音录音相关
|
||
isVoiceMode: false,
|
||
isRecording: false,
|
||
voiceCancelHint: false,
|
||
recordingDuration: 0,
|
||
recordingStartY: 0,
|
||
// 下单相关
|
||
showOrderModal: false,
|
||
selectedDuration: 30,
|
||
selectedServiceType: 'text', // 服务类型:text/voice
|
||
durations: [
|
||
{ value: 15, label: '15分钟' },
|
||
{ value: 30, label: '30分钟' },
|
||
{ value: 60, label: '60分钟' }
|
||
],
|
||
// 人物介绍弹窗
|
||
showProfileModal: false,
|
||
// 评价弹窗
|
||
showReviewModal: false,
|
||
reviews: [],
|
||
reviewStats: {
|
||
totalCount: 0,
|
||
goodRate: 100
|
||
},
|
||
hasMoreReviews: true,
|
||
loadingReviews: false,
|
||
reviewPage: 1,
|
||
// 更多功能面板
|
||
showMorePanel: false,
|
||
// 表情面板
|
||
showEmoji: false,
|
||
emojis: [
|
||
"😊", "😀", "😁", "😃", "😂", "🤣", "😅", "😆", "😉", "😋", "😎", "😍", "😘", "🥰", "😗", "😙",
|
||
"🙂", "🤗", "🤩", "🤔", "😐", "😑", "😶", "🙄", "😏", "😣", "😥", "😮", "😯", "😪", "😫", "😴",
|
||
"🥱", "😌", "😛", "😜", "😝", "🤤", "😒", "😓", "😔", "😕", "🙃", "🤑", "😲", "☹️", "🙁", "😖",
|
||
"😞", "😟", "😤", "😢", "😭", "😦", "😧", "😨", "😩", "🤯", "😬", "😰", "😱", "🥵", "🥶", "😳",
|
||
"🤪", "😵", "🥴", "😠", "😡", "🤬", "😷", "🤒", "🤕", "🤢", "🤮", "🤧", "😇", "🥳", "🥺", "🤠",
|
||
"❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔", "❣️", "💕", "💞", "💓", "💗", "💖",
|
||
"💘", "💝", "💟", "👍", "👎", "👏", "🙌", "👐", "🤲", "🤝", "🙏", "✌️", "🤞", "🤟", "🤘", "👌"
|
||
]
|
||
},
|
||
|
||
onLoad(options) {
|
||
const systemInfo = wx.getSystemInfoSync()
|
||
const statusBarHeight = systemInfo.statusBarHeight || 44
|
||
|
||
// 获取胶囊按钮位置信息
|
||
const menuButton = wx.getMenuButtonBoundingClientRect()
|
||
|
||
// 正确计算导航栏高度的方法:
|
||
// 导航内容高度 = 胶囊按钮高度 + 上下边距
|
||
// 胶囊按钮距离状态栏的间距 = menuButton.top - statusBarHeight
|
||
// 导航内容高度 = 胶囊按钮高度 + 2 * 间距(上下对称)
|
||
const navContentHeight = menuButton.height + (menuButton.top - statusBarHeight) * 2
|
||
|
||
// 整个导航栏高度(包含状态栏)= 状态栏高度 + 导航内容高度
|
||
const totalNavHeight = statusBarHeight + navContentHeight
|
||
|
||
// 获取当前时间
|
||
const now = new Date()
|
||
const hours = now.getHours()
|
||
const minutes = now.getMinutes().toString().padStart(2, '0')
|
||
const period = hours < 12 ? '上午' : (hours < 18 ? '下午' : '晚上')
|
||
const displayHour = hours > 12 ? hours - 12 : hours
|
||
|
||
this.setData({
|
||
statusBarHeight,
|
||
navBarHeight: navContentHeight, // 导航内容高度(不含状态栏)
|
||
totalNavHeight, // 总高度(含状态栏)
|
||
menuButtonTop: menuButton.top,
|
||
menuButtonHeight: menuButton.height,
|
||
currentTime: `${period} ${displayHour}:${minutes}`
|
||
})
|
||
|
||
// 加载陪聊师数据
|
||
if (options.id) {
|
||
this.loadCounselorDetail(options.id)
|
||
} else {
|
||
wx.showToast({ title: '参数错误', icon: 'none' })
|
||
setTimeout(() => wx.navigateBack(), 1500)
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 页面卸载时清理资源
|
||
*/
|
||
onUnload() {
|
||
// 停止录音
|
||
if (this.data.isRecording && this.recorderManager) {
|
||
this.voiceCanceled = true
|
||
this.recorderManager.stop()
|
||
}
|
||
|
||
// 清除录音计时器
|
||
if (this.recordingTimer) {
|
||
clearInterval(this.recordingTimer)
|
||
this.recordingTimer = null
|
||
}
|
||
|
||
// 清理录音管理器
|
||
if (this.recorderManager) {
|
||
this.recorderManager = null
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 页面隐藏时
|
||
*/
|
||
onHide() {
|
||
// 停止录音
|
||
if (this.data.isRecording && this.recorderManager) {
|
||
this.voiceCanceled = true
|
||
this.recorderManager.stop()
|
||
this.setData({ isRecording: false })
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 加载陪聊师详情
|
||
*/
|
||
async loadCounselorDetail(id) {
|
||
this.setData({ loading: true })
|
||
|
||
try {
|
||
console.log('加载陪聊师详情,ID:', id)
|
||
const res = await api.companion.getDetail(id)
|
||
|
||
console.log('陪聊师详情响应:', res)
|
||
|
||
// 兼容两种返回格式:{ success: true, data: {...} } 或 { code: 0, data: {...} }
|
||
if ((res.success || res.code === 0) && res.data) {
|
||
const counselor = this.transformCounselor(res.data)
|
||
|
||
console.log('转换后的陪聊师数据:', counselor)
|
||
|
||
// 设置欢迎消息
|
||
const welcomeMsg = {
|
||
id: 1,
|
||
content: `您好,我是${counselor.name}。很高兴在这里陪伴您。有什么可以帮助您的吗?【系统自动回复】`
|
||
}
|
||
|
||
this.setData({
|
||
counselor,
|
||
messages: [welcomeMsg],
|
||
loading: false
|
||
})
|
||
} else {
|
||
console.error('API返回失败:', res.error || res.message)
|
||
wx.showToast({ title: res.error || '加载失败', icon: 'none' })
|
||
this.setData({ loading: false })
|
||
setTimeout(() => wx.navigateBack(), 1500)
|
||
}
|
||
} catch (err) {
|
||
console.error('加载陪聊师详情失败', err)
|
||
wx.showToast({ title: '网络错误', icon: 'none' })
|
||
this.setData({ loading: false })
|
||
setTimeout(() => wx.navigateBack(), 1500)
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 加载模拟陪聊师数据
|
||
*/
|
||
loadMockCounselor(id) {
|
||
const mockData = {
|
||
'c001': {
|
||
id: 'c001',
|
||
name: '林心怡',
|
||
avatarColor: '#e8b4d8',
|
||
avatarColorEnd: '#c984cd',
|
||
location: '北京',
|
||
city: '北京',
|
||
age: '28岁',
|
||
experience: '5年咨询经验',
|
||
education: '心理学硕士',
|
||
serviceCount: 1286,
|
||
repeatCount: 423,
|
||
rating: 4.96,
|
||
quote: '每一次倾诉,都是心灵的释放',
|
||
certification: '国家二级心理咨询师 | 情感咨询专家认证',
|
||
status: 'online',
|
||
statusText: '在线',
|
||
isBusy: false,
|
||
levelCode: 'junior',
|
||
levelName: '初级',
|
||
textPrice: 0.5,
|
||
voicePrice: 1
|
||
},
|
||
'c002': {
|
||
id: 'c002',
|
||
name: '张明辉',
|
||
avatarColor: '#a8d8ea',
|
||
avatarColorEnd: '#6bb3d9',
|
||
location: '上海',
|
||
city: '上海',
|
||
age: '35岁',
|
||
experience: '8年咨询经验',
|
||
education: '应用心理学博士',
|
||
serviceCount: 2156,
|
||
repeatCount: 687,
|
||
rating: 4.92,
|
||
quote: '用专业的态度,温暖每一颗心',
|
||
certification: '高级心理咨询师 | 职场心理专家',
|
||
status: 'online',
|
||
statusText: '在线',
|
||
isBusy: false,
|
||
levelCode: 'senior',
|
||
levelName: '高级',
|
||
textPrice: 1.5,
|
||
voicePrice: 3
|
||
},
|
||
'c003': {
|
||
id: 'c003',
|
||
name: '王雨萱',
|
||
avatarColor: '#f8c8dc',
|
||
avatarColorEnd: '#e89bb8',
|
||
location: '深圳',
|
||
city: '深圳',
|
||
age: '26岁',
|
||
experience: '3年咨询经验',
|
||
education: '心理学学士',
|
||
serviceCount: 856,
|
||
repeatCount: 298,
|
||
rating: 4.89,
|
||
quote: '倾听你的故事,陪伴你的成长',
|
||
certification: '情感咨询师 | 青年心理辅导员',
|
||
status: 'offline',
|
||
statusText: '离线',
|
||
isBusy: true,
|
||
levelCode: 'intermediate',
|
||
levelName: '中级',
|
||
textPrice: 1,
|
||
voicePrice: 2
|
||
},
|
||
'c004': {
|
||
id: 'c004',
|
||
name: '李思远',
|
||
avatarColor: '#b8d4e3',
|
||
avatarColorEnd: '#8ab4cf',
|
||
location: '广州',
|
||
city: '广州',
|
||
age: '42岁',
|
||
experience: '15年咨询经验',
|
||
education: '临床心理学硕士',
|
||
serviceCount: 3421,
|
||
repeatCount: 1156,
|
||
rating: 4.98,
|
||
quote: '专业倾听,用心陪伴每一刻',
|
||
certification: '资深心理治疗师 | 家庭治疗师认证',
|
||
status: 'online',
|
||
statusText: '在线',
|
||
isBusy: false,
|
||
levelCode: 'expert',
|
||
levelName: '资深',
|
||
textPrice: 2,
|
||
voicePrice: 4
|
||
},
|
||
'c005': {
|
||
id: 'c005',
|
||
name: '陈晓琳',
|
||
avatarColor: '#d4b8e8',
|
||
avatarColorEnd: '#b088d4',
|
||
location: '杭州',
|
||
city: '杭州',
|
||
age: '31岁',
|
||
experience: '6年咨询经验',
|
||
education: '发展心理学硕士',
|
||
serviceCount: 1567,
|
||
repeatCount: 512,
|
||
rating: 4.94,
|
||
quote: '让每一次对话都充满温暖',
|
||
certification: '心理咨询师 | 情绪管理专家',
|
||
status: 'online',
|
||
statusText: '在线',
|
||
isBusy: false,
|
||
levelCode: 'senior',
|
||
levelName: '高级',
|
||
textPrice: 1.5,
|
||
voicePrice: 3
|
||
},
|
||
'c006': {
|
||
id: 'c006',
|
||
name: '赵文博',
|
||
avatarColor: '#a8e6cf',
|
||
avatarColorEnd: '#7bc9a6',
|
||
location: '成都',
|
||
city: '成都',
|
||
age: '38岁',
|
||
experience: '10年咨询经验',
|
||
education: '社会心理学博士',
|
||
serviceCount: 1892,
|
||
repeatCount: 634,
|
||
rating: 4.91,
|
||
quote: '理性分析,感性陪伴',
|
||
certification: '高级心理顾问 | 企业EAP咨询师',
|
||
status: 'busy',
|
||
statusText: '忙碌中',
|
||
isBusy: true,
|
||
levelCode: 'intermediate',
|
||
levelName: '中级',
|
||
textPrice: 1,
|
||
voicePrice: 2
|
||
}
|
||
}
|
||
|
||
const counselor = mockData[id] || mockData['c001']
|
||
|
||
// 设置欢迎消息
|
||
const welcomeMsg = {
|
||
id: 1,
|
||
content: `您好,我是${counselor.name}。很高兴在这里陪伴您。有什么可以帮助您的吗?【系统自动回复】`
|
||
}
|
||
|
||
this.setData({
|
||
counselor,
|
||
messages: [welcomeMsg],
|
||
loading: false
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 转换陪聊师数据格式
|
||
*/
|
||
transformCounselor(data) {
|
||
const config = require('../../config/index')
|
||
console.log('原始陪聊师数据:', data)
|
||
|
||
const statusMap = {
|
||
online: { text: '在线', isBusy: false },
|
||
busy: { text: '忙碌中', isBusy: true },
|
||
offline: { text: '离线', isBusy: true }
|
||
}
|
||
|
||
const status = data.status || data.onlineStatus || data.online_status || 'offline'
|
||
const statusInfo = statusMap[status] || statusMap.offline
|
||
|
||
// 等级名称映射
|
||
const levelNameMap = {
|
||
junior: '初级',
|
||
intermediate: '中级',
|
||
senior: '高级',
|
||
expert: '资深'
|
||
}
|
||
|
||
// 优先使用 displayName,然后是 name,最后是 nickname
|
||
const name = data.displayName || data.display_name || data.name || data.nickname || '未知'
|
||
|
||
// 处理地址,只显示城市名
|
||
let location = data.location || data.city || ''
|
||
if (location) {
|
||
// 如果地址包含多个部分(如"北京 北京市 朝阳区"),只取第一个城市名
|
||
const parts = location.split(/[\s,,]+/).filter(p => p.trim())
|
||
if (parts.length > 0) {
|
||
location = parts[0].replace(/[省市区县]$/, '') || parts[0]
|
||
}
|
||
}
|
||
|
||
// 处理头像URL - 如果是相对路径,拼接完整域名
|
||
let avatar = data.avatar || ''
|
||
if (avatar && avatar.startsWith('/')) {
|
||
// 从 API_BASE_URL 提取域名(去掉 /api 后缀)
|
||
const baseUrl = config.API_BASE_URL.replace(/\/api$/, '')
|
||
avatar = baseUrl + avatar
|
||
}
|
||
|
||
const result = {
|
||
id: data.id,
|
||
name: name,
|
||
avatarColor: data.avatar_color || data.avatarColor || '#c984cd',
|
||
avatarColorEnd: data.avatar_color_end || data.avatarColorEnd || '#b06ab3',
|
||
avatar: avatar,
|
||
location: location,
|
||
city: location,
|
||
age: data.age_group || data.ageGroup || data.age || '',
|
||
experience: data.experience || '',
|
||
education: data.education || '',
|
||
serviceCount: data.service_count || data.serviceCount || data.totalOrders || data.total_orders || 0,
|
||
repeatCount: data.repeat_count || data.repeatCount || 0,
|
||
rating: data.rating || 5.0,
|
||
quote: data.quote || data.bio || data.introduction || '',
|
||
certification: data.certification || '',
|
||
status: status,
|
||
statusText: data.statusText || statusInfo.text,
|
||
isBusy: data.isBusy !== undefined ? data.isBusy : statusInfo.isBusy,
|
||
// 等级信息
|
||
levelCode: data.levelCode || data.level_code || 'junior',
|
||
levelName: data.levelName || data.level_name || levelNameMap[data.levelCode || data.level_code] || '初级',
|
||
// 基于等级的价格
|
||
textPrice: data.textPrice || data.text_price || data.pricePerMinute || data.price_per_minute || 0.5,
|
||
voicePrice: data.voicePrice || data.voice_price || 1
|
||
}
|
||
|
||
console.log('转换后的陪聊师数据:', result)
|
||
return result
|
||
},
|
||
|
||
onBack() {
|
||
wx.navigateBack()
|
||
},
|
||
|
||
onMore() {
|
||
wx.showActionSheet({
|
||
itemList: ['举报', '拉黑', '分享'],
|
||
success: (res) => {
|
||
const actions = ['举报', '拉黑', '分享']
|
||
if (res.tapIndex === 1) {
|
||
// 拉黑
|
||
this.addToBlacklist()
|
||
} else {
|
||
wx.showToast({ title: actions[res.tapIndex], icon: 'none' })
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 添加到黑名单
|
||
*/
|
||
async addToBlacklist() {
|
||
try {
|
||
const res = await api.settings.addToBlacklist(this.data.counselor.id)
|
||
if (res.success) {
|
||
wx.showToast({ title: '已拉黑', icon: 'success' })
|
||
}
|
||
} catch (err) {
|
||
wx.showToast({ title: '操作失败', icon: 'none' })
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 免费倾诉/下单
|
||
*/
|
||
onFreeConsult() {
|
||
const { counselor } = this.data
|
||
|
||
if (counselor.isBusy) {
|
||
wx.showToast({ title: '陪聊师当前不在线', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 显示下单弹窗
|
||
this.setData({ showOrderModal: true })
|
||
},
|
||
|
||
/**
|
||
* 关闭下单弹窗
|
||
*/
|
||
closeOrderModal() {
|
||
this.setData({ showOrderModal: false })
|
||
},
|
||
|
||
/**
|
||
* 选择时长
|
||
*/
|
||
selectDuration(e) {
|
||
const duration = e.currentTarget.dataset.duration
|
||
this.setData({ selectedDuration: duration })
|
||
},
|
||
|
||
/**
|
||
* 选择服务类型
|
||
*/
|
||
selectServiceType(e) {
|
||
const type = e.currentTarget.dataset.type
|
||
this.setData({ selectedServiceType: type })
|
||
},
|
||
|
||
/**
|
||
* 计算订单价格
|
||
*/
|
||
calculatePrice() {
|
||
const { counselor, selectedDuration, selectedServiceType } = this.data
|
||
if (!counselor) return 0
|
||
|
||
const unitPrice = selectedServiceType === 'voice' ? counselor.voicePrice : counselor.textPrice
|
||
return (unitPrice * selectedDuration).toFixed(2)
|
||
},
|
||
|
||
/**
|
||
* 确认下单
|
||
*/
|
||
async confirmOrder() {
|
||
const { counselor, selectedDuration, selectedServiceType } = this.data
|
||
|
||
// 检查登录
|
||
if (app.checkNeedLogin && app.checkNeedLogin()) return
|
||
|
||
wx.showLoading({ title: '创建订单...' })
|
||
|
||
try {
|
||
const res = await api.order.createCompanionOrder({
|
||
companion_id: counselor.id,
|
||
duration: selectedDuration,
|
||
service_type: selectedServiceType,
|
||
message: ''
|
||
})
|
||
|
||
wx.hideLoading()
|
||
|
||
if (res.success && res.data) {
|
||
this.setData({ showOrderModal: false })
|
||
|
||
// 跳转到陪聊聊天页
|
||
wx.navigateTo({
|
||
url: `/pages/companion-chat/companion-chat?orderId=${res.data.id}&companionId=${counselor.id}&name=${encodeURIComponent(counselor.name)}`
|
||
})
|
||
} else {
|
||
wx.showToast({ title: res.message || '下单失败', icon: 'none' })
|
||
}
|
||
} catch (err) {
|
||
wx.hideLoading()
|
||
console.error('下单失败', err)
|
||
wx.showToast({ title: '下单失败', icon: 'none' })
|
||
}
|
||
},
|
||
|
||
onViewProfile() {
|
||
this.setData({ showProfileModal: true })
|
||
},
|
||
|
||
closeProfileModal() {
|
||
this.setData({ showProfileModal: false })
|
||
},
|
||
|
||
onViewReviews() {
|
||
this.setData({
|
||
showReviewModal: true,
|
||
reviews: [],
|
||
reviewPage: 1,
|
||
hasMoreReviews: true
|
||
})
|
||
this.loadReviews()
|
||
},
|
||
|
||
closeReviewModal() {
|
||
this.setData({ showReviewModal: false })
|
||
},
|
||
|
||
/**
|
||
* 加载评价列表
|
||
*/
|
||
async loadReviews() {
|
||
if (this.data.loadingReviews || !this.data.hasMoreReviews) return
|
||
|
||
this.setData({ loadingReviews: true })
|
||
|
||
try {
|
||
const res = await api.companion.getReviews(this.data.counselor.id, {
|
||
page: this.data.reviewPage,
|
||
limit: 10
|
||
})
|
||
|
||
if (res.success && res.data) {
|
||
const newReviews = res.data.reviews || res.data.list || []
|
||
const formattedReviews = newReviews.map(review => ({
|
||
...review,
|
||
userName: this.maskPhone(review.userName || review.user_name || '匿名用户'),
|
||
createdAt: this.formatDate(review.createdAt || review.created_at),
|
||
expanded: false
|
||
}))
|
||
|
||
// 获取统计数据(兼容多种返回格式)
|
||
const stats = res.data.stats || {}
|
||
const totalCount = stats.reviewCount || res.data.totalCount || res.data.total || formattedReviews.length
|
||
const goodRate = stats.goodRate || res.data.goodRate || 100
|
||
const avgRating = stats.avgRating || res.data.avgRating || this.data.counselor?.rating || 5
|
||
|
||
this.setData({
|
||
reviews: [...this.data.reviews, ...formattedReviews],
|
||
reviewStats: {
|
||
totalCount: totalCount,
|
||
goodRate: goodRate,
|
||
avgRating: avgRating
|
||
},
|
||
hasMoreReviews: res.data.hasMore !== undefined ? res.data.hasMore : formattedReviews.length >= 10,
|
||
reviewPage: this.data.reviewPage + 1,
|
||
loadingReviews: false
|
||
})
|
||
} else {
|
||
// 使用模拟数据
|
||
this.loadMockReviews()
|
||
}
|
||
} catch (err) {
|
||
console.error('加载评价失败', err)
|
||
this.loadMockReviews()
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 加载模拟评价数据
|
||
*/
|
||
loadMockReviews() {
|
||
const mockReviews = [
|
||
{
|
||
id: 1,
|
||
userName: '138****6172',
|
||
userAvatar: '',
|
||
rating: 5,
|
||
content: '老师很有耐心,倾听我的问题后给出了很中肯的建议。咨询后感觉心里轻松了很多,对未来也有了新的规划...',
|
||
tags: ['专业', '耐心', '有效果'],
|
||
reply: '谢谢您的信任,很高兴能够帮助到您。希望您能继续保持积极的心态,有任何问题随时可以来找我交流。',
|
||
likeCount: 23,
|
||
createdAt: '2024-12-15 14:32'
|
||
},
|
||
{
|
||
id: 2,
|
||
userName: '186****3298',
|
||
userAvatar: '',
|
||
rating: 5,
|
||
content: '第一次尝试心理咨询,老师非常专业,让我感觉很放松。通过几次咨询,我对自己的情绪有了更好的认识...',
|
||
tags: ['专业', '温暖', '有帮助'],
|
||
reply: '能够陪伴您成长是我的荣幸,继续加油!',
|
||
likeCount: 15,
|
||
createdAt: '2024-12-10 09:15'
|
||
},
|
||
{
|
||
id: 3,
|
||
userName: '159****7721',
|
||
userAvatar: '',
|
||
rating: 5,
|
||
content: '老师的声音很温柔,聊天的过程中感觉很舒服。虽然问题还在,但是心态好了很多,会继续找老师咨询的...',
|
||
tags: ['温柔', '善于倾听'],
|
||
reply: '',
|
||
likeCount: 8,
|
||
createdAt: '2024-12-05 20:48'
|
||
},
|
||
{
|
||
id: 4,
|
||
userName: '177****4532',
|
||
userAvatar: '',
|
||
rating: 5,
|
||
content: '咨询师很专业,能够快速理解我的问题并给出建议。性价比很高,会推荐给朋友...',
|
||
tags: ['专业', '高效'],
|
||
reply: '',
|
||
likeCount: 12,
|
||
createdAt: '2024-11-28 16:22'
|
||
},
|
||
{
|
||
id: 5,
|
||
userName: '133****8965',
|
||
userAvatar: '',
|
||
rating: 5,
|
||
content: '非常好的一次体验,老师很有同理心,让我感受到了被理解和支持...',
|
||
tags: ['有同理心', '支持'],
|
||
reply: '感谢您的认可,祝您生活愉快!',
|
||
likeCount: 6,
|
||
createdAt: '2024-11-20 11:05'
|
||
}
|
||
]
|
||
|
||
this.setData({
|
||
reviews: mockReviews,
|
||
reviewStats: {
|
||
totalCount: this.data.counselor?.serviceCount || 2952,
|
||
goodRate: 100
|
||
},
|
||
hasMoreReviews: false,
|
||
loadingReviews: false
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 加载更多评价
|
||
*/
|
||
loadMoreReviews() {
|
||
this.loadReviews()
|
||
},
|
||
|
||
/**
|
||
* 展开评价内容
|
||
*/
|
||
expandReview(e) {
|
||
const index = e.currentTarget.dataset.index
|
||
const reviews = this.data.reviews
|
||
reviews[index].expanded = true
|
||
this.setData({ reviews })
|
||
},
|
||
|
||
/**
|
||
* 点赞评价
|
||
*/
|
||
async likeReview(e) {
|
||
const reviewId = e.currentTarget.dataset.id
|
||
const reviews = this.data.reviews
|
||
const index = reviews.findIndex(r => r.id === reviewId)
|
||
|
||
if (index !== -1) {
|
||
// 检查是否已点赞
|
||
if (reviews[index].liked) {
|
||
wx.showToast({ title: '已点赞过了', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 乐观更新UI
|
||
reviews[index].likeCount = (reviews[index].likeCount || 0) + 1
|
||
reviews[index].liked = true
|
||
this.setData({ reviews })
|
||
|
||
// 调用API
|
||
try {
|
||
const res = await api.companion.likeReview(reviewId)
|
||
if (!res.success) {
|
||
// 失败时回滚
|
||
reviews[index].likeCount -= 1
|
||
reviews[index].liked = false
|
||
this.setData({ reviews })
|
||
wx.showToast({ title: '点赞失败', icon: 'none' })
|
||
}
|
||
} catch (err) {
|
||
// 失败时回滚
|
||
reviews[index].likeCount -= 1
|
||
reviews[index].liked = false
|
||
this.setData({ reviews })
|
||
console.log('点赞API调用失败', err)
|
||
wx.showToast({ title: '点赞失败', icon: 'none' })
|
||
}
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 手机号脱敏
|
||
*/
|
||
maskPhone(phone) {
|
||
if (!phone || phone.length < 7) return phone
|
||
if (phone.includes('****')) return phone
|
||
return phone.substring(0, 3) + '****' + phone.substring(phone.length - 4)
|
||
},
|
||
|
||
/**
|
||
* 格式化日期
|
||
*/
|
||
formatDate(dateStr) {
|
||
if (!dateStr) return ''
|
||
const date = new Date(dateStr)
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
const hours = String(date.getHours()).padStart(2, '0')
|
||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||
},
|
||
|
||
onRemind() {
|
||
wx.showToast({ title: '已设置提醒', icon: 'success' })
|
||
},
|
||
|
||
onVoiceMode() {
|
||
const isVoiceMode = !this.data.isVoiceMode
|
||
this.setData({
|
||
isVoiceMode,
|
||
inputFocus: !isVoiceMode
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 语音按钮触摸开始 - 开始录音
|
||
*/
|
||
onVoiceTouchStart(e) {
|
||
// 记录起始Y坐标
|
||
const startY = e.touches[0].clientY
|
||
this.setData({
|
||
recordingStartY: startY,
|
||
voiceCancelHint: false,
|
||
recordingDuration: 0
|
||
})
|
||
|
||
// 检查录音权限
|
||
wx.authorize({
|
||
scope: 'scope.record',
|
||
success: () => {
|
||
// 开始录音
|
||
this.startVoiceRecord()
|
||
},
|
||
fail: () => {
|
||
wx.showModal({
|
||
title: '需要录音权限',
|
||
content: '请在设置中开启录音权限',
|
||
confirmText: '去设置',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
wx.openSetting()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 语音按钮触摸移动 - 检测上划取消
|
||
*/
|
||
onVoiceTouchMove(e) {
|
||
if (!this.data.isRecording) return
|
||
|
||
const currentY = e.touches[0].clientY
|
||
const startY = this.data.recordingStartY
|
||
const moveDistance = startY - currentY
|
||
|
||
// 上划超过80px显示取消提示
|
||
const shouldCancel = moveDistance > 80
|
||
|
||
if (shouldCancel !== this.data.voiceCancelHint) {
|
||
this.setData({ voiceCancelHint: shouldCancel })
|
||
|
||
// 震动反馈
|
||
if (shouldCancel) {
|
||
wx.vibrateShort({ type: 'light' })
|
||
}
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 语音按钮触摸结束 - 停止录音并发送/取消
|
||
*/
|
||
onVoiceTouchEnd() {
|
||
if (!this.data.isRecording) return
|
||
|
||
const { voiceCancelHint } = this.data
|
||
|
||
// 标记是否取消
|
||
this.voiceCanceled = voiceCancelHint
|
||
|
||
// 停止录音
|
||
if (this.recorderManager) {
|
||
this.recorderManager.stop()
|
||
}
|
||
|
||
// 清除录音计时器
|
||
if (this.recordingTimer) {
|
||
clearInterval(this.recordingTimer)
|
||
this.recordingTimer = null
|
||
}
|
||
|
||
this.setData({
|
||
isRecording: false,
|
||
voiceCancelHint: false,
|
||
recordingDuration: 0
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 语音按钮触摸取消
|
||
*/
|
||
onVoiceTouchCancel() {
|
||
this.voiceCanceled = true
|
||
this.onVoiceTouchEnd()
|
||
},
|
||
|
||
/**
|
||
* 开始语音录音
|
||
*/
|
||
startVoiceRecord() {
|
||
this.setData({
|
||
isRecording: true,
|
||
voiceCancelHint: false,
|
||
recordingDuration: 0
|
||
})
|
||
|
||
// 初始化录音管理器
|
||
const recorderManager = wx.getRecorderManager()
|
||
this.recorderManager = recorderManager
|
||
|
||
// 监听录音结束
|
||
recorderManager.onStop((res) => {
|
||
// 清除计时器
|
||
if (this.recordingTimer) {
|
||
clearInterval(this.recordingTimer)
|
||
this.recordingTimer = null
|
||
}
|
||
|
||
this.setData({ isRecording: false })
|
||
|
||
// 如果是取消的,不发送
|
||
if (this.voiceCanceled) {
|
||
this.voiceCanceled = false
|
||
wx.showToast({ title: '已取消', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 录音时间太短
|
||
if (res.duration < 1000) {
|
||
wx.showToast({ title: '录音时间太短', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
// 发送语音消息
|
||
this.sendVoiceMessage(res.tempFilePath, Math.ceil(res.duration / 1000))
|
||
})
|
||
|
||
recorderManager.onError((err) => {
|
||
console.error('录音失败', err)
|
||
|
||
// 清除计时器
|
||
if (this.recordingTimer) {
|
||
clearInterval(this.recordingTimer)
|
||
this.recordingTimer = null
|
||
}
|
||
|
||
this.setData({
|
||
isRecording: false,
|
||
voiceCancelHint: false,
|
||
recordingDuration: 0
|
||
})
|
||
|
||
// 模拟器不支持录音,给出友好提示
|
||
if (err.errMsg && err.errMsg.includes('NotFoundError')) {
|
||
wx.showToast({ title: '请在真机上测试录音', icon: 'none' })
|
||
} else {
|
||
wx.showToast({ title: '录音失败', icon: 'none' })
|
||
}
|
||
})
|
||
|
||
// 开始录音
|
||
recorderManager.start({
|
||
duration: 60000,
|
||
format: 'mp3',
|
||
sampleRate: 16000,
|
||
numberOfChannels: 1
|
||
})
|
||
|
||
// 录音计时器
|
||
this.recordingTimer = setInterval(() => {
|
||
const duration = this.data.recordingDuration + 1
|
||
this.setData({ recordingDuration: duration })
|
||
|
||
// 最长60秒自动停止
|
||
if (duration >= 60) {
|
||
this.onVoiceTouchEnd()
|
||
}
|
||
}, 1000)
|
||
},
|
||
|
||
/**
|
||
* 发送语音消息
|
||
*/
|
||
async sendVoiceMessage(filePath, duration) {
|
||
const { counselor, messages } = this.data
|
||
|
||
// 添加语音消息到列表
|
||
const voiceMessage = {
|
||
id: Date.now(),
|
||
content: `[语音消息 ${duration}″]`,
|
||
isUser: true,
|
||
type: 'voice',
|
||
audioUrl: filePath,
|
||
duration: duration
|
||
}
|
||
|
||
this.setData({
|
||
messages: [...messages, voiceMessage]
|
||
})
|
||
|
||
wx.showToast({ title: '语音已发送', icon: 'success' })
|
||
|
||
// 如果陪聊师在线,提示开始对话
|
||
if (!counselor.isBusy) {
|
||
setTimeout(() => {
|
||
wx.showModal({
|
||
title: '提示',
|
||
content: '是否开始与陪聊师对话?',
|
||
confirmText: '开始对话',
|
||
cancelText: '继续留言',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
this.onFreeConsult()
|
||
}
|
||
}
|
||
})
|
||
}, 500)
|
||
}
|
||
|
||
// TODO: 上传语音文件到服务器
|
||
},
|
||
|
||
onInput(e) {
|
||
this.setData({ inputText: e.detail.value })
|
||
},
|
||
|
||
/**
|
||
* 发送消息
|
||
*/
|
||
onSendMessage() {
|
||
const { inputText, messages, counselor } = this.data
|
||
if (!inputText.trim()) return
|
||
|
||
// 添加用户消息
|
||
const userMsg = {
|
||
id: Date.now(),
|
||
content: inputText,
|
||
isUser: true
|
||
}
|
||
|
||
this.setData({
|
||
messages: [...messages, userMsg],
|
||
inputText: ''
|
||
})
|
||
|
||
// 如果陪聊师在线,可以跳转到聊天页面
|
||
if (!counselor.isBusy) {
|
||
wx.showModal({
|
||
title: '提示',
|
||
content: '是否开始与陪聊师对话?',
|
||
confirmText: '开始对话',
|
||
cancelText: '继续留言',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
this.onFreeConsult()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
},
|
||
|
||
onEmoji() {
|
||
// 切换表情面板
|
||
this.setData({
|
||
showEmoji: !this.data.showEmoji,
|
||
showMorePanel: false,
|
||
isVoiceMode: false
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 选择表情
|
||
*/
|
||
onEmojiSelect(e) {
|
||
const emoji = e.currentTarget.dataset.emoji
|
||
this.setData({
|
||
inputText: this.data.inputText + emoji
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 删除表情/文字
|
||
*/
|
||
onEmojiDelete() {
|
||
const text = this.data.inputText
|
||
if (text.length > 0) {
|
||
// 处理emoji字符(可能占用多个字符位置)
|
||
const arr = Array.from(text)
|
||
arr.pop()
|
||
this.setData({
|
||
inputText: arr.join('')
|
||
})
|
||
}
|
||
},
|
||
|
||
onAdd() {
|
||
// 切换更多功能面板显示状态
|
||
this.setData({
|
||
showMorePanel: !this.data.showMorePanel,
|
||
showEmoji: false,
|
||
isVoiceMode: false
|
||
})
|
||
},
|
||
|
||
// 关闭更多功能面板和表情面板
|
||
closeMorePanel() {
|
||
this.setData({
|
||
showMorePanel: false,
|
||
showEmoji: false
|
||
})
|
||
},
|
||
|
||
// 拍照
|
||
onTakePhoto() {
|
||
this.setData({ showMorePanel: false })
|
||
wx.chooseMedia({
|
||
count: 1,
|
||
mediaType: ['image'],
|
||
sourceType: ['camera'],
|
||
camera: 'back',
|
||
success: (res) => {
|
||
const tempFilePath = res.tempFiles[0].tempFilePath
|
||
wx.showToast({ title: '照片已选择', icon: 'success' })
|
||
// TODO: 发送图片消息
|
||
},
|
||
fail: (err) => {
|
||
if (err.errMsg !== 'chooseMedia:fail cancel') {
|
||
wx.showToast({ title: '拍照失败', icon: 'none' })
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
// 从相册选择图片
|
||
onChooseImage() {
|
||
this.setData({ showMorePanel: false })
|
||
wx.chooseMedia({
|
||
count: 9,
|
||
mediaType: ['image'],
|
||
sourceType: ['album'],
|
||
success: (res) => {
|
||
wx.showToast({ title: `已选择${res.tempFiles.length}张图片`, icon: 'success' })
|
||
// TODO: 发送图片消息
|
||
},
|
||
fail: (err) => {
|
||
if (err.errMsg !== 'chooseMedia:fail cancel') {
|
||
wx.showToast({ title: '选择图片失败', icon: 'none' })
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
// 发送礼物
|
||
onSendGift() {
|
||
this.setData({ showMorePanel: false })
|
||
wx.showToast({ title: '礼物功能开发中', icon: 'none' })
|
||
},
|
||
|
||
// 语音通话
|
||
onVoiceCall() {
|
||
this.setData({ showMorePanel: false })
|
||
wx.showToast({ title: '语音通话功能开发中', icon: 'none' })
|
||
},
|
||
|
||
// 常用语
|
||
onQuickReply() {
|
||
this.setData({ showMorePanel: false })
|
||
const quickReplies = [
|
||
'你好,很高兴认识你~',
|
||
'最近怎么样?',
|
||
'有什么想聊的吗?',
|
||
'今天心情如何?',
|
||
'晚安,好梦~'
|
||
]
|
||
wx.showActionSheet({
|
||
itemList: quickReplies,
|
||
success: (res) => {
|
||
this.setData({ inputText: quickReplies[res.tapIndex] })
|
||
}
|
||
})
|
||
},
|
||
|
||
// 约时间
|
||
onScheduleTime() {
|
||
this.setData({ showMorePanel: false })
|
||
wx.showToast({ title: '约时间功能开发中', icon: 'none' })
|
||
},
|
||
|
||
// 抢红包
|
||
onRedPacket() {
|
||
this.setData({ showMorePanel: false })
|
||
wx.showToast({ title: '红包功能开发中', icon: 'none' })
|
||
},
|
||
|
||
// 测结果
|
||
onTestResult() {
|
||
this.setData({ showMorePanel: false })
|
||
wx.showToast({ title: '测结果功能开发中', icon: 'none' })
|
||
}
|
||
})
|