feat(im): 重构优化 store 方案

im
YunaiV 2026-04-25 16:45:31 +08:00
parent e30e30ea51
commit 2785e2bea6
5 changed files with 125 additions and 33 deletions

View File

@ -13,6 +13,9 @@ import { parseMessage, buildRecallTip, type TextMessage } from '../../utils/mess
import type { Conversation, Message, ConversationsData } from '../types'
const AT_ALL_FLAG = -1 // @全体成员 的特殊 userId 标识atUserIds 中包含 -1 表示 @all
// 单会话持久化消息数上限localStorage 整体配额一般 5~10MB全量序列化容易撑爆。
// 内存里保留完整历史,落盘只截最近 N 条;用户重启后历史不够再向后端拉。
const MAX_PERSISTED_MESSAGES_PER_CONVERSATION = 100
/** 获取当前登录用户编号 */
function getCurrentUserId(): number {
@ -113,11 +116,17 @@ export const useConversationStore = defineStore('imConversationStore', {
return
}
// TODO @AI这个方案可能存不下太多数据需要调整
// TODO @AI可能要调整存储方案
// 落盘前对每个会话的 messages 做尾部截断,避免长会话把 localStorage 撑爆
const storageData: ConversationsData = {
privateMessageMaxId: this.privateMessageMaxId,
groupMessageMaxId: this.groupMessageMaxId,
conversations: this.conversations.filter((c) => !c.deleted)
conversations: this.conversations
.filter((c) => !c.deleted)
.map((c) => ({
...c,
messages: c.messages.slice(-MAX_PERSISTED_MESSAGES_PER_CONVERSATION)
}))
}
try {
localStorage.setItem(currentConversationsKey(), JSON.stringify(storageData))

View File

@ -101,8 +101,6 @@ export const useFriendStore = defineStore('imFriendStore', {
/** 本地合并 / 新增某个好友WebSocket 事件 & 手动刷新都用) */
upsertFriend(friend: Friend) {
// TODO DONE @AIindex
// TODO DONE @AI注释
// 按 friendUserId 查已有记录下标:>=0 命中则覆盖合并,<0 则追加
const index = this.friends.findIndex((f) => f.friendUserId === friend.friendUserId)
if (index >= 0) {
@ -128,14 +126,12 @@ export const useFriendStore = defineStore('imFriendStore', {
/** 本地标记删除WebSocket FRIEND_DEL 事件触发;同时级联清私聊会话) */
removeFriend(friendUserId: number) {
// TODO DONE @AI变量叫 friend
// 标记墓碑:保留记录但置为 DISABLE避免后续误判"陌生人"
// 软删:保留记录但置为 DISABLE避免后续误判"陌生人"
const friend = this.getFriend(friendUserId)
if (friend) {
friend.status = CommonStatusEnum.DISABLE
friend.deleteTime = new Date().toISOString()
friend.deleteTime = Date.now()
}
// TODO DONE @AI注释
// 级联清理:把对应的私聊会话也软删,避免会话列表里留着已删好友
const conversationStore = useConversationStore()
conversationStore.removePrivateConversation(friendUserId)
@ -144,7 +140,6 @@ export const useFriendStore = defineStore('imFriendStore', {
/** 切换免打扰 */
async setMuted(friendUserId: number, muted: boolean) {
await apiUpdateFriend({ friendUserId, muted })
// TODO DONE @AI变量叫 friend
const friend = this.getFriend(friendUserId)
if (friend) {
friend.muted = muted
@ -167,8 +162,8 @@ function toFriend(vo: ImFriendRespVO): Friend {
avatar: vo.avatar,
muted: !!vo.muted,
status: vo.status,
addTime: vo.addTime,
deleteTime: vo.deleteTime
addTime: vo.addTime ? new Date(vo.addTime).getTime() : undefined,
deleteTime: vo.deleteTime ? new Date(vo.deleteTime).getTime() : undefined
}
}

View File

@ -29,9 +29,11 @@ export const useGroupStore = defineStore('imGroupStore', {
}),
getters: {
getGroup: (state) => (id: number): Group | undefined => {
return state.groups.find((g) => g.id === id)
}
getGroup:
(state) =>
(id: number): Group | undefined => {
return state.groups.find((g) => g.id === id)
}
},
actions: {
@ -40,7 +42,6 @@ export const useGroupStore = defineStore('imGroupStore', {
if (this.loaded && !force) {
return
}
// TODO DONE @AI注释下
// 拉取当前登录用户加入的所有群(不带成员;成员按需再走 loadGroupMembers
const list = await apiGetMyGroupList()
this.groups = (list || []).map(toGroup)
@ -62,7 +63,6 @@ export const useGroupStore = defineStore('imGroupStore', {
if (!data) {
return
}
// TODO DONE @AIgroup
this.upsertGroup(toGroup(data))
} catch (e) {
console.warn('[IM groupStore] loadGroupInfo 失败', e)
@ -71,17 +71,15 @@ export const useGroupStore = defineStore('imGroupStore', {
/** 按群拉取成员带缓存force=true 强制刷新) */
async loadGroupMembers(groupId: number, force = false): Promise<GroupMember[]> {
// TODO DONE @AIgroup
// 命中缓存群已加载且成员列表已就绪直接返回force=true 时强制刷)
const group = this.getGroup(groupId)
if (group && group.members && !force) {
return group.members
}
// TODO DONE @AI注释
// 拉取该群所有成员(聚合自 AdminUser含 nickname / avatar / displayUserName
const list = await apiGetGroupMemberList(groupId)
const members = (list || []).map((m) => toGroupMember(m, groupId))
const members = (list || []).map((member) => toGroupMember(member, groupId))
// 成员列表可能在群列表之前触发,此时需要占位一个 group
if (!group) {
this.upsertGroup({
@ -98,7 +96,6 @@ export const useGroupStore = defineStore('imGroupStore', {
},
upsertGroup(group: Group) {
// TODO DONE @AIindex
// 按 id 查已有记录下标:>=0 命中则覆盖合并,<0 则追加
const index = this.groups.findIndex((g) => g.id === group.id)
if (index >= 0) {
@ -117,7 +114,6 @@ export const useGroupStore = defineStore('imGroupStore', {
/** 本地移除WebSocket GROUP_DEL 事件触发;同时级联清群聊会话) */
removeGroup(id: number) {
// TODO DONE @AI注释
// 直接从本地列表里移除(群解散是硬删,不留墓碑,区别于好友的软删)
this.groups = this.groups.filter((g) => g.id !== id)
// 级联清理:对应群聊会话也软删,避免会话列表里留着已解散的群
@ -127,7 +123,6 @@ export const useGroupStore = defineStore('imGroupStore', {
/** 切换免打扰(仅本地状态;后端 /im/group/update 接入 muted 字段后再补) */
setMuted(id: number, muted: boolean) {
// TODO DONE @AI注释
// 在本地 group 上直接打 muted 标记conversationStore 的会话级 muted 由 ConversationItem 单独 setMuted 写
const group = this.getGroup(id)
if (group) {
@ -135,6 +130,7 @@ export const useGroupStore = defineStore('imGroupStore', {
}
},
/** 切换用户时清空 */
clear() {
this.groups = []
this.loaded = false
@ -145,23 +141,23 @@ export const useGroupStore = defineStore('imGroupStore', {
function toGroup(vo: ImGroupRespVO): Group {
return {
id: vo.id,
name: vo.name || '',
name: vo.name,
avatar: vo.avatar,
notice: vo.notice,
ownerUserId: vo.ownerUserId
}
}
function toGroupMember(m: ImGroupMemberRespVO, groupId: number): GroupMember {
function toGroupMember(member: ImGroupMemberRespVO, groupId: number): GroupMember {
return {
id: m.id,
userId: m.userId,
id: member.id,
userId: member.userId,
groupId,
nickname: m.nickname || String(m.userId),
avatar: m.avatar,
displayUserName: m.displayUserName,
displayGroupName: m.displayGroupName,
status: m.status
nickname: member.nickname || String(member.userId),
avatar: member.avatar,
displayUserName: member.displayUserName,
displayGroupName: member.displayGroupName,
status: member.status
}
}

View File

@ -0,0 +1,80 @@
import { defineStore } from 'pinia'
import { reactive } from 'vue'
import type { UserInfo } from '../types'
/**
* IM UI store
*
* N 1
* open DOM store
* `Index.vue`
*/
export const useImUiStore = defineStore('imUiStore', () => {
// ==================== 用户名片 UserInfoCard ====================
// 用户名片悬浮卡:头像 / 昵称等触发点遍布会话、群成员、@ 选择器等列表,
const userInfoCard = reactive({
show: false,
user: null as UserInfo | null,
position: { x: 0, y: 0 }
})
/** 打开用户名片 */
function openUserInfoCard(user: UserInfo, position: { x: number; y: number }) {
const viewportWidth = document.documentElement.clientWidth
const viewportHeight = document.documentElement.clientHeight
userInfoCard.user = user
userInfoCard.position.x = Math.min(position.x, viewportWidth - 350)
userInfoCard.position.y = Math.min(position.y, viewportHeight - 220)
userInfoCard.show = true
}
/** 关闭用户名片 */
function closeUserInfoCard() {
userInfoCard.show = false
}
// ==================== 右键菜单 ContextMenu ====================
// 右键菜单虽然是一个组件挂在主壳上,但其触发时机分散在各列表
interface ContextMenuItem {
key: string
name: string
disabled?: boolean
}
const contextMenu = reactive({
show: false,
position: { x: 0, y: 0 },
items: [] as ContextMenuItem[],
/** 选中回调:每次 open 时由调用方传入 */
onSelect: null as ((item: ContextMenuItem) => void) | null
})
/** 打开右键菜单 */
function openContextMenu(
position: { x: number; y: number },
items: ContextMenuItem[],
onSelect: (item: ContextMenuItem) => void
) {
contextMenu.position = position
contextMenu.items = items
contextMenu.onSelect = onSelect
contextMenu.show = true
}
/** 关闭右键菜单 */
function closeContextMenu() {
contextMenu.show = false
contextMenu.onSelect = null
}
return {
userInfoCard,
openUserInfoCard,
closeUserInfoCard,
contextMenu,
openContextMenu,
closeContextMenu
}
})

View File

@ -133,6 +133,18 @@ export interface Friend {
avatar?: string // 好友头像
muted?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
status?: number // 好友状态,对齐 CommonStatusEnumDISABLE = 已删除/墓碑)
addTime?: string // 添加好友时间
deleteTime?: string // 删除好友时间
addTime?: number // 添加好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 toFriend 转换)
deleteTime?: number // 删除好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 toFriend 转换)
}
// ==================== 用户名片 ====================
// 用户精简信息(对齐后端 UserSimpleRespVO名片 / 头像 hover 等场景共用)
export interface UserInfo {
id: number
nickname?: string
avatar?: string
sex?: number
deptId?: number
deptName?: string
}