Skip to content

Commit f702d87

Browse files
committed
feat(review): 复习功能独立为顶层页(会话全页化+组过滤+到期数+深链拆件)
- ReviewSessionOverlay 全页化迁移 components/review/ReviewSessionPanel(git mv 保历史,去 fixed 遮罩渲染页面主体区;front→reveal→四档评分流与后端命令不变,一轮上限 200=后端 DUE_LIST_LIMIT_MAX) - 新 pages/ReviewPage:总览=全量/各组到期数 chips + 开始复习 + 范围引导与空态(含卡源指引);会话=按范围挂载面板;active 门控切回重载(TD-004,闪卡域无事件总线);空态判定/到期归约抽 utils/reviewStats 纯函数 - App.tsx 第 9 Tab「🔄 复习」(顶层 Tab=9,收纳观察项登记不设计)+ focusReviewGroupId 跨页深链(消费即清空,同组可重复触发) - 笔记域拆件:NotesPage 删 review state/Overlay 宿主(575→540);GroupSidebar 删「🎴 复习 N」按钮与 dueTotal 拉取(455→418,无被动提醒裁决);ⓘ「复习本组」=App 转页+组预选深链 - 测试:ReviewPage 8 例/ReviewSessionPanel 4 例/reviewStats 4 例;GroupSidebar 入口唯一化护栏;RouteInfoPopover 深链断言;全量 vitest 612→630、tsc 0 错;Rust 零改动(子集 19/19 回归)
1 parent 05971fe commit f702d87

14 files changed

Lines changed: 864 additions & 232 deletions

‎app/src/App.tsx‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import NotesPage from "./pages/NotesPage";
2222
import SessionsPage from "./pages/SessionsPage";
2323
// v0.20.5:行动域页(做——行动中心独立成页,意图分层)
2424
import ActionPage from "./pages/ActionPage";
25+
// v0.20.10(批 5,用户问题 6):复习域页(练——复习面独立顶层 Tab;spec §9 二期兑现)
26+
import ReviewPage from "./pages/ReviewPage";
2527
// v0.16.0:AI 对话页(纯聊天 + AI 任务对话视图——DSH 交互范式)
2628
import ChatPage from "./pages/ChatPage";
2729
// 2026-08-21 用户需求:设置页(课堂助手设置类面板迁出,单页滚动+分组)
@@ -42,14 +44,17 @@ import { CaptureStatusProvider, useCaptureControl } from "./hooks/useLiveCapture
4244
import { pauseReasonLabel } from "./hooks/liveCaptureState";
4345
import type { AiTaskState } from "./types";
4446

45-
type Page = "classroom" | "sessions" | "notes" | "action" | "chat" | "knowledge" | "goals" | "settings";
47+
type Page = "classroom" | "sessions" | "notes" | "action" | "review" | "chat" | "knowledge" | "goals" | "settings";
4648

4749
const NAV_ITEMS: { key: Page; label: string }[] = [
4850
{ key: "classroom", label: "📡 课堂助手" },
4951
{ key: "sessions", label: "🗂 会话" },
5052
{ key: "notes", label: "📝 笔记" },
5153
// v0.20.5:行动域页(做——行动裁决/SOP/练习/问题;独立 Tab 唯一入口)
5254
{ key: "action", label: "✅ 行动" },
55+
// v0.20.10(批 5):复习域页(练——到期感知唯一入口,无被动提醒;
56+
// 顶层 Tab=9 触发 spec §9 导航收纳观察项,收纳设计另行立项)
57+
{ key: "review", label: "🔄 复习" },
5358
// v0.16.0:AI 对话页(纯聊天 + AI 任务对话视图)
5459
{ key: "chat", label: "💬 AI 对话" },
5560
{ key: "knowledge", label: "🧠 体系" },
@@ -112,6 +117,9 @@ function MainShell() {
112117
const [createSystemSignal, setCreateSystemSignal] = useState(0);
113118
// v0.14 C2:图谱组节点 → 笔记页过滤该组(同 focusNoteId 模式)
114119
const [focusGroupId, setFocusGroupId] = useState<number | null>(null);
120+
// v0.20.10(批 5):跨页直达复习页并预选组(笔记域 ⓘ「复习本组」深链;
121+
// 仿 focusGroupId 模式——ReviewPage 消费后经 onFocusGroupConsumed 清空)
122+
const [focusReviewGroupId, setFocusReviewGroupId] = useState<number | null>(null);
115123
// v0.16.1:工作台深链(对话页任务视图 → 会话页自动展开精修工作台)与
116124
// 任务进入对话页(会话页精修启动 → AI 对话页选中该任务)
117125
const [focusRefineTaskId, setFocusRefineTaskId] = useState<number | null>(null);
@@ -331,6 +339,11 @@ function MainShell() {
331339
focusNoteId={focusNoteId}
332340
focusNoteSearch={focusNoteSearch}
333341
focusGroupId={focusGroupId}
342+
// v0.20.10(批 5):ⓘ「复习本组」深链 → 复习页组预选
343+
onOpenReview={(groupId) => {
344+
setFocusReviewGroupId(groupId);
345+
setPage("review");
346+
}}
334347
onOpenSystem={(id) => {
335348
setFocusSystemId(id);
336349
setPage("knowledge");
@@ -349,6 +362,16 @@ function MainShell() {
349362
<div style={{ flex: 1, display: page === "action" ? "block" : "none", overflow: "hidden" }}>
350363
<ActionPage active={page === "action"} />
351364
</div>
365+
{/* v0.20.10(批 5):复习域页(保活挂载 + active 门控切回重载——
366+
闪卡域无事件总线,TD-004 模式同 ActionPage;focusReviewGroupId 深链
367+
组预选,消费后清空) */}
368+
<div style={{ flex: 1, display: page === "review" ? "block" : "none", overflow: "hidden" }}>
369+
<ReviewPage
370+
active={page === "review"}
371+
focusGroupId={focusReviewGroupId}
372+
onFocusGroupConsumed={() => setFocusReviewGroupId(null)}
373+
/>
374+
</div>
352375
<div style={{ flex: 1, display: page === "chat" ? "block" : "none", overflow: "hidden" }}>
353376
{/* v0.16.0:AI 对话页——跨页跳转复用 focus 机制(任务对话引用 → 会话/笔记/设置)。
354377
2026-09-09 批 1:active 门控透传——保活挂载下切回重同步(别页发起/完成的

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

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
* @ai-context: 覆盖串组场景——切换 ⓘ 弹层目标组时表单态必须重置(key=group.id
66
* 修复):组 A 改了判类(未确认)→ 点组 B ⓘ → 弹层显示 B 且
77
* 判类下拉回到 B.kind(防把 A 的选择误用到 B——路径: 改判误操作)。
8-
* invoke 全 mock(list_note_groups/count_due_cards/list_fragments/
8+
* invoke 全 mock(list_note_groups/list_fragments/
99
* get_feature_flags/week_contract_status/list_knowledge_systems/
10-
* list_knowledge_links)。
10+
* list_knowledge_links)。v0.20.10 批 5:复习入口剥离顶层页——
11+
* 侧栏不再拉 count_due_cards、不渲染「🎴 复习」入口(无被动提醒)。
1112
*/
1213
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
1314
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
@@ -34,7 +35,6 @@ beforeEach(() => {
3435
invokeMock.mockImplementation(async (cmd: string, args?: Record<string, unknown>) => {
3536
switch (cmd) {
3637
case "list_note_groups": return [groupA, groupB];
37-
case "count_due_cards": return 0;
3838
case "list_fragments": return [];
3939
case "get_feature_flags": return { feedCapture: true };
4040
case "week_contract_status":
@@ -209,7 +209,6 @@ describe("GroupSidebar v0.14 C1 Obsidian 式", () => {
209209
invokeMock.mockImplementation(async (cmd: string) => {
210210
if (cmd === "move_note_to_group") throw new Error("group not found");
211211
if (cmd === "list_note_groups") return [groupA, groupB];
212-
if (cmd === "count_due_cards") return 0;
213212
if (cmd === "list_fragments") return [];
214213
if (cmd === "get_feature_flags") return { feedCapture: true };
215214
if (cmd === "week_contract_status") return { contract: null, weekStart: 0, actualDays: 0, actualCards: 0, minimalDayMet: false };
@@ -224,8 +223,34 @@ describe("GroupSidebar v0.14 C1 Obsidian 式", () => {
224223
});
225224
});
226225

227-
describe("GroupSidebar v0.14.1 新建/重命名", () => {
228-
beforeEach(() => localStorage.clear());
226+
describe("GroupSidebar v0.20.10 复习入口剥离", () => {
227+
it("侧栏无「🎴 复习」全量入口、不拉 count_due_cards;ⓘ「复习本组」为深链回调", async () => {
228+
const onOpenReview = vi.fn();
229+
render(
230+
<GroupSidebar
231+
groupFilter={null}
232+
onGroupFilterChange={vi.fn()}
233+
onChanged={vi.fn()}
234+
onOpenReview={onOpenReview}
235+
selectedNoteId={null}
236+
onOpenInbox={vi.fn()}
237+
inboxActive={false}
238+
refreshToken={0}
239+
onOpenSystem={vi.fn()}
240+
/>,
241+
);
242+
await screen.findByTestId("group-row-1");
243+
// 无被动提醒:到期感知收敛顶层「🔄 复习」Tab——侧栏按钮与徽标数据源移除
244+
expect(screen.queryByText(/🎴 复习/)).toBeNull();
245+
expect(invokeMock).not.toHaveBeenCalledWith("count_due_cards", expect.anything());
246+
// ⓘ 弹层「复习本组」仍在——语义改为跨页深链回调(App 层转页预选)
247+
fireEvent.click(screen.getByTestId("group-info-1"));
248+
fireEvent.click(await screen.findByText("🎴 复习本组"));
249+
expect(onOpenReview).toHaveBeenCalledWith(1, "化妆课 A");
250+
});
251+
});
252+
253+
describe("GroupSidebar v0.14.1 新建/重命名", () => { beforeEach(() => localStorage.clear());
229254

230255
it("「+ 新建组」→ 弹窗校验拦截(空名/无领域),合法提交走 create→color→onChanged", async () => {
231256
// Arrange

‎app/src/components/GroupSidebar.tsx‎

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
* GroupSidebar — 笔记页左侧组筛选侧栏(v0.14 C1 Obsidian 式重构)。
33
*
44
* @ai-context: 原 240px 平铺混排(痛点 C1:展示混乱)——重构为分区组织:
5-
* 最近使用区(LRU≤5,访问组时写入 localStorage)+ 收件箱/全部/
6-
* 复习(移出"组"概念区)+ 组分区(按 kind:课程/主题/独立/feed,
5+
* 最近使用区(LRU≤5,访问组时写入 localStorage)+ 收件箱/全部
6+
* (移出"组"概念区)+ 组分区(按 kind:课程/主题/独立/feed,
77
* 折叠记忆 localStorage)+ 组过滤(纯函数 filterGroups)+
88
* 拖拽归组(组行为 drop target,笔记卡片为 drag source,
99
* move_note_to_group 命令)。组行渲染提取至 GroupSidebarRow
1010
* (行内徽标收敛为 1-2 个)。三条设计契约延续:展示层组织,
1111
* 不改组模型(无父子组)。
12+
* @ai-context: v0.20.10(批 5)复习入口移除——组侧栏不再放「🎴 复习」按钮/
13+
* 到期徽标(无被动提醒裁决,同 v0.20.5 行动入口先例);到期感知
14+
* 收敛于顶层「🔄 复习」Tab;本栏 ⓘ 弹层「复习本组」=跨页深链。
1215
*/
1316
import { useCallback, useEffect, useMemo, useState } from "react";
1417
import { invoke } from "@tauri-apps/api/core";
@@ -38,7 +41,8 @@ interface Props {
3841
onGroupFilterChange: (id: number | null) => void;
3942
/** 组/笔记变更后的刷新回调(NotesPage 重载列表) */
4043
onChanged: () => void;
41-
/** 打开复习面(groupId=null 全量;name 呈现用) */
44+
/** 复习域页深链(v0.20.10 批 5:ⓘ「复习本组」→ App 转顶层复习页并预选组;
45+
* groupId=null 全量语义保留(组侧栏全量按钮已随无被动提醒裁决移除) */
4246
onOpenReview: (groupId: number | null, name: string) => void;
4347
/** 当前选中笔记 id(ⓘ 弹层"移入/移出选中笔记"用;null=无) */
4448
selectedNoteId: number | null;
@@ -61,8 +65,6 @@ export default function GroupSidebar({
6165
const [groups, setGroups] = useState<NoteGroup[]>([]);
6266
const [feedCaptureOn, setFeedCaptureOn] = useState(true);
6367
const [fragText, setFragText] = useState("");
64-
// 全量到期卡数("复习 N"徽标)
65-
const [dueTotal, setDueTotal] = useState(0);
6668
// 收件箱待处理数(active 碎片计数)
6769
const [inboxCount, setInboxCount] = useState(0);
6870
const [status, setStatus] = useState("");
@@ -98,8 +100,6 @@ export default function GroupSidebar({
98100
const list = await invoke<NoteGroup[]>("list_note_groups", { terrain: null });
99101
// feed 组仅开关开启时显示(开关关闭时碎片操作面不可见——后端另有校验)
100102
setGroups(feedCaptureOn ? list : list.filter((g) => g.terrain !== "feed"));
101-
const due = await invoke<number>("count_due_cards", { groupId: null });
102-
setDueTotal(due);
103103
const frags = await invoke<Fragment[]>("list_fragments", { status: "active", limit: 500 });
104104
setInboxCount(frags.length);
105105
// v0.13.7 触点①:体系 + 引用拉取(并行——徽标数据与组列表无依赖)
@@ -410,18 +410,6 @@ export default function GroupSidebar({
410410
暂无笔记组——会话转笔记时自动归组
411411
</p>
412412
)}
413-
414-
{/* 🎴 复习(全量入口:UI 最小化——一个按钮 + 到期数;v0.20.5:✅ 行动
415-
入口与徽标已移除——行动独立为顶层「✅ 行动」Tab 唯一入口) */}
416-
<div style={{ marginTop: "auto", paddingTop: 6 }}>
417-
<button
418-
onClick={() => onOpenReview(null, "全部组")}
419-
style={{ width: "100%", fontSize: 11, cursor: "pointer", padding: "4px 8px", borderRadius: 4, border: "1px solid #0f766e", background: dueTotal > 0 ? "#f0fdfa" : "#fff", color: "#0f766e" }}
420-
title="开始复习到期卡片"
421-
>
422-
🎴 复习{dueTotal > 0 ? ` ${dueTotal}` : ""}
423-
</button>
424-
</div>
425413
</div>
426414

427415
{status && <p style={{ padding: 8, fontSize: 12, color: "#dc2626" }}>{status}</p>}
@@ -437,6 +425,7 @@ export default function GroupSidebar({
437425
onClose={() => setPopover(null)}
438426
// via onChanged(refreshToken)驱动本组件 load——不再显式双跑
439427
onChanged={onChanged}
428+
// 复习深链:关弹层后交 NotesPage→App 转复习页组预选(v0.20.10 批 5)
440429
onOpenReview={(gid, name) => { setPopover(null); onOpenReview(gid, name); }}
441430
selectedNoteId={selectedNoteId}
442431
/>

‎app/src/components/ReviewSessionOverlay.tsx‎

Lines changed: 0 additions & 189 deletions
This file was deleted.

0 commit comments

Comments
 (0)