|
7 | 7 | * 结算/复习本组/移入移出选中笔记);④ 周契约卡(REQ-200)。 |
8 | 8 | * @ai-context: 弹层由 GroupSidebar 锚定渲染(fixed 定位 + 透明背板关闭 + |
9 | 9 | * ESC 关闭);所有变更经 onChanged 通知 NotesPage 刷新列表。 |
| 10 | + * @ai-context: v0.13.7 触点③ 结算体系简报——弹层挂载时并行拉取组内 model 卡 |
| 11 | + * 计数与体系引用/概念失效态(90 天未应用);纯提示、加载失败静默 |
| 12 | + * 降级(不阻塞结算区);list_knowledge_links 契约强制 system_id, |
| 13 | + * 按非归档体系逐个查询聚合。 |
10 | 14 | */ |
11 | | -import { useEffect, useMemo, useState } from "react"; |
| 15 | +import { useCallback, useEffect, useMemo, useState } from "react"; |
12 | 16 | import { invoke } from "@tauri-apps/api/core"; |
13 | | -import type { NoteGroup } from "../types"; |
| 17 | +import type { Flashcard, NoteGroup } from "../types"; |
| 18 | +import type { KnowledgeConcept, KnowledgeLink, KnowledgeSystem } from "../types/knowledge"; |
14 | 19 | import { humanRouteLine, parseRouteReason } from "../utils/routeReason"; |
15 | 20 | import WeekContractCard from "./WeekContractCard"; |
16 | 21 | import ModelCardCreateDialog from "./ModelCardCreateDialog"; |
@@ -67,6 +72,8 @@ export default function RouteInfoPopover({ |
67 | 72 | const [busy, setBusy] = useState(false); |
68 | 73 | // v0.13.2:组侧「+ 概念卡」弹窗开合(组仍是唯一容器——只在此组内建卡) |
69 | 74 | const [cardDialogOpen, setCardDialogOpen] = useState(false); |
| 75 | + // v0.13.7 触点③:结算体系简报(model 卡计数 + 关联体系概念失效数) |
| 76 | + const [sysBrief, setSysBrief] = useState<{ modelCount: number; staleConcepts: number; systemName: string } | null>(null); |
70 | 77 |
|
71 | 78 | const reason = useMemo(() => parseRouteReason(group.routeReason), [group.routeReason]); |
72 | 79 |
|
@@ -109,6 +116,39 @@ export default function RouteInfoPopover({ |
109 | 116 | } |
110 | 117 | }; |
111 | 118 |
|
| 119 | + // ③ v0.13.7 触点③ 结算体系简报(弹层挂载时并行拉取;纯提示——失败静默降级) |
| 120 | + const loadSystemBrief = useCallback(async () => { |
| 121 | + try { |
| 122 | + const [cards, sysList] = await Promise.all([ |
| 123 | + invoke<Flashcard[]>("list_group_cards", { groupId: group.id }), |
| 124 | + invoke<KnowledgeSystem[]>("list_knowledge_systems"), |
| 125 | + ]); |
| 126 | + const modelCount = cards.filter((c) => c.kind === "model").length; |
| 127 | + // 后端 list_knowledge_links 强制 system_id——按非归档体系聚合 |
| 128 | + const activeSystems = sysList.filter((s) => s.status !== "archived"); |
| 129 | + const linkArrays = await Promise.all( |
| 130 | + activeSystems.map((s) => invoke<KnowledgeLink[]>("list_knowledge_links", { systemId: s.id })), |
| 131 | + ); |
| 132 | + const links = linkArrays.flat(); |
| 133 | + const groupLinks = links.filter((l) => l.targetType === "note_group" && l.targetId === group.id); |
| 134 | + if (modelCount === 0 && groupLinks.length === 0) { setSysBrief(null); return; } |
| 135 | + const sys = sysList.find((s) => s.id === groupLinks[0]?.systemId) ?? null; |
| 136 | + let staleConcepts = 0; |
| 137 | + if (sys) { |
| 138 | + const concepts = await invoke<KnowledgeConcept[]>("list_knowledge_concepts", { systemId: sys.id }); |
| 139 | + const now = Date.now(); |
| 140 | + staleConcepts = concepts.filter((c) => { |
| 141 | + const base = c.lastAppliedAt ?? c.createdAt; |
| 142 | + return now - base > 90 * 86400_000; |
| 143 | + }).length; |
| 144 | + } |
| 145 | + setSysBrief({ modelCount, staleConcepts, systemName: sys?.name ?? "" }); |
| 146 | + } catch { setSysBrief(null); } // 简报为纯提示——加载失败静默降级 |
| 147 | + }, [group.id]); |
| 148 | + |
| 149 | + // 弹层挂载时触发(一次性;结算区展开时并行拉取) |
| 150 | + useEffect(() => { void loadSystemBrief(); }, [loadSystemBrief]); |
| 151 | + |
112 | 152 | // ③ 结算计划(呈现→确认→执行) |
113 | 153 | const runSettlementPlan = async () => { |
114 | 154 | setBusy(true); |
@@ -238,7 +278,7 @@ export default function RouteInfoPopover({ |
238 | 278 | <div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}> |
239 | 279 | <button onClick={() => void runGenerateCards()} disabled={busy} style={BTN} title="从组内笔记词汇表/碎片生成闪卡(幂等)">⚙ 生成闪卡</button> |
240 | 280 | <button onClick={() => setCardDialogOpen(true)} data-testid="model-card-open" style={BTN} title="在组内建一张概念卡(模型卡)——组内记忆面">+ 概念卡</button> |
241 | | - <button onClick={() => void runSettlementPlan()} disabled={busy} style={BTN} title="对账本组:提炼核心/合并重复/归档低价值">🧹 结算</button> |
| 281 | + <button onClick={() => void runSettlementPlan()} disabled={busy} data-testid="settle-button" style={BTN} title="对账本组:提炼核心/合并重复/归档低价值">🧹 结算</button> |
242 | 282 | <button onClick={() => onOpenReview(group.id, group.name)} style={BTN}>🎴 复习本组</button> |
243 | 283 | {selectedNoteId != null && ( |
244 | 284 | <> |
@@ -270,6 +310,18 @@ export default function RouteInfoPopover({ |
270 | 310 | </div> |
271 | 311 | </div> |
272 | 312 | )} |
| 313 | + {/* v0.13.7 触点③ 结算体系简报(model 卡计数 + 体系概念失效) */} |
| 314 | + {sysBrief && (sysBrief.modelCount > 0 || sysBrief.staleConcepts > 0) && ( |
| 315 | + <div data-testid="sys-brief" style={{ marginTop: 8, padding: 8, background: "#f0fdfa", borderRadius: 6, border: "1px solid #99f6e4" }}> |
| 316 | + <div style={{ fontWeight: 600, color: "#0f766e", marginBottom: 4 }}>体系简报</div> |
| 317 | + {sysBrief.modelCount > 0 && ( |
| 318 | + <div data-testid="sys-brief-model" style={{ fontSize: 11, color: "#0f766e" }}>🧬 本组有 {sysBrief.modelCount} 张 model 卡可纳入体系(复习面「纳入体系」)</div> |
| 319 | + )} |
| 320 | + {sysBrief.staleConcepts > 0 && sysBrief.systemName && ( |
| 321 | + <div data-testid="sys-brief-stale" style={{ fontSize: 11, color: "#b45309", marginTop: 2 }}>⏳ 体系「{sysBrief.systemName}」有 {sysBrief.staleConcepts} 个概念 90 天未引用</div> |
| 322 | + )} |
| 323 | + </div> |
| 324 | + )} |
273 | 325 | </div> |
274 | 326 |
|
275 | 327 | {/* ④ 周契约卡(REQ-200 弹性承诺呈现层) */} |
|
0 commit comments