|
| 1 | +/** |
| 2 | + * NoteListBatchMenu — 批量操作栏 + 选集批处理菜单(批 0-C2·Task 2 自 NoteListView 抽出)。 |
| 3 | + * |
| 4 | + * @ai-context: 两个**受控展示件**同置一个模块(共享按钮基底与同一批处理语义): |
| 5 | + * ① `NoteListBatchBar`——选集非空时浮出的底部条(移动到组 / 批量删除 / |
| 6 | + * 完成 / 取消);② `NoteListBatchContextMenu`——行右键选集或批量栏唤起的 |
| 7 | + * 菜单,含二级「移动到组」视图(全部组 + 移出组)。 |
| 8 | + * @ai-context: 做成受控件的理由(R8 风险):菜单态与 **唯一 Esc handler** 的优先级链 |
| 9 | + * (batchMenu → contextMenu → 批量模式)留在 hooks/useNoteListSelection。 |
| 10 | + * 若本模块自带 window keydown(照抄 NoteRowContextMenu 的范式),会出现两个 |
| 11 | + * window 监听同时判定,行为依赖 React 批处理时序。⇒ 本模块**零状态、 |
| 12 | + * 零副作用、零 invoke**。 |
| 13 | + * @ai-context: 文案与 data-testid 是测试契约(`batch-move-btn` / `batch-delete-btn` / |
| 14 | + * `batch-done-btn` / `batch-context-menu` / `batch-menu-delete`,以及 |
| 15 | + * 「已选 N 个」「📁 移动到组…」「删除选中(N)」「清除选择」「完成」 |
| 16 | + * 「取消」「(移出组)」「📁 {组名}」「← 返回」——测试用 getByText 精确匹配, |
| 17 | + * 含全角括号与省略号),**逐字保留**。zIndex 40/41 与拆分前一致 |
| 18 | + * (本批不迁移到 ADR-032 的 zIndex 标尺);`busyMove` 禁用态同前。 |
| 19 | + */ |
| 20 | +import type { NoteGroup } from "../types"; |
| 21 | +import { ghostBtn } from "./NoteListToolbar"; |
| 22 | + |
| 23 | +interface BatchBarProps { |
| 24 | + /** 选集大小(「已选 N 个」) */ |
| 25 | + count: number; |
| 26 | + /** 批量选择模式(决定是否显示「完成」) */ |
| 27 | + selectionMode: boolean; |
| 28 | + /** 打开选集菜单(父层以选集构造 batchMenu,坐标为 0/0 → 由菜单回退到 12/12) */ |
| 29 | + onMoveToGroup: () => void; |
| 30 | + /** 批量删除(父层确认;成功才清选) */ |
| 31 | + onDelete: () => void; |
| 32 | + /** 退出选择模式并清选 */ |
| 33 | + onDone: () => void; |
| 34 | + /** 仅清选(保留模式) */ |
| 35 | + onCancel: () => void; |
| 36 | +} |
| 37 | + |
| 38 | +/** 批量操作栏(去勾选框后的批量入口:删除 + 移动到组;选择模式下同样可用) */ |
| 39 | +export function NoteListBatchBar({ count, selectionMode, onMoveToGroup, onDelete, onDone, onCancel }: BatchBarProps) { |
| 40 | + return ( |
| 41 | + <div style={{ borderTop: "1px solid #e5e7eb", padding: 8, display: "flex", gap: 6, alignItems: "center", background: "#fff" }}> |
| 42 | + <span style={{ fontSize: 12, color: "#3730a3" }}>已选 {count} 个</span> |
| 43 | + <button data-testid="batch-move-btn" style={ghostBtn} onClick={onMoveToGroup}>移动到组</button> |
| 44 | + <button data-testid="batch-delete-btn" style={{ ...ghostBtn, borderColor: "#fca5a5", color: "#dc2626" }} onClick={onDelete}>批量删除</button> |
| 45 | + {selectionMode && <button data-testid="batch-done-btn" style={{ ...ghostBtn, color: "#3730a3" }} onClick={onDone}>完成</button>} |
| 46 | + <button style={{ ...ghostBtn, marginLeft: "auto" }} onClick={onCancel}>取消</button> |
| 47 | + </div> |
| 48 | + ); |
| 49 | +} |
| 50 | + |
| 51 | +interface BatchMenuProps { |
| 52 | + /** 选集 ids(菜单标题「已选 N 个」与「删除选中(N)」) */ |
| 53 | + ids: number[]; |
| 54 | + x: number; |
| 55 | + y: number; |
| 56 | + /** 二级「移动到组」视图开关(父层持有——Esc 优先级链同闭包) */ |
| 57 | + moveOpen: boolean; |
| 58 | + groups: NoteGroup[]; |
| 59 | + /** 归组进行中:二级视图全部按钮禁用 */ |
| 60 | + busyMove: boolean; |
| 61 | + /** 收起菜单(背板点击) */ |
| 62 | + onClose: () => void; |
| 63 | + onOpenMove: () => void; |
| 64 | + onDelete: () => void; |
| 65 | + /** 清除选择(收起菜单 + 清选集) */ |
| 66 | + onClearSelection: () => void; |
| 67 | + /** 归入目标组(null=移出组) */ |
| 68 | + onMoveToGroup: (groupId: number | null) => void; |
| 69 | + /** 从二级视图返回一级 */ |
| 70 | + onBack: () => void; |
| 71 | +} |
| 72 | + |
| 73 | +/** 选集批处理菜单(删除/移动到组)——透明背板 + 面板,整体一个 fragment(无额外 DOM 层) */ |
| 74 | +export function NoteListBatchContextMenu({ |
| 75 | + ids, x, y, moveOpen, groups, busyMove, onClose, onOpenMove, onDelete, onClearSelection, onMoveToGroup, onBack, |
| 76 | +}: BatchMenuProps) { |
| 77 | + return ( |
| 78 | + <> |
| 79 | + <div style={{ position: "fixed", inset: 0, zIndex: 40 }} onClick={onClose} /> |
| 80 | + <div data-testid="batch-context-menu" style={{ position: "fixed", zIndex: 41, left: x || 12, top: y || 12, minWidth: 180, background: "#fff", border: "1px solid #e5e7eb", borderRadius: 8, boxShadow: "0 6px 18px rgba(0,0,0,0.12)", padding: 6, fontSize: 12 }}> |
| 81 | + <div style={{ padding: "2px 6px", color: "#9ca3af", fontSize: 11 }}>已选 {ids.length} 个</div> |
| 82 | + {!moveOpen ? ( |
| 83 | + <> |
| 84 | + <button style={{ ...ghostBtn, width: "100%", marginTop: 4, textAlign: "left" }} disabled={busyMove} onClick={onOpenMove}>📁 移动到组…</button> |
| 85 | + <button data-testid="batch-menu-delete" style={{ ...ghostBtn, width: "100%", marginTop: 4, textAlign: "left", borderColor: "#fca5a5", color: "#dc2626" }} disabled={busyMove} onClick={onDelete}>删除选中({ids.length})</button> |
| 86 | + <button style={{ ...ghostBtn, width: "100%", marginTop: 4, textAlign: "left" }} onClick={onClearSelection}>清除选择</button> |
| 87 | + </> |
| 88 | + ) : ( |
| 89 | + <div style={{ display: "flex", flexDirection: "column", gap: 2, maxHeight: 260, overflowY: "auto" }}> |
| 90 | + <button style={{ ...ghostBtn, textAlign: "left" }} disabled={busyMove} onClick={() => onMoveToGroup(null)}>(移出组)</button> |
| 91 | + {groups.map((g) => ( |
| 92 | + <button key={g.id} style={{ ...ghostBtn, textAlign: "left" }} disabled={busyMove} onClick={() => onMoveToGroup(g.id)}>📁 {g.name}</button> |
| 93 | + ))} |
| 94 | + <button style={{ border: "none", background: "none", color: "#6b7280", cursor: "pointer", textAlign: "left" }} onClick={onBack}>← 返回</button> |
| 95 | + </div> |
| 96 | + )} |
| 97 | + </div> |
| 98 | + </> |
| 99 | + ); |
| 100 | +} |
0 commit comments