|
| 1 | +# 快捷指令分组功能增强方案 |
| 2 | + |
| 3 | +## 1. 问题描述 |
| 4 | + |
| 5 | +当前分组只是渲染时按 `cmd.group` 字符串聚合的"虚拟分组",没有独立元数据,导致: |
| 6 | + |
| 7 | +| 缺陷 | 现状 | |
| 8 | +|------|------| |
| 9 | +| 不可重命名 | 只能逐个移动命令到新分组间接实现 | |
| 10 | +| 不可拖动排序 | 顺序由 `Object.entries()` 插入顺序决定 | |
| 11 | +| 不可删除 | 只能逐个移出命令后分组自动消失 | |
| 12 | +| 命令不可拖拽排序 | 命令项无拖拽能力(仅宏步骤有) | |
| 13 | + |
| 14 | +## 2. 整改方案 |
| 15 | + |
| 16 | +### 2.1 数据模型变更 |
| 17 | + |
| 18 | +新增分组元数据存储,localStorage key `fpbinject-quick-command-groups`: |
| 19 | + |
| 20 | +```javascript |
| 21 | +[ |
| 22 | + { name: "System", order: 0 }, |
| 23 | + { name: "Init", order: 1 } |
| 24 | +] |
| 25 | +``` |
| 26 | + |
| 27 | +命令仍通过 `cmd.group` 字符串关联。 |
| 28 | + |
| 29 | +导出格式直接改为 version 2,不兼容旧格式: |
| 30 | + |
| 31 | +```json |
| 32 | +{ |
| 33 | + "version": 2, |
| 34 | + "groups": [ |
| 35 | + { "name": "System", "order": 0 }, |
| 36 | + { "name": "Init", "order": 1 } |
| 37 | + ], |
| 38 | + "commands": [ ... ] |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +### 2.2 新增函数 |
| 43 | + |
| 44 | +```javascript |
| 45 | +// 分组元数据 |
| 46 | +function loadGroupMeta() |
| 47 | +function saveGroupMeta(groups) |
| 48 | +function ensureGroupMeta(commands) |
| 49 | + |
| 50 | +// 分组操作 |
| 51 | +function renameGroup(oldName) |
| 52 | +function deleteGroup(groupName) |
| 53 | +function showGroupContextMenu(event, name) |
| 54 | +function qcGroupContextAction(action) |
| 55 | + |
| 56 | +// 拖拽排序 |
| 57 | +function initCommandDrag() |
| 58 | +function initGroupDrag() |
| 59 | + |
| 60 | +// 选择性导入/导出 |
| 61 | +function openExportDialog() // 打开导出选择对话框 |
| 62 | +function openImportDialog(fileData) // 打开导入预览 + 冲突处理对话框 |
| 63 | +function resolveImportConflicts(incoming, existing) // 检测并返回冲突列表 |
| 64 | +``` |
| 65 | + |
| 66 | +### 2.3 修改函数 |
| 67 | + |
| 68 | +| 函数 | 修改内容 | |
| 69 | +|------|----------| |
| 70 | +| `renderQuickCommands()` | 按 groupMeta order 排序;分组 header 加右键菜单和拖拽手柄;命令项加拖拽手柄 | |
| 71 | +| `saveQuickCommand()` | 新分组时同步追加 groupMeta | |
| 72 | +| `moveToGroup()` | 同步更新 groupMeta | |
| 73 | +| `clearAllQuickCommands()` | 同时清空 groupMeta | |
| 74 | +| `exportQuickCommands()` | 改为调用 `openExportDialog()` | |
| 75 | +| `importQuickCommands()` | 文件读取后调用 `openImportDialog()` | |
| 76 | + |
| 77 | +### 2.4 UI 变更 |
| 78 | + |
| 79 | +#### 分组 header 增强 |
| 80 | + |
| 81 | +``` |
| 82 | +现有: ▼ 📁 System (仅折叠) |
| 83 | +改为: ≡ ▼ 📁 System [⋯] (拖拽 + 右键菜单) |
| 84 | +``` |
| 85 | + |
| 86 | +#### 命令项增强 |
| 87 | + |
| 88 | +``` |
| 89 | +现有: ▸ ps -A [▶] [⋯] |
| 90 | +改为: ≡ ▸ ps -A [▶] [⋯] (新增拖拽手柄) |
| 91 | +``` |
| 92 | + |
| 93 | +#### 分组右键菜单(新增 `qcGroupContextMenu`) |
| 94 | + |
| 95 | +```html |
| 96 | +<div class="qc-context-menu" id="qcGroupContextMenu" style="display: none"> |
| 97 | + <div class="qc-context-item" onclick="qcGroupContextAction('rename')"> |
| 98 | + <i class="codicon codicon-edit"></i> |
| 99 | + <span data-i18n="quick_commands.rename_group">Rename Group</span> |
| 100 | + </div> |
| 101 | + <div class="qc-context-separator"></div> |
| 102 | + <div class="qc-context-item danger" onclick="qcGroupContextAction('delete')"> |
| 103 | + <i class="codicon codicon-trash"></i> |
| 104 | + <span data-i18n="quick_commands.delete_group">Delete Group</span> |
| 105 | + </div> |
| 106 | +</div> |
| 107 | +``` |
| 108 | + |
| 109 | +#### 导出选择对话框 |
| 110 | + |
| 111 | +点击 Export 后弹出模态框,按分组展示 checkbox 树: |
| 112 | + |
| 113 | +``` |
| 114 | +┌─────────────────────────────────────────┐ |
| 115 | +│ Export Commands ✕ │ |
| 116 | +├─────────────────────────────────────────┤ |
| 117 | +│ │ |
| 118 | +│ Select commands to export: │ |
| 119 | +│ │ |
| 120 | +│ ☑ Select All │ |
| 121 | +│ │ |
| 122 | +│ ☑ 📁 System │ |
| 123 | +│ ☑ ps -A │ |
| 124 | +│ ☑ free │ |
| 125 | +│ ☐ top -n 1 │ |
| 126 | +│ ☑ 📁 Init Sequence │ |
| 127 | +│ ☑ [Macro] Init Sequence │ |
| 128 | +│ ☑ (Ungrouped) │ |
| 129 | +│ ☑ reboot │ |
| 130 | +│ │ |
| 131 | +├─────────────────────────────────────────┤ |
| 132 | +│ Selected: 4 / 5 commands │ |
| 133 | +│ [Cancel] [Export] │ |
| 134 | +└─────────────────────────────────────────┘ |
| 135 | +``` |
| 136 | + |
| 137 | +交互细节: |
| 138 | +- 勾选分组 checkbox → 全选/全不选该分组下所有命令 |
| 139 | +- 子命令部分勾选时分组 checkbox 显示 indeterminate 状态(`[-]`) |
| 140 | +- "Select All" 控制全局 |
| 141 | +- 底部实时显示已选数量 |
| 142 | +- 导出时只包含选中命令及其所属分组 |
| 143 | + |
| 144 | +#### 导入预览 + 冲突处理对话框 |
| 145 | + |
| 146 | +选择文件后弹出模态框,展示即将导入的内容和冲突: |
| 147 | + |
| 148 | +``` |
| 149 | +┌─────────────────────────────────────────┐ |
| 150 | +│ Import Commands ✕ │ |
| 151 | +├─────────────────────────────────────────┤ |
| 152 | +│ │ |
| 153 | +│ File: my_commands.json (5 commands) │ |
| 154 | +│ │ |
| 155 | +│ ☑ 📁 System │ |
| 156 | +│ ⚠ ps -A [Skip] [Overwrite] │ ← 冲突:同名命令已存在 |
| 157 | +│ ☑ free │ ← 新命令,直接导入 |
| 158 | +│ ☑ 📁 Debug │ |
| 159 | +│ ☑ trace on │ |
| 160 | +│ ☑ trace off │ |
| 161 | +│ ☑ (Ungrouped) │ |
| 162 | +│ ☑ reboot [Skip] [Overwrite] │ ← 冲突 |
| 163 | +│ │ |
| 164 | +│ ─────────────────────────────────────── │ |
| 165 | +│ Conflict strategy: │ |
| 166 | +│ (●) Ask per item (above) │ |
| 167 | +│ ( ) Skip all duplicates │ |
| 168 | +│ ( ) Overwrite all duplicates │ |
| 169 | +│ │ |
| 170 | +├─────────────────────────────────────────┤ |
| 171 | +│ New: 3 Conflicts: 2 Skip: 0 │ |
| 172 | +│ [Cancel] [Import] │ |
| 173 | +└─────────────────────────────────────────┘ |
| 174 | +``` |
| 175 | + |
| 176 | +交互细节: |
| 177 | +- 冲突检测规则:`name` + `type` + `command`(单条)或 `name` + `type`(宏)相同视为重复 |
| 178 | +- 每个冲突项可单独选择 Skip / Overwrite |
| 179 | +- 底部提供全局策略快捷切换(切换后批量更新所有冲突项状态) |
| 180 | +- 非冲突项默认勾选导入,也可取消 |
| 181 | +- 分组冲突(同名分组已存在):自动合并,命令追加到已有分组末尾 |
| 182 | +- 底部统计实时更新 |
| 183 | + |
| 184 | +### 2.5 i18n 新增 key |
| 185 | + |
| 186 | +``` |
| 187 | +quick_commands.rename_group → "Rename Group" / "重命名分组" |
| 188 | +quick_commands.delete_group → "Delete Group" / "删除分组" |
| 189 | +quick_commands.confirm_delete_group → "Delete group \"{{name}}\"? Commands will be ungrouped." |
| 190 | +quick_commands.rename_prompt → "Enter new group name:" |
| 191 | +quick_commands.drag_command → "Drag to reorder" |
| 192 | +quick_commands.export_title → "Export Commands" / "导出指令" |
| 193 | +quick_commands.import_title → "Import Commands" / "导入指令" |
| 194 | +quick_commands.select_all → "Select All" / "全选" |
| 195 | +quick_commands.selected_count → "Selected: {{selected}} / {{total}} commands" |
| 196 | +quick_commands.conflict_found → "Conflicts: {{count}}" |
| 197 | +quick_commands.conflict_skip → "Skip" / "跳过" |
| 198 | +quick_commands.conflict_overwrite → "Overwrite" / "覆盖" |
| 199 | +quick_commands.strategy_per_item → "Ask per item" / "逐项选择" |
| 200 | +quick_commands.strategy_skip_all → "Skip all duplicates" / "跳过所有重复" |
| 201 | +quick_commands.strategy_overwrite_all → "Overwrite all duplicates" / "覆盖所有重复" |
| 202 | +quick_commands.import_summary → "New: {{new}} Conflicts: {{conflicts}} Skip: {{skip}}" |
| 203 | +quick_commands.ungrouped → "(Ungrouped)" / "(未分组)" |
| 204 | +``` |
| 205 | + |
| 206 | +## 3. 实现计划 |
| 207 | + |
| 208 | +| 阶段 | 内容 | 工作量 | |
| 209 | +|------|------|--------| |
| 210 | +| S1 | 分组元数据存储 + load/save/ensure | 0.5d | |
| 211 | +| S2 | 分组重命名 + 删除 + 右键菜单 | 0.5d | |
| 212 | +| S3 | 分组拖拽排序 + 命令项拖拽排序 | 0.5d | |
| 213 | +| S4 | 选择性导出对话框 | 0.5d | |
| 214 | +| S5 | 导入预览 + 冲突处理对话框 | 1d | |
| 215 | +| S6 | i18n + 测试 | 0.5d | |
| 216 | + |
| 217 | +总计 **3.5 人天**。 |
| 218 | + |
| 219 | +## 4. 测试用例 |
| 220 | + |
| 221 | +### 4.1 分组元数据存储 |
| 222 | + |
| 223 | +| # | 用例 | 操作 | 预期结果 | |
| 224 | +|---|------|------|----------| |
| 225 | +| T01 | loadGroupMeta 无数据 | localStorage 无 key | 返回 `[]` | |
| 226 | +| T02 | loadGroupMeta 异常 JSON | 存入非法 JSON | 返回 `[]`,不抛异常 | |
| 227 | +| T03 | saveGroupMeta 正确存储 | 保存 `[{name:"A",order:0}]` | localStorage 写入正确 | |
| 228 | +| T04 | saveGroupMeta 处理异常 | setItem 抛异常 | 不崩溃 | |
| 229 | +| T05 | ensureGroupMeta 补全缺失 | 命令有 group="X" 但元数据无 | 自动追加 | |
| 230 | +| T06 | ensureGroupMeta 清理孤立 | 元数据有 "Y" 但无命令引用 | 移除 "Y" | |
| 231 | + |
| 232 | +### 4.2 分组重命名 |
| 233 | + |
| 234 | +| # | 用例 | 操作 | 预期结果 | |
| 235 | +|---|------|------|----------| |
| 236 | +| T07 | 正常重命名 | 输入 "NewName" 确认 | 分组名和所有命令 group 字段同步更新 | |
| 237 | +| T08 | 重命名为空串 | 输入空串 | 不执行,保持原名 | |
| 238 | +| T09 | 重命名为已有分组名 | 输入 "System" | 两组合并 | |
| 239 | +| T10 | 取消重命名 | prompt 返回 null | 无操作 | |
| 240 | +| T11 | 重命名后 UI 刷新 | 完成重命名 | 侧边栏立即更新 | |
| 241 | + |
| 242 | +### 4.3 分组删除 |
| 243 | + |
| 244 | +| # | 用例 | 操作 | 预期结果 | |
| 245 | +|---|------|------|----------| |
| 246 | +| T12 | 正常删除 | 确认删除 | 分组消失,命令移至未分组 | |
| 247 | +| T13 | 取消删除 | confirm 返回 false | 无操作 | |
| 248 | +| T14 | 删除后命令 group 清空 | 删除 "Init" | 原属命令 group 变 null | |
| 249 | +| T15 | 删除后元数据同步 | 删除 | groupMeta 移除对应条目 | |
| 250 | + |
| 251 | +### 4.4 分组拖拽排序 |
| 252 | + |
| 253 | +| # | 用例 | 操作 | 预期结果 | |
| 254 | +|---|------|------|----------| |
| 255 | +| T16 | 拖拽分组到新位置 | 拖 "Init" 到 "System" 上方 | 顺序变为 Init → System,order 更新 | |
| 256 | +| T17 | 刷新后保持顺序 | 拖拽后刷新 | 顺序不变 | |
| 257 | +| T18 | 拖拽不影响命令归属 | 拖拽分组 | 命令 group 字段不变 | |
| 258 | + |
| 259 | +### 4.5 命令项拖拽排序 |
| 260 | + |
| 261 | +| # | 用例 | 操作 | 预期结果 | |
| 262 | +|---|------|------|----------| |
| 263 | +| T19 | 同分组内排序 | 拖 "ps" 到 "free" 下方 | order 更新 | |
| 264 | +| T20 | 跨分组拖拽 | 拖 "ps" 从 "System" 到 "Init" | group 变为 "Init" | |
| 265 | +| T21 | 拖到未分组区域 | 拖出分组 | group 变 null | |
| 266 | +| T22 | 从未分组拖入分组 | 拖入 "System" | group 变 "System" | |
| 267 | +| T23 | 拖拽后持久化 | 刷新页面 | 顺序保持 | |
| 268 | + |
| 269 | +### 4.6 分组右键菜单 |
| 270 | + |
| 271 | +| # | 用例 | 操作 | 预期结果 | |
| 272 | +|---|------|------|----------| |
| 273 | +| T24 | 右键显示菜单 | 右键分组 header | 显示 Rename / Delete 菜单 | |
| 274 | +| T25 | 菜单定位正确 | 右键 | left/top = clientX/clientY | |
| 275 | +| T26 | 超出视口自动调整 | 右下角右键 | 不超出 window | |
| 276 | +| T27 | 点击空白关闭 | 点击其他区域 | 菜单隐藏 | |
| 277 | +| T28 | 与命令菜单互斥 | 先开命令菜单再右键分组 | 命令菜单关闭 | |
| 278 | + |
| 279 | +### 4.7 选择性导出 |
| 280 | + |
| 281 | +| # | 用例 | 操作 | 预期结果 | |
| 282 | +|---|------|------|----------| |
| 283 | +| T29 | 打开导出对话框 | 点击 Export | 弹出模态框,所有命令默认勾选 | |
| 284 | +| T30 | 全选/全不选 | 点击 "Select All" | 所有 checkbox 联动切换 | |
| 285 | +| T31 | 分组 checkbox 联动 | 取消勾选分组 | 该分组下所有命令取消勾选 | |
| 286 | +| T32 | 子项部分勾选 | 勾选分组下部分命令 | 分组 checkbox 显示 indeterminate | |
| 287 | +| T33 | 选中计数实时更新 | 勾选/取消 | 底部 "Selected: x / y" 实时变化 | |
| 288 | +| T34 | 导出选中项 | 勾选 3/5 个命令后点 Export | JSON 只含 3 个命令及其所属分组 | |
| 289 | +| T35 | 未选中分组不导出 | 分组下命令全部取消 | 导出 JSON 的 groups 不含该分组 | |
| 290 | +| T36 | 空选择禁止导出 | 全部取消勾选 | Export 按钮禁用 | |
| 291 | +| T37 | 无命令时提示 | 命令列表为空 | 直接提示无可导出内容,不弹对话框 | |
| 292 | + |
| 293 | +### 4.8 导入预览与冲突处理 |
| 294 | + |
| 295 | +| # | 用例 | 操作 | 预期结果 | |
| 296 | +|---|------|------|----------| |
| 297 | +| T38 | 打开导入预览 | 选择有效 JSON 文件 | 弹出预览对话框,按分组展示命令 | |
| 298 | +| T39 | 无冲突全部导入 | 导入全新命令 | 所有项默认勾选,无冲突标记,直接导入 | |
| 299 | +| T40 | 检测同名冲突 | 导入含已有同名命令的文件 | 冲突项显示 ⚠ 和 Skip/Overwrite 按钮 | |
| 300 | +| T41 | 逐项选择 Skip | 点击冲突项的 Skip | 该项标记为跳过,统计更新 | |
| 301 | +| T42 | 逐项选择 Overwrite | 点击冲突项的 Overwrite | 该项标记为覆盖,统计更新 | |
| 302 | +| T43 | 全局策略:Skip all | 选择 "Skip all duplicates" | 所有冲突项批量设为 Skip | |
| 303 | +| T44 | 全局策略:Overwrite all | 选择 "Overwrite all duplicates" | 所有冲突项批量设为 Overwrite | |
| 304 | +| T45 | 全局策略切回逐项 | 选择 "Ask per item" | 恢复各冲突项独立状态 | |
| 305 | +| T46 | 取消勾选非冲突项 | 取消某个新命令的 checkbox | 该命令不导入 | |
| 306 | +| T47 | Skip 执行结果 | 导入时冲突项选 Skip | 本地已有命令保持不变 | |
| 307 | +| T48 | Overwrite 执行结果 | 导入时冲突项选 Overwrite | 本地命令被导入数据覆盖(保留本地 id) | |
| 308 | +| T49 | 同名分组自动合并 | 导入含已有分组名的数据 | 命令追加到已有分组末尾,不创建重复分组 | |
| 309 | +| T50 | 导入新分组 | 导入含本地不存在的分组 | groupMeta 追加新分组 | |
| 310 | +| T51 | 底部统计正确 | 操作冲突选项 | "New / Conflicts / Skip" 数字实时正确 | |
| 311 | +| T52 | 空文件处理 | 导入 commands 为空数组的文件 | 提示无可导入内容 | |
| 312 | +| T53 | 非法文件处理 | 导入非 JSON 或缺少 commands 字段 | 提示格式错误 | |
| 313 | + |
| 314 | +### 4.9 边界与回归 |
| 315 | + |
| 316 | +| # | 用例 | 操作 | 预期结果 | |
| 317 | +|---|------|------|----------| |
| 318 | +| T54 | 无分组时无分组菜单入口 | 全部未分组 | 无分组 header | |
| 319 | +| T55 | clearAll 清空 groupMeta | Clear All | 两个 key 均清空 | |
| 320 | +| T56 | 新建命令选已有分组 | 下拉选 "System" | groupMeta 不变 | |
| 321 | +| T57 | 新建命令创建新分组 | 输入 "Debug" | groupMeta 追加 | |
| 322 | +| T58 | 特殊字符分组名 | 名为 `<script>` | HTML 转义正确,无 XSS | |
0 commit comments