import { MESSAGE_TYPE, MESSAGE_STATUS } from "@/common/enums.js" import localForage from 'localforage' import {myCache} from "../utils/utils"; // 全局缓存变量(用于延迟渲染) let cacheChats = [] // =================== Vuex 模块 =================== const chatModule = { namespaced: true, state: () => ({ activeChat: null, privateMsgMaxId: 0, groupMsgMaxId: 0, loadingPrivateMsg: false, loadingGroupMsg: false, chats: [] // 实际渲染的会话列表(仅前 N 个) }), getters: { // 是否正在加载离线消息 isLoading: (state) => () => { return state.loadingPrivateMsg || state.loadingGroupMsg }, // 返回当前应显示的会话列表(加载中返回缓存列表,否则返回真实列表) curChats: (state, getters) => () => { if (cacheChats && getters.isLoading()) { return cacheChats } return state.chats }, // 查找会话索引 findChatIdx: (state, getters) => (chat) => { const chats = getters.curChats() for (let idx in chats) { if (chats[idx].type === chat.type && chats[idx].targetId === chat.targetId) { return parseInt(idx) } } return -1 }, // 查找会话对象 findChat: (state, getters) => (chat) => { const idx = getters.findChatIdx(chat) return idx >= 0 ? getters.curChats()[idx] : null }, findChatByFriend: (state, getters) => (fid) => { const chats = getters.curChats() return chats.find(c => c.type === 'PRIVATE' && c.targetId === fid) }, findChatByGroup: (state, getters) => (gid) => { const chats = getters.curChats() return chats.find(c => c.type === 'GROUP' && c.targetId === gid) }, // 在指定 chat 中查找消息 findMessage: (state, getters) => (chat, msgInfo) => { if (!chat) return null for (let idx in chat.messages) { const m = chat.messages[idx] if (msgInfo.id && m.id === msgInfo.id) return m if (msgInfo.tmpId && m.tmpId && m.tmpId === msgInfo.tmpId) return m } return null } }, mutations: { // ---------- 基础 mutations ---------- SET_CHATS(state, chats) { state.chats = chats }, SET_PRIVATE_MSG_MAX_ID(state, id) { state.privateMsgMaxId = id }, SET_GROUP_MSG_MAX_ID(state, id) { state.groupMsgMaxId = id }, SET_LOADING_PRIVATE(state, loading) { state.loadingPrivateMsg = loading }, SET_LOADING_GROUP(state, loading) { state.loadingGroupMsg = loading }, SET_ACTIVE_CHAT(state, chat) { state.activeChat = chat }, // ---------- 会话操作 ---------- ADD_CHAT(state, newChat) { // 插入到列表头部 state.chats.unshift(newChat) }, REMOVE_CHAT(state, idx) { if (idx >= 0 && idx < state.chats.length) { state.chats.splice(idx, 1) } }, MOVE_CHAT_TO_TOP(state, idx) { if (idx > 0 && idx < state.chats.length) { const chat = state.chats[idx] state.chats.splice(idx, 1) state.chats.unshift(chat) chat.lastSendTime = new Date().getTime() chat.stored = false } }, UPDATE_CHAT(state, { idx, chat }) { if (idx >= 0 && idx < state.chats.length) { state.chats[idx] = chat } }, RESET_UNREAD_COUNT(state, { type, targetId }) { for (let c of state.chats) { if (c.type === type && c.targetId === targetId) { c.unreadCount = 0 c.atMe = false c.atAll = false c.stored = false break } } }, // ---------- 消息操作 ---------- UPDATE_MESSAGE(state, { chatIdx, msgInfo }) { const chat = state.chats[chatIdx] if (!chat) return const msg = chat.messages.find(m => (m.id && m.id === msgInfo.id) || (m.tmpId && m.tmpId === msgInfo.tmpId)) if (msg) { Object.assign(msg, msgInfo) chat.stored = false } }, DELETE_MESSAGE(state, { chatIdx, msgInfo }) { const chat = state.chats[chatIdx] if (!chat) return const idx = chat.messages.findIndex(m => (m.id && m.id === msgInfo.id) || (m.tmpId && m.tmpId === msgInfo.tmpId)) if (idx !== -1) { chat.messages.splice(idx, 1) chat.stored = false } }, // 更新会话的头像/名称 UPDATE_CHAT_INFO(state, { targetId, type, headImage, showName }) { for (let c of state.chats) { if (c.type === type && c.targetId === targetId) { if (headImage) c.headImage = headImage if (showName) c.showName = showName c.stored = false break } } }, // 更新 chat 的基本信息(lastContent, lastSendTime, sendNickName, unreadCount, atMe, atAll) UPDATE_CHAT_SUMMARY(state, { idx, lastContent, lastSendTime, sendNickName, unreadCount, atMe, atAll }) { const chat = state.chats[idx]; if (!chat) return; if (lastContent !== undefined) chat.lastContent = lastContent; if (lastSendTime !== undefined) chat.lastSendTime = lastSendTime; if (sendNickName !== undefined) chat.sendNickName = sendNickName; if (unreadCount !== undefined) chat.unreadCount = unreadCount; if (atMe !== undefined) chat.atMe = atMe; if (atAll !== undefined) chat.atAll = atAll; chat.stored = false; }, // 插入时间戳消息 PUSH_TIME_TIP(state, { idx, sendTime }) { const chat = state.chats[idx]; if (!chat) return; chat.messages.push({ sendTime, type: MESSAGE_TYPE.TIP_TIME }); chat.lastTimeTip = sendTime; chat.stored = false; }, // 插入消息(支持指定位置) INSERT_MESSAGE(state, { idx, msg, insertPos }) { const chat = state.chats[idx]; if (!chat) return; if (insertPos === undefined || insertPos === chat.messages.length) { chat.messages.push(msg); } else { chat.messages.splice(insertPos, 0, msg); } chat.stored = false; }, // 更新已有的消息(合并属性) UPDATE_EXISTING_MESSAGE(state, { idx, msgInfo }) { const chat = state.chats[idx]; if (!chat) return; const msg = chat.messages.find(m => (m.id && m.id === msgInfo.id) || (m.tmpId && m.tmpId === msgInfo.tmpId)); if (msg) Object.assign(msg, msgInfo); chat.stored = false; }, // 删除消息 DELETE_MESSAGE_BY_ID(state, { idx, msgInfo }) { const chat = state.chats[idx]; if (!chat) return; const pos = chat.messages.findIndex(m => (m.id && m.id === msgInfo.id) || (m.tmpId && m.tmpId === msgInfo.tmpId)); if (pos !== -1) { chat.messages.splice(pos, 1); chat.stored = false; } }, // 撤回消息 RECALL_MESSAGE(state, { idx, targetId, recallContent, sendTime, selfSend }) { const chat = state.chats[idx]; if (!chat) return; for (let m of chat.messages) { if (m.id === targetId) { m.status = MESSAGE_STATUS.RECALL; m.content = recallContent; m.type = MESSAGE_TYPE.TIP_TEXT; // 更新会话最后一条(如果撤回的是最后一条) if (chat.messages[chat.messages.length - 1] === m) { chat.lastContent = recallContent; chat.lastSendTime = sendTime; chat.sendNickName = ''; } break; } } // 处理引用消息 for (let m of chat.messages) { if (m.quoteMessage && m.quoteMessage.id === targetId) { m.quoteMessage.content = "引用内容已撤回"; m.quoteMessage.status = MESSAGE_STATUS.RECALL; m.quoteMessage.type = MESSAGE_TYPE.TIP_TEXT; } } chat.stored = false; }, // 更新会话头像和名称 UPDATE_CHAT_AVATAR_NAME(state, { type, targetId, headImage, showName }) { for (let chat of state.chats) { if (chat.type === type && chat.targetId === targetId) { if (headImage !== undefined) chat.headImage = headImage; if (showName !== undefined) chat.showName = showName; chat.stored = false; break; } } }, // 一次性替换所有会话(用于 refreshChats) REPLACE_CHATS(state, chats) { state.chats = chats; // 清空缓存(全局变量) cacheChats = null; }, // 更新单个 chat 的 hotMinIdx 和 stored 等 UPDATE_CHAT_META(state, { idx, hotMinIdx, stored }) { const chat = state.chats[idx]; if (!chat) return; if (hotMinIdx !== undefined) chat.hotMinIdx = hotMinIdx; if (stored !== undefined) chat.stored = stored; }, CLEAR_UNREAD_COUNT(state, idx) { const chats = state.chats; // 或者通过 getter 获取当前列表 if (idx >= 0 && idx < chats.length) { const chat = chats[idx]; if (chat) { chat.unreadCount = 0; chat.atMe = false; chat.atAll = false; chat.stored = false; // 标记需持久化 } } } }, actions: { // ========== 初始化 ========== initChats({ commit, state }, chatsData) { cacheChats = [] commit('SET_CHATS', []) commit('SET_PRIVATE_MSG_MAX_ID', chatsData.privateMsgMaxId || 0) commit('SET_GROUP_MSG_MAX_ID', chatsData.groupMsgMaxId || 0) const allChats = chatsData.chats || [] for (let chat of allChats) { chat.stored = false // 深拷贝存入缓存,用于延迟渲染 cacheChats.push(JSON.parse(JSON.stringify(chat))) // 仅渲染前 15 个会话(可调) if (state.chats.length < 15) { // 直接 push 新对象,避免引用缓存 state.chats.push({ ...chat }) } } // 防止图片一直处在加载中状态 cacheChats.forEach((chat) => { chat.messages.forEach((msg) => { if (msg.loadStatus == "loading") { msg.loadStatus = "fail" } }) }) }, // ========== 打开会话 ========== openChat({ state, getters, commit, dispatch }, chatInfo) { // 加载中直接返回(原逻辑) // if (getters.isLoading()) return const chats = getters.curChats() let existingIdx = -1 for (let idx in chats) { if (chats[idx].type === chatInfo.type && chats[idx].targetId === chatInfo.targetId) { existingIdx = parseInt(idx) break } } if (existingIdx !== -1) { // 已存在 → 移到顶部 dispatch('moveTop', existingIdx) } else { // 新建会话 const newChat = { targetId: chatInfo.targetId, type: chatInfo.type, showName: chatInfo.showName, headImage: chatInfo.headImage, lastContent: "", lastSendTime: new Date().getTime(), unreadCount: 0, hotMinIdx: 0, messages: [], atMe: false, atAll: false, stored: false, delete: false } commit('ADD_CHAT', newChat) // 原逻辑未立即保存,等待后续消息触发保存 dispatch('saveToStorage', { withColdMessage: false }) } }, activeChat({ commit, getters }, idx) { const chats = getters.curChats(); // 使用 getter 获取当前列表(支持加载状态) if (idx >= 0 && idx < chats.length) { commit('CLEAR_UNREAD_COUNT', idx); } }, // ========== 移动会话到顶部 ========== moveTop({ state, getters, commit, dispatch }, idx) { if (getters.isLoading()) return if (idx > 0 && idx < state.chats.length) { commit('MOVE_CHAT_TO_TOP', idx) // 移动后保存(原逻辑在 moveTop 最后调用了 saveToStorage) dispatch('saveToStorage', { withColdMessage: false }) } }, removeChat({ state, getters, commit, dispatch }, idx) { const chats = getters.curChats() chats[idx].delete = true chats[idx].stored = false dispatch('saveToStorage', { withColdMessage: false }) }, resetUnreadCount({ state, getters, dispatch }, chatInfo) { const chats = getters.curChats() for (let idx in chats) { if (chats[idx].type === chatInfo.type && chats[idx].targetId === chatInfo.targetId) { chats[idx].unreadCount = 0 chats[idx].atMe = false chats[idx].atAll = false chats[idx].stored = false dispatch('saveToStorage', { withColdMessage: false }) break } } }, readedMessage({ state, getters, dispatch }, pos) { const chat = getters.findChatByFriend(pos.friendId) if (!chat) return chat.messages.forEach((m) => { let status=Number(m.status) if (m.id && m.selfSend && status < MESSAGE_STATUS.RECALL) { // pos.maxId为空表示整个会话已读 if (!pos.maxId || m.id <= pos.maxId) { m.status = MESSAGE_STATUS.READED chat.stored = false } } }) dispatch('saveToStorage', { withColdMessage: false }) }, /** * 删除私聊会话 * @param {Object} context - Vuex action 上下文 * @param {Number|String} userId - 对方用户ID */ removePrivateChat({ getters, dispatch }, userId) { const chats = getters.curChats() for (let idx in chats) { if (chats[idx].type === 'PRIVATE' && chats[idx].targetId === userId) { // 将索引转为数字(因为 for...in 返回字符串) dispatch('removeChat', parseInt(idx)) break // 一个用户只有一个私聊会话,找到后退出 } } }, /** * 删除群聊会话 * @param {Object} context - Vuex action 上下文 * @param {Number|String} groupId - 群ID */ removeGroupChat({ getters, dispatch }, groupId) { const chats = getters.curChats() for (let idx in chats) { if (chats[idx].type === 'GROUP' && chats[idx].targetId === groupId) { dispatch('removeChat', parseInt(idx)) break } } }, insertMessage({ state, getters, commit, dispatch, rootState }, { msgInfo, chatInfo }) { const type = chatInfo.type; // 更新最大消息 ID if (msgInfo.id && type === "PRIVATE" && msgInfo.id > state.privateMsgMaxId) { commit('SET_PRIVATE_MSG_MAX_ID', msgInfo.id); } if (msgInfo.id && type === "GROUP" && msgInfo.id > state.groupMsgMaxId) { commit('SET_GROUP_MSG_MAX_ID', msgInfo.id); } // 查找会话 const idx = getters.findChatIdx(chatInfo); if (idx === -1) return; const chat = state.chats[idx]; // 直接引用 // 检查是否已存在该消息(重发或更新) const existingMsg = getters.findMessage(chat, msgInfo); if (existingMsg) { commit('UPDATE_EXISTING_MESSAGE', { idx, msgInfo }); dispatch('saveToStorage', { withColdMessage: false }); return; } let msgInfoType=Number(msgInfo.type) let msgInfoStatus=Number(msgInfo.status) // 准备要更新的字段 let lastContent = chat.lastContent; if (msgInfoType === MESSAGE_TYPE.IMAGE) lastContent = "[图片]"; else if (msgInfoType === MESSAGE_TYPE.FILE) lastContent = "[文件]"; else if (msgInfoType === MESSAGE_TYPE.AUDIO) lastContent = "[语音]"; else if (msgInfoType === MESSAGE_TYPE.ACT_RT_VOICE) lastContent = "[语音通话]"; else if (msgInfoType === MESSAGE_TYPE.ACT_RT_VIDEO) lastContent = "[视频通话]"; else if (msgInfoType === MESSAGE_TYPE.TEXT || msgInfoType === MESSAGE_TYPE.RECALL || msgInfoType === MESSAGE_TYPE.TIP_TEXT) { lastContent = msgInfo.content; } // 更新最后消息摘要 let unreadCount = chat.unreadCount || 0; let atMe = chat.atMe || false; let atAll = chat.atAll || false; // 未读计数 if (!msgInfo.selfSend && msgInfoStatus !== MESSAGE_STATUS.READED && msgInfoStatus !== MESSAGE_STATUS.RECALL && msgInfoType !== MESSAGE_TYPE.TIP_TEXT) { unreadCount++; } // @ 处理 if (!msgInfo.selfSend && chat.type === "GROUP" && msgInfo.atUserIds && msgInfoStatus !== MESSAGE_STATUS.READED) { const userInfo = rootState.user?.userInfo; if (userInfo) { if (msgInfo.atUserIds.includes(userInfo.id)) atMe = true; if (msgInfo.atUserIds.includes(-1)) atAll = true; } } // 更新会话摘要(通过 mutation) commit('UPDATE_CHAT_SUMMARY', { idx, lastContent, lastSendTime: msgInfo.sendTime, sendNickName: msgInfo.sendNickName, unreadCount, atMe, atAll }); // 时间戳提示(间隔10分钟) if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) { commit('PUSH_TIME_TIP', { idx, sendTime: msgInfo.sendTime }); } // 计算插入位置(防止乱序) let insertPos = chat.messages.length; if (msgInfo.id && msgInfo.id > 0) { for (let i = 0; i < chat.messages.length; i++) { const m = chat.messages[i]; if (m.id && msgInfo.id < m.id) { insertPos = i; console.log(`消息乱序修正: ${chat.messages.length} -> ${insertPos}`); break; } } } // 插入消息 commit('INSERT_MESSAGE', { idx, msg: msgInfo, insertPos }); // 标记已存储为 false(已在 mutation 中完成) // 保存到本地 dispatch('saveToStorage', { withColdMessage: false }); }, // ========== 更新消息 ========== updateMessage({ getters, commit, dispatch }, { msgInfo, chatInfo }) { const idx = getters.findChatIdx(chatInfo); if (idx === -1) return; commit('UPDATE_EXISTING_MESSAGE', { idx, msgInfo }); dispatch('saveToStorage', { withColdMessage: false }); }, // ========== 删除消息 ========== deleteMessage({ getters, commit, dispatch }, { msgInfo, chatInfo }) { const idx = getters.findChatIdx(chatInfo); if (idx === -1) return; const chat = getters.findChat(chatInfo); // 找到消息位置,判断是否为冷消息(仅用于记录,不影响存储) let isCold = false; for (let i = 0; i < chat.messages.length; i++) { const m = chat.messages[i]; if ((m.id && m.id === msgInfo.id) || (m.tmpId && m.tmpId === msgInfo.tmpId)) { isCold = i < chat.hotMinIdx; break; } } commit('DELETE_MESSAGE_BY_ID', { idx, msgInfo }); dispatch('saveToStorage', { withColdMessage: isCold }); }, // ========== 撤回消息 ========== recallMessage({ getters, commit, dispatch, rootState }, { msgInfo, chatInfo }) { const idx = getters.findChatIdx(chatInfo); if (idx === -1) return; const chat = getters.findChat(chatInfo); const targetId = msgInfo.content; // 被撤回的消息ID // 构建撤回提示内容 const name = msgInfo.selfSend ? '你' : (chat.type === 'PRIVATE' ? '对方' : msgInfo.sendNickName); const recallContent = name + "撤回了一条消息"; // 判断是否为冷消息 let isCold = false; for (let i = 0; i < chat.messages.length; i++) { if (chat.messages[i].id === targetId) { isCold = i < chat.hotMinIdx; break; } } commit('RECALL_MESSAGE', { idx, targetId, recallContent, sendTime: msgInfo.sendTime, selfSend: msgInfo.selfSend }); // 更新未读计数(如果撤回的是对方消息且未读) if (!msgInfo.selfSend && Number(msgInfo.status) !== MESSAGE_STATUS.READED) { // 需要在 mutation 中增加未读计数?但原逻辑是 chat.unreadCount++,我们可以在 mutation 中处理 // 但为了复用,我们在 action 中手动增加(通过 UPDATE_CHAT_SUMMARY) const chatRef = state.chats[idx]; if (chatRef) { commit('UPDATE_CHAT_SUMMARY', { idx, unreadCount: (chatRef.unreadCount || 0) + 1 }); } } dispatch('saveToStorage', { withColdMessage: isCold }); }, // ========== 更新会话信息(好友) ========== updateChatFromFriend({ commit, dispatch }, friend) { commit('UPDATE_CHAT_AVATAR_NAME', { type: 'PRIVATE', targetId: friend.id, headImage: friend.headImage, showName: friend.nickName }); // 存储(会影响多个会话,但只更新了头像名称,可延迟保存) dispatch('saveToStorage', { withColdMessage: false }); }, // ========== 更新会话信息(用户自身) ========== updateChatFromUser({ commit, dispatch }, user) { commit('UPDATE_CHAT_AVATAR_NAME', { type: 'PRIVATE', targetId: user.id, headImage: user.headImageThumb, showName: user.nickName }); dispatch('saveToStorage', { withColdMessage: false }); }, // ========== 更新会话信息(群组) ========== updateChatFromGroup({ commit, dispatch }, group) { commit('UPDATE_CHAT_AVATAR_NAME', { type: 'GROUP', targetId: group.id, headImage: group.headImageThumb, showName: group.name }); dispatch('saveToStorage', { withColdMessage: false }); }, setLoadingPrivateMsg({ commit, dispatch, getters }, loading) { commit('SET_LOADING_PRIVATE', loading); if (!getters.isLoading()) { // 使用 context.getters(模块内局部 getters) dispatch('refreshChats'); } }, setLoadingGroupMsg({ commit, dispatch, getters }, loading) { commit('SET_LOADING_GROUP', loading); if (!getters.isLoading()) { // 同上 dispatch('refreshChats'); } }, // ========== 刷新会话列表(将缓存数据一次性渲染) ========== refreshChats({ commit, dispatch }) { if (!cacheChats) return; // 排序(按最后消息时间倒序) cacheChats.sort((a, b) => b.lastSendTime - a.lastSendTime); // 非 App 端容量限制:每个会话最多保留1000条消息 // #ifndef APP-PLUS cacheChats.forEach(chat => { if (chat.messages.length > 1000) { chat.messages = chat.messages.slice(-1000); } }); // #endif // 记录热数据索引(所有消息都视为热数据) cacheChats.forEach(chat => chat.hotMinIdx = chat.messages.length); // 一次性替换 state.chats commit('REPLACE_CHATS', cacheChats); // 清空缓存变量(已在 mutation 中处理) // 持久化 dispatch('saveToStorage', { withColdMessage: true }); }, // ========== 保存到本地 ========== saveToStorage({ state, getters, rootState }, { withColdMessage = false } = {}) { if (getters.isLoading()) return const userId = myCache('user').userid; if (!userId) return const key = `chats-app-${userId}` const chatKeys = [] // 按会话为单位存储, state.chats.forEach(chat => { // 只存储有改动的会话 const chatKey = `${key}-${chat.type}-${chat.targetId}` if (!chat.stored) { if (chat.delete) { uni.removeStorageSync(chatKey); } else { // 存储冷数据 if (withColdMessage) { const coldChat = { ...chat, messages: chat.messages.slice(0, chat.hotMinIdx) } uni.setStorageSync(chatKey, coldChat) } // 存储热消息 const hotKey = chatKey + '-hot' const hotChat = { ...chat, messages: chat.messages.slice(chat.hotMinIdx) } uni.setStorageSync(hotKey, hotChat); } chat.stored = true } if (!chat.delete) { chatKeys.push(chatKey) } }) const chatsData = { privateMsgMaxId: state.privateMsgMaxId, groupMsgMaxId: state.groupMsgMaxId, chatKeys } uni.setStorageSync(key, chatsData) // 移除已删除的会话 state.chats = state.chats.filter(c => !c.delete) }, // ========== 加载离线消息 ========== loadChat({ commit, dispatch, rootState }) { return new Promise((resolve, reject) => { var userId = myCache('user').userid; if (!userId) return resolve() const key = `chats-app-${userId}` const chatsData = uni.getStorageSync(`chats-app-${userId}`); if (chatsData) { if (chatsData.chatKeys) { chatsData.chats = []; chatsData.chatKeys.forEach(key => { const coldChat = uni.getStorageSync(key); const hotChat = uni.getStorageSync(key + '-hot'); if (!coldChat && !hotChat) { return; } // 合并冷热消息 const chat = Object.assign({}, coldChat, hotChat); if (hotChat && coldChat) { chat.messages = coldChat.messages.concat(hotChat.messages); } chatsData.chats.push(chat); }); } // 调用 initChats action 初始化数据 dispatch('initChats', chatsData); } resolve(); }) }, clear(state){ }, // ... 其他 actions 类似迁移 } } export default chatModule