feat(im):好友模块 code review 多项修复(补 block/unblock 全链路 + UserInfo 菜单入口、silent 后主动入库、防 currentUserId 切账号失活、雪崩去重与命名/枚举清理)
parent
5c2a185ff9
commit
7618d58a66
|
|
@ -20,15 +20,14 @@
|
|||
|
||||
<el-scrollbar v-loading="loading" class="h-[400px] mt-2.5">
|
||||
<div
|
||||
v-if="users.length === 0"
|
||||
v-if="visibleUsers.length === 0"
|
||||
class="py-10 text-13px text-center text-[var(--el-text-color-disabled)]"
|
||||
>
|
||||
{{ searched ? '没有搜到用户' : '输入关键字后回车开始搜索' }}
|
||||
</div>
|
||||
<div
|
||||
v-for="user in users"
|
||||
v-for="user in visibleUsers"
|
||||
:key="user.id"
|
||||
v-show="user.id !== currentUserId"
|
||||
class="flex gap-3 items-center px-2 py-2.5 border-b border-[var(--el-border-color-lighter)]"
|
||||
>
|
||||
<UserAvatar
|
||||
|
|
@ -174,6 +173,9 @@ const message = useMessage()
|
|||
|
||||
/** 当前登录用户编号;用 computed 包一层,切账号后随 wsCache 重取,避免顶层求值在 keep-alive 实例里持有旧 id */
|
||||
const currentUserId = computed(() => getCurrentUserId())
|
||||
|
||||
/** 搜索结果过滤掉自己;用 v-if 而非 v-show,避免 DOM 占位 + 头像无效请求 */
|
||||
const visibleUsers = computed(() => users.value.filter((u) => u.id !== currentUserId.value))
|
||||
const keyword = ref('')
|
||||
const users = ref<UserVO[]>([])
|
||||
const searched = ref(false)
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ import UserAvatar from './UserAvatar.vue'
|
|||
import FriendAddDialog from '../friend/FriendAddDialog.vue'
|
||||
import { getSimpleUser, type UserVO } from '@/api/system/user'
|
||||
import { useFriendStore } from '../../store/friendStore'
|
||||
import { ImFriendAddSource } from '../../../utils/constants'
|
||||
import { getGenderColor, getGenderIcon } from '../../../utils/user'
|
||||
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
|
|
@ -205,15 +206,15 @@ const props = withDefaults(
|
|||
displayName?: string
|
||||
/** UserAvatar 预览层 z-index;放在高 z-index 浮层(如 UserInfoCard)里需手动抬高 */
|
||||
previewZIndex?: number
|
||||
/** 加好友来源:1=搜索 2=群聊 3=扫码 4=名片;默认 1(搜索);参见 ImFriendAddSourceEnum */
|
||||
/** 加好友来源;默认 SEARCH;参见 ImFriendAddSource */
|
||||
addSource?: number
|
||||
/** 来源附带信息:addSource=2(群聊)时传群名,用于「我是 XX 群的 YY」预填话术 */
|
||||
/** 来源附带信息:addSource=GROUP(群聊)时传群名,用于「我是 XX 群的 YY」预填话术 */
|
||||
addSourceExtra?: string
|
||||
}>(),
|
||||
{
|
||||
relation: 'readonly',
|
||||
previewZIndex: 2000,
|
||||
addSource: 1
|
||||
addSource: ImFriendAddSource.SEARCH
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ const message = useMessage()
|
|||
/** 当前登录用户编号;用 computed 包一层,切账号后随 wsCache 重取,避免顶层求值在 keep-alive 实例里持有旧 id */
|
||||
const currentUserId = computed(() => getCurrentUserId())
|
||||
|
||||
/** 是不是我发起的(fromUserId === me) */
|
||||
/** 是不是我发起的(fromUserId === currentUserId) */
|
||||
const iSentIt = computed(() => props.request.fromUserId === currentUserId.value)
|
||||
|
||||
/** 是否「已拒绝」态:模板里多处用到,computed 一次省得到处写枚举比对 */
|
||||
|
|
|
|||
|
|
@ -13,13 +13,18 @@
|
|||
>
|
||||
<Icon :icon="expanded ? 'ep:caret-bottom' : 'ep:caret-right'" :size="14" />
|
||||
<span class="flex-1">新的朋友</span>
|
||||
<!-- 红点:未处理且别人加我的 -->
|
||||
<el-badge v-if="unhandledCount > 0" :value="unhandledCount" :max="99" class="mr-2" />
|
||||
<!-- 红点:未处理且别人加我的(统一走 store getter,避免本地 computed 跟 store 双口径) -->
|
||||
<el-badge
|
||||
v-if="friendStore.getUnhandledRequestCount > 0"
|
||||
:value="friendStore.getUnhandledRequestCount"
|
||||
:max="99"
|
||||
class="mr-2"
|
||||
/>
|
||||
<span class="text-sm text-[var(--el-text-color-secondary)]">{{ requests.length }}</span>
|
||||
</div>
|
||||
<div v-show="expanded">
|
||||
<div
|
||||
v-for="request in requests"
|
||||
v-for="{ request, peer } in enrichedRequests"
|
||||
:key="request.id"
|
||||
class="flex gap-3 items-start px-3.5 py-2.5 cursor-pointer transition-colors hover:bg-[var(--el-fill-color-light)]"
|
||||
:class="{
|
||||
|
|
@ -28,16 +33,16 @@
|
|||
@click="emit('select', request)"
|
||||
>
|
||||
<UserAvatar
|
||||
:id="getPeerUserId(request)"
|
||||
:url="getPeerAvatar(request)"
|
||||
:name="getPeerNickname(request)"
|
||||
:id="peer.id"
|
||||
:url="peer.avatar"
|
||||
:name="peer.nickname"
|
||||
:size="36"
|
||||
:clickable="false"
|
||||
/>
|
||||
<div class="flex-1 min-w-0 overflow-hidden">
|
||||
<div class="flex justify-between gap-2 items-center">
|
||||
<span class="flex-1 text-sm font-medium truncate text-[var(--el-text-color-primary)]">
|
||||
{{ getPeerNickname(request) }}
|
||||
{{ peer.nickname }}
|
||||
</span>
|
||||
<span class="flex-shrink-0 text-12px text-[var(--el-text-color-secondary)]">
|
||||
{{ getDictLabel(DICT_TYPE.IM_FRIEND_REQUEST_HANDLE_RESULT, request.handleResult) }}
|
||||
|
|
@ -65,8 +70,8 @@
|
|||
import { computed, ref } from 'vue'
|
||||
import Icon from '@/components/Icon/src/Icon.vue'
|
||||
import UserAvatar from '../../components/user/UserAvatar.vue'
|
||||
import { useFriendStore } from '../../store/friendStore'
|
||||
import { getCurrentUserId } from '../../../utils/storage'
|
||||
import { ImFriendRequestHandleResult } from '../../../utils/constants'
|
||||
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
||||
import type { FriendRequest } from '../../types'
|
||||
|
||||
|
|
@ -81,32 +86,26 @@ const emit = defineEmits<{
|
|||
select: [request: FriendRequest]
|
||||
}>()
|
||||
|
||||
const friendStore = useFriendStore()
|
||||
const expanded = ref(true)
|
||||
/** 当前登录用户编号;用 computed 包一层,切账号后随 wsCache 重取,避免顶层求值在 keep-alive 实例里持有旧 id */
|
||||
const currentUserId = computed(() => getCurrentUserId())
|
||||
|
||||
/** 未处理 + 别人加我的(接收方=我)才进红点;我发起的不进 */
|
||||
const unhandledCount = computed(
|
||||
() => props.requests.filter(
|
||||
(r) => r.handleResult === ImFriendRequestHandleResult.UNHANDLED && r.toUserId === currentUserId.value
|
||||
).length
|
||||
/** 列表项展示对端:fromUserId == 我 → 对端 = toUser;否则对端 = fromUser */
|
||||
function getPeer(request: FriendRequest) {
|
||||
const sentByMe = request.fromUserId === currentUserId.value
|
||||
return {
|
||||
id: sentByMe ? request.toUserId : request.fromUserId,
|
||||
nickname: sentByMe
|
||||
? request.toNickname || String(request.toUserId)
|
||||
: request.fromNickname || String(request.fromUserId),
|
||||
avatar: sentByMe ? request.toAvatar : request.fromAvatar
|
||||
}
|
||||
}
|
||||
|
||||
/** 列表项预先附 peer 字段,模板里直接 {{ peer.xxx }} 一次成型,省 3 次 helper 调用 */
|
||||
const enrichedRequests = computed(() =>
|
||||
props.requests.map((request) => ({ request, peer: getPeer(request) }))
|
||||
)
|
||||
|
||||
/** 列表项展示对端:fromUserId == 我 → 对端 = toUser;否则对端 = fromUser */
|
||||
function getPeerUserId(request: FriendRequest): number {
|
||||
return request.fromUserId === currentUserId.value ? request.toUserId : request.fromUserId
|
||||
}
|
||||
|
||||
/** 列表项展示对端的昵称(fromUserId == 我 → toUser 昵称;否则 fromUser 昵称;缺则用 id 兜底) */
|
||||
function getPeerNickname(request: FriendRequest): string {
|
||||
return request.fromUserId === currentUserId.value
|
||||
? request.toNickname || String(request.toUserId)
|
||||
: request.fromNickname || String(request.fromUserId)
|
||||
}
|
||||
|
||||
/** 列表项展示对端的头像(fromUserId == 我 → toUser 头像;否则 fromUser 头像) */
|
||||
function getPeerAvatar(request: FriendRequest): string | undefined {
|
||||
return request.fromUserId === currentUserId.value ? request.toAvatar : request.fromAvatar
|
||||
}
|
||||
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -20,17 +20,13 @@ import {
|
|||
type ImFriendRequestRespVO
|
||||
} from '@/api/im/friend/request'
|
||||
import { useConversationStore } from './conversationStore'
|
||||
import { ImConversationType } from '../../utils/constants'
|
||||
import { ImConversationType, ImFriendRequestHandleResult } from '../../utils/constants'
|
||||
import { getCurrentUserId, imStorage, setQuietly, StorageKeys } from '../../utils/storage'
|
||||
import { getFriendDisplayName } from '../../utils/user'
|
||||
import type { Friend, FriendRequest } from '../types'
|
||||
|
||||
/** 好友申请处理结果(对齐后端 ImFriendRequestHandleResultEnum) */
|
||||
const FriendRequestHandleResult = {
|
||||
UNHANDLED: 0,
|
||||
AGREED: 1,
|
||||
REFUSED: 2
|
||||
} as const
|
||||
/** 当前正在进行的好友申请列表拉取;多端连续多条申请到达时复用同一 Promise,避免雪崩重拉 */
|
||||
let inflightFetchRequests: Promise<void> | null = null
|
||||
|
||||
/** 好友通知 payload(对齐后端 BaseFriendNotification + 子类裁减后的字段) */
|
||||
export interface FriendNotificationPayload {
|
||||
|
|
@ -90,9 +86,11 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
},
|
||||
/** 未处理申请数(接收方=我)—— 实时派生,「新的朋友」红点用 */
|
||||
getUnhandledRequestCount: (state): number => {
|
||||
const me = Number(getCurrentUserId() || 0)
|
||||
const currentUserId = Number(getCurrentUserId() || 0)
|
||||
return state.friendRequests.filter(
|
||||
(r) => r.handleResult === FriendRequestHandleResult.UNHANDLED && r.toUserId === me
|
||||
(request) =>
|
||||
request.handleResult === ImFriendRequestHandleResult.UNHANDLED &&
|
||||
request.toUserId === currentUserId
|
||||
).length
|
||||
}
|
||||
},
|
||||
|
|
@ -175,7 +173,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
await apiAgreeFriendRequest(requestId)
|
||||
const request = this.findFriendRequest(requestId)
|
||||
if (request) {
|
||||
request.handleResult = FriendRequestHandleResult.AGREED
|
||||
request.handleResult = ImFriendRequestHandleResult.AGREED
|
||||
request.handleTime = Date.now()
|
||||
} else {
|
||||
// 列表过期场景兜底重拉
|
||||
|
|
@ -189,7 +187,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
await apiRefuseFriendRequest(requestId, handleContent)
|
||||
const request = this.findFriendRequest(requestId)
|
||||
if (request) {
|
||||
request.handleResult = FriendRequestHandleResult.REFUSED
|
||||
request.handleResult = ImFriendRequestHandleResult.REFUSED
|
||||
request.handleContent = handleContent
|
||||
request.handleTime = Date.now()
|
||||
} else {
|
||||
|
|
@ -197,16 +195,24 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
}
|
||||
},
|
||||
|
||||
/** 拉取「我相关」的好友申请列表(页面打开时 / 收到 FRIEND_APPLICATION 时刷新) */
|
||||
/** 拉取「我相关」的好友申请列表(页面打开时 / 收到 FRIEND_APPLICATION 时刷新);in-flight 期间复用同一 Promise */
|
||||
async fetchFriendRequests() {
|
||||
const list = await apiGetMyFriendRequestList()
|
||||
this.friendRequests = (list || []).map(convertFriendRequest)
|
||||
if (inflightFetchRequests) {
|
||||
return inflightFetchRequests
|
||||
}
|
||||
inflightFetchRequests = apiGetMyFriendRequestList()
|
||||
.then((list) => {
|
||||
this.friendRequests = (list || []).map(convertFriendRequest)
|
||||
})
|
||||
.finally(() => {
|
||||
inflightFetchRequests = null
|
||||
})
|
||||
return inflightFetchRequests
|
||||
},
|
||||
|
||||
/** 按 id 查申请记录 */
|
||||
/** 按 id 查申请记录;列表是按 id 倒序的小列表,O(n) find 即可,不再维护 Map 索引 */
|
||||
findFriendRequest(requestId: number): FriendRequest | undefined {
|
||||
// TODO @AI:request
|
||||
return this.friendRequests.find((r) => r.id === requestId)
|
||||
return this.friendRequests.find((request) => request.id === requestId)
|
||||
},
|
||||
|
||||
// ==================== 好友关系操作 ====================
|
||||
|
|
@ -323,7 +329,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
applyFriendRequestApprovedNotification(payload: FriendNotificationPayload) {
|
||||
const request = payload.requestId ? this.findFriendRequest(payload.requestId) : undefined
|
||||
if (request) {
|
||||
request.handleResult = FriendRequestHandleResult.AGREED
|
||||
request.handleResult = ImFriendRequestHandleResult.AGREED
|
||||
request.handleTime = Date.now()
|
||||
} else {
|
||||
this.fetchFriendRequests().catch(() => undefined)
|
||||
|
|
@ -334,7 +340,7 @@ export const useFriendStore = defineStore('imFriendStore', {
|
|||
applyFriendRequestRejectedNotification(payload: FriendNotificationPayload) {
|
||||
const request = payload.requestId ? this.findFriendRequest(payload.requestId) : undefined
|
||||
if (request) {
|
||||
request.handleResult = FriendRequestHandleResult.REFUSED
|
||||
request.handleResult = ImFriendRequestHandleResult.REFUSED
|
||||
request.handleContent = payload.handleContent
|
||||
request.handleTime = Date.now()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
/**
|
||||
* 群聊统一帧分发:按 payload.type(ImMessageType)分到已读 / 回执 / 群个人信号 / 普通消息
|
||||
*
|
||||
* 1530 GROUP_MEMBER_SETTING_UPDATE 是个人信号;其它(普通消息 + 1501-1520 OpenIM 段位群广播事件)走 handleGroupMessage 入库 + 触发 applyGroupNotification 旁路
|
||||
* 1530 GROUP_MEMBER_SETTING_UPDATE 是个人信号;其它(普通消息 + 1501-1520 段位群广播事件)走 handleGroupMessage 入库 + 触发 applyGroupNotification 旁路
|
||||
*/
|
||||
dispatchGroupFrame(websocketMessage: ImGroupMessageDTO) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -9,18 +9,18 @@ export const ImMessageType = {
|
|||
READ: 11, // 已读
|
||||
RECEIPT: 12, // 回执
|
||||
TIP_TEXT: 21, // 提示文本(撤回提示等)
|
||||
// 好友通知(1201-1210 复用 OpenIM 段位编号)
|
||||
// ========== 好友通知(1201-1210 直接复用 OpenIM 段位编号) ==========
|
||||
FRIEND_REQUEST_APPROVED: 1201, // 好友申请被同意
|
||||
FRIEND_REQUEST_REJECTED: 1202, // 好友申请被拒绝
|
||||
FRIEND_APPLICATION: 1203, // 收到新的好友申请
|
||||
FRIEND_ADD: 1204, // 新增好友(双方建立关系)
|
||||
FRIEND_DELETE: 1205, // 好友被删除
|
||||
// 1206 对应 OpenIM FriendRemarkSetNotification;本系统并入 FRIEND_UPDATE(1210) 统一推送
|
||||
// 1206 对应 OpenIM FriendRemarkSetNotification;本系统并入 FRIEND_UPDATE(1210) 统一推送,单一字段变更不再独立通道
|
||||
FRIEND_BLOCK: 1207, // 加入黑名单
|
||||
FRIEND_UNBLOCK: 1208, // 移出黑名单
|
||||
FRIEND_INFO_UPDATED: 1209, // 好友资料变更(昵称 / 头像)
|
||||
FRIEND_UPDATE: 1210, // 好友信息批量更新(muted / pinned)
|
||||
// 群事件(1501-1520 复用 OpenIM 段位编号;1530+ 自有扩展段)
|
||||
// ========== 群事件(1501-1520 直接复用 OpenIM 段位编号;1530+ 自有扩展段) ==========
|
||||
GROUP_CREATE: 1501, // 群创建
|
||||
GROUP_INFO_UPDATE: 1502, // 群信息变更(NAME / NOTICE 之外字段兜底)
|
||||
// 1503 GROUP_JOIN_APPLICATION TODO 未实现:入群申请
|
||||
|
|
@ -41,9 +41,10 @@ export const ImMessageType = {
|
|||
GROUP_ADMIN_REMOVE: 1518, // 撤销管理员
|
||||
GROUP_NOTICE_UPDATE: 1519, // 群公告变更
|
||||
GROUP_NAME_UPDATE: 1520, // 群名变更
|
||||
// ========== 自有扩展段(1530+,OpenIM 1500-1520 段位无对应物) ==========
|
||||
GROUP_MEMBER_SETTING_UPDATE: 1530, // 群成员个人设置变更:muted / groupRemark 个人多端同步
|
||||
GROUP_MESSAGE_PIN: 1531, // 群消息置顶
|
||||
GROUP_MESSAGE_UNPIN: 1532 // 群消息取消置顶
|
||||
GROUP_MESSAGE_PIN: 1531, // 群消息置顶(自有扩展,OpenIM 无)
|
||||
GROUP_MESSAGE_UNPIN: 1532 // 群消息取消置顶(自有扩展,OpenIM 无)
|
||||
} as const
|
||||
|
||||
/** 判断是否「群广播事件」:[GROUP_CREATE, GROUP_MESSAGE_UNPIN] 段位都算,仅 GROUP_MEMBER_SETTING_UPDATE 是个人信号排除 */
|
||||
|
|
|
|||
Loading…
Reference in New Issue