✨ feat(im): IM 5 个 store 补 HMR + 抽 atAll 常量 + 全面补齐 JSDoc
- 全部 5 个 store(conversation / friend / group / ui / websocket)加 acceptHMRUpdate;Pinia 单例的 actions 是 wrapper 闭包,Vite 推新模块时 不会自动替换闭包内的旧函数体,导致改 store 后看着热重载、跑的还是旧逻辑 - 抽 IM_AT_ALL_USER_ID(-1)+ IM_AT_ALL_NICKNAME('所有人')到 utils/constants.ts;conversationStore 删本地 AT_ALL_FLAG 改用共享常量; MentionPicker 渲染虚拟项 / ChatGroupMember 类型注释也都引这两个常量 - groupStore.loadGroups 改成合并而非全量替换:用 groupMap 按 id 找已有项, 保留 loadGroupMembers 写过的 members / memberCount / muted(这三个字段 不在 ImGroupRespVO 里,全量替换会被冲掉) - groupStore.loadGroupMembers 重写为分步注释(1. 缓存 / 2. 拉取 / 3. 回填 muted / 4.1 占位 / 4.2 直写);await 之后必须重新 getGroup 防 race(loadGroupMembers 与 loadGroups 并发时用入口快照会把真实 name 覆盖成 String(groupId)) - types/GroupMember 补 muted 字段,convertGroupMember 透传, 解决 vue-tsc TS2339 / TS2353 - 5 个 store 缺 JSDoc 的方法全部补齐:removePrivateConversation / removeGroupConversation / getFriend / getActiveFriends / isFriend / loadGroupInfo / upsertGroup / stopHeartbeat - 全局"墓碑"措辞统一为"软删保留记录",types / friendStore / groupStore 三处 - groupStore 删冗余注释(与代码自描述重复的)若干处;变量 g/old 改 group/existingim
parent
a0ed0d800c
commit
3ea04663f2
|
|
@ -1,4 +1,4 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||||
import { toRaw } from 'vue'
|
||||
import { store } from '@/store'
|
||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
ImConversationType,
|
||||
ImMessageType,
|
||||
ImMessageStatus,
|
||||
IM_AT_ALL_USER_ID,
|
||||
TIME_TIP_GAP_MS
|
||||
} from '../../utils/constants'
|
||||
import { imStorage, StorageKeys } from '../../utils/storage'
|
||||
|
|
@ -19,8 +20,6 @@ import {
|
|||
} from '../../utils/message'
|
||||
import type { Conversation, ConversationStoreMeta, Message } from '../types'
|
||||
|
||||
const AT_ALL_FLAG = -1 // @全体成员 的特殊 userId 标识:atUserIds 中包含 -1 表示 @all
|
||||
|
||||
// TODO @芋艿:单个 conversation 的消息过多后,可能存储起来会很慢,后续看看怎么优化。
|
||||
// TODO @芋艿:首次拉取消息时,如果消息过多,可能导致渲染卡顿。(1% 场景)
|
||||
|
||||
|
|
@ -303,10 +302,12 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
this.saveConversations()
|
||||
},
|
||||
|
||||
/** 删私聊会话的语义糖:friendStore 删好友时调,避免外面手写 ImConversationType.PRIVATE */
|
||||
removePrivateConversation(friendId: number) {
|
||||
this.removeConversation(ImConversationType.PRIVATE, friendId)
|
||||
},
|
||||
|
||||
/** 删群聊会话的语义糖:groupStore 群解散时调,避免外面手写 ImConversationType.GROUP */
|
||||
removeGroupConversation(groupId: number) {
|
||||
this.removeConversation(ImConversationType.GROUP, groupId)
|
||||
},
|
||||
|
|
@ -371,7 +372,7 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
if (currentUserId && messageInfo.atUserIds.includes(currentUserId)) {
|
||||
conversation.atMe = true
|
||||
}
|
||||
if (messageInfo.atUserIds.includes(AT_ALL_FLAG)) {
|
||||
if (messageInfo.atUserIds.includes(IM_AT_ALL_USER_ID)) {
|
||||
conversation.atAll = true
|
||||
}
|
||||
}
|
||||
|
|
@ -674,3 +675,9 @@ export const useConversationStore = defineStore('imConversationStore', {
|
|||
export const useConversationStoreWithOut = () => {
|
||||
return useConversationStore(store)
|
||||
}
|
||||
|
||||
// dev: 让 Pinia 的 actions / state 改动支持 HMR,避免每次改 store 都得硬刷
|
||||
// 否则 Vite 把新模块推下来后,老 store 实例的 action 闭包仍指向旧函数体
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useConversationStore, import.meta.hot))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||||
import { store } from '@/store'
|
||||
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
|
@ -29,14 +29,17 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
}),
|
||||
|
||||
getters: {
|
||||
/** 按 friendUserId 找好友(含已软删的 DISABLE 记录,调用方自行判定) */
|
||||
getFriend:
|
||||
(state) =>
|
||||
(friendUserId: number): Friend | undefined => {
|
||||
return state.friends.find((f) => f.friendUserId === friendUserId)
|
||||
},
|
||||
/** 当前生效的好友列表(过滤掉 DISABLE 软删记录) */
|
||||
getActiveFriends: (state): Friend[] => {
|
||||
return state.friends.filter((f) => f.status !== CommonStatusEnum.DISABLE)
|
||||
},
|
||||
/** 判断对方是否是当前用户的有效好友(存在 + 非 DISABLE) */
|
||||
isFriend() {
|
||||
return (friendUserId: number): boolean => {
|
||||
const entry = this.getFriend(friendUserId)
|
||||
|
|
@ -93,7 +96,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
}
|
||||
},
|
||||
|
||||
/** 删除好友(保留墓碑记录,同时级联清理本地私聊会话) */
|
||||
/** 删除好友(软删,保留记录但置 DISABLE;同时级联清理本地私聊会话) */
|
||||
async deleteFriend(friendUserId: number) {
|
||||
await apiDeleteFriend(friendUserId)
|
||||
this.removeFriend(friendUserId)
|
||||
|
|
@ -168,3 +171,9 @@ function convertFriend(vo: ImFriendRespVO): Friend {
|
|||
}
|
||||
|
||||
export const useFriendStoreWithOut = () => useFriendStore(store)
|
||||
|
||||
// dev: 让 Pinia 的 actions / state 改动支持 HMR,避免每次改 store 都得硬刷
|
||||
// 否则 Vite 把新模块推下来后,老 store 实例的 action 闭包仍指向旧函数体
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useFriendStore, import.meta.hot))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||||
import { store } from '@/store'
|
||||
|
||||
import {
|
||||
|
|
@ -46,19 +46,34 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
}
|
||||
// 拉取当前登录用户加入的所有群(不带成员;成员按需再走 loadGroupMembers)
|
||||
const list = await apiGetMyGroupList()
|
||||
this.groups = (list || []).map(convertGroup)
|
||||
const fresh = (list || []).map(convertGroup)
|
||||
// 合并而非全量替换:保留 loadGroupMembers 已经写入的 members / memberCount / muted
|
||||
// (这些字段不在 ImGroupRespVO 里,全量替换会把成员级数据全冲掉)
|
||||
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,
|
||||
muted: existing.muted ?? group.muted
|
||||
}
|
||||
})
|
||||
this.loaded = true
|
||||
const conversationStore = useConversationStore()
|
||||
for (const g of this.groups) {
|
||||
conversationStore.updateConversation(ImConversationType.GROUP, g.id, {
|
||||
name: g.name,
|
||||
avatar: g.avatar,
|
||||
muted: g.muted
|
||||
for (const group of this.groups) {
|
||||
conversationStore.updateConversation(ImConversationType.GROUP, group.id, {
|
||||
name: group.name,
|
||||
avatar: group.avatar,
|
||||
muted: group.muted
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/** 刷新单个群详情 */
|
||||
/** 单群刷新:用 /im/group/get 拉一份最新元数据再 upsert,常用于 GROUP_UPDATE 推送后或手动 reload */
|
||||
async loadGroupInfo(groupId: number) {
|
||||
try {
|
||||
const data = await apiGetGroup(groupId)
|
||||
|
|
@ -71,24 +86,39 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
}
|
||||
},
|
||||
|
||||
/** 按群拉取成员(带缓存,force=true 强制刷新) */
|
||||
/**
|
||||
* 按群拉取成员(带缓存,force=true 强制刷新)
|
||||
*
|
||||
* 1. 缓存:group 已加载且 members 就绪 → 直接返回
|
||||
* 2. 拉取 + 转换:调 /im/group-member/list 后映射成本地 GroupMember
|
||||
* 3. 回填当前用户的 muted:
|
||||
* 后端只在成员维度返回 muted(apiGetMyGroupList 不带),借这次拉成员把它落到 group / conversation;
|
||||
* 否则冷启动 / 清缓存后,服务端已免打扰的群在会话列表里仍显示为未免打扰
|
||||
* 4. 落地(关键:race-safe 重新 getGroup):
|
||||
* apiGetGroupMemberList 期间 loadGroups 可能已经把真实 group 填进 store,
|
||||
* 沿用入口快照会让我们错走 4.1 分支、把真实 name 覆盖成 String(groupId)
|
||||
* 4.1 group 还没就位(loadGroupMembers 跑在 loadGroups 之前)→ 占位 upsertGroup
|
||||
* 4.2 group 已就位 → 直接写 members 字段,并把 muted 单独推回 conversation
|
||||
*/
|
||||
async loadGroupMembers(groupId: number, force = false): Promise<GroupMember[]> {
|
||||
// 命中缓存:群已加载且成员列表已就绪,直接返回(force=true 时强制刷)
|
||||
const group = this.getGroup(groupId)
|
||||
if (group && group.members && !force) {
|
||||
return group.members
|
||||
// 1. 缓存
|
||||
const cached = this.getGroup(groupId)
|
||||
if (cached && cached.members && !force) {
|
||||
return cached.members
|
||||
}
|
||||
|
||||
// 拉取该群所有成员(聚合自 AdminUser,含 nickname / avatar / displayUserName)
|
||||
// 2. 拉取 + 转换
|
||||
const list = await apiGetGroupMemberList(groupId)
|
||||
const members = (list || []).map((member) => convertGroupMember(member, groupId))
|
||||
// 后端只在成员维度返回当前用户的 muted(apiGetMyGroupList 不带),借这次拉成员一起回填
|
||||
// 否则冷启动 / 清缓存后,服务端已免打扰的群在会话列表里仍显示为未免打扰
|
||||
|
||||
// 3. 回填 muted
|
||||
const userStore = useUserStore()
|
||||
const currentUserId = Number(userStore.getUser?.id) || 0
|
||||
const me = members.find((m) => m.userId === currentUserId)
|
||||
const muted = !!me?.muted
|
||||
// 成员列表可能在群列表之前触发,此时需要占位一个 group
|
||||
|
||||
// 4. 落地(必须 await 之后重新 getGroup,避免踩 race)
|
||||
const group = this.getGroup(groupId)
|
||||
if (!group) {
|
||||
this.upsertGroup({
|
||||
id: groupId,
|
||||
|
|
@ -101,22 +131,20 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
group.members = members
|
||||
group.memberCount = members.length
|
||||
group.muted = muted
|
||||
// 已有 group 分支没走 upsertGroup,单独把 muted 推回 conversation 保证会话列表展示一致
|
||||
const conversationStore = useConversationStore()
|
||||
conversationStore.updateConversation(ImConversationType.GROUP, groupId, { muted })
|
||||
}
|
||||
return members
|
||||
},
|
||||
|
||||
/** 按 id 插入或合并群(命中则浅合并保留旧字段,未命中则追加),同步把 name / avatar / muted 推到对应会话 */
|
||||
upsertGroup(group: Group) {
|
||||
// 按 id 查已有记录下标:>=0 命中则覆盖合并,<0 则追加
|
||||
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)
|
||||
}
|
||||
// 同步对应群聊会话的展示
|
||||
const conversationStore = useConversationStore()
|
||||
conversationStore.updateConversation(ImConversationType.GROUP, group.id, {
|
||||
name: group.name,
|
||||
|
|
@ -125,16 +153,15 @@ export const useGroupStore = defineStore('imGroupStore', {
|
|||
})
|
||||
},
|
||||
|
||||
/** 本地移除(WebSocket GROUP_DEL 事件触发;同时级联清群聊会话) */
|
||||
/** 本地移除(由 WebSocket GROUP_DEL 事件触发) */
|
||||
removeGroup(id: number) {
|
||||
// 直接从本地列表里移除(群解散是硬删,不留墓碑,区别于好友的软删)
|
||||
// 群解散是硬删(区别于好友删除的软删保留记录);级联清群聊会话避免列表里留死群
|
||||
this.groups = this.groups.filter((g) => g.id !== id)
|
||||
// 级联清理:对应群聊会话也软删,避免会话列表里留着已解散的群
|
||||
const conversationStore = useConversationStore()
|
||||
conversationStore.removeGroupConversation(id)
|
||||
},
|
||||
|
||||
/** 切换免打扰:调 /im/group-member/update 推后端,再把当前用户在该群的 muted 标记落到本地 */
|
||||
/** 切换免打扰:推后端 + 落本地 */
|
||||
async setMuted(id: number, muted: boolean) {
|
||||
await apiUpdateGroupMember({ groupId: id, muted })
|
||||
const group = this.getGroup(id)
|
||||
|
|
@ -176,3 +203,8 @@ function convertGroupMember(member: ImGroupMemberRespVO, groupId: number): Group
|
|||
}
|
||||
|
||||
export const useGroupStoreWithOut = () => useGroupStore(store)
|
||||
|
||||
// dev: 让 Pinia 的 actions 改动支持 HMR,免去每次改 store 都要硬刷
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useGroupStore, import.meta.hot))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
import type { UserInfo } from '../types'
|
||||
|
|
@ -80,3 +80,9 @@ export const useImUiStore = defineStore('imUiStore', () => {
|
|||
closeContextMenu
|
||||
}
|
||||
})
|
||||
|
||||
// dev: 让 Pinia 的 actions / state 改动支持 HMR,避免每次改 store 都得硬刷
|
||||
// 否则 Vite 把新模块推下来后,老 store 实例的 action 闭包仍指向旧函数体
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useImUiStore, import.meta.hot))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||||
import { store } from '@/store'
|
||||
import { getRefreshToken } from '@/utils/auth'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
|
@ -582,6 +582,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
}, 5000)
|
||||
},
|
||||
|
||||
/** 停心跳:disconnect / 重连前调,避免老 timer 在新 socket 上继续触发 sendHeartBeat */
|
||||
stopHeartbeat() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
|
|
@ -594,3 +595,9 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
export const useImWebSocketStoreWithOut = () => {
|
||||
return useImWebSocketStore(store)
|
||||
}
|
||||
|
||||
// dev: 让 Pinia 的 actions / state 改动支持 HMR,避免每次改 store 都得硬刷
|
||||
// 否则 Vite 把新模块推下来后,老 store 实例的 action 闭包仍指向旧函数体
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept(acceptHMRUpdate(useImWebSocketStore, import.meta.hot))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export interface Friend {
|
|||
nickname: string // 好友昵称
|
||||
avatar?: string // 好友头像
|
||||
muted?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
|
||||
status?: number // 好友状态,对齐 CommonStatusEnum(DISABLE = 已删除/墓碑)
|
||||
status?: number // 好友状态,对齐 CommonStatusEnum(DISABLE = 已删除,软删保留记录)
|
||||
addTime?: number // 添加好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换)
|
||||
deleteTime?: number // 删除好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,3 +73,14 @@ export const GROUP_MESSAGE_PULL_SIZE = 100
|
|||
|
||||
/** 会话之间插入"时间分隔线"的阈值:10 分钟 */
|
||||
export const TIME_TIP_GAP_MS = 10 * 60 * 1000
|
||||
|
||||
/**
|
||||
* @全体成员 的特殊 userId 标识:atUserIds 中包含 -1 表示 @ 全体成员
|
||||
*
|
||||
* 与后端约定:群消息 atUserIds 数组里出现 -1 时,所有成员都收到提醒
|
||||
* MentionPicker 渲染虚拟项 + conversationStore.applyAt 判定 atAll 都靠这个值
|
||||
*/
|
||||
export const IM_AT_ALL_USER_ID = -1
|
||||
|
||||
/** @全体成员 的展示名(对齐微信 PC) */
|
||||
export const IM_AT_ALL_NICKNAME = '所有人'
|
||||
|
|
|
|||
Loading…
Reference in New Issue