Skip to content

Commit b41ce53

Browse files
committed
feat(WebServer): enhance quick command group management with selective import/export
- Add group metadata storage for persistent group ordering - Add group rename, delete, and drag-to-reorder support - Add command item drag-to-reorder and cross-group drag - Add group context menu (rename/delete) on right-click - Replace bulk export with selective export dialog (checkbox tree) - Replace bulk import with preview dialog and conflict handling (per-item skip/overwrite, global strategy switch) - Add i18n keys for en, zh-CN, zh-TW - Add design doc: quick-command-group-enhancement.md - Rewrite and expand test suite (1798 tests, 80%+ coverage)
1 parent 32d4634 commit b41ce53

7 files changed

Lines changed: 3720 additions & 1473 deletions

File tree

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
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

Comments
 (0)