You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

12 KiB

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/urlsdevicesStore.sendCommandprototype.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 段:

// Commands
export const COMMANDS = '/v1/commands'
export const COMMAND = (id) => `/v1/commands/${id}`
  • Step 2: Commit
git add src/config/urls.js
git commit -m "feat: 增加单条指令查询 URL"

Task 2: Monitor 指令进度状态机与轮询

Files:

  • Modify: src/views/MonitorView/MonitorView.vue

  • Step 1: 增加 import

确保已有:

import request from '@/utils/http'
import * as urls from '@/config/urls'

若当前是别的 http 路径,保持项目现有 import 风格(与文件顶部一致),只补 urls.COMMAND

  • Step 2: 增加状态常量与 progress 状态

放在 script setup 控制相关区域(runCommand 附近):

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
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
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 中追加:

closeCommandProgress()

并在 onGlobalKeydown 增加:

if (event.key === 'Escape' && commandProgress.open) {
  closeCommandProgress()
  return
}
  • Step 6: Commit
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 改为动态

机巢主按钮(有「状态:待执行」的):

<small>{{ buttonStatusText('一键起飞') }}</small>
<small>{{ buttonStatusText('一键降落') }}</small>
<!-- stage / reset 同理title  runCommand 第一个参数一致 -->

更多控制里 dockExtraCommands

<small>{{ commandProgress.buttonTitle === cmd.title && commandProgress.status ? buttonStatusText(cmd.title) : cmd.hint }}</small>

无人机侧:对已有 <small> 的主按钮(一键返航/起飞/紧急停止)同样用 buttonStatusText(title);纯文字 secondary 按钮可不加。

  • Step 2: 增加浮窗模板

放在 MonitorView 根模板末尾(与 dock status modal 同级):

<div
  v-if="commandProgress.open"
  class="command-progress-float"
  role="status"
  aria-live="polite"
>
  <header>
    <div>
      <strong>{{ commandProgress.title }}</strong>
      <small>{{ commandProgress.deviceName }}</small>
    </div>
    <button type="button" class="icon-command" title="关闭" @click="closeCommandProgress">
      <svg><use href="#i-x" /></svg>
    </button>
  </header>
  <ol>
    <li :class="{ done: commandProgress.step > 1, active: commandProgress.step === 1 }">
      <i></i><span>确认下发</span><em>已确认</em>
    </li>
    <li :class="{ done: commandProgress.step > 2, active: commandProgress.step === 2 }">
      <i></i><span>已下发</span><em>{{ commandProgress.step >= 2 ? '等待应答' : '…' }}</em>
    </li>
    <li
      :class="{
        done: commandProgress.step >= 3 && !commandProgress.error && commandProgress.status === 'acked',
        active: commandProgress.step === 3,
        fail: commandProgress.step >= 3 && (commandProgress.status === 'timeout' || commandProgress.status === 'terminal' || !!commandProgress.error),
      }"
    >
      <i></i>
      <span>{{ commandProgress.status === 'acked' ? '已确认' : (COMMAND_STATUS[commandProgress.status]?.name || '执行结果') }}</span>
      <em>{{ commandProgress.error || commandProgress.result || '…' }}</em>
    </li>
  </ol>
</div>

注意:模板里若不能直接用 COMMAND_STATUS,改成 computed commandProgressTerminalLabel

  • Step 3: 增加样式

prototype.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
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/1001status:'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:

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。
  • 工作流子步骤(开门/归中)不在本计划范围。