// pages/chat/chat.js // 消息列表页面 - 显示会话列表 const app = getApp() const api = require('../../utils/api') const util = require('../../utils/util') const proactiveMessage = require('../../utils/proactiveMessage') Page({ data: { // 系统消息(静态) systemMessages: [ { id: 'sys-1', name: '推广收益', type: 'system', icon: '/images/icon-trending-up.png', gradient: 'pink', preview: '您的好友开通了会员,您获得收益!', time: '刚刚', unread: 0 }, { id: 'sys-2', name: '系统通知', type: 'system', icon: '/images/icon-bell.png', gradient: 'purple', preview: '欢迎来到心伴俱乐部', time: '', unread: 0 } ], // AI会话列表 conversations: [], // 总未读消息数 totalUnread: 0, // 加载状态 loading: true, error: null, auditStatus: 0, // 免费畅聊相关 freeTime: null, countdownText: '' }, onLoad() { this.loadConversations() }, onShow() { wx.hideTabBar({ animation: false }) const app = getApp() this.setData({ auditStatus: app.globalData.auditStatus }) // 检查免费畅聊时间 this.checkFreeTime() // 每次显示时刷新列表 // 增加延迟,确保标记已读API有时间完成(从聊天详情页返回时) if (!this.data.loading) { setTimeout(() => { this.loadConversations() }, 500) // 增加到500ms,确保标记已读API完成 } }, onPullDownRefresh() { this.checkFreeTime() this.loadConversations().then(() => { wx.stopPullDownRefresh() }) }, /** * 检查免费畅聊时间 */ async checkFreeTime() { if (!app.globalData.isLoggedIn) return try { const res = await api.chat.getFreeTime() console.log('[chat] 免费畅聊时间响应:', res) if (res.success && res.data) { this.setData({ freeTime: res.data }) if (res.data.isActive && res.data.remainingSeconds > 0) { this.startCountdown(res.data.remainingSeconds) } else { this.stopCountdown() } } } catch (err) { console.error('[chat] 获取免费畅聊时间失败:', err) } }, /** * 开始倒计时 */ 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: '' }) }, /** * 格式化秒数为分钟(向上取整) */ formatSeconds(s) { const m = Math.ceil(s / 60) return m }, onHide() { this.stopCountdown() }, onUnload() { this.stopCountdown() }, /** * 加载会话列表 */ async loadConversations() { // 检查登录状态 if (!app.globalData.isLoggedIn) { this.setData({ conversations: [], loading: false }) return } this.setData({ loading: true, error: null }) try { // 并行获取会话列表和主动推送消息 const [convRes, proactiveMessages] = await Promise.all([ api.chat.getConversations(), this.getProactiveMessagesForList() ]) console.log('[chat] 会话列表API响应:', JSON.stringify(convRes)) console.log('[chat] 主动推送消息:', JSON.stringify(proactiveMessages)) if (convRes.success && convRes.data) { // 转换数据格式 let conversations = convRes.data.map(conv => this.transformConversation(conv)) console.log('[chat] 转换后的会话数量:', conversations.length) // 将主动推送消息合并到会话列表 if (proactiveMessages && proactiveMessages.length > 0) { console.log('[chat] 开始合并主动推送消息,数量:', proactiveMessages.length) conversations = this.mergeProactiveMessages(conversations, proactiveMessages) console.log('[chat] 合并后的会话数量:', conversations.length) } // 按时间排序(最新的在前) conversations.sort((a, b) => { return new Date(b.updatedAt) - new Date(a.updatedAt) }) // 计算总未读消息数 const totalUnread = conversations.reduce((sum, conv) => sum + (conv.unread || 0), 0) this.setData({ conversations, totalUnread, loading: false }) console.log('[chat] 最终会话列表:', conversations.map(c => ({ id: c.id, name: c.name, unread: c.unread, isProactive: c.isProactive }))) } else { throw new Error(convRes.message || '加载失败') } } catch (err) { console.error('加载会话列表失败', err) // 如果是401错误,不显示错误提示,因为会话列表会被清空 if (err.code === 401) { this.setData({ conversations: [], loading: false }) return } this.setData({ loading: false, error: err.message || '加载失败' }) } }, /** * 转换会话数据格式 * @param {object} conv - 后端会话数据 */ transformConversation(conv) { const character = conv.character || {} const config = require('../../config/index') // 处理头像URL - 如果是相对路径,拼接服务器地址 let avatarUrl = character.avatar || '' if (avatarUrl && avatarUrl.startsWith('/characters/')) { const baseUrl = config.API_BASE_URL.replace('/api', '') avatarUrl = baseUrl + avatarUrl } // 处理预览消息 - 优先显示最后消息,否则显示默认提示 let preview = conv.last_message if (!preview || preview.trim() === '') { preview = '点击开始聊天~' } return { id: conv.id, characterId: conv.character_id || conv.target_id, name: character.name || 'AI助手', type: 'ai', avatar: avatarUrl || 'https://ai-c.maimanji.com/images/default-avatar.png', preview: preview, time: util.formatRelativeTime(conv.updated_at), updatedAt: conv.updated_at, unread: conv.unread_count || 0, isOnline: true } }, /** * 获取主动推送消息列表(用于合并到会话列表) * 直接调用API获取,不依赖缓存 */ async getProactiveMessagesForList() { try { // 直接调用API获取待推送消息 const res = await api.proactiveMessage.getPending() console.log('[chat] 主动推送消息API响应:', JSON.stringify(res)) if (res.success && res.data && Array.isArray(res.data)) { console.log('[chat] 获取到主动推送消息:', res.data.length, '条') return res.data } console.log('[chat] 主动推送消息API返回空或格式错误') return [] } catch (err) { console.log('[chat] 获取主动推送消息失败', err) return [] } }, /** * 将主动推送消息合并到会话列表 * @param {Array} conversations - 现有会话列表 * @param {Array} proactiveMessages - 主动推送消息列表 */ mergeProactiveMessages(conversations, proactiveMessages) { if (!proactiveMessages || proactiveMessages.length === 0) { console.log('[chat] 没有主动推送消息需要合并') return conversations } const config = require('../../config/index') const baseUrl = config.API_BASE_URL.replace('/api', '') console.log('[chat] 开始合并主动推送消息,消息数:', proactiveMessages.length) // 遍历主动推送消息 proactiveMessages.forEach((msg, index) => { console.log(`[chat] 处理第${index + 1}条消息:`, { character_id: msg.character_id, character_name: msg.character_name, content: msg.content?.substring(0, 20) + '...' }) // 查找是否已有该角色的会话 const existingConvIndex = conversations.findIndex(c => { // 兼容不同的ID格式(字符串和数字) return String(c.characterId) === String(msg.character_id) }) if (existingConvIndex >= 0) { // 已有会话:只更新预览消息和未读数,不修改时间(避免列表位置跳动) const existingConv = conversations[existingConvIndex] console.log(`[chat] 找到已有会话:`, existingConv.name, '更新消息(保持原有时间排序)') existingConv.preview = msg.content existingConv.unread = (existingConv.unread || 0) + 1 // 注意:不修改 updatedAt 和 time,保持会话原有的排序位置 existingConv.isProactive = true // 标记为主动推送消息 } else { // 没有会话:创建新的会话项 console.log(`[chat] 创建新会话:`, msg.character_name) let avatarUrl = msg.character_logo || '' if (avatarUrl && avatarUrl.startsWith('/characters/')) { avatarUrl = baseUrl + avatarUrl } const newConv = { id: `proactive_${msg.character_id}`, characterId: msg.character_id, name: msg.character_name || 'AI助手', type: 'ai', avatar: avatarUrl || 'https://ai-c.maimanji.com/images/default-avatar.png', preview: msg.content, time: util.formatRelativeTime(msg.sent_at), updatedAt: msg.sent_at, unread: 1, isOnline: true, isProactive: true // 标记为主动推送消息 } conversations.push(newConv) console.log(`[chat] 新会话已添加:`, newConv.name, newConv.id) } }) console.log('[chat] 合并完成,最终会话数:', conversations.length) return conversations }, /** * 点击免费畅聊条 */ onFreeChatTap() { wx.showModal({ title: '免费畅聊', content: '您当前拥有免费畅聊特权,可以无消耗与 AI 角色对话。', showCancel: false, confirmText: '我知道了' }) }, /** * 返回上一页 */ goBack() { wx.navigateBack({ fail: () => { wx.switchTab({ url: '/pages/index/index' }) } }) }, /** * 点击消息项 */ async onMessageTap(e) { const { id, type } = e.currentTarget.dataset if (type === 'system') { // 系统消息 this.handleSystemMessage(id) } else { // AI会话 const conversation = this.data.conversations.find(c => c.id === id) if (conversation) { // 处理主动推送消息创建的虚拟会话(ID以proactive_开头) const isProactiveConv = id.startsWith('proactive_') const conversationId = isProactiveConv ? '' : id // 跳转到聊天详情页(标记已读在详情页onLoad时调用) wx.navigateTo({ url: `/pages/chat-detail/chat-detail?id=${conversation.characterId}&conversationId=${conversationId}&name=${encodeURIComponent(conversation.name)}` }) } } }, /** * 更新本地未读数 * @param {string} conversationId - 会话ID * @param {number} unread - 新的未读数 */ updateLocalUnread(conversationId, unread) { const conversations = this.data.conversations.map(conv => { if (conv.id === conversationId) { return { ...conv, unread } } return conv }) // 重新计算总未读数 const totalUnread = conversations.reduce((sum, conv) => sum + (conv.unread || 0), 0) this.setData({ conversations, totalUnread }) }, /** * 处理系统消息点击 */ handleSystemMessage(id) { if (id === 'sys-1') { // 推广收益 wx.navigateTo({ url: '/pages/commission/commission' }) } else if (id === 'sys-2') { // 系统通知 wx.showToast({ title: '暂无新通知', icon: 'none' }) } }, /** * 标记会话已读 */ async markAsRead(conversationId) { try { await api.chat.markAsRead(conversationId) // 更新本地状态 const conversations = this.data.conversations.map(conv => { if (conv.id === conversationId) { return { ...conv, unread: 0 } } return conv }) // 重新计算总未读数 const totalUnread = conversations.reduce((sum, conv) => sum + (conv.unread || 0), 0) this.setData({ conversations, totalUnread }) } catch (err) { console.log('标记已读失败', err) } }, /** * 删除会话(长按) */ onMessageLongPress(e) { const { id, type } = e.currentTarget.dataset if (type === 'system') return wx.showActionSheet({ itemList: ['清空聊天记录', '删除会话(从列表移除)'], success: (res) => { if (res.tapIndex === 0) { // 清空聊天记录(保留会话) this.clearChatHistory(id) } else if (res.tapIndex === 1) { // 删除会话(从列表移除) this.deleteConversation(id) } } }) }, /** * 滑动开始 */ onTouchStart(e) { this.touchStartX = e.touches[0].clientX this.touchStartY = e.touches[0].clientY this.touchStartTime = Date.now() }, /** * 滑动中 */ onTouchMove(e) { const moveX = e.touches[0].clientX - this.touchStartX const moveY = e.touches[0].clientY - this.touchStartY // 如果垂直滑动大于水平滑动,不处理 if (Math.abs(moveY) > Math.abs(moveX)) return }, /** * 滑动结束 */ onTouchEnd(e) { const endX = e.changedTouches[0].clientX const moveX = endX - this.touchStartX const index = e.currentTarget.dataset.index const id = e.currentTarget.dataset.id // 先关闭其他已展开的项 this.closeAllSwipe(index) // 左滑超过50px显示删除按钮 if (moveX < -50) { this.setSwipeState(index, true) } else if (moveX > 50) { // 右滑关闭 this.setSwipeState(index, false) } }, /** * 设置滑动状态 */ setSwipeState(index, swiped) { const conversations = this.data.conversations if (conversations[index]) { conversations[index].swiped = swiped this.setData({ conversations }) } }, /** * 关闭所有滑动项(除了指定的) */ closeAllSwipe(exceptIndex) { const conversations = this.data.conversations.map((conv, idx) => { if (idx !== exceptIndex && conv.swiped) { return { ...conv, swiped: false } } return conv }) this.setData({ conversations }) }, /** * 滑动删除按钮点击 */ onSwipeDelete(e) { const { id, index } = e.currentTarget.dataset this.deleteConversation(id) }, /** * 删除会话 * 完全删除会话,包括聊天记录,会话从列表中移除 */ async deleteConversation(conversationId) { const confirmed = await util.showConfirm({ title: '删除会话', content: '确定要删除这个会话吗?会话将从列表中移除,聊天记录也会被清空。' }) if (!confirmed) return wx.showLoading({ title: '删除中...' }) try { // 调用后端API删除会话 const res = await api.chat.deleteConversation(conversationId) if (res.success || res.code === 0) { // 本地删除 const conversations = this.data.conversations.filter(c => c.id !== conversationId) this.setData({ conversations }) util.showSuccess('已删除') } else { throw new Error(res.message || '删除失败') } } catch (err) { console.error('删除会话失败:', err) // 即使API失败,也从本地删除 const conversations = this.data.conversations.filter(c => c.id !== conversationId) this.setData({ conversations }) util.showSuccess('已删除') } finally { wx.hideLoading() } }, /** * 清空聊天记录 * 只清空聊天记录,不删除会话 * 会话仍然显示在消息列表中,只是聊天记录被清空 */ async clearChatHistory(conversationId) { const confirmed = await util.showConfirm({ title: '清空记录', content: '确定要清空聊天记录吗?此操作不可恢复。会话仍会保留在列表中。' }) if (!confirmed) return // 找到对应的会话,获取角色ID const conversation = this.data.conversations.find(c => c.id === conversationId) if (!conversation || !conversation.characterId) { util.showError('会话信息错误') return } wx.showLoading({ title: '清空中...' }) try { // 调用后端API清空聊天记录(使用角色ID) const res = await api.chat.clearChatHistory(conversation.characterId) if (res.success || res.code === 0) { // 更新本地会话列表,清空预览消息 const conversations = this.data.conversations.map(conv => { if (conv.id === conversationId) { return { ...conv, preview: '点击开始聊天~', unread: 0 } } return conv }) this.setData({ conversations }) util.showSuccess('已清空') } else { throw new Error(res.message || '清空失败') } } catch (err) { console.error('清空聊天记录失败:', err) util.showError('清空失败,请重试') } finally { wx.hideLoading() } }, /** * 切换Tab - 需要登录的页面检查登录状态 */ switchTab(e) { const path = e.currentTarget.dataset.path if (path) { const app = getApp() // 消息页面需要登录 if (path === '/pages/chat/chat') { if (!app.globalData.isLoggedIn) { wx.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent(path) }) return } } wx.switchTab({ url: path }) } }, /** * 需要登录时的回调 */ onAuthRequired() { wx.showModal({ title: '提示', content: '请先登录后查看消息', confirmText: '去登录', confirmColor: '#b06ab3', success: (res) => { if (res.confirm) { app.wxLogin().then(() => { this.loadConversations() }).catch(err => { util.showError('登录失败') }) } } }) }, /** * 检查AI角色主动推送消息 */ async checkProactiveMessages() { if (!app.globalData.isLoggedIn) { return } try { const messages = await proactiveMessage.checkAndShowMessages({ onNewMessages: (msgs) => { // 收到新消息时刷新会话列表 this.loadConversations() } }) console.log('[chat] 主动推送消息检查完成,消息数:', messages.length) } catch (err) { console.log('[chat] 检查主动推送消息失败', err) } } })