diff --git a/apps/web-antd/src/views/im/home/index.vue b/apps/web-antd/src/views/im/home/index.vue index b15c34aca..f7ca5d72b 100644 --- a/apps/web-antd/src/views/im/home/index.vue +++ b/apps/web-antd/src/views/im/home/index.vue @@ -6,8 +6,16 @@ import { useRoute } from 'vue-router'; import { preferences } from '@vben/preferences'; +import { getCurrentUserId } from '#/views/im/utils/auth'; + import { ImConversationType } from '../utils/constants'; -import { initDb, stopRequests, StorageKeys } from '../utils/db'; +import { + getDbSession, + initDb, + isCurrentDbSession, + stopRequests, + StorageKeys, +} from '../utils/db'; import { ContextMenu, ToolBar } from './components'; import { GroupInfoCard } from './components/group'; import { RtcCallContainer } from './components/rtc'; @@ -39,9 +47,21 @@ const { pullOnce, cancelPull } = useMessagePuller(); const { readActive, syncPrivateReadStatus } = useMessageSender(); const voicePlayer = useVoicePlayer(); const childRouteReady = ref(false); // 子路由是否可挂载 +let disposed = false; // 当前 IM 主壳是否已经卸载 + +/** 判断当前首页初始化任务仍属于本组件与登录账号 */ +function isInitializationActive(userId: number, session?: number) { + return ( + !disposed && + getCurrentUserId() === userId && + (session === undefined || isCurrentDbSession(session)) + ); +} /** 初始化:先吃本地缓存让首屏立即渲染,再远端刷新最新数据,最后建实时通信拉离线消息 */ onMounted(async () => { + const userId = getCurrentUserId(); + let session: number | undefined; // 0.1 系统表情包后台预拉:独立链路与首屏 IDB / 远端拉取并发,消除表情面板首次展开白屏;失败仅记日志,不阻塞主流程 void faceStore .ensureFacePackList() @@ -51,6 +71,10 @@ onMounted(async () => { try { // 1.2 打开当前用户 IM DB await initDb(); + session = getDbSession(); + if (!isInitializationActive(userId, session)) { + return; + } // 1.3 多个 store 并发从 IDB 读取本地缓存 const cacheResults = await Promise.all([ conversationStore.loadConversationList(), @@ -60,6 +84,9 @@ onMounted(async () => { channelStore.loadChannelList(), groupRequestStore.loadGroupRequestList(), ]); + if (!isInitializationActive(userId, session)) { + return; + } const hasFriendRows = cacheResults[2]; const hasGroupRows = cacheResults[3]; const hasChannelRows = cacheResults[4]; @@ -102,6 +129,9 @@ onMounted(async () => { // 2.4 执行加载 if (requiredFetches.length > 0) { await Promise.all(requiredFetches); + if (!isInitializationActive(userId, session)) { + return; + } } // 2.5 好友申请增量补偿:首登也要跑,离线期间好友申请变更不会影响好友主表 @@ -111,12 +141,22 @@ onMounted(async () => { // 3. 会话读位置先补偿,消息入库时可直接过滤已读历史消息 await conversationStore - .pullConversationReads() - .catch((error) => console.warn('[IM] 拉取会话读位置失败', error)); + .pullConversationReads(() => isInitializationActive(userId, session)) + .catch((error) => { + if (isInitializationActive(userId, session)) { + console.warn('[IM] 拉取会话读位置失败', error); + } + }); + if (!isInitializationActive(userId, session)) { + return; + } // 4. 实时通信:建 WebSocket 长连接 + 拉离线消息(pullOnce finally 把 loading 归位) webSocketStore.connect(); await pullOnce(); + if (!isInitializationActive(userId, session)) { + return; + } // 5. 默认选中第一个会话;若置顶分组处于折叠态,需跳过被折叠隐藏的置顶项,避免自动展开折叠 const sorted = conversationStore.getSortedConversationList; @@ -125,6 +165,9 @@ onMounted(async () => { conversationStore.setActiveConversation(firstVisible); } } catch (error) { + if (!isInitializationActive(userId, session)) { + return; + } // 1. 首拉失败:手动复位 loading(pullOnce 没跑到,它的 finally 兜不到这里),否则后续会话列表写入全被早 return 阻断 // 2. WebSocket 不在这里 disconnect——路由离开会走 onUnmounted 自然清理,用户也可以刷新重试 conversationStore.loading = false; @@ -164,6 +207,7 @@ window.addEventListener('beforeunload', onBeforeUnload); /** 离开 IM 主壳:取消 pull、断开 WebSocket、保存草稿、停止语音、解绑 unload,并结束当前 IM session */ onUnmounted(() => { + disposed = true; cancelPull(); webSocketStore.disconnect(); conversationStore.flushConversationDraftSave();