import { defineStore, acceptHMRUpdate } from 'pinia' import { store } from '@/store' import { getMyGroupList as apiGetMyGroupList, getGroup as apiGetGroup, type ImGroupRespVO } from '@/api/im/group' import { getGroupMember as apiGetGroupMember, getGroupMemberList as apiGetGroupMemberList, updateGroupMember as apiUpdateGroupMember, type ImGroupMemberRespVO } from '@/api/im/group/member' import { useConversationStore } from './conversationStore' import { ImConversationType, ImGroupMemberRole, ImMessageType } from '../../utils/constants' import { getCurrentUserId, imStorage, removeQuietly, setQuietly, StorageKeys } from '../../utils/storage' import { getGroupDisplayName, type GroupNotificationPayload } from '../../utils/user' import type { Group, GroupMember, Message } from '../types' /** * fetchGroupMembers 并发去重表:同 groupId 同时进的请求共用一个 Promise * * key 必须带 userId——账号切换时 A 的请求不能被 B 复用,否则 IIFE 内部的 saveGroupMembers 会把 A 的成员数据写进 B 的 IDB 桶 */ const pendingMemberFetches = new Map>() const pendingMemberKey = (userId: number, groupId: number) => `${userId}:${groupId}` /** * fetchGroupMember 单成员并发去重表:同 (groupId, memberUserId) 同时进的请求共用一个 Promise * * 跟整群表分开:单成员 fetch 跟整群 fetch 语义不同(单成员不回填 me 的 silent),不能互相代替 */ const pendingSingleMemberFetches = new Map>() const pendingSingleMemberKey = (userId: number, groupId: number, memberUserId: number) => `${userId}:${groupId}:${memberUserId}` /** 判断当前用户是否在 payload.memberUserIds 里(GROUP_CREATE / INVITE / KICK 自判用) */ function isSelfInPayloadMembers(payload: GroupNotificationPayload): boolean { const selfUserId = getCurrentUserId() return !!selfUserId && (payload.memberUserIds || []).includes(selfUserId) } /** * IM 群 Store * * 负责: * - 拉取 / 缓存当前登录用户加入的群列表 * - 按 groupId 懒加载群成员(供 ConversationGroupSide / MentionPicker / MessageReadStatus 消费) * - 成员"已读 / 未读"等聚合查询由 MessageReadStatus 另行组合 */ export const useGroupStore = defineStore('imGroupStore', { state: () => ({ groups: [] as Group[], // 仅 fetchGroups 成功后置位;loadGroups(IDB)不置位,否则后台 SWR 刷新会被缓存命中跳过 loaded: false }), getters: { getGroup: (state) => (id: number): Group | undefined => { return state.groups.find((g) => g.id === id) } }, actions: { // ==================== 本地缓存 ==================== /** 从 IDB 恢复群列表(不带 members),返回是否命中缓存 */ async loadGroups(): Promise { const userId = getCurrentUserId() if (!userId) { return false } try { const cached = await imStorage.getItem(StorageKeys.groups(userId)) if (!cached || cached.length === 0) { return false } this.groups = cached return true } catch (e) { console.warn('[IM groupStore] 本地群缓存读取失败', e) return false } }, /** 整桶持久化群列表;剥离 members 字段,成员另走 groupMembers:${groupId} 分桶 */ saveGroups(): void { const userId = getCurrentUserId() if (!userId) { return } const groupsWithoutMembers = this.groups.map(({ members, ...rest }) => rest) setQuietly( StorageKeys.groups(userId), groupsWithoutMembers, '[IM groupStore] 本地群缓存写入失败' ) }, /** 从 IDB 恢复指定群成员;命中返回成员数组,未命中返回 null */ async loadGroupMembers(groupId: number): Promise { const userId = getCurrentUserId() if (!userId) { return null } // in-memory 已"完整"加载(fetchGroupMembers 跑过或上次冷启动从 IDB 整桶恢复过):直接复用; // 单成员补齐(fetchGroupMember)写进的 partial members 不在此返回缓存——其 membersLoaded=false const cachedGroup = this.getGroup(groupId) if (cachedGroup?.members && cachedGroup.membersLoaded) { return cachedGroup.members } try { const cached = await imStorage.getItem( StorageKeys.groupMembers(userId, groupId) ) if (!cached || cached.length === 0) { return null } // 把 IDB 拿到的成员落到对应 group const group = this.getGroup(groupId) if (!group) { // group 还没就位:仅 in-memory 占位(name='' 表示未知),不调 upsertGroup —— 避免把假名灌进 conversation.name + groups IDB 桶; // 后续,等 fetchGroups 浅合并时,被真名覆盖 this.groups.push({ id: groupId, name: '', members: cached, memberCount: cached.length, membersLoaded: true }) } else { group.members = cached group.memberCount = cached.length group.membersLoaded = true } return cached } catch (e) { console.warn('[IM groupStore] 本地群成员缓存读取失败', { groupId }, e) return null } }, /** 整桶持久化指定群成员 */ saveGroupMembers(groupId: number): void { const userId = getCurrentUserId() if (!userId) { return } const members = this.getGroup(groupId)?.members if (!members) { return } setQuietly( StorageKeys.groupMembers(userId, groupId), members, `[IM groupStore] 本地群成员缓存写入失败 (groupId=${groupId})` ) }, // ==================== 远端拉取 ==================== /** 拉取群列表;同步刷新对应群聊会话的展示名 / 头像 + 落 IDB */ async fetchGroups(force = false) { if (this.loaded && !force) { return } // 拉取当前登录用户加入的所有群(不带成员;成员按需再走 fetchGroupMembers) const list = await apiGetMyGroupList() const fresh = (list || []).map(convertGroup) // 合并而非全量替换:silent / groupRemark / 成员缓存这些字段不在 ImGroupRespVO 里,得从旧 group 保留 const groupMap = new Map(this.groups.map((group) => [group.id, group])) this.groups = fresh.map((group) => { const existing = groupMap.get(group.id) if (!existing) { return group } return { ...group, members: existing.members, memberCount: existing.memberCount ?? group.memberCount, silent: existing.silent ?? group.silent, groupRemark: existing.groupRemark, membersLoaded: existing.membersLoaded } }) this.loaded = true const conversationStore = useConversationStore() for (const group of this.groups) { conversationStore.updateConversation(ImConversationType.GROUP, group.id, { name: getGroupDisplayName(group), avatar: group.avatar, silent: group.silent }) } this.saveGroups() }, /** 单群刷新:用 /im/group/get 拉一份最新元数据再 upsert,常用于 GROUP_UPDATE 推送后或手动 reload */ async fetchGroupInfo(groupId: number) { try { const data = await apiGetGroup(groupId) if (!data) { return } this.upsertGroup(convertGroup(data)) } catch (e) { console.warn('[IM groupStore] fetchGroupInfo 失败', e) } }, /** 按群拉取成员(in-memory 缓存 + 并发去重,force=true 强刷)+ 落 IDB */ fetchGroupMembers(groupId: number, force = false): Promise { // in-memory "完整"加载过才命中——单成员补齐写入的 partial members 不在此返回(membersLoaded=false) const cached = this.getGroup(groupId) if (cached && cached.members && cached.membersLoaded && !force) { return Promise.resolve(cached.members) } // 未登录:不发起请求也不登记 in-flight,避免污染单飞表 const requestUserId = getCurrentUserId() if (!requestUserId) { return Promise.resolve([]) } // 同 (userId, groupId) 已经有正在飞的请求:直接复用,避免重复打接口 const key = pendingMemberKey(requestUserId, groupId) const inflight = pendingMemberFetches.get(key) if (inflight) { return inflight } const promise = (async () => { // 拉接口 + 单 pass 转换:同时捕获 me 的原始 VO,给下面回填 user-per-group 字段(silent / groupRemark)用 const list = await apiGetGroupMemberList(groupId) let meRaw: ImGroupMemberRespVO | undefined const members = (list || []).map((member) => { if (member.userId === requestUserId) { meRaw = member } return convertGroupMember(member, groupId) }) const silent = !!meRaw?.silent const groupRemark = meRaw?.groupRemark || '' // 必须 await 之后重新 getGroup,避免 fetchGroups 已并发写入真实 group 的 race const group = this.getGroup(groupId) const isPlaceholder = !group let groupFieldsChanged = false if (!group) { // group 还没就位:仅 in-memory push 占位(name='' 表示未知),不调 upsertGroup——避免把假名灌进 conversation.name + groups IDB 桶; // 后续,等 fetchGroups 浅合并时,被真名覆盖 this.groups.push({ id: groupId, name: '', members, memberCount: members.length, silent, groupRemark, membersLoaded: true }) } else { group.members = members group.memberCount = members.length group.membersLoaded = true // silent / groupRemark 任一变化才同步到 conversation 和 IDB;groupRemark 变化要顺带刷会话名 if (group.silent !== silent || group.groupRemark !== groupRemark) { group.silent = silent group.groupRemark = groupRemark groupFieldsChanged = true const conversationStore = useConversationStore() conversationStore.updateConversation(ImConversationType.GROUP, groupId, { name: getGroupDisplayName(group), silent }) } } // groups 桶仅在 user-per-group 字段实际变化时写——避免一次批量进群引发多次整桶重写 this.saveGroupMembers(groupId) if (!isPlaceholder && groupFieldsChanged) { this.saveGroups() } return members // 无论成功 / 失败都要从单飞表清掉,否则后续同 group 请求永远拿到这个 stale Promise })().finally(() => pendingMemberFetches.delete(key)) // 把 Promise 登记进单飞表,让此后短时间内的同 (userId, groupId) 请求复用 pendingMemberFetches.set(key, promise) return promise }, /** * 按 (groupId, memberUserId) 单成员补齐——deriveLastSenderDisplayName 兜底场景用 * * 跟 fetchGroupMembers 区别:只拉这一个成员,不动 me 的 silent / groupRemark(不是 me 的话拿不到); * 命中时把成员 upsert 进 group.members 数组并落 IDB,让后续渲染能用 displayUserName */ fetchGroupMember(groupId: number, memberUserId: number): Promise { // in-memory 命中直接返回,不打接口 const cached = this.getGroup(groupId)?.members?.find((m) => m.userId === memberUserId) if (cached) { return Promise.resolve(cached) } // 未登录:不发起请求也不登记 in-flight,避免污染单飞表 const requestUserId = getCurrentUserId() if (!requestUserId) { return Promise.resolve(null) } // 同 (userId, groupId, memberUserId) 已经有正在飞的请求:直接复用 const key = pendingSingleMemberKey(requestUserId, groupId, memberUserId) const inflight = pendingSingleMemberFetches.get(key) if (inflight) { return inflight } const promise = (async () => { const data = await apiGetGroupMember(groupId, memberUserId) if (!data) { return null } const member = convertGroupMember(data, groupId) // 把这一条 upsert 进 group.members 仅供 in-memory 渲染兜底;group 还没就位则用 placeholder // 注意:不写 IDB——成员桶语义是"全量",存"1 人桶"会污染下次冷启动的 loadGroupMembers const group = this.getGroup(groupId) if (!group) { // memberCount 不设:后续 fetchGroups 合并 `existing.memberCount ?? fresh.memberCount` 时, // 占位值会顶替真实值(fresh 不带 memberCount),等 fetchGroupMembers 跑过才能拿到真实数 this.groups.push({ id: groupId, name: '', members: [member] }) } else { const memberList = group.members ?? [] const index = memberList.findIndex((m) => m.userId === memberUserId) if (index >= 0) { memberList[index] = member } else { memberList.push(member) } group.members = memberList } return member })().finally(() => pendingSingleMemberFetches.delete(key)) pendingSingleMemberFetches.set(key, promise) return promise }, /** 按 id 插入或合并群(命中则浅合并保留旧字段,未命中则追加),同步把展示名 / 头像 / 免打扰推到对应会话 */ upsertGroup(group: Group) { const index = this.groups.findIndex((g) => g.id === group.id) if (index >= 0) { this.groups[index] = { ...this.groups[index], ...group } } else { this.groups.push(group) } // 同步推到 conversation:群名 / 头像 / 免打扰是会话列表展示用的,必须紧随 group 变更 const merged = this.getGroup(group.id) ?? group const conversationStore = useConversationStore() conversationStore.updateConversation(ImConversationType.GROUP, group.id, { name: getGroupDisplayName(merged), avatar: merged.avatar, silent: merged.silent }) // 持久化到 IDB(fire-and-forget) this.saveGroups() }, /** 本地移除(由 WebSocket GROUP_DEL 事件触发) */ removeGroup(id: number) { // 群解散是硬删(区别于好友删除的软删保留记录);级联清群聊会话避免列表里留死群 this.groups = this.groups.filter((g) => g.id !== id) const conversationStore = useConversationStore() conversationStore.removeGroupConversation(id) this.saveGroups() // 群解散后顺手删 IDB 里该群的成员桶——这桶仅靠 groupId 索引,不删会一直留在 IDB 占空间 const userId = getCurrentUserId() if (userId) { removeQuietly( StorageKeys.groupMembers(userId, id), `[IM groupStore] 群成员缓存删除失败 (groupId=${id})` ) } }, /** 切换免打扰:推后端 + 落本地 */ async setSilent(id: number, silent: boolean) { await apiUpdateGroupMember({ groupId: id, silent }) const group = this.getGroup(id) if (!group) { return } group.silent = silent this.saveGroups() }, /** 批量更新群成员角色;本地不命中则忽略,等 fetchGroupMembers 兜底 */ updateMembersRole(groupId: number, userIds: number[], role: number) { const group = this.getGroup(groupId) if (!group?.members?.length) { return } // 命中目标且角色已变化才标记 changed,避免无变化时整数组重建触发响应式 const idSet = new Set(userIds) let changed = false const newMembers = group.members.map((member) => { if (!idSet.has(member.userId) || member.role === role) { return member } changed = true return { ...member, role } }) // 有变化才整组替换,让响应式只在真有更新时通知下游 if (changed) { group.members = newMembers } }, /** 群主转让:群表 ownerUserId 改为新值;旧群主 role → NORMAL;新群主 role → OWNER */ transferOwner(groupId: number, oldOwnerId: number, newOwnerId: number) { const group = this.getGroup(groupId) if (!group) { return } if (group.ownerUserId !== newOwnerId) { group.ownerUserId = newOwnerId } this.updateMembersRole(groupId, [oldOwnerId], ImGroupMemberRole.NORMAL) this.updateMembersRole(groupId, [newOwnerId], ImGroupMemberRole.OWNER) this.saveGroups() }, /** 本地剔除群成员(GROUP_MEMBER_QUIT / KICK 事件);不命中则等 fetchGroupMembers 兜底 */ removeMembersLocal(groupId: number, userIds: number[]) { const group = this.getGroup(groupId) if (!group?.members?.length || !userIds.length) { return } const idSet = new Set(userIds) const next = group.members.filter((member) => !idSet.has(member.userId)) if (next.length === group.members.length) { return } group.members = next group.memberCount = next.length this.saveGroupMembers(groupId) }, /** 本地更新群成员的 displayUserName(GROUP_MEMBER_NICKNAME_UPDATE 事件);不命中则等 fetchGroupMembers 兜底 */ updateMemberDisplayUserName(groupId: number, userId: number, displayUserName: string) { const group = this.getGroup(groupId) const member = group?.members?.find((m) => m.userId === userId) if (!member || member.displayUserName === displayUserName) { return } member.displayUserName = displayUserName this.saveGroupMembers(groupId) }, /** 局部更新群字段(name / notice / avatar 等);未命中本地缓存时静默忽略,等 fetchGroups 兜底;新值跟旧值都相同时跳过响应式 + IDB 写 */ updateGroupFields(groupId: number, fields: Partial) { const group = this.getGroup(groupId) if (!group) { return } const changed = (Object.keys(fields) as (keyof Group)[]).some((k) => group[k] !== fields[k]) if (!changed) { return } Object.assign(group, fields) const conversationStore = useConversationStore() conversationStore.updateConversation(ImConversationType.GROUP, groupId, { name: getGroupDisplayName(group), avatar: group.avatar, silent: group.silent }) this.saveGroups() }, /** * 接收 GROUP_* 群广播事件,按 type 分发到对应私有 action * * WebSocket 实时收 + useMessagePuller 离线 pull 都走 conversationStore.insertMessage 旁路调用 * store 里没缓存的群静默忽略,等 fetchGroups 兜底 */ applyGroupNotification(groupId: number, type: number, content?: string) { if (!groupId) { return } let payload: GroupNotificationPayload = {} try { payload = content ? JSON.parse(content) : {} } catch (error) { console.warn( '[IM groupStore] applyGroupNotification 解析 content 失败', { groupId, type, contentLength: content?.length ?? 0 }, error ) return } switch (type) { case ImMessageType.GROUP_CREATE: this.applyGroupCreateNotification(groupId, payload) break case ImMessageType.GROUP_NAME_UPDATE: this.applyGroupNameUpdateNotification(groupId, payload) break case ImMessageType.GROUP_NOTICE_UPDATE: this.applyGroupNoticeUpdateNotification(groupId, payload) break case ImMessageType.GROUP_INFO_UPDATE: this.applyGroupInfoUpdateNotification(groupId, payload) break case ImMessageType.GROUP_DISSOLVE: this.removeGroup(groupId) break case ImMessageType.GROUP_MEMBER_INVITE: this.applyGroupMemberInviteNotification(groupId, payload) break case ImMessageType.GROUP_MEMBER_QUIT: this.applyGroupMemberQuitNotification(groupId, payload) break case ImMessageType.GROUP_MEMBER_KICK: this.applyGroupMemberKickNotification(groupId, payload) break case ImMessageType.GROUP_MEMBER_NICKNAME_UPDATE: this.applyGroupMemberNicknameUpdateNotification(groupId, payload) break case ImMessageType.GROUP_ADMIN_ADD: this.updateMembersRole(groupId, payload.memberUserIds || [], ImGroupMemberRole.ADMIN) break case ImMessageType.GROUP_ADMIN_REMOVE: this.updateMembersRole(groupId, payload.memberUserIds || [], ImGroupMemberRole.NORMAL) break case ImMessageType.GROUP_OWNER_TRANSFER: this.applyGroupOwnerTransferNotification(groupId, payload) break case ImMessageType.GROUP_MESSAGE_PIN: this.applyGroupMessagePinNotification(groupId, payload) break case ImMessageType.GROUP_MESSAGE_UNPIN: this.applyGroupMessageUnpinNotification(groupId, payload) break case ImMessageType.GROUP_MEMBER_MUTED: this.applyGroupMemberMutedNotification(groupId, payload) break case ImMessageType.GROUP_MEMBER_CANCEL_MUTED: this.applyGroupMemberCancelMutedNotification(groupId, payload) break case ImMessageType.GROUP_MUTED: this.updateGroupFields(groupId, { mutedAll: true }) break case ImMessageType.GROUP_CANCEL_MUTED: this.updateGroupFields(groupId, { mutedAll: false }) break case ImMessageType.GROUP_BANNED: this.updateGroupFields(groupId, { banned: !!payload.banned }) break } }, /** 创建群广播:创建者多端同步 + 初始成员 bootstrap;payload.memberUserIds 含自己 → 拉群详情 / 成员;本端发起者已经 upsert 过本群,跳过避免双拉 */ applyGroupCreateNotification(groupId: number, payload: GroupNotificationPayload) { if (!isSelfInPayloadMembers(payload)) { return } const selfUserId = getCurrentUserId() const selfIsOperator = !!selfUserId && payload.operatorUserId === selfUserId if (selfIsOperator && this.getGroup(groupId)) { return } this.fetchGroupInfo(groupId).catch(() => undefined) this.fetchGroupMembers(groupId, true).catch(() => undefined) }, /** 群名变更:按 newName 局部更新本地群名 */ applyGroupNameUpdateNotification(groupId: number, payload: GroupNotificationPayload) { if (payload.newName) { this.updateGroupFields(groupId, { name: payload.newName }) } }, /** 群公告变更:按 newNotice 局部更新(允许空串作为「清空公告」) */ applyGroupNoticeUpdateNotification(groupId: number, payload: GroupNotificationPayload) { this.updateGroupFields(groupId, { notice: payload.newNotice ?? '' }) }, /** 群信息变更(NAME / NOTICE 之外字段,当前承载头像变更) */ applyGroupInfoUpdateNotification(groupId: number, payload: GroupNotificationPayload) { const fields: Partial = {} if (payload.newAvatar) { fields.avatar = payload.newAvatar } if (Object.keys(fields).length > 0) { this.updateGroupFields(groupId, fields) } }, /** 成员加入:被邀请者本端 group 未就位先 fetchGroupInfo bootstrap;所有人都刷成员列表(新成员 nickname / avatar 不在 payload) */ applyGroupMemberInviteNotification(groupId: number, payload: GroupNotificationPayload) { if (isSelfInPayloadMembers(payload) && !this.getGroup(groupId)) { this.fetchGroupInfo(groupId).catch(() => undefined) } this.fetchGroupMembers(groupId, true).catch(() => undefined) }, /** 成员退群:退群者本人多端同步走 removeGroup;其他成员从本地列表移除 quitter */ applyGroupMemberQuitNotification(groupId: number, payload: GroupNotificationPayload) { const selfUserId = getCurrentUserId() if (selfUserId && payload.operatorUserId === selfUserId) { this.removeGroup(groupId) } else if (payload.operatorUserId) { this.removeMembersLocal(groupId, [payload.operatorUserId]) } }, /** 成员被移出:被踢者本人 removeGroup;其他成员从本地列表移除被踢者 */ applyGroupMemberKickNotification(groupId: number, payload: GroupNotificationPayload) { const memberIds = payload.memberUserIds || [] if (isSelfInPayloadMembers(payload)) { this.removeGroup(groupId) } else if (memberIds.length) { this.removeMembersLocal(groupId, memberIds) } }, /** 成员昵称变更:按 operatorUserId 局部更新对应 member.displayUserName */ applyGroupMemberNicknameUpdateNotification(groupId: number, payload: GroupNotificationPayload) { if (payload.operatorUserId) { this.updateMemberDisplayUserName( groupId, payload.operatorUserId, payload.displayUserName ?? '' ) } }, /** 群主转让:旧群主 → NORMAL,新群主 → OWNER */ applyGroupOwnerTransferNotification(groupId: number, payload: GroupNotificationPayload) { if (payload.operatorUserId && payload.newOwnerUserId) { this.transferOwner(groupId, payload.operatorUserId, payload.newOwnerUserId) } }, /** 群消息置顶:从 payload 直接拿完整消息对象 push 到 pinnedMessages */ applyGroupMessagePinNotification(groupId: number, payload: GroupNotificationPayload) { const message = payload.message if (!message) { return } const group = this.getGroup(groupId) if (!group) { return } // 幂等:已存在同 messageId 不重复 push const existing = group.pinnedMessages || [] if (existing.some((msg) => msg.id === message.id)) { return } // message 字段名跟 Message 一致(仅 sendTime 需要从 ISO 串转毫秒戳,selfSend 按当前用户算) const newMessage: Message = { ...message, clientMessageId: message.clientMessageId || '', sendTime: new Date(message.sendTime).getTime(), targetId: message.groupId, selfSend: message.senderId === getCurrentUserId(), atUserIds: message.atUserIds || [], receiverUserIds: message.receiverUserIds || [] } group.pinnedMessages = [...existing, newMessage] this.saveGroups() }, /** 群消息取消置顶:按 messageId 从本地置顶列表中移除 */ applyGroupMessageUnpinNotification(groupId: number, payload: GroupNotificationPayload) { if (!payload.messageId) { return } const group = this.getGroup(groupId) if (!group?.pinnedMessages?.length) { return } const newPinnedMessages = group.pinnedMessages.filter((m) => m.id !== payload.messageId) if (newPinnedMessages.length === group.pinnedMessages.length) { return } group.pinnedMessages = newPinnedMessages this.saveGroups() }, /** 单成员禁言:更新目标成员的 muteEndTime */ applyGroupMemberMutedNotification(groupId: number, payload: GroupNotificationPayload) { const group = this.getGroup(groupId) const member = group?.members?.find((m) => m.userId === payload.mutedUserId) if (member && payload.muteEndTime) { member.muteEndTime = payload.muteEndTime this.saveGroupMembers(groupId) } }, /** 单成员取消禁言:清空目标成员的 muteEndTime */ applyGroupMemberCancelMutedNotification(groupId: number, payload: GroupNotificationPayload) { const group = this.getGroup(groupId) const member = group?.members?.find((m) => m.userId === payload.mutedUserId) if (member) { member.muteEndTime = undefined this.saveGroupMembers(groupId) } }, /** 切账号时仅清 in-memory,IDB 按 userId 分桶天然隔离,回切秒开 */ clear() { this.groups = [] this.loaded = false // 单飞表跟 in-memory state 一起重置;旧账号 in-flight 的请求 finally 也会自己 delete key,提前清空只是更干脆 pendingMemberFetches.clear() pendingSingleMemberFetches.clear() } } }) function convertGroup(group: ImGroupRespVO): Group { return { id: group.id, name: group.name, avatar: group.avatar, notice: group.notice, ownerUserId: group.ownerUserId, pinnedMessages: group.pinnedMessages?.map(convertGroupMessageVO), mutedAll: group.mutedAll, banned: group.banned } } /** 后端 ImGroupMessageRespVO -> 前端 Message:补 targetId / selfSend / sendTime 等派生字段 */ function convertGroupMessageVO(message: NonNullable[number]): Message { const currentUserId = getCurrentUserId() return { id: message.id, clientMessageId: message.clientMessageId || '', type: message.type, content: message.content, status: message.status, sendTime: new Date(message.sendTime).getTime(), senderId: message.senderId, targetId: message.groupId, selfSend: !!currentUserId && message.senderId === currentUserId, atUserIds: message.atUserIds || [], receiverUserIds: message.receiverUserIds || [], receiptStatus: message.receiptStatus, readCount: message.readCount } } function convertGroupMember(member: ImGroupMemberRespVO, groupId: number): GroupMember { return { id: member.id, userId: member.userId, groupId, nickname: member.nickname || String(member.userId), avatar: member.avatar, displayUserName: member.displayUserName, status: member.status, role: member.role, muteEndTime: member.muteEndTime } } export const useGroupStoreWithOut = () => useGroupStore(store) // dev: 让 Pinia 的 actions 改动支持 HMR,免去每次改 store 都要硬刷 if (import.meta.hot) { import.meta.hot.accept(acceptHMRUpdate(useGroupStore, import.meta.hot)) }