Browse Source

feat: 对齐监控右侧环境与飞行文案

机巢环境/雨量、飞行电量地速、所属机巢与任务态徽标按 jcWeb 3 调整。
main
xiaosi 3 weeks ago
parent
commit
c70ea805ba
  1. 158
      docs/superpowers/plans/2026-08-26-monitor-jcweb3-align.md
  2. 53
      src/views/MonitorView/MonitorView.vue

158
docs/superpowers/plans/2026-08-26-monitor-jcweb3-align.md

@ -0,0 +1,158 @@
# Monitor 整页对齐 jcWeb 3 Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 按 `jcWeb 3` 整页对齐 `/monitor`:右侧栏文案、机巢状态弹窗、无人机图传与当前任务、地图工具条/比例尺/绑定虚线。
**Architecture:** 在现有 `MonitorView` + Mapbox 能力上增量改。弹窗与任务块本地实现;地图样式切换走 `mapStyles` + `style.load` 恢复 layers;绑定线用 line layer。
**Tech Stack:** Vue 3、Mapbox GL、现有 live API / devicesStore realtime、`prototype.css`
**Spec:** `docs/superpowers/specs/2026-08-26-monitor-jcweb3-align-design.md`
---
### Task 1: 右侧栏文案与飞行/环境对齐
**Files:**
- Modify: `src/views/MonitorView/MonitorView.vue`
- Modify: `src/styles/prototype.css`(如需 mission-state / 环境副文案)
**Steps:**
1. 环境区:
- 标题改为「机巢环境」
- 副文案显示更新时间
- 字段:舱外温度 / 风速 / 雨量
- 雨量 helper:有数值 → `x mm`;否则 bool;都无 → `--`
2. 无人机飞行区:
- 四格文案改为:飞行电量 / 相对高度 / 地速 / 卫星数量
- 标题旁 `mission-state`(任务中/停放待命等),基于 `statusClass` / status
3. 关联:`绑定机巢` → `所属机巢`(按钮仍 `select(parent)`
4. 无人机 eyebrow 可改为「无人机设备」(对齐原型)
**Acceptance:**
- 机巢环境三字段文案对齐
- 无人机飞行四格文案对齐且速度一位小数
- 所属机巢可切换
---
### Task 2: 机巢状态弹窗替换「查看详情」跳转
**Files:**
- Modify: `src/views/MonitorView/MonitorView.vue`
- Modify: `src/styles/prototype.css`(dock-status-dialog 样式,可从 jcWeb 3 迁移精简版)
**Steps:**
1. 新增弹窗状态 `dockStatusOpen`
2. 运行状态旁「查看详情」→ `openDockStatus()`,**不再** `router.push`
3. header maximize 保留 `goDetail()` 进设备详情(职责分离)
4. 弹窗结构对齐原型:
- header:机巢运行状态 + 名称 + 关闭
- 摘要格:设备连接 / 无人机状态 / 舱门 / 充电 / 控制模式 / 当前告警
- 明细板:舱门/归中/系统状态(有 realtime 则高亮 active)
5. 复用已有 `rt` / `doorValue` / `chargeText` / `centering` 映射
6. 遮罩点击与 Escape 关闭
**Acceptance:**
- 点「查看详情」开弹窗,不跳路由
- maximize 仍可进 `/device/:id`
- 缺字段不造假
---
### Task 3: 无人机视频区
**Files:**
- Modify: `src/views/MonitorView/MonitorView.vue`
- Modify: `src/components/LivePlayer.vue`(仅当需透传 label)
**Steps:**
1. 视频 section 从 `v-if="!isDrone"` 改为机巢/无人机都显示
2. `:camera-label="isDrone ? '无人机图传' : '机巢摄像头'"`
3. live join 逻辑:无人机选中时用所属机巢 `dockId` 拉流(现有 live API 以 dock 为维度);切换选中关闭旧 session
4. 无流保持待机壳
**Acceptance:**
- 选中无人机可见视频区,标签为「无人机图传」
- 切换设备时 session 正确切换/关闭
---
### Task 4: 当前任务块
**Files:**
- Modify: `src/views/MonitorView/MonitorView.vue`
- Modify: `src/styles/prototype.css`(mission-progress 样式,可参考 jcWeb 3)
**Steps:**
1. 无人机态在飞行状态与控制区之间插入 `mission-progress`
2. 从 realtime / record 读取:
- `missionName` / `mission`
- 进度、航点、时长、距离(有则显示,无则 `--`
3. 无任务名/无进行中任务 → 空态「暂无任务」
4. 「任务详情」:有 task id 则跳任务页;否则 toast 或 disabled
5. 1080p 密度规则补 mission 块间距
**Acceptance:**
- 有数据展示进度信息;无数据空态
- 不引入新 API 字段协议
---
### Task 5: 地图工具条、比例尺、绑定虚线
**Files:**
- Modify: `src/views/MonitorView/MonitorView.vue`
- Modify: `src/core/mapHelper.js` 和/或 `src/layout/MapLayer.vue`(如需 `setStyle` 封装)
- Modify: `src/config/map.js`(如需 export 双样式)
- Modify: `src/styles/prototype.css`(toolbar / scale 定位)
**Steps:**
1. 地图层叠加 UI:
- 左上:`卫星` / `地图` + fitAll 按钮
- 右下:动态 `map-scale` 文案
2. 样式切换:
- `map.setStyle(mapStyles.satelliteMap | streetMap)`
- 监听 `style.load` 后重建 marker images / symbol layers / binding line source
3. fitAll:
- 有选中:`clearSelection` + `resetOverviewCamera`
- 无选中:`fitBounds` 全部有效坐标
4. 绑定虚线 layer:
- source geojson LineString:选中机巢坐标 ↔ 飞行中子无人机坐标
- 仅 `!inDock` 且两端坐标有效时 `setData`;否则空 feature
5. zoom/move 时更新比例尺估算(简单 zoom→米/公里映射即可)
**Acceptance:**
- 卫星/地图切换后点位仍在
- fitAll 行为正确
- 选中飞行关系出现虚线;在巢/无坐标不显示
- 比例尺可见并随缩放变化
---
### Task 6: 浏览器冒烟(1920×1080)
**Steps:**
1. mock docks/drones + realtime(含 door/charge/mission/坐标)
2. 验证:
- 环境/飞行文案
- 查看详情弹窗
- 无人机视频标签
- 任务块有/无数据
- 地图切换、fitAll、绑定线、比例尺
- 右侧栏尽量无外层滚动
3. 回归:机巢运行态四格、速度一位小数
**Acceptance:**
- 上述路径 DOM/截图可证
- 无控制台致命错误
---
### Execution note
- 跳过全仓 formatter / 全量测试
- 不改后端协议
- 中文 commit
- 与工作区其他 WIP 隔离提交

53
src/views/MonitorView/MonitorView.vue

@ -87,7 +87,7 @@
<template v-if="selectedAsset"> <template v-if="selectedAsset">
<header class="detail-header"> <header class="detail-header">
<div> <div>
<span class="eyebrow">{{ isDrone ? '巡检无人机' : '机巢设备' }}</span>
<span class="eyebrow">{{ isDrone ? '无人机设备' : '机巢设备' }}</span>
<h2>{{ selectedAsset.name }}</h2> <h2>{{ selectedAsset.name }}</h2>
<p><svg><use href="#i-map-pin" /></svg>{{ selectedAsset.location }}</p> <p><svg><use href="#i-map-pin" /></svg>{{ selectedAsset.location }}</p>
</div> </div>
@ -100,8 +100,9 @@
<!-- Drone only: bound dock --> <!-- Drone only: bound dock -->
<div class="asset-relation" v-if="isDrone"> <div class="asset-relation" v-if="isDrone">
<span><svg><use href="#i-link" /></svg>绑定机巢{{ selectedDroneParentName }}</span>
<button v-if="selectedAsset.parent" @click="select(selectedAsset.parent)">切换<svg><use href="#i-chevron" /></svg></button>
<span><svg><use href="#i-home" /></svg>所属机巢</span>
<button v-if="selectedAsset.parent" @click="select(selectedAsset.parent)">{{ selectedDroneParentName }}<svg><use href="#i-chevron" /></svg></button>
<button v-else disabled>{{ selectedDroneParentName }}</button>
</div> </div>
<!-- Dock only: live video --> <!-- Dock only: live video -->
@ -142,22 +143,28 @@
<!-- Drone only: flight status --> <!-- Drone only: flight status -->
<section v-if="isDrone" class="status-section"> <section v-if="isDrone" class="status-section">
<div class="section-title"><h3>飞行状态</h3><span>更新于 {{ record?.updated || '刚刚' }}</span></div>
<div class="metric-grid">
<div><svg><use href="#i-battery" /></svg><span>剩余电量</span><strong>{{ record?.battery || '--' }}</strong></div>
<div><svg><use href="#i-plane" /></svg><span>飞行高度</span><strong>{{ displayWithUnit(selectedAsset.altitude, 'm') }}</strong></div>
<div><svg><use href="#i-wind" /></svg><span>飞行速度</span><strong>{{ displayWithUnit(selectedAsset.speed, 'm/s') }}</strong></div>
<div class="section-title">
<h3>飞行状态</h3>
<span class="mission-state">{{ droneMissionState }}</span>
</div>
<div class="metric-grid drone-metrics">
<div><svg><use href="#i-battery" /></svg><span>飞行电量</span><strong>{{ record?.battery || '--' }}</strong></div>
<div><svg><use href="#i-plane" /></svg><span>相对高度</span><strong>{{ displayWithUnit(selectedAsset.altitude, 'm') }}</strong></div>
<div><svg><use href="#i-route" /></svg><span>地速</span><strong>{{ displayWithUnit(selectedAsset.speed, 'm/s', 1) }}</strong></div>
<div><svg><use href="#i-satellite" /></svg><span>卫星数量</span><strong>{{ displayWithUnit(selectedAsset.satellites, '颗') }}</strong></div> <div><svg><use href="#i-satellite" /></svg><span>卫星数量</span><strong>{{ displayWithUnit(selectedAsset.satellites, '颗') }}</strong></div>
</div> </div>
</section> </section>
<!-- Drone only: mission --> <!-- Drone only: mission -->
<section class="environment-section"> <section class="environment-section">
<div class="section-title"><h3>环境信息</h3></div>
<div class="section-title">
<h3>机巢环境</h3>
<span>{{ selectedDock?.updated || record?.updated || '刚刚' }}</span>
</div>
<div v-if="hasEnvironment" class="environment-row"> <div v-if="hasEnvironment" class="environment-row">
<div><svg><use href="#i-sun" /></svg><span>舱外温度</span><strong>{{ displayWithUnit(selectedDockEnvironment?.outsideTemperature, '℃') }}</strong></div>
<div><svg><use href="#i-wind" /></svg><span>风速</span><strong>{{ displayWithUnit(selectedDockEnvironment?.windSpeed, 'm/s') }}</strong></div> <div><svg><use href="#i-wind" /></svg><span>风速</span><strong>{{ displayWithUnit(selectedDockEnvironment?.windSpeed, 'm/s') }}</strong></div>
<div><svg><use href="#i-sun" /></svg><span>室外温度</span><strong>{{ displayWithUnit(selectedDockEnvironment?.outsideTemperature, '℃') }}</strong></div>
<div><svg><use href="#i-cloud-rain" /></svg><span>降雨</span><strong>{{ boolText(selectedDockEnvironment?.rain) }}</strong></div>
<div><svg><use href="#i-cloud-rain" /></svg><span>雨量</span><strong>{{ rainfallText(selectedDockEnvironment) }}</strong></div>
</div> </div>
<div v-else class="table-empty">暂无环境数据</div> <div v-else class="table-empty">暂无环境数据</div>
</section> </section>
@ -488,6 +495,13 @@ const selectedDrone = computed(() => (isDrone.value ? record.value : devices.dro
const selectedDock = computed(() => (isDrone.value ? devices.docks.find((d) => d.dockId === record.value?.dockId) || null : record.value)) const selectedDock = computed(() => (isDrone.value ? devices.docks.find((d) => d.dockId === record.value?.dockId) || null : record.value))
const selectedDockEnvironment = computed(() => selectedDock.value?.environment || null) const selectedDockEnvironment = computed(() => selectedDock.value?.environment || null)
const hasEnvironment = computed(() => Object.values(selectedDockEnvironment.value || {}).some((value) => value != null && value !== '')) const hasEnvironment = computed(() => Object.values(selectedDockEnvironment.value || {}).some((value) => value != null && value !== ''))
const droneMissionState = computed(() => {
if (!isDrone.value || !record.value) return ''
if (record.value.statusClass === 'mission') return record.value.status || '任务中'
if (record.value.statusClass === 'offline') return '设备离线'
return record.value.status || '停放待命'
})
const livePhaseText = computed(() => { const livePhaseText = computed(() => {
if (!live.session) return '待机' if (!live.session) return '待机'
const phase = live.session.phase const phase = live.session.phase
@ -575,6 +589,23 @@ function boolText(value) {
return '--' return '--'
} }
function rainfallText(env) {
if (!env) return '--'
const amount = env.rainfall ?? env.rainAmount ?? env.rainFall ?? env.precipitation
if (amount != null && amount !== '') {
const num = Number(amount)
if (Number.isFinite(num)) return `${num} mm`
return `${amount} mm`
}
if (env.rain != null && env.rain !== '') {
if (env.rain === true || env.rain === 'true' || env.rain === 1 || env.rain === '1') return '有降雨'
if (env.rain === false || env.rain === 'false' || env.rain === 0 || env.rain === '0') return '0 mm'
return String(env.rain)
}
return '--'
}
function displayValue(value) { function displayValue(value) {
return value == null || value === '' ? '--' : value return value == null || value === '' ? '--' : value
} }

Loading…
Cancel
Save