Skip to content

Commit b824c1e

Browse files
committed
feat(knowledge): 组行体系徽标接线(触点①)
1 parent d555ca1 commit b824c1e

4 files changed

Lines changed: 78 additions & 5 deletions

File tree

‎app/src/components/GroupSidebar.test.tsx‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
* 修复):组 A 改了判类(未确认)→ 点组 B ⓘ → 弹层显示 B 且
77
* 判类下拉回到 B.kind(防把 A 的选择误用到 B——路径: 改判误操作)。
88
* invoke 全 mock(list_note_groups/count_due_cards/list_fragments/
9-
* get_feature_flags/week_contract_status)。
9+
* get_feature_flags/week_contract_status/list_knowledge_systems/
10+
* list_knowledge_links)。
1011
*/
1112
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
1213
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
@@ -38,6 +39,10 @@ beforeEach(() => {
3839
case "get_feature_flags": return { feedCapture: true };
3940
case "week_contract_status":
4041
return { contract: null, weekStart: 0, actualDays: 0, actualCards: 0, minimalDayMet: false };
42+
case "list_knowledge_systems":
43+
return [{ id: 10, parentSystemId: null, name: "摄影", kind: "domain", coreQuestion: null, status: "active", createdAt: 0, updatedAt: 0 }];
44+
case "list_knowledge_links":
45+
return [{ id: 1, systemId: 10, nodeId: null, conceptId: null, modelId: null, targetType: "note_group", targetId: 1, createdAt: 0 }];
4146
case "override_group_route": return true;
4247
default:
4348
throw new Error(`unexpected: ${cmd}`);
@@ -58,6 +63,7 @@ function renderSidebar() {
5863
onOpenInbox={vi.fn()}
5964
inboxActive={false}
6065
refreshToken={0}
66+
onOpenSystem={vi.fn()}
6167
/>,
6268
);
6369
}
@@ -75,6 +81,7 @@ describe("GroupSidebar ⓘ 弹层", () => {
7581
onOpenInbox={vi.fn()}
7682
inboxActive={false}
7783
refreshToken={0}
84+
onOpenSystem={vi.fn()}
7885
/>,
7986
);
8087
await screen.findByTestId("group-row-1");
@@ -102,4 +109,29 @@ describe("GroupSidebar ⓘ 弹层", () => {
102109
// 明细折叠态与结算计划同样重置
103110
expect(screen.queryByTestId("route-details")).toBeNull();
104111
});
112+
113+
it("组行显示关联体系徽标并点击跳转(触点①)", async () => {
114+
const onOpenSystem = vi.fn();
115+
const onGroupFilterChange = vi.fn();
116+
render(
117+
<GroupSidebar
118+
groupFilter={null}
119+
onGroupFilterChange={onGroupFilterChange}
120+
onChanged={vi.fn()}
121+
onOpenReview={vi.fn()}
122+
selectedNoteId={null}
123+
onOpenInbox={vi.fn()}
124+
inboxActive={false}
125+
refreshToken={0}
126+
onOpenSystem={onOpenSystem}
127+
/>,
128+
);
129+
await screen.findByTestId("group-row-1");
130+
const badge = await screen.findByTestId("system-badge");
131+
expect(badge.textContent).toContain("摄影");
132+
// 点击徽标跳体系页(stopPropagation——不触发组过滤)
133+
fireEvent.click(badge);
134+
expect(onOpenSystem).toHaveBeenCalledWith(10);
135+
expect(onGroupFilterChange).not.toHaveBeenCalled();
136+
});
105137
});

‎app/src/components/GroupSidebar.tsx‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
import { useCallback, useEffect, useState } from "react";
1212
import { invoke } from "@tauri-apps/api/core";
1313
import type { Fragment, NoteGroup } from "../types";
14+
import type { KnowledgeLink, KnowledgeSystem } from "../types/knowledge";
1415
import { parseRouteReason, routeLineState } from "../utils/routeReason";
1516
import RouteInfoPopover from "./RouteInfoPopover";
17+
import SystemBadge from "./SystemBadge";
1618

1719
interface Props {
1820
/** 当前过滤组(null=全部笔记) */
@@ -30,6 +32,8 @@ interface Props {
3032
inboxActive: boolean;
3133
/** 外部刷新令牌(捕获/升笔记/升卡后触发本栏重载计数) */
3234
refreshToken: number;
35+
/** 跳转体系页并选中体系(v0.13.7 触点①) */
36+
onOpenSystem: (systemId: number) => void;
3337
}
3438

3539
/** 组类别徽标(文案+配色) */
@@ -52,7 +56,7 @@ async function blobToBase64(blob: Blob): Promise<string> {
5256

5357
export default function GroupSidebar({
5458
groupFilter, onGroupFilterChange, onChanged, onOpenReview, selectedNoteId,
55-
onOpenInbox, inboxActive, refreshToken,
59+
onOpenInbox, inboxActive, refreshToken, onOpenSystem,
5660
}: Props) {
5761
const [groups, setGroups] = useState<NoteGroup[]>([]);
5862
const [feedCaptureOn, setFeedCaptureOn] = useState(true);
@@ -62,6 +66,9 @@ export default function GroupSidebar({
6266
// 收件箱待处理数(active 碎片计数)
6367
const [inboxCount, setInboxCount] = useState(0);
6468
const [status, setStatus] = useState("");
69+
// 体系引用映射(组 id → 引用它的体系列表;v0.13.7 触点① 徽标)
70+
const [systemLinks, setSystemLinks] = useState<Record<number, { systemId: number; count: number }[]>>({});
71+
const [systems, setSystems] = useState<KnowledgeSystem[]>([]);
6572
// ⓘ 弹层态(受控单开——同一时间只开一个组)
6673
const [popover, setPopover] = useState<{ group: NoteGroup; anchor: { x: number; y: number } } | null>(null);
6774

@@ -74,6 +81,24 @@ export default function GroupSidebar({
7481
setDueTotal(due);
7582
const frags = await invoke<Fragment[]>("list_fragments", { status: "active", limit: 500 });
7683
setInboxCount(frags.length);
84+
// v0.13.7 触点①:体系 + 引用拉取(并行——徽标数据与组列表无依赖)
85+
const [sysList, links] = await Promise.all([
86+
invoke<KnowledgeSystem[]>("list_knowledge_systems"),
87+
invoke<KnowledgeLink[]>("list_knowledge_links"),
88+
]);
89+
setSystems(sysList);
90+
const map: Record<number, { systemId: number; count: number }[]> = {};
91+
for (const l of links) {
92+
if (l.targetType !== "note_group") continue;
93+
(map[l.targetId] ??= []).push({ systemId: l.systemId, count: 0 });
94+
}
95+
// 合并同体系多引用为一条(count 累加——一行一个徽标,不重复堆叠)
96+
for (const gid of Object.keys(map)) {
97+
const merged: Record<number, number> = {};
98+
for (const item of map[Number(gid)]) merged[item.systemId] = (merged[item.systemId] ?? 0) + 1;
99+
map[Number(gid)] = Object.entries(merged).map(([sid, count]) => ({ systemId: Number(sid), count }));
100+
}
101+
setSystemLinks(map);
77102
setStatus("");
78103
} catch (e) {
79104
setStatus(`组加载失败: ${e}`);
@@ -222,6 +247,19 @@ export default function GroupSidebar({
222247
{g.terrain === "feed" && (
223248
<span style={{ fontSize: 10, color: "#7c3aed", background: "#faf5ff", borderRadius: 8, padding: "0 5px" }}>feed</span>
224249
)}
250+
{/* 体系徽标(触点①:该组被哪些体系引用;点击跳体系页) */}
251+
{systemLinks[g.id]?.map((sl) => {
252+
const sys = systems.find((s) => s.id === sl.systemId);
253+
if (!sys || sys.status === "archived") return null;
254+
return (
255+
<SystemBadge
256+
key={sl.systemId}
257+
name={sys.name}
258+
linkCount={sl.count}
259+
onClick={() => onOpenSystem(sl.systemId)}
260+
/>
261+
);
262+
})}
225263
<span style={{ fontSize: 10, color: line.needsConfirm ? "#b45309" : "#9ca3af", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 }}>
226264
{line.label}
227265
</span>

‎app/src/components/SystemBadge.test.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* 只显示名字,有计数时显示"名 · N 引用"。
77
*/
88
import { afterEach, describe, expect, it, vi } from "vitest";
9-
import { cleanup, render, screen } from "@testing-library/react";
9+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
1010
import SystemBadge from "./SystemBadge";
1111

1212
describe("SystemBadge", () => {
@@ -24,7 +24,7 @@ describe("SystemBadge", () => {
2424
it("点击触发 onClick", () => {
2525
const onClick = vi.fn();
2626
render(<SystemBadge name="摄影" linkCount={0} onClick={onClick} />);
27-
screen.getByTestId("system-badge").click();
27+
fireEvent.click(screen.getByTestId("system-badge"));
2828
expect(onClick).toHaveBeenCalledTimes(1);
2929
});
3030
});

‎app/src/pages/NotesPage.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ import { useNoteAttention } from "../components/useNoteAttention";
3030
interface Props {
3131
focusNoteId?: number | null;
3232
onOpenSessions?: (sessionId: number) => void;
33+
/** 打开体系页并选中体系(v0.13.7 触点① 组行徽标) */
34+
onOpenSystem?: (systemId: number) => void;
3335
}
3436

3537
/** 中部视图:notes=笔记列表(组过滤/搜索/标签);inbox=收件箱碎片列表 */
3638
type MiddleView = "notes" | "inbox";
3739

38-
export default function NotesPage({ focusNoteId, onOpenSessions }: Props) {
40+
export default function NotesPage({ focusNoteId, onOpenSessions, onOpenSystem }: Props) {
3941
const [notes, setNotes] = useState<Note[]>([]);
4042
const [keyword, setKeyword] = useState("");
4143
const [tagFilter, setTagFilter] = useState<string | null>(null);
@@ -294,6 +296,7 @@ export default function NotesPage({ focusNoteId, onOpenSessions }: Props) {
294296
onOpenInbox={() => { setGroupFilter(null); setView("inbox"); }}
295297
inboxActive={view === "inbox"}
296298
refreshToken={refreshToken}
299+
onOpenSystem={(id) => onOpenSystem?.(id)}
297300
/>
298301

299302
{/* ── 中部:收件箱视图 ↔ 笔记列表原位切换(布局不变)── */}

0 commit comments

Comments
 (0)