Skip to content

Commit fa45caf

Browse files
committed
refactor(app): 抽出 PageSlot 与 AiToast 两个装配件
1 parent c29a1b8 commit fa45caf

5 files changed

Lines changed: 80 additions & 70 deletions

File tree

‎app/src/App.tsx‎

Lines changed: 4 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,10 @@ import { CommandPalette } from "./shell/CommandPalette";
4242
// 批 3 T13:壳层的**首访加载态**与**叶级错误边界**(批 2 §瓶颈清单转交的三条)——自足实现、
4343
// 刻意不 import 原语层(非目标 2,两个组件是批 4 换 `Loading` / `StatusLine` 的迁移点)。
4444
import { ShellFallback, SlotErrorBoundary } from "./shell/ShellFallback";
45-
// 批 4 T10:AI toast 从自足内联实现交给 L1 的 `Toast` 原语(文件末尾 `AiToast` 处)。
46-
// @ai-context: 批 3 T7 曾把它从 56px 导航行搬到固定覆盖层(实测它是 1024 溢出的唯一主因:
47-
// 单项 373.75 px = 视口的 36.5%;含它 1375.74 px、剔除它 997.99 px),本任务承其结论不动落点,
48-
// 只把**定位 / 层级 / 三档墨度 / 退场**四项交给原语(fixed + `--ed-nav-h` 锚点 + `zIndex("toast")`
49-
// 全在原语层)。⚠️ 这是 B5 的 barrel 代价在首屏的第二个触发点(T9 先到则归 T9)。
50-
import { Toast } from "./ui/primitives";
45+
// 批 7 T1:`PageSlot`(页面容器)与 `AiToast`(AI toast 装配件)的定义体从本文件抽到 `shell/`(C9.2 腾行数)——
46+
// 两件**签名与语义逐字不变**(只搬不改,C9.19 第 1 条):文档段随代码搬走,本文件只剩装配与调用点。
47+
import { PageSlot } from "./shell/PageSlot";
48+
import { AiToast } from "./shell/aiToast";
5149
// REQ-274(v0.19.4):全局 AI 对话面板(丙案——按需唤起 + 内容保活)
5250
// 批 2 包体治理:从静态 import 改为按需 import,并在它之前加一道「首开挂载」闸门。
5351
// @ai-context: 原语义(见文件末尾 dock 渲染处的注释)是「常驻挂载——开合仅切 display,
@@ -143,66 +141,6 @@ function App() {
143141
);
144142
}
145143

146-
/**
147-
* PageSlot — 页面容器:首访挂载 + 保活 + display 门控 + 独立 Suspense。
148-
*
149-
* @ai-context: 批 2 包体治理的挂载闸门,也是「保留挂载」语义的唯一实现点。
150-
* · mounted=false ⇒ 整棵子树不渲染 ⇒ 该页的 lazy chunk **不会被请求**(首屏收益的来源);
151-
* · mounted=true 之后永不回到 false ⇒ 已访问页面常驻(TD-004 保活语义,状态与事件监听不重置);
152-
* · 每页一个独立 Suspense + **叶级** `SlotErrorBoundary`:只有**新挂载**的页会挂起,
153-
* 已经可见的页不会因为邻居加载而被替换成 fallback(避免可见的闪烁)。
154-
* @ai-context: 批 3 T13:fallback 从 `null` 换成 `ShellFallback`(首访加载态,静态无动效),并在
155-
* ⚠️ Suspense 之外加了**叶级**边界:懒 chunk 失败原本会一路抛到最外层 AppErrorBoundary ⇒
156-
* **整个 MainShell 被卸载**(已访问页状态一起丢,批 2 评审 M-1)。边界在本函数内 ⇒ 只卸载出错的
157-
* 那一页,兄弟槽位与壳层状态保留(证明见 `shell/ShellFallback.test.tsx` 的「叶级」用例)。
158-
* 副作用:无。边界:children 是懒组件元素,未 mounted 时不会被 React 渲染 ⇒ 不触发 dynamic import。
159-
*/
160-
function PageSlot({ show, mounted, children }: { show: boolean; mounted: boolean; children: React.ReactNode }) {
161-
if (!mounted) return null;
162-
return (
163-
<div style={{ flex: 1, display: show ? "block" : "none", overflow: "hidden" }}>
164-
<SlotErrorBoundary>
165-
<Suspense fallback={<ShellFallback />}>{children}</Suspense>
166-
</SlotErrorBoundary>
167-
</div>
168-
);
169-
}
170-
171-
/**
172-
* AiToast — AI 任务完成/失败通知的**装配层**(批 4 T10:渲染交给 L1 `Toast`)。
173-
*
174-
* @ai-context: 状态仍由 `MainShell` 持有(监听 `ai:task-update`),本组件只做「状态 → 原语 props」
175-
* 的映射,并把迁移前逐字保留的三样东西钉在一处:文案(含 ✨/❌,逐字沿用)、时长 **3500ms**、
176-
* testId **`ai-toast`**(批 3 裁决 A3 的语义锚;B15 要求它以**渲染级**断言保住 —— 本仓
177-
* `MainShell` 未导出,故判据渲染这个**真实**装配件,见 `components/toastMigration.test.tsx`)。
178-
* 位置档 `placement="belowNav"`(读原语 `.ed-toast--below-nav`,消费壳层 token `--ed-nav-h`):
179-
* 调用点写行内 `top` 覆盖类语义被 ADR-033 §4 逐字禁止 ⇒ 走 B6 特殊条款加的那个具名 prop。
180-
* 副作用:无(不读 store、不发请求、不写磁盘)。自动消失由原语计时(进入 `entered` 才开始,
181-
* 边界①),到点走 140ms 退场后回调 `onDismiss` —— 父级自己置 `open=false` 不会收到回敬(边界②)。
182-
* 边界:**同文案连续事件不重置窗口**(原语边界③ 以 `message`/`kind` 判「接管」)——迁移前
183-
* `MainShell` 的裸 `setTimeout` 是「每个事件都重新计时」;差异只在「两次 AI 任务在同一 3.5s 内
184-
* 完成且文案逐字相同」时出现(该场景下可见时长可能比旧实现短,不会更长)。已登记在 T10 报告。
185-
*/
186-
export function AiToast({
187-
toast,
188-
onDismiss,
189-
}: {
190-
toast: { text: string; kind: "ok" | "err" } | null;
191-
onDismiss: () => void;
192-
}): React.ReactElement {
193-
return (
194-
<Toast
195-
open={toast !== null}
196-
message={toast?.text ?? ""}
197-
kind={toast?.kind ?? "ok"}
198-
durationMs={3500}
199-
placement="belowNav"
200-
testId="ai-toast"
201-
onDismiss={onDismiss}
202-
/>
203-
);
204-
}
205-
206144
/** 主导航壳(唯一实例由 App 包入 provider——同窗单实例纪律;状态与事件监听
207145
* 保留挂载语义不变,TD-004) */
208146
function MainShell() {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { dirname, join, relative, sep } from "node:path";
3131
import { fileURLToPath } from "node:url";
3232
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
3333
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
34-
import { AiToast } from "../App";
34+
import { AiToast } from "../shell/aiToast"; // 批 7 T1(Y1):装配件搬到 shell/aiToast.tsx(纯搬迁)
3535
import { Z_TIER } from "../ui/zIndex";
3636

3737
const HERE = dirname(fileURLToPath(import.meta.url));

‎app/src/shell/PageSlot.tsx‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @ai-context: 自 `App.tsx` 抽出(批 7 T1,C9.2 腾行数);签名与语义逐字不变。
3+
* 抽出原因:`App.tsx` 贴 600 硬限(599/600),而 R4/R9.1 两项还要往它加行。
4+
*/
5+
import { Suspense } from "react";
6+
import { ShellFallback, SlotErrorBoundary } from "./ShellFallback";
7+
8+
/**
9+
* PageSlot — 页面容器:首访挂载 + 保活 + display 门控 + 独立 Suspense。
10+
*
11+
* @ai-context: 批 2 包体治理的挂载闸门,也是「保留挂载」语义的唯一实现点。
12+
* · mounted=false ⇒ 整棵子树不渲染 ⇒ 该页的 lazy chunk **不会被请求**(首屏收益的来源);
13+
* · mounted=true 之后永不回到 false ⇒ 已访问页面常驻(TD-004 保活语义,状态与事件监听不重置);
14+
* · 每页一个独立 Suspense + **叶级** `SlotErrorBoundary`:只有**新挂载**的页会挂起,
15+
* 已经可见的页不会因为邻居加载而被替换成 fallback(避免可见的闪烁)。
16+
* @ai-context: 批 3 T13:fallback 从 `null` 换成 `ShellFallback`(首访加载态,静态无动效),并在
17+
* ⚠️ Suspense 之外加了**叶级**边界:懒 chunk 失败原本会一路抛到最外层 AppErrorBoundary ⇒
18+
* **整个 MainShell 被卸载**(已访问页状态一起丢,批 2 评审 M-1)。边界在本函数内 ⇒ 只卸载出错的
19+
* 那一页,兄弟槽位与壳层状态保留(证明见 `shell/ShellFallback.test.tsx` 的「叶级」用例)。
20+
* 副作用:无。边界:children 是懒组件元素,未 mounted 时不会被 React 渲染 ⇒ 不触发 dynamic import。
21+
*/
22+
export function PageSlot({ show, mounted, children }: { show: boolean; mounted: boolean; children: React.ReactNode }) {
23+
if (!mounted) return null;
24+
return (
25+
<div style={{ flex: 1, display: show ? "block" : "none", overflow: "hidden" }}>
26+
<SlotErrorBoundary>
27+
<Suspense fallback={<ShellFallback />}>{children}</Suspense>
28+
</SlotErrorBoundary>
29+
</div>
30+
);
31+
}

‎app/src/shell/aiToast.tsx‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* @ai-context: 自 `App.tsx` 抽出(批 7 T1,C9.2 腾行数);签名与语义逐字不变 ——
3+
* 含 `durationMs={3500}` / `placement="belowNav"` / `testId="ai-toast"` 三个字面量。
4+
* 调用点仍在 `App.tsx` 的 `MainShell` 最外层(壳级覆盖层,见该处注释)。
5+
*/
6+
import { Toast } from "../ui/primitives";
7+
8+
/**
9+
* AiToast — AI 任务完成/失败通知的**装配层**(批 4 T10:渲染交给 L1 `Toast`)。
10+
*
11+
* @ai-context: 状态仍由 `MainShell` 持有(监听 `ai:task-update`),本组件只做「状态 → 原语 props」
12+
* 的映射,并把迁移前逐字保留的三样东西钉在一处:文案(含 ✨/❌,逐字沿用)、时长 **3500ms**、
13+
* testId **`ai-toast`**(批 3 裁决 A3 的语义锚;B15 要求它以**渲染级**断言保住 —— 本仓
14+
* `MainShell` 未导出,故判据渲染这个**真实**装配件,见 `components/toastMigration.test.tsx`)。
15+
* 位置档 `placement="belowNav"`(读原语 `.ed-toast--below-nav`,消费壳层 token `--ed-nav-h`):
16+
* 调用点写行内 `top` 覆盖类语义被 ADR-033 §4 逐字禁止 ⇒ 走 B6 特殊条款加的那个具名 prop。
17+
* 副作用:无(不读 store、不发请求、不写磁盘)。自动消失由原语计时(进入 `entered` 才开始,
18+
* 边界①),到点走 140ms 退场后回调 `onDismiss` —— 父级自己置 `open=false` 不会收到回敬(边界②)。
19+
* 边界:**同文案连续事件不重置窗口**(原语边界③ 以 `message`/`kind` 判「接管」)——迁移前
20+
* `MainShell` 的裸 `setTimeout` 是「每个事件都重新计时」;差异只在「两次 AI 任务在同一 3.5s 内
21+
* 完成且文案逐字相同」时出现(该场景下可见时长可能比旧实现短,不会更长)。已登记在 T10 报告。
22+
*/
23+
export function AiToast({
24+
toast,
25+
onDismiss,
26+
}: {
27+
toast: { text: string; kind: "ok" | "err" } | null;
28+
onDismiss: () => void;
29+
}): React.ReactElement {
30+
return (
31+
<Toast
32+
open={toast !== null}
33+
message={toast?.text ?? ""}
34+
kind={toast?.kind ?? "ok"}
35+
durationMs={3500}
36+
placement="belowNav"
37+
testId="ai-toast"
38+
onDismiss={onDismiss}
39+
/>
40+
);
41+
}

‎docs/standards/line-limit-exemptions.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
| 文件 | 行数 | 豁免理由 | 拆分计划 |
2222
|---|---|---|---|
23-
| app/src/App.tsx | 599 | 根装配(页面切换/焦点跨页直达状态机/全局事件监听)+ v0.20.5 行动页 + v0.20.10 批 5 复习页 Tab/深链/保活挂载(f702d876)——登记值 363 过期,实测纠偏 | 若再增长:焦点跨页直达 state 族拆至 useFocusRouting.ts |
23+
| app/src/App.tsx | 537 | 根装配(页面切换/焦点跨页直达状态机/全局事件监听)+ v0.20.5 行动页 + v0.20.10 批 5 复习页 Tab/深链/保活挂载(f702d876)——登记值 363 过期,实测纠偏 | 若再增长:焦点跨页直达 state 族拆至 useFocusRouting.ts |
2424
| app/src/pages/ChatPage.tsx | 579 | AI 对话页编排;批 1(REQ-306/307)active 门控/终态订阅接线净增——2026-09-09 实测纠偏(登记值 529 过期) | 若再增长:任务工具条与发起流拆至 ChatTasksToolbar.tsx |
2525
| app/src-tauri/src/db_migrations.rs | 573 | v0.20.11 批 6(REQ-315)再增:note_groups.pin ensure_column + note_group_orders 建表(+21,实测 573——登记值 492 过期;schema 单点收敛理由同左) | 若再增长:kb_* 与 chat_* 表 DDL 拆至 db_migrations_kb.rs |
2626
| app/src-tauri/src/lib.rs | 572 | crate 根 **322 `mod` + 15 `#[cfg]` = 337 行地板**(Task 1 评审实测更正:原写「321 `mod` + 16 `#[cfg]`」,总数 337 不变;322 含本任务新增的 `mod app_commands;`);注册清单已移至 `app_commands.rs`(**数据文件**,同属 300–600 豁免带)—— 结构性下界,非欠账 | 已完成(批 0-C3 Task 1,2026-09-11);余下 161 行模块理由注释 + 装配逻辑,无进一步拆分标的 |
@@ -42,7 +42,7 @@
4242
| app/src-tauri/src/live_session_persist.rs | 488 | 定稿落库域(persist_final/digest_merged/handle_final_event)+ P2 flush_tail_and_persist(停止/暂停共用尾句落库)内聚 | 若再增长:flush_tail_and_persist 与 digest_merged 拆至 live_session_persist_tail.rs |
4343
| app/src-tauri/src/db_sessions.rs | 482 | 会话仓储;批 4 delete_sessions_batch + 审查修复轮 3(transaction() 改造)——2026-09-09 实测纠偏(登记值 394 过期) | 若再增长:recent_ocr_texts 等建议查询拆至 db_sessions_queries.rs |
4444
| app/src/components/RefineWorkbench.tsx | 482 | 并排双栏(规则版 + 精修版)+ 章节级 diff 高亮 + 同步滚动 +采纳/重新生成/放弃。批 3(问题11):行级三态染色(removed删除线红/added 绿)与 并排/差异 单列视图切换——数据源为整篇有序行级 diff(diff_markdown_ops,见 load 内 Why)。数据源:非只读带 taskResult(采纳前内存结果)→ refine_workbench回传 result(消除未落库右侧恒空);无 taskResult →refine_workbench(后端兜底未采纳任务/已落库笔记,重启可恢复);只读(VersionPanel)→ ruleMd/refinedMd 透传。(自动摘取,待细化) | 若再增长:按职责拆分 |
45-
| app/src-tauri/src/app_commands.rs | 479 | 宏约束下的**唯一**注册点:**313 条** `generate_handler!` 条目是**数据不是逻辑**(批 1 删除批收口实测:334 → 312,−22;批 6 T22 新增 1 条 `session_audio_path` ⇒ 313;此数由本行人工维护,`--write` 只刷新数字列) —— `Invoke` 按值语义使分域组合不可行(见文件头),拆成多份只会在域间新增「命令名 → 域」路由漂移面;一致性由 `scripts/check-command-registry.mjs` 机器门禁守 | 不拆(数据文件);新增命令时同步条目,由门禁强制 |
45+
| app/src-tauri/src/app_commands.rs | 476 | 宏约束下的**唯一**注册点:**310 条** `generate_handler!` 条目是**数据不是逻辑**(批 1 删除批收口实测:334 → 312,−22;批 6 T22 新增 1 条 `session_audio_path` ⇒ 313;批 7 T4 撤下 3 条 ⇒ 313 → 310;T19 新增 1 条后为 311;此数由本行人工维护,`--write` 只刷新数字列) —— `Invoke` 按值语义使分域组合不可行(见文件头),拆成多份只会在域间新增「命令名 → 域」路由漂移面;一致性由 `scripts/check-command-registry.mjs` 机器门禁守 | 不拆(数据文件);新增命令时同步条目,由门禁强制 |
4646
| app/src-tauri/src/commands_ai_enrich.rs | 479 | v0.8.0 M3(REQ-142)+ F1/F2/F3:知识补充命令域(九子项校验/预估/异步任务/采纳/撤销 + 配额去重门控 + 成本硬拦截 + 任务落库)——与精修共用任务注册表上下文,命令域内聚;2026-09 修复(章节目录注入/逐块审查回执)微增 | 若再增长:门控/拦截拆至 commands_ai_enrich_gate.rs |
4747
| app/src-tauri/src/region_tracker.rs | 478 | v0.4.0 M2(REQ-037)起:ROI 跟踪状态机(播放区域检测/锁定聚簇/重扫/前台切换冻结)+ 纯函数单测内联;与 RoiTracker 状态强耦合 | 若再增长:lock_roi/prior_roi 纯函数拆至 region_lock.rs |
4848
| app/src-tauri/src/layout_analyzer.rs | 475 | v0.5.0 M3(REQ-047):规则版版面分析(行/列投影 + 表格线检测 + 区域分类启发式)内聚于同一分类管线;审查加固(公式启发 + 低信息纯色方差滤除) | 若再增长:区域分类启发式拆至 layout_classify.rs |
@@ -53,7 +53,7 @@
5353
| app/src-tauri/src/structure_note_tests.rs | 458 | v0.7.6(REQ-177~181):结构渲染层单测域(章节插入位置/命名窗口/词汇表排序上限锚点/零回归护栏/JSON 往返)单模块 #[path] 挂载 | 若再增长:词汇表组拆至 structure_note_glossary_tests.rs |
5454
| app/src-tauri/src/db_note_group_clean_tests.rs | 447 | REQ-316(批 7)测试域:判定表(自动/系列/手动/改判/五类残留/影响面外/级联卫生)+ 写路径集成 16 例,单模块 #[path] 挂载 | 若再增长:写路径集成组拆至 db_note_group_clean_flow_tests.rs |
5555
| app/src/pages/KnowledgePage.tsx | 444 | v0.13.8 画布接线越线(原 296):中栏「画布」标签 + 树/画布双入口 + v0.15 左列/详情列状态迁移(useColumnLayout + 折叠窄条 + 宽度 prop)——页面编排层内聚(数据获取/选中态/标签态为页面本地状态),子组件已全部下沉(Tree/Canvas/DetailPanel/Wizard/ConceptCardRow/Sample) | 若再增长:中栏视图块(树/画布/概念/模型 + 标签栏)拆至 KnowledgeMiddlePane.tsx,SystemCard 拆至 SystemSidebarCard.tsx |
56-
| app/src-tauri/src/commands_session.rs | 443 | v0.6.0 M6 + v0.7.6 审查硬拆后回归:会话命令域(CRUD/质量报告/课程分组/段搜索)内聚;笔记转换管线已拆至 commands_session_note.rs;M2 修复(search_ocr_blocks 传 data_dir 参数) | 若再增长:course/search 拆至 commands_session_extra.rs |
56+
| app/src-tauri/src/commands_session.rs | 447 | v0.6.0 M6 + v0.7.6 审查硬拆后回归:会话命令域(CRUD/质量报告/课程分组/段搜索)内聚;笔记转换管线已拆至 commands_session_note.rs;M2 修复(search_ocr_blocks 传 data_dir 参数) | 若再增长:course/search 拆至 commands_session_extra.rs |
5757
| app/src-tauri/src/symbol_normalize.rs | 443 | v0.6.0 M1(REQ-060):口语符号映射域(映射表/上下文守卫/中文数字解析)内聚;数字解析与守卫共享字符判定 | 若再增长:parse_chinese_number/replace_number_runs 拆至 symbol_numbers.rs |
5858
| app/src-tauri/src/commands_goals_plan.rs | 442 | 规划=单次同步调用 + spawn_blocking(10-30s 交互等待可接受,对话框 loading 态);审计完整(ai_tasks op_type='goal_plan',trajectory/成本落库——轨迹可见性达成,任务化轮询留观察项)。(自动摘取,待细化) | 若再增长:按职责拆分 |
5959
| app/src-tauri/src/screens.rs | 437 | v0.7.3(REQ-155/156/160):画面要点屏构建编排(分组/聚类/图匹配 IO)+ 可消费块过滤扩展——编排与纯函数分层(纯函数在 screen_merge.rs) | 若再增长:filter_usable_blocks 拆至 screen_filter.rs |

0 commit comments

Comments
 (0)