From d8a4af39732f610ad28cfc91bf596aa77ccdb665 Mon Sep 17 00:00:00 2001 From: xiaosi <2652281683@qq.com> Date: Mon, 31 Aug 2026 14:47:16 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=A2=9E=E5=8A=A0=20CommandProgressFlo?= =?UTF-8?q?at=20=E8=AE=BE=E8=AE=A1=E4=B8=8E=E5=AE=9E=E7=8E=B0=E8=AE=A1?= =?UTF-8?q?=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 地图指令进度浮层抽离,轮询状态机仍留页面。 --- .../2026-08-28-command-progress-float.md | 429 +----------------- ...026-08-28-command-progress-float-design.md | 125 +---- 2 files changed, 48 insertions(+), 506 deletions(-) diff --git a/docs/superpowers/plans/2026-08-28-command-progress-float.md b/docs/superpowers/plans/2026-08-28-command-progress-float.md index 80de4e0..b3ea9c7 100644 --- a/docs/superpowers/plans/2026-08-28-command-progress-float.md +++ b/docs/superpowers/plans/2026-08-28-command-progress-float.md @@ -1,425 +1,42 @@ -# Command Progress Float Implementation Plan +# CommandProgressFloat 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`,并同步按钮旁状态文案。 +**Goal:** 抽出地图指令进度浮层为展示组件;轮询/状态机留页面。 -**Architecture:** 在 `MonitorView` 本地维护 `commandProgress` 状态;`runCommand` 使用 `sendCommand` 返回的 `cmd.id` 启动轮询;右下角非模态浮窗展示三步进度;同 title 按钮 `small` 文案跟随状态。不改后端协议。 - -**Tech Stack:** Vue 3、现有 `request`/`urls`、`devicesStore.sendCommand`、`prototype.css` +**Architecture:** `CommandProgressFloat.vue` 接收 open/title/deviceName/step/status/result/error/terminalLabel,emit `close`。 **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` +### Task 1 -- [ ] **Step 1: 按钮 `small` 改为动态** +- Create `src/components/CommandProgressFloat.vue` +- Modify `src/views/MonitorView/MonitorView.vue` -机巢主按钮(有「状态:待执行」的): +替换 map-panel 内 float 为: ```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: 指令进度浮窗冒烟问题" -``` - ---- +Commit: `refactor: 抽取 CommandProgressFloat 指令进度浮层` -## 覆盖对照(spec) +### Task 2 冒烟 -| 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` | -| 不改后端协议 | 全程 | +浮层 DOM/class 在 open=true 时出现;点关闭触发 close;无 console error。 -## 执行注意 +## 约束 -- 不要改 `devicesStore.sendCommand` 的返回值语义(已返回 cmd)。 -- 不要引入 WebSocket。 -- 工作流子步骤(开门/归中)不在本计划范围。 +不改后台;精确 git add;中文 commit diff --git a/docs/superpowers/specs/2026-08-28-command-progress-float-design.md b/docs/superpowers/specs/2026-08-28-command-progress-float-design.md index 853e27e..bffc6ad 100644 --- a/docs/superpowers/specs/2026-08-28-command-progress-float-design.md +++ b/docs/superpowers/specs/2026-08-28-command-progress-float-design.md @@ -1,116 +1,41 @@ -# 监控页指令进度浮窗 - -## 背景 - -监控页快捷控制按钮旁写死「状态:待执行」。下发后只 toast「指令已下发」,领导反馈「指令没有状态」。 - -设备详情 / 操作记录已接入 `/v1/commands`,状态机为: - -`sent → acked | timeout | terminal` - -后端项目:`/Users/qingyuan/CodeProject/laic-backend` - -已核实: - -- `POST /v1/docks/:id/command` 返回完整 `DeviceCommandLog`(含 `id`、`status`) -- `GET /v1/commands/:id` 可查单条 -- `GET /v1/commands` 可查历史 -- 状态枚举:`sent` / `acked` / `timeout` / `terminal` -- 指令单**无**子步骤字段;工作流逐步事件走 MQTT `state/workflow`,本轮不接入 - -本轮不新增后端协议字段。 +# CommandProgressFloat 设计 ## 目标 -1. 监控页下发指令后弹出**当前指令进度浮窗**,逐步展示状态。 -2. 与详情页操作记录同一套状态语义。 -3. 同步更新对应控制按钮旁 `small` 文案。 -4. 不打断地图与侧栏主流程。 +将 Monitor 地图上的 `command-progress-float` 抽为展示组件。轮询 / `startCommandProgress` / `closeCommandProgress` / Escape 关闭仍留在 MonitorView。 ## 非目标 -- 工作流 PLC 子步骤时间线(开门/归中等)——需另开需求。 -- 批量多选指令。 -- ControlView / 设备详情操作记录大改。 -- 用 WebSocket 替换轮询(可后续增强)。 - -## 方案 - -### 1. 触发与数据流 +- 不迁指令下发与轮询逻辑 +- 不改 map-toolbar / 图例 / 比例尺 +- 不改按钮状态文案计算(`buttonStatusText` 仍读页面 `commandProgress`) -1. 用户确认后调用现有 `devices.sendCommand(rec, title)`。 -2. 取返回体 `cmd.id` / `cmd.status`(axios 已解包 `data`)。 -3. 打开浮窗,初始化步骤: - - 步骤 1:确认下发(本地立即完成) - - 步骤 2:已下发(`sent`) - - 步骤 3:终态(`acked` / `timeout` / `terminal`) -4. 以 `1.5s` 间隔轮询 `GET /v1/commands/:id`,直到终态或超时上限。 -5. 轮询上限:`max(ttlMs, 30000) + 5000`;超时仍未终态则展示「等待超时,请稍后在操作记录查看」。 +## 契约 -若下发失败(HTTP/业务错误):不进入轮询,仅 toast(与现网一致)。 +**Props** -### 2. 浮窗 UI - -- 位置:监控页右下角(避开比例尺),非模态,不挡主操作。 -- 标题:指令中文名(如「一键起飞」)+ 目标设备名。 -- 主体:垂直步骤列表,当前步高亮;完成步打勾;失败/超时用警示色。 -- 副文案: - - `sent`:等待应答 - - `acked`:`ackResultCode` 或「执行成功」 - - `timeout`:已超时 - - `terminal`:`ackResultCode` 或「执行失败」 -- 操作:手动关闭;终态后 **5s** 自动关闭。 -- Escape:关闭浮窗(不取消已下发指令)。 - -同设备再次下发新指令:替换当前浮窗内容与轮询目标(只跟踪最新一条)。 - -### 3. 按钮旁状态 - -监控页已有 `状态:待执行` 的 `small`: - -| 阶段 | 文案 | +| prop | 说明 | |------|------| -| 默认 | 状态:待执行 | -| 下发中 / sent | 状态:已下发 | -| acked | 状态:已确认 | -| timeout | 状态:已超时 | -| terminal | 状态:执行失败 | - -仅更新**本次点击的那颗按钮**(或同 `title` 的按钮);其他按钮保持原状。离开监控页或卸载时清理定时器与浮窗状态。 - -### 4. 实现落点 - -- 主改:`src/views/MonitorView/MonitorView.vue` - - `runCommand` 接收 `sendCommand` 返回值 - - 本地 `commandProgress` 状态 + 轮询 - - 浮窗模板与样式(复用现有视觉 token) -- `src/config/urls.js` 增加 `COMMAND = (id) => `/v1/commands/${id}`` -- `devicesStore.sendCommand` 保持返回后端 `cmd` 对象(已返回,勿吞掉) - -状态映射复用详情页: - -```js -sent → 已下发 / 等待应答 -acked → 已确认 / 执行成功 -timeout → 已超时 -terminal → 执行失败 -``` +| `open` | 是否显示 | +| `title` | 指令名 | +| `deviceName` | 设备名 | +| `step` | 1–3 | +| `status` | sent/acked/timeout/terminal… | +| `result` | 结果文案 | +| `error` | 错误文案 | +| `terminalLabel` | 第三步标题(原 `commandProgressTerminalLabel`) | -### 5. 权限与错误 +**Emits:** `close` -- `GET /v1/commands/:id` 若 403:停止轮询,浮窗提示无权限查看进度,保留「已下发」。 -- 404:停止轮询并提示。 -- 网络抖动:单次失败不关浮窗,连续失败 3 次再提示。 +组件内保留三步 `
    ` class 规则(done/active/fail)。 -## 验收 +## 实现 -1. 在线机巢点「打开舱门」:确认后出现浮窗;步骤从确认下发推进到已下发,再至已确认/失败/超时。 -2. 按钮 `small` 同步变化;终态约 5s 后浮窗自动消失,可提前手动关。 -3. 连续点两条指令:浮窗跟踪最新一条。 -4. 设备离线:仍只 toast,不出现成功态浮窗。 -5. 详情页「操作记录」能查到对应指令,状态与浮窗终态一致。 +1. `src/components/CommandProgressFloat.vue` +2. MonitorView map-panel 内替换内联 float +3. Commit: `refactor: 抽取 CommandProgressFloat 指令进度浮层` +4. 冒烟:通过页面临时打开态或 evaluate 设置/触发 close;结构 class 存在即可(后端不可用时可不真下发) -## 风险 +## 约束 -- 后端 ACK 慢或 mock 不回 ACK:浮窗会停在「已下发」直到 timeout —— 符合真实状态机。 -- 工作流多步(起飞准备等)目前只有指令级状态;若领导后续要「开门/归中」子步骤,需后端补步骤事件或前端用 realtime 推断(另开需求)。 +精确 git add;中文 commit;跳过全量单测