# 实时监控 / 设备详情状态轮询 日期:2026-09-03 状态:已确认(待写实现计划) 范围:仅前端 `laic-frontend` ## 1. 背景 「实时监控」页当前只在 `onMounted`(以及指令成功后的 `reload`)调用一次 `devices.load()`。左侧设备列表、右侧详情面板、地图 marker 都读同一份 Pinia `devicesStore`;store 不刷新,UI 就不更新。 直播会话(`useMonitorLive`)与指令进度(`useCommandProgress`)已有各自定时器,但**设备状态本身没有轮询**,与「实时监控」语义不符。 现有 `devices.load()` 契约: 1. `GET /v1/docks` + `GET /v1/drones`(近全量 pageSize=100) 2. 再对每条 `GET /v1/docks/:id` / `GET /v1/drones/:id` 取 `online` + `realtime` 3. 映射 status / environment / alarms / 地图坐标后写入 `docks` / `drones` / `assets` 设备详情页 `DeviceDetailView` 同样只在挂载时 `devices.load()` 一次,无持续刷新。 ## 2. 目标 1. **实时监控页**停在页面上时,设备列表 / 选中详情 / 地图状态约每 **5 秒**自动更新。 2. **设备详情页**只刷新**当前路由打开的那一台**设备详情,约每 **5 秒**一次。 3. 离开页面或标签页隐藏时停止轮询;回到前台立即补刷一次再恢复。 4. 不改变直播 heartbeat / phase / play-url,也不改变指令进度轮询。 ## 3. 非目标 - **设备管理列表页(`DevicesView`)不自动刷新**(仍手动刷新按钮)。 - 不新增后端批量快照 / WebSocket / SSE。 - 不在本轮优化掉 Monitor 的 N+1 详情请求(已知成本,后续可换批量 API)。 - 不改 `DevicesView` 分页本地 `tableRecords` 数据源。 ## 4. 方案(页面各自定时器) 采用页面级 `setInterval`,不引入 store 全局轮询器。 ### 4.1 `MonitorView` 1. `onMounted`:先做现有首次 `devices.load()`(失败仍 toast 一次,与现状一致);无论首次成败,只要仍挂在本页就启动 `setInterval(() => void pollDevices(), 5000)`。 2. `pollDevices`: - 若上一轮 `load` 仍 in-flight → **跳过本轮**(防请求堆积)。 - 否则 `await devices.load()`。 - 单次失败:静默(`console.warn` 即可),**不 toast**,下一轮继续。 - `401` 仍走现有 `http` 拦截器(登出 / 跳转)。 3. `onUnmounted` / `pagehide`:`clearInterval`;用页面级 `disposed`/`generation` 忽略卸载后才返回的轮询结果(避免卸载后无意义的续跑逻辑;store 已被 `load()` 写入可保留,不必回滚)。 4. `document.visibilitychange`: - `hidden` → 暂停(clearInterval) - `visible` → 立即 `pollDevices()` 一次,再重新 `setInterval` 5. 指令路径现有 `reload()` 继续直接 `devices.load()`;与轮询共享同一 in-flight 闸门更佳(同一 `loading`/`inflight` 标志),避免指令 reload 与定时器重叠打双份。 ### 4.2 `DeviceDetailView` 1. 保留挂载时一次 `devices.load()`(以及非 admin 的 logs/alarms)。 2. 另启 **当前 `route.params.id`** 的详情轮询,间隔 5000ms: - dock → `GET /v1/docks/:id` - drone → `GET /v1/drones/:id` 3. 响应经 store 新方法合并进**对应一条**记录(见 4.3),驱动本页与若仍挂着的 Monitor 共享状态。 4. `watch(() => route.params.id)`:停旧定时器,按新 id 立即拉一次并重启 interval。 5. 生命周期 / visibility / in-flight 跳过 / 失败静默:与 Monitor 同规则。 6. 离开页:清除定时器。 ### 4.3 `devicesStore` 增量回写 新增(命名可微调,语义固定): - `applyDockDetail(id, detail)` - `applyDroneDetail(id, detail)` 行为: 1. 找到 `docks` / `drones` 中对应 `id`;找不到则 **no-op + `console.warn`**(不触发全量 `load`,避免详情页单独制造全表风暴)。 2. 用与 `load()` 相同的映射规则更新该条:`online`、`realtime`、`status` / `statusClass`、`environment`、`alarms`、`mode`、无人机 `battery` / `_lat` `_lon` `_alt` 等派生字段。 3. 同步更新 `assets[id]` 上用于地图的 status / 坐标 / realtime 相关字段,避免 Monitor 与详情不一致。 4. **不**重拉列表,**不**重建无关条目。 `load()` 本身可抽一小段「detail → record 字段」纯函数供全量与增量共用,避免两套映射分叉;若改动面过大,允许增量路径先复制必要映射,但字段语义必须与 `load()` 一致。 ## 5. 错误与并发 | 场景 | 行为 | |------|------| | 轮询 HTTP 业务/网络失败 | 静默,保留旧数据,下轮再试 | | 401 | 现有拦截器登出 | | 上轮未完成 | 跳过本轮 | | 页面 hidden | 暂停 timer | | 页面 visible | 立即补一次 + 重启 timer | | 详情 id 切换 | 取消旧轮询,新 id 立即请求 | ## 6. 验收 1. 停留在实时监控:约 5s 内列表状态 / 右侧详情(电量、舱门、环境、在线等)/ 地图 marker 会随后端变化更新。 2. 打开设备详情 A:网络面板仅见对 A 的详情轮询;切换到 B 后改为 B。 3. 设备管理列表页打开时**无**定时 `/docks/:id` `/drones/:id` 轮询(仅用户点击刷新或进入时的请求)。 4. 从监控/详情路由离开,或切到其它浏览器标签:轮询停止;切回后先补刷再按 5s 继续。 5. 开播、停播、指令进度浮层行为与改前一致。 ## 7. 风险 - Monitor 每 5s 全量 `load()` 仍是列表 + N 次详情;设备数量上来后 QPS 偏高。本轮按产品选择接受;后续应用批量状态接口替换 `pollDevices` 内部实现,页面契约可不变。 - 详情增量映射若与 `load()` 分叉,会出现「详情页字段」与「监控全量刷新后字段」不一致;优先抽共享映射。 ## 8. 实现落点(预告) | 文件 | 变更 | |------|------| | `src/stores/modules/devicesStore.js` | 抽共享映射;新增 `applyDockDetail` / `applyDroneDetail` | | `src/views/MonitorView/MonitorView.vue` | 5s 轮询 + visibility + 与 `reload` 共用 in-flight | | `src/views/DeviceDetailView/DeviceDetailView.vue` | 当前 id 详情轮询 + id watch + visibility | 完成后:实现计划 → 编码 → 构建部署 → 人工看监控与详情是否按 5s 变。