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

715 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// pages/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)
}
}
})