All checks were successful
deploy to server / build-and-deploy (push) Successful in 3m38s
- 使用pinyin-pro进行汉语拼音转换 - 调整搜索权重
137 lines
3.5 KiB
TypeScript
137 lines
3.5 KiB
TypeScript
import Fuse from 'fuse.js';
|
||
|
||
interface FuzzyFilterOptions<T> {
|
||
/** 匹配关键字 */
|
||
keyword: string;
|
||
|
||
/** 搜索字段 */
|
||
keys: Array<FuzzyKeyOf<T>>;
|
||
|
||
/** 模糊程度 (0~1,越低越严格) */
|
||
threshold?: number;
|
||
|
||
/** 最小匹配字符数 */
|
||
minMatchCharLength?: number;
|
||
|
||
/** 当前语言 */
|
||
locale?: string;
|
||
}
|
||
|
||
/** 限定 keys 只能选择字符串字段 */
|
||
type FuzzyKeyOf<T> = {
|
||
[K in keyof T]: T[K] extends string ? K : never;
|
||
}[keyof T];
|
||
|
||
export function fuzzyMatch<T extends object>(
|
||
source: T[],
|
||
options: FuzzyFilterOptions<T>
|
||
): T[] {
|
||
const {
|
||
keyword,
|
||
keys,
|
||
threshold = 0.35,
|
||
minMatchCharLength = 1,
|
||
locale = 'auto',
|
||
} = options;
|
||
|
||
// --- 多语言比较器 ---
|
||
const collator = new Intl.Collator(locale === 'auto' ? undefined : locale, {
|
||
sensitivity: 'base',
|
||
ignorePunctuation: true,
|
||
});
|
||
|
||
// --- 文本标准化函数 ---
|
||
const normalizeText = (text: string): string => {
|
||
const normalizedText = text.normalize('NFKC').toLowerCase().trim();
|
||
const translit = transliterateText(normalizedText);
|
||
return `${normalizedText} ${translit}`;
|
||
};
|
||
|
||
/**
|
||
* 类型安全的对象取值函数
|
||
*/
|
||
function getPropertyByPath<T>(
|
||
obj: T,
|
||
path: string | string[]
|
||
): string | undefined {
|
||
const keys = Array.isArray(path) ? path : path.split('.');
|
||
let value: unknown = obj;
|
||
for (const key of keys) {
|
||
if (value && typeof value === 'object' && key in value) {
|
||
value = (value as Record<string, unknown>)[key];
|
||
} else {
|
||
return undefined;
|
||
}
|
||
}
|
||
return typeof value === 'string' ? value : undefined;
|
||
}
|
||
|
||
// --- fallback 模糊匹配算法 ---
|
||
const fallbackFuzzyMatch = (text: string, pattern: string): boolean => {
|
||
const normalizedText = normalizeText(text);
|
||
const normalizedPattern = normalizeText(pattern);
|
||
let i = 0;
|
||
for (const char of normalizedPattern) {
|
||
i = normalizedText.indexOf(char, i);
|
||
if (i === -1) return false;
|
||
i++;
|
||
}
|
||
return true;
|
||
};
|
||
|
||
const k = keyword.trim();
|
||
|
||
if (!k) {
|
||
return source;
|
||
}
|
||
|
||
if (import.meta.client) {
|
||
// 客户端使用Fuze.js进行模糊匹配
|
||
const fuse = new Fuse<T>(source, {
|
||
keys: keys as string[],
|
||
threshold,
|
||
minMatchCharLength,
|
||
ignoreLocation: true,
|
||
findAllMatches: true,
|
||
isCaseSensitive: false,
|
||
getFn: (obj, path) => {
|
||
const value = getPropertyByPath(obj, path);
|
||
if (typeof value === 'string') {
|
||
const normalized = value
|
||
.normalize('NFKC')
|
||
.replace(/\s+/g, '')
|
||
.toLowerCase()
|
||
.trim();
|
||
const translit = transliterateText(normalized);
|
||
logger.debug(`${normalized} => ${translit}`);
|
||
// 拼接原文 + 转写,提升中外文混合匹配效果
|
||
return `${normalized} ${translit}`;
|
||
}
|
||
return value;
|
||
},
|
||
sortFn: (a, b) => {
|
||
const itemA = a.item as T;
|
||
const itemB = b.item as T;
|
||
const key = keys[0];
|
||
const aValue = itemA[key];
|
||
const bValue = itemB[key];
|
||
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
||
return collator.compare(aValue, bValue);
|
||
}
|
||
return 0;
|
||
},
|
||
});
|
||
|
||
logger.debug('Fuzzy search options:', options);
|
||
logger.debug('Fuzzy search keyword:', k);
|
||
return fuse.search(k).map((result) => result.item);
|
||
}
|
||
|
||
return source.filter((item) =>
|
||
keys.some((key) => {
|
||
const value = item[key];
|
||
return typeof value === 'string' ? fallbackFuzzyMatch(value, k) : false;
|
||
})
|
||
);
|
||
}
|