715 lines
19 KiB
JavaScript
715 lines
19 KiB
JavaScript
// 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)
|
||
}
|
||
}
|
||
})
|