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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user