feat: 为下拉选择组件添加搜索过滤功能,支持拼音匹配

- 新增 search.ts 搜索工具,支持中文拼音(全拼/首字母)模糊匹配
- 新增 select-search-context.ts,通过 provide/inject 为 radix-vue Select 组件注入搜索能力
- MultiSelect、ModelMultiSelect 组件集成搜索框,选项超过阈值时自动显示
- select-content/select-item 组件支持搜索过滤与空状态提示
- StandaloneKeyFormDialog、UserFormDialog 中手写下拉框替换为复用 MultiSelect 组件
- 引入 pinyin-pro 依赖,按需懒加载

Closes #210

Co-authored-by: AoaoMH <kidxb@vip.qq.com>
This commit is contained in:
fawney19
2026-03-09 19:51:02 +08:00
parent 0258d01ee6
commit 46bab1b97f
10 changed files with 667 additions and 333 deletions

View File

@@ -0,0 +1,111 @@
const CHINESE_CHAR_REGEX = /[\u3400-\u9fff]/u
type PinyinModule = typeof import('pinyin-pro')
let pinyinMod: PinyinModule | null = null
let pinyinLoading: Promise<void> | null = null
function loadPinyin(): Promise<void> {
if (pinyinMod) return Promise.resolve()
if (pinyinLoading) return pinyinLoading
pinyinLoading = import('pinyin-pro')
.then((mod) => {
pinyinMod = mod
})
.catch(() => {
pinyinLoading = null
})
return pinyinLoading
}
export function preloadPinyin(): void {
loadPinyin()
}
function normalizeSearchText(value: string): string {
return value.normalize('NFKC').toLocaleLowerCase().replace(/\s+/g, ' ').trim()
}
function compactSearchText(value: string): string {
return normalizeSearchText(value).replace(/\s+/g, '')
}
function addSearchTerm(terms: Set<string>, value: string): void {
const normalizedValue = normalizeSearchText(value)
if (!normalizedValue) {
return
}
terms.add(normalizedValue)
const compactValue = normalizedValue.replace(/\s+/g, '')
if (compactValue && compactValue !== normalizedValue) {
terms.add(compactValue)
}
}
function getPinyinSearchTerms(value: string): string[] {
if (!CHINESE_CHAR_REGEX.test(value) || !pinyinMod) {
return []
}
const fullPinyin = pinyinMod.pinyin(value, {
toneType: 'none',
type: 'array',
})
const initials = pinyinMod.pinyin(value, {
pattern: 'first',
toneType: 'none',
type: 'array',
})
return [
fullPinyin.join(' '),
fullPinyin.join(''),
initials.join(' '),
initials.join(''),
]
}
export function buildSearchTerms(
...values: Array<string | null | undefined>
): string[] {
const terms = new Set<string>()
for (const rawValue of values) {
const value = rawValue?.trim()
if (!value) {
continue
}
addSearchTerm(terms, value)
for (const pinyinTerm of getPinyinSearchTerms(value)) {
addSearchTerm(terms, pinyinTerm)
}
}
return Array.from(terms)
}
export function matchesSearchQuery(
query: string,
...values: Array<string | null | undefined>
): boolean {
const normalizedQuery = normalizeSearchText(query)
if (!normalizedQuery) {
return true
}
const compactQuery = compactSearchText(normalizedQuery)
const queryVariants =
compactQuery && compactQuery !== normalizedQuery
? [normalizedQuery, compactQuery]
: [normalizedQuery]
const searchTerms = buildSearchTerms(...values)
return searchTerms.some((term) =>
queryVariants.some((variant) => term.includes(variant)),
)
}