203 lines
7.1 KiB
TypeScript
203 lines
7.1 KiB
TypeScript
import { defineStore, acceptHMRUpdate } from 'pinia'
|
||
import { store } from '@/store'
|
||
|
||
import { CommonStatusEnum } from '@/utils/constants'
|
||
import {
|
||
getMyFriendList as apiGetMyFriendList,
|
||
getFriend as apiGetFriend,
|
||
addFriend as apiAddFriend,
|
||
deleteFriend as apiDeleteFriend,
|
||
updateFriend as apiUpdateFriend,
|
||
type ImFriendRespVO
|
||
} from '@/api/im/friend'
|
||
import { useConversationStore } from './conversationStore'
|
||
import { ImConversationType } from '../../utils/constants'
|
||
import { getFriendDisplayName } from '../../utils/user'
|
||
import type { Friend } from '../types'
|
||
|
||
/**
|
||
* IM 好友 Store
|
||
*
|
||
* 负责:
|
||
* - 拉取 / 缓存当前登录用户的好友列表
|
||
* - 加好友 / 删好友(走后端 API + 本地乐观同步)
|
||
* - 被 ConversationItem / FriendPage / MessageInput 等多处消费
|
||
*/
|
||
export const useFriendStore = defineStore('imFriendStore', {
|
||
state: () => ({
|
||
friends: [] as Friend[],
|
||
loaded: false
|
||
}),
|
||
|
||
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)
|
||
return !!entry && entry.status !== CommonStatusEnum.DISABLE
|
||
}
|
||
}
|
||
},
|
||
|
||
actions: {
|
||
/** 从后端拉取并覆盖本地列表;同步刷新对应私聊会话的展示名 / 头像 */
|
||
async loadFriends(force = false) {
|
||
if (this.loaded && !force) {
|
||
return
|
||
}
|
||
const list = await apiGetMyFriendList()
|
||
this.friends = (list || []).map(convertFriend)
|
||
this.loaded = true
|
||
// 同步 conversationStore 私聊会话的展示名 / 头像 / 免打扰
|
||
const conversationStore = useConversationStore()
|
||
for (const f of this.friends) {
|
||
conversationStore.updateConversation(ImConversationType.PRIVATE, f.friendUserId, {
|
||
name: getFriendDisplayName(f),
|
||
avatar: f.avatar,
|
||
muted: f.muted
|
||
})
|
||
}
|
||
},
|
||
|
||
/** 按 friendUserId 获取详情并合并到本地(保证 nickname / avatar 最新) */
|
||
async loadFriendInfo(friendUserId: number) {
|
||
try {
|
||
const data = await apiGetFriend(friendUserId)
|
||
if (!data) {
|
||
return
|
||
}
|
||
this.upsertFriend(convertFriend(data))
|
||
} catch (e) {
|
||
console.warn('[IM friendStore] loadFriendInfo 失败', e)
|
||
}
|
||
},
|
||
|
||
/** 添加好友:后端双向建立关系后,本地占位插入(服务端返回后可 loadFriends 刷新) */
|
||
async addFriend(friendUserId: number, preview?: Partial<Friend>) {
|
||
await apiAddFriend(friendUserId)
|
||
if (preview) {
|
||
this.upsertFriend({
|
||
friendUserId,
|
||
nickname: preview.nickname || String(friendUserId),
|
||
avatar: preview.avatar,
|
||
status: CommonStatusEnum.ENABLE
|
||
})
|
||
} else {
|
||
await this.loadFriendInfo(friendUserId)
|
||
}
|
||
},
|
||
|
||
/** 删除好友(软删,保留记录但置 DISABLE;同时级联清理本地私聊会话) */
|
||
async deleteFriend(friendUserId: number) {
|
||
await apiDeleteFriend(friendUserId)
|
||
this.removeFriend(friendUserId)
|
||
},
|
||
|
||
/** 本地合并 / 新增某个好友(WebSocket 事件 & 手动刷新都用) */
|
||
upsertFriend(friend: Friend) {
|
||
// 按 friendUserId 查已有记录下标:>=0 命中则覆盖合并,<0 则追加
|
||
const index = this.friends.findIndex((f) => f.friendUserId === friend.friendUserId)
|
||
if (index >= 0) {
|
||
this.friends[index] = {
|
||
...this.friends[index],
|
||
...friend,
|
||
status: friend.status ?? CommonStatusEnum.ENABLE
|
||
}
|
||
} else {
|
||
this.friends.push({
|
||
...friend,
|
||
status: friend.status ?? CommonStatusEnum.ENABLE
|
||
})
|
||
}
|
||
// 同步对应私聊会话的展示
|
||
const conversationStore = useConversationStore()
|
||
const merged = this.getFriend(friend.friendUserId)
|
||
conversationStore.updateConversation(ImConversationType.PRIVATE, friend.friendUserId, {
|
||
name: merged ? getFriendDisplayName(merged) : friend.nickname,
|
||
avatar: friend.avatar,
|
||
muted: friend.muted
|
||
})
|
||
},
|
||
|
||
/** 本地标记删除(WebSocket FRIEND_DEL 事件触发;同时级联清私聊会话) */
|
||
removeFriend(friendUserId: number) {
|
||
// 软删:保留记录但置为 DISABLE,避免后续误判"陌生人"
|
||
const friend = this.getFriend(friendUserId)
|
||
if (friend) {
|
||
friend.status = CommonStatusEnum.DISABLE
|
||
friend.deleteTime = Date.now()
|
||
}
|
||
// 级联清理:把对应的私聊会话也软删,避免会话列表里留着已删好友
|
||
const conversationStore = useConversationStore()
|
||
conversationStore.removePrivateConversation(friendUserId)
|
||
},
|
||
|
||
/** 切换免打扰 */
|
||
async setMuted(friendUserId: number, muted: boolean) {
|
||
await apiUpdateFriend({ friendUserId, muted })
|
||
const friend = this.getFriend(friendUserId)
|
||
if (friend) {
|
||
friend.muted = muted
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 修改好友展示备注(仅自己可见)
|
||
*
|
||
* 走后端 /im/friend/update 接口;保存成功后再同步本地 friend + 会话列表 name,
|
||
* 失败就直接抛给上层,让 UI 决定是否回滚 / 提示用户
|
||
*/
|
||
async setDisplayName(friendUserId: number, displayName: string) {
|
||
const value = displayName.trim()
|
||
// 后端的 displayName 语义:null/undefined = 不改,"" = 清空,所以这里直接传 value(可能是空串)
|
||
await apiUpdateFriend({ friendUserId, displayName: value })
|
||
const friend = this.getFriend(friendUserId)
|
||
if (friend) {
|
||
friend.displayName = value
|
||
const conversationStore = useConversationStore()
|
||
conversationStore.updateConversation(ImConversationType.PRIVATE, friendUserId, {
|
||
name: getFriendDisplayName(friend)
|
||
})
|
||
}
|
||
},
|
||
|
||
/** 切换用户时清空 */
|
||
clear() {
|
||
this.friends = []
|
||
this.loaded = false
|
||
}
|
||
}
|
||
})
|
||
|
||
function convertFriend(vo: ImFriendRespVO): Friend {
|
||
return {
|
||
id: vo.id,
|
||
friendUserId: vo.friendUserId,
|
||
nickname: vo.nickname || String(vo.friendUserId),
|
||
avatar: vo.avatar,
|
||
muted: !!vo.muted,
|
||
displayName: vo.displayName || '',
|
||
status: vo.status,
|
||
addTime: vo.addTime ? new Date(vo.addTime).getTime() : undefined,
|
||
deleteTime: vo.deleteTime ? new Date(vo.deleteTime).getTime() : undefined
|
||
}
|
||
}
|
||
|
||
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))
|
||
}
|