Files
jinshen-website/app/utils/fuzzyFilter.ts
R2m1liA 4c8dfb5b56 feat: 产品筛选器的模糊搜索功能
- 引入纯前端依赖Fuse.js用于模糊匹配
- 新增Utils-fuzzyFilter封装Fuse的初始化与匹配
- 在前端将字符串匹配改为模糊匹配
2025-11-10 15:08:32 +08:00

96 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
})
);
}