From b64a0deae5b5d33f9e20671a68936b27f0b9b00b Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 21 七月 2026 18:03:03 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_pro2.0' into dev_pro2.0
---
src/views/im/utils/user.ts | 367 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 367 insertions(+), 0 deletions(-)
diff --git a/src/views/im/utils/user.ts b/src/views/im/utils/user.ts
new file mode 100644
index 0000000..8775dba
--- /dev/null
+++ b/src/views/im/utils/user.ts
@@ -0,0 +1,367 @@
+// ====================================================================
+// IM 鐢ㄦ埛灞曠ず utility
+// ====================================================================
+// 鑱岃矗锛氱粺涓�鍥炵瓟"鏌愪釜鐢ㄦ埛鍦� UI 涓婂簲璇ュ浣曞睍绀�"锛屽寘鍚細
+// 1. 鏄剧ず鍚嶏紙getFriendDisplayName / getMemberDisplayName / getGroupDisplayName / getSenderDisplayName 绛夛級
+// 2. 涓婁笅鏂囨劅鐭ュ悕锛坱ryGetSenderDisplayName / getSenderRealNickname锛夛細娓叉煋鏃舵寜 conversation 涓婁笅鏂囧疄鏃舵煡 friendStore / groupStore / userStore锛岃澶囨敞 / 缇ゆ樀绉� / 鐪熷疄鏄电О鍙樻洿鍚庢墍鏈夊巻鍙叉秷鎭珛鍗冲搷搴斿紡鍒锋柊
+// 3. 鎬у埆鍥炬爣 / 棰滆壊锛坓etGenderIcon / getGenderColor锛夛細鐢疯摑銆佸コ绮夛紝鏈煡涓嶅睍绀猴紝鎵�鏈� UserInfo 鍗$墖 / 鍒楄〃琛屽叡鐢�
+//
+// 鍛藉悕绾﹀畾锛氭樉绀哄悕鐩稿叧鍑芥暟涓�寰嬩娇鐢� displayName锛屼笌 friend.displayName / member.displayUserName 瀛楁瀵归綈
+// ====================================================================
+
+import type { Conversation, Friend, Group, User } from '../home/types'
+
+import { CommonStatusEnum } from '#/packages/constants/src'
+import { useUserStore } from '#/packages/stores/src'
+
+import { getCurrentUserId } from '#/views/im/utils/auth'
+
+import { useConversationStore } from '../home/store/conversationStore'
+import { useFriendStore } from '../home/store/friendStore'
+import { useGroupStore } from '../home/store/groupStore'
+import { useImUiStore } from '../home/store/uiStore'
+import {
+ IM_AT_ALL_NICKNAME,
+ IM_AT_ALL_USER_ID,
+ ImConversationType,
+ ImFriendAddSource
+} from './constants'
+import { type MentionCandidate } from './message'
+
+const SystemUserSexEnum = {
+ FEMALE: 2,
+ MALE: 1,
+ UNKNOWN: 0
+} as const
+
+// 鍊欓�夌己澶卞満鏅殑绋冲畾绌烘暟缁勶紱璁� textMentions computed 鍦ㄩ潪 TEXT / 闈炵兢鑱� / 鏃� @ 鏃惰繑鍥炲悓涓�寮曠敤锛�
+// MessageBubble 鐨� textSegments 鎵嶄笉浼氳窡鐫�鏃犺皳閲嶇畻
+const EMPTY_MENTIONS: MentionCandidate[] = []
+
+/**
+ * 鏄惁鍘嗗彶閫�缇ょ兢锛歫oinStatus 涓� DISABLE锛堝凡閫�缇� / 琚Щ闄わ級
+ *
+ * /im/group/list 浼氬甫鍥炲巻鍙查��缇ょ兢锛堜緵灞曠ず鍘嗗彶娑堟伅鐨勭兢鍚� / 澶村儚锛夛紱杩欑被缇ゅ簲绂佹鍙戦�併�侀殣钘忕兢鎿嶄綔鍏ュ彛銆佷笉鍙杞彂 / 鎺ㄨ崘閫変腑
+ */
+export function isGroupQuit(group?: Group | null): boolean {
+ return group?.joinStatus === CommonStatusEnum.DISABLE
+}
+
+/**
+ * 绉佽亰濂藉弸鏄剧ず鍚嶏細澶囨敞 > 鐪熷疄鏄电О
+ *
+ * displayName 鏄�屾垜瀵硅繖涓汉鐨勭浜虹О鍛笺�嶅睘浜庢垜鐨勬暟鎹紝鍒犲ソ鍙嬶紙DISABLE锛変篃淇濈暀锛涘垹浜嗗啀鍔犲洖鏉ユ椂澶囨敞鑷劧寤剁画锛屽巻鍙叉秷鎭噷浠嶄互澶囨敞杈ㄨ瘑
+ */
+export function getFriendDisplayName(friend: Pick<Friend, 'displayName' | 'nickname'>): string {
+ return friend.displayName || friend.nickname
+}
+
+/**
+ * 缇ゆ垚鍛樻樉绀哄悕锛氬ソ鍙嬪娉� > 鐢ㄦ埛缇ゅ娉紙displayUserName锛� > 鐪熷疄鏄电О
+ *
+ * WeChat 浼樺厛绾э細濂藉弸澶囨敞鏄�"鎴�"瀵硅鎴愬憳鐨勭浜虹О鍛硷紝鏈�楂樹紭鍏堬紱鍏舵鏄� ta 鍦ㄧ兢鍐呰嚜瀹氫箟鏄电О锛涙渶鍚庣湡瀹炴樀绉板厹搴曪紱璋冪敤鏂规嬁鍒� friend 鎵嶄紶鍏ワ紝娌℃嬁鍒帮紙闄岀敓浜猴級灏卞彧鐢� member 瀛楁闄嶇骇
+ */
+export function getMemberDisplayName(
+ member: { displayUserName?: string; nickname: string },
+ friend?: null | Pick<Friend, 'displayName'>
+): string {
+ return friend?.displayName || member.displayUserName || member.nickname
+}
+
+/** 缇ゆ樉绀哄悕锛氬綋鍓嶇敤鎴峰璇ョ兢鐨勫娉紙groupRemark锛� > 缇ゅ悕锛坣ame锛� */
+export function getGroupDisplayName(group: Pick<Group, 'groupRemark' | 'name'>): string {
+ return group.groupRemark || group.name
+}
+
+/**
+ * 娑堟伅鍙戦�佽�呮樉绀哄悕锛堜弗鏍肩増锛夛細绠椾笉鍑虹湡鍚嶈繑鍥� undefined
+ *
+ * 缁欓渶瑕�"鏄惁鐪熷悕"淇″彿鐨勮皟鐢ㄦ柟鐢ㄢ�斺�旀瘮濡� conversationStore 鍐冲畾瑕佷笉瑕佸啓 lastSenderDisplayName 蹇収銆佽涓嶈瑙﹀彂 fetchGroupMemberList 鍏滃簳鎷夋垚鍛�
+ *
+ * GROUP 鍦烘櫙涓� member 宸插氨浣嶄紭鍏� displayUserName / 濂藉弸澶囨敞 / 鐪熷疄鏄电О锛沵ember 缂哄け鏃跺尯鍒嗕袱绉� sender锛�
+ * - self 鈫� 鐩存帴鎷� userStore.nickname 鍏滃簳锛堟湰绔案杩滅煡閬撹嚜宸辩殑鏄电О锛屼笉闇�瑕� fetch锛涘悓鏃堕伩鍏� self 閫�缇ゅ悗 GROUP_MEMBER_QUIT 閫氱煡瑙﹀彂鍏滃簳鎷夋垚鍛� 鈫� 403锛�
+ * - 鍏朵粬浜� 鈫� 杩斿洖 undefined锛岃 deriveLastSenderDisplayName 璧板厹搴曟媺鎴愬憳
+ */
+export function tryGetSenderDisplayName(
+ senderId: number,
+ conversationType: number,
+ conversationTargetId: number
+): string | undefined {
+ if (conversationType === ImConversationType.GROUP) {
+ const group = useGroupStore().getGroup(conversationTargetId)
+ const member = group?.members?.find((m) => m.userId === senderId)
+ if (member) {
+ const friend = useFriendStore().getFriend(senderId)
+ return getMemberDisplayName(member, friend)
+ }
+ if (senderId === getCurrentUserId()) {
+ return useUserStore().userInfo?.nickname || undefined
+ }
+ return undefined
+ }
+
+ // PRIVATE / 鏈煡浼氳瘽绫诲瀷锛歴elf 璧� userStore锛屽鏂硅蛋 friend
+ if (senderId === getCurrentUserId()) {
+ return useUserStore().userInfo?.nickname || undefined
+ }
+ if (conversationType === ImConversationType.PRIVATE) {
+ const friend = useFriendStore().getFriend(senderId)
+ return friend ? getFriendDisplayName(friend) : undefined
+ }
+ return undefined
+}
+
+/**
+ * 娑堟伅鍙戦�佽�呮樉绀哄悕锛氭覆鏌撴椂瀹炴椂绠楋紝鎸� WeChat 浼樺厛绾�
+ *
+ * - 鑷繁锛歶serStore.nickname
+ * - 绉佽亰瀵规柟锛氬ソ鍙嬪娉� > 鐪熷疄鏄电О
+ * - 缇よ亰瀵规柟锛氬ソ鍙嬪娉� > 缇ゅ娉紙displayUserName锛� > 鐪熷疄鏄电О
+ * - 鏌ヤ笉鍒帮細fallbackName || (self 璧� userStore.nickname) || String(senderId)
+ */
+export function getSenderDisplayName(
+ senderId: number,
+ conversationType: number,
+ conversationTargetId: number,
+ fallbackName?: string
+): string {
+ const real = tryGetSenderDisplayName(senderId, conversationType, conversationTargetId)
+ if (real) {
+ return real
+ }
+ if (fallbackName) {
+ return fallbackName
+ }
+ // self 鍦� GROUP members 娌″姞杞芥椂锛岃嚦灏戠敤鐪熷疄鏄电О鍏滃簳娓叉煋锛堟瘮 String(senderId) 鍙嬪ソ锛夛紱鍏滃簳鎷夋垚鍛樼敱 conversationStore 瑙﹀彂锛屽洖鏉ュ悗 try 鐗堟湰鑳藉懡涓湡鍚嶈嚜鐒跺埛鏂�
+ const userStore = useUserStore()
+ if (senderId === getCurrentUserId()) {
+ return userStore.userInfo?.nickname || String(senderId)
+ }
+ return String(senderId)
+}
+
+/**
+ * 娑堟伅鍙戦�佽�呫�岀湡瀹炴樀绉般�嶏細姘歌繙鏄� nickname锛屼笉鎺哄娉�
+ *
+ * - 鑷繁锛歶serStore.nickname
+ * - 绉佽亰瀵规柟锛歠riend.nickname
+ * - 缇よ亰瀵规柟锛歮ember.nickname
+ *
+ * 涓撶粰 UserAvatar 鐨� :name 鐢ㄢ�斺�旇壊鍗¢瀛楁瘝 / alt 鏂囨湰瑕佷繚璇佸悓涓�涓汉鍦ㄦ墍鏈夌晫闈竴鑷达紝涓嶈窡澶囨敞鍙�
+ */
+export function getSenderRealNickname(
+ senderId: number,
+ conversationType: number,
+ conversationTargetId: number
+): string {
+ const userStore = useUserStore()
+ const selfUserId = getCurrentUserId()
+
+ // 缇よ亰鍏堣蛋 member.nickname锛坰elf 涔熸槸 member锛夛紝寮傚父鏃跺啀璧� self / senderId 鍏滃簳
+ if (conversationType === ImConversationType.GROUP) {
+ const group = useGroupStore().getGroup(conversationTargetId)
+ const member = group?.members?.find((m) => m.userId === senderId)
+ if (member?.nickname) {
+ return member.nickname
+ }
+ if (senderId === selfUserId) {
+ return userStore.userInfo?.nickname || String(senderId)
+ }
+ return String(senderId)
+ }
+
+ if (conversationType === ImConversationType.PRIVATE) {
+ if (senderId === selfUserId) {
+ return userStore.userInfo?.nickname || String(senderId)
+ }
+ const friend = useFriendStore().getFriend(senderId)
+ return friend?.nickname || String(senderId)
+ }
+
+ if (senderId === selfUserId) {
+ return userStore.userInfo?.nickname || String(senderId)
+ }
+ return String(senderId)
+}
+
+/**
+ * 娑堟伅鍙戦�佽�呭ご鍍忥紱鎸� conversation 涓婁笅鏂囧疄鏃舵煡 group.members / friend / userStore
+ *
+ * - 鑷繁锛歶serStore.avatar
+ * - 绉佽亰瀵规柟锛歠riend.avatar
+ * - 缇よ亰瀵规柟锛氬厛 member.avatar锛岀己鍒欓檷绾� friend.avatar
+ * - 鏌ヤ笉鍒帮細杩斿洖绌轰覆锛岃皟鐢ㄦ柟璧� UserAvatar 鑹插崱鍏滃簳
+ */
+export function getSenderAvatar(
+ senderId: number,
+ conversationType: number,
+ conversationTargetId: number
+): string {
+ const userStore = useUserStore()
+ if (senderId === getCurrentUserId()) {
+ return userStore.userInfo?.avatar || ''
+ }
+ if (conversationType === ImConversationType.GROUP) {
+ const group = useGroupStore().getGroup(conversationTargetId)
+ const member = group?.members?.find((m) => m.userId === senderId)
+ if (member?.avatar) {
+ return member.avatar
+ }
+ }
+ return useFriendStore().getFriend(senderId)?.avatar || ''
+}
+
+/**
+ * 缇ゆ秷鎭� @ mention 鍊欓�夛細鎸� atUserIds 鍙嶆煡缇ゆ垚鍛橈紝name 鏀堕泦鎵�鏈夊彲鑳藉瓧闈㈤噺锛堝惈濂藉弸澶囨敞 / 缇よ嚜瀹氫箟鏄电О锛夛紝displayName 缁熶竴鎸囧悜鐪熷疄鏄电О
+ *
+ * 鍚屽瓧闈㈤噺琚 userId 鍏变韩鏃舵爣璁� ambiguous锛岀敱 parser 鏁存鍚冩垚鏅�氭枃鏈紱
+ * 鐩存帴鍓旈櫎浼氳鐭墠缂�鍊欓�夛紙濡傘�孈寮犮�嶏級鎶㈠悆銆孈寮犱笁銆嶇殑鍓嶇紑锛岄敊缁戝埌鍞竴鐨勩�屽紶銆嶇敤鎴�
+ */
+export function getMentionCandidates(
+ atUserIds: number[] | undefined,
+ conversation: null | Pick<Conversation, 'targetId' | 'type'> | undefined
+): MentionCandidate[] {
+ if (!atUserIds || atUserIds.length === 0) {
+ return EMPTY_MENTIONS
+ }
+ if (!conversation || conversation.type !== ImConversationType.GROUP) {
+ return EMPTY_MENTIONS
+ }
+ // 缇ゆ垚鍛橀寤� Map锛岄伩鍏嶆瘡涓� atUserId 璧颁竴娆� array find锛園鍏ㄤ綋鎴愬憳鍦烘櫙涓嬫垚鍛樻暟 脳 atUserIds 鏄� N虏锛�
+ const members = useGroupStore().getGroup(conversation.targetId)?.members || []
+ const memberById = new Map(members.map((m) => [m.userId, m]))
+ const friendStore = useFriendStore()
+ const candidates: MentionCandidate[] = []
+ const seen = new Set<string>()
+ for (const userId of atUserIds) {
+ // @鍏ㄤ綋鎴愬憳鏄櫄鎷熶吉鎴愬憳锛寀serId = -1 鍦� group.members 閲屾煡涓嶅埌锛屾敞鍏ュ瓧闈㈤噺銆屾墍鏈変汉銆嶅�欓��
+ if (userId === IM_AT_ALL_USER_ID) {
+ const key = `${IM_AT_ALL_USER_ID}#${IM_AT_ALL_NICKNAME}`
+ if (!seen.has(key)) {
+ seen.add(key)
+ candidates.push({
+ userId: IM_AT_ALL_USER_ID,
+ name: IM_AT_ALL_NICKNAME,
+ displayName: IM_AT_ALL_NICKNAME
+ })
+ }
+ continue
+ }
+ const member = memberById.get(userId)
+ const friend = friendStore.getFriend(userId)
+ const nickname = (member?.nickname || friend?.nickname || '').trim()
+ if (!nickname) {
+ continue
+ }
+ for (const literal of [nickname, friend?.displayName, member?.displayUserName]) {
+ const trimmed = (literal || '').trim()
+ if (!trimmed) {
+ continue
+ }
+ const key = `${userId}#${trimmed}`
+ if (seen.has(key)) {
+ continue
+ }
+ seen.add(key)
+ candidates.push({ userId, name: trimmed, displayName: nickname })
+ }
+ }
+ const nameCount = new Map<string, number>()
+ for (const candidate of candidates) {
+ nameCount.set(candidate.name, (nameCount.get(candidate.name) ?? 0) + 1)
+ }
+ for (const candidate of candidates) {
+ if ((nameCount.get(candidate.name) ?? 0) > 1) {
+ candidate.ambiguous = true
+ }
+ }
+ return candidates
+}
+
+/**
+ * 鏂囨湰 / tip 閲� mention 娈电偣鍑荤殑缁熶竴鍏ュ彛锛氭淳鐢� User 寮� UserInfoCard
+ *
+ * 澶村儚璧� getSenderAvatar锛涙樀绉板彇鐪熷疄鏄电О锛宖riend / member 閮芥煡涓嶅埌鏃剁敤 fallbackName 鍏滃簳锛堝鐏版潯 tip 娈电殑瀛楅潰閲忔枃鏈級锛�
+ * addSource 缇よ亰甯� GROUP + 缇ゅ悕锛堢敤浜庛�屽姞涓哄ソ鍙嬨�嶇殑鏉ユ簮 + 澶囨敞寮规枃妗堬級锛岀鑱婇檷绾� SEARCH锛�
+ * @鍏ㄤ綋鎴愬憳 鏄箍鎾� mention 娌℃湁鍏蜂綋鐢ㄦ埛瀹炰綋锛屽榻愬井淇� PC 涓嶅脊鍗$墖
+ */
+export function openMentionUserInfoCardAtEvent(
+ userId: number,
+ event: MouseEvent,
+ fallbackName?: string
+): void {
+ if (userId === IM_AT_ALL_USER_ID) {
+ return
+ }
+ const conversation = useConversationStore().activeConversation
+ const isGroup = conversation?.type === ImConversationType.GROUP
+ const group = isGroup && conversation ? useGroupStore().getGroup(conversation.targetId) : undefined
+ const member = group?.members?.find((m) => m.userId === userId)
+ const friend = useFriendStore().getFriend(userId)
+ const user: User = {
+ id: userId,
+ nickname: friend?.nickname || member?.nickname || fallbackName || String(userId),
+ avatar: getSenderAvatar(userId, conversation?.type ?? 0, conversation?.targetId ?? 0)
+ }
+ useImUiStore().openUserInfoCardAtEvent(
+ user,
+ event,
+ isGroup ? ImFriendAddSource.GROUP : ImFriendAddSource.SEARCH,
+ isGroup ? group?.name || '' : ''
+ )
+}
+
+/** 鎬у埆鍥炬爣锛沀NKNOWN / null / undefined 涓�寰嬩笉灞曠ず锛屽榻愬井淇$暀鐧� */
+export function getGenderIcon(sex?: number): string {
+ if (sex === SystemUserSexEnum.MALE) {
+ return 'mdi:human-male'
+ }
+ if (sex === SystemUserSexEnum.FEMALE) {
+ return 'mdi:human-female'
+ }
+ return ''
+}
+
+/** 鎬у埆鍥炬爣涓婚鑹诧細鐢疯摑銆佸コ绮� */
+export function getGenderColor(sex?: number): string {
+ if (sex === SystemUserSexEnum.MALE) {
+ return '#5b97f5'
+ }
+ if (sex === SystemUserSexEnum.FEMALE) {
+ return '#f56c92'
+ }
+ return ''
+}
+
+/** 澶村儚鑹插崱搴曡壊璋冭壊鏉匡紙鍙傝�冨井淇★級 */
+const AVATAR_BG_COLORS = ['#07C160', '#1A95FF', '#FA9D3B', '#9163E0', '#F76760', '#1ABC9C']
+
+/** 澶村儚鑹插崱鏂囧瓧锛氫腑鏂囧彇棣栧瓧銆佽嫳鏂囧彇鍓� 2 瀛楁瘝澶у啓銆佸叾浠栧彇棣栧瓧澶у啓銆佺┖鍚嶈繑鍥炵┖涓� */
+export function getAvatarText(name?: string): string {
+ const trimmed = name?.trim()
+ if (!trimmed) {
+ return ''
+ }
+ const first = trimmed.charAt(0)
+ const code = first.codePointAt(0) ?? 0
+ if (code >= 0x4E_00 && code <= 0x9F_A5) {
+ return first
+ }
+ const letters = trimmed.match(/[A-Za-z]/g)
+ if (!letters || letters.length === 0) {
+ return first.toUpperCase()
+ }
+ return letters.slice(0, 2).join('').toUpperCase()
+}
+
+/** 澶村儚鑹插崱搴曡壊锛氭寜 name charCode 涔嬪拰鍙栬皟鑹叉澘鑹诧紝绌哄悕璧伴粯璁ょ伆 */
+export function getAvatarBgColor(name?: string): string {
+ if (!name) {
+ return '#909399'
+ }
+ let hash = 0
+ for (let i = 0; i < name.length; i++) {
+ hash += name.codePointAt(i) ?? 0
+ }
+ return AVATAR_BG_COLORS[hash % AVATAR_BG_COLORS.length] || '#909399'
+}
--
Gitblit v1.9.3