|
| 1 | +# 3.43 右键上下文菜单、优雅退出与模板变量表单增强 |
| 2 | + |
| 3 | +## 概述 |
| 4 | + |
| 5 | +v3.1.3 新增三项功能:场景/部署列表右键上下文菜单、应用优雅退出确认、模板变量类型感知表单控件。同时移除了全局右键禁用功能以避免冲突。 |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 1. 右键上下文菜单 |
| 10 | + |
| 11 | +### 问题 |
| 12 | +用户需要展开场景详情才能访问常用操作(启动/停止/SSH/删除等),操作路径过长。 |
| 13 | + |
| 14 | +### 方案 |
| 15 | +在场景列表行和自定义部署列表行添加 `oncontextmenu` 事件,弹出浮层菜单。 |
| 16 | + |
| 17 | +### 菜单项(根据状态动态显示) |
| 18 | + |
| 19 | +| 场景状态 | 显示菜单项 | |
| 20 | +|---------|-----------| |
| 21 | +| running | SSH 运维 → 查看输出 → \| → 停止 → \| → 克隆 → 标签 → 发送到 AI → \| → 删除 | |
| 22 | +| created/stopped | 启动 → 预览 → \| → 克隆 → 标签 → 发送到 AI → \| → 删除 | |
| 23 | +| error/terminated | 启动 → \| → 克隆 → 标签 → 发送到 AI → \| → 删除 | |
| 24 | +| starting/stopping/removing | 灰色文字"正在xxx..." | |
| 25 | + |
| 26 | +### 实现细节 |
| 27 | + |
| 28 | +**状态管理**(两个组件各自维护): |
| 29 | +```javascript |
| 30 | +let contextMenu = $state({ |
| 31 | + show: false, x: 0, y: 0, |
| 32 | + caseId: null, caseName: '', caseState: '', caseType: '' |
| 33 | +}); |
| 34 | +``` |
| 35 | + |
| 36 | +**打开菜单**: |
| 37 | +```javascript |
| 38 | +function openContextMenu(e, c) { |
| 39 | + e.preventDefault(); |
| 40 | + // 边缘防溢出 |
| 41 | + const x = Math.min(e.clientX, window.innerWidth - 200); |
| 42 | + const y = Math.min(e.clientY, window.innerHeight - 300); |
| 43 | + contextMenu = { show: true, x, y, ... }; |
| 44 | +} |
| 45 | +``` |
| 46 | + |
| 47 | +**操作分发**: |
| 48 | +```javascript |
| 49 | +// 注意:先执行操作,再关闭菜单 |
| 50 | +// 因为操作函数通过闭包读取 contextMenu 的值, |
| 51 | +// 如果先 close 再执行,contextMenu 已被重置为空 |
| 52 | +function ctxAction(fn) { |
| 53 | + fn(); |
| 54 | + closeContextMenu(); |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +**关闭方式**: |
| 59 | +- 点击背景遮罩层 |
| 60 | +- 右键背景遮罩层 |
| 61 | +- 按 Escape 键(通过 onMount 注册 window keydown 监听) |
| 62 | + |
| 63 | +**UI 层级**: |
| 64 | +- 背景遮罩:`z-[100]`,`fixed inset-0` |
| 65 | +- 菜单面板:`z-[101]`,绝对定位到鼠标位置 |
| 66 | + |
| 67 | +### 涉及文件 |
| 68 | + |
| 69 | +| 文件 | 改动 | |
| 70 | +|------|------| |
| 71 | +| `frontend/src/components/Cases/Cases.svelte` | 添加 contextMenu 状态、openContextMenu/closeContextMenu/ctxAction 函数、`<tr>` 添加 oncontextmenu、底部添加菜单 UI | |
| 72 | +| `frontend/src/components/CustomDeployment/CustomDeploymentList.svelte` | 同上,适配 CustomDeployment 的数据结构和操作函数 | |
| 73 | + |
| 74 | +--- |
| 75 | + |
| 76 | +## 2. 优雅退出确认 |
| 77 | + |
| 78 | +### 问题 |
| 79 | +用户在 terraform apply/destroy 进行中时关闭应用,可能导致云资源状态不一致(如资源已创建但状态未写入本地)。 |
| 80 | + |
| 81 | +### 方案 |
| 82 | +使用 Wails v2 的 `OnBeforeClose` 钩子,在窗口关闭前检查是否有活跃操作,有则弹出原生 OS 确认对话框。 |
| 83 | + |
| 84 | +### 活跃操作跟踪 |
| 85 | + |
| 86 | +在 `App` 结构体添加原子计数器: |
| 87 | +```go |
| 88 | +type App struct { |
| 89 | + // ... |
| 90 | + activeOps atomic.Int32 // 追踪进行中的异步操作数 |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +所有异步操作的 goroutine 入口处递增,退出时递减: |
| 95 | +```go |
| 96 | +go func() { |
| 97 | + a.activeOps.Add(1) |
| 98 | + defer a.activeOps.Add(-1) |
| 99 | + defer func() { /* recover + emitRefresh */ }() |
| 100 | + // ... 实际操作 |
| 101 | +}() |
| 102 | +``` |
| 103 | + |
| 104 | +### 被跟踪的操作 |
| 105 | + |
| 106 | +| 文件 | 函数 | 操作 | |
| 107 | +|------|------|------| |
| 108 | +| `app_scene.go` | `StartCase` | terraform apply | |
| 109 | +| `app_scene.go` | `StopCase` | terraform destroy | |
| 110 | +| `app_scene.go` | `RemoveCase` | terraform destroy + 清理 | |
| 111 | +| `app_scene.go` | `CreateAndRunCase` | terraform init + apply | |
| 112 | +| `app_compose.go` | `ComposeUp` | compose 多服务部署 | |
| 113 | +| `app_compose.go` | `ComposeDown` | compose 销毁 | |
| 114 | + |
| 115 | +### 关闭确认对话框 |
| 116 | + |
| 117 | +```go |
| 118 | +func (a *App) beforeClose(ctx context.Context) bool { |
| 119 | + if a.activeOps.Load() > 0 { |
| 120 | + result, _ := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{ |
| 121 | + Type: runtime.QuestionDialog, |
| 122 | + Title: "确认退出", |
| 123 | + Message: "有正在进行的操作...", |
| 124 | + Buttons: []string{"取消", "强制退出"}, |
| 125 | + DefaultButton: "取消", |
| 126 | + CancelButton: "取消", |
| 127 | + }) |
| 128 | + if result != "强制退出" { |
| 129 | + return true // 阻止关闭 |
| 130 | + } |
| 131 | + } |
| 132 | + return false // 允许关闭 |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +**注意**:Wails v2 的 `Buttons` 字段用于自定义按钮文本。`DefaultButton` 和 `CancelButton` 控制默认焦点和 Escape 键行为。返回 `true` 阻止关闭,`false` 允许关闭。 |
| 137 | + |
| 138 | +### 涉及文件 |
| 139 | + |
| 140 | +| 文件 | 改动 | |
| 141 | +|------|------| |
| 142 | +| `app.go` | 添加 `activeOps atomic.Int32` 字段、`HasActiveOperations()` 方法、`beforeClose()` 方法 | |
| 143 | +| `main.go` | 添加 `OnBeforeClose: app.beforeClose` 到 Wails 选项 | |
| 144 | +| `app_scene.go` | 4 个 goroutine 添加 `activeOps.Add(1)` / `defer activeOps.Add(-1)` | |
| 145 | +| `app_compose.go` | 2 个 goroutine 添加计数器 | |
| 146 | +| `i18n/zh.go` | 添加 `app_quit_confirm_title`、`app_quit_confirm_message`、`app_quit_btn_confirm`、`app_quit_btn_cancel` | |
| 147 | +| `i18n/en.go` | 对应英文翻译 | |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +## 3. 模板变量表单增强 |
| 152 | + |
| 153 | +### 问题 |
| 154 | +所有 Terraform 变量(bool/number/string/sensitive)都使用相同的文本输入框,用户体验差且容易输入错误。 |
| 155 | + |
| 156 | +### 方案 |
| 157 | +根据 `variables.tf` 中的 `type` 和 `sensitive` 属性生成对应 UI 控件。 |
| 158 | + |
| 159 | +### 控件映射 |
| 160 | + |
| 161 | +| 变量类型 | 控件 | 说明 | |
| 162 | +|---------|------|------| |
| 163 | +| `bool` | 切换开关(toggle) | 自定义 CSS 实现,值为 "true"/"false" 字符串 | |
| 164 | +| `number` | `<input type="number">` | 原生数字输入 | |
| 165 | +| `string` + `sensitive=true` | `<input type="password">` | 密码遮罩 | |
| 166 | +| `string`(默认) | `<input type="text">` | 普通文本 | |
| 167 | + |
| 168 | +### 类型标签 |
| 169 | +非 string 类型在变量名旁显示灰色小标签(如 `number`、`bool`),帮助用户识别。 |
| 170 | + |
| 171 | +### 后端解析 |
| 172 | + |
| 173 | +在 `app_templates_registry.go` 的 `parseVariablesTf()` 中新增 sensitive 检测: |
| 174 | +```go |
| 175 | +var sensitiveRegex = regexp.MustCompile(`^\s*sensitive\s*=\s*true`) |
| 176 | +``` |
| 177 | + |
| 178 | +`TemplateVariable` 结构体(`app.go`)新增: |
| 179 | +```go |
| 180 | +type TemplateVariable struct { |
| 181 | + // ... |
| 182 | + Sensitive bool `json:"sensitive"` |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +### Bool 默认值初始化 |
| 187 | + |
| 188 | +加载模板变量时,bool 类型默认值为 `"false"`(如果没有 default),确保切换开关初始状态正确: |
| 189 | +```javascript |
| 190 | +if (v.type === 'bool' && !variableValues[v.name]) { |
| 191 | + variableValues[v.name] = v.defaultValue || 'false'; |
| 192 | +} |
| 193 | +``` |
| 194 | + |
| 195 | +### 值序列化 |
| 196 | + |
| 197 | +所有变量值始终序列化为字符串(`String(value)`),因为后端使用 `map[string]string` 写入 `terraform.tfvars`。bool/number 值以 `"true"/"false"/"42"` 形式传递,Terraform 可以正确解析。 |
| 198 | + |
| 199 | +### 涉及文件 |
| 200 | + |
| 201 | +| 文件 | 改动 | |
| 202 | +|------|------| |
| 203 | +| `app.go` | TemplateVariable 添加 `Sensitive bool` 字段 | |
| 204 | +| `app_templates_registry.go` | parseVariablesTf 添加 sensitiveRegex 和解析逻辑 | |
| 205 | +| `frontend/src/components/Cases/Cases.svelte` | 变量表单:bool→toggle、number→number input、sensitive→password | |
| 206 | +| `frontend/src/components/CustomDeployment/ConfigEditor.svelte` | 必选参数和可选参数两个区域均添加类型感知控件 | |
| 207 | +| `frontend/src/components/LocalTemplates/LocalTemplates.svelte` | 只读表格:添加 sensitive 徽章、默认值遮罩 | |
| 208 | + |
| 209 | +### 当前模板变量类型分布 |
| 210 | + |
| 211 | +通过扫描所有模板的 variables.tf 统计: |
| 212 | +- string: 93 个 |
| 213 | +- number: 8 个 |
| 214 | +- bool: 2 个 |
| 215 | +- list/map: 0 个(暂无,未来可扩展 KV 编辑器) |
| 216 | + |
| 217 | +--- |
| 218 | + |
| 219 | +## 4. 移除右键禁用功能 |
| 220 | + |
| 221 | +### 问题 |
| 222 | +全局右键禁用(`window.addEventListener('contextmenu', e.preventDefault()))`与新增的场景右键菜单功能冲突。 |
| 223 | + |
| 224 | +### 方案 |
| 225 | +完全移除右键禁用功能链路: |
| 226 | + |
| 227 | +| 删除位置 | 内容 | |
| 228 | +|---------|------| |
| 229 | +| `App.svelte` | `rightClickDisabled` 状态变量、`rightClickDisabledSync` 同步变量、`$effect` 同步逻辑、`contextmenu` 全局拦截监听、`GetDisableRightClick` 调用 | |
| 230 | +| `Settings.svelte` | `rightClickSaving` 状态、`handleToggleRightClick` 函数、右键菜单开关 UI、`SetDisableRightClick` 导入 | |
| 231 | + |
| 232 | +**注意**:后端 Go 代码(`SetDisableRightClick`/`GetDisableRightClick`/`disableRightClick` 字段/`DisableRightClick` 配置项)保留不动,因为删除会破坏现有配置文件兼容性,且不影响功能。 |
| 233 | + |
| 234 | +--- |
| 235 | + |
| 236 | +## 注意事项 |
| 237 | + |
| 238 | +1. **ctxAction 执行顺序**:必须先执行操作函数再关闭菜单。因为操作函数通过闭包引用 `contextMenu` 状态对象,如果先 close 则所有字段已被重置为空值。 |
| 239 | +2. **activeOps 使用 atomic.Int32**:不使用 sync.Mutex,因为需要在 `beforeClose`(主线程)和 goroutine 中无锁访问。 |
| 240 | +3. **Bool 开关实现**:使用 `<button>` + CSS 自定义样式(relative/absolute positioned div),而非原生 `<input type="checkbox">`,保持设计系统一致。 |
| 241 | +4. **变量值始终为字符串**:前端 `variableValues` 是 `map[string]string`,bool 值存为 `"true"/"false"`,number 值存为数字字符串,后端直接写入 tfvars。 |
| 242 | +5. **TemplateVariable 双定义**:`app.go` 和 `mod/custom_deployment.go` 各有一个 TemplateVariable 结构体,JSON tag 不同(`defaultValue` vs `default_value`)。Sensitive 字段仅加在 `app.go` 中,因为 CustomDeployment 的变量表单走的是相同的前端解析路径。 |
0 commit comments