feat: 产品筛选器的模糊搜索功能
- 引入纯前端依赖Fuse.js用于模糊匹配 - 新增Utils-fuzzyFilter封装Fuse的初始化与匹配 - 在前端将字符串匹配改为模糊匹配
This commit is contained in:
@ -73,8 +73,13 @@
|
|||||||
return products;
|
return products;
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredDocuments = computed(() =>
|
const filteredDocuments = computed(() => {
|
||||||
documents.value.filter((doc: DocumentListView) => {
|
const fuzzyMatchedDocuments = fuzzyMatch(documents.value, {
|
||||||
|
keyword: filters.keyword,
|
||||||
|
keys: ['title'],
|
||||||
|
threshold: 0.2,
|
||||||
|
});
|
||||||
|
return fuzzyMatchedDocuments.filter((doc: DocumentListView) => {
|
||||||
const matchProduct = filters.selectedProduct
|
const matchProduct = filters.selectedProduct
|
||||||
? doc.products?.some(
|
? doc.products?.some(
|
||||||
(product: DocumentListProduct) =>
|
(product: DocumentListProduct) =>
|
||||||
@ -87,13 +92,9 @@
|
|||||||
)
|
)
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
const matchKeyword = filters.keyword
|
return matchProduct;
|
||||||
? doc.title && doc.title.includes(filters.keyword)
|
});
|
||||||
: true;
|
});
|
||||||
|
|
||||||
return matchProduct && matchKeyword;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => filters.selectedType,
|
() => filters.selectedType,
|
||||||
|
|||||||
@ -74,7 +74,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const filteredQuestions = computed(() => {
|
const filteredQuestions = computed(() => {
|
||||||
return questions.value.filter((question: QuestionListView) => {
|
const fuzzyMatchedQuestions = fuzzyMatch(questions.value, {
|
||||||
|
keyword: filters.keyword,
|
||||||
|
keys: ['title', 'content'],
|
||||||
|
threshold: 0.2,
|
||||||
|
});
|
||||||
|
return fuzzyMatchedQuestions.filter((question: QuestionListView) => {
|
||||||
const matchProduct = filters.selectedProduct
|
const matchProduct = filters.selectedProduct
|
||||||
? question.products?.some(
|
? question.products?.some(
|
||||||
(product: QuestionListProduct) =>
|
(product: QuestionListProduct) =>
|
||||||
@ -87,12 +92,7 @@
|
|||||||
)
|
)
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
const matchKeyword = filters.keyword
|
return matchProduct;
|
||||||
? (question.title && question.title.includes(filters.keyword)) ||
|
|
||||||
(question.content && question.content.includes(filters.keyword))
|
|
||||||
: true;
|
|
||||||
|
|
||||||
return matchProduct && matchKeyword;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
95
app/utils/fuzzyFilter.ts
Normal file
95
app/utils/fuzzyFilter.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@
|
|||||||
"@vueuse/nuxt": "^13.6.0",
|
"@vueuse/nuxt": "^13.6.0",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "^3.2.6",
|
||||||
"element-plus": "^2.10.7",
|
"element-plus": "^2.10.7",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"meilisearch": "^0.53.0",
|
"meilisearch": "^0.53.0",
|
||||||
"nuxt": "^4.0.3",
|
"nuxt": "^4.0.3",
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -47,6 +47,9 @@ importers:
|
|||||||
element-plus:
|
element-plus:
|
||||||
specifier: ^2.10.7
|
specifier: ^2.10.7
|
||||||
version: 2.11.2(vue@3.5.21(typescript@5.9.2))
|
version: 2.11.2(vue@3.5.21(typescript@5.9.2))
|
||||||
|
fuse.js:
|
||||||
|
specifier: ^7.1.0
|
||||||
|
version: 7.1.0
|
||||||
markdown-it:
|
markdown-it:
|
||||||
specifier: ^14.1.0
|
specifier: ^14.1.0
|
||||||
version: 14.1.0
|
version: 14.1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user