From 31ad35780a4f3c425dc7d97b03ee5fb00070d917 Mon Sep 17 00:00:00 2001 From: xiaosi <2652281683@qq.com> Date: Fri, 28 Aug 2026 14:35:42 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=A2=9E=E5=8A=A0=E7=9B=91=E6=8E=A7?= =?UTF-8?q?=E9=A1=B5=E6=8C=87=E4=BB=A4=E8=BF=9B=E5=BA=A6=E6=B5=AE=E7=AA=97?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 拆分 URL、轮询状态机、浮窗 UI 与浏览器冒烟四步, 对齐 sent/acked/timeout/terminal 状态展示。 --- .../2026-08-28-command-progress-float.md | 425 ++++++++++++++++++ 1 file changed, 425 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-28-command-progress-float.md diff --git a/docs/superpowers/plans/2026-08-28-command-progress-float.md b/docs/superpowers/plans/2026-08-28-command-progress-float.md new file mode 100644 index 0000000..80de4e0 --- /dev/null +++ b/docs/superpowers/plans/2026-08-28-command-progress-float.md @@ -0,0 +1,425 @@ +# Command Progress Float Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 监控页下发指令后弹出当前指令进度浮窗,轮询 `/v1/commands/:id` 逐步展示 `sent/acked/timeout/terminal`,并同步按钮旁状态文案。 + +**Architecture:** 在 `MonitorView` 本地维护 `commandProgress` 状态;`runCommand` 使用 `sendCommand` 返回的 `cmd.id` 启动轮询;右下角非模态浮窗展示三步进度;同 title 按钮 `small` 文案跟随状态。不改后端协议。 + +**Tech Stack:** Vue 3、现有 `request`/`urls`、`devicesStore.sendCommand`、`prototype.css` + +**Spec:** `docs/superpowers/specs/2026-08-28-command-progress-float-design.md` + +--- + +### Task 1: 增加指令详情 URL + +**Files:** +- Modify: `src/config/urls.js` + +- [ ] **Step 1: 增加 `COMMAND(id)`** + +在 `// Commands` 段: + +```js +// Commands +export const COMMANDS = '/v1/commands' +export const COMMAND = (id) => `/v1/commands/${id}` +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/config/urls.js +git commit -m "feat: 增加单条指令查询 URL" +``` + +--- + +### Task 2: Monitor 指令进度状态机与轮询 + +**Files:** +- Modify: `src/views/MonitorView/MonitorView.vue` + +- [ ] **Step 1: 增加 import** + +确保已有: + +```js +import request from '@/utils/http' +import * as urls from '@/config/urls' +``` + +若当前是别的 http 路径,保持项目现有 import 风格(与文件顶部一致),只补 `urls.COMMAND`。 + +- [ ] **Step 2: 增加状态常量与 progress 状态** + +放在 `script setup` 控制相关区域(`runCommand` 附近): + +```js +const COMMAND_STATUS = { + sent: { name: '已下发', step: 2, result: '等待应答', terminal: false }, + acked: { name: '已确认', step: 3, result: '执行成功', terminal: true, ok: true }, + timeout: { name: '已超时', step: 3, result: '已超时', terminal: true, ok: false }, + terminal: { name: '执行失败', step: 3, result: '执行失败', terminal: true, ok: false }, +} + +const commandProgress = reactive({ + open: false, + title: '', + deviceName: '', + commandId: '', + status: '', // sent/acked/timeout/terminal + result: '', + step: 1, // 1 确认下发 2 已下发 3 终态 + error: '', + buttonTitle: '', +}) + +let commandPollTimer = null +let commandAutoCloseTimer = null +let commandPollFails = 0 +``` + +- [ ] **Step 3: 增加 helper** + +```js +function buttonStatusText(title) { + if (commandProgress.buttonTitle !== title || !commandProgress.status) return '状态:待执行' + const meta = COMMAND_STATUS[commandProgress.status] + return meta ? `状态:${meta.name}` : '状态:待执行' +} + +function clearCommandTimers() { + if (commandPollTimer) { + clearInterval(commandPollTimer) + commandPollTimer = null + } + if (commandAutoCloseTimer) { + clearTimeout(commandAutoCloseTimer) + commandAutoCloseTimer = null + } +} + +function closeCommandProgress() { + clearCommandTimers() + commandProgress.open = false +} + +function applyCommandStatus(cmd) { + const status = cmd?.status || 'sent' + const meta = COMMAND_STATUS[status] || COMMAND_STATUS.sent + commandProgress.status = status + commandProgress.step = meta.step + commandProgress.result = cmd?.ackResultCode || meta.result + commandProgress.error = '' + return meta +} + +function scheduleAutoClose() { + if (commandAutoCloseTimer) clearTimeout(commandAutoCloseTimer) + commandAutoCloseTimer = setTimeout(() => { + closeCommandProgress() + }, 5000) +} + +async function pollCommandOnce() { + if (!commandProgress.commandId) return + try { + const cmd = await request.get(urls.COMMAND(commandProgress.commandId)) + commandPollFails = 0 + const meta = applyCommandStatus(cmd) + if (meta.terminal) { + clearInterval(commandPollTimer) + commandPollTimer = null + scheduleAutoClose() + } + } catch (e) { + commandPollFails += 1 + const status = e?.response?.status + if (status === 403 || status === 404 || commandPollFails >= 3) { + clearInterval(commandPollTimer) + commandPollTimer = null + commandProgress.error = + status === 403 + ? '无权限查看进度,请稍后在操作记录确认' + : status === 404 + ? '未找到指令记录' + : (e.message || '进度查询失败') + scheduleAutoClose() + } + } +} + +function startCommandProgress(cmd, title, deviceName) { + clearCommandTimers() + commandPollFails = 0 + commandProgress.open = true + commandProgress.title = title + commandProgress.deviceName = deviceName || '--' + commandProgress.commandId = String(cmd?.id || '') + commandProgress.buttonTitle = title + commandProgress.step = 1 + commandProgress.error = '' + applyCommandStatus(cmd || { status: 'sent' }) + + if (!commandProgress.commandId) { + commandProgress.error = '未返回指令 ID,无法跟踪进度' + scheduleAutoClose() + return + } + + const ttl = Number(cmd?.ttlMs) > 0 ? Number(cmd.ttlMs) : 30000 + const deadline = Date.now() + ttl + 5000 + + commandPollTimer = setInterval(async () => { + if (Date.now() > deadline) { + clearInterval(commandPollTimer) + commandPollTimer = null + if (!COMMAND_STATUS[commandProgress.status]?.terminal) { + commandProgress.error = '等待超时,请稍后在操作记录查看' + commandProgress.step = 3 + } + scheduleAutoClose() + return + } + await pollCommandOnce() + }, 1500) + + // 立即查一次,避免干等 1.5s + pollCommandOnce() +} +``` + +- [ ] **Step 4: 改写 `runCommand`** + +```js +async function runCommand(title, desc) { + const rec = record.value + if (!rec) return + if (!rec.online) { + ui.toast('设备当前离线,无法下发指令') + return + } + if (rec.kind === 'drone' && rec.bindingState !== 'bound') { + ui.toast('无人机未绑定机巢,无法下发指令') + return + } + const ok = await ui.confirm(title, desc) + if (!ok) return + try { + const cmd = await devices.sendCommand(rec, title) + ui.toast(`${title}指令已下发`) + startCommandProgress(cmd, title, rec.name) + await reload() + } catch (e) { + ui.toast(e.message || '指令下发失败') + } +} +``` + +- [ ] **Step 5: 卸载清理** + +在现有 `onUnmounted` 中追加: + +```js +closeCommandProgress() +``` + +并在 `onGlobalKeydown` 增加: + +```js +if (event.key === 'Escape' && commandProgress.open) { + closeCommandProgress() + return +} +``` + +- [ ] **Step 6: Commit** + +```bash +git add src/views/MonitorView/MonitorView.vue +git commit -m "feat: 监控指令下发后轮询命令状态" +``` + +--- + +### Task 3: 浮窗 UI + 按钮状态文案 + +**Files:** +- Modify: `src/views/MonitorView/MonitorView.vue` +- Modify: `src/styles/prototype.css` + +- [ ] **Step 1: 按钮 `small` 改为动态** + +机巢主按钮(有「状态:待执行」的): + +```vue +{{ buttonStatusText('一键起飞') }} +{{ buttonStatusText('一键降落') }} + +``` + +更多控制里 `dockExtraCommands`: + +```vue +{{ commandProgress.buttonTitle === cmd.title && commandProgress.status ? buttonStatusText(cmd.title) : cmd.hint }} +``` + +无人机侧:对已有 `` 的主按钮(一键返航/起飞/紧急停止)同样用 `buttonStatusText(title)`;纯文字 secondary 按钮可不加。 + +- [ ] **Step 2: 增加浮窗模板** + +放在 `MonitorView` 根模板末尾(与 dock status modal 同级): + +```vue +
+
+
+ {{ commandProgress.title }} + {{ commandProgress.deviceName }} +
+ +
+
    +
  1. + 确认下发已确认 +
  2. +
  3. + 已下发{{ commandProgress.step >= 2 ? '等待应答' : '…' }} +
  4. +
  5. + + {{ commandProgress.status === 'acked' ? '已确认' : (COMMAND_STATUS[commandProgress.status]?.name || '执行结果') }} + {{ commandProgress.error || commandProgress.result || '…' }} +
  6. +
+
+``` + +注意:模板里若不能直接用 `COMMAND_STATUS`,改成 computed `commandProgressTerminalLabel`。 + +- [ ] **Step 3: 增加样式** + +`prototype.css` 追加: + +```css +.command-progress-float { + position: absolute; + right: 18px; + bottom: 56px; + z-index: 36; + width: 280px; + padding: 12px 12px 10px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(255,255,255,.96); + box-shadow: 0 10px 28px rgba(20, 34, 48, .18); + color: #334553; +} +.command-progress-float > header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + margin-bottom: 10px; +} +.command-progress-float > header strong { display: block; font-size: 13px; } +.command-progress-float > header small { color: var(--muted); font-size: 11px; } +.command-progress-float ol { margin: 0; padding: 0; list-style: none; display: grid; gap: 8px; } +.command-progress-float li { + display: grid; + grid-template-columns: 14px 1fr auto; + align-items: center; + gap: 8px; + min-height: 28px; + color: #7b8792; + font-size: 12px; +} +.command-progress-float li i { + width: 10px; height: 10px; border-radius: 50%; + border: 1.5px solid #c0CAD3; background: #fff; +} +.command-progress-float li.active { color: #1f5f9f; font-weight: 600; } +.command-progress-float li.active i { border-color: #2d82da; background: #2d82da; } +.command-progress-float li.done { color: #2f6b4f; } +.command-progress-float li.done i { border-color: #2f9d6a; background: #2f9d6a; } +.command-progress-float li.fail { color: #a53e45; } +.command-progress-float li.fail i { border-color: #c8454b; background: #c8454b; } +.command-progress-float li em { font-style: normal; color: var(--muted); font-size: 11px; } +``` + +- [ ] **Step 4: Commit** + +```bash +git add src/views/MonitorView/MonitorView.vue src/styles/prototype.css +git commit -m "feat: 增加指令进度浮窗与按钮状态文案" +``` + +--- + +### Task 4: 浏览器冒烟 + +**Steps:** + +- [ ] **Step 1: 启动前端,mock 指令接口** + +对监控页: + +1. 登录进 `/monitor`,选中在线机巢。 +2. 拦截: + - `POST /api/v1/docks/:id/command` → `{ code:200, data:{ id: 1001, status:'sent', ttlMs:30000, commandType:'dock.open' } }` + - 前 2 次 `GET /api/v1/commands/1001` → `status:'sent'` + - 之后 → `status:'acked', ackResultCode:'OK'` +3. 点击「打开舱门」或「一键起飞」并确认。 + +- [ ] **Step 2: 验收清单** + +1. 浮窗出现,步骤 1 完成 → 步骤 2「已下发/等待应答」→ 步骤 3「已确认/执行成功」。 +2. 对应按钮 `small` 变为「状态:已下发」再变「状态:已确认」。 +3. 终态约 5s 后浮窗自动关闭;期间可手动关。 +4. Escape 可关闭浮窗。 +5. 连续点另一条指令:浮窗切换到新指令。 +6. 离线设备:仅 toast,不出现成功浮窗。 + +- [ ] **Step 3: Commit 验收说明(若有小修)** + +若冒烟中修了样式/边界,单独 commit: + +```bash +git add -A +git commit -m "fix: 指令进度浮窗冒烟问题" +``` + +--- + +## 覆盖对照(spec) + +| Spec 要求 | Task | +|-----------|------| +| 下发后浮窗 + 三步状态 | Task 2/3 | +| 轮询 `/v1/commands/:id` 1.5s,TTL+5s | Task 2 | +| 按钮 small 同步 | Task 3 | +| 终态 5s 自动关 / 手动关 / Escape | Task 2/3 | +| 同设备新指令替换跟踪 | Task 2 `startCommandProgress` | +| 403/404/连续失败 | Task 2 `pollCommandOnce` | +| 不改后端协议 | 全程 | + +## 执行注意 + +- 不要改 `devicesStore.sendCommand` 的返回值语义(已返回 cmd)。 +- 不要引入 WebSocket。 +- 工作流子步骤(开门/归中)不在本计划范围。