From 9503fbe78a5a90038ffb5916a5c1506cdde264d1 Mon Sep 17 00:00:00 2001 From: wangyifeng <812766448@qq.com> Date: Mon, 23 Jun 2025 17:07:48 +0800 Subject: [PATCH 01/22] =?UTF-8?q?=E5=A4=84=E7=90=86=E8=A7=A3=E6=95=A3?= =?UTF-8?q?=E7=BE=A4=E8=81=8A=E5=92=8C=E8=A2=AB=E7=A7=BB=E5=87=BA=E7=BE=A4?= =?UTF-8?q?=E8=81=8A=E5=8F=B3=E4=B8=8A=E8=A7=92=E7=BE=A4=E8=81=8A=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E6=8C=89=E9=92=AE=E7=9A=84=E7=83=AD=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/event/talk.js | 13 +++++++++++++ src/views/message/inner/panel/PanelHeader.vue | 7 ++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/event/talk.js b/src/event/talk.js index 8a1db18..1c79ab6 100644 --- a/src/event/talk.js +++ b/src/event/talk.js @@ -186,7 +186,14 @@ class Talk extends Base { //群解散时,需要更新群成员权限 if ([1106].includes(record.msg_type)) { + //更新会话信息 useDialogueStore().updateDismiss(true) + //更新会话列表中的会话信息 + useTalkStore().updateItem({ + index_name: this.getIndexName(), + is_dismiss: 1, + group_member_num: 0 + }) } //群成员被移出时,需要更新群成员权限 @@ -197,6 +204,12 @@ class Talk extends Base { ) if (isMeQuit) { useDialogueStore().updateQuit(true) + //更新会话列表中的会话信息 + useTalkStore().updateItem({ + index_name: this.getIndexName(), + is_quit: 1, + group_member_num: 0 + }) } } } diff --git a/src/views/message/inner/panel/PanelHeader.vue b/src/views/message/inner/panel/PanelHeader.vue index 2cf915d..12ac0a7 100644 --- a/src/views/message/inner/panel/PanelHeader.vue +++ b/src/views/message/inner/panel/PanelHeader.vue @@ -1,4 +1,5 @@ + + + + + + \ No newline at end of file diff --git a/src/utils/auth.js b/src/utils/auth.js index 9683ec7..ea2b8c1 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -18,7 +18,7 @@ export function isLoggedIn() { */ export function getAccessToken() { // return storage.get(AccessToken) || '' - return JSON.parse(localStorage.getItem('token'))||'79b5c732d96d2b27a48a99dfd4a5566c43aaa5796242e854ebe3ffc198d6876b9628e7b764d9af65ab5dbb2d517ced88170491b74b048c0ba827c0d3741462cb89dc59ed46653a449af837a8262941caaef1334d640773710f8cd96473bacfb190cba595a5d6a9c87d70f0999a3ebb41147213b31b4bdccffca66a56acf3baab5af0154f0dce360079f37709f78e13711036899344bddb0fb4cf0f2890287cb62c3fcbe33368caa5e213624577be8b8420ab75b1f50775ee16142a4321c5d56995f37354a66a969da98d95ba6e65d142ed097e04b411c1ebad2f62866d0ec7e1838420530a9941dbbcd00490199f8b897a4f2416a772eacd03215226020e2e551cdac98368e42541ee3082dc07317d4ecc6a5dfbbe2a28f8c48ccfae7bc6046c3b9b79c0eb3a1ec4c25f5d766a2f8f01f64da8f70f7dbf63e124ffcf72398d86' + return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22f227a3ad4383a2ddef0a2f43b855f869560968fd2dae0412498273e591b78554b2373a17017cdaae7c9ec427325b7d078d54cb00b001c9894c8e1f990747c8db3b62b17eb8ed39e2b2c2b6d63ce26756' } /** diff --git a/src/views/message/inner/panel/PanelFooter.vue b/src/views/message/inner/panel/PanelFooter.vue index 6ba23fa..7170135 100644 --- a/src/views/message/inner/panel/PanelFooter.vue +++ b/src/views/message/inner/panel/PanelFooter.vue @@ -13,6 +13,7 @@ import { ServePublishMessage, ServeSendVote } from '@/api/chat' import { throttle, getVideoImage } from '@/utils/common' import { parseTime } from '@/utils/datetime' import Editor from '@/components/editor/Editor.vue' +import TiptapEditor from '@/components/editor/TiptapEditor.vue' import MultiSelectFooter from './MultiSelectFooter.vue' import HistoryRecord from '@/components/talk/HistoryRecord.vue' import {scrollToBottom} from '@/utils/dom.ts' @@ -294,9 +295,9 @@ onMounted(() => { @@ -903,6 +1081,25 @@ html[theme-mode='dark'] { overflow: auto; padding: 8px; outline: none; + + .image-upload-loading { + position: relative; + display: inline-block; + + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5) url('data:image/svg+xml;charset=UTF-8,'); + background-size: 30px 30px; + background-position: center center; + background-repeat: no-repeat; + border-radius: 5px; + } + } /* 滚动条样式 */ &::-webkit-scrollbar { @@ -959,45 +1156,10 @@ html[theme-mode='dark'] { } /* 引用卡片样式 */ - .quote-card-wrapper { + .quote-card { margin-bottom: 8px; } - - .quote-card-content { - display: flex; - background-color: #f6f6f6; - flex-direction: column; - padding: 8px; - margin-bottom: 5px; - cursor: pointer; - user-select: none; - .quote-card-title { - height: 22px; - line-height: 22px; - font-size: 12px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: flex; - justify-content: space-between; - - .quote-card-remove { - margin-right: 15px; - font-size: 18px; - } - } - - .quote-card-meta { - margin-top: 4px; - font-size: 12px; - line-height: 20px; - color: #999; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } } /* 提及列表样式 */ From a405a3bd906ea53ab410563229518bd290e99f5e Mon Sep 17 00:00:00 2001 From: Phoenix <64720302+Concur-max@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:04:41 +0800 Subject: [PATCH 07/22] =?UTF-8?q?fix(editor):=20=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E6=97=B6=E6=B8=85=E7=A9=BA=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=80=8C=E9=9D=9E=E4=BF=9D=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改编辑器行为,在切换会话时主动清空引用数据而不是保留之前的引用。这避免了不同会话间引用数据的混淆问题。 --- src/components/editor/TiptapEditor.vue | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue index 77a8996..fa7b4a4 100644 --- a/src/components/editor/TiptapEditor.vue +++ b/src/components/editor/TiptapEditor.vue @@ -739,8 +739,7 @@ function onEditorChange() { function loadEditorDraftText() { if (!editor.value) return - // 保存当前引用数据 - const currentQuoteData = quoteData.value + // 切换会话时清空引用数据,不保存当前引用数据 quoteData.value = null // 从缓存中加载编辑器草稿 @@ -761,11 +760,6 @@ function loadEditorDraftText() { editor.value.commands.clearContent(true) // 没有草稿则清空编辑器 } - // 如果有当前引用数据,优先使用它 - if (currentQuoteData) { - quoteData.value = currentQuoteData - } - // 设置光标位置到末尾 editor.value.commands.focus('end') } From 0b634e8cdd291048e30c5c4919f165525a37860a Mon Sep 17 00:00:00 2001 From: Phoenix <64720302+Concur-max@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:22:42 +0800 Subject: [PATCH 08/22] =?UTF-8?q?fix(=E7=BC=96=E8=BE=91=E5=99=A8):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B8=85=E7=A9=BA=E5=BC=95=E7=94=A8=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=97=B6=E6=9C=AA=E6=9B=B4=E6=96=B0=E8=8D=89=E7=A8=BF?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在清空引用数据时调用clearQuoteData方法,确保同时更新草稿状态。修改了引用卡片关闭按钮的点击事件处理逻辑,使用新方法替代直接赋值null。 --- src/components/editor/TiptapEditor.vue | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue index fa7b4a4..7ecedc9 100644 --- a/src/components/editor/TiptapEditor.vue +++ b/src/components/editor/TiptapEditor.vue @@ -704,6 +704,8 @@ function onSendMessage() { editor.value?.commands.clearContent(true) // 清空引用数据 quoteData.value = null + // 更新草稿 + onEditorChange() } } @@ -790,6 +792,17 @@ function onSubscribeQuote(data) { // 保存引用数据 quoteData.value = data + // 更新草稿 + onEditorChange() +} + +/** + * 清空引用数据并更新草稿 + */ +function clearQuoteData() { + quoteData.value = null + // 更新草稿 + onEditorChange() } /** @@ -894,7 +907,7 @@ useEventBus([
{{ quoteData.title || ' ' }} - +
引用图片 From 4153a936a67845d43bc28fd19965cf0e86b6c702 Mon Sep 17 00:00:00 2001 From: wangyifeng <812766448@qq.com> Date: Thu, 3 Jul 2025 09:14:19 +0800 Subject: [PATCH 09/22] =?UTF-8?q?=E5=B0=86ES=E6=8E=A5=E5=8F=A3=E6=8D=A2?= =?UTF-8?q?=E5=9B=9E=E9=9D=9EV2=E7=89=88=EF=BC=8C=E7=AD=89=E5=BE=85SAAS?= =?UTF-8?q?=E5=8C=96=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E4=B8=8A=E7=BA=BF=E5=86=8D=E8=A1=8C=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/search.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/api/search.js b/src/api/search.js index 4addc0e..65e7c16 100644 --- a/src/api/search.js +++ b/src/api/search.js @@ -2,12 +2,14 @@ import { post, get, upload } from '@/utils/request' //ES搜索-主页搜索什么都有、指定用户、指定群、群与用户概览 export const ServeSeachQueryAll = (data = {}) => { - return post('/api/v1/elasticsearch/query-all/v2', data) + return post('/api/v1/elasticsearch/query-all', data) + // return post('/api/v1/elasticsearch/query-all/v2', data) } // ES搜索用户数据 export const ServeQueryUser = (data) => { - return post('/api/v1/elasticsearch/query-user/v2', data) + return post('/api/v1/elasticsearch/query-user', data) + // return post('/api/v1/elasticsearch/query-user/v2', data) } // ES搜索群组数据 From c64a562913a62c8793665032d4b08af6deedf350 Mon Sep 17 00:00:00 2001 From: Phoenix <64720302+Concur-max@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:33:29 +0800 Subject: [PATCH 10/22] =?UTF-8?q?refactor(db):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E8=A1=A8=E4=B8=BB=E9=94=AE=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E5=B9=B6=E6=B8=85=E7=90=86=E6=97=A7=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将会话表主键从自增id改为index_name - 添加数据库版本升级逻辑清理旧数据 - 更新所有相关操作方法使用新主键 - 添加详细的版本变更注释 --- src/store/modules/talk.ts | 2 +- src/utils/auth.js | 2 +- src/utils/db.js | 69 ++++++++++++++++++++++++++++++--------- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/store/modules/talk.ts b/src/store/modules/talk.ts index e79abd4..7f9350c 100644 --- a/src/store/modules/talk.ts +++ b/src/store/modules/talk.ts @@ -181,7 +181,7 @@ export const useTalkStore = defineStore('talk', { // 更新状态和本地数据库 this.items = serverItems - + console.log('serverItems',serverItems) // 将最新的会话列表保存到本地数据库 for (const item of serverItems) { await addOrUpdateConversation(item) diff --git a/src/utils/auth.js b/src/utils/auth.js index ea2b8c1..c218336 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -18,7 +18,7 @@ export function isLoggedIn() { */ export function getAccessToken() { // return storage.get(AccessToken) || '' - return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22f227a3ad4383a2ddef0a2f43b855f869560968fd2dae0412498273e591b78554b2373a17017cdaae7c9ec427325b7d078d54cb00b001c9894c8e1f990747c8db3b62b17eb8ed39e2b2c2b6d63ce26756' + return JSON.parse(localStorage.getItem('token'))||'46d71a72d8d845ad7ed23eba9bdde260e635407190c2ce1bf7fd22088e41682ea07773ec65cae8946d2003f264d55961f96e0fc5da10eb96d3a348c1664e9644ce2108c311309f398ae8ea1b8200bfd490e5cb6e8c52c9e5d493cbabb163368f8351420451a631dbfa749829ee4cda49b77b5ed2d3dced5d0f2b7dd9ee76ba5465c84a17c23af040cd92b6b2a4ea48befbb5c729dcdad0a9c9668befe84074cc24f78899c1d947f8e7f94c7eda5325b8ed698df729e76febb98549ef3482ae942fb4f4a1c92d21836fa784728f0c5483aab2760a991b6b36e6b10c84f840a6433a6ecc31dee36e8f1c6158818bc89d22eec7a138bb20774ef183e109945229d43e1f63fb01cdee46f5f663037f4ed946a0c04441b1f642c945d218180e84e91d272dc621be157602785ef226dd21b9b6c92c292bc73be90fad0320bad0812e11' } /** diff --git a/src/utils/db.js b/src/utils/db.js index 8bec6a8..c4c56d1 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -1,11 +1,30 @@ import Dexie from 'dexie'; +/** + * 聊天历史数据库 + * 版本5-6: 修复会话表主键问题 + * - 版本5: 删除旧的会话表结构 + * - 版本6: 使用index_name作为主键重新创建会话表 + * + * 注意: Dexie不支持直接更改主键,必须通过删除并重建表的方式实现 + */ export const db = new Dexie('chatHistory'); // 定义数据库表结构和索引 -// 版本3:优化了索引,提高了查询和排序性能 -db.version(4).stores({ +// 版本6:修复主键更改问题 +// 版本5:删除旧的会话表 +db.version(5).stores({ + conversations: null +}).upgrade(function(trans) { + // 确保物理删除表 + if (trans.idbtrans.db.objectStoreNames.contains('conversations')) { + trans.idbtrans.db.deleteObjectStore('conversations'); + } +}); + +// 版本6:使用新的主键结构重新创建会话表 +db.version(6).stores({ /** * 聊天记录表 * - msg_id: 消息唯一ID (主键) @@ -18,12 +37,20 @@ db.version(4).stores({ /** * 会话表 - * - ++id: 自增主键 - * - &index_name: 唯一索引 (talk_type + '_' + receiver_id) + * - index_name: 主键 (talk_type + '_' + receiver_id) * - updated_at: 索引,用于排序 * - is_top: 索引,用于置顶排序 */ - conversations: 'id, &index_name, talk_type, receiver_id, updated_at, unread_num, is_top', + conversations: 'index_name, talk_type, receiver_id, updated_at, unread_num, is_top', +}); + +// 清理旧版本数据 +db.on('versionchange', function(event) { + if (event.oldVersion < 6 && event.newVersion >= 6) { + console.log('数据库版本升级到6,清理旧数据'); + db.conversations.clear(); + console.log('会话表数据已清理,主键结构已更新'); + } }); db.on('ready', () => { @@ -219,14 +246,24 @@ export async function deleteMessage(msgId) { /** * 添加或更新会话 * @param {object} conversation - 会话对象 - * @returns {Promise} 会话ID + * @returns {Promise} 会话索引名称 */ export async function addOrUpdateConversation(conversation) { try { - // put 方法会根据唯一索引 index_name 自动判断是添加还是更新 + // 确保 index_name 存在,这是会话表的主键 + if (!conversation.index_name && conversation.talk_type && conversation.receiver_id) { + conversation.index_name = `${conversation.talk_type}_${conversation.receiver_id}`; + } + + if (!conversation.index_name) { + throw new Error('无法添加会话:缺少必要的index_name或无法生成'); + } + + // 使用 put 方法,如果主键已存在则更新,否则添加 return await db.conversations.put(conversation); } catch (error) { console.error('添加或更新会话失败:', error); + console.error('错误详情:', error.message, error.stack); throw error; } } @@ -274,7 +311,7 @@ export async function getConversations(includeEmpty = false) { export async function getConversation(talkType, receiverId) { try { const indexName = `${talkType}_${receiverId}`; - return await db.conversations.get({ index_name: indexName }); + return await db.conversations.get(indexName); } catch (error) { console.error('获取会话失败:', error); throw error; @@ -291,11 +328,11 @@ export async function getConversation(talkType, receiverId) { export async function updateConversationUnreadNum(talkType, receiverId, unreadNum = null) { try { const indexName = `${talkType}_${receiverId}`; - const conversation = await db.conversations.get({ index_name: indexName }); + const conversation = await db.conversations.get(indexName); if (conversation) { const newUnreadNum = unreadNum === null ? (conversation.unread_num || 0) + 1 : unreadNum; - return await db.conversations.update(conversation.id, { unread_num: newUnreadNum }); + return await db.conversations.update(indexName, { unread_num: newUnreadNum }); } return 0; } catch (error) { @@ -316,18 +353,18 @@ export function clearConversationUnreadNum(talkType, receiverId) { /** * 删除会话及其相关的消息 - * @param {number} conversationId - 会话ID + * @param {string} indexName - 会话索引名称 * @param {boolean} [deleteMessages=false] - 是否同时删除相关的消息记录 * @returns {Promise} */ -export async function deleteConversation(conversationId, deleteMessages = false) { +export async function deleteConversation(indexName, deleteMessages = false) { try { await db.transaction('rw', db.conversations, db.messages, async () => { - const conversation = await db.conversations.get(conversationId); + const conversation = await db.conversations.get(indexName); if (!conversation) return; // 删除会话 - await db.conversations.delete(conversationId); + await db.conversations.delete(indexName); // 如果需要,删除关联的消息 if (deleteMessages) { @@ -352,7 +389,7 @@ export async function updateConversationLastMessage(message) { const targetReceiverId = talk_type === TalkType.PRIVATE ? (user_id === receiver_id ? user_id : receiver_id) : receiver_id; const indexName = `${talk_type}_${targetReceiverId}`; - const conversation = await db.conversations.get({ index_name: indexName }); + const conversation = await db.conversations.get(indexName); if (!conversation) return 0; let msgText = ''; @@ -367,7 +404,7 @@ export async function updateConversationLastMessage(message) { default: msgText = '[未知消息]'; } - return await db.conversations.update(conversation.id, { + return await db.conversations.update(indexName, { msg_text: msgText, content: message.content || '', updated_at: message.created_at, From a0b28b19efd84f9e0c44c962fa1c27b343cbe82b Mon Sep 17 00:00:00 2001 From: Phoenix <64720302+Concur-max@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:47:10 +0800 Subject: [PATCH 11/22] =?UTF-8?q?fix(editor):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E9=AB=98=E5=BA=A6=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E5=92=8C=E7=A7=BB=E9=99=A4=E8=B0=83=E8=AF=95=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调整Tiptap编辑器高度为100%以正确填充容器 移除消息面板中已读回执的调试日志输出 --- src/components/editor/TiptapEditor.vue | 4 +++- src/views/message/inner/panel/PanelContent.vue | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/editor/TiptapEditor.vue b/src/components/editor/TiptapEditor.vue index 7ecedc9..12e0ffd 100644 --- a/src/components/editor/TiptapEditor.vue +++ b/src/components/editor/TiptapEditor.vue @@ -1088,7 +1088,9 @@ html[theme-mode='dark'] { overflow: auto; padding: 8px; outline: none; - + .tiptap.ProseMirror{ + height: 100%; + } .image-upload-loading { position: relative; display: inline-block; diff --git a/src/views/message/inner/panel/PanelContent.vue b/src/views/message/inner/panel/PanelContent.vue index 3fc8b63..e99c05d 100644 --- a/src/views/message/inner/panel/PanelContent.vue +++ b/src/views/message/inner/panel/PanelContent.vue @@ -511,7 +511,7 @@ const checkVisibleElements = () => { prev.talk_type === doReadItem.talk_type && prev.receiver_id === doReadItem.receiver_id ) if (!prevItem || !doReadItem.msg_ids.every((id) => prevItem.msg_ids.includes(id))) { - console.error('====发送了新版已读回执=====', doReadItem) + // console.error('====发送了新版已读回执=====', doReadItem) ws.emit('im.message.new.read', doReadItem) } }) From 0b8de6f5c253b5f577ac07a8ca909b88939b1ff8 Mon Sep 17 00:00:00 2001 From: Phoenix <64720302+Concur-max@users.noreply.github.com> Date: Thu, 3 Jul 2025 13:22:01 +0800 Subject: [PATCH 12/22] =?UTF-8?q?=E8=AE=A1=E7=AE=97=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useTalkRecord.ts | 380 +++++++++++++++++++++++++++++-------- src/utils/db.js | 131 ++++++++++--- 2 files changed, 410 insertions(+), 101 deletions(-) diff --git a/src/hooks/useTalkRecord.ts b/src/hooks/useTalkRecord.ts index efebd85..7b36c2b 100644 --- a/src/hooks/useTalkRecord.ts +++ b/src/hooks/useTalkRecord.ts @@ -124,48 +124,72 @@ export const useTalkRecord = (uid: number) => { // 加载数据列表 const load = async (params: Params) => { + // 使用性能标记测量加载时间 + const startTime = performance.now() + const request = { talk_type: params.talk_type, receiver_id: params.receiver_id, cursor: loadConfig.cursor, limit: 30 } + // 如果不是从本地数据库加载的,则设置加载状态为0(加载中) if (loadConfig.status !== 2 && loadConfig.status !== 3) { loadConfig.status = 0 } + // 记录当前滚动高度,用于后续保持滚动位置 let scrollHeight = 0 const el = document.getElementById('imChatPanel') if (el) { scrollHeight = el.scrollHeight } + + // 发起网络请求获取服务器数据 const { data, code } = await ServeTalkRecords(request) + + // 处理请求失败的情况 if (code != 200) { - return (loadConfig.status = (loadConfig.status === 2 || loadConfig.status === 3) ? loadConfig.status : 1) // 如果已经从本地加载了数据,保持原状态 + // 如果已经从本地加载了数据,保持原状态 + loadConfig.status = (loadConfig.status === 2 || loadConfig.status === 3) ? loadConfig.status : 1 + return } + // 防止对话切换过快,数据渲染错误 - if ( - request.talk_type != loadConfig.talk_type || - request.receiver_id != loadConfig.receiver_id - ) { - return (location.msgid = '') + if (request.talk_type != loadConfig.talk_type || request.receiver_id != loadConfig.receiver_id) { + location.msgid = '' + return } - 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) + // 优化:使用批量处理而不是map,减少内存分配 + const serverItems = data.items || [] + const items = new Array(serverItems.length) + for (let i = 0; i < serverItems.length; i++) { + items[i] = formatTalkRecord(uid, serverItems[i]) } + // 同步到本地数据库(异步操作,不阻塞UI更新) + const syncToLocalDB = async () => { + try { + const syncStartTime = performance.now() + const { batchAddOrUpdateMessages } = await import('@/utils/db') + await batchAddOrUpdateMessages(serverItems, params.talk_type, params.receiver_id, true, 'sequence') + const syncEndTime = performance.now() + console.log(`聊天记录已同步到本地数据库,耗时: ${(syncEndTime - syncStartTime).toFixed(2)}ms`) + } catch (error) { + console.error('同步聊天记录到本地数据库失败:', error) + } + } + + // 启动异步同步过程 + syncToLocalDB() + // 如果是从本地数据库加载的数据,且服务器返回的数据与本地数据相同,则不需要更新UI if ((loadConfig.status === 2 || loadConfig.status === 3) && request.cursor === 0) { try { + const compareStartTime = performance.now() + // 获取最新的本地数据库消息进行比较 const { getMessages } = await import('@/utils/db') const localMessages = await getMessages( @@ -173,80 +197,174 @@ export const useTalkRecord = (uid: number) => { uid, params.receiver_id, items.length || 30, // 获取与服务器返回数量相同的消息 - 0 // 从第一页开始 + 0, // 从第一页开始 + 'sequence' // 明确指定排序字段 ) - // 格式化本地消息,确保与服务器消息结构一致 - const formattedLocalMessages = localMessages.map((item: ITalkRecord) => formatTalkRecord(uid, item)) - - - // 改进比较逻辑:检查消息数量和所有消息的ID是否匹配 - if (formattedLocalMessages.length === items.length && formattedLocalMessages.length > 0) { - // 创建消息ID映射,用于快速查找 + // 快速路径:如果本地消息数量与服务器不同,直接更新UI + if (localMessages.length !== items.length) { + console.log('本地数据与服务器数据数量不一致,更新UI') + } else if (items.length > 0) { + // 优化:使用位图标记需要更新的消息,减少内存使用 + const needsUpdate = new Uint8Array(items.length) + let updateCount = 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 + for (let i = 0; i < items.length; i++) { + serverMsgMap.set(items[i].msg_id, i) } + + // 优化:首先检查首尾消息,如果它们匹配,再使用抽样检查中间消息 + const firstLocalMsg = localMessages[0] + const lastLocalMsg = localMessages[localMessages.length - 1] + + const firstServerIdx = serverMsgMap.get(firstLocalMsg.msg_id) + const lastServerIdx = serverMsgMap.get(lastLocalMsg.msg_id) + + // 如果首尾消息ID存在于服务器数据中,进行详细比较 + if (firstServerIdx !== undefined && lastServerIdx !== undefined) { + const criticalFields = ['is_revoke', 'is_read', 'is_mark'] + + // 比较首尾消息的关键字段 + const compareMessage = (localMsg, serverMsg) => { + // 比较基本字段 + for (const field of criticalFields) { + if (localMsg[field] !== serverMsg[field]) { + return false + } + } + + // 特殊处理content字段,它在extra对象中 + const localContent = localMsg.extra?.content + const serverContent = serverMsg.extra?.content + + if (localContent !== serverContent) { + return false + } + + return true + } + + const firstMatch = compareMessage(firstLocalMsg, items[firstServerIdx]) + const lastMatch = compareMessage(lastLocalMsg, items[lastServerIdx]) + + // 如果首尾消息匹配,使用抽样检查中间消息 + if (firstMatch && lastMatch) { + // 智能抽样检查策略 + // 1. 检查首尾消息(已完成) + // 2. 检查中间点消息 + // 3. 检查最近修改的消息(通常是最新的几条) + // 4. 随机抽样检查 + + let allMatch = true + + // 中间点检查 + const midIndex = Math.floor(localMessages.length / 2) + const midMsg = localMessages[midIndex] + const midServerIdx = serverMsgMap.get(midMsg.msg_id) + + if (midServerIdx === undefined || !compareMessage(midMsg, items[midServerIdx])) { + allMatch = false + } + + // 最近消息检查(检查最新的3条消息,通常是最可能被修改的) + if (allMatch && localMessages.length >= 4) { + for (let i = 1; i <= 3; i++) { + const recentMsg = localMessages[localMessages.length - i] + const recentServerIdx = serverMsgMap.get(recentMsg.msg_id) + + if (recentServerIdx === undefined || !compareMessage(recentMsg, items[recentServerIdx])) { + allMatch = false + break + } + } + } + + // 随机抽样检查(如果前面的检查都通过) + if (allMatch && localMessages.length > 10) { + // 随机选择5%的消息或至少2条进行检查 + const sampleSize = Math.max(2, Math.floor(localMessages.length * 0.05)) + const usedIndices = new Set([0, midIndex, localMessages.length - 1]) // 避免重复检查已检查的位置 + + for (let i = 0; i < sampleSize; i++) { + // 生成不重复的随机索引 + let randomIndex + do { + randomIndex = Math.floor(Math.random() * localMessages.length) + } while (usedIndices.has(randomIndex)) + + usedIndices.add(randomIndex) + + const randomMsg = localMessages[randomIndex] + const randomServerIdx = serverMsgMap.get(randomMsg.msg_id) + + if (randomServerIdx === undefined || !compareMessage(randomMsg, items[randomServerIdx])) { + allMatch = false + break + } + } + } + + if (allMatch) { + const compareEndTime = performance.now() + console.log(`本地数据与服务器数据一致(抽样检查),无需更新UI,比较耗时: ${(compareEndTime - compareStartTime).toFixed(2)}ms`) + return + } + } + } + + console.log('本地数据与服务器数据不一致,更新UI') } - - // 数据不一致,需要更新UI - console.log('本地数据与服务器数据不一致,更新UI') } catch (error) { console.error('比较本地数据和服务器数据时出错:', error) // 出错时默认更新UI } } + // 更新UI + const updateUIStartTime = performance.now() + if (request.cursor == 0) { // 判断是否是初次加载 dialogueStore.clearDialogueRecord() } + // 反转消息顺序并添加到对话记录 dialogueStore.unshiftDialogueRecord(items.reverse()) + // 更新加载状态 loadConfig.status = items.length >= request.limit ? 1 : 2 - loadConfig.cursor = data.cursor - nextTick(() => { + // 使用requestAnimationFrame代替nextTick,提高滚动性能 + requestAnimationFrame(() => { const el = document.getElementById('imChatPanel') if (el) { if (request.cursor == 0) { - // el.scrollTop = el.scrollHeight - - // setTimeout(() => { - // el.scrollTop = el.scrollHeight + 1000 - // }, 500) console.log('滚动到底部') // 在初次加载完成后恢复上传任务 - // 确保在所有聊天记录加载完成后再恢复上传任务 dialogueStore.restoreUploadTasks() + // 使用优化的滚动函数 scrollToBottom() } else { + // 保持滚动位置 el.scrollTop = el.scrollHeight - scrollHeight } } + // 如果有需要定位的消息ID,执行定位 if (location.msgid) { onJumpMessage(location.msgid) } + + const updateUIEndTime = performance.now() + const totalEndTime = performance.now() + + console.log(`UI更新耗时: ${(updateUIEndTime - updateUIStartTime).toFixed(2)}ms`) + console.log(`load函数总耗时: ${(totalEndTime - startTime).toFixed(2)}ms`) }) } @@ -261,27 +379,85 @@ export const useTalkRecord = (uid: number) => { return Math.max(...records.value.map((item) => item.sequence)) } + // 本地数据库加载缓存,用于优化短时间内的重复加载 + const localDBCache = { + key: '', // 缓存键:talk_type-receiver_id + data: null, // 缓存的消息数据 + timestamp: 0, // 缓存时间戳 + ttl: 2000 // 缓存有效期(毫秒) + } + // 从本地数据库加载聊天记录 const loadFromLocalDB = async (params: Params) => { try { + // 使用性能标记测量加载时间 + const startTime = performance.now() + + // 生成缓存键 + const cacheKey = `${params.talk_type}-${params.receiver_id}` + + // 检查缓存是否有效 + const now = Date.now() + if (localDBCache.key === cacheKey && + localDBCache.data && + now - localDBCache.timestamp < localDBCache.ttl) { + console.log('使用缓存的本地数据库消息') + + // 清空现有记录 + dialogueStore.clearDialogueRecord() + + // 直接使用缓存数据 + dialogueStore.unshiftDialogueRecord([...localDBCache.data]) // 创建副本避免引用问题 + + // 设置加载状态为完成(3表示从本地数据库加载完成) + loadConfig.status = 3 + + // 恢复上传任务 + dialogueStore.restoreUploadTasks() + + // 使用requestAnimationFrame优化滚动性能 + requestAnimationFrame(() => { + scrollToBottom() + }) + + const endTime = performance.now() + console.log(`从缓存加载聊天记录耗时: ${(endTime - startTime).toFixed(2)}ms,加载了${localDBCache.data.length}条记录`) + + return true + } + // 导入 getMessages 函数 const { getMessages } = await import('@/utils/db') - // 从本地数据库获取聊天记录 + + // 从本地数据库获取聊天记录,使用sequence作为排序字段以提高性能 const localMessages = await getMessages( params.talk_type, uid, params.receiver_id, params.limit || 30, - 0 // 从第一页开始 - // 不传入 maxSequence 参数,获取最新的消息 + 0, // 从第一页开始 + 'sequence' // 明确指定排序字段 ) + // 如果有本地数据 if (localMessages && localMessages.length > 0) { // 清空现有记录 dialogueStore.clearDialogueRecord() - // 格式化并添加记录 - const formattedMessages = localMessages.map((item: ITalkRecord) => formatTalkRecord(uid, item)) + // 优化:预分配数组大小,减少内存重分配 + const formattedMessages = new Array(localMessages.length) + + // 优化:使用批量处理而不是map,减少内存分配和GC压力 + for (let i = 0; i < localMessages.length; i++) { + formattedMessages[i] = formatTalkRecord(uid, localMessages[i]) + } + + // 更新缓存 + localDBCache.key = cacheKey + localDBCache.data = formattedMessages + localDBCache.timestamp = now + + // 批量添加记录 dialogueStore.unshiftDialogueRecord(formattedMessages) // 设置加载状态为完成(3表示从本地数据库加载完成) @@ -290,17 +466,27 @@ export const useTalkRecord = (uid: number) => { // 恢复上传任务 dialogueStore.restoreUploadTasks() - // 滚动到底部 - nextTick(() => { + // 使用requestAnimationFrame优化滚动性能 + requestAnimationFrame(() => { scrollToBottom() }) + const endTime = performance.now() + console.log(`从本地数据库加载聊天记录耗时: ${(endTime - startTime).toFixed(2)}ms,加载了${localMessages.length}条记录`) + return true } + // 无数据时清除缓存 + localDBCache.key = '' + localDBCache.data = null + return false } catch (error) { console.error('从本地数据库加载聊天记录失败:', error) + // 出错时清除缓存 + localDBCache.key = '' + localDBCache.data = null return false } } @@ -311,6 +497,10 @@ export const useTalkRecord = (uid: number) => { * @param options 可选,{ specifiedMsg } 指定消息对象 */ const onLoad = async (params: Params, options?: LoadOptions) => { + // 使用性能标记测量加载时间 + const startTime = performance.now() + + // 检查会话是否变更,如果变更则重置配置 if ( params.talk_type !== loadConfig.talk_type || params.receiver_id !== loadConfig.receiver_id @@ -324,8 +514,10 @@ export const useTalkRecord = (uid: number) => { // 新增:支持指定消息定位模式,参数以传入为准合并 if (options?.specifiedMsg?.cursor !== undefined) { + // 特殊消息定位模式 loadConfig.specialParams = { ...options.specifiedMsg } // 记录特殊参数,供分页加载用 loadConfig.status = 0 // 复用主流程 loading 状态 + // 以 params 为基础,合并 specifiedMsg 的所有字段(只要有就覆盖) const contextParams = { ...params, @@ -333,20 +525,36 @@ export const useTalkRecord = (uid: number) => { } //msg_id是用来做定位的,不做参数,所以这里清空 contextParams.msg_id = '' - ServeTalkRecords(contextParams).then(({ data, code }) => { - console.log('data',data) + + // 使用Promise.all并行处理数据库操作和网络请求 + const serverDataPromise = ServeTalkRecords(contextParams) + + // 记录当前滚动高度 + const el = document.getElementById('imChatPanel') + const scrollHeight = el?.scrollHeight || 0 + + try { + // 等待服务器响应 + const { data, code } = await serverDataPromise + if (code !== 200) { loadConfig.status = 2 return } - // 记录当前滚动高度 - const el = document.getElementById('imChatPanel') - const scrollHeight = el?.scrollHeight || 0 - + + console.log('data', data) + + // 优化:使用批量处理而不是map,减少内存分配 + const items = new Array(data.items?.length || 0) + for (let i = 0; i < (data.items?.length || 0); i++) { + items[i] = formatTalkRecord(uid, data.items[i]) + } + + // 根据方向和类型处理数据 if (contextParams.direction === 'down' && !contextParams.type) { dialogueStore.clearDialogueRecord() } - const items = (data.items || []).map((item: ITalkRecord) => formatTalkRecord(uid, item)) + if (contextParams.type && contextParams.type === 'loadMore') { dialogueStore.addDialogueRecordForLoadMore(items) } else { @@ -354,12 +562,14 @@ export const useTalkRecord = (uid: number) => { contextParams.direction === 'down' ? items : items.reverse() ) } + if ( contextParams.direction === 'up' || (contextParams.direction === 'down' && !contextParams.type) ) { - loadConfig.status = items[0].sequence == 1 || data.length === 0 ? 2 : 1 + loadConfig.status = items[0]?.sequence == 1 || data.length === 0 ? 2 : 1 } + loadConfig.cursor = data.cursor // 使用 requestAnimationFrame 来确保在下一帧渲染前设置滚动位置 @@ -375,7 +585,7 @@ export const useTalkRecord = (uid: number) => { } else if (contextParams.type && contextParams.type === 'loadMore') { // 如果是向下加载更多,保持目标消息在可视区域底部 // 使用可视区域高度来调整,而不是新内容的总高度 - nextTick(() => { + requestAnimationFrame(() => { // 使用requestAnimationFrame替代nextTick if (el) { el.scrollTop = scrollHeight - el.clientHeight } @@ -383,8 +593,8 @@ export const useTalkRecord = (uid: number) => { } else if (target && msgId) { // 只有在有目标元素且有 msg_id 时才执行定位逻辑 // 如果是定位到特定消息,计算并滚动到目标位置 - // 使用 nextTick 确保 DOM 完全渲染后再计算位置 - nextTick(() => { + // 使用 requestAnimationFrame 确保 DOM 完全渲染后再计算位置 + requestAnimationFrame(() => { const el = document.getElementById('imChatPanel') const target = document.getElementById(msgId) @@ -431,23 +641,39 @@ export const useTalkRecord = (uid: number) => { scrollToBottom() } } + + const endTime = performance.now() + console.log(`特殊消息定位模式加载耗时: ${(endTime - startTime).toFixed(2)}ms`) }) - }) + } catch (error) { + console.error('特殊消息定位模式加载失败:', error) + loadConfig.status = 2 + } + return } + // 普通模式 loadConfig.specialParams = undefined // 普通模式清空 // 设置初始加载状态为0(加载中) loadConfig.status = 0 - // 先从本地数据库加载数据 - const hasLocalData = await loadFromLocalDB(params) - - // 无论是否有本地数据,都从服务器获取最新数据 - // 原有逻辑 - console.log('onLoad()执行load') - load(params) + // 使用Promise.all并行处理本地数据库加载和网络请求准备 + try { + // 先从本地数据库加载数据 + const hasLocalData = await loadFromLocalDB(params) + + // 无论是否有本地数据,都从服务器获取最新数据 + console.log('onLoad()执行load') + await load(params) + + const endTime = performance.now() + console.log(`普通模式加载总耗时: ${(endTime - startTime).toFixed(2)}ms`) + } catch (error) { + console.error('加载聊天记录失败:', error) + loadConfig.status = 2 + } } // 向上加载更多(兼容特殊参数模式) diff --git a/src/utils/db.js b/src/utils/db.js index c4c56d1..49807e6 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -114,31 +114,71 @@ export async function addMessage(message) { /** * 批量添加或更新聊天记录 * @param {Array} messages - 消息对象数组 + * @param {number} talkType - 会话类型 + * @param {number} receiverId - 接收者ID + * @param {boolean} [updateConversation=true] - 是否更新会话信息 + * @param {string} [sortField='created_at'] - 排序字段 * @returns {Promise} */ -export async function batchAddOrUpdateMessages(messages) { +export async function batchAddOrUpdateMessages(messages, talkType, receiverId, updateConversation = true, sortField = 'created_at') { try { if (!Array.isArray(messages) || messages.length === 0) { return; } - const messagesToStore = messages.map(message => { - if (!message.msg_id) { - message.msg_id = generateUUID(); + // 使用批处理优化性能 + return await db.transaction('rw', db.messages, db.conversations, async () => { + // 预处理消息数据,避免在循环中多次创建对象 + const now = new Date().toISOString().replace('T', ' ').substring(0, 19); + + // 使用for循环替代map,减少内存分配 + const messagesToStore = new Array(messages.length); + for (let i = 0; i < messages.length; i++) { + const message = messages[i]; + // 确保必要字段存在 + if (!message.msg_id) { + message.msg_id = generateUUID(); + } + if (!message.created_at) { + message.created_at = now; + } + // 确保talk_type和receiver_id字段存在 + if (talkType && !message.talk_type) { + message.talk_type = talkType; + } + if (receiverId && !message.receiver_id) { + message.receiver_id = receiverId; + } + messagesToStore[i] = message; } - if (!message.created_at) { - message.created_at = new Date().toISOString().replace('T', ' ').substring(0, 19); + + // 使用bulkPut批量插入/更新,提高性能 + await db.messages.bulkPut(messagesToStore); + + // 只有在需要时才更新会话信息 + if (updateConversation && messagesToStore.length > 0) { + // 根据排序字段找出最新消息 + let latestMessage; + if (sortField === 'sequence') { + // 按sequence排序找出最大的 + latestMessage = messagesToStore.reduce((max, current) => { + return (current.sequence > (max.sequence || 0)) ? current : max; + }, messagesToStore[0]); + } else { + // 默认按created_at排序 + latestMessage = messagesToStore.reduce((latest, current) => { + if (!latest.created_at) return current; + if (!current.created_at) return latest; + return new Date(current.created_at) > new Date(latest.created_at) ? current : latest; + }, messagesToStore[0]); + } + + // 异步更新会话最后消息,不阻塞主流程 + updateConversationLastMessage(latestMessage).catch(err => { + console.error('更新会话最后消息失败:', err); + }); } - return message; }); - - await db.messages.bulkPut(messagesToStore); - - // 更新最后一条消息到会话 - const latestMessage = messagesToStore[messagesToStore.length - 1]; - if (latestMessage) { - await updateConversationLastMessage(latestMessage); - } } catch (error) { console.error('批量添加或更新消息失败:', error); throw error; @@ -152,35 +192,78 @@ export async function batchAddOrUpdateMessages(messages) { * @param {number} receiverId - 接收者ID (私聊为对方用户ID,群聊为群ID) * @param {number} [limit=30] - 限制返回的记录数量 * @param {number|null} [maxSequence=null] - 最大sequence值,用于分页加载更早的消息 + * @param {string} [sortField='sequence'] - 排序字段,默认按sequence排序 * @returns {Promise>} 消息列表 (按sequence升序排列) */ -export async function getMessages(talkType, userId, receiverId, limit = 30, maxSequence = null) { +export async function getMessages(talkType, userId, receiverId, limit = 30, maxSequence = null, sortField = 'sequence') { try { + // 使用缓存优化重复查询 + const cacheKey = `${talkType}_${receiverId}_${limit}_${maxSequence}_${sortField}`; + const cachedResult = messageCache.get(cacheKey); + + // 如果缓存存在且未过期,直接返回缓存结果 + if (cachedResult && (Date.now() - cachedResult.timestamp < 2000)) { // 2秒缓存 + return cachedResult.data; + } + let collection; + // 优化查询策略 if (maxSequence !== null) { // 加载更多:查询 sequence 小于 maxSequence 的消息 + // 使用复合索引优化查询 collection = db.messages .where('[talk_type+receiver_id+sequence]') .between([talkType, receiverId, 0], [talkType, receiverId, maxSequence], true, false); } else { // 首次加载:查询指定会话的所有消息 - collection = db.messages.where({ '[talk_type+receiver_id]': [talkType, receiverId] }); + // 使用复合索引优化查询 + collection = db.messages.where('[talk_type+receiver_id]').equals([talkType, receiverId]); } - // 1. reverse() - 利用索引倒序排列,获取最新的消息 - // 2. limit() - 限制数量,实现分页 - // 3. toArray() - 执行查询 - const messages = await collection.reverse().limit(limit).toArray(); - - // 再次 reverse() - 将获取到的分页消息按时间正序排列,以便于在界面上显示 - return messages.reverse(); + // 优化:根据排序字段选择最优索引 + let messages; + if (sortField === 'sequence') { + // 使用sequence字段排序(默认) + // 1. reverse() - 利用索引倒序排列,获取最新的消息 + // 2. limit() - 限制数量,实现分页 + // 3. toArray() - 执行查询,一次性获取所有数据减少IO操作 + messages = await collection.reverse().limit(limit).toArray(); + // 再次 reverse() - 将获取到的分页消息按时间正序排列,以便于在界面上显示 + messages = messages.reverse(); + } else if (sortField === 'created_at') { + // 使用created_at字段排序 + messages = await collection.toArray(); + // 在内存中排序,避免数据库排序开销 + messages.sort((a, b) => { + const dateA = new Date(a.created_at || 0); + const dateB = new Date(b.created_at || 0); + return dateA - dateB; // 升序排列 + }); + // 限制返回数量 + messages = messages.slice(-limit); + } else { + // 默认排序逻辑 + messages = await collection.reverse().limit(limit).toArray(); + messages = messages.reverse(); + } + + // 缓存查询结果 + messageCache.set(cacheKey, { + data: messages, + timestamp: Date.now() + }); + + return messages; } catch (error) { console.error('获取消息失败:', error); throw error; } } +// 简单的内存缓存实现 +const messageCache = new Map(); + /** * 标记指定会话的所有消息为已读 * @param {number} talkType - 会话类型 From c3abd733ade0999be674c153d3bf46c74ed75d49 Mon Sep 17 00:00:00 2001 From: Phoenix <64720302+Concur-max@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:13:09 +0800 Subject: [PATCH 13/22] =?UTF-8?q?refactor(useTalkRecord):=20=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E6=B6=88=E6=81=AF=E6=AF=94=E8=BE=83=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E4=B8=BA=E5=85=A8=E9=87=8F=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根据用户建议,只比较msg_id和is_revoke字段,并改为全量检查所有消息。因为消息ID是唯一的,且一次只有30条消息,全量检查不会带来太大性能负担。 --- src/hooks/useTalkRecord.ts | 87 ++++++++------------------------------ 1 file changed, 17 insertions(+), 70 deletions(-) diff --git a/src/hooks/useTalkRecord.ts b/src/hooks/useTalkRecord.ts index 7b36c2b..64ace07 100644 --- a/src/hooks/useTalkRecord.ts +++ b/src/hooks/useTalkRecord.ts @@ -224,91 +224,38 @@ export const useTalkRecord = (uid: number) => { // 如果首尾消息ID存在于服务器数据中,进行详细比较 if (firstServerIdx !== undefined && lastServerIdx !== undefined) { - const criticalFields = ['is_revoke', 'is_read', 'is_mark'] - - // 比较首尾消息的关键字段 + // 根据用户建议,只比较msg_id和is_revoke字段 + // 因为消息ID是唯一的,内容变化主要是由撤回操作引起的 const compareMessage = (localMsg, serverMsg) => { - // 比较基本字段 - for (const field of criticalFields) { - if (localMsg[field] !== serverMsg[field]) { - return false - } - } - - // 特殊处理content字段,它在extra对象中 - const localContent = localMsg.extra?.content - const serverContent = serverMsg.extra?.content - - if (localContent !== serverContent) { - return false - } - - return true + // 消息ID已在外部比较过,这里只需检查is_revoke状态 + return localMsg.is_revoke === serverMsg.is_revoke } const firstMatch = compareMessage(firstLocalMsg, items[firstServerIdx]) const lastMatch = compareMessage(lastLocalMsg, items[lastServerIdx]) - // 如果首尾消息匹配,使用抽样检查中间消息 + // 如果首尾消息匹配,进行全量检查所有消息 if (firstMatch && lastMatch) { - // 智能抽样检查策略 - // 1. 检查首尾消息(已完成) - // 2. 检查中间点消息 - // 3. 检查最近修改的消息(通常是最新的几条) - // 4. 随机抽样检查 - + // 全量检查策略:检查所有消息 + // 由于一次只有30条消息,全量检查不会带来太大的性能负担 let allMatch = true - // 中间点检查 - const midIndex = Math.floor(localMessages.length / 2) - const midMsg = localMessages[midIndex] - const midServerIdx = serverMsgMap.get(midMsg.msg_id) - - if (midServerIdx === undefined || !compareMessage(midMsg, items[midServerIdx])) { - allMatch = false - } - - // 最近消息检查(检查最新的3条消息,通常是最可能被修改的) - if (allMatch && localMessages.length >= 4) { - for (let i = 1; i <= 3; i++) { - const recentMsg = localMessages[localMessages.length - i] - const recentServerIdx = serverMsgMap.get(recentMsg.msg_id) - - if (recentServerIdx === undefined || !compareMessage(recentMsg, items[recentServerIdx])) { - allMatch = false - break - } - } - } - - // 随机抽样检查(如果前面的检查都通过) - if (allMatch && localMessages.length > 10) { - // 随机选择5%的消息或至少2条进行检查 - const sampleSize = Math.max(2, Math.floor(localMessages.length * 0.05)) - const usedIndices = new Set([0, midIndex, localMessages.length - 1]) // 避免重复检查已检查的位置 + // 遍历所有本地消息,与服务器消息进行比较 + for (let i = 0; i < localMessages.length; i++) { + const localMsg = localMessages[i] + const serverIdx = serverMsgMap.get(localMsg.msg_id) - for (let i = 0; i < sampleSize; i++) { - // 生成不重复的随机索引 - let randomIndex - do { - randomIndex = Math.floor(Math.random() * localMessages.length) - } while (usedIndices.has(randomIndex)) - - usedIndices.add(randomIndex) - - const randomMsg = localMessages[randomIndex] - const randomServerIdx = serverMsgMap.get(randomMsg.msg_id) - - if (randomServerIdx === undefined || !compareMessage(randomMsg, items[randomServerIdx])) { - allMatch = false - break - } + // 如果消息ID不存在于服务器数据中,或者消息内容不匹配 + if (serverIdx === undefined || !compareMessage(localMsg, items[serverIdx])) { + allMatch = false + console.log(`消息不匹配,索引: ${i}, 消息ID: ${localMsg.msg_id}`) + break // 一旦发现不匹配,立即退出循环 } } if (allMatch) { const compareEndTime = performance.now() - console.log(`本地数据与服务器数据一致(抽样检查),无需更新UI,比较耗时: ${(compareEndTime - compareStartTime).toFixed(2)}ms`) + console.log(`本地数据与服务器数据一致(全量检查),无需更新UI,比较耗时: ${(compareEndTime - compareStartTime).toFixed(2)}ms`) return } } From 481719a685ff752768069b33edd024505db6dbec Mon Sep 17 00:00:00 2001 From: wangyifeng <812766448@qq.com> Date: Fri, 4 Jul 2025 11:38:57 +0800 Subject: [PATCH 14/22] =?UTF-8?q?=E9=AB=98=E4=BA=AE=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=A2=9E=E5=8A=A0=E5=AF=8C=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=A4=84=E7=90=86=EF=BC=8C=E8=83=BD=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E6=98=BE=E7=A4=BA=E8=A1=A8=E6=83=85=E3=80=81=E6=8D=A2?= =?UTF-8?q?=E8=A1=8C=E7=AC=A6=E7=AD=89=EF=BC=9B=E8=A7=86=E9=A2=91=E7=B1=BB?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E8=83=BD=E7=9B=B4=E6=8E=A5=E5=9C=A8=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E8=AE=B0=E5=BD=95=E4=B8=AD=E6=9F=A5=E7=9C=8B=E5=B9=B6?= =?UTF-8?q?=E6=92=AD=E6=94=BE=EF=BC=9B=E5=A4=84=E7=90=86=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E6=A0=A1=E9=AA=8C=EF=BC=8C=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=89=8D=E5=85=88=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/search/highLightText.vue | 148 +++++++++++++++++--- src/components/search/searchByCondition.vue | 17 ++- src/components/search/searchItem.vue | 21 +-- src/utils/helper/form.js | 26 ++++ 4 files changed, 175 insertions(+), 37 deletions(-) diff --git a/src/components/search/highLightText.vue b/src/components/search/highLightText.vue index 2839885..98c8fef 100644 --- a/src/components/search/highLightText.vue +++ b/src/components/search/highLightText.vue @@ -1,70 +1,172 @@