Browse Source

feat: 对齐监控右侧机巢运行态并压缩 1080p 密度

将机巢指标改为电量/充电/舱门/控制模式,增加查看详情跳转,并在 1920×1080 下压缩右侧栏密度以尽量一屏显示。
main
xiaosi 4 weeks ago
parent
commit
fa5f50e545
  1. 87
      docs/superpowers/plans/2026-08-25-monitor-detail-panel-fit.md
  2. 32
      src/styles/prototype.css
  3. 59
      src/views/MonitorView/MonitorView.vue

87
docs/superpowers/plans/2026-08-25-monitor-detail-panel-fit.md

@ -0,0 +1,87 @@
# Monitor Detail Panel Fit + Dock Runtime Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 监控页右侧详情栏对齐 `jcWeb 3` 机巢运行态(电量/充电/舱门/控制模式 + 查看详情),并在 `1920×1080` 尽量整栏一屏不滚动。
**Architecture:** 在 `MonitorView` 本地复用 DeviceDetail 已有 realtime 映射思路(充电/舱门汇总),替换机巢 metric 四格并加「查看详情」;用 CSS media query 压缩 1080p 密度,不改控制指令与数据协议。
**Tech Stack:** Vue 3、现有 `devicesStore` realtime、`prototype.css`
**Spec:** `docs/superpowers/specs/2026-08-25-monitor-detail-panel-fit-design.md`
---
### Task 1: 机巢运行态字段与查看详情
**Files:**
- Modify: `src/views/MonitorView/MonitorView.vue`
**Steps:**
1. 将机巢 `status-section` 标题改为「运行状态」
2. 标题右侧加文本按钮「查看详情」,`@click="goDetail"`
3. 替换 metric 四格:
- 无人机电量 → `selectedDrone?.battery || '--'`
- 充电状态 → `chargeText(rt('chargingState'))`,充电中加 success 样式
- 舱门状态 → `doorSummary`
- 控制模式 → `record?.mode || controlModeText(rt('controlMode')) || '--'`
4. 在 `<script setup>` 增加本地 helper(不要跨文件硬耦合 DeviceDetail):
- `rt(key)` / `rtObj(key)` 基于 `record.value?.realtime`
- `chargeText` / `doorSummary` / `isClosed` / `doorValue`
5. 删除或停用机巢态旧的温湿度/PLC 四格(环境区保留)
**Acceptance:**
- 选中机巢可见四格新文案
- 点「查看详情」进入 `/device/:id`
- 缺字段时显示 `--`,不造假
---
### Task 2: 1080p 密度样式
**Files:**
- Modify: `src/styles/prototype.css`
- Modify (如需 class): `src/views/MonitorView/MonitorView.vue`
**Steps:**
1. 增加针对约 1080p 高度的密度规则,例如:
- `@media (min-height: 900px) and (max-height: 1100px)`
2. 压缩:
- `.detail-header` min-height / padding
- `.detail-panel section` padding
- `.section-title` margin
- 视频区高度(`.video-frame` / LivePlayer 容器)
- `.metric-grid > div` min-height / font-size
- `.workflow-action` / `.stage-actions button` / `.reset-action` / extra control 按钮高度与 gap
3. 展开更多控制时(`.quick-actions.all-controls`)再压一档 extra 按钮
4. 小屏(低于 900px 高)不强制套用,保留滚动
**Acceptance:**
- `1920×1080` 机巢默认态右侧栏尽量无纵向滚动
- 展开更多控制尽量仍一屏;极限溢出时优先控制区内微滚
- 无人机态同样尽量一屏
---
### Task 3: 浏览器冒烟
**Steps:**
1. 启动 `vite-dev`
2. mock / 真实数据选中机巢与无人机
3. 视口设为 `1920×1080`
4. 验证:
- 机巢四格文案正确
- 「查看详情」跳转
- 默认/展开控制尽量无外层滚动
- 飞行速度仍一位小数(回归)
**Acceptance:**
- 截图或 DOM/样式检查证明 1080p 下外层滚动条不出现(或仅控制区内部)
- 字段与跳转行为正确
---
### Execution note
- 跳过全仓 formatter / 全量测试;只做本页冒烟
- 不改后端协议;realtime 缺值显示 `--`

32
src/styles/prototype.css

@ -745,6 +745,38 @@ svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width:
.dock-control-extra-grid button > span { display: flex; flex-direction: column; gap: 3px; } .dock-control-extra-grid button > span { display: flex; flex-direction: column; gap: 3px; }
.dock-control-extra-grid button strong { font-size: 11px; font-weight: 500; } .dock-control-extra-grid button strong { font-size: 11px; font-weight: 500; }
.dock-control-extra-grid button small { color: var(--muted); font-size: 8px; } .dock-control-extra-grid button small { color: var(--muted); font-size: 8px; }
/* 1920×1080:监控右侧详情栏尽量一屏 */
@media (min-height: 900px) and (max-height: 1100px) {
.detail-header { min-height: 72px; padding: 12px 14px 10px; }
.detail-header h2 { margin: 2px 0 3px; font-size: 16px; }
.detail-panel section { padding: 10px 14px; }
.section-title { min-height: 20px; margin-bottom: 7px; }
.section-title h3 { font-size: 13px; }
.video-frame { aspect-ratio: 16 / 7.2; }
.metric-grid { gap: 6px; }
.metric-grid > div { min-height: 50px; padding: 7px 8px 6px 28px; gap: 2px; }
.metric-grid svg { left: 8px; top: 8px; width: 14px; }
.metric-grid span { font-size: 10px; }
.metric-grid strong { font-size: 15px; }
.metric-grid .success { font-size: 13px; }
.environment-row strong { font-size: 11px; }
.workflow-actions { gap: 6px; }
.workflow-action { min-height: 44px; padding: 0 8px; gap: 6px; }
.workflow-action > svg { width: 17px; }
.stage-actions { gap: 6px; margin-top: 6px; }
.stage-actions button { min-height: 40px; padding: 5px 6px; }
.reset-action { height: 34px; margin-top: 6px; }
.primary-action { min-height: 44px; }
.secondary-actions { gap: 6px; margin-top: 6px; }
.secondary-actions button { height: 32px; font-size: 11px; }
.dock-control-extra { margin-top: 10px; padding-top: 10px; }
.dock-control-extra-grid { gap: 6px; }
.dock-control-extra-grid button { min-height: 38px; padding: 5px 8px; }
.quick-actions.all-controls .dock-control-extra-grid button { min-height: 36px; padding: 5px 6px; }
.drone-control-all { gap: 6px; margin-top: 6px; }
.drone-control-all button { min-height: 36px; padding: 5px 6px; }
}
@media (max-width: 1180px) { @media (max-width: 1180px) {
.monitor-layout.focused.control-expanded { grid-template-columns: 225px minmax(280px, 1fr) 460px; } .monitor-layout.focused.control-expanded { grid-template-columns: 225px minmax(280px, 1fr) 460px; }
.monitor-layout.control-expanded .detail-panel { width: 460px; } .monitor-layout.control-expanded .detail-panel { width: 460px; }

59
src/views/MonitorView/MonitorView.vue

@ -128,12 +128,15 @@
<!-- Dock only: status metrics --> <!-- Dock only: status metrics -->
<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="section-title">
<h3>运行状态</h3>
<button class="text-button" type="button" @click="goDetail">查看详情</button>
</div>
<div class="metric-grid"> <div class="metric-grid">
<div><svg><use href="#i-battery" /></svg><span>无人机电量</span><strong>{{ selectedDrone?.battery || '--' }}</strong></div> <div><svg><use href="#i-battery" /></svg><span>无人机电量</span><strong>{{ selectedDrone?.battery || '--' }}</strong></div>
<div><svg><use href="#i-wind" /></svg><span>舱内温度</span><strong>{{ displayWithUnit(selectedAsset.environment?.insideTemperature, '℃') }}</strong></div>
<div><svg><use href="#i-cloud-rain" /></svg><span>内湿度</span><strong>{{ displayWithUnit(selectedAsset.environment?.insideHumidity, '%') }}</strong></div>
<div><svg><use href="#i-satellite" /></svg><span>PLC连接</span><strong>{{ boolText(selectedAsset.realtime?.plcConnected) }}</strong></div>
<div><svg><use href="#i-zap" /></svg><span>充电状态</span><strong :class="{ success: chargeTone }">{{ chargeText(rt('chargingState')) }}</strong></div>
<div><svg><use href="#i-door" /></svg><span>门状态</span><strong>{{ doorSummary }}</strong></div>
<div><svg><use href="#i-satellite" /></svg><span>控制模式</span><strong>{{ dockControlMode }}</strong></div>
</div> </div>
</section> </section>
@ -572,6 +575,54 @@ function boolText(value) {
return '--' return '--'
} }
function displayValue(value) {
return value == null || value === '' ? '--' : value
}
function rt(key, ...aliases) {
const realtime = record.value?.realtime || {}
for (const name of [key, ...aliases]) {
if (realtime[name] != null && realtime[name] !== '') return realtime[name]
}
return null
}
function rtObj(key) {
const raw = record.value?.realtime?.[key]
if (!raw) return {}
if (typeof raw === 'object') return raw
try { return JSON.parse(raw) || {} } catch { return {} }
}
function doorValue(side) {
return rtObj('door')[side]
}
function isClosed(value) {
return value === 'closed' || value === 'close'
}
function chargeText(value) {
const key = String(value || '').toLowerCase()
if (key === 'charging') return '充电中'
if (key === 'idle') return '空闲'
if (key === 'full' || key === 'charged') return '已充满'
return displayValue(value)
}
const chargeTone = computed(() => String(rt('chargingState') || '').toLowerCase() === 'charging')
const doorSummary = computed(() => {
const left = doorValue('left')
const right = doorValue('right')
if (isClosed(left) && isClosed(right)) return '已关闭'
if (left === 'open' || right === 'open') return '已打开'
return displayValue(left || right)
})
const dockControlMode = computed(() => record.value?.mode || displayValue(rt('controlMode')))
async function reload() { async function reload() {
try { try {
await devices.load() await devices.load()

Loading…
Cancel
Save