|
| 1 | +/** |
| 2 | + * NoteListBody — 笔记列表体(滚动容器 + 节/行装配;批 0-C2·Task 2 自 NoteListView 抽出)。 |
| 3 | + * |
| 4 | + * @ai-context: 展示适配器——只做「透传 + 一层组合」,**不含业务逻辑**(逻辑在 |
| 5 | + * hooks/useNoteSections、useNoteMoves、useNoteListSelection 与 |
| 6 | + * utils/noteSectionModel)。行 id / data-testid 契约由 NoteListRow 与 |
| 7 | + * NoteTreeSection 承载,本文件不新增 DOM 属性、不改文案。 |
| 8 | + * @ai-context: 硬点——① 行色条 accent 由 paletteHex(noteColors[id], theme) 现场解析 |
| 9 | + * (与拆分前同一表达式);② 平铺分支渲染 `notes.map(renderRow)` 而 |
| 10 | + * 树分支渲染 `sec.items.map(renderRow)` —— 二者今日等价(flat 节的 items |
| 11 | + * 就是 notes),**勿"顺手统一"**(`sec.items` 走的是 orderScopeNotes 短路, |
| 12 | + * 改动会牵动重排语义);③ 折叠只由 NoteTreeSection 的 folded 决定, |
| 13 | + * 本文件不裁剪 sec.items。 |
| 14 | + * @ai-context: 边界——无副作用、无内部状态;空态(无节)渲染「暂无笔记」与拆分前同款。 |
| 15 | + */ |
| 16 | +import type { Note } from "../types"; |
| 17 | +import type { ThemeMode } from "../utils/colorPalette"; |
| 18 | +import { paletteHex } from "../utils/colorPalette"; |
| 19 | +import type { SectionData } from "../utils/noteSectionModel"; |
| 20 | +import NoteListRow from "./NoteListRow"; |
| 21 | +import NoteTreeSection from "./NoteTreeSection"; |
| 22 | + |
| 23 | +interface Props { |
| 24 | + /** 展示节(树=未分组+各组顺次;平铺=单 flat 节) */ |
| 25 | + sections: SectionData[]; |
| 26 | + /** 平铺分支的行源(经备注 ② —— 故意不用 sec.items) */ |
| 27 | + notes: Note[]; |
| 28 | + /** 折叠态(裸键:`String(groupId)` / `"none"`) */ |
| 29 | + groupFolds: Record<string, boolean>; |
| 30 | + groupFilter: number | null; |
| 31 | + /** scope → 手排有序 ids(组头「手排 ↺」徽标判据) */ |
| 32 | + manualOrders: Record<string, number[]>; |
| 33 | + selection: ReadonlySet<number>; |
| 34 | + selectedId: number | null; |
| 35 | + theme: ThemeMode; |
| 36 | + noteColors?: Record<number, string | null>; |
| 37 | + tagColors?: Record<string, string>; |
| 38 | + /** 行单击(批量模式下由 hook 改判勾选) */ |
| 39 | + onOpen: (note: Note) => void; |
| 40 | + /** Ctrl/⌘=加减选;Shift=可见序区间选 */ |
| 41 | + onModifierClick: (note: Note, ctrl: boolean, shift: boolean) => void; |
| 42 | + /** 行间落点(组内手动排序;钩子在父层判定启用/禁入) */ |
| 43 | + onDropOnRow: (ids: number[], targetId: number, before: boolean) => void; |
| 44 | + /** 行右键(父层按多选语义分派单行菜单 / 选集菜单) */ |
| 45 | + onContextMenu: (e: React.MouseEvent, note: Note) => void; |
| 46 | + onOpenSession: (sessionId: number) => void; |
| 47 | + /** 组名点击=过滤切换(父层裁决 决策 1 语义) */ |
| 48 | + onGroupFilterChange?: (id: number | null) => void; |
| 49 | + /** 组折叠翻转(裸键) */ |
| 50 | + onToggleFold: (key: string) => void; |
| 51 | + /** 一键回自动排序(清 note_orders scope) */ |
| 52 | + onResetManual: (scope: string) => void; |
| 53 | + /** 拖入组头 → 归组 */ |
| 54 | + onDropNotes: (ids: number[], groupId: number | null) => void; |
| 55 | + /** 组头空白按下 → 划选起点 */ |
| 56 | + onMarqueeStart: (scope: string) => void; |
| 57 | +} |
| 58 | + |
| 59 | +export default function NoteListBody({ |
| 60 | + sections, notes, groupFolds, groupFilter, manualOrders, selection, selectedId, theme, |
| 61 | + noteColors, tagColors, onOpen, onModifierClick, onDropOnRow, onContextMenu, onOpenSession, |
| 62 | + onGroupFilterChange, onToggleFold, onResetManual, onDropNotes, onMarqueeStart, |
| 63 | +}: Props) { |
| 64 | + const rowAccent = (n: Note) => paletteHex(noteColors?.[n.id] ?? null, theme); |
| 65 | + |
| 66 | + const renderRow = (n: Note) => ( |
| 67 | + <NoteListRow |
| 68 | + key={n.id} |
| 69 | + note={n} |
| 70 | + accent={rowAccent(n)} |
| 71 | + openId={selectedId} |
| 72 | + multiSelected={selection.has(n.id)} |
| 73 | + tagColors={tagColors} |
| 74 | + onOpen={onOpen} |
| 75 | + onModifierClick={onModifierClick} |
| 76 | + dragIds={selection.size > 0 && selection.has(n.id) ? [...selection] : []} |
| 77 | + onDropOnRow={onDropOnRow} |
| 78 | + onOpenSession={onOpenSession} |
| 79 | + onContextMenu={onContextMenu} |
| 80 | + /> |
| 81 | + ); |
| 82 | + |
| 83 | + return ( |
| 84 | + <div style={{ flex: 1, overflowY: "auto" }}> |
| 85 | + {sections.length === 0 && <p style={{ fontSize: 12, color: "#9ca3af", textAlign: "center", marginTop: 24 }}>暂无笔记</p>} |
| 86 | + {sections.map((sec) => ( |
| 87 | + sec.scope === "flat" ? ( |
| 88 | + <div key="flat">{notes.map(renderRow)}</div> |
| 89 | + ) : ( |
| 90 | + <NoteTreeSection |
| 91 | + key={sec.scope} |
| 92 | + title={sec.title} |
| 93 | + count={sec.items.length} |
| 94 | + accent={sec.accent} |
| 95 | + active={sec.groupId === null ? groupFilter === null : groupFilter === sec.groupId} |
| 96 | + folded={groupFolds[sec.groupId == null ? "none" : String(sec.groupId)] === true} |
| 97 | + onToggleFold={() => onToggleFold(sec.groupId == null ? "none" : String(sec.groupId))} |
| 98 | + onSelectTitle={() => onGroupFilterChange?.(sec.groupId === null ? null : (groupFilter === sec.groupId ? null : sec.groupId))} |
| 99 | + manual={!!manualOrders[sec.scope]} |
| 100 | + onResetManual={() => onResetManual(sec.scope)} |
| 101 | + onDropNotes={(ids) => onDropNotes(ids, sec.groupId)} |
| 102 | + onMarqueeStart={() => onMarqueeStart(sec.scope)} |
| 103 | + > |
| 104 | + {sec.items.map(renderRow)} |
| 105 | + </NoteTreeSection> |
| 106 | + ) |
| 107 | + ))} |
| 108 | + </div> |
| 109 | + ); |
| 110 | +} |
0 commit comments