From 0ab8b292f25cb49eb819632219e15331a92536be Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 30 Apr 2026 15:22:35 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(im):=20=E5=A2=9E=E5=8A=A0=20pi?= =?UTF-8?q?nyin=20=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/im/friend/index.ts | 2 + .../im/home/pages/contact/FriendList.vue | 62 +++-- src/views/im/home/pages/contact/index.vue | 3 + .../friend/components/AddFriendDialog.vue | 211 ------------------ src/views/im/home/store/friendStore.ts | 2 + src/views/im/home/types/index.ts | 4 + 6 files changed, 55 insertions(+), 229 deletions(-) delete mode 100644 src/views/im/home/pages/friend/components/AddFriendDialog.vue diff --git a/src/api/im/friend/index.ts b/src/api/im/friend/index.ts index e0730485b..bd39c3c1c 100644 --- a/src/api/im/friend/index.ts +++ b/src/api/im/friend/index.ts @@ -6,11 +6,13 @@ export interface ImFriendRespVO { friendUserId: number // 好友的用户编号 muted?: boolean // 是否免打扰 displayName?: string // 好友展示备注(仅自己可见) + displayNamePinyin?: string // 备注的拼音(小写无空格,前端按首字母分桶 / 拼音搜索) status?: number // 好友状态(0=正常,1=已删除) addTime?: string // 添加好友时间 deleteTime?: string // 删除好友时间 // 聚合字段(自 AdminUser) nickname?: string // 好友昵称 + nicknamePinyin?: string // 昵称的拼音(小写无空格,前端按首字母分桶 / 拼音搜索) avatar?: string // 好友头像 } diff --git a/src/views/im/home/pages/contact/FriendList.vue b/src/views/im/home/pages/contact/FriendList.vue index a5e418076..6cbf9a33d 100644 --- a/src/views/im/home/pages/contact/FriendList.vue +++ b/src/views/im/home/pages/contact/FriendList.vue @@ -64,7 +64,18 @@ const emit = defineEmits<{ const expanded = ref(true) -/** 关键字过滤:兼顾备注 + 原昵称,记不住哪个就按哪个搜 */ +/** 拼音首字母拼接:「lao zhang」→ "lz",用于支持「输 lz 搜老张」 */ +function pinyinInitials(pinyin?: string): string { + if (!pinyin) { + return '' + } + return pinyin + .split(' ') + .map((word) => word.charAt(0)) + .join('') +} + +/** 关键字过滤:备注 / 昵称 / 全拼 / 首字母任一命中即可,记不住哪个就按哪个搜 */ const filtered = computed(() => { const keywordLower = props.keyword.trim().toLowerCase() return props.friends.filter((friend) => { @@ -74,47 +85,62 @@ const filtered = computed(() => { if (!keywordLower) { return true } + // 全拼搜索去掉空格,让「laozhang」也能命中「lao zhang」 + const nicknamePinyin = friend.nicknamePinyin || '' + const displayNamePinyin = friend.displayNamePinyin || '' return ( (friend.nickname || '').toLowerCase().includes(keywordLower) || - (friend.displayName || '').toLowerCase().includes(keywordLower) + (friend.displayName || '').toLowerCase().includes(keywordLower) || + nicknamePinyin.replace(/\s/g, '').includes(keywordLower) || + displayNamePinyin.replace(/\s/g, '').includes(keywordLower) || + pinyinInitials(nicknamePinyin).includes(keywordLower) || + pinyinInitials(displayNamePinyin).includes(keywordLower) ) }) }) -/** - * 字母分桶: - * - ASCII 字母直接取首字母大写 - * - 中文 / 其它非拉丁字符统一进 "#"(项目未引 pinyin 库,留中文按 "#" 显示,避免引入新依赖) - * - 桶内按显示名 localeCompare 自然序 - */ +/** 桶排序键:优先备注拼音,回落昵称拼音 / 名字本身(兜底英文 / 数字场景) */ +function getSortKey(friend: FriendLite): string { + return ( + friend.displayNamePinyin || + friend.nicknamePinyin || + (friend.displayName || friend.nickname || '').toLowerCase() + ) +} + +/** 取分桶字母:拼音首字母大写,非字母(如纯符号)兜底 "#" */ +function getBucketLetter(friend: FriendLite): string { + const first = getSortKey(friend).charAt(0) + return /^[a-zA-Z]$/.test(first) ? first.toUpperCase() : '#' +} + interface FriendBucket { letter: string list: FriendLite[] } -// TODO @AI:需要增加拼音返回;我们要讨论下,hutool 有拼音库;可能要引入下库; +/** 字母分桶:A-Z 优先,"#" 兜底;桶内按拼音 / 名字自然序 */ const buckets = computed(() => { const map = new Map() for (const friend of filtered.value) { - const name = (friend.displayName || friend.nickname || '').trim() - const first = name.charAt(0) - const letter = /^[a-zA-Z]$/.test(first) ? first.toUpperCase() : '#' + const letter = getBucketLetter(friend) if (!map.has(letter)) { map.set(letter, []) } map.get(letter)!.push(friend) } - // letter 排序:A-Z 在前,"#" 兜底 const letters = Array.from(map.keys()).sort((a, b) => { - if (a === '#') return 1 - if (b === '#') return -1 + if (a === '#') { + return 1 + } + if (b === '#') { + return -1 + } return a.localeCompare(b) }) return letters.map((letter) => ({ letter, - list: map.get(letter)!.sort((a, b) => - (a.displayName || a.nickname || '').localeCompare(b.displayName || b.nickname || '') - ) + list: map.get(letter)!.sort((a, b) => getSortKey(a).localeCompare(getSortKey(b))) })) }) diff --git a/src/views/im/home/pages/contact/index.vue b/src/views/im/home/pages/contact/index.vue index 840e9daf9..14ce229e6 100644 --- a/src/views/im/home/pages/contact/index.vue +++ b/src/views/im/home/pages/contact/index.vue @@ -108,12 +108,15 @@ type Selection = { type: 'friend'; friend: FriendLite } | { type: 'group'; group const selection = ref(null) const keyword = ref('') +/** 好友列表的展示快照:附带后端算好的拼音,给 FriendList 做字母分桶 / 拼音搜索 */ const friends = computed(() => friendStore.getActiveFriends.map((friend: Friend) => ({ id: friend.friendUserId, nickname: friend.nickname, + nicknamePinyin: friend.nicknamePinyin, avatar: friend.avatar, displayName: friend.displayName, + displayNamePinyin: friend.displayNamePinyin, deleted: friend.status === CommonStatusEnum.DISABLE })) ) diff --git a/src/views/im/home/pages/friend/components/AddFriendDialog.vue b/src/views/im/home/pages/friend/components/AddFriendDialog.vue deleted file mode 100644 index d8451ac0f..000000000 --- a/src/views/im/home/pages/friend/components/AddFriendDialog.vue +++ /dev/null @@ -1,211 +0,0 @@ - - - - - diff --git a/src/views/im/home/store/friendStore.ts b/src/views/im/home/store/friendStore.ts index 1c6c7ee09..4d7816421 100644 --- a/src/views/im/home/store/friendStore.ts +++ b/src/views/im/home/store/friendStore.ts @@ -226,9 +226,11 @@ function convertFriend(vo: ImFriendRespVO): Friend { id: vo.id, friendUserId: vo.friendUserId, nickname: vo.nickname || String(vo.friendUserId), + nicknamePinyin: vo.nicknamePinyin, avatar: vo.avatar, muted: !!vo.muted, displayName: vo.displayName || '', + displayNamePinyin: vo.displayNamePinyin, status: vo.status, addTime: vo.addTime ? new Date(vo.addTime).getTime() : undefined, deleteTime: vo.deleteTime ? new Date(vo.deleteTime).getTime() : undefined diff --git a/src/views/im/home/types/index.ts b/src/views/im/home/types/index.ts index 04527c757..3e546aabf 100644 --- a/src/views/im/home/types/index.ts +++ b/src/views/im/home/types/index.ts @@ -145,9 +145,11 @@ export interface Friend { id?: number // 好友关系记录编号(本地乐观新增时可能暂缺) friendUserId: number // 好友用户编号(与 Conversation.targetId 对齐) nickname: string // 好友昵称(对方真实昵称,永远不被备注覆盖;UI 显示走 displayName || nickname) + nicknamePinyin?: string // 昵称的拼音(后端用 Pinyin4j 算好回填,小写无空格) avatar?: string // 好友头像 muted?: boolean // 是否免打扰(不展示未读徽标 + 不响提示音) displayName?: string // 好友展示备注:仅自己可见的别名(单字段不歧义,不带 Friend 前缀) + displayNamePinyin?: string // 备注的拼音(后端用 Pinyin4j 算好回填,小写无空格) status?: number // 好友状态,对齐 CommonStatusEnum(DISABLE = 已删除,软删保留记录) addTime?: number // 添加好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换) deleteTime?: number // 删除好友时间(毫秒时间戳;后端为 LocalDateTime 字符串,在 convertFriend 转换) @@ -175,8 +177,10 @@ export interface User { export interface FriendLite { id: number nickname: string + nicknamePinyin?: string // 昵称拼音(用于字母分桶 / 拼音搜索) avatar?: string displayName?: string + displayNamePinyin?: string // 备注拼音(优先于 nicknamePinyin 参与分桶) deleted?: boolean }