✨ feat(im): 重构优化 store 方案
parent
e30e30ea51
commit
2785e2bea6
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -101,8 +101,6 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
|
||||
/** 本地合并 / 新增某个好友(WebSocket 事件 & 手动刷新都用) */
|
||||
upsertFriend(friend: Friend) {
|
||||
// TODO DONE @AI:index
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 @AI:group
|
||||
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 @AI:group
|
||||
// 命中缓存:群已加载且成员列表已就绪,直接返回(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 @AI:index
|
||||
// 按 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
@ -133,6 +133,18 @@ export interface Friend {
|
|||
avatar?: string // 好友头像
|
||||
muted?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音)
|
||||
status?: number // 好友状态,对齐 CommonStatusEnum(DISABLE = 已删除/墓碑)
|
||||
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
|
||||
}
|
||||
Loading…
Reference in New Issue