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

2096 lines
58 KiB
JavaScript
Raw Permalink 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/chat-detail/chat-detail.js
// 聊天详情页面 - 与AI角色聊天
const app = getApp()
const api = require('../../utils/api')
const util = require('../../utils/util')
const proactiveMessage = require('../../utils/proactiveMessage')
const imageUrl = require('../../utils/imageUrl')
const config = require('../../config/index')
// 常用表情
const EMOJIS = [
"😊", "😀", "😁", "😃", "😂", "🤣", "😅", "😆", "😉", "😋", "😎", "😍", "😘", "🥰", "😗", "😙",
"🙂", "🤗", "🤩", "🤔", "😐", "😑", "😶", "🙄", "😏", "😣", "😥", "😮", "😯", "😪", "😫", "😴",
"😌", "😛", "😜", "😝", "😒", "😓", "😔", "😕", "🙃", "😲", "😖", "😞", "😟", "😤", "😢", "😭",
"😨", "😩", "😬", "😰", "😱", "😳", "😵", "😡", "😠", "😷", "🤒", "🤕", "😇", "🥳", "🥺",
"👋", "👌", "✌️", "🤞", "👍", "👎", "👏", "🙌", "🤝", "🙏", "💪", "❤️", "🧡", "💛", "💚", "💙",
"💜", "🖤", "💔", "💕", "💖", "💗", "💘", "💝", "🌹", "🌺", "🌻", "🌼", "🌷", "🎉", "🎊", "🎁"
]
Page({
data: {
statusBarHeight: 44,
navHeight: 96,
// 角色信息
characterId: '',
conversationId: '',
character: {
id: '',
name: '加载中...',
avatar: '',
isOnline: true,
job: '',
location: '',
gender: '',
age: '',
education: '',
serviceCount: 0,
returnCount: 0,
rating: '4.9',
motto: '',
qualification: '',
skills: [],
introduction: ''
},
// 用户头像 - 从用户信息获取,使用默认头像
myAvatar: '/images/default-avatar.svg',
// 消息列表
messages: [],
// 输入状态
inputText: '',
inputFocus: false,
isVoiceMode: false,
isRecording: false,
showEmoji: false,
recordingDuration: 0,
voiceCancelHint: false,
// AI状态
isTyping: false,
isSending: false,
playingVoiceId: null,
// 滚动控制
scrollIntoView: '',
scrollTop: 0, // 当前滚动位置
// 加载状态
loading: true,
loadingMore: false,
hasMore: true,
page: 1,
pageSize: 20, // 每页加载20条消息
isFirstLoad: true, // 是否首次加载
// 人物介绍弹窗
showProfilePopup: false,
// 查看评价弹窗
showReviewPopup: false,
reviews: [],
// 更多功能面板
showMorePanel: false,
// 常用语列表
quickReplies: [
'你好,很高兴认识你~',
'最近怎么样?',
'有什么想聊的吗?',
'今天心情如何?',
'晚安,好梦~',
'早安,新的一天开始了!'
],
showQuickReplyPopup: false,
// 表情列表
emojis: EMOJIS,
// 约时间弹窗
showSchedulePopup: false,
scheduleDate: '',
scheduleTime: '',
// 礼物相关
showGiftPopup: false,
giftList: [],
selectedGift: null,
userFlowers: 0,
// 聊天配额相关
remainingCount: 10, // 剩余免费次数(已废弃,保留兼容)
maxCount: 10, // 每日最大免费次数(已废弃,保留兼容)
isUnlocked: false, // 是否已解锁该角色
isVip: false, // 是否为VIP用户
showUnlockPopup: false, // 显示解锁弹窗
todayCharacterId: '', // 今天已聊天的角色ID
heartCount: 0, // 用户爱心余额
unlockHeartsCost: 500, // 默认解锁爱心成本
// 免费畅聊相关
freeTime: null,
countdownText: ''
},
onLoad(options) {
const { statusBarHeight, navHeight } = app.globalData
// 初始化消息处理相关变量
this.pendingMessages = []
this.messageTimer = null
this.isProcessing = false
// 获取参数
const characterId = options.id || ''
const conversationId = options.conversationId || ''
const characterName = decodeURIComponent(options.name || '')
// 设置用户头像
const userInfo = app.globalData.userInfo
const myAvatar = imageUrl.getAvatarUrl(userInfo?.avatar)
this.setData({
statusBarHeight,
navHeight,
characterId,
conversationId,
myAvatar,
'character.name': characterName || '加载中...'
})
// 进入聊天详情页时,调用标记已读接口清除未读数
if (conversationId) {
this.markConversationAsRead(conversationId)
}
// 加载角色信息和聊天历史
this.initChat()
},
onShow() {
// 每次显示页面时,刷新一次配额状态,确保免费畅聊时间等状态是最新的
if (!this.data.loading) {
this.loadQuotaStatus()
}
},
onUnload() {
// 页面卸载时清理
// 清除消息处理定时器
if (this.messageTimer) {
clearTimeout(this.messageTimer)
this.messageTimer = null
}
// 清除图片回复定时器
if (this.imageReplyTimers && this.imageReplyTimers.length > 0) {
this.imageReplyTimers.forEach(timer => clearTimeout(timer))
this.imageReplyTimers = []
}
// 清空待处理消息队列
this.pendingMessages = []
this.isProcessing = false
// 离开页面时标记该角色的主动推送消息为已读
if (this.data.characterId) {
proactiveMessage.markAsRead(this.data.characterId)
}
},
/**
* 标记会话已读
* 进入聊天详情页时调用,清除未读数
* @param {string} conversationId - 会话ID
*/
async markConversationAsRead(conversationId) {
if (!conversationId) {
console.log('[chat-detail] 没有conversationId跳过标记已读')
return
}
console.log('[chat-detail] 开始调用标记已读接口conversationId:', conversationId)
try {
const res = await api.chat.markAsRead(conversationId)
console.log('[chat-detail] 标记已读API响应:', JSON.stringify(res))
if (res.code === 0 || res.success) {
console.log('[chat-detail] 标记已读成功')
} else {
console.log('[chat-detail] 标记已读失败,响应:', res.message || res.error)
}
} catch (err) {
console.error('[chat-detail] 标记已读请求异常:', err)
}
},
/**
* 初始化聊天
*/
async initChat() {
this.setData({ loading: true })
try {
// 检查登录状态和Token
const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN)
const userId = wx.getStorageSync(config.STORAGE_KEYS.USER_ID)
console.log('[chat-detail] ========== 初始化聊天 ==========')
console.log('[chat-detail] Token存在:', !!token)
console.log('[chat-detail] Token长度:', token ? token.length : 0)
console.log('[chat-detail] UserId:', userId)
console.log('[chat-detail] CharacterId:', this.data.characterId)
// 先加载配额状态
await this.loadQuotaStatus()
// 并行加载角色信息和聊天历史
const [characterRes, historyRes] = await Promise.all([
this.loadCharacterInfo(),
this.loadChatHistory()
])
this.setData({ loading: false })
// 如果没有会话ID创建新会话
if (!this.data.conversationId && this.data.characterId) {
await this.createConversation()
}
} catch (err) {
console.error('初始化聊天失败', err)
this.setData({ loading: false })
util.showError('加载失败')
}
},
/**
* 加载聊天配额状态
*/
async loadQuotaStatus() {
const { characterId } = this.data
if (!characterId) {
console.log('[chat-detail] 没有角色ID使用默认配额')
return
}
try {
const res = await api.chat.getQuota(characterId)
console.log('[chat-detail] 配额API响应:', JSON.stringify(res))
if (res && res.success && res.data) {
const quota = res.data
const isUnlocked = quota.is_unlocked || false
const isVip = quota.isVip || (quota.free_chat_time && quota.free_chat_time.isVip) || false
const freeChatTime = quota.free_chat_time || null
const canChatByVip = !!isVip
const canChatByFreeTime = !!(freeChatTime && freeChatTime.isActive)
const canChat = isUnlocked || canChatByVip || canChatByFreeTime
console.log('[chat-detail] 解析权限状态:', { isUnlocked, isVip, canChat, canChatByFreeTime })
this.setData({
remainingCount: -1,
maxCount: 0,
isUnlocked: !!isUnlocked,
isVip: !!isVip,
todayCharacterId: quota.today_character_id || '',
freeTime: freeChatTime,
unlockHeartsCost: quota.unlock_config?.hearts_cost || 500
})
// 处理免费畅聊倒计时
if (freeChatTime && freeChatTime.isActive && freeChatTime.remainingSeconds > 0) {
this.startCountdown(freeChatTime.remainingSeconds)
} else {
this.stopCountdown()
}
// 如果不能聊天且未解锁,显示解锁弹窗
if (!canChat && !isUnlocked) {
console.log('[chat-detail] 无聊天权限,显示解锁弹窗')
this.setData({ showUnlockPopup: true })
} else {
console.log('[chat-detail] 拥有聊天权限,确保解锁弹窗关闭')
this.setData({ showUnlockPopup: false })
}
}
} catch (err) {
console.log('[chat-detail] 加载权限状态失败', err)
this.setData({
remainingCount: -1,
maxCount: 0,
isUnlocked: false
})
}
// 同时加载用户爱心值
await this.loadHeartBalance()
},
/**
* 开始倒计时
*/
startCountdown(seconds) {
this.stopCountdown()
let remaining = seconds
this.setData({
countdownText: this.formatSeconds(remaining)
})
this.countdownTimer = setInterval(() => {
remaining--
if (remaining <= 0) {
this.stopCountdown()
this.setData({
'freeTime.isActive': false,
'freeTime.remainingSeconds': 0
})
} else {
this.setData({
countdownText: this.formatSeconds(remaining)
})
}
}, 1000)
},
/**
* 停止倒计时
*/
stopCountdown() {
if (this.countdownTimer) {
clearInterval(this.countdownTimer)
this.countdownTimer = null
}
this.setData({ countdownText: '' })
},
/**
* 格式化秒数为 MM:SS
*/
formatSeconds(s) {
const m = Math.floor(s / 60)
const rs = s % 60
return `${m}:${rs < 10 ? '0' : ''}${rs}`
},
/**
* 加载用户爱心值
* 使用 /api/auth/me 接口,该接口从 im_users.grass_balance 读取余额
*/
async loadHeartBalance() {
try {
const res = await api.auth.getCurrentUser()
if (res.success && res.data) {
this.setData({
heartCount: res.data.grass_balance || 0
})
console.log('[chat-detail] 爱心值加载成功:', res.data.grass_balance)
}
} catch (err) {
console.log('加载爱心值失败', err)
}
},
/**
* 加载角色信息
*/
async loadCharacterInfo() {
if (!this.data.characterId) return
try {
const res = await api.character.getDetail(this.data.characterId)
if (res.success && res.data) {
// 处理头像URL - 后端已返回完整URL前端只需兜底处理
const avatarUrl = imageUrl.getCharacterAvatarUrl(res.data.avatar || res.data.logo)
// 解析擅长领域
let skills = res.data.hobbiesTags || res.data.traits || []
if (!Array.isArray(skills) || skills.length === 0) {
skills = ['情感困惑', '职业压力', '成长创伤']
}
// 使用API返回的真实统计数据
const serviceCount = res.data.serviceCount || 0
const returnCount = res.data.returnCount || 0
const avgRating = res.data.avgRating || 4.9
this.setData({
character: {
id: res.data.id,
name: res.data.name,
avatar: avatarUrl,
isOnline: true,
voiceId: res.data.voice_id,
job: res.data.occupation || res.data.companionType || '心理咨询师',
location: res.data.location || res.data.province || '北京',
gender: res.data.gender === 'male' ? '男' : '女',
age: res.data.age || '90后',
education: '本科',
serviceCount: serviceCount,
returnCount: returnCount,
rating: avgRating.toFixed(2),
motto: res.data.openingLine || '每一次倾诉,都是心灵的释放',
qualification: '国家二级心理咨询师 | 情感咨询专家认证',
skills: skills.slice(0, 3),
introduction: res.data.selfIntroduction || res.data.about || '专业心理咨询培训'
}
})
}
} catch (err) {
console.log('加载角色信息失败', err)
}
},
/**
* 加载聊天历史首次加载最近20条
*/
async loadChatHistory() {
const { characterId, pageSize } = this.data
if (!characterId) {
const welcomeMsg = {
id: 'welcome',
text: `你好!我是${this.data.character.name},很高兴认识你~`,
isMe: false,
time: util.formatTime(new Date(), 'HH:mm'),
type: 'text'
}
this.setData({
messages: [welcomeMsg],
isFirstLoad: false,
hasMore: false
})
return
}
try {
console.log('[chat-detail] 开始加载聊天历史, characterId:', characterId)
// 首次只加载最近20条消息
const res = await api.chat.getChatHistoryByCharacter(characterId, {
limit: pageSize,
page: 1
})
console.log('[chat-detail] API响应:', JSON.stringify(res).substring(0, 200))
if (res.success && res.data && res.data.length > 0) {
console.log('[chat-detail] 收到历史消息数量:', res.data.length)
const messages = res.data.map(msg => this.transformMessage(msg))
this.setData({
messages,
hasMore: res.data.length >= pageSize,
page: 1,
isFirstLoad: false
})
console.log('[chat-detail] 消息已设置, 当前数量:', this.data.messages.length)
console.log('[chat-detail] 首次加载完成,不自动滚动到底部')
} else {
console.log('[chat-detail] 没有历史记录,显示欢迎消息')
const welcomeMsg = {
id: 'welcome',
text: `你好!我是${this.data.character.name},很高兴认识你~`,
isMe: false,
time: util.formatTime(new Date(), 'HH:mm'),
type: 'text'
}
this.setData({
messages: [welcomeMsg],
isFirstLoad: false,
hasMore: false
})
}
} catch (err) {
console.log('加载聊天历史失败:', err)
const welcomeMsg = {
id: 'welcome',
text: `你好!我是${this.data.character.name},很高兴认识你~`,
isMe: false,
time: util.formatTime(new Date(), 'HH:mm'),
type: 'text'
}
this.setData({
messages: [welcomeMsg],
isFirstLoad: false,
hasMore: false
})
}
},
/**
* 加载更多历史消息(向上翻页)
*/
async loadMoreHistory() {
const { characterId, loadingMore, hasMore, page, pageSize, messages } = this.data
if (loadingMore || !hasMore || !characterId) {
return
}
console.log('[chat-detail] 开始加载更多历史消息, page:', page + 1)
this.setData({ loadingMore: true })
try {
const res = await api.chat.getChatHistoryByCharacter(characterId, {
limit: pageSize,
page: page + 1
})
if (res.success && res.data && res.data.length > 0) {
console.log('[chat-detail] 加载到更多消息:', res.data.length, '条')
const newMessages = res.data.map(msg => this.transformMessage(msg))
// 将新消息插入到列表开头(历史消息在前)
this.setData({
messages: [...newMessages, ...messages],
hasMore: res.data.length >= pageSize,
page: page + 1,
loadingMore: false
})
console.log('[chat-detail] 历史消息加载完成,总消息数:', this.data.messages.length)
} else {
console.log('[chat-detail] 没有更多历史消息了')
this.setData({
hasMore: false,
loadingMore: false
})
}
} catch (err) {
console.error('[chat-detail] 加载更多历史消息失败:', err)
this.setData({ loadingMore: false })
}
},
/**
* 滚动事件监听(检测是否滚动到顶部)
*/
onScroll(e) {
const { scrollTop } = e.detail
// 滚动到顶部时加载更多历史消息
if (scrollTop < 50 && !this.data.loadingMore && this.data.hasMore) {
console.log('[chat-detail] 滚动到顶部,触发加载更多')
this.loadMoreHistory()
}
},
/**
* 转换消息格式
*/
transformMessage(msg) {
const baseMessage = {
id: msg.id,
text: msg.content,
isMe: msg.role === 'user',
time: util.formatTime(msg.created_at || msg.timestamp, 'HH:mm'),
type: msg.message_type || 'text'
}
// 根据消息类型添加额外字段
if (msg.message_type === 'image' && msg.image_url) {
baseMessage.imageUrl = msg.image_url
} else if (msg.message_type === 'voice' && msg.voice_url) {
baseMessage.audioUrl = msg.voice_url
baseMessage.duration = msg.voice_duration
} else if (msg.message_type === 'gift' && msg.gift_info) {
baseMessage.giftInfo = typeof msg.gift_info === 'string' ? JSON.parse(msg.gift_info) : msg.gift_info
}
return baseMessage
},
/**
* 创建新会话
* 注意:后端通过 Token 验证用户身份,不再需要传递 userId
*/
async createConversation() {
try {
const res = await api.chat.createConversation(this.data.characterId)
if (res.code === 0 && res.data) {
this.setData({
conversationId: res.data.id
})
} else if (res.success && res.data) {
this.setData({
conversationId: res.data.id
})
}
} catch (err) {
console.log('创建会话失败', err)
}
},
/**
* 返回上一页
*/
onBack() {
wx.navigateBack()
},
/**
* 跳转到角色详情页
*/
onGoToCharacterDetail() {
const { characterId } = this.data
if (characterId) {
wx.navigateTo({
url: `/pages/character-detail/character-detail?id=${characterId}`
})
}
},
/**
* 用户头像加载失败时的处理
*/
onAvatarError() {
this.setData({
myAvatar: '/images/default-avatar.svg'
})
},
/**
* 更多操作
*/
onMore() {
wx.showActionSheet({
itemList: ['查看资料', '清空聊天记录', '举报'],
success: (res) => {
if (res.tapIndex === 0) {
// 查看资料
wx.navigateTo({
url: `/pages/character-detail/character-detail?id=${this.data.characterId}`
})
} else if (res.tapIndex === 1) {
this.clearMessages()
} else if (res.tapIndex === 2) {
util.showSuccess('举报已提交')
}
}
})
},
/**
* 清空消息
* 只清空聊天记录,不删除会话
* 会话仍然显示在消息列表中
*/
async clearMessages() {
const confirmed = await util.showConfirm({
title: '清空记录',
content: '确定要清空聊天记录吗?此操作不可恢复。'
})
if (!confirmed) return
const { characterId } = this.data
// 如果没有角色ID只清空本地消息
if (!characterId) {
this.setData({ messages: [] })
util.showSuccess('已清空')
return
}
wx.showLoading({ title: '清空中...' })
try {
// 调用后端API清空聊天记录使用角色ID
const res = await api.chat.clearChatHistory(characterId)
if (res.success || res.code === 0) {
// 清空本地消息列表
this.setData({ messages: [] })
util.showSuccess('已清空')
} else {
throw new Error(res.message || '清空失败')
}
} catch (err) {
console.error('清空聊天记录失败:', err)
// 即使API失败也清空本地消息
this.setData({ messages: [] })
util.showSuccess('已清空')
} finally {
wx.hideLoading()
}
},
/**
* 输入文字
*/
onInput(e) {
this.setData({ inputText: e.detail.value })
},
/**
* 发送消息
*/
async onSend() {
const { inputText, characterId, conversationId, character, isUnlocked, isVip, freeTime, remainingCount } = this.data
// 只检查输入是否为空
if (!inputText.trim()) return
// 检查登录
if (app.checkNeedLogin()) return
// 检查聊天权限
// 1. 如果已解锁或VIP直接放行
// 2. 如果未解锁且非VIP检查免费畅聊时间
const canChatByFreeTime = !!(freeTime && freeTime.isActive)
const canChatByVip = !!isVip
if (!isUnlocked && !canChatByVip && !canChatByFreeTime) {
console.log('[chat-detail] 无聊天权限,显示解锁弹窗', { isUnlocked, isVip, canChatByFreeTime })
this.setData({ showUnlockPopup: true })
return
}
const messageText = inputText.trim()
const newId = util.generateId()
// 添加用户消息到列表
const userMessage = {
id: newId,
text: messageText,
isMe: true,
time: util.formatTime(new Date(), 'HH:mm'),
type: 'text' // 标记为文字消息
}
// 立即清空输入框,允许用户继续输入
this.setData({
messages: [...this.data.messages, userMessage],
inputText: ''
}, () => {
// 发送消息后立即滚动到底部
this.scrollToBottom()
})
console.log('[chat-detail] 发送消息')
// 将消息加入待处理队列
this.pendingMessages.push(messageText)
// 如果没有正在等待的定时器,启动延迟处理
if (!this.messageTimer) {
this.startMessageTimer(characterId, conversationId, character, isUnlocked, remainingCount)
}
},
/**
* 待处理消息队列
*/
pendingMessages: [],
messageTimer: null,
isProcessing: false,
/**
* 启动消息处理定时器
* 等待随机 2-8 秒,期间收集用户发送的所有消息
*/
startMessageTimer(characterId, conversationId, character, isUnlocked, remainingCount) {
// 随机延迟 2-4 秒
const randomDelay = Math.floor(Math.random() * 2000) + 2000
console.log('[chat-detail] 启动消息收集定时器,延迟:', randomDelay, 'ms')
this.messageTimer = setTimeout(() => {
this.messageTimer = null
this.processPendingMessages(characterId, conversationId, character, isUnlocked, remainingCount)
}, randomDelay)
},
/**
* 处理待处理的消息队列
* 将多条消息合并后发送给 AI
*/
async processPendingMessages(characterId, conversationId, character, isUnlocked, remainingCount) {
if (this.pendingMessages.length === 0) return
// 如果正在处理中,延迟重试而不是直接丢弃消息
if (this.isProcessing) {
console.log('[chat-detail] 消息处理中延迟500ms后重试')
setTimeout(() => {
this.processPendingMessages(characterId, conversationId, character, isUnlocked, remainingCount)
}, 500)
return
}
this.isProcessing = true
// 取出所有待处理消息并清空队列
const messagesToProcess = [...this.pendingMessages]
this.pendingMessages = []
// 合并多条消息为一条(用换行分隔)
const combinedMessage = messagesToProcess.join('\n')
console.log('[chat-detail] 合并处理消息:', messagesToProcess.length, '条')
console.log('[chat-detail] 合并后内容:', combinedMessage)
// 显示AI正在输入
this.setData({ isTyping: true })
try {
// 构建对话历史最近10条消息只包含文字消息
// 过滤掉图片消息,因为后端不需要处理图片内容
const conversationHistory = this.data.messages
.slice(-10)
.filter(msg => msg.type !== 'image' && msg.text) // 只保留有文字内容的消息
.map(msg => ({
role: msg.isMe ? 'user' : 'assistant',
content: msg.text
}))
// 发送合并后的消息到后端
const res = await api.chat.sendMessage({
character_id: characterId,
conversation_id: this.data.conversationId || conversationId,
message: combinedMessage,
conversationHistory: conversationHistory
})
this.setData({ isTyping: false })
// 检查是否需要解锁
if (!res.success && (res.error === 'FREE_CHAT_TIME_EXPIRED' || res.error === 'FREE_CHAT_TIME_NOT_CLAIMED')) {
this.setData({
showUnlockPopup: true,
'freeTime.isActive': false
})
if (res.error === 'FREE_CHAT_TIME_NOT_CLAIMED') {
wx.showModal({
title: '领取免费畅聊',
content: '领取100爱心值即可获得60分钟免费畅聊时间是否现在去领取',
confirmText: '去领取',
success: (modalRes) => {
if (modalRes.confirm) {
wx.switchTab({ url: '/pages/profile/profile' })
}
}
})
}
this.isProcessing = false
return
}
// 检查是否切换了角色
if (!res.success && res.error === 'DIFFERENT_CHARACTER') {
this.setData({ showUnlockPopup: true })
wx.showToast({
title: res.message || '今天已与其他角色聊天',
icon: 'none'
})
this.isProcessing = false
return
}
if (res.success && res.data) {
// 更新会话ID如果是新会话
if (res.data.conversation_id && !this.data.conversationId) {
this.setData({ conversationId: res.data.conversation_id })
}
// 更新解锁状态从后端返回的quota字段
if (res.data.quota) {
const newIsUnlocked = res.data.quota.is_unlocked
console.log('[chat-detail] 后端返回解锁状态:', { newIsUnlocked })
this.setData({
isUnlocked: newIsUnlocked || this.data.isUnlocked
})
}
// 添加AI回复
const aiMessage = {
id: res.data.id || util.generateId(),
text: res.data.content || res.data.message,
isMe: false,
time: util.formatTime(new Date(), 'HH:mm'),
audioUrl: res.data.audio_url,
type: 'text' // 标记为文字消息
}
this.setData({
messages: [...this.data.messages, aiMessage]
}, () => {
// AI回复后滚动到底部
this.scrollToBottom()
})
} else {
throw new Error(res.error || res.message || '发送失败')
}
} catch (err) {
console.error('发送消息失败', err)
// 开发模式下使用模拟AI回复
const config = require('../../config/index')
if (config.DEBUG) {
console.log('[DEV] 使用模拟AI回复')
const mockResponse = this.getMockAIResponse(combinedMessage, character.name)
const aiMessage = {
id: util.generateId(),
text: mockResponse,
isMe: false,
time: util.formatTime(new Date(), 'HH:mm'),
type: 'text' // 标记为文字消息
}
this.setData({
messages: [...this.data.messages, aiMessage],
isTyping: false
}, () => {
this.scrollToBottom()
})
this.isProcessing = false
return
}
this.setData({ isTyping: false })
util.showError(err.message || '发送失败,请重试')
}
this.isProcessing = false
},
/**
* 获取模拟AI回复开发模式
*/
getMockAIResponse(userMessage, characterName) {
const responses = [
`嗯,我明白你的意思~`,
`这个问题很有趣呢,让我想想...`,
`谢谢你愿意和我分享这些~`,
`我觉得你说得很有道理!`,
`哈哈,你真的很有趣~`,
`我一直都在这里陪着你哦~`,
`能和你聊天真的很开心!`,
`你今天心情怎么样呀?`,
`我很喜欢和你聊天的感觉~`,
`嗯嗯,我在认真听你说呢~`
]
// 根据用户消息内容选择合适的回复
if (userMessage.includes('你好') || userMessage.includes('嗨') || userMessage.includes('hi')) {
return `你好呀!我是${characterName},很高兴认识你~今天想聊点什么呢?`
}
if (userMessage.includes('名字') || userMessage.includes('叫什么')) {
return `我叫${characterName}呀,你可以这样叫我~`
}
if (userMessage.includes('喜欢')) {
return `我也很喜欢和你聊天呢!你喜欢什么呀?`
}
if (userMessage.includes('开心') || userMessage.includes('高兴')) {
return `看到你开心我也很开心呢!希望你每天都这么快乐~`
}
if (userMessage.includes('难过') || userMessage.includes('伤心') || userMessage.includes('不开心')) {
return `抱抱你~有什么不开心的事情可以和我说说,我会一直陪着你的。`
}
// 随机选择一个回复
const randomIndex = Math.floor(Math.random() * responses.length)
return responses[randomIndex]
},
/**
* 滚动到底部(仅在发送/接收新消息时调用)
* 使用 scroll-into-view 属性,自动滚动到最后一条消息
*/
scrollToBottom() {
const messages = this.data.messages
if (messages && messages.length > 0) {
// 使用 setTimeout 确保 DOM 已更新
setTimeout(() => {
this.setData({
scrollIntoView: `msg-${messages.length - 1}`
})
}, 100)
}
},
/**
* 滚动到顶部(加载更多历史消息后保持位置)
*/
scrollToTop() {
this.setData({
scrollTop: 0
})
},
/**
* 切换语音模式
*/
onVoiceMode() {
this.setData({
isVoiceMode: !this.data.isVoiceMode,
showEmoji: false
})
},
/**
* 开始录音
*/
onVoiceStart() {
this.setData({ isRecording: true })
wx.showToast({
title: '正在录音...',
icon: 'none',
duration: 60000
})
// 开始录音
const recorderManager = wx.getRecorderManager()
recorderManager.start({
duration: 60000,
format: 'mp3'
})
this.recorderManager = recorderManager
},
/**
* 结束录音
*/
onVoiceEnd() {
this.setData({ isRecording: false })
wx.hideToast()
if (this.recorderManager) {
this.recorderManager.stop()
this.recorderManager.onStop((res) => {
// 发送语音消息(暂时转为文字)
const newId = util.generateId()
const voiceMessage = {
id: newId,
text: '[语音消息]',
isMe: true,
time: util.formatTime(new Date(), 'HH:mm'),
type: 'voice',
audioUrl: res.tempFilePath
}
this.setData({
messages: [...this.data.messages, voiceMessage]
})
this.scrollToBottom()
})
}
},
/**
* 切换表情面板
*/
onEmojiToggle() {
// 收起键盘
this.setData({
inputFocus: false
})
// 延迟一点再显示面板,确保键盘先收起
setTimeout(() => {
this.setData({
showEmoji: !this.data.showEmoji,
showMorePanel: false,
isVoiceMode: false
})
}, 50)
},
/**
* 点击+号,显示更多功能面板
*/
onAddMore() {
// 收起键盘
this.setData({
inputFocus: false
})
// 延迟一点再显示面板,确保键盘先收起
setTimeout(() => {
this.setData({
showMorePanel: !this.data.showMorePanel,
showEmoji: false,
isVoiceMode: false
})
}, 50)
},
/**
* 关闭所有面板
*/
onClosePanels() {
this.setData({
showEmoji: false,
showMorePanel: false
})
},
/**
* 点击聊天区域关闭面板
*/
onTapChatArea() {
if (this.data.showEmoji || this.data.showMorePanel) {
this.setData({
showEmoji: false,
showMorePanel: false
})
}
},
/**
* 选择表情
*/
onEmojiSelect(e) {
const emoji = e.currentTarget.dataset.emoji
this.setData({
inputText: this.data.inputText + emoji
})
},
/**
* 播放语音消息
*/
onPlayVoice(e) {
const { id, url } = e.currentTarget.dataset
if (!url) {
util.showError('语音不可用')
return
}
const innerAudioContext = wx.createInnerAudioContext()
innerAudioContext.src = url
innerAudioContext.play()
innerAudioContext.onEnded(() => {
innerAudioContext.destroy()
})
},
/**
* 播放AI语音
*/
async onPlayAIVoice(e) {
const { id, text } = e.currentTarget.dataset
const { character } = this.data
if (!character.voiceId) {
util.showError('该角色暂不支持语音')
return
}
util.showLoading('生成语音中...')
try {
const res = await api.tts.synthesize({
text: text,
voice_id: character.voiceId,
character_id: character.id
})
util.hideLoading()
if (res.success && res.data && res.data.audio_url) {
const innerAudioContext = wx.createInnerAudioContext()
innerAudioContext.src = res.data.audio_url
innerAudioContext.play()
} else {
util.showError('语音生成失败')
}
} catch (err) {
util.hideLoading()
util.showError('语音生成失败')
}
},
/**
* 开始聊天(免费倾诉)
*/
onStartChat() {
// 聚焦输入框
this.setData({ showEmoji: false })
},
/**
* 显示人物介绍弹窗
*/
onShowProfile() {
this.setData({ showProfilePopup: true })
},
/**
* 关闭人物介绍弹窗
*/
onCloseProfile() {
this.setData({ showProfilePopup: false })
},
/**
* 查看评价
*/
onShowReviews() {
// 生成模拟评价数据
const mockReviews = [
{
id: 1,
phone: '138****6172',
date: '2024-12-15 14:32',
content: '老师很有耐心,倾听我的问题后给出了很中肯的建议。咨询后感觉心里轻松了很多,对未来也有了新的规划...',
tags: ['专业', '耐心', '有效果'],
reply: '谢谢您的信任,很高兴能够帮助到您。希望您能继续保持积极的心态,有任何问题随时可以来找我交流。',
likes: 23
},
{
id: 2,
phone: '186****3298',
date: '2024-12-10 09:15',
content: '第一次尝试心理咨询,老师非常专业,让我感觉很放松。通过几次咨询,我对自己的情绪有了更好的认识...',
tags: ['专业', '温暖', '有帮助'],
reply: '能够陪伴您成长是我的荣幸,继续加油!',
likes: 15
},
{
id: 3,
phone: '159****7721',
date: '2024-12-05 20:48',
content: '老师的声音很温柔,聊天的过程中感觉很舒服。虽然问题还在,但是心态好了很多,会继续找老师咨询的...',
tags: ['温柔', '善于倾听'],
reply: '',
likes: 8
},
{
id: 4,
phone: '177****4532',
date: '2024-11-28 16:22',
content: '咨询师很专业,能够快速理解我的问题并给出建议。性价比很高,会推荐给朋友...',
tags: ['专业', '高效'],
reply: '',
likes: 12
},
{
id: 5,
phone: '133****8965',
date: '2024-11-20 11:05',
content: '非常好的一次体验,老师很有同理心,让我感受到了被理解和支持...',
tags: ['有同理心', '支持'],
reply: '感谢您的认可,祝您生活愉快!',
likes: 6
}
]
this.setData({
showReviewPopup: true,
reviews: mockReviews
})
},
/**
* 关闭评价弹窗
*/
onCloseReviews() {
this.setData({ showReviewPopup: false })
},
/**
* 阻止弹窗滚动穿透
*/
preventMove() {
return false
},
/**
* 关闭解锁弹窗
*/
closeUnlockPopup() {
this.setData({ showUnlockPopup: false })
},
/**
* 跳转到个人中心(去领取奖励)
*/
onGoToProfile() {
wx.switchTab({
url: '/pages/profile/profile'
})
},
/**
* 爱心兑换解锁
*/
async onExchangeHearts() {
const { character, heartCount, unlockHeartsCost } = this.data
const config = require('../../config/index')
// 检查登录
const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN)
if (!token) {
wx.showToast({ title: '请先登录', icon: 'none' })
setTimeout(() => {
wx.navigateTo({ url: '/pages/login/login' })
}, 1500)
return
}
// 检查爱心值,不足时提示并跳转充值页面
if (heartCount < unlockHeartsCost) {
wx.showToast({ title: '爱心值不足,去充值', icon: 'none' })
setTimeout(() => {
this.setData({ showUnlockPopup: false })
wx.navigateTo({ url: '/pages/recharge/recharge' })
}, 1500)
return
}
wx.showLoading({ title: '兑换中...' })
try {
const res = await api.character.unlock({
character_id: character.id,
unlock_type: 'hearts'
})
wx.hideLoading()
if (res.success || res.code === 0) {
wx.showToast({ title: '解锁成功', icon: 'success' })
// 更新状态,使用后端返回的剩余爱心数
const remainingHearts = res.data?.remaining_hearts ?? (heartCount - unlockHeartsCost)
this.setData({
heartCount: remainingHearts,
isUnlocked: true,
remainingCount: -1, // -1 表示无限
showUnlockPopup: false
})
} else {
wx.showToast({ title: res.message || '兑换失败', icon: 'none' })
}
} catch (err) {
wx.hideLoading()
console.error('爱心兑换失败', err)
wx.showToast({ title: '网络错误,请重试', icon: 'none' })
}
},
/**
* 直接购买解锁9.9元)
* 使用 /api/payment/unified-order 接口
* 测试模式下返回 testMode: true订单直接完成无需调用微信支付
*/
async onPurchaseDirect() {
const { character } = this.data
const config = require('../../config/index')
// 检查登录
const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN)
if (!token) {
wx.showToast({ title: '请先登录', icon: 'none' })
setTimeout(() => {
wx.navigateTo({ url: '/pages/login/login' })
}, 1500)
return
}
wx.showLoading({ title: '创建订单中...' })
try {
// 调用统一支付订单接口
const res = await api.payment.createUnifiedOrder({
type: 'character_unlock',
character_id: character.id,
amount: 9.9
})
wx.hideLoading()
console.log('[chat-detail onPurchaseDirect] API返回:', JSON.stringify(res))
if (res.success) {
// 检查是否为测试模式(兼容多种判断方式)
const isTestMode = res.testMode || res.test_mode || res.data?.testMode || res.data?.test_mode ||
// 如果 payParams.package 包含 mock_prepay也认为是测试模式
(res.payParams?.package && res.payParams.package.includes('mock_prepay'))
if (isTestMode) {
// 测试模式:订单已直接完成,无需调用微信支付
wx.showToast({ title: '购买成功', icon: 'success' })
// 重新加载配额状态
await this.loadQuotaStatus()
// 更新状态
this.setData({
isUnlocked: true,
remainingCount: -1,
showUnlockPopup: false
})
} else {
// 正式模式:调用微信支付
const payParams = res.payParams || res.pay_params || res.data?.payParams || res.data?.pay_params
if (!payParams || !payParams.timeStamp) {
wx.showToast({ title: '支付参数错误', icon: 'none' })
return
}
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.package,
signType: payParams.signType || 'RSA',
paySign: payParams.paySign,
success: async () => {
wx.showToast({ title: '解锁成功', icon: 'success' })
// 重新加载配额状态
await this.loadQuotaStatus()
// 更新状态
this.setData({
isUnlocked: true,
remainingCount: -1,
showUnlockPopup: false
})
},
fail: (err) => {
if (err.errMsg !== 'requestPayment:fail cancel') {
wx.showToast({ title: '支付失败', icon: 'none' })
}
}
})
}
} else {
wx.showToast({ title: res.message || '创建订单失败', icon: 'none' })
}
} catch (err) {
wx.hideLoading()
console.error('购买解锁失败', err)
wx.showToast({ title: '网络错误,请重试', icon: 'none' })
}
},
/**
* 拍照
*/
onTakePhoto() {
this.setData({ showMorePanel: false })
wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['camera'],
camera: 'back',
success: (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath
this.sendImageMessage(tempFilePath)
},
fail: (err) => {
if (err.errMsg !== 'chooseMedia:fail cancel') {
util.showError('拍照失败')
}
}
})
},
/**
* 从相册选择图片
*/
onChooseImage() {
this.setData({ showMorePanel: false })
wx.chooseMedia({
count: 9,
mediaType: ['image'],
sourceType: ['album'],
success: (res) => {
res.tempFiles.forEach(file => {
this.sendImageMessage(file.tempFilePath)
})
},
fail: (err) => {
if (err.errMsg !== 'chooseMedia:fail cancel') {
util.showError('选择图片失败')
}
}
})
},
/**
* 发送图片消息
* 发送图片后先上传到服务器然后保存到数据库最后AI返回预设的图片回复话术
*/
async sendImageMessage(tempFilePath) {
const newId = util.generateId()
// 先添加本地消息(显示上传中状态)
const imageMessage = {
id: newId,
type: 'image',
imageUrl: tempFilePath,
isMe: true,
time: util.formatTime(new Date(), 'HH:mm'),
uploading: true // 标记为上传中
}
this.setData({
messages: [...this.data.messages, imageMessage]
}, () => {
this.scrollToBottom()
})
try {
// 1. 上传图片到服务器
console.log('[chat-detail] 开始上传图片:', tempFilePath)
// 检查登录状态
const token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN)
console.log('[chat-detail] Token存在:', !!token)
console.log('[chat-detail] Token长度:', token ? token.length : 0)
const uploadRes = await api.uploadFile(tempFilePath, 'uploads')
if (!uploadRes || !uploadRes.success || !uploadRes.data || !uploadRes.data.url) {
throw new Error('图片上传失败')
}
const imageUrl = uploadRes.data.url
console.log('[chat-detail] 图片上传成功:', imageUrl)
// 2. 更新本地消息,移除上传中状态
const messages = this.data.messages.map(msg => {
if (msg.id === newId) {
return { ...msg, imageUrl: imageUrl, uploading: false }
}
return msg
})
this.setData({ messages })
// 3. 保存图片消息到数据库
try {
await api.chat.sendImage({
character_id: this.data.characterId,
conversation_id: this.data.conversationId,
image_url: imageUrl
})
console.log('[chat-detail] 图片消息已保存到数据库')
} catch (err) {
console.error('[chat-detail] 保存图片消息失败:', err)
// 保存失败不影响显示,继续执行
}
// 4. 使用独立的图片回复定时器,避免与文字消息冲突
// 随机延迟 2-4 秒后显示AI回复
const randomDelay = Math.floor(Math.random() * 2000) + 2000
// 存储定时器ID用于页面卸载时清理
const imageReplyTimer = setTimeout(async () => {
// 检查页面是否还存在
if (!this.data) return
// 标记图片回复正在处理中
this.isImageReplyProcessing = true
// 只有在没有正在输入状态时才显示(避免覆盖文字消息的输入状态)
const shouldShowTyping = !this.data.isTyping
if (shouldShowTyping) {
this.setData({ isTyping: true })
}
try {
// 调用图片回复话术API
const res = await api.imageReply.getRandom()
// 检查页面是否还存在
if (!this.data) {
this.isImageReplyProcessing = false
return
}
// 只有当前是图片回复触发的isTyping时才关闭
if (shouldShowTyping && !this.isProcessing) {
this.setData({ isTyping: false })
}
if (res && res.success && res.data && res.data.content) {
// 添加AI回复
const aiMessage = {
id: util.generateId(),
text: res.data.content,
isMe: false,
time: util.formatTime(new Date(), 'HH:mm'),
type: 'text' // 标记为文字消息
}
this.setData({
messages: [...this.data.messages, aiMessage]
}, () => {
this.scrollToBottom()
})
} else {
// API返回失败使用默认回复
this.showDefaultImageReply()
}
} catch (err) {
console.error('[chat-detail] 获取图片回复话术失败:', err)
if (this.data) {
// 只有当前是图片回复触发的isTyping时才关闭
if (shouldShowTyping && !this.isProcessing) {
this.setData({ isTyping: false })
}
// 使用默认回复
this.showDefaultImageReply()
}
} finally {
// 清除图片回复处理标记
this.isImageReplyProcessing = false
}
}, randomDelay)
// 保存定时器引用,用于清理
if (!this.imageReplyTimers) {
this.imageReplyTimers = []
}
this.imageReplyTimers.push(imageReplyTimer)
} catch (err) {
console.error('[chat-detail] 图片上传失败:', err)
// 更新消息状态为失败
const messages = this.data.messages.map(msg => {
if (msg.id === newId) {
return { ...msg, uploading: false, uploadFailed: true }
}
return msg
})
this.setData({ messages })
util.showError('图片发送失败')
}
},
/**
* 显示默认图片回复API失败时的兜底
*/
showDefaultImageReply() {
const defaultReplies = [
'哇,这张图片真好看!',
'谢谢你分享这张图片给我~',
'这张图片很有意思呢!',
'收到你的图片啦,真棒!'
]
const randomIndex = Math.floor(Math.random() * defaultReplies.length)
const aiMessage = {
id: util.generateId(),
text: defaultReplies[randomIndex],
isMe: false,
time: util.formatTime(new Date(), 'HH:mm'),
type: 'text' // 标记为文字消息
}
this.setData({
messages: [...this.data.messages, aiMessage]
}, () => {
this.scrollToBottom()
})
},
/**
* 预览图片
*/
onPreviewImage(e) {
const url = e.currentTarget.dataset.url
const urls = this.data.messages
.filter(msg => msg.type === 'image')
.map(msg => msg.imageUrl)
wx.previewImage({
current: url,
urls: urls
})
},
/**
* 发送礼物
*/
onSendGift() {
this.setData({ showMorePanel: false })
// 加载礼物列表
this.loadGiftList()
this.setData({ showGiftPopup: true })
},
/**
* 加载礼物列表
*/
async loadGiftList() {
// 模拟礼物数据
const giftList = [
{ id: 1, name: '玫瑰花', price: 10, image: '/images/gift-rose.png' },
{ id: 2, name: '爱心', price: 20, image: '/images/gift-heart.png' },
{ id: 3, name: '蛋糕', price: 50, image: '/images/gift-cake.png' },
{ id: 4, name: '钻戒', price: 100, image: '/images/gift-ring.png' },
{ id: 5, name: '跑车', price: 500, image: '/images/gift-car.png' },
{ id: 6, name: '城堡', price: 1000, image: '/images/gift-castle.png' },
{ id: 7, name: '火箭', price: 2000, image: '/images/gift-rocket.png' },
{ id: 8, name: '皇冠', price: 5000, image: '/images/gift-crown.png' }
]
this.setData({ giftList })
},
/**
* 选择礼物
*/
onSelectGift(e) {
const gift = e.currentTarget.dataset.gift
this.setData({ selectedGift: gift })
},
/**
* 关闭礼物弹窗
*/
onCloseGiftPopup() {
this.setData({
showGiftPopup: false,
selectedGift: null
})
},
/**
* 确认发送礼物
*/
async onConfirmSendGift() {
const { selectedGift, userFlowers, character } = this.data
if (!selectedGift) {
util.showError('请选择礼物')
return
}
if (userFlowers < selectedGift.price) {
util.showError('花朵余额不足')
// 跳转充值页面
setTimeout(() => {
wx.navigateTo({ url: '/pages/recharge/recharge' })
}, 1500)
return
}
// 发送礼物消息
const newId = util.generateId()
const giftMessage = {
id: newId,
type: 'gift',
text: `送出了 ${selectedGift.name}`,
giftInfo: selectedGift,
isMe: true,
time: util.formatTime(new Date(), 'HH:mm')
}
this.setData({
messages: [...this.data.messages, giftMessage],
showGiftPopup: false,
selectedGift: null,
userFlowers: userFlowers - selectedGift.price
}, () => {
this.scrollToBottom()
})
util.showSuccess('礼物已送出')
// TODO: 调用后端API发送礼物
},
/**
* 语音通话
*/
onVoiceCall() {
this.setData({ showMorePanel: false })
wx.showModal({
title: '语音通话',
content: '语音通话功能即将上线,敬请期待~',
showCancel: false,
confirmText: '知道了'
})
},
/**
* 常用语
*/
onQuickReply() {
this.setData({ showMorePanel: false })
wx.showActionSheet({
itemList: this.data.quickReplies,
success: (res) => {
const selectedReply = this.data.quickReplies[res.tapIndex]
this.setData({ inputText: selectedReply })
}
})
},
/**
* 约时间
*/
onScheduleTime() {
this.setData({ showMorePanel: false })
wx.showModal({
title: '约时间',
content: '预约功能即将上线,敬请期待~',
showCancel: false,
confirmText: '知道了'
})
},
/**
* 抢红包
*/
onRedPacket() {
this.setData({ showMorePanel: false })
wx.showModal({
title: '抢红包',
content: '红包功能即将上线,敬请期待~',
showCancel: false,
confirmText: '知道了'
})
},
/**
* 测结果
*/
onTestResult() {
this.setData({ showMorePanel: false })
// 跳转到测试结果页面
wx.showModal({
title: '测结果',
content: '心理测试功能即将上线,敬请期待~',
showCancel: false,
confirmText: '知道了'
})
},
/**
* 语音录制相关方法
*/
onVoiceTouchStart(e) {
this.touchStartY = e.touches[0].clientY
this.setData({
isRecording: true,
voiceCancelHint: false,
recordingDuration: 0
})
// 开始录音
const recorderManager = wx.getRecorderManager()
recorderManager.start({
duration: 60000,
format: 'mp3'
})
this.recorderManager = recorderManager
// 录音时长计时
this.recordingTimer = setInterval(() => {
this.setData({
recordingDuration: this.data.recordingDuration + 1
})
}, 1000)
},
onVoiceTouchMove(e) {
const moveY = e.touches[0].clientY
const diff = this.touchStartY - moveY
// 上滑超过50px显示取消提示
this.setData({
voiceCancelHint: diff > 50
})
},
onVoiceTouchEnd() {
clearInterval(this.recordingTimer)
const { voiceCancelHint, recordingDuration, characterId, character, isUnlocked, remainingCount } = this.data
this.setData({ isRecording: false })
if (this.recorderManager) {
this.recorderManager.stop()
if (voiceCancelHint) {
// 取消发送
util.showToast('已取消')
return
}
if (recordingDuration < 1) {
util.showError('录音时间太短')
return
}
this.recorderManager.onStop(async (res) => {
console.log('[chat-detail] 录音完成:', res.tempFilePath, '时长:', recordingDuration)
// 先显示语音消息(带识别中状态)
const newId = util.generateId()
const voiceMessage = {
id: newId,
type: 'voice',
audioUrl: res.tempFilePath,
duration: recordingDuration,
isMe: true,
time: util.formatTime(new Date(), 'HH:mm'),
recognizing: true, // 识别中状态
recognizedText: '' // 识别出的文字
}
this.setData({
messages: [...this.data.messages, voiceMessage]
}, () => {
this.scrollToBottom()
})
// 进行语音识别
try {
wx.showLoading({ title: '语音识别中...' })
// 读取音频文件并转换为base64
const fs = wx.getFileSystemManager()
const audioData = fs.readFileSync(res.tempFilePath)
const audioBase64 = wx.arrayBufferToBase64(audioData)
// 调用语音识别API
const recognizeRes = await api.speech.recognize({
audio: audioBase64,
format: 'mp3'
})
wx.hideLoading()
let recognizedText = ''
if (recognizeRes.success && recognizeRes.data && recognizeRes.data.text) {
recognizedText = recognizeRes.data.text
console.log('[chat-detail] 语音识别结果:', recognizedText)
} else {
// 识别失败,使用默认文字
recognizedText = '[语音消息]'
console.log('[chat-detail] 语音识别失败,使用默认文字')
}
// 更新语音消息的识别状态
const messages = this.data.messages.map(msg => {
if (msg.id === newId) {
return { ...msg, recognizing: false, recognizedText }
}
return msg
})
this.setData({ messages })
// 如果识别出了有效文字发送给AI
if (recognizedText && recognizedText !== '[语音消息]') {
// 检查聊天权限
const canChatByFreeTime = !!(this.data.freeTime && this.data.freeTime.isActive)
const canChatByVip = !!this.data.isVip
if (!isUnlocked && !canChatByVip && !canChatByFreeTime) {
console.log('[chat-detail] 语音消息无聊天权限', { isUnlocked, isVip, canChatByFreeTime })
this.setData({ showUnlockPopup: true })
return
}
// 将识别出的文字加入待处理队列
this.pendingMessages.push(recognizedText)
// 如果没有正在等待的定时器,启动延迟处理
if (!this.messageTimer) {
this.startMessageTimer(characterId, this.data.conversationId, character, isUnlocked, remainingCount)
}
}
} catch (err) {
wx.hideLoading()
console.error('[chat-detail] 语音识别失败:', err)
// 更新消息状态
const messages = this.data.messages.map(msg => {
if (msg.id === newId) {
return { ...msg, recognizing: false, recognizedText: '[语音消息]' }
}
return msg
})
this.setData({ messages })
util.showError('语音识别失败')
}
})
}
},
onVoiceTouchCancel() {
clearInterval(this.recordingTimer)
this.setData({ isRecording: false })
if (this.recorderManager) {
this.recorderManager.stop()
}
},
/**
* 消息长按操作
*/
onMessageLongPress(e) {
const item = e.currentTarget.dataset.item
wx.showActionSheet({
itemList: ['复制', '删除'],
success: (res) => {
if (res.tapIndex === 0) {
// 复制
wx.setClipboardData({
data: item.text,
success: () => {
util.showSuccess('已复制')
}
})
} else if (res.tapIndex === 1) {
// 删除
const messages = this.data.messages.filter(msg => msg.id !== item.id)
this.setData({ messages })
}
}
})
},
/**
* 阻止事件冒泡
*/
preventBubble() {
return
},
/**
* 阻止触摸穿透
*/
preventTouchMove() {
return false
}
})