From 27cd042df9aca0383a49f3514bc21958dd890912 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 29 六月 2026 15:42:23 +0800
Subject: [PATCH] 银川 1.联调产品维护页面 2.添加IM即时通讯模块

---
 src/views/im/home/store/conversationStore.ts |  910 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 910 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/store/conversationStore.ts b/src/views/im/home/store/conversationStore.ts
new file mode 100644
index 0000000..a799ffc
--- /dev/null
+++ b/src/views/im/home/store/conversationStore.ts
@@ -0,0 +1,910 @@
+import type {
+  Conversation,
+  ConversationDO,
+  ConversationRead,
+  ConversationReadDO,
+  MessageDO
+} from '../types'
+
+import type { ImConversationReadApi } from '#/api/im/conversation/read'
+
+import { acceptHMRUpdate, defineStore } from 'pinia'
+
+import { pullMyConversationReadList as apiPullMyConversationReadList } from '#/api/im/conversation/read'
+import { getCurrentUserId } from '#/views/im/utils/auth'
+
+import { CONVERSATION_RECENT_FORWARD_MAX } from '../../utils/config'
+import {
+  ImConversationType,
+  ImMessageReceiptStatus,
+  ImMessageStatus,
+  isNormalMessage
+} from '../../utils/constants'
+import { type DbTransaction, getClientConversationId, getDb, StorageKeys } from '../../utils/db'
+import { runIncrementalPull } from '../../utils/pull'
+import { useMessageStore } from './messageStore'
+
+const PERSIST_DRAFT_DEBOUNCE_MS = 500
+const pendingDraftConversations = new Set<Conversation>()
+
+/** 鍒涘缓浼氳瘽璇讳綅缃褰� */
+function createConversationRead(
+  type: number,
+  targetId: number,
+  messageId: number
+): ConversationRead {
+  return {
+    conversationType: type,
+    targetId,
+    messageId,
+    updateTime: Date.now()
+  }
+}
+
+/** 鍒涘缓鑽夌淇濆瓨闃叉姈鍑芥暟 */
+function createDraftDebounce(fn: () => void, wait: number) {
+  let timer: ReturnType<typeof setTimeout> | undefined
+
+  const run = () => {
+    if (timer) {
+      clearTimeout(timer)
+      timer = undefined
+    }
+    fn()
+  }
+  const debounced = () => {
+    if (timer) {
+      clearTimeout(timer)
+    }
+    timer = setTimeout(run, wait)
+  }
+  debounced.cancel = () => {
+    if (timer) {
+      clearTimeout(timer)
+      timer = undefined
+    }
+  }
+  debounced.flush = run
+  return debounced
+}
+
+/** 浼氳瘽杞� IndexedDB 璁板綍 */
+function toConversationDO(conversation: Conversation): ConversationDO {
+  const draft = conversation.draft
+  return {
+    targetId: conversation.targetId,
+    type: conversation.type,
+    name: conversation.name,
+    avatar: conversation.avatar,
+    unreadCount: conversation.unreadCount,
+    lastContent: conversation.lastContent,
+    lastSendTime: conversation.lastSendTime,
+    lastSenderId: conversation.lastSenderId,
+    lastMessageType: conversation.lastMessageType,
+    lastMessageId: conversation.lastMessageId,
+    lastClientMessageId: conversation.lastClientMessageId,
+    lastMessageStatus: conversation.lastMessageStatus,
+    lastReceiptStatus: conversation.lastReceiptStatus,
+    lastSelfSend: conversation.lastSelfSend,
+    lastSenderDisplayName: conversation.lastSenderDisplayName,
+    reportedReadMessageId: conversation.reportedReadMessageId,
+    deleted: conversation.deleted,
+    top: conversation.top,
+    silent: conversation.silent,
+    atMe: conversation.atMe,
+    atAll: conversation.atAll,
+    draft: draft ? { ...draft, reply: draft.reply ? { ...draft.reply } : undefined } : undefined,
+    clientConversationId: getClientConversationId(conversation.type, conversation.targetId)
+  }
+}
+
+/** IndexedDB 璁板綍杞細璇� */
+function fromConversationDO(conversation: ConversationDO): Conversation {
+  const {
+    clientConversationId: _clientConversationId,
+    ...rest
+  } = conversation
+  return rest
+}
+
+/** 浼氳瘽璇讳綅缃浆 IndexedDB 璁板綍 */
+function toConversationReadDO(record: ConversationRead): ConversationReadDO {
+  return {
+    conversationType: record.conversationType,
+    targetId: record.targetId,
+    messageId: record.messageId,
+    updateTime: record.updateTime,
+    clientConversationId: getClientConversationId(record.conversationType, record.targetId)
+  }
+}
+
+/** IndexedDB 璁板綍杞細璇濊浣嶇疆 */
+function fromConversationReadDO(record: ConversationReadDO): ConversationRead {
+  const { clientConversationId: _clientConversationId, ...rest } = record
+  return rest
+}
+
+/** 鏄惁涓烘湁鏁堜細璇濊浣嶇疆 */
+function isValidConversationReadRecord(record: ImConversationReadApi.ConversationReadRespVO): boolean {
+  return !!record.conversationType && !!record.targetId && !!record.messageId
+}
+
+/** 鑾峰彇瀵规柟鏅�氭秷鎭渶澶х紪鍙� */
+function getMaxIncomingNormalMessageId(
+  messages: Array<Pick<MessageDO, 'id' | 'selfSend' | 'status' | 'type'>>
+): number {
+  let maxMessageId = 0
+  for (const message of messages) {
+    if (
+      message.id &&
+      !message.selfSend &&
+      isNormalMessage(message.type) &&
+      message.status !== ImMessageStatus.RECALL &&
+      message.id > maxMessageId
+    ) {
+      maxMessageId = message.id
+    }
+  }
+  return maxMessageId
+}
+
+export const useConversationStore = defineStore('imConversationStore', {
+  state: () => ({
+    conversations: [] as Conversation[], // 鍏ㄩ噺浼氳瘽鍒楄〃锛堢鑱� + 缇よ亰 + 棰戦亾锛�
+    conversationReads: {} as Record<string, ConversationRead>, // 浼氳瘽璇讳綅缃�
+    activeConversation: null as Conversation | null, // 褰撳墠婵�娲荤殑浼氳瘽
+    loading: false, // 鏄惁姝e湪鎵归噺鍔犺浇
+    recentForwardConversationKeys: [] as string[] // 鏈�杩戣浆鍙戜細璇� key 鍒楄〃
+  }),
+
+  getters: {
+    /** 鎺掑簭鍚庣殑浼氳瘽鍒楄〃 */
+    getSortedConversationList(state): Conversation[] {
+      return state.conversations
+        .filter((conversation) => !conversation.deleted)
+        .toSorted((a, b) => {
+          const aTop = a.top ? 1 : 0
+          const bTop = b.top ? 1 : 0
+          if (aTop !== bTop) {
+            return bTop - aTop
+          }
+          return (b.lastSendTime || 0) - (a.lastSendTime || 0)
+        })
+    },
+
+    /** 鏈鎬绘暟 */
+    getTotalUnreadCount(state): number {
+      return state.conversations
+        .filter((conversation) => !conversation.deleted && !conversation.silent)
+        .reduce((sum, conversation) => sum + (conversation.unreadCount || 0), 0)
+    },
+
+    /** 鏌ユ壘浼氳瘽 */
+    getConversation:
+      (state) =>
+      (type: number, targetId: number): Conversation | undefined =>
+        state.conversations.find(
+          (conversation) => conversation.type === type && conversation.targetId === targetId
+        ),
+
+    /** 鏌ユ壘浼氳瘽璇讳綅缃� */
+    getConversationRead:
+      (state) =>
+      (type: number, targetId: number): ConversationRead | undefined =>
+        state.conversationReads[getClientConversationId(type, targetId)]
+  },
+
+  actions: {
+    /** 鍔犺浇浼氳瘽 */
+    async loadConversationList() {
+      // 1. 娓呯悊鏃ц处鍙峰唴瀛�
+      const userId = getCurrentUserId()
+      if (!userId) {
+        this.clear()
+        return
+      }
+      const previousActiveKey = this.activeConversation
+        ? getClientConversationId(this.activeConversation.type, this.activeConversation.targetId)
+        : null
+      this.clear()
+      // 2. 浠� IndexedDB 璇诲彇浼氳瘽鍜岃交閲忚缃�
+      const db = getDb()
+      const [conversations, conversationReads, recent] = await Promise.all([
+        db.getAll<ConversationDO>('conversations'),
+        db.getAll<ConversationReadDO>('conversationReads'),
+        db.getSetting<string[]>(StorageKeys.settings.recentForwardConversationKeys)
+      ])
+      const nextConversationReads: Record<string, ConversationRead> = {}
+      for (const record of conversationReads) {
+        const item = fromConversationReadDO(record)
+        nextConversationReads[getClientConversationId(item.conversationType, item.targetId)] = item
+      }
+      const nextConversations = conversations.map((conversation) => fromConversationDO(conversation))
+      this.conversationReads = nextConversationReads
+      await this.applyLocalConversationReads(nextConversations)
+      this.conversations = nextConversations
+      if (Array.isArray(recent)) {
+        this.recentForwardConversationKeys = recent.slice(0, CONVERSATION_RECENT_FORWARD_MAX)
+      }
+      // 3. 鎭㈠褰撳墠婵�娲讳細璇�
+      if (previousActiveKey) {
+        this.activeConversation =
+          this.conversations.find(
+            (conversation) =>
+              !conversation.deleted &&
+              getClientConversationId(conversation.type, conversation.targetId) ===
+                previousActiveKey
+          ) ?? null
+      }
+    },
+
+    /** 娓呯┖浼氳瘽鍐呭瓨 */
+    clear() {
+      saveDraftConversationListDebounced.cancel()
+      pendingDraftConversations.clear()
+      this.conversations = []
+      this.conversationReads = {}
+      this.activeConversation = null
+      this.recentForwardConversationKeys = []
+    },
+
+    /** 鎸佷箙鍖栦細璇濊浣嶇疆 */
+    async saveConversationReadRecord(
+      target: ConversationRead | ConversationRead[] | null | undefined,
+      tx?: DbTransaction
+    ): Promise<void> {
+      let targets: ConversationRead[] = []
+      if (Array.isArray(target)) {
+        targets = target
+      } else if (target) {
+        targets = [target]
+      }
+      const records = targets.map((record) => toConversationReadDO(record))
+      if (records.length === 0) {
+        return
+      }
+      const db = getDb()
+      if (tx) {
+        for (const record of records) {
+          await db.put('conversationReads', record, tx)
+        }
+        return
+      }
+      await db.transaction(['conversationReads'], 'readwrite', async (tx) => {
+        for (const record of records) {
+          await db.put('conversationReads', record, tx)
+        }
+      })
+    },
+
+    /** 搴旂敤鏈湴浼氳瘽璇讳綅缃� */
+    async applyLocalConversationReads(conversations?: Conversation[]) {
+      const targetConversations = conversations || this.conversations
+      const changedConversations: Conversation[] = []
+      for (const conversation of targetConversations) {
+        const record = this.getConversationRead(conversation.type, conversation.targetId)
+        if (!record) {
+          continue
+        }
+        if (this.applyReadToConversation(conversation, record.messageId)) {
+          changedConversations.push(conversation)
+          continue
+        }
+        if (conversation.unreadCount === 0 && !conversation.atMe && !conversation.atAll) {
+          continue
+        }
+        const messages = await getDb().getAllByIndex<MessageDO>(
+          'messages',
+          'clientConversationId',
+          getClientConversationId(conversation.type, conversation.targetId)
+        )
+        const maxIncomingMessageId = getMaxIncomingNormalMessageId(messages)
+        if (maxIncomingMessageId > 0 && maxIncomingMessageId <= record.messageId) {
+          conversation.unreadCount = 0
+          conversation.atMe = false
+          conversation.atAll = false
+          changedConversations.push(conversation)
+        }
+      }
+      if (changedConversations.length > 0) {
+        await this.saveConversationRecord(changedConversations)
+      }
+    },
+
+    /** 鍒ゆ柇娑堟伅鏄惁宸茶浼氳瘽璇讳綅缃鐩� */
+    isMessageCoveredByReadPosition(
+      conversation: Pick<Conversation, 'targetId' | 'type'>,
+      message?: null | { id?: number }
+    ): boolean {
+      if (!message?.id) {
+        return false
+      }
+      const record = this.getConversationRead(conversation.type, conversation.targetId)
+      return !!record && message.id <= record.messageId
+    },
+
+    /** 鍒ゆ柇浼氳瘽璇讳綅缃槸鍚﹁鐩栨秷鎭紪鍙� */
+    isReadPositionCovered(type: number, targetId: number, messageId?: number): boolean {
+      if (!messageId) {
+        return false
+      }
+      const record = this.getConversationRead(type, targetId)
+      return !!record && record.messageId >= messageId
+    },
+
+    /** 鍒ゆ柇鏈嶅姟绔凡璇讳綅缃槸鍚﹁鐩栨秷鎭紪鍙� */
+    isReportedReadPositionCovered(type: number, targetId: number, messageId?: number): boolean {
+      if (!messageId) {
+        return false
+      }
+      const conversation = this.getConversation(type, targetId)
+      return (conversation?.reportedReadMessageId || 0) >= messageId
+    },
+
+    /** 搴旂敤璇讳綅缃埌浼氳瘽 */
+    applyReadToConversation(conversation: Conversation, messageId: number): boolean {
+      if (!conversation.lastMessageId || conversation.lastMessageId > messageId) {
+        return false
+      }
+      if (conversation.unreadCount === 0 && !conversation.atMe && !conversation.atAll) {
+        return false
+      }
+      conversation.unreadCount = 0
+      conversation.atMe = false
+      conversation.atAll = false
+      return true
+    },
+
+    /** 搴旂敤浼氳瘽璇讳綅缃� */
+    async applyConversationReadList(
+      records: ImConversationReadApi.ConversationReadRespVO[],
+      isActive?: () => boolean
+    ): Promise<void> {
+      if (records.length === 0) {
+        return
+      }
+      const changedReads = new Map<string, ConversationRead>()
+      const changedConversations = new Map<string, Conversation>()
+      const changedMessages = new Map<string, MessageDO>()
+      const db = getDb()
+      const messageStore = useMessageStore()
+
+      // 1. 鎸夎浣嶇疆鏇存柊浼氳瘽鏈鍜岄閬撳凡璇绘��
+      for (const record of records) {
+        if (isActive && !isActive()) {
+          return
+        }
+        if (!isValidConversationReadRecord(record)) {
+          continue
+        }
+        const clientConversationId = getClientConversationId(
+          record.conversationType,
+          record.targetId
+        )
+        let storedMessages: MessageDO[] | undefined
+        const getStoredMessages = async () => {
+          if (!storedMessages) {
+            storedMessages = await db.getAllByIndex<MessageDO>(
+              'messages',
+              'clientConversationId',
+              clientConversationId
+            )
+          }
+          return storedMessages
+        }
+        const current = this.conversationReads[clientConversationId]
+        const messageId = Math.max(record.messageId, current?.messageId || 0)
+        const conversation = this.getConversation(record.conversationType, record.targetId)
+        if (conversation && record.messageId > (conversation.reportedReadMessageId || 0)) {
+          conversation.reportedReadMessageId = record.messageId
+          changedConversations.set(clientConversationId, conversation)
+        }
+        if (!current || messageId > current.messageId) {
+          const next = {
+            conversationType: record.conversationType,
+            targetId: record.targetId,
+            messageId,
+            updateTime: record.updateTime
+          }
+          this.conversationReads[clientConversationId] = next
+          changedReads.set(clientConversationId, next)
+        }
+
+        if (conversation && this.applyReadToConversation(conversation, messageId)) {
+          changedConversations.set(clientConversationId, conversation)
+        } else if (conversation) {
+          const maxIncomingMessageId = getMaxIncomingNormalMessageId(await getStoredMessages())
+          if (maxIncomingMessageId > 0 && maxIncomingMessageId <= messageId) {
+            conversation.unreadCount = 0
+            conversation.atMe = false
+            conversation.atAll = false
+            changedConversations.set(clientConversationId, conversation)
+          }
+        }
+        if (record.conversationType !== ImConversationType.CHANNEL) {
+          continue
+        }
+        const memoryMessages = messageStore.getMessages(clientConversationId)
+        for (const message of memoryMessages) {
+          if (
+            message.id &&
+            message.id <= messageId &&
+            message.receiptStatus !== ImMessageReceiptStatus.DONE
+          ) {
+            message.receiptStatus = ImMessageReceiptStatus.DONE
+          }
+        }
+        for (const message of await getStoredMessages()) {
+          if (
+            message.id &&
+            message.id <= messageId &&
+            message.receiptStatus !== ImMessageReceiptStatus.DONE
+          ) {
+            message.receiptStatus = ImMessageReceiptStatus.DONE
+            changedMessages.set(message.messageKey, message)
+          }
+        }
+      }
+
+      // 2. 鎸佷箙鍖栨湰杞彉鏇�
+      if (
+        changedReads.size === 0 &&
+        changedConversations.size === 0 &&
+        changedMessages.size === 0
+      ) {
+        return
+      }
+      if (isActive && !isActive()) {
+        return
+      }
+      const stores: Array<'conversationReads' | 'conversations' | 'messages'> = []
+      if (changedReads.size > 0) {
+        stores.push('conversationReads')
+      }
+      if (changedConversations.size > 0) {
+        stores.push('conversations')
+      }
+      if (changedMessages.size > 0) {
+        stores.push('messages')
+      }
+      await db.transaction(stores, 'readwrite', async (tx) => {
+        if (changedReads.size > 0) {
+          await this.saveConversationReadRecord([...changedReads.values()], tx)
+        }
+        if (changedConversations.size > 0) {
+          await this.saveConversationRecord([...changedConversations.values()], tx)
+        }
+        for (const message of changedMessages.values()) {
+          await db.put('messages', message, tx)
+        }
+      })
+    },
+
+    /** 澧為噺鎷夊彇浼氳瘽璇讳綅缃� */
+    async pullConversationReads(isActive?: () => boolean): Promise<void> {
+      await runIncrementalPull(
+        StorageKeys.settings.conversationReadPullCursor,
+        apiPullMyConversationReadList,
+        async (records) => {
+          if (isActive && !isActive()) {
+            return false
+          }
+          await this.applyConversationReadList(records, isActive)
+          if (isActive && !isActive()) {
+            return false
+          }
+          return true
+        },
+        isActive
+      )
+    },
+
+    /** 鎵ц浼氳瘽璁板綍鎸佷箙鍖� */
+    async saveConversationRecord(
+      target: Conversation | Conversation[] | null | undefined,
+      tx?: DbTransaction
+    ): Promise<void> {
+      const db = getDb()
+      const conversations = (Array.isArray(target) ? target : (target ? [target] : [])).map(
+        (conversation) => toConversationDO(conversation)
+      )
+      if (conversations.length === 0) {
+        return
+      }
+      if (tx) {
+        for (const conversation of conversations) {
+          await db.put('conversations', conversation, tx)
+        }
+        return
+      }
+      await db.transaction(['conversations'], 'readwrite', async (tx) => {
+        for (const conversation of conversations) {
+          await db.put('conversations', conversation, tx)
+        }
+      })
+    },
+
+    /** 鎸佷箙鍖栧崟涓細璇� */
+    saveConversation(conversation: Conversation | null | undefined, tx?: DbTransaction): void {
+      if (!conversation) {
+        return
+      }
+      void this.saveConversationRecord(conversation, tx).catch((error) =>
+        console.warn('[IM conversationStore] 浼氳瘽鍐欏叆澶辫触', error)
+      )
+    },
+
+    /** 鎸佷箙鍖栦細璇濆垪琛� */
+    saveConversationList(conversations?: Conversation[] | null, tx?: DbTransaction): void {
+      if (this.loading && !tx) {
+        return
+      }
+      void this.saveConversationRecord(conversations || this.conversations, tx).catch((error) =>
+        console.warn('[IM conversationStore] 浼氳瘽鍐欏叆澶辫触', error)
+      )
+    },
+
+    /** 纭繚浼氳瘽瀛樺湪 */
+    ensureConversation(info: {
+      avatar: string
+      name: string
+      silent?: boolean
+      targetId: number
+      type: number
+    }): Conversation {
+      // 1. 鍒涘缓涓嶅瓨鍦ㄧ殑浼氳瘽
+      let conversation = this.getConversation(info.type, info.targetId)
+      if (!conversation) {
+        conversation = this.createEmptyConversation(
+          info.type,
+          info.targetId,
+          info.name,
+          info.avatar,
+          info.silent
+        )
+        this.conversations.unshift(conversation)
+      } else if (conversation.deleted) {
+        // 2. 鎭㈠杞垹闄や細璇�
+        conversation.deleted = false
+        conversation.name = info.name || conversation.name
+        conversation.avatar = info.avatar || conversation.avatar
+        if (info.silent !== undefined) {
+          conversation.silent = info.silent
+        }
+      } else {
+        // 3. 鍚屾浼氳瘽灞曠ず鍏冩暟鎹�
+        if (info.name) {
+          conversation.name = info.name
+        }
+        if (info.avatar) {
+          conversation.avatar = info.avatar
+        }
+        if (info.silent !== undefined) {
+          conversation.silent = info.silent
+        }
+      }
+      return conversation
+    },
+
+    /** 鎵撳紑鎴栧垱寤轰細璇� */
+    openConversation(
+      targetId: number,
+      type: number,
+      name: string,
+      avatar: string,
+      options?: { silent?: boolean }
+    ): Conversation {
+      // 1. 纭繚浼氳瘽鍦ㄥ垪琛ㄤ腑
+      const conversation = this.ensureConversation({
+        type,
+        targetId,
+        name,
+        avatar,
+        silent: options?.silent
+      })
+      // 2. 婵�娲讳細璇濆苟淇濆瓨
+      this.setActiveConversation(conversation)
+      this.saveConversation(conversation)
+      return conversation
+    },
+
+    /** 璁剧疆褰撳墠浼氳瘽 */
+    setActiveConversation(conversation: Conversation | null) {
+      this.activeConversation = conversation
+      if (!conversation) {
+        return
+      }
+      // 鎳掑姞杞芥秷鎭苟淇濆瓨浼氳瘽鎽樿
+      void useMessageStore().ensureConversationMessageListLoaded(conversation)
+      this.saveConversation(conversation)
+    },
+
+    /** 鍒涘缓绌轰細璇� */
+    createEmptyConversation(
+      type: number,
+      targetId: number,
+      name: string,
+      avatar: string,
+      silent = false
+    ): Conversation {
+      return {
+        targetId,
+        type,
+        name,
+        avatar,
+        lastContent: '',
+        lastSendTime: 0,
+        unreadCount: 0,
+        deleted: false,
+        top: false,
+        silent,
+        atMe: false,
+        atAll: false
+      }
+    },
+
+    /** 璁剧疆缃《 */
+    setConversationTop(type: number, targetId: number, top: boolean) {
+      const conversation = this.getConversation(type, targetId)
+      if (!conversation) {
+        return
+      }
+      conversation.top = top
+      this.saveConversation(conversation)
+    },
+
+    /** 璁剧疆鍏嶆墦鎵� */
+    setConversationSilent(type: number, targetId: number, silent: boolean) {
+      const conversation = this.getConversation(type, targetId)
+      if (!conversation) {
+        return
+      }
+      conversation.silent = silent
+      this.saveConversation(conversation)
+    },
+
+    /** 鍒犻櫎浼氳瘽 */
+    removeConversation(type: number, targetId: number) {
+      // 1. 鏍囪浼氳瘽鍒犻櫎
+      const conversation = this.getConversation(type, targetId)
+      if (!conversation) {
+        return
+      }
+      if (this.activeConversation === conversation) {
+        this.activeConversation = null
+      }
+      conversation.deleted = true
+      // 2. 鍒犻櫎浼氳瘽鍏宠仈鐨勬秷鎭拰鑽夌
+      useMessageStore().deleteConversationMessageList(type, targetId)
+      this.clearConversationDraft(conversation)
+      this.saveConversation(conversation)
+    },
+
+    /** 鍒犻櫎绉佽亰浼氳瘽 */
+    removePrivateConversation(friendId: number) {
+      this.removeConversation(ImConversationType.PRIVATE, friendId)
+    },
+
+    /** 鍒犻櫎缇よ亰浼氳瘽 */
+    removeGroupConversation(groupId: number) {
+      this.removeConversation(ImConversationType.GROUP, groupId)
+    },
+
+    /** 鏍囪浼氳瘽宸茶 */
+    markConversationRead(type: number, targetId: number, messageId?: number): void {
+      const conversation = this.getConversation(type, targetId)
+      if (!conversation) {
+        return
+      }
+      const key = getClientConversationId(type, targetId)
+      const current = this.conversationReads[key]
+      const readMessageIdAdvanced = !!messageId && messageId > (current?.messageId || 0)
+      if (
+        conversation.unreadCount === 0 &&
+        !conversation.atMe &&
+        !conversation.atAll &&
+        !readMessageIdAdvanced
+      ) {
+        return
+      }
+      conversation.unreadCount = 0
+      conversation.atMe = false
+      conversation.atAll = false
+      if (readMessageIdAdvanced) {
+        const record = createConversationRead(type, targetId, messageId)
+        this.conversationReads[key] = record
+        void getDb()
+          .transaction(['conversations', 'conversationReads'], 'readwrite', async (tx) => {
+            await this.saveConversationRecord(conversation, tx)
+            await this.saveConversationReadRecord(record, tx)
+          })
+          .catch((error) =>
+            console.warn(
+              '[IM conversationStore] 浼氳瘽宸茶鍐欏叆澶辫触',
+              {
+                conversationType: type,
+                targetId,
+                messageId,
+                conversationKey: key
+              },
+              error
+            )
+          )
+        return
+      }
+      this.saveConversation(conversation)
+    },
+
+    /** 鏍囪浼氳瘽宸蹭笂鎶ユ湇鍔$璇讳綅缃� */
+    markConversationReadReported(type: number, targetId: number, messageId?: number): void {
+      if (!messageId) {
+        return
+      }
+      const conversation = this.getConversation(type, targetId)
+      if (!conversation || messageId <= (conversation.reportedReadMessageId || 0)) {
+        return
+      }
+      conversation.reportedReadMessageId = messageId
+      this.saveConversation(conversation)
+    },
+
+    // ==================== 鏈�杩戣浆鍙� ====================
+
+    /** 鎺ㄩ�佹渶杩戣浆鍙戜細璇� */
+    pushRecentForwardConversationKeyList(keys: string[]) {
+      if (!keys || keys.length === 0) {
+        return
+      }
+      const merged = [...keys, ...this.recentForwardConversationKeys]
+      this.recentForwardConversationKeys = [...new Set(merged)].slice(
+        0,
+        CONVERSATION_RECENT_FORWARD_MAX
+      )
+      this.saveRecentForwardConversationKeyList()
+    },
+
+    /** 绉婚櫎鏈�杩戣浆鍙戜細璇� */
+    removeRecentForwardConversationKey(key: string) {
+      const index = this.recentForwardConversationKeys.indexOf(key)
+      if (index === -1) {
+        return
+      }
+      this.recentForwardConversationKeys.splice(index, 1)
+      this.saveRecentForwardConversationKeyList()
+    },
+
+    /** 淇濆瓨鏈�杩戣浆鍙戜細璇� */
+    saveRecentForwardConversationKeyList() {
+      void getDb()
+        .setSetting(
+          StorageKeys.settings.recentForwardConversationKeys,
+          this.recentForwardConversationKeys.slice(0, CONVERSATION_RECENT_FORWARD_MAX)
+        )
+        .catch((error) => console.warn('[IM conversationStore] 鏈�杩戣浆鍙戝垪琛ㄥ啓鍏ュけ璐�', error))
+    },
+
+    // ==================== 浼氳瘽缁存姢 ====================
+
+    /** 閲嶆帓浼氳瘽 */
+    sortConversationList() {
+      this.conversations.sort((a, b) => (b.lastSendTime || 0) - (a.lastSendTime || 0))
+      this.saveConversationList(this.conversations)
+    },
+
+    /** 鍚屾浼氳瘽灞曠ず鍏冩暟鎹� */
+    updateConversation(
+      type: number,
+      targetId: number,
+      info: { avatar?: string; name?: string; silent?: boolean }
+    ) {
+      const conversation = this.getConversation(type, targetId)
+      if (!conversation) {
+        return
+      }
+      let changed = false
+      if (info.name && conversation.name !== info.name) {
+        conversation.name = info.name
+        changed = true
+      }
+      if (info.avatar !== undefined && conversation.avatar !== info.avatar) {
+        conversation.avatar = info.avatar || ''
+        changed = true
+      }
+      if (info.silent !== undefined && conversation.silent !== info.silent) {
+        conversation.silent = info.silent
+        changed = true
+      }
+      if (changed) {
+        this.saveConversation(conversation)
+      }
+    },
+
+    // ==================== 鑽夌 ====================
+
+    /** 鑾峰彇鑽夌 */
+    getConversationDraft(conversation: { targetId: number; type: number; }): Conversation['draft'] | undefined {
+      return this.getConversation(conversation.type, conversation.targetId)?.draft
+    },
+
+    /** 璁剧疆鑽夌 */
+    setConversationDraft(
+      conversation: { targetId: number; type: number; },
+      snapshot: NonNullable<Conversation['draft']>
+    ): void {
+      if (!snapshot.plain.trim() && !snapshot.reply) {
+        this.clearConversationDraft(conversation)
+        return
+      }
+      const target = this.getConversation(conversation.type, conversation.targetId)
+      if (!target) {
+        return
+      }
+      target.draft = snapshot
+      this.scheduleConversationDraftSave(target)
+    },
+
+    /** 娓呴櫎鑽夌 */
+    clearConversationDraft(conversation: { targetId: number; type: number; }): void {
+      const target = this.getConversation(conversation.type, conversation.targetId)
+      if (!target?.draft) {
+        return
+      }
+      target.draft = undefined
+      this.scheduleConversationDraftSave(target)
+    },
+
+    /** 璁剧疆鍥炲鑽夌 */
+    setConversationReplyDraft(
+      conversation: { targetId: number; type: number; },
+      quote: NonNullable<Conversation['draft']>['reply']
+    ) {
+      if (!quote) {
+        return
+      }
+      const existing = this.getConversationDraft(conversation)
+      this.setConversationDraft(conversation, {
+        html: existing?.html ?? '',
+        plain: existing?.plain ?? '',
+        reply: quote
+      })
+    },
+
+    /** 娓呴櫎鍥炲鑽夌 */
+    clearConversationReplyDraft(conversation: { targetId: number; type: number; }): void {
+      const existing = this.getConversationDraft(conversation)
+      if (!existing?.reply) {
+        return
+      }
+      this.setConversationDraft(conversation, { ...existing, reply: undefined })
+    },
+
+    /** 璋冨害鑽夌淇濆瓨 */
+    scheduleConversationDraftSave(conversation: Conversation): void {
+      pendingDraftConversations.add(conversation)
+      saveDraftConversationListDebounced()
+    },
+
+    /** 绔嬪嵆淇濆瓨鑽夌 */
+    flushConversationDraftSave(): void {
+      saveDraftConversationListDebounced.flush()
+    }
+  }
+})
+
+export const useConversationStoreWithOut = () => useConversationStore()
+
+/** 鍚堝苟鑽夌鍐欏叆 */
+const saveDraftConversationListDebounced = createDraftDebounce(() => {
+  const conversations = [...pendingDraftConversations]
+  pendingDraftConversations.clear()
+  if (conversations.length === 0) {
+    return
+  }
+  void useConversationStoreWithOut()
+    .saveConversationRecord(conversations)
+    .catch((error) => console.warn('[IM conversationStore] 鑽夌鍐欏叆澶辫触', error))
+}, PERSIST_DRAFT_DEBOUNCE_MS)
+
+if (import.meta.hot) {
+  import.meta.hot.accept(acceptHMRUpdate(useConversationStore, import.meta.hot))
+}

--
Gitblit v1.9.3