96 lines
2.2 KiB
TypeScript
96 lines
2.2 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 =>
|
||
text.normalize('NFKC').toLowerCase().trim();
|
||
|
||
// --- 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,
|
||
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;
|
||
},
|
||
});
|
||
|
||
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;
|
||
})
|
||
);
|
||
}
|