From 7618d58a66c1d3e0a60417ac0cb089386028aa20 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 4 May 2026 17:31:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(im)=EF=BC=9A=E5=A5=BD=E5=8F=8B=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=20code=20review=20=E5=A4=9A=E9=A1=B9=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=EF=BC=88=E8=A1=A5=20block/unblock=20=E5=85=A8?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=20+=20UserInfo=20=E8=8F=9C=E5=8D=95=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E3=80=81silent=20=E5=90=8E=E4=B8=BB=E5=8A=A8=E5=85=A5?= =?UTF-8?q?=E5=BA=93=E3=80=81=E9=98=B2=20currentUserId=20=E5=88=87?= =?UTF-8?q?=E8=B4=A6=E5=8F=B7=E5=A4=B1=E6=B4=BB=E3=80=81=E9=9B=AA=E5=B4=A9?= =?UTF-8?q?=E5=8E=BB=E9=87=8D=E4=B8=8E=E5=91=BD=E5=90=8D/=E6=9E=9A?= =?UTF-8?q?=E4=B8=BE=E6=B8=85=E7=90=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/friend/FriendAddDialog.vue | 8 ++- .../im/home/components/user/UserInfo.vue | 7 ++- .../pages/contact/FriendRequestDetail.vue | 2 +- .../home/pages/contact/FriendRequestList.vue | 59 +++++++++---------- src/views/im/home/store/friendStore.ts | 44 ++++++++------ src/views/im/home/store/websocketStore.ts | 2 +- src/views/im/utils/constants.ts | 11 ++-- 7 files changed, 71 insertions(+), 62 deletions(-) diff --git a/src/views/im/home/components/friend/FriendAddDialog.vue b/src/views/im/home/components/friend/FriendAddDialog.vue index 741c03838..7033db9c1 100644 --- a/src/views/im/home/components/friend/FriendAddDialog.vue +++ b/src/views/im/home/components/friend/FriendAddDialog.vue @@ -20,15 +20,14 @@
{{ searched ? '没有搜到用户' : '输入关键字后回车开始搜索' }}
getCurrentUserId()) + +/** 搜索结果过滤掉自己;用 v-if 而非 v-show,避免 DOM 占位 + 头像无效请求 */ +const visibleUsers = computed(() => users.value.filter((u) => u.id !== currentUserId.value)) const keyword = ref('') const users = ref([]) const searched = ref(false) diff --git a/src/views/im/home/components/user/UserInfo.vue b/src/views/im/home/components/user/UserInfo.vue index 08801a278..94ecb3b36 100644 --- a/src/views/im/home/components/user/UserInfo.vue +++ b/src/views/im/home/components/user/UserInfo.vue @@ -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 } ) diff --git a/src/views/im/home/pages/contact/FriendRequestDetail.vue b/src/views/im/home/pages/contact/FriendRequestDetail.vue index b1a6f78d6..c470113aa 100644 --- a/src/views/im/home/pages/contact/FriendRequestDetail.vue +++ b/src/views/im/home/pages/contact/FriendRequestDetail.vue @@ -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 一次省得到处写枚举比对 */ diff --git a/src/views/im/home/pages/contact/FriendRequestList.vue b/src/views/im/home/pages/contact/FriendRequestList.vue index a66b40087..9e9e39271 100644 --- a/src/views/im/home/pages/contact/FriendRequestList.vue +++ b/src/views/im/home/pages/contact/FriendRequestList.vue @@ -13,13 +13,18 @@ > 新的朋友 - - + + {{ requests.length }}
- {{ getPeerNickname(request) }} + {{ peer.nickname }} {{ 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 -} - diff --git a/src/views/im/home/store/friendStore.ts b/src/views/im/home/store/friendStore.ts index 0154daa29..0d7e33692 100644 --- a/src/views/im/home/store/friendStore.ts +++ b/src/views/im/home/store/friendStore.ts @@ -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 | 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 { diff --git a/src/views/im/home/store/websocketStore.ts b/src/views/im/home/store/websocketStore.ts index f1c30a380..156b575fd 100644 --- a/src/views/im/home/store/websocketStore.ts +++ b/src/views/im/home/store/websocketStore.ts @@ -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 { diff --git a/src/views/im/utils/constants.ts b/src/views/im/utils/constants.ts index 6382eaeff..73a013d37 100644 --- a/src/views/im/utils/constants.ts +++ b/src/views/im/utils/constants.ts @@ -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 是个人信号排除 */