From 27cd042df9aca0383a49f3514bc21958dd890912 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 29 六月 2026 15:42:23 +0800
Subject: [PATCH] 银川 1.联调产品维护页面 2.添加IM即时通讯模块

---
 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