From 4863b4c77cd1e9e630e5fd142b89af5ae9284f83 Mon Sep 17 00:00:00 2001 From: Phoenix <64720302+Concur-max@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:00:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E5=AE=9E=E7=8E=B0=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E8=AE=B0=E5=BD=95=E6=9C=AC=E5=9C=B0=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加Dexie.js作为本地数据库,实现聊天记录和会话的本地存储与同步 修改消息和会话相关store方法,支持本地数据库操作 优化消息加载逻辑,优先从本地加载再同步服务器数据 添加数据库工具函数,包括消息增删改查和会话管理功能 --- package.json | 3 + pnpm-lock.yaml | 35 + src/hooks/useTalkRecord.ts | 132 ++- src/store/modules/dialogue.js | 44 +- src/store/modules/talk.ts | 162 +++- src/utils/db.js | 766 ++++++++++++++++++ src/views/message/inner/IndexContent.vue | 1 - src/views/message/inner/IndexSider.vue | 3 + .../message/inner/panel/PanelContent.vue | 20 +- src/views/message/inner/panel/PanelFooter.vue | 9 +- 10 files changed, 1106 insertions(+), 69 deletions(-) create mode 100644 src/utils/db.js diff --git a/package.json b/package.json index 53c5bd8..838fa27 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,11 @@ "@vicons/ionicons5": "^0.13.0", "@vueup/vue-quill": "^1.2.0", "@vueuse/core": "^10.7.0", + "@vueuse/rxjs": "^13.4.0", "ant-design-vue": "^4.2.6", "axios": "^1.6.2", "dayjs": "^1.11.13", + "dexie": "^4.0.11", "highlight.js": "^11.5.0", "js-audio-recorder": "^1.0.7", "lodash-es": "^4.17.21", @@ -36,6 +38,7 @@ "quill": "^1.3.7", "quill-image-uploader": "^1.3.0", "quill-mention": "^4.1.0", + "rxjs": "^7.8.2", "sortablejs": "^1.15.6", "viewerjs": "^1.11.7", "vue": "^3.3.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad1fb09..7a35f7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@vueuse/core': specifier: ^10.7.0 version: 10.11.1(vue@3.5.17(typescript@5.2.2)) + '@vueuse/rxjs': + specifier: ^13.4.0 + version: 13.4.0(rxjs@7.8.2)(vue@3.5.17(typescript@5.2.2)) ant-design-vue: specifier: ^4.2.6 version: 4.2.6(vue@3.5.17(typescript@5.2.2)) @@ -47,6 +50,9 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + dexie: + specifier: ^4.0.11 + version: 4.0.11 highlight.js: specifier: ^11.5.0 version: 11.11.1 @@ -74,6 +80,9 @@ importers: quill-mention: specifier: ^4.1.0 version: 4.1.0 + rxjs: + specifier: ^7.8.2 + version: 7.8.2 sortablejs: specifier: ^1.15.6 version: 1.15.6 @@ -1121,9 +1130,20 @@ packages: '@vueuse/metadata@10.11.1': resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} + '@vueuse/rxjs@13.4.0': + resolution: {integrity: sha512-KhfZ7qHlZXuV76KLpk10Ozg3Jq99GiuX+1sk/MMh3Jt8ubkZ8bhLRq2+M2buYdjlPt8O6600WV9K7fBIOLqKZQ==} + peerDependencies: + rxjs: '>=6.0.0' + vue: ^3.5.0 + '@vueuse/shared@10.11.1': resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} + '@vueuse/shared@13.4.0': + resolution: {integrity: sha512-+AxuKbw8R1gYy5T21V5yhadeNM7rJqb4cPaRI9DdGnnNl3uqXh+unvQ3uCaA2DjYLbNr1+l7ht/B4qEsRegX6A==} + peerDependencies: + vue: ^3.5.0 + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -1755,6 +1775,9 @@ packages: engines: {node: '>=0.10'} hasBin: true + dexie@4.0.11: + resolution: {integrity: sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==} + diff@5.2.0: resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} @@ -4616,6 +4639,12 @@ snapshots: '@vueuse/metadata@10.11.1': {} + '@vueuse/rxjs@13.4.0(rxjs@7.8.2)(vue@3.5.17(typescript@5.2.2))': + dependencies: + '@vueuse/shared': 13.4.0(vue@3.5.17(typescript@5.2.2)) + rxjs: 7.8.2 + vue: 3.5.17(typescript@5.2.2) + '@vueuse/shared@10.11.1(vue@3.5.17(typescript@5.2.2))': dependencies: vue-demi: 0.14.10(vue@3.5.17(typescript@5.2.2)) @@ -4623,6 +4652,10 @@ snapshots: - '@vue/composition-api' - vue + '@vueuse/shared@13.4.0(vue@3.5.17(typescript@5.2.2))': + dependencies: + vue: 3.5.17(typescript@5.2.2) + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -5329,6 +5362,8 @@ snapshots: detect-libc@1.0.3: optional: true + dexie@4.0.11: {} + diff@5.2.0: {} dir-glob@2.2.2: diff --git a/src/hooks/useTalkRecord.ts b/src/hooks/useTalkRecord.ts index b2e4ab9..d6530cb 100644 --- a/src/hooks/useTalkRecord.ts +++ b/src/hooks/useTalkRecord.ts @@ -130,19 +130,19 @@ export const useTalkRecord = (uid: number) => { cursor: loadConfig.cursor, limit: 30 } - - loadConfig.status = 0 + // 如果不是从本地数据库加载的,则设置加载状态为0(加载中) + if (loadConfig.status !== 2 && loadConfig.status !== 3) { + loadConfig.status = 0 + } let scrollHeight = 0 - console.log('加载数据列表load') const el = document.getElementById('imChatPanel') if (el) { scrollHeight = el.scrollHeight } - const { data, code } = await ServeTalkRecords(request) if (code != 200) { - return (loadConfig.status = 1) + return (loadConfig.status = (loadConfig.status === 2 || loadConfig.status === 3) ? loadConfig.status : 1) // 如果已经从本地加载了数据,保持原状态 } // 防止对话切换过快,数据渲染错误 if ( @@ -154,20 +154,77 @@ export const useTalkRecord = (uid: number) => { const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item)) + // 同步到本地数据库 + try { + const { batchAddOrUpdateMessages } = await import('@/utils/db') + await batchAddOrUpdateMessages(data.items || [], params.talk_type, params.receiver_id, true, 'sequence') + console.log('聊天记录已同步到本地数据库') + } catch (error) { + console.error('同步聊天记录到本地数据库失败:', error) + } + + // 如果是从本地数据库加载的数据,且服务器返回的数据与本地数据相同,则不需要更新UI + if ((loadConfig.status === 2 || loadConfig.status === 3) && request.cursor === 0) { + try { + // 获取最新的本地数据库消息进行比较 + const { getMessages } = await import('@/utils/db') + const localMessages = await getMessages( + params.talk_type, + uid, + params.receiver_id, + items.length || 30, // 获取与服务器返回数量相同的消息 + 0 // 从第一页开始 + ) + + // 格式化本地消息,确保与服务器消息结构一致 + const formattedLocalMessages = localMessages.map((item: ITalkRecord) => formatTalkRecord(uid, item)) + + + // 改进比较逻辑:检查消息数量和所有消息的ID是否匹配 + if (formattedLocalMessages.length === items.length && formattedLocalMessages.length > 0) { + // 创建消息ID映射,用于快速查找 + const serverMsgMap = new Map() + items.forEach(item => serverMsgMap.set(item.msg_id, item)) + + // 检查每条本地消息是否与服务器消息匹配 + const allMatch = formattedLocalMessages.every(localMsg => { + const serverMsg = serverMsgMap.get(localMsg.msg_id) + // 检查消息是否存在且关键状态是否一致(考虑撤回、已读等状态变化) + return serverMsg && + serverMsg.is_revoke === localMsg.is_revoke && + serverMsg.is_read === localMsg.is_read && + (serverMsg.send_status === localMsg.send_status || + (!serverMsg.send_status && !localMsg.send_status)) && + serverMsg.content === localMsg.content + }) + + if (allMatch) { + console.log('本地数据与服务器数据一致,无需更新UI') + return + } + } + + // 数据不一致,需要更新UI + console.log('本地数据与服务器数据不一致,更新UI') + } catch (error) { + console.error('比较本地数据和服务器数据时出错:', error) + // 出错时默认更新UI + } + } + if (request.cursor == 0) { // 判断是否是初次加载 dialogueStore.clearDialogueRecord() } dialogueStore.unshiftDialogueRecord(items.reverse()) - + loadConfig.status = items.length >= request.limit ? 1 : 2 loadConfig.cursor = data.cursor nextTick(() => { const el = document.getElementById('imChatPanel') -console.log('request',request) if (el) { if (request.cursor == 0) { // el.scrollTop = el.scrollHeight @@ -195,9 +252,7 @@ console.log('request',request) // 获取当前消息的最小 sequence const getMinSequence = () => { - console.error('records.value', records.value) if (!records.value.length) return 0 - console.error(Math.min(...records.value.map((item) => item.sequence))) return Math.min(...records.value.map((item) => item.sequence)) } // 获取当前消息的最大 sequence @@ -206,13 +261,56 @@ console.log('request',request) return Math.max(...records.value.map((item) => item.sequence)) } + // 从本地数据库加载聊天记录 + const loadFromLocalDB = async (params: Params) => { + try { + // 导入 getMessages 函数 + const { getMessages } = await import('@/utils/db') + // 从本地数据库获取聊天记录 + const localMessages = await getMessages( + params.talk_type, + uid, + params.receiver_id, + params.limit || 30, + 0 // 从第一页开始 + // 不传入 maxSequence 参数,获取最新的消息 + ) + // 如果有本地数据 + if (localMessages && localMessages.length > 0) { + // 清空现有记录 + dialogueStore.clearDialogueRecord() + + // 格式化并添加记录 + const formattedMessages = localMessages.map((item: ITalkRecord) => formatTalkRecord(uid, item)) + dialogueStore.unshiftDialogueRecord(formattedMessages) + + // 设置加载状态为完成(3表示从本地数据库加载完成) + loadConfig.status = 3 + + // 恢复上传任务 + dialogueStore.restoreUploadTasks() + + // 滚动到底部 + nextTick(() => { + scrollToBottom() + }) + + return true + } + + return false + } catch (error) { + console.error('从本地数据库加载聊天记录失败:', error) + return false + } + } + /** * 加载数据主入口,支持指定消息定位模式 * @param params 原有参数 * @param options 可选,{ specifiedMsg } 指定消息对象 */ - const onLoad = (params: Params, options?: LoadOptions) => { - // 如果会话切换,重置所有状态 + const onLoad = async (params: Params, options?: LoadOptions) => { if ( params.talk_type !== loadConfig.talk_type || params.receiver_id !== loadConfig.receiver_id @@ -227,7 +325,6 @@ console.log('request',request) // 新增:支持指定消息定位模式,参数以传入为准合并 if (options?.specifiedMsg?.cursor !== undefined) { loadConfig.specialParams = { ...options.specifiedMsg } // 记录特殊参数,供分页加载用 - console.error('options', options) loadConfig.status = 0 // 复用主流程 loading 状态 // 以 params 为基础,合并 specifiedMsg 的所有字段(只要有就覆盖) const contextParams = { @@ -237,6 +334,7 @@ console.log('request',request) //msg_id是用来做定位的,不做参数,所以这里清空 contextParams.msg_id = '' ServeTalkRecords(contextParams).then(({ data, code }) => { + console.log('data',data) if (code !== 200) { loadConfig.status = 2 return @@ -339,6 +437,14 @@ console.log('request',request) } loadConfig.specialParams = undefined // 普通模式清空 + + // 设置初始加载状态为0(加载中) + loadConfig.status = 0 + + // 先从本地数据库加载数据 + const hasLocalData = await loadFromLocalDB(params) + + // 无论是否有本地数据,都从服务器获取最新数据 // 原有逻辑 console.log('onLoad()执行load') load(params) @@ -347,7 +453,7 @@ console.log('request',request) // 向上加载更多(兼容特殊参数模式) const onRefreshLoad = () => { console.error('loadConfig.status', loadConfig.status) - if (loadConfig.status == 1) { + if (loadConfig.status == 1 || loadConfig.status == 3) { console.log('specialParams', loadConfig.specialParams) // 判断是否是特殊参数模式 if (loadConfig.specialParams && typeof loadConfig.specialParams === 'object') { diff --git a/src/store/modules/dialogue.js b/src/store/modules/dialogue.js index 920bec8..022688a 100644 --- a/src/store/modules/dialogue.js +++ b/src/store/modules/dialogue.js @@ -175,10 +175,12 @@ export const useDialogueStore = defineStore('dialogue', { // 数组头部压入对话记录 unshiftDialogueRecord(records) { + console.log('unshiftDialogueRecord') this.records.unshift(...records) }, //数组尾部加入更多对话记录 addDialogueRecordForLoadMore(records){ + console.log('addDialogueRecordForLoadMore') this.records.push(...records) }, async getGroupInfo(){ @@ -190,24 +192,56 @@ export const useDialogueStore = defineStore('dialogue', { } }, // 推送对话记录 - addDialogueRecord(record) { + async addDialogueRecord(record) { // TOOD 需要通过 sequence 排序,保证消息一致性 // this.records.splice(index, 0, record) - + console.log('addDialogueRecord',addDialogueRecord) this.records.push(record) + + // 同步到本地数据库 + try { + const { addMessage } = await import('@/utils/db') + await addMessage(record) + } catch (error) { + console.error('同步消息到本地数据库失败:', error) + } }, // 更新对话记录 - updateDialogueRecord(params) { + async updateDialogueRecord(params) { const { msg_id = '' } = params const item = this.records.find((item) => item.msg_id === msg_id) - item && Object.assign(item, params) + if (item) { + Object.assign(item, params) + + // 同步到本地数据库 + try { + // 如果是撤回消息 + if (params.is_revoke === 1) { + const { revokeMessage } = await import('@/utils/db') + await revokeMessage(msg_id) + } + } catch (error) { + console.error('同步消息更新到本地数据库失败:', error) + } + } }, // 批量删除对话记录 - batchDelDialogueRecord(msgIds = []) { + async batchDelDialogueRecord(msgIds = []) { + // 同步到本地数据库 + try { + const { deleteMessage } = await import('@/utils/db') + for (const msgid of msgIds) { + await deleteMessage(msgid) + } + } catch (error) { + console.error('同步消息删除到本地数据库失败:', error) + } + + // 从内存中删除 msgIds.forEach((msgid) => { const index = this.records.findIndex((item) => item.msg_id === msgid) diff --git a/src/store/modules/talk.ts b/src/store/modules/talk.ts index c2feae8..d6185ff 100644 --- a/src/store/modules/talk.ts +++ b/src/store/modules/talk.ts @@ -3,6 +3,7 @@ import { ServeGetTalkList, ServeCreateTalkList } from '@/api/chat' import { formatTalkItem, ttime, KEY_INDEX_NAME } from '@/utils/talk' import { useEditorDraftStore } from './editor-draft' import { ISession } from '@/types/chat' +import { getConversations, addOrUpdateConversation, deleteConversation, getConversation } from '@/utils/db' interface TalkStoreState { loadStatus: number @@ -45,56 +46,126 @@ export const useTalkStore = defineStore('talk', { }, // 更新对话节点 - updateItem(params: any) { + async updateItem(params: any) { const item = this.items.find((item) => item.index_name === params.index_name) - item && Object.assign(item, params) + if (item) { + Object.assign(item, params) + + // 同步更新本地数据库 + try { + await addOrUpdateConversation(item) + } catch (error) { + console.error('更新本地会话失败:', error) + } + } }, // 新增对话节点 - addItem(params: any) { + async addItem(params: any) { this.items = [params, ...this.items] + + // 同步添加到本地数据库 + try { + await addOrUpdateConversation(params) + } catch (error) { + console.error('添加本地会话失败:', error) + } }, // 移除对话节点 - delItem(index_name: string) { + async delItem(index_name: string) { const i = this.items.findIndex((item) => item.index_name === index_name) if (i >= 0) { + const item = this.items[i] this.items.splice(i, 1) + + // 同步从本地数据库删除 + try { + // 从本地数据库中查找并删除会话 + const [talkType, receiverId] = index_name.split('_') + const conversation = await getConversation(Number(talkType), Number(receiverId)) + + if (conversation && conversation.id) { + await deleteConversation(conversation.id, false) // 不删除相关消息 + } + } catch (error) { + console.error('删除本地会话失败:', error) + } } this.items = [...this.items] }, // 更新对话消息 - updateMessage(params: any) { + async updateMessage(params: any) { const item = this.items.find((item) => item.index_name === params.index_name) if (item) { item.unread_num++ item.msg_text = params.msg_text item.updated_at = params.updated_at + + // 同步更新本地数据库中的会话信息 + try { + await addOrUpdateConversation(item) + } catch (error) { + console.error('更新本地会话消息失败:', error) + } } }, // 更新联系人备注 - setRemark(params: any) { + async setRemark(params: any) { const item = this.items.find((item) => item.index_name === `1_${params.user_id}`) - item && (item.remark = params.remark) + if (item) { + item.remark = params.remark + + // 同步更新本地数据库 + try { + await addOrUpdateConversation(item) + } catch (error) { + console.error('更新本地联系人备注失败:', error) + } + } }, // 加载会话列表 - loadTalkList() { + async loadTalkList() { this.loadStatus = 2 - const resp = ServeGetTalkList() + try { + // 先从本地数据库加载会话列表 + const localConversations = await getConversations() + if (localConversations && localConversations.length > 0) { + // 将本地会话列表转换为应用所需格式 + this.items = localConversations.map((item: any) => { + // 确保本地存储的会话格式与应用一致 + const value = formatTalkItem(item) - resp.then(({ code, data }) => { - if (code == 200) { + const draft = useEditorDraftStore().items[value.index_name] + if (draft) { + value.draft_text = JSON.parse(draft).text || '' + } - this.items = data.items.map((item: any) => { + if (value.is_robot == 1) { + value.is_online = 1 + } + return value + }) + + // 设置为加载完成状态,因为已从本地加载了数据,不需要等待服务器数据就可以显示 + this.loadStatus = 3 + } + + // 从服务器获取最新会话列表 + const resp = await ServeGetTalkList() + + if (resp.code == 200) { + // 将服务器返回的会话列表转换为应用所需格式 + const serverItems = resp.data.items.map((item: any) => { const value = formatTalkItem(item) const draft = useEditorDraftStore().items[value.index_name] @@ -108,22 +179,40 @@ export const useTalkStore = defineStore('talk', { return value }) + // 更新状态和本地数据库 + this.items = serverItems + + // 将最新的会话列表保存到本地数据库 + for (const item of serverItems) { + await addOrUpdateConversation(item) + } + this.loadStatus = 3 } else { - this.loadStatus = 4 + // 如果服务器请求失败但本地有数据,保持使用本地数据 + if (this.items.length === 0) { + this.loadStatus = 4 + } else { + this.loadStatus = 3 + } } - }) - - resp.catch(() => { - this.loadStatus = 4 - }) + } catch (error) { + console.error('加载会话列表失败:', error) + + // 如果有本地数据,即使服务器请求失败也显示本地数据 + if (this.items.length === 0) { + this.loadStatus = 4 + } else { + this.loadStatus = 3 + } + } }, findTalkIndex(index_name: string) { return this.items.findIndex((item: ISession) => item.index_name === index_name) }, - toTalk(talk_type: number, receiver_id: number, router: any) { + async toTalk(talk_type: number, receiver_id: number, router: any) { const route = { path: '/message', query: { @@ -136,13 +225,31 @@ export const useTalkStore = defineStore('talk', { return router.push(route) } - ServeCreateTalkList({ - talk_type, - receiver_id - }).then(({ code, data, message }) => { - if (code == 200) { + try { + // 先检查本地数据库中是否有该会话 + const localConversation = await getConversation(talk_type, receiver_id) + + if (localConversation) { + // 如果本地有该会话,直接添加到列表中 if (this.findTalkIndex(`${talk_type}_${receiver_id}`) === -1) { - this.addItem(formatTalkItem(data)) + this.addItem(formatTalkItem(localConversation)) + } + + sessionStorage.setItem(KEY_INDEX_NAME, `${talk_type}_${receiver_id}`) + return router.push(route) + } + + // 如果本地没有,则从服务器创建 + const { code, data, message } = await ServeCreateTalkList({ + talk_type, + receiver_id + }) + + if (code == 200) { + const formattedItem = formatTalkItem(data) + + if (this.findTalkIndex(`${talk_type}_${receiver_id}`) === -1) { + await this.addItem(formattedItem) // 使用 await 确保本地数据库同步更新 } sessionStorage.setItem(KEY_INDEX_NAME, `${talk_type}_${receiver_id}`) @@ -150,7 +257,10 @@ export const useTalkStore = defineStore('talk', { } else { window['$message'].info(message) } - }) + } catch (error) { + console.error('创建会话失败:', error) + window['$message'].error('创建会话失败,请稍后再试') + } } } }) diff --git a/src/utils/db.js b/src/utils/db.js new file mode 100644 index 0000000..6ac699e --- /dev/null +++ b/src/utils/db.js @@ -0,0 +1,766 @@ +// src/db.js +import Dexie from 'dexie'; + +// 创建数据库实例 +export const db = new Dexie('chatHistory'); + +// 定义数据库结构 +db.version(2).stores({ + // 表名: 索引字段列表 + // ++id 表示自增主键 + // name, age 表示这些字段将被索引 +// friends: '++id, name, age', + + // 聊天记录表 + // 为常用查询字段创建索引 + messages: 'msg_id, sequence, talk_type, msg_type, user_id, receiver_id, is_read, created_at', + + // 会话表(包含私聊和群聊) + // 为常用查询字段创建索引 + conversations: 'id, talk_type, receiver_id, index_name, updated_at, unread_num' +}); + +// 数据库升级处理 +db.on('ready', function() { + // 检查是否需要从服务器同步数据 + console.log('数据库已就绪,版本:', db.verno); +}); + +/** + * 消息类型常量 + */ +export const MessageType = { + TEXT: 1, // 文本消息 + IMAGE: 2, // 图片消息 + FILE: 3, // 文件消息 + AUDIO: 4, // 语音消息 + VIDEO: 5, // 视频消息 + LOCATION: 6, // 位置消息 + CARD: 7, // 名片消息 +}; + +/** + * 会话类型常量 + */ +export const TalkType = { + PRIVATE: 1, // 私聊 + GROUP: 2, // 群聊 +}; + +/** + * 添加一条聊天记录 + * @param {Object} message - 消息对象 + * @returns {Promise} - 返回消息ID + */ +export async function addMessage(message) { + try { + // 确保消息有唯一ID + if (!message.msg_id) { + message.msg_id = generateUUID(); + } + + // 添加时间戳(如果没有) + if (!message.created_at) { + message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19); + } + + // 添加到数据库 + const id = await db.messages.add(message); + return message.msg_id; + } catch (error) { + console.error('添加消息失败:', error); + throw error; + } +} + +/** + * 批量添加或更新聊天记录 + * @param {Array} messages - 消息对象数组 + * @param {number} talkType - 会话类型(1:私聊, 2:群聊) + * @param {number} receiverId - 接收者ID(私聊为对方用户ID,群聊为群ID) + * @param {boolean} replaceExisting - 是否替换已存在的消息 + * @param {string} sortKey - 排序和比较的键,可选值:'sequence'或'created_at' + * @returns {Promise<{added: number, updated: number, total: number}>} - 返回添加和更新的消息数量 + */ +export async function batchAddOrUpdateMessages(messages, talkType, receiverId, replaceExisting = true, sortKey = 'sequence') { + try { + if (!Array.isArray(messages) || messages.length === 0) { + return { added: 0, updated: 0, total: 0 }; + } + + // 获取现有消息 + let existingMessages = []; + if (replaceExisting) { + // 不传入 maxSequence 参数,获取所有消息 + existingMessages = await getMessages(talkType, null, receiverId, 1000, 0); + } + + // 创建msg_id到消息的映射,用于快速查找 + const existingMap = new Map(); + existingMessages.forEach(msg => { + existingMap.set(msg.msg_id, msg); + }); + + // 创建sequence/created_at到消息的映射,用于比较和替换 + const existingKeyMap = new Map(); + existingMessages.forEach(msg => { + if (msg[sortKey]) { + existingKeyMap.set(msg[sortKey], msg); + } + }); + + let added = 0; + let updated = 0; + + // 批量处理事务 + await db.transaction('rw', db.messages, async () => { + for (const message of messages) { + // 确保消息有必要的字段 + if (!message.msg_id) { + message.msg_id = generateUUID(); + } + + if (!message.created_at) { + message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19); + } + + // 检查消息是否已存在 + const existing = existingMap.get(message.msg_id); + if (existing) { + // 更新现有消息,使用 msg_id 而不是 id + await db.messages.update(existing.msg_id, message); + updated++; + } else if (replaceExisting && message[sortKey] && existingKeyMap.has(message[sortKey])) { + // 根据sortKey替换现有消息 + const existingByKey = existingKeyMap.get(message[sortKey]); + await db.messages.update(existingByKey.msg_id, message); + updated++; + } else { + console.log('message',message) + // 添加新消息 + await db.messages.add(message); + added++; + } + } + }); + + // 如果有新消息添加,更新会话的最后一条消息 + if (added > 0 || updated > 0) { + // 假设消息数组中的最后一条消息是最新的(保持原始顺序) + // 如果需要找出最新消息,可以使用数组中的最后一条消息 + const latestMessage = messages[messages.length - 1]; + + // 更新会话的最后一条消息 + await updateConversationLastMessage(latestMessage); + } + + return { added, updated, total: added + updated }; + } catch (error) { + console.error('批量添加或更新消息失败:', error); + throw error; + } +} + +/** + * 获取指定会话的聊天记录 + * @param {number} talkType - 会话类型(1:私聊, 2:群聊) + * @param {number} userId - 当前用户ID + * @param {number} receiverId - 接收者ID(私聊为对方用户ID,群聊为群ID) + * @param {number} limit - 限制返回的记录数量 + * @param {number} offset - 偏移量,用于分页 + * @returns {Promise} - 返回消息列表 + */ +/** + * 获取消息列表 + * @param {number} talkType - 会话类型(1:私聊, 2:群聊) + * @param {number} userId - 用户ID(私聊时用于区分消息方向) + * @param {number} receiverId - 接收者ID(私聊时为对方ID,群聊时为群ID) + * @param {number} limit - 每页条数 + * @param {number} offset - 偏移量 + * @param {number} maxSequence - 最大sequence值,用于分页加载更早的消息 + * @returns {Promise} - 消息列表 + */ +export async function getMessages(talkType, userId, receiverId, limit = 30, offset = 0, maxSequence = null) { + try { + let query; + + if (talkType === TalkType.PRIVATE) { + // 私聊消息(双向获取) + query = db.messages + .where('talk_type').equals(TalkType.PRIVATE) + .and(item => { + return (item.user_id === userId && item.receiver_id === receiverId) || + (item.user_id === receiverId && item.receiver_id === userId); + }); + } else { + // 群聊消息 + query = db.messages + .where('talk_type').equals(TalkType.GROUP) + .and(item => item.receiver_id === receiverId); + } + + // 如果提供了maxSequence,则只获取sequence小于该值的消息 + if (maxSequence !== null) { + query = query.and(item => item.sequence < maxSequence); + } + + // 按sequence升序排序 + let messages = await query + .sortBy('sequence'); + + // 获取尾部的limit条记录(最新的消息) + if (messages.length > limit) { + messages = messages.slice(Math.max(0, messages.length - limit)); + } + + return messages; + } catch (error) { + console.error('获取消息失败:', error); + throw error; + } +} + +/** + * 标记消息为已读 + * @param {string} msgId - 消息ID + * @returns {Promise} - 操作是否成功 + */ +export async function markMessageAsRead(msgId) { + try { + await db.messages.update(msgId, { is_read: 1 }); + return true; + } catch (error) { + console.error('标记消息已读失败:', error); + return false; + } +} + +/** + * 批量标记消息为已读 + * @param {number} talkType - 会话类型 + * @param {number} userId - 当前用户ID + * @param {number} receiverId - 接收者ID + * @returns {Promise} - 返回更新的消息数量 + */ +export async function markMessagesAsRead(talkType, userId, receiverId) { + try { + let count; + + if (talkType === TalkType.PRIVATE) { + // 私聊消息(只标记对方发给我的消息) + count = await db.messages + .where('talk_type').equals(TalkType.PRIVATE) + .and(item => item.user_id === receiverId && item.receiver_id === userId) + .and(item => item.is_read === 0) + .modify({ is_read: 1 }); + } else { + // 群聊消息(标记该群的所有未读消息) + count = await db.messages + .where('talk_type').equals(TalkType.GROUP) + .and(item => item.receiver_id === receiverId) + .and(item => item.is_read === 0 && item.user_id !== userId) + .modify({ is_read: 1 }); + } + + return count; + } catch (error) { + console.error('批量标记消息已读失败:', error); + throw error; + } +} + +/** + * 撤回消息 + * @param {string} msgId - 消息ID + * @returns {Promise} - 操作是否成功 + */ +export async function revokeMessage(msgId) { + try { + await db.messages.update(msgId, { is_revoke: 1 }); + return true; + } catch (error) { + console.error('撤回消息失败:', error); + return false; + } +} + +/** + * 删除消息 + * @param {string} msgId - 消息ID + * @returns {Promise} - 操作是否成功 + */ +export async function deleteMessage(msgId) { + try { + await db.messages.delete(msgId); + return true; + } catch (error) { + console.error('删除消息失败:', error); + return false; + } +} + +/** + * 获取未读消息数量 + * @param {number} userId - 当前用户ID + * @returns {Promise} - 返回未读消息统计信息 + */ +export async function getUnreadCount(userId) { + try { + // 私聊未读消息数量 + const privateUnread = await db.messages + .where('talk_type').equals(TalkType.PRIVATE) + .and(item => item.receiver_id === userId && item.is_read === 0) + .count(); + + // 群聊未读消息数量 + const groupUnread = await db.messages + .where('talk_type').equals(TalkType.GROUP) + .and(item => item.user_id !== userId && item.is_read === 0) + .count(); + + // 按会话分组的未读消息数量 + const privateUnreadBySession = await db.messages + .where('talk_type').equals(TalkType.PRIVATE) + .and(item => item.receiver_id === userId && item.is_read === 0) + .toArray() + .then(messages => { + const result = {}; + messages.forEach(msg => { + if (!result[msg.user_id]) { + result[msg.user_id] = 0; + } + result[msg.user_id]++; + }); + return result; + }); + + // 按群组分组的未读消息数量 + const groupUnreadBySession = await db.messages + .where('talk_type').equals(TalkType.GROUP) + .and(item => item.user_id !== userId && item.is_read === 0) + .toArray() + .then(messages => { + const result = {}; + messages.forEach(msg => { + if (!result[msg.receiver_id]) { + result[msg.receiver_id] = 0; + } + result[msg.receiver_id]++; + }); + return result; + }); + + return { + total: privateUnread + groupUnread, + private: privateUnread, + group: groupUnread, + privateBySession: privateUnreadBySession, + groupBySession: groupUnreadBySession + }; + } catch (error) { + console.error('获取未读消息数量失败:', error); + throw error; + } +} + +/** + * 生成UUID + * @returns {string} - 返回UUID + */ +function generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} + +/** + * 添加或更新会话 + * @param {Object} conversation - 会话对象 + * @returns {Promise} - 返回会话ID + */ +export async function addOrUpdateConversation(conversation) { + try { + // 检查会话是否已存在 + const existingConversation = await db.conversations + .where('index_name') + .equals(conversation.index_name) + .first(); + + if (existingConversation) { + // 更新现有会话 + await db.conversations.update(existingConversation.id, conversation); + return existingConversation.id; + } else { + // 添加新会话 + const id = await db.conversations.add(conversation); + return id; + } + } catch (error) { + console.error('添加或更新会话失败:', error); + throw error; + } +} + +/** + * 获取所有会话列表 + * @param {boolean} includeEmpty - 是否包含没有消息的会话 + * @returns {Promise} - 返回会话列表,按更新时间倒序排列 + */ +export async function getConversations(includeEmpty = false) { + try { + let query = db.conversations; + + if (!includeEmpty) { + // 只获取有消息的会话 + query = query.filter(item => item.msg_text && item.msg_text.length > 0); + } + + // 按更新时间倒序排列 + const conversations = await query + .toArray() + .then(items => items.sort((a, b) => { + // 置顶的会话优先 + if (a.is_top !== b.is_top) return b.is_top - a.is_top; + // 然后按更新时间排序 + return new Date(b.updated_at) - new Date(a.updated_at); + })); + + return conversations; + } catch (error) { + console.error('获取会话列表失败:', error); + throw error; + } +} + +/** + * 获取指定会话 + * @param {number} talkType - 会话类型(1:私聊, 2:群聊) + * @param {number} receiverId - 接收者ID(私聊为对方用户ID,群聊为群ID) + * @returns {Promise} - 返回会话对象,如果不存在则返回null + */ +export async function getConversation(talkType, receiverId) { + try { + const indexName = `${talkType}_${receiverId}`; + const conversation = await db.conversations + .where('index_name') + .equals(indexName) + .first(); + + return conversation || null; + } catch (error) { + console.error('获取会话失败:', error); + throw error; + } +} + +/** + * 更新会话的未读消息数 + * @param {number} talkType - 会话类型 + * @param {number} receiverId - 接收者ID + * @param {number} unreadNum - 未读消息数量,如果为null则增加1 + * @returns {Promise} - 操作是否成功 + */ +export async function updateConversationUnreadNum(talkType, receiverId, unreadNum = null) { + try { + const indexName = `${talkType}_${receiverId}`; + const conversation = await db.conversations + .where('index_name') + .equals(indexName) + .first(); + + if (conversation) { + if (unreadNum === null) { + // 增加未读消息数 + unreadNum = (conversation.unread_num || 0) + 1; + } + + await db.conversations.update(conversation.id, { unread_num: unreadNum }); + return true; + } + + return false; + } catch (error) { + console.error('更新会话未读消息数失败:', error); + return false; + } +} + +/** + * 清空会话的未读消息数 + * @param {number} talkType - 会话类型 + * @param {number} receiverId - 接收者ID + * @returns {Promise} - 操作是否成功 + */ +export async function clearConversationUnreadNum(talkType, receiverId) { + return updateConversationUnreadNum(talkType, receiverId, 0); +} + +/** + * 删除会话 + * @param {number} conversationId - 会话ID + * @param {boolean} deleteMessages - 是否同时删除相关的消息记录 + * @returns {Promise} - 操作是否成功 + */ +export async function deleteConversation(conversationId, deleteMessages = false) { + try { + const conversation = await db.conversations.get(conversationId); + + if (!conversation) return false; + + // 删除会话 + await db.conversations.delete(conversationId); + + // 如果需要,同时删除相关的消息记录 + if (deleteMessages) { + const { talk_type, receiver_id } = conversation; + + if (talk_type === TalkType.PRIVATE) { + // 删除私聊消息 + await db.messages + .where('talk_type').equals(TalkType.PRIVATE) + .and(item => { + return (item.user_id === receiver_id || item.receiver_id === receiver_id); + }) + .delete(); + } else { + // 删除群聊消息 + await db.messages + .where('talk_type').equals(TalkType.GROUP) + .and(item => item.receiver_id === receiver_id) + .delete(); + } + } + + return true; + } catch (error) { + console.error('删除会话失败:', error); + return false; + } +} + +/** + * 更新会话的最后一条消息 + * @param {Object} message - 消息对象 + * @returns {Promise} - 操作是否成功 + */ +export async function updateConversationLastMessage(message) { + try { + const { talk_type, user_id, receiver_id, msg_type } = message; + let targetReceiverId; + + if (talk_type === TalkType.PRIVATE) { + // 私聊:对方ID作为会话的receiver_id + targetReceiverId = user_id === receiver_id ? user_id : receiver_id; + } else { + // 群聊:群ID作为会话的receiver_id + targetReceiverId = receiver_id; + } + + const indexName = `${talk_type}_${targetReceiverId}`; + const conversation = await db.conversations + .where('index_name') + .equals(indexName) + .first(); + + if (!conversation) return false; + + // 根据消息类型生成显示文本 + let msgText = ''; + switch (msg_type) { + case MessageType.TEXT: + msgText = message.content || ''; + break; + case MessageType.IMAGE: + msgText = '[图片消息]'; + break; + case MessageType.FILE: + msgText = '[文件消息]'; + break; + case MessageType.AUDIO: + msgText = '[语音消息]'; + break; + case MessageType.VIDEO: + msgText = '[视频消息]'; + break; + case MessageType.LOCATION: + msgText = '[位置消息]'; + break; + case MessageType.CARD: + msgText = '[名片消息]'; + break; + default: + msgText = '[未知类型消息]'; + } + + // 更新会话的最后消息和时间 + await db.conversations.update(conversation.id, { + msg_text: msgText, + content: message.content || '', + updated_at: message.created_at + }); + + return true; + } catch (error) { + console.error('更新会话最后消息失败:', error); + return false; + } +} + +/** + * 示例:如何使用聊天记录数据库 + * + * // 1. 添加一条消息 + * const newMessage = { + * msg_id: '5a266eb831594b2b8be7c9fdf34986df', + * sequence: 184, + * talk_type: 1, // 私聊 + * msg_type: 4, // 语音消息 + * user_id: 1496, + * receiver_id: 1774, + * nickname: '王一峰', + * avatar: 'https://cdn-test.szjixun.cn/fonchain-main/dev/image/4692/oa/36b09c00-c2c3-453d-9aaf-ac41d47d564b.png', + * is_revoke: 0, + * is_mark: 0, + * is_read: 0, + * created_at: '2025-06-30 09:11:14', + * extra: { + * duration: 0, + * name: '', + * size: 15984, + * url: 'https://cdn-test.szjixun.cn/fonchain-main/test/file/default/fonchain-chat/2b595dbc-6a2b-498e-802a-880e93d1e45a.mp3' + * }, + * erp_user_id: 4692 + * }; + * + * addMessage(newMessage).then(msgId => { + * console.log('消息添加成功,ID:', msgId); + * }); + * + * // 2. 获取私聊消息列表 + * getMessages(TalkType.PRIVATE, 1496, 1774, 20, 0).then(messages => { + * console.log('获取到20条私聊消息:', messages); + * }); + * + * // 获取特定sequence之前的消息 + * getMessages(TalkType.PRIVATE, 1496, 1774, 20, 0, 1000).then(messages => { + * console.log('获取sequence小于1000的20条私聊消息:', messages); + * }); + * + * // 3. 标记消息为已读 + * markMessageAsRead('5a266eb831594b2b8be7c9fdf34986df').then(success => { + * console.log('标记消息已读:', success ? '成功' : '失败'); + * }); + * + * // 4. 批量标记会话消息为已读 + * markMessagesAsRead(TalkType.PRIVATE, 1496, 1774).then(count => { + * console.log(`成功标记 ${count} 条消息为已读`); + * }); + * + * // 5. 撤回消息 + * revokeMessage('5a266eb831594b2b8be7c9fdf34986df').then(success => { + * console.log('撤回消息:', success ? '成功' : '失败'); + * }); + * + * // 6. 获取未读消息数量 + * getUnreadCount(1496).then(result => { + * console.log('未读消息统计:', result); + * }); + * + * // 7. 添加或更新会话 + * const newConversation = { + * talk_type: 1, // 私聊 + * receiver_id: 1774, + * name: "周俊耀", + * remark: "", + * avatar: "https://e-cdn.fontree.cn/fonchain-main/prod/image/18248/avatar/a0b2bee7-947f-465a-986e-10a1b2b87032.png", + * is_disturb: 0, + * is_top: 1, + * is_online: 0, + * is_robot: 0, + * is_dismiss: 0, + * is_quit: 0, + * unread_num: 0, + * content: "......", + * draft_text: "", + * msg_text: "[语音消息]", + * index_name: "1_1774", + * updated_at: "2025-06-30 10:27:27", + * atsign_num: 0, + * erp_user_id: 3346, + * group_member_num: 0, + * group_type: 0 + * }; + * + * addOrUpdateConversation(newConversation).then(id => { + * console.log('会话添加或更新成功,ID:', id); + * }); + * + * // 8. 获取所有会话列表 + * getConversations().then(conversations => { + * console.log('获取所有会话列表:', conversations); + * }); + * + * // 9. 获取指定会话 + * getConversation(TalkType.PRIVATE, 1774).then(conversation => { + * console.log('获取指定会话:', conversation); + * }); + * + * // 10. 更新会话未读消息数 + * updateConversationUnreadNum(TalkType.PRIVATE, 1774, 5).then(success => { + * console.log('更新会话未读消息数:', success ? '成功' : '失败'); + * }); + * + * // 11. 清空会话未读消息数 + * clearConversationUnreadNum(TalkType.PRIVATE, 1774).then(success => { + * console.log('清空会话未读消息数:', success ? '成功' : '失败'); + * }); + * + * // 12. 更新会话最后一条消息 + * updateConversationLastMessage(newMessage).then(success => { + * console.log('更新会话最后一条消息:', success ? '成功' : '失败'); + * }); + * + * // 13. 删除会话 + * deleteConversation(5811, false).then(success => { + * console.log('删除会话:', success ? '成功' : '失败'); + * }); + * + * // 14. 批量添加或更新聊天记录 + * const messagesFromServer = [ + * { + * msg_id: 'server-msg-id-1', + * talk_type: TalkType.PRIVATE, + * msg_type: MessageType.TEXT, + * user_id: 1000, + * receiver_id: 2000, + * content: '你好,这是第一条消息', + * is_read: 1, + * sequence: 1, + * created_at: '2023-05-01 10:00:00' + * }, + * { + * msg_id: 'server-msg-id-2', + * talk_type: TalkType.PRIVATE, + * msg_type: MessageType.TEXT, + * user_id: 2000, + * receiver_id: 1000, + * content: '你好,这是回复消息', + * is_read: 0, + * sequence: 2, + * created_at: '2023-05-01 10:01:00' + * } + * ]; + * + * // 使用sequence作为排序和替换的键 + * batchAddOrUpdateMessages( + * messagesFromServer, + * TalkType.PRIVATE, + * 2000, // 接收者ID(私聊为对方用户ID,群聊为群ID) + * true, // 替换已存在的消息 + * 'sequence' // 使用sequence作为排序和替换的键 + * ).then(result => { + * console.log(`添加了${result.added}条新消息,更新了${result.updated}条消息`); + * }); + */ \ No newline at end of file diff --git a/src/views/message/inner/IndexContent.vue b/src/views/message/inner/IndexContent.vue index e2ad11e..bbfb77f 100644 --- a/src/views/message/inner/IndexContent.vue +++ b/src/views/message/inner/IndexContent.vue @@ -145,7 +145,6 @@ watch( if (talkParams.type !== 2) { ServeCheckFriend({ receiver_id: newValue.receiver_id, talk_type: 1 }).then((res) => { if (res?.code === 200) { - console.log(res, 'ress') isFriend.value = res.data.is_friend } }) diff --git a/src/views/message/inner/IndexSider.vue b/src/views/message/inner/IndexSider.vue index 6251080..afa8273 100644 --- a/src/views/message/inner/IndexSider.vue +++ b/src/views/message/inner/IndexSider.vue @@ -519,6 +519,7 @@ const items = computed((): ISession[] => { return [...topItems, ...normalItems] }) +setTimeout(()=>{console.log('items',items)},2000) watch( () => state.addressBookSearchNickName, (newValue, oldValue) => { @@ -592,6 +593,8 @@ const indexName = computed(() => dialogueStore.index_name) // 切换会话 const onTabTalk = (item: ISession, follow = false) => { +console.log('onTabTalk') +console.log('item.index_name === indexName.value',item.index_name === indexName.value) if (item.index_name === indexName.value) return searchKeyword.value = '' diff --git a/src/views/message/inner/panel/PanelContent.vue b/src/views/message/inner/panel/PanelContent.vue index 6508739..0cc7fc5 100644 --- a/src/views/message/inner/panel/PanelContent.vue +++ b/src/views/message/inner/panel/PanelContent.vue @@ -372,6 +372,7 @@ let noRefreshTimer: number | null = null watch( () => props, async (newProps) => { + console.log('监听props') await nextTick() // 生成当前会话的唯一标识 const newSessionKey = `${newProps.talk_type}_${newProps.receiver_id}` @@ -414,7 +415,6 @@ watch( }, 3000) return } - console.log('执行逻辑') onLoad( { receiver_id: newProps.receiver_id, @@ -424,7 +424,7 @@ watch( specialParams ? { specifiedMsg: specialParams } : undefined ) }, - { deep: true } + { deep: true,immediate:true } ) // onMounted(() => { @@ -609,25 +609,11 @@ const handleIntersection = (entries) => { watch( () => records.value, () => { - console.log() nextTick(() => { // 断开旧的观察者 if (observer) { observer.disconnect() } - // 创建原数组的副本进行遍历 -// const recordsCopy = [...dialogueStore.records]; -// for (const [y, iy] of dialogueStore.globalUploadList.entries()) { -// console.log('y',y) -// console.log('iy',iy) -// for (const [x, ix] of recordsCopy.entries()) { -// if(x.msg_id === y.pre_msg){ -// // 注意:这里的ix是原数组的索引,需要考虑已插入元素的偏移 -// dialogueStore.records.splice(ix + 1 + (dialogueStore.records.length - recordsCopy.length), 0, y); -// } -// } -// } -// console.log('dialogueStore.records',dialogueStore.records) // 重新初始化观察者 const options = { root: null, @@ -802,7 +788,7 @@ const onCustomSkipBottomEvent = () => {
正在加载数据中 ... 查看更多消息 ... - 没有更多消息了 + 没有更多消息了
{ ? dialogueStore.records[dialogueStore.records.length-1].sequence : 0, sequence: Date.now(), - talk_type: props.talk_type, + talk_type: props.talk_type, msg_type: 5, // 视频消息类型 user_id: props.uid, receiver_id: props.receiver_id, @@ -154,8 +154,7 @@ const onSendVideoEvent = async ({ data }) => { dialogueStore.updateUploadProgress(uploadId, percentage) }, async () => { - // 上传完成后,上传任务已经被removeUploadTask方法移除 - // 不需要再次从globalUploadList中移除 + dialogueStore.batchDelDialogueRecord([uploadId]) } ) } @@ -167,10 +166,6 @@ const onSendCodeEvent = ({ data, callBack }) => { // 发送文件消息 const onSendFileEvent = ({ data }) => { - let maxsize = 200 * 1024 * 1024 - if (data.size > maxsize) { - return window['$message'].warning('上传文件不能超过100M!') - } const clientUploadId = `file-${Date.now()}-${Math.floor(Math.random() * 1000)}` const tempMessage = { msg_id: clientUploadId,