gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import type { FriendLite } from '../types'
 
import { computed, type ComputedRef, type Ref } from 'vue'
 
/** 字母分桶结果:letter 取 'A'-'Z' 或兜底 '#';list 桶内按拼音 / 名字自然序 */
export interface FriendBucket {
  letter: string
  list: FriendLite[]
}
 
/** 取分桶 / 排序键:备注拼音优先 → 昵称拼音 → 名字本身(兜底英文 / 数字) */
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() : '#'
}
 
/** 拼音首字母拼接:「lao zhang」→ 'lz',支持「输 lz 搜老张」 */
function pinyinInitials(pinyin?: string): string {
  if (!pinyin) {
    return ''
  }
  return pinyin
    .split(' ')
    .map((word) => word.charAt(0))
    .join('')
}
 
/**
 * 好友列表的搜索 + 字母分桶
 *
 * - 搜索匹配:备注 / 昵称 / 全拼(去空格)/ 首字母任一命中
 * - 分桶规则:A-Z + '#' 兜底,桶内按 getSortKey 自然序
 *
 * 通讯录页 FriendList 与选择类弹窗 FriendPickerPanel 共用,避免规则各自实现走偏
 */
export function useFriendBuckets(
  friends: ComputedRef<FriendLite[]> | Ref<FriendLite[]>,
  keyword: Ref<string>
): {
  buckets: ComputedRef<FriendBucket[]>
  filtered: ComputedRef<FriendLite[]>
} {
  const filtered = computed(() => {
    const keywordLower = keyword.value.trim().toLowerCase()
    if (!keywordLower) {
      return friends.value
    }
    return friends.value.filter((friend) => {
      const nicknamePinyin = friend.nicknamePinyin || ''
      const displayNamePinyin = friend.displayNamePinyin || ''
      // 全拼搜索去掉空格,让「laozhang」也能命中「lao zhang」
      return (
        (friend.nickname || '').toLowerCase().includes(keywordLower) ||
        (friend.displayName || '').toLowerCase().includes(keywordLower) ||
        nicknamePinyin.replaceAll(/\s/g, '').includes(keywordLower) ||
        displayNamePinyin.replaceAll(/\s/g, '').includes(keywordLower) ||
        pinyinInitials(nicknamePinyin).includes(keywordLower) ||
        pinyinInitials(displayNamePinyin).includes(keywordLower)
      )
    })
  })
 
  const buckets = computed<FriendBucket[]>(() => {
    const map = new Map<string, FriendLite[]>()
    for (const friend of filtered.value) {
      const letter = getBucketLetter(friend)
      const bucket = map.get(letter) ?? []
      bucket.push(friend)
      map.set(letter, bucket)
    }
    const letters = [...map.keys()].toSorted((a, b) => {
      // '#' 永远排末尾,A-Z 走 localeCompare
      if (a === '#') {
        return 1
      }
      if (b === '#') {
        return -1
      }
      return a.localeCompare(b)
    })
    return letters.map((letter) => ({
      letter,
      list: (map.get(letter) ?? []).toSorted((a, b) =>
        getSortKey(a).localeCompare(getSortKey(b))
      )
    }))
  })
 
  return { filtered, buckets }
}