✨ feat(im): 修一批前端性能 / 跨账号防御与侧边栏占位
- friendStore.getFriend 改 friendMap 索引,高频反查从 O(N) 降到 O(1) - faceStore 加 storeEpoch,切账号后旧表情拉取 / 增删响应不再回写新账号 - friendStore 写路径统一补 epoch 守卫(loadFriendInfo / 单查申请 / 删好友 / 免打扰 / 置顶 / 拉黑 / 备注),切账号瞬间的旧响应不污染新账号好友状态 - 私聊侧边栏 friend 缺失时给加载占位,替代原本的空白抽屉im
parent
9893aedbb2
commit
38ecc4f40c
|
|
@ -13,7 +13,15 @@
|
||||||
append-to-body
|
append-to-body
|
||||||
modal-class="im-conversation-private-side__modal"
|
modal-class="im-conversation-private-side__modal"
|
||||||
>
|
>
|
||||||
<div v-if="friend" class="flex flex-col h-full bg-[var(--el-bg-color)]">
|
<!-- friend 缺失场景:陌生人会话刚打开 / 好友数据还没补拉到;空白会让用户以为抽屉坏了,给个加载占位 -->
|
||||||
|
<div
|
||||||
|
v-if="!friend"
|
||||||
|
v-loading="true"
|
||||||
|
class="flex flex-col items-center justify-center h-full text-13px text-[var(--el-text-color-placeholder)] bg-[var(--el-bg-color)]"
|
||||||
|
>
|
||||||
|
加载中…
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col h-full bg-[var(--el-bg-color)]">
|
||||||
<div class="flex-1 overflow-y-auto bg-[var(--el-fill-color-light)]">
|
<div class="flex-1 overflow-y-auto bg-[var(--el-fill-color-light)]">
|
||||||
<!-- 好友宫格:原 tile + "+" tile,对齐 GroupSide 视觉,让两种抽屉看起来是一家的 -->
|
<!-- 好友宫格:原 tile + "+" tile,对齐 GroupSide 视觉,让两种抽屉看起来是一家的 -->
|
||||||
<div class="flex flex-wrap gap-1 px-4 pt-4 pb-[14px] bg-[var(--el-bg-color)]">
|
<div class="flex flex-wrap gap-1 px-4 pt-4 pb-[14px] bg-[var(--el-bg-color)]">
|
||||||
|
|
@ -194,7 +202,7 @@ watch(visible, (open) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 备注 popover 点击保存:先走 store API 同步后端,成功后再关 popover + 提示 */
|
/** 备注 popover 点击保存:先走 store API 同步后端,成功后再关 popover + 提示(接口错误由全局拦截器统一 toast,不重复 catch) */
|
||||||
async function handleSaveDisplayName() {
|
async function handleSaveDisplayName() {
|
||||||
if (!props.friend) {
|
if (!props.friend) {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ export const useFaceStore = defineStore('imFace', () => {
|
||||||
/** 个人表情包列表(用户长按「添加到表情」/ 上传产生) */
|
/** 个人表情包列表(用户长按「添加到表情」/ 上传产生) */
|
||||||
const faceUserItems = ref<ImFaceUserItemVO[]>([])
|
const faceUserItems = ref<ImFaceUserItemVO[]>([])
|
||||||
|
|
||||||
|
/** reset() 时递增;旧账号那次还没返回的请求 resolve 后比对一致才写 store,防跨账号数据泄漏 */
|
||||||
|
let storeEpoch = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统表情包拉取 promise;ensureFacePacks 内 cache:
|
* 系统表情包拉取 promise;ensureFacePacks 内 cache:
|
||||||
* - null = 还没拉过,下次调用真发请求
|
* - null = 还没拉过,下次调用真发请求
|
||||||
|
|
@ -38,13 +41,19 @@ export const useFaceStore = defineStore('imFace', () => {
|
||||||
/** 按需拉取系统表情包(已拉过则直接复用 cached promise) */
|
/** 按需拉取系统表情包(已拉过则直接复用 cached promise) */
|
||||||
async function ensureFacePacks(): Promise<void> {
|
async function ensureFacePacks(): Promise<void> {
|
||||||
if (!facePacksPromise) {
|
if (!facePacksPromise) {
|
||||||
|
const requestEpoch = storeEpoch
|
||||||
facePacksPromise = apiGetFacePackList()
|
facePacksPromise = apiGetFacePackList()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
if (requestEpoch !== storeEpoch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
facePacks.value = data || []
|
facePacks.value = data || []
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.warn('[IM] 拉取表情包失败', e)
|
console.warn('[IM] 拉取表情包失败', e)
|
||||||
facePacksPromise = null
|
if (requestEpoch === storeEpoch) {
|
||||||
|
facePacksPromise = null
|
||||||
|
}
|
||||||
throw e
|
throw e
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -56,13 +65,19 @@ export const useFaceStore = defineStore('imFace', () => {
|
||||||
/** 按需拉取个人表情(已拉过则直接复用 cached promise) */
|
/** 按需拉取个人表情(已拉过则直接复用 cached promise) */
|
||||||
async function ensureFaceUserItems(): Promise<void> {
|
async function ensureFaceUserItems(): Promise<void> {
|
||||||
if (!faceUserItemsPromise) {
|
if (!faceUserItemsPromise) {
|
||||||
|
const requestEpoch = storeEpoch
|
||||||
faceUserItemsPromise = apiGetFaceUserItemList()
|
faceUserItemsPromise = apiGetFaceUserItemList()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
if (requestEpoch !== storeEpoch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
faceUserItems.value = data || []
|
faceUserItems.value = data || []
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.warn('[IM] 拉取个人表情失败', e)
|
console.warn('[IM] 拉取个人表情失败', e)
|
||||||
faceUserItemsPromise = null
|
if (requestEpoch === storeEpoch) {
|
||||||
|
faceUserItemsPromise = null
|
||||||
|
}
|
||||||
throw e
|
throw e
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -76,11 +91,16 @@ export const useFaceStore = defineStore('imFace', () => {
|
||||||
* 返回 true / false 与 removeFaceUserItem 风格对齐;调用方按 boolean 决定是否提示
|
* 返回 true / false 与 removeFaceUserItem 风格对齐;调用方按 boolean 决定是否提示
|
||||||
*/
|
*/
|
||||||
async function addFaceUserItem(reqVO: ImFaceUserItemSaveReqVO): Promise<boolean> {
|
async function addFaceUserItem(reqVO: ImFaceUserItemSaveReqVO): Promise<boolean> {
|
||||||
|
const requestEpoch = storeEpoch
|
||||||
try {
|
try {
|
||||||
const id = await apiCreateFaceUserItem(reqVO)
|
const id = await apiCreateFaceUserItem(reqVO)
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
// reset 已切账号:旧请求拿到的 id 不能再 unshift 进新账号内存
|
||||||
|
if (requestEpoch !== storeEpoch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
// id 不在缓存里才插入;服务端唯一约束兜底了 race,本地理论上不会拿到重复 id
|
// id 不在缓存里才插入;服务端唯一约束兜底了 race,本地理论上不会拿到重复 id
|
||||||
if (!faceUserItems.value.some((item) => item.id === id)) {
|
if (!faceUserItems.value.some((item) => item.id === id)) {
|
||||||
faceUserItems.value.unshift({
|
faceUserItems.value.unshift({
|
||||||
|
|
@ -100,8 +120,13 @@ export const useFaceStore = defineStore('imFace', () => {
|
||||||
|
|
||||||
/** 删除个人表情;本地立即移除 */
|
/** 删除个人表情;本地立即移除 */
|
||||||
async function removeFaceUserItem(id: number): Promise<boolean> {
|
async function removeFaceUserItem(id: number): Promise<boolean> {
|
||||||
|
const requestEpoch = storeEpoch
|
||||||
try {
|
try {
|
||||||
await apiDeleteFaceUserItem(id)
|
await apiDeleteFaceUserItem(id)
|
||||||
|
// reset 已切账号:不要再 filter 新账号列表
|
||||||
|
if (requestEpoch !== storeEpoch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
faceUserItems.value = faceUserItems.value.filter((item) => item.id !== id)
|
faceUserItems.value = faceUserItems.value.filter((item) => item.id !== id)
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -116,6 +141,7 @@ export const useFaceStore = defineStore('imFace', () => {
|
||||||
faceUserItems.value = []
|
faceUserItems.value = []
|
||||||
facePacksPromise = null
|
facePacksPromise = null
|
||||||
faceUserItemsPromise = null
|
faceUserItemsPromise = null
|
||||||
|
storeEpoch++
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -77,12 +77,23 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
|
/**
|
||||||
|
* friendUserId → Friend 的 O(1) 索引;从 friends 数组派生,Pinia 在 friends 变化时自动重算
|
||||||
|
*
|
||||||
|
* 消息渲染需要按 senderId 反查发送人头像 / 备注,每条消息渲染都会 getFriend;
|
||||||
|
* 直接 find 时 N 条消息 × M 好友 = O(N×M);建索引后单次读 O(1),重建只在写好友(fetchFriends / upsertFriend 等)时发生
|
||||||
|
*/
|
||||||
|
friendMap: (state): Map<number, Friend> => {
|
||||||
|
const map = new Map<number, Friend>()
|
||||||
|
for (const friend of state.friends) {
|
||||||
|
map.set(friend.friendUserId, friend)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
},
|
||||||
/** 按 friendUserId 找好友(含已软删的 DISABLE 记录,调用方自行判定) */
|
/** 按 friendUserId 找好友(含已软删的 DISABLE 记录,调用方自行判定) */
|
||||||
getFriend:
|
getFriend(): (friendUserId: number) => Friend | undefined {
|
||||||
(state) =>
|
return (friendUserId: number) => this.friendMap.get(friendUserId)
|
||||||
(friendUserId: number): Friend | undefined => {
|
},
|
||||||
return state.friends.find((friend) => friend.friendUserId === friendUserId)
|
|
||||||
},
|
|
||||||
/** 当前生效的好友列表(过滤掉 DISABLE 软删记录) */
|
/** 当前生效的好友列表(过滤掉 DISABLE 软删记录) */
|
||||||
getActiveFriends: (state): Friend[] => {
|
getActiveFriends: (state): Friend[] => {
|
||||||
return state.friends.filter((friend) => friend.status !== CommonStatusEnum.DISABLE)
|
return state.friends.filter((friend) => friend.status !== CommonStatusEnum.DISABLE)
|
||||||
|
|
@ -195,11 +206,16 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
|
|
||||||
/** 按 friendUserId 获取详情并合并到本地(保证 nickname / avatar 最新) */
|
/** 按 friendUserId 获取详情并合并到本地(保证 nickname / avatar 最新) */
|
||||||
async loadFriendInfo(friendUserId: number) {
|
async loadFriendInfo(friendUserId: number) {
|
||||||
|
const requestEpoch = storeEpoch
|
||||||
try {
|
try {
|
||||||
const data = await apiGetFriend(friendUserId)
|
const data = await apiGetFriend(friendUserId)
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// clear() 已切账号:旧请求的好友详情不能再 upsert 进新账号的 friends
|
||||||
|
if (requestEpoch !== storeEpoch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.upsertFriend(convertFriend(data))
|
this.upsertFriend(convertFriend(data))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[IM friendStore] loadFriendInfo 失败', e)
|
console.warn('[IM friendStore] loadFriendInfo 失败', e)
|
||||||
|
|
@ -301,10 +317,15 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
|
|
||||||
/** 按 id 从后端单查并 upsert 到本地(dispatcher 兜底用,避免全量重拉);后端带越权过滤 */
|
/** 按 id 从后端单查并 upsert 到本地(dispatcher 兜底用,避免全量重拉);后端带越权过滤 */
|
||||||
async loadFriendRequest(requestId: number) {
|
async loadFriendRequest(requestId: number) {
|
||||||
|
const requestEpoch = storeEpoch
|
||||||
const data = await apiGetMyFriendRequest(requestId)
|
const data = await apiGetMyFriendRequest(requestId)
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// clear() 已切账号:旧请求的申请记录不能再写进新账号的 friendRequests
|
||||||
|
if (requestEpoch !== storeEpoch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const next = convertFriendRequest(data)
|
const next = convertFriendRequest(data)
|
||||||
const existing = this.findFriendRequest(requestId)
|
const existing = this.findFriendRequest(requestId)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
|
@ -329,13 +350,21 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
|
|
||||||
/** 删除好友(单向软删,本端置 DISABLE);clear=true 时级联清理本地相关数据(如私聊会话),并透传后端给多端同步 */
|
/** 删除好友(单向软删,本端置 DISABLE);clear=true 时级联清理本地相关数据(如私聊会话),并透传后端给多端同步 */
|
||||||
async deleteFriend(friendUserId: number, clear: boolean = true) {
|
async deleteFriend(friendUserId: number, clear: boolean = true) {
|
||||||
|
const requestEpoch = storeEpoch
|
||||||
await apiDeleteFriend(friendUserId, clear)
|
await apiDeleteFriend(friendUserId, clear)
|
||||||
|
if (requestEpoch !== storeEpoch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.removeFriend(friendUserId, clear)
|
this.removeFriend(friendUserId, clear)
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 切换免打扰:同步会话的 silent 字段,避免会话列表 silent 图标等 1210 推到才更新 */
|
/** 切换免打扰:同步会话的 silent 字段,避免会话列表 silent 图标等 1210 推到才更新 */
|
||||||
async setSilent(friendUserId: number, silent: boolean) {
|
async setSilent(friendUserId: number, silent: boolean) {
|
||||||
|
const requestEpoch = storeEpoch
|
||||||
await apiUpdateFriend({ friendUserId, silent })
|
await apiUpdateFriend({ friendUserId, silent })
|
||||||
|
if (requestEpoch !== storeEpoch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const friend = this.getFriend(friendUserId)
|
const friend = this.getFriend(friendUserId)
|
||||||
if (friend) {
|
if (friend) {
|
||||||
friend.silent = silent
|
friend.silent = silent
|
||||||
|
|
@ -347,7 +376,11 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
|
|
||||||
/** 切换联系人置顶 */
|
/** 切换联系人置顶 */
|
||||||
async setPinned(friendUserId: number, pinned: boolean) {
|
async setPinned(friendUserId: number, pinned: boolean) {
|
||||||
|
const requestEpoch = storeEpoch
|
||||||
await apiUpdateFriend({ friendUserId, pinned })
|
await apiUpdateFriend({ friendUserId, pinned })
|
||||||
|
if (requestEpoch !== storeEpoch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const friend = this.getFriend(friendUserId)
|
const friend = this.getFriend(friendUserId)
|
||||||
if (friend) {
|
if (friend) {
|
||||||
friend.pinned = pinned
|
friend.pinned = pinned
|
||||||
|
|
@ -357,7 +390,11 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
|
|
||||||
/** 拉黑好友:本端乐观更新 + 调接口;后端 FRIEND_BLOCK 推到时由 dispatcher 兜底同步多端 */
|
/** 拉黑好友:本端乐观更新 + 调接口;后端 FRIEND_BLOCK 推到时由 dispatcher 兜底同步多端 */
|
||||||
async blockFriend(friendUserId: number) {
|
async blockFriend(friendUserId: number) {
|
||||||
|
const requestEpoch = storeEpoch
|
||||||
await apiBlockFriend(friendUserId)
|
await apiBlockFriend(friendUserId)
|
||||||
|
if (requestEpoch !== storeEpoch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const friend = this.getFriend(friendUserId)
|
const friend = this.getFriend(friendUserId)
|
||||||
if (friend) {
|
if (friend) {
|
||||||
friend.blocked = true
|
friend.blocked = true
|
||||||
|
|
@ -367,7 +404,11 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
|
|
||||||
/** 移出黑名单:本端乐观更新 + 调接口;后端 FRIEND_UNBLOCK 推到时由 dispatcher 兜底同步多端 */
|
/** 移出黑名单:本端乐观更新 + 调接口;后端 FRIEND_UNBLOCK 推到时由 dispatcher 兜底同步多端 */
|
||||||
async unblockFriend(friendUserId: number) {
|
async unblockFriend(friendUserId: number) {
|
||||||
|
const requestEpoch = storeEpoch
|
||||||
await apiUnblockFriend(friendUserId)
|
await apiUnblockFriend(friendUserId)
|
||||||
|
if (requestEpoch !== storeEpoch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const friend = this.getFriend(friendUserId)
|
const friend = this.getFriend(friendUserId)
|
||||||
if (friend) {
|
if (friend) {
|
||||||
friend.blocked = false
|
friend.blocked = false
|
||||||
|
|
@ -377,9 +418,13 @@ export const useFriendStore = defineStore('imFriendStore', {
|
||||||
|
|
||||||
/** 修改好友展示备注(仅自己可见) */
|
/** 修改好友展示备注(仅自己可见) */
|
||||||
async setDisplayName(friendUserId: number, displayName: string) {
|
async setDisplayName(friendUserId: number, displayName: string) {
|
||||||
|
const requestEpoch = storeEpoch
|
||||||
const value = displayName.trim()
|
const value = displayName.trim()
|
||||||
// 后端 displayName 语义:null/undefined = 不改,"" = 清空,所以这里直接传 value(可能是空串)
|
// 后端 displayName 语义:null/undefined = 不改,"" = 清空,所以这里直接传 value(可能是空串)
|
||||||
await apiUpdateFriend({ friendUserId, displayName: value })
|
await apiUpdateFriend({ friendUserId, displayName: value })
|
||||||
|
if (requestEpoch !== storeEpoch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const friend = this.getFriend(friendUserId)
|
const friend = this.getFriend(friendUserId)
|
||||||
if (friend) {
|
if (friend) {
|
||||||
friend.displayName = value
|
friend.displayName = value
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue