Skip to content

Commit 521a848

Browse files
committed
refactor(notes): 抽出阅读列的加载器与展示性子件
1 parent 5b69279 commit 521a848

3 files changed

Lines changed: 218 additions & 103 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* NotesReadingColumn.parts — 笔记阅读列的**展示性子件**(自 `NotesReadingColumn.tsx` 纯搬迁:批 8 T7)。
3+
*
4+
* @ai-context: 为什么单独成件:宿主件已贴 ≤300 硬线(实测 300 / 余 0),而 T8/T10 的能力还要落在
5+
* 同一个宿主上 ⇒ 先按**可判据的切割线**把头寸腾出来。切下来的是**一块零状态片段**:
6+
* `ResidentNoteView` —— 原文常驻视图的**整棵子树**(`NoteReadingView` + `auxPanels`
7+
* + 空态占位)。
8+
* @ai-context: ⚠️ **C4 的就近错误行(`StatusLine`)没有搬进本件**(卡片的 ② 把它列作候选):真构建实测
9+
* 「多一个子件 + 一层元素」把 `NotesPage-` 族推过 `scripts/lazyBudget.json` 的逐族容差
10+
* (48,697 > 48,627 + 64)⇒ 按字节预算回退为**宿主内联**(另有独立读数,见 T7 报告 V5)。
11+
* @ai-context: 🔴 为什么**只有**这一块能动:`NotesReadingColumn.tsx` 上钉着 `views/architecture.guard.test.ts`
12+
* **A5②③** 的逐文件判据(`isDefault` 判定 + `display: isDefault ?` 常驻三元 + 恰 1 个
13+
* `<ViewSwitcher>` + 恰 1 个 `data-view-error-slot` 且**位于切换器之后**)⇒ 切换器行、
14+
* C11 槽位、两个容器的开关逻辑**必须留在宿主**,搬走即红。本件只承载「宿主已算好的值
15+
* 怎么画」,**不含**任何状态 / ref / effect。
16+
* @ai-context: 组件边界不动 DOM —— 本件逐字渲染原来那棵树(`data-testid` / 类名 / 文案一字不改)。
17+
* `RichEditorView` / `NoteReadingView` / `NoteHeaderActions` / `VersionPanel` / `Text` 的
18+
* import 随本件走 ⇒ 宿主不再直接依赖这五个模块。宿主用 `<ResidentNoteView {...props} />`
19+
* **原样透传整份 props**(比列 22 条具名属性省一个对象字面量 —— 逐族字节读数的同一处约束)。
20+
* 副作用:只挂 React 树(与宿主同)。边界:本件在六棘轮的 PROD 域内 ⇒ 零颜色 / 零字号 / 零边框字面量
21+
* (`style` 只承载布局,ADR-033 §4),且**零原生 `<button>`**。
22+
*/
23+
import type { RefObject } from "react";
24+
import type { Note, NoteGroup } from "../../types";
25+
import type { NoteEditHandle } from "../NoteEditView";
26+
import type { ColumnLayout } from "../../hooks/useColumnLayout";
27+
import type { SelectionNoteAction } from "../../utils/noteSelectionMenu";
28+
import NoteReadingView from "../NoteReadingView";
29+
import RichEditorView from "../RichEditorView";
30+
import NoteHeaderActions from "../NoteHeaderActions";
31+
import VersionPanel from "../VersionPanel";
32+
import { Text } from "../../ui/primitives";
33+
34+
/** 原文常驻子树的 props 面 —— 与宿主的 `Props` **逐字段同型**(本件不新增、不改窄任何一条)。 */
35+
export interface ResidentViewProps {
36+
/** 当前选中笔记(null=空态占位) */
37+
selected: Note | null;
38+
/** 编辑态(useNotesPageEditing 持有) */
39+
editing: boolean;
40+
setEditing: (v: boolean) => void;
41+
/** 命中词阅读搜索请求(来自引用跳转;过期请求按 noteId 过滤不注入) */
42+
readerSearch: { noteId: number; search: string; key: number } | null;
43+
/** 笔记色板(noteId → 解析色,含组继承;headerExtra 色点用) */
44+
noteColors: Record<number, string | null>;
45+
groups: NoteGroup[];
46+
/** 编辑器命令式出口(RichEditorView 的 ref 目标——ESC 先 await 保存再刷新;C4 的守卫读它) */
47+
editorRef: RefObject<NoteEditHandle | null>;
48+
/** 大纲列状态(页面 useColumnLayout("notes-outline")) */
49+
outlineCol: ColumnLayout;
50+
/** 刷新中枢:列表重载 + 选中笔记回读(H3/AI/移组/编辑退出三个出口共用) */
51+
onChanged: () => void;
52+
/** 错误上抛(页面 status 区展示) */
53+
onError: (msg: string) => void;
54+
/** 打开体系页并建体系向导(TD-2026-09-05-A 空体系引导) */
55+
onCreateSystem?: () => void;
56+
/** 打开编辑态 AI 能力对话框(阅读态点击=页面进入编辑态 + 内容快照) */
57+
onOpenAi: () => void;
58+
/** 打开模型卡草稿对话框(组内 model 卡唯一生成链) */
59+
onOpenModelCard: () => void;
60+
/** 正文选区行动类上抛(转问题/模型卡预填——阅读与编辑两宿主同一入口) */
61+
onSelectionAction: (action: SelectionNoteAction, text: string) => void;
62+
onPinToggle: (note: Note) => void;
63+
onDelete: (id: number) => void;
64+
/** H1:任务清单勾选回写(本地先行 + 建版本快照) */
65+
onTaskToggle: (newContent: string) => void;
66+
/** 标签点击 → 页面切过滤态(清关键词 + 切回笔记视图) */
67+
onTagClick: (tag: string) => void;
68+
/** 来源会话反跳(缺省=不可跳) */
69+
onOpenSession?: (sessionId: number) => void;
70+
/** 批 6 T26:`[[ts:ms]]` 回链的带毫秒分支(透传给 `NoteReadingView`;缺省 ⇒ 退回 `onOpenSession`) */
71+
onOpenSessionAt?: (sessionId: number, ms: number) => void;
72+
/** 图片放大预览入口(阅读态与编辑态共用同一遮罩) */
73+
onImageOpen: (src: string, title?: string) => void;
74+
/** 空组清理留痕上抛(移组触发,父层 toast) */
75+
onCleanNotice: (groupNames: string[]) => void;
76+
}
77+
78+
export function ResidentNoteView({
79+
selected, editing, setEditing, readerSearch, noteColors, groups, editorRef, outlineCol,
80+
onChanged, onError, onCreateSystem, onOpenAi, onOpenModelCard, onSelectionAction,
81+
onPinToggle, onDelete, onTaskToggle, onTagClick, onOpenSession, onOpenSessionAt, onImageOpen, onCleanNotice,
82+
}: ResidentViewProps) {
83+
// H3:辅助面板插槽——VersionPanel(知识补充已迁移至编辑态 🤖 AI 菜单——
84+
// v0.17.0 REQ-246:阅读态独立面板移除,用 AI 直接进入编辑态)
85+
const auxPanels = selected ? (
86+
<>
87+
<VersionPanel key={`version-${selected.id}`} noteId={selected.id} onChanged={() => void onChanged()} onOpenSessionAt={onOpenSessionAt} />
88+
</>
89+
) : null;
90+
91+
/** 默认视图(原文)节点:T14 之前逐字相同,只是现在由常驻容器承载(§7.3①) */
92+
return selected ? (
93+
<NoteReadingView
94+
note={selected}
95+
editing={editing}
96+
// v0.19.1:命中词阅读搜索(仅当请求属于当前选中笔记——过期请求不注入)
97+
externalSearch={readerSearch && selected?.id === readerSearch.noteId
98+
? { key: readerSearch.key, query: readerSearch.search }
99+
: null}
100+
outlineFolded={outlineCol.folded}
101+
// 批 3 T8:大纲列宽/拖拽经 props 注入(此前 NoteReadingView 写死 180,
102+
// hook 的宽度记忆与 min/max 夹取全部失效——审计 J1-6「假可调」)
103+
outlineWidth={outlineCol.width}
104+
onOutlineResize={outlineCol.resizeBy}
105+
onOutlineReset={outlineCol.resetWidth}
106+
// 批 3 T8(J1-3):窄条(ColumnBar)点击必须走 expand()——它同时清自动/手动
107+
// 折叠态;旧实现只翻 manualFolded,窄窗自动折叠下 folded 恒为真 ⇒ 点窄条
108+
// 永不展开。未折叠时的 ✕「收起大纲」仍是手动折叠(与 NotesGroupsColumn 的
109+
// bar=expand / onCollapse=setManualFolded 同款形态)
110+
onToggleOutline={() =>
111+
outlineCol.folded ? outlineCol.expand() : outlineCol.setManualFolded(true)
112+
}
113+
editor={
114+
<RichEditorView
115+
key={selected.id}
116+
ref={editorRef}
117+
note={selected}
118+
onCancel={() => {
119+
// v0.13.6:完成编辑 → 列表重载 + 选中笔记重取(右栏立即显示新标题/正文)
120+
setEditing(false);
121+
void onChanged();
122+
}}
123+
// v0.14 A:编辑态图片点击放大(与阅读态同一入口)
124+
onImageOpen={(src, title) => onImageOpen(src, title)}
125+
// 批 8(REQ-317):编辑态选区行动类(转问题/模型卡预填)
126+
onSelectionAction={onSelectionAction}
127+
/>
128+
}
129+
auxPanels={auxPanels}
130+
headerExtra={
131+
<NoteHeaderActions
132+
key={`hdr-${selected.id}`}
133+
note={selected}
134+
resolvedColor={noteColors[selected.id] ?? null}
135+
groups={groups}
136+
onChanged={() => void onChanged()}
137+
onError={(m) => onError(m)}
138+
onGotoKnowledgeSystem={onCreateSystem}
139+
onOpenAi={onOpenAi}
140+
onOpenModelCard={onOpenModelCard}
141+
onCleanNotice={onCleanNotice}
142+
/>
143+
}
144+
onEdit={() => setEditing(true)}
145+
onPinToggle={() => void onPinToggle(selected)}
146+
onDelete={() => void onDelete(selected.id)}
147+
onTagClick={onTagClick}
148+
onOpenSession={(id) => onOpenSession?.(id)}
149+
onOpenSessionAt={onOpenSessionAt}
150+
onTaskToggle={onTaskToggle}
151+
onImageOpen={(src, title) => onImageOpen(src, title)}
152+
// 批 8(REQ-317):阅读态选区行动类(转问题/模型卡预填)
153+
onSelectionAction={onSelectionAction}
154+
/>
155+
) : (
156+
<Text as="div" size={4} tone="ink-3" style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
157+
从左侧选择一条笔记查看
158+
</Text>
159+
);
160+
}

‎app/src/components/notes/NotesReadingColumn.tsx‎

Lines changed: 11 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -50,28 +50,22 @@
5050
* 可见),不会静默白屏。棘轮口径:新增代码 0 处颜色/边框/字号字面量(错误色走
5151
* `StatusLine` 原语 · 间距走 `--ed-space-*`),`style` 只承载布局(ADR-033 §4)。
5252
*/
53-
import { Suspense, createElement, lazy, useCallback, useMemo, useState } from "react";
54-
import type { ComponentType, RefObject } from "react";
53+
import { Suspense, createElement, useCallback, useMemo, useState } from "react";
54+
import type { RefObject } from "react";
5555
import type { Note, NoteGroup } from "../../types";
5656
import type { NoteEditHandle } from "../NoteEditView";
5757
import type { ColumnLayout } from "../../hooks/useColumnLayout";
5858
import type { SelectionNoteAction } from "../../utils/noteSelectionMenu";
5959
import type { NoteViewSlot, ViewSpec } from "../../views/registry";
6060
import { useViewMemory } from "../../views/useViewMemory";
61-
import NoteReadingView from "../NoteReadingView";
62-
import RichEditorView from "../RichEditorView";
63-
import NoteHeaderActions from "../NoteHeaderActions";
64-
import VersionPanel from "../VersionPanel";
6561
import { ShellFallback } from "../../shell/ShellFallback";
66-
import { StatusLine, Text, ViewSwitcher } from "../../ui/primitives";
62+
import { StatusLine, ViewSwitcher } from "../../ui/primitives";
63+
import { ResidentNoteView } from "./NotesReadingColumn.parts";
64+
import { CARD_FLOW_LOAD, lazyMapOf } from "./readingColumnLoaders";
6765

6866
/** `views` 缺席时的空清单:**模块级常量**(稳定引用 ⇒ 下面的 `useMemo` 不每渲染重算) */
6967
const NO_VIEWS: readonly ViewSpec<NoteViewSlot>[] = [];
7068

71-
/** 卡片流键(= 注册表的 `NOTE_VIEWS[1]`;本件不 import 注册表 —— C1①)与其**带 seek 包装件**加载器(T17/C10.3:该槽缺 `onOpenSessionAt`);仍经 `lazy()` ⇒ 模块级惰性不变。 */
72-
const CARD_FLOW_KEY = "cardflow";
73-
const CARD_FLOW_LOAD = (): Promise<{ default: ComponentType<NoteViewSlot> }> => import("../../views/note/NoteCardFlowWithSeek");
74-
7569
interface Props {
7670
/** 当前选中笔记(null=空态占位) */
7771
selected: Note | null;
@@ -117,20 +111,8 @@ interface Props {
117111
views?: readonly ViewSpec<NoteViewSlot>[];
118112
}
119113

120-
export default function NotesReadingColumn({
121-
selected, editing, setEditing, readerSearch, noteColors, groups, editorRef, outlineCol,
122-
onChanged, onError, onCreateSystem, onOpenAi, onOpenModelCard, onSelectionAction,
123-
onPinToggle, onDelete, onTaskToggle, onTagClick, onOpenSession, onOpenSessionAt, onImageOpen, onCleanNotice,
124-
views,
125-
}: Props) {
126-
// H3:辅助面板插槽——VersionPanel(知识补充已迁移至编辑态 🤖 AI 菜单——
127-
// v0.17.0 REQ-246:阅读态独立面板移除,用 AI 直接进入编辑态)
128-
const auxPanels = selected ? (
129-
<>
130-
<VersionPanel key={`version-${selected.id}`} noteId={selected.id} onChanged={() => void onChanged()} onOpenSessionAt={onOpenSessionAt} />
131-
</>
132-
) : null;
133-
114+
export default function NotesReadingColumn(props: Props) {
115+
const { selected, editorRef, onTaskToggle, onOpenSession, onOpenSessionAt, onImageOpen, views } = props;
134116
// ── 批 5 T14:视图宿主(C1① 注入 · C5 记忆 · C4 守卫 · C11 槽位)──
135117
const specs = views ?? NO_VIEWS;
136118
const keys = useMemo(() => specs.map((v) => v.key), [specs]);
@@ -163,15 +145,8 @@ export default function NotesReadingColumn({
163145
[viewKey, editorRef, setViewKey],
164146
);
165147

166-
/**
167-
* 惰性映射:**默认视图无 `load` ⇒ 值为 `null`**,永不进 `React.lazy`(§7.3② 的构造性证据)。
168-
* `lazy()` 只按 `specs` 引用建一次 —— 每渲染新建会让子树恒重挂(F5 的挂载计数会当场红)。
169-
* T17:**卡片流**经包装件加载(键漂移会让它静默失效 ⇒ F11 用例即守卫)。
170-
*/
171-
const lazyOf = useMemo(
172-
() => new Map(specs.map((spec) => [spec.key, spec.load ? lazy(spec.key === CARD_FLOW_KEY ? CARD_FLOW_LOAD : spec.load) : null])),
173-
[specs],
174-
);
148+
/** 惰性映射(键常量与工厂已搬到 `readingColumnLoaders`);`useMemo` 留在宿主 —— 工厂是**纯函数**、不持有记忆。 */
149+
const lazyOf = useMemo(() => lazyMapOf<NoteViewSlot>(specs, CARD_FLOW_LOAD), [specs]);
175150
const isDefault = viewKey === defaultKey;
176151
const LazyView = isDefault ? null : lazyOf.get(viewKey) ?? null;
177152
/** 非默认视图的槽(数据全在这里,视图自身零取数 —— C14②);空态 ⇒ `null`(连卡片流都不挂)。
@@ -181,75 +156,8 @@ export default function NotesReadingColumn({
181156
: null;
182157
const viewProps = slot === null ? null : { ...slot, onOpenSessionAt };
183158

184-
/** 默认视图(原文)节点:T14 之前逐字相同,只是现在由常驻容器承载(§7.3①) */
185-
const residentView = selected ? (
186-
<NoteReadingView
187-
note={selected}
188-
editing={editing}
189-
// v0.19.1:命中词阅读搜索(仅当请求属于当前选中笔记——过期请求不注入)
190-
externalSearch={readerSearch && selected?.id === readerSearch.noteId
191-
? { key: readerSearch.key, query: readerSearch.search }
192-
: null}
193-
outlineFolded={outlineCol.folded}
194-
// 批 3 T8:大纲列宽/拖拽经 props 注入(此前 NoteReadingView 写死 180,
195-
// hook 的宽度记忆与 min/max 夹取全部失效——审计 J1-6「假可调」)
196-
outlineWidth={outlineCol.width}
197-
onOutlineResize={outlineCol.resizeBy}
198-
onOutlineReset={outlineCol.resetWidth}
199-
// 批 3 T8(J1-3):窄条(ColumnBar)点击必须走 expand()——它同时清自动/手动
200-
// 折叠态;旧实现只翻 manualFolded,窄窗自动折叠下 folded 恒为真 ⇒ 点窄条
201-
// 永不展开。未折叠时的 ✕「收起大纲」仍是手动折叠(与 NotesGroupsColumn 的
202-
// bar=expand / onCollapse=setManualFolded 同款形态)
203-
onToggleOutline={() =>
204-
outlineCol.folded ? outlineCol.expand() : outlineCol.setManualFolded(true)
205-
}
206-
editor={
207-
<RichEditorView
208-
key={selected.id}
209-
ref={editorRef}
210-
note={selected}
211-
onCancel={() => {
212-
// v0.13.6:完成编辑 → 列表重载 + 选中笔记重取(右栏立即显示新标题/正文)
213-
setEditing(false);
214-
void onChanged();
215-
}}
216-
// v0.14 A:编辑态图片点击放大(与阅读态同一入口)
217-
onImageOpen={(src, title) => onImageOpen(src, title)}
218-
// 批 8(REQ-317):编辑态选区行动类(转问题/模型卡预填)
219-
onSelectionAction={onSelectionAction}
220-
/>
221-
}
222-
auxPanels={auxPanels}
223-
headerExtra={
224-
<NoteHeaderActions
225-
key={`hdr-${selected.id}`}
226-
note={selected}
227-
resolvedColor={noteColors[selected.id] ?? null}
228-
groups={groups}
229-
onChanged={() => void onChanged()}
230-
onError={(m) => onError(m)}
231-
onGotoKnowledgeSystem={onCreateSystem}
232-
onOpenAi={onOpenAi}
233-
onOpenModelCard={onOpenModelCard}
234-
onCleanNotice={onCleanNotice}
235-
/>
236-
}
237-
onEdit={() => setEditing(true)}
238-
onPinToggle={() => void onPinToggle(selected)}
239-
onDelete={() => void onDelete(selected.id)}
240-
onTagClick={onTagClick}
241-
onOpenSession={(id) => onOpenSession?.(id)}
242-
onOpenSessionAt={onOpenSessionAt}
243-
onTaskToggle={onTaskToggle}
244-
onImageOpen={(src, title) => onImageOpen(src, title)}
245-
// 批 8(REQ-317):阅读态选区行动类(转问题/模型卡预填)
246-
onSelectionAction={onSelectionAction}
247-
/>
248-
) : (
249-
<Text as="div" size={4} tone="ink-3" style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }}>
250-
从左侧选择一条笔记查看
251-
</Text>
252-
);
159+
/** 原文常驻子树的**展示装配**已搬到 `NotesReadingColumn.parts`(本件把整份 props 原样透传 —— 见该件文件头的字节预算理由)。 */
160+
const residentView = <ResidentNoteView {...props} />;
253161

254162
return (
255163
<div style={{ flex: 1, minWidth: 0, display: "flex", overflow: "hidden" }}>

0 commit comments

Comments
 (0)