Skip to content

Commit 8eb9806

Browse files
committed
refactor(ui): 拆 NoteListView 第 6 步 — 折叠记忆与节派生
1 parent 2d0cdda commit 8eb9806

3 files changed

Lines changed: 102 additions & 49 deletions

File tree

‎app/src/components/NoteListView.tsx‎

Lines changed: 10 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@
1313
* @ai-context: 树序范围:全局可见序(树=未分组+各组顺次、折叠组行不参与)为
1414
* 区间/划选唯一基准;跨组语义=归组(目标手排时按落点插入)。
1515
*/
16-
import { useCallback, useEffect, useMemo, useState } from "react";
16+
import { useCallback, useMemo } from "react";
1717
import type { Note, NoteGroup } from "../types";
1818
import { paletteHex } from "../utils/colorPalette";
1919
import type { ThemeMode } from "../utils/colorPalette";
2020
// 批 0-C2:展示节/可见序纯函数层(含 scope 键与裸折叠键派生——纯逻辑与副作用分离)
21-
import { buildSections, buildVisibleOrder, isTreeMode, manualBaseIds, scopeKey } from "../utils/noteSectionModel";
21+
import { manualBaseIds, scopeKey } from "../utils/noteSectionModel";
2222
// 批 0-C2:序行 store(笔记手排序行 + 组手排序行)——兑现既有登记拆分计划
2323
import { useNoteOrders } from "../hooks/useNoteOrders";
24+
// 批 0-C2:折叠记忆(裸键)+ 展树数据接线(treeMode/sections/visibleOrder)
25+
import { useNoteSections } from "../hooks/useNoteSections";
2426
// 批 0-C2:拖拽/移动三入口接线(共享一把行落点并发锁——硬性同文件约束)
2527
import { useNoteMoves } from "../hooks/useNoteMoves";
2628
// 批 0-C2:多选三通道 + 菜单三态 + 唯一 Esc handler(可见序镜像同文件)
@@ -73,11 +75,6 @@ interface Props {
7375
refreshToken?: number;
7476
}
7577

76-
/** 读组折叠记忆(localStorage 损坏/无 → 默认展开) */
77-
function readGroupFold(key: string): boolean {
78-
try { return window.localStorage.getItem(`notes:group-fold:${key}`) === "1"; } catch { return false; }
79-
}
80-
8178
export default function NoteListView({
8279
width = 320, notes, groups = [], groupFilter = null, onGroupFilterChange,
8380
keyword, tagFilter, sortMode, allTags, selectedId, status,
@@ -91,48 +88,15 @@ export default function NoteListView({
9188
[],
9289
);
9390

94-
// 组折叠态
95-
const [groupFolds, setGroupFolds] = useState<Record<string, boolean>>({});
96-
9791
// 序行 store(笔记手排序行 + 组手排序行)——装载/保存路径下沉 hooks/useNoteOrders
98-
const { manualOrders, groupOrderRows, saveOrder, resetOrder } = useNoteOrders(refreshToken);
99-
100-
// 手动序装载(REQ-287:notes 行)+ REQ-315:组序行(树组头排序)——见 hooks/useNoteOrders
10192
// (依赖 refreshToken 的理由随实现搬至该 hook 头注释:审查 P2-10 的令牌重拉)
93+
const { manualOrders, groupOrderRows, saveOrder, resetOrder } = useNoteOrders(refreshToken);
10294

103-
// ── 分组树数据(同 v0.15 结构)——可见序统一从本结构生成 ──
104-
// Why 不再有分桶 memo:旧 `grouped` 是死载荷——仅被当作 treeMode 的第二真值,
105-
// 其 ungrouped/byGroup 从未被读取(真分桶一直在 sections 内重算);批 0-C2 去重
106-
const treeMode = isTreeMode(keyword, tagFilter, sortMode);
107-
108-
// 折叠初始值(沿用 v0.15)
109-
useEffect(() => {
110-
setGroupFolds((cur) => {
111-
let changed = false;
112-
const next = { ...cur };
113-
for (const g of groups) {
114-
const key = String(g.id);
115-
if (!(key in next)) { next[key] = readGroupFold(key); changed = true; }
116-
}
117-
if (!("none" in next)) { next.none = readGroupFold("none"); changed = true; }
118-
return changed ? next : cur;
119-
});
120-
}, [groups]);
121-
useEffect(() => {
122-
try {
123-
for (const [k, v] of Object.entries(groupFolds)) window.localStorage.setItem(`notes:group-fold:${k}`, v ? "1" : "0");
124-
} catch { /* 隐私模式 */ }
125-
}, [groupFolds]);
126-
127-
// 显示节(树/平铺)——节内展示序/分桶/accent 全在 utils/noteSectionModel.buildSections
128-
// deps 保留 groupFolds:函数体不读它,但移除会改变重算次数(本批只搬家不优化)
129-
const sections = useMemo(
130-
() => buildSections({ notes, groups, groupFilter, theme, treeMode, manualOrders, groupOrderRows }),
131-
[treeMode, groups, notes, groupFolds, manualOrders, theme, groupFilter, groupOrderRows],
132-
);
133-
134-
// 可见序(L1 审查:折叠组行不参与区间/划选——与渲染可见一致;折叠组头仍在)
135-
const visibleOrder = useMemo(() => buildVisibleOrder(sections, groupFolds), [sections, groupFolds]);
95+
// 折叠记忆 + 展树数据(treeMode 总闸 / sections 展示节 / visibleOrder 全局可见序)
96+
// —— 裸键折叠记忆与纯函数派生见 hooks/useNoteSections + utils/noteSectionModel
97+
const { treeMode, sections, visibleOrder, groupFolds, toggleGroupFold } = useNoteSections({
98+
notes, groups, keyword, tagFilter, sortMode, groupFilter, theme, manualOrders, groupOrderRows,
99+
});
136100

137101
// ── 行交互 / 多选 / 菜单(Esc 优先级链、可见序镜像、选集裁剪)——见 hooks/useNoteListSelection
138102
const {
@@ -172,8 +136,6 @@ export default function NoteListView({
172136
/>
173137
);
174138

175-
const toggleGroupFold = (key: string) => setGroupFolds((cur) => ({ ...cur, [key]: !cur[key] }));
176-
177139
/**
178140
* 右键菜单「上移/下移」可用性(REQ-315):仅树视图 scope 上下文开放(交互矩阵:
179141
* 平铺/搜索/标签/非默认排序禁移动——移动=scope 级手排快照,过滤结果是子集非

‎app/src/hooks/useNoteSections.ts‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* useNoteSections — 组折叠记忆 + 展示节/可见序派生(批 0-C2·Task 2 自 NoteListView 抽出)。
3+
*
4+
* @ai-context: 折叠态用**裸键**记忆:`String(groupId)` / `"none"`,localStorage 键
5+
* `notes:group-fold:{裸键}`。⚠️ 这与落库用的 scope 键(`g:{id}`,见
6+
* noteSectionModel.scopeKey)**不是一回事**——混用会让用户折叠记忆静默
7+
* 失效,而既有测试每测 `localStorage.clear()` 恰好掩盖它(键派生断言见
8+
* components/NoteListView.fold.test.tsx)。
9+
* @ai-context: 副作用——① 组列表变化时幂等补默认键(缺失才读 localStorage;损坏/无 →
10+
* 默认展开,try/catch 兜底);② 折叠态变化时全量写回 localStorage(隐私
11+
* 模式写失败静默)。展示节与可见序的纯逻辑在 utils/noteSectionModel,
12+
* 本 hook 只做记忆 + 接线(纯逻辑与副作用物理分离)。
13+
* @ai-context: 边界——treeMode(无关键词/无标签/默认排序)是拖拽与移动矩阵的总闸;
14+
* 可见序**排除折叠组行**(L1 审查:与渲染可见一致,Shift 区间选/划选共用);
15+
* sections memo 的 deps 保留 groupFolds(函数体不读它,但移除会改变重算
16+
* 次数——本批只搬家不优化)。
17+
*/
18+
import { useEffect, useMemo, useState } from "react";
19+
import type { Note, NoteGroup } from "../types";
20+
import type { ThemeMode } from "../utils/colorPalette";
21+
import type { GroupOrderRows } from "../utils/groupOrder";
22+
import { buildSections, buildVisibleOrder, isTreeMode } from "../utils/noteSectionModel";
23+
24+
/** localStorage 折叠记忆键(裸键=group id / "none") */
25+
const foldStorageKey = (key: string): string => `notes:group-fold:${key}`;
26+
27+
/** 读组折叠记忆(localStorage 损坏/无 → 默认展开) */
28+
function readGroupFold(key: string): boolean {
29+
try { return window.localStorage.getItem(foldStorageKey(key)) === "1"; } catch { return false; }
30+
}
31+
32+
export interface NoteSectionsInput {
33+
notes: Note[];
34+
groups: NoteGroup[];
35+
keyword: string;
36+
tagFilter: string | null;
37+
sortMode: string;
38+
groupFilter: number | null;
39+
theme: ThemeMode;
40+
/** scope → 手排有序 ids(useNoteOrders) */
41+
manualOrders: Record<string, number[]>;
42+
/** 组手排序行(树组头排序消费;useNoteOrders) */
43+
groupOrderRows: GroupOrderRows;
44+
}
45+
46+
export function useNoteSections({
47+
notes, groups, keyword, tagFilter, sortMode, groupFilter, theme, manualOrders, groupOrderRows,
48+
}: NoteSectionsInput) {
49+
// 组折叠态
50+
const [groupFolds, setGroupFolds] = useState<Record<string, boolean>>({});
51+
52+
// 折叠初始值(沿用 v0.15)
53+
useEffect(() => {
54+
setGroupFolds((cur) => {
55+
let changed = false;
56+
const next = { ...cur };
57+
for (const g of groups) {
58+
const key = String(g.id);
59+
if (!(key in next)) { next[key] = readGroupFold(key); changed = true; }
60+
}
61+
if (!("none" in next)) { next.none = readGroupFold("none"); changed = true; }
62+
return changed ? next : cur;
63+
});
64+
}, [groups]);
65+
useEffect(() => {
66+
try {
67+
for (const [k, v] of Object.entries(groupFolds)) window.localStorage.setItem(foldStorageKey(k), v ? "1" : "0");
68+
} catch { /* 隐私模式 */ }
69+
}, [groupFolds]);
70+
71+
const toggleGroupFold = (key: string) => setGroupFolds((cur) => ({ ...cur, [key]: !cur[key] }));
72+
73+
// ── 分组树数据(同 v0.15 结构)——可见序统一从本结构生成 ──
74+
// Why 不再有分桶 memo:旧 `grouped` 是死载荷——仅被当作 treeMode 的第二真值,
75+
// 其 ungrouped/byGroup 从未被读取(真分桶一直在 sections 内重算);批 0-C2 去重
76+
const treeMode = isTreeMode(keyword, tagFilter, sortMode);
77+
78+
// 显示节(树/平铺)——节内展示序/分桶/accent 全在 utils/noteSectionModel.buildSections
79+
// deps 保留 groupFolds:函数体不读它,但移除会改变重算次数(本批只搬家不优化)
80+
const sections = useMemo(
81+
() => buildSections({ notes, groups, groupFilter, theme, treeMode, manualOrders, groupOrderRows }),
82+
[treeMode, groups, notes, groupFolds, manualOrders, theme, groupFilter, groupOrderRows],
83+
);
84+
85+
// 可见序(L1 审查:折叠组行不参与区间/划选——与渲染可见一致;折叠组头仍在)
86+
const visibleOrder = useMemo(() => buildVisibleOrder(sections, groupFolds), [sections, groupFolds]);
87+
88+
// toggleGroupFold 保持普通函数(非 useCallback):消费点(组头 onToggleFold、可见序
89+
// 重算)本就是内联箭头,包装反而改变引用语义——纯搬运不加装饰
90+
return { treeMode, sections, visibleOrder, groupFolds, toggleGroupFold };
91+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@
2929

3030
| 文件 | 行数 | 豁免理由 | 拆分计划 |
3131
|---|---|---|---|
32-
| app/src/components/SessionListPanel.tsx | 598 | 超硬限(>600 行),不允许豁免 —— v0.20.9 批 4(REQ-313)列表交互重写(选择模式/行右键/行内改名/批量栏)+ 审查修复轮 3(全选可见行基准/pending)净增——2026-09-09 实测纠偏(登记值 491 过期);**超 600 硬限随 TD-2026-09-09-D 登记** | **批 0-C2 Task 5 八文件边界**(原计划「批量栏 + 选择模式」只减 166 行 ⇒ 仍 ≈468 >300,不足):`utils/sessionEligibility.ts`(可转化纯函数)· `hooks/useSessionSelection.ts`(选择态机 + visibleOrderRef + 裁剪 effect;**Esc 监听留面板**)· `hooks/useSessionListView.ts`(matchFilters/sorted/filtered/groupedView/visibleOrder)· `hooks/useSessionSearch.ts`(三模式搜索 + 两个 invoke 落点)· `components/SessionSearchBar.tsx`(三模式单输入框)· `components/SessionSearchHits.tsx`(段/画面命中视图)· `components/SessionListBody.tsx`(列表容器含 renderRow)· `components/SessionSelectionToolbar.tsx`(头部选择控件 + 底部批量栏);列表行已拆 SessionListRow.tsx |
3332
| app/src-tauri/src/commands.rs | 597 | 命令装配域(AppState + 通用命令 + 导入管线编排);登记值 466 过期快照——2026-09-09 实测纠偏(含批 7 delete_note 结果契约 +12;600 硬限内压线) | 若再增长:导入管线命令拆至 commands_import.rs(既有登记计划) |
3433
| app/src/pages/ChatPage.tsx | 593 | AI 对话页编排;批 1(REQ-306/307)active 门控/终态订阅接线净增——2026-09-09 实测纠偏(登记值 529 过期) | 若再增长:任务工具条与发起流拆至 ChatTasksToolbar.tsx |
3534
| app/src-tauri/src/lib.rs | 577 | crate 根 321 `mod` + 16 `#[cfg]` = **337 行地板**;注册清单已移至 `app_commands.rs`(**数据文件**,同属 300–600 豁免带)—— 结构性下界,非欠账 | 已完成(批 0-C3 Task 1,2026-09-11);余下 161 行模块理由注释 + 装配逻辑,无进一步拆分标的 |
3635
| 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 |
3736
| app/src/types/knowledge.ts | 559 | 知识体系类型域(体系/节点/概念/模型/引用/审计/决策 + v0.13.8 画布契约 + v0.14.1 画布偏好枚举与下拉文案常量)——类型与文案常量同域防漂移(前端类型域拆分任务待执行) | 若再增长:画布偏好类型与文案拆至 types/canvas.ts |
3837
| app/src-tauri/src/capture/audio_loopback.rs | 558 | ADR-007 重连机制(重试循环/退避/恢复回调)内聚于捕获线程实现,拆出需跨函数传递 COM 生命周期参数,内聚性优先;2026-08 A1 硬暂停(端点 Stop/Start + 暂停时长补偿 + 残留缓冲清空)再增 | 若再增长:将 run_capture_inner 拆至 audio_loopback_session.rs |
3938
| app/src-tauri/src/screen_merge.rs | 548 | v0.7.3(REQ-155/158):屏级聚合纯函数域(聚类/行合并/角色分类/块去重)+ v0.7.5 净化纯函数(单字符/边缘条带/零跨度合并/图去重/包含率)——纯逻辑内聚便于单测 | 若再增长:零跨度合并与图去重拆至 screen_fix.rs |
39+
| app/src/components/SessionListPanel.tsx | 547 | 超硬限(>600 行),不允许豁免 —— v0.20.9 批 4(REQ-313)列表交互重写(选择模式/行右键/行内改名/批量栏)+ 审查修复轮 3(全选可见行基准/pending)净增——2026-09-09 实测纠偏(登记值 491 过期);**超 600 硬限随 TD-2026-09-09-D 登记** | **批 0-C2 Task 5 八文件边界**(原计划「批量栏 + 选择模式」只减 166 行 ⇒ 仍 ≈468 >300,不足):`utils/sessionEligibility.ts`(可转化纯函数)· `hooks/useSessionSelection.ts`(选择态机 + visibleOrderRef + 裁剪 effect;**Esc 监听留面板**)· `hooks/useSessionListView.ts`(matchFilters/sorted/filtered/groupedView/visibleOrder)· `hooks/useSessionSearch.ts`(三模式搜索 + 两个 invoke 落点)· `components/SessionSearchBar.tsx`(三模式单输入框)· `components/SessionSearchHits.tsx`(段/画面命中视图)· `components/SessionListBody.tsx`(列表容器含 renderRow)· `components/SessionSelectionToolbar.tsx`(头部选择控件 + 底部批量栏);列表行已拆 SessionListRow.tsx |
4040
| app/src/components/action-center/ActionCenterPanel.tsx | 537 | v0.20.5 行动中心独立页化:原 ActionCenterOverlay.tsx(509 行登记)更名迁移至 action-center/ 并去遮罩/关闭形态(refreshToken 切回重载)——编排内聚(TD-2026-09-06-G 预留目录兑现);2026-09-06 实测登记 | 若再增长:队列/历史/SOP 三区拆至 action-center/ 子组件 |
4141
| app/src-tauri/src/commands_knowledge_core.rs | 529 | v0.13.1(REQ-202~205):知识体系命令域(概念/模型/引用/审计——commands 9-18)内聚;源 commands_knowledge.rs(18 命令 + 校验)超限按规格 §四拆,本文件承接后半;commands 薄壳 + inner 纯函数 + @ai-context 注释内聚于命令域 | 若再增长:引用与审计拆至 commands_knowledge_links.rs |
4242
| app/src-tauri/src/live_frame_process.rs | 529 | v0.6.0 ADR-011 拆分产物:帧处理域(网格差异触发/两级判变/带外事件驱动/UI 面板抑制/字幕落库)内聚;process_frame 上下文参数 20+;H2 修复(OCR 热路径切超时变体)+ L2 修复(score 口径诚实化)行数微增 + v0.11.5 Task 2 新颖度变化区域接线再增 | 若再增长:handle_subtitle_frame 与 persist_voted_subtitle 拆至 live_subtitle_persist.rs |

0 commit comments

Comments
 (0)