feat(M4): FR-R2/R6/R7 搜索+标签筛选——search hay 加 tags+links 元数据(R2); TagFilter 加折叠按钮(R6); tagPaths IPC+FileTree AND 过滤选中标签的文档(R7); store 加 tagFilter; search-tag E2E 2 项

This commit is contained in:
hunter-dev
2026-08-26 14:53:00 +08:00
parent bfb1f4b014
commit 01ea477d18
7 changed files with 150 additions and 13 deletions
@@ -12,8 +12,8 @@ export class SearchService {
constructor(private root: string, private fileService: { getRoot: () => string }) {}
// 遍历库读取所有文档,建立简易索引(M4:内存全量,大库后续优化)
private async readAll(): Promise<{ path: string; title: string; body: string; tags: string[] }[]> {
const out: { path: string; title: string; body: string; tags: string[] }[] = [];
private async readAll(): Promise<{ path: string; title: string; body: string; tags: string[]; links: string[] }[]> {
const out: { path: string; title: string; body: string; tags: string[]; links: string[] }[] = [];
const walk = async (dirAbs: string, dirRel: string) => {
const entries = await fs.readdir(dirAbs, { withFileTypes: true });
for (const e of entries) {
@@ -25,7 +25,7 @@ export class SearchService {
const raw = await fs.readFile(abs, 'utf8');
const { meta, body } = splitFrontmatter(raw);
const relPosix = rel.split(nodePath.sep).join('/');
out.push({ path: relPosix, title: meta.title ?? e.name.slice(0, -MD_EXT.length), body, tags: meta.tags ?? [] });
out.push({ path: relPosix, title: meta.title ?? e.name.slice(0, -MD_EXT.length), body, tags: meta.tags ?? [], links: meta.links ?? [] });
}
}
};
@@ -39,7 +39,7 @@ export class SearchService {
const all = await this.readAll();
const hits: SearchHit[] = [];
for (const d of all) {
const hay = (d.title + '\n' + d.body).toLowerCase();
const hay = (d.title + '\n' + d.body + '\n' + d.tags.join(' ') + '\n' + (d.links ?? []).join(' ')).toLowerCase();
const idx = hay.indexOf(q);
if (idx !== -1) {
// 摘要:命中位置附近,超长截尾保留 ~120 字符
@@ -58,4 +58,14 @@ export class SearchService {
for (const d of all) for (const t of d.tags) m.set(t, (m.get(t) ?? 0) + 1);
return [...m.entries()].map(([tag, count]) => ({ tag, count })).sort((a, b) => a.tag.localeCompare(b.tag));
}
// FR-R7:返回标签→文档路径列表映射,供 FileTree AND 过滤
async tagPaths(): Promise<Record<string, string[]>> {
const all = await this.readAll();
const m = new Map<string, string[]>();
for (const d of all) for (const t of d.tags) { const arr = m.get(t) ?? []; arr.push(d.path); m.set(t, arr); }
const out: Record<string, string[]> = {};
for (const [tag, paths] of m) out[tag] = paths;
return out;
}
}