Skip to content

Commit b0ee557

Browse files
committed
fix(review): ESC 门控页面可见与深链两缺口对齐(批5)
1 parent 2ab400c commit b0ee557

4 files changed

Lines changed: 51 additions & 17 deletions

File tree

‎app/src/components/review/ReviewSessionPanel.test.tsx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,16 @@ describe("ReviewSessionPanel 复习流", () => {
9595
fireEvent.keyDown(window, { key: "Escape" });
9696
expect(onExit).toHaveBeenCalledTimes(2);
9797
});
98+
99+
it("active=false(页面隐藏保活期)→ ESC 不退出会话;active=true 后 ESC 恢复生效(审查 P2-11)", async () => {
100+
const { onExit, rerender } = renderPanel({ active: false });
101+
await screen.findByText("隔离霜作用");
102+
// 隐藏期:别页 ESC 属他页语义,不得静默结束本会话
103+
fireEvent.keyDown(window, { key: "Escape" });
104+
expect(onExit).not.toHaveBeenCalled();
105+
// 切回可见 → 监听随 active 重新注册
106+
rerender(<ReviewSessionPanel groupId={null} groupName="全部组" active={true} onExit={onExit} />);
107+
fireEvent.keyDown(window, { key: "Escape" });
108+
expect(onExit).toHaveBeenCalledTimes(1);
109+
});
98110
});

‎app/src/components/review/ReviewSessionPanel.tsx‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,14 @@ interface Props {
3838
groupId: number | null;
3939
/** 会话范围名(头部展示:组名 / 全部组) */
4040
groupName: string;
41+
/** 页面是否可见(ReviewPage active 透传——display:none 保活期不注册 ESC;
42+
* 缺省 true=既有测试/独立宿主兼容) */
43+
active?: boolean;
4144
/** 退出本轮(返回总览;头部按钮与 ESC 同义) */
4245
onExit: () => void;
4346
}
4447

45-
export default function ReviewSessionPanel({ groupId, groupName, onExit }: Props) {
48+
export default function ReviewSessionPanel({ groupId, groupName, active = true, onExit }: Props) {
4649
const [queue, setQueue] = useState<Flashcard[]>([]);
4750
const [index, setIndex] = useState(0);
4851
const [revealed, setRevealed] = useState(false);
@@ -64,14 +67,21 @@ export default function ReviewSessionPanel({ groupId, groupName, onExit }: Props
6467

6568
useEffect(() => { void loadQueue(); }, [loadQueue]);
6669

67-
// ESC 退出(复习中误触保护:原模态语义——直接退出本轮,未评分卡不算)
70+
// ESC 退出(复习中误触保护:原模态语义——直接退出本轮,未评分卡不算)。
71+
// 审查 P2-11:监听注册门控到 active——复习会话在页面隐藏期(display:none
72+
// 保活)仍可能进行中,全局 window 监听会把**别页的 ESC** 误判为退出信号
73+
// (静默结束隐藏中的复习会话);仅可见期注册、隐藏期不注册,卸载清理照旧
6874
useEffect(() => {
75+
if (!active) return;
6976
const handler = (e: KeyboardEvent) => {
70-
if (e.key === "Escape") onExit();
77+
// 长按自动重复(e.repeat)只退一次——首次已触发 onExit 卸载面板,
78+
// 重复 keydown 属物理按键噪声(防抖口径与行菜单 ESC 一致)
79+
if (e.key !== "Escape" || e.repeat) return;
80+
onExit();
7181
};
7282
window.addEventListener("keydown", handler);
7383
return () => window.removeEventListener("keydown", handler);
74-
}, [onExit]);
84+
}, [active, onExit]);
7585

7686
const current = queue[index] ?? null;
7787

‎app/src/pages/ReviewPage.test.tsx‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ describe("ReviewPage 总览与组过滤器", () => {
117117
// 组 2 到期 0——chips 不列(到期>0 才可作范围),深链仍可达并显示范围空态
118118
render(<ReviewPage active={false} focusGroupId={2} />);
119119
const empty = await screen.findByTestId("review-empty");
120-
expect(empty.textContent).toContain("该组当前没有到期卡片");
120+
// 标题内嵌 scopeLabel 组名(审查 P3-5b:零到期深链只预选不弹窗,无组名时
121+
// 用户看不出预选到了哪一组)
122+
expect(empty.textContent).toContain("「手账 B」组当前没有到期卡片");
121123
expect(empty.textContent).toContain("其余组共 1 张到期");
122124
});
123125
});
@@ -157,9 +159,12 @@ describe("ReviewPage 刷新与深链", () => {
157159
// 先在全量范围开会话
158160
fireEvent.click(await screen.findByTestId("review-start"));
159161
await screen.findByTestId("session-exit");
160-
// 深链到组 2:会话退出回总览 + 组 2 预选
162+
const before = dueCalls();
163+
// 深链到组 2:会话退出回总览 + 组 2 预选——退出与手动同一出口(token++
164+
// 重载到期统计,审查 P3-5a:裸清 session 会残留会话前的过期统计)
161165
rerender(<ReviewPage active={false} focusGroupId={2} />);
162166
await waitFor(() => expect(screen.queryByTestId("session-exit")).toBeNull());
167+
await waitFor(() => expect(dueCalls()).toBeGreaterThan(before));
163168
await screen.findByTestId("review-scope-all");
164169
const start = screen.getByTestId("review-start") as HTMLButtonElement;
165170
expect(start.textContent).toContain("1");

‎app/src/pages/ReviewPage.tsx‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,22 @@ export default function ReviewPage({ active, focusGroupId, onFocusGroupConsumed
8484

8585
useEffect(() => { void load(); }, [load, token]);
8686

87-
// 跨页深链消费:预选组 + 退出进行中的会话(新意图优先)+ 通知父层清空
87+
// 会话退出/完成:回总览并重载到期统计(评分已改变到期分布)。useCallback
88+
// 化供深链打断复用(审查 P3-5a:两处退出必须同语义——裸 setSession(null)
89+
// 会残留旧到期统计,深链新意图就位后展示的是会话前的过期数字)
90+
const exitSession = useCallback(() => {
91+
setSession(null);
92+
setToken((t) => t + 1);
93+
}, []);
94+
95+
// 跨页深链消费:预选组 + 退出进行中的会话(新意图优先)+ 通知父层清空。
96+
// 退出走 exitSession(token++ 重载——与手动退出/完成同出口,防旧统计残留)
8897
useEffect(() => {
8998
if (focusGroupId == null) return;
90-
setSession(null);
99+
exitSession();
91100
setSelGroupId(focusGroupId);
92101
onFocusGroupConsumed?.();
93-
}, [focusGroupId, onFocusGroupConsumed]);
102+
}, [focusGroupId, onFocusGroupConsumed, exitSession]);
94103

95104
// 到期>0 的组行(chips 数据源;纯归约见 utils/reviewStats)
96105
const dueRows = useMemo(() => dueGroupRows(groups, byGroupDue), [groups, byGroupDue]);
@@ -103,28 +112,26 @@ export default function ReviewPage({ active, focusGroupId, onFocusGroupConsumed
103112
setSession({ groupId: selGroupId, groupName: scopeName });
104113
};
105114

106-
// 会话退出/完成:回总览并重载到期统计(评分已改变到期分布)
107-
const exitSession = () => {
108-
setSession(null);
109-
setToken((t) => t + 1);
110-
};
111-
112115
if (session) {
113116
return (
114117
<div style={{ height: "calc(100vh - 56px)", display: "flex", flexDirection: "column", minHeight: 0, background: "#fff" }}>
115118
<ReviewSessionPanel
116119
groupId={session.groupId}
117120
groupName={session.groupName}
121+
// 审查 P2-11:ESC 监听门控到页面可见(隐藏期保活会话不被别页 ESC 误退)
122+
active={active}
118123
onExit={exitSession}
119124
/>
120125
</div>
121126
);
122127
}
123128

124-
// 空态文案分支:全局无到期 / 本组无到期但别组有(引导换范围,避免困惑)
129+
// 空态文案分支:全局无到期 / 本组无到期但别组有(引导换范围,避免困惑)。
130+
// 审查 P3-5b:范围空态标题内嵌 scopeLabel 组名——零到期深链(focusGroupId
131+
// 指向无到期组)只预选不弹窗,标题不带组名时用户看不出"预选到了哪一组"
125132
const emptyTitle = totalDue === 0
126133
? "当前没有到期卡片 🎉"
127-
: "该组当前没有到期卡片";
134+
: `「${scopeName}」组当前没有到期卡片`;
128135
const emptyBody = totalDue === 0
129136
? "闪卡由笔记组产出(组 ⓘ 管理「⚙ 生成闪卡」/「+ 概念卡」/碎片升卡),到期时间由 FSRS 间隔调度——到期后再来复习(弹性承诺,不追债)。若从未生成过闪卡:到「📝 笔记」页任一组点 ⓘ →「⚙ 生成闪卡」起步。"
130137
: `其余组共 ${totalDue} 张到期——切换上方范围即可复习。`;

0 commit comments

Comments
 (0)