docs: 一些文档修改
This commit is contained in:
parent
3f517c5f48
commit
43bbc36106
|
|
@ -26,3 +26,8 @@
|
|||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.claude/
|
||||
.VSCodeCounter/
|
||||
cl.bat
|
||||
col.bat
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
# PLC Control
|
||||
|
||||
PLC Control 是一个面向 PLC / OPC UA 场景的本地控制与监控系统,提供设备建模、点位管理、控制单元自动控制、实时信号展示、日志追踪和文档查看能力。
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 数据源与点位管理
|
||||
|
||||
- 支持 OPC UA 数据源接入、重连和节点浏览
|
||||
- 支持从节点批量创建点位
|
||||
- 支持点位与设备、标签、信号角色的绑定
|
||||
- 支持批量写点和实时值查看
|
||||
|
||||
### 2. 设备与控制单元建模
|
||||
|
||||
- 设备可按 `unit_id` 归属到控制单元
|
||||
- 点位可按 `signal_role` 标记为 `start_cmd`、`stop_cmd`、`run`、`flt`、`rem` 等控制信号
|
||||
- 控制单元通过时长参数描述启停节奏与累计运行逻辑
|
||||
|
||||
### 3. 自动控制引擎
|
||||
|
||||
- 后端为每个启用中的控制单元维护独立异步任务
|
||||
- 支持 `Stopped -> Running -> DistributorRunning` 状态流转
|
||||
- 支持通信锁定、故障锁定、人工确认
|
||||
- 支持单个和批量启动/停止自动控制
|
||||
|
||||
### 4. 手动控制与联锁
|
||||
|
||||
- 设备手动启停前会检查 REM、FLT、信号质量和单元运行时状态
|
||||
- 自动控制启用时会阻止冲突的手动命令
|
||||
- `fault_locked`、`comm_locked`、`manual_ack_required` 都会阻止自动启动
|
||||
|
||||
### 5. 前端双视图
|
||||
|
||||
- `Ops` 视图:
|
||||
- 展示控制单元状态
|
||||
- 展示设备卡片和 REM/RUN/FLT 实时信号
|
||||
- 提供设备启停和批量自动控制入口
|
||||
- `Config` 视图:
|
||||
- 展示数据源、设备、点位、事件、图表
|
||||
- 展示实时日志流
|
||||
|
||||
### 6. 实时通信
|
||||
|
||||
- WebSocket 推送点位实时值
|
||||
- WebSocket 推送控制单元运行时变化
|
||||
- SSE 推送日志增量
|
||||
- 日志流默认跟随最新 `app.log*` 文件,支持轮转切换
|
||||
|
||||
## 系统设计
|
||||
|
||||
## 后端结构
|
||||
|
||||
- `src/main.rs`
|
||||
- 启动 Axum 服务
|
||||
- 注册 HTTP 路由、WebSocket 路由和静态页面
|
||||
- `src/handler`
|
||||
- HTTP 接口层
|
||||
- `src/service`
|
||||
- 数据查询与写入封装
|
||||
- `src/control`
|
||||
- 自动控制引擎、运行时存储、手动控制校验、模拟反馈
|
||||
- `src/connection.rs`
|
||||
- OPC UA 连接管理、订阅、轮询、批量写点
|
||||
- `src/event.rs`
|
||||
- 控制事件、实时点位事件和事件持久化
|
||||
- `src/websocket.rs`
|
||||
- WebSocket 房间与实时消息广播
|
||||
|
||||
## 控制引擎设计
|
||||
|
||||
控制引擎的几个关键点:
|
||||
|
||||
- 每个启用单元有一个独立任务
|
||||
- 每轮循环都会重读单元配置和设备角色映射
|
||||
- 设备/点位控制相关配置变更后,会主动唤醒对应单元任务
|
||||
- `wait_phase` 使用 `sleep_until(deadline)`,保证阶段计时不会被中途轮询漂移
|
||||
|
||||
当前时长约束:
|
||||
|
||||
- `run_time_sec > 0`
|
||||
- `stop_time_sec > 0`
|
||||
- `acc_time_sec > 0`
|
||||
- `bl_time_sec > 0`
|
||||
- `acc_time_sec > run_time_sec`
|
||||
|
||||
## 前端 Web 设计
|
||||
|
||||
前端采用原生 ES Module 和分片 HTML 结构。
|
||||
|
||||
关键模块:
|
||||
|
||||
- `web/js/app.js`: 页面启动、视图切换、事件绑定
|
||||
- `web/js/ops.js`: 运维视图渲染
|
||||
- `web/js/logs.js`: WebSocket 与日志 SSE 处理
|
||||
- `web/js/docs.js`: Markdown 文档抽屉
|
||||
- `web/js/units.js` / `equipment.js` / `points.js`: 配置视图业务逻辑
|
||||
|
||||
文档查看入口:
|
||||
|
||||
- 可在前端页面中查看 `API.md`
|
||||
- 可在前端页面中查看 `README.md`
|
||||
|
||||
两者都通过统一的文档抽屉组件展示。
|
||||
|
||||
## 实时日志设计
|
||||
|
||||
日志能力包含两部分:
|
||||
|
||||
- `GET /api/logs`
|
||||
- 拉取日志内容
|
||||
- `GET /api/logs/stream`
|
||||
- SSE 增量推送
|
||||
|
||||
日志轮转行为:
|
||||
|
||||
- 如果未指定 `file`,后端总是跟随最新 `app.log*`
|
||||
- 一旦轮转到新文件,SSE 流自动切换
|
||||
- 前端会插入分隔提示,标识当前切换到了哪个日志文件
|
||||
|
||||
## 运行方式
|
||||
|
||||
## 主要依赖
|
||||
|
||||
- Rust
|
||||
- Axum
|
||||
- Tokio
|
||||
- SQLx
|
||||
- PostgreSQL
|
||||
- async-opcua
|
||||
|
||||
## 关键环境变量
|
||||
|
||||
- `DATABASE_URL`
|
||||
- `HOST`
|
||||
- `PORT`
|
||||
- `WRITE_API_KEY`
|
||||
- `SIMULATE_PLC`
|
||||
|
||||
## 文档索引
|
||||
|
||||
- API 接口说明: `API.md`
|
||||
- 控制引擎计划: `docs/superpowers/plans/2026-03-24-control-engine.md`
|
||||
- 双视图前端计划: `docs/superpowers/plans/2026-03-25-dual-view-web.md`
|
||||
|
||||
## 当前实现特点
|
||||
|
||||
- 后端偏本地部署和现场控制使用
|
||||
- 控制状态保存在内存中,重启后重置
|
||||
- 前端和后端都围绕“实时控制 + 快速排障”设计
|
||||
|
|
@ -1,272 +1,128 @@
|
|||
# 控制引擎实现计划
|
||||
|
||||
> **适用于代理执行:** 必须使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务执行。步骤使用复选框(`- [ ]`)语法跟踪进度。
|
||||
## 当前状态
|
||||
|
||||
**目标:** 实现投煤器 / 布料机单元的自动控制引擎,包括状态机、故障/通信保护、运行时 API 及前端控制面板。
|
||||
该计划对应的核心功能已经落地,且在后续修复中做了几次重要收敛。当前代码状态以仓库实现为准,下面记录的是最新有效设计。
|
||||
|
||||
**架构:** 引擎为每个已启用的单元各启动一个异步任务(由10秒扫描器监督)。每个任务通过 `tokio::time::sleep_until` 控制阶段计时,通过 `tokio::sync::Notify` 在外部状态变化时(自动启停、故障确认)立即唤醒。状态保存在 `ControlRuntimeStore`(内存中,不持久化)。前端通过 `WsMessage::UnitRuntimeChanged` 实时接收更新——**仅在状态转换时推送**,不做周期性推送。
|
||||
## 已完成范围
|
||||
|
||||
**技术栈:** Rust/Axum 后端、sqlx/PostgreSQL、tokio 异步、Vanilla JS ES 模块前端。
|
||||
- 已实现 `ControlRuntimeStore` 和 `UnitRuntime`
|
||||
- 已实现单元自动控制状态机
|
||||
- 已实现故障锁定、通信锁定、人工确认流程
|
||||
- 已实现自动控制相关 API
|
||||
- 已实现 `UnitRuntimeChanged` WebSocket 推送
|
||||
- 已实现前端单元运行时展示
|
||||
- 已实现手动控制前的运行时阻断校验
|
||||
|
||||
---
|
||||
## 关键实现更新
|
||||
|
||||
## 文件清单
|
||||
### 1. 时长配置约束
|
||||
|
||||
| 文件 | 操作 | 职责 |
|
||||
|------|------|------|
|
||||
| `src/control/runtime.rs` | ✅ 已完成 | `UnitRuntime` 结构体 + `ControlRuntimeStore`(含 `Notify`) |
|
||||
| `src/control/command.rs` | ✅ 已完成 | 共享 `send_pulse_command()` 和 `simulate_run_feedback()` |
|
||||
| `src/control/engine.rs` | ✅ 已完成 | 监督器 + 单元异步任务 + `wait_phase` |
|
||||
| `src/control/validator.rs` | ✅ 已完成 | 故障/通信锁定时阻断手动指令 |
|
||||
| `src/control/mod.rs` | ✅ 已完成 | 导出 `command`、`engine`、`runtime`、`validator` |
|
||||
| `src/event.rs` | ✅ 已完成 | 7个 `AppEvent` 变体;`UnitStateChanged` 触发但**不持久化到数据库** |
|
||||
| `src/websocket.rs` | ✅ 已完成 | `WsMessage::UnitRuntimeChanged` |
|
||||
| `src/service/control.rs` | ✅ 已完成 | `get_all_enabled_units`、`get_equipment_by_unit_id` |
|
||||
| `src/handler/control.rs` | ✅ 已完成 | `start_auto`、`stop_auto`、`batch_start_auto`、`batch_stop_auto`、`ack_fault`、`get_unit_runtime`;每次状态变更后调用 `notify_unit` |
|
||||
| `src/main.rs` | ✅ 已完成 | 上述端点的路由注册 |
|
||||
| `web/js/state.js` | ✅ 已完成 | `runtimes: new Map()` |
|
||||
| `web/js/units.js` | ✅ 已完成 | 运行时状态徽章、Auto Start/Stop、Ack Fault;显示 `display_acc_sec` |
|
||||
| `web/js/ops.js` | ✅ 已完成 | 运维面板单元卡片显示运行时徽章与 `display_acc_sec` |
|
||||
| `web/js/app.js` | ✅ 已完成 | 处理 `UnitRuntimeChanged` WS 消息 |
|
||||
| `web/styles.css` | ✅ 已完成 | `.event-card { flex-shrink: 0 }` 防止 flex 列表中文字重叠 |
|
||||
控制单元时长字段现在统一要求:
|
||||
|
||||
---
|
||||
- `run_time_sec > 0`
|
||||
- `stop_time_sec > 0`
|
||||
- `acc_time_sec > 0`
|
||||
- `bl_time_sec > 0`
|
||||
- `acc_time_sec > run_time_sec`
|
||||
|
||||
## UnitRuntime 结构体(当前)
|
||||
这些约束已在创建和更新接口中统一校验。
|
||||
|
||||
### 2. 自动控制启动条件统一
|
||||
|
||||
单个启动和批量启动现在使用同一套阻断规则。以下任一条件成立时都不能启动自动控制:
|
||||
|
||||
- `fault_locked = true`
|
||||
- `comm_locked = true`
|
||||
- `manual_ack_required = true`
|
||||
|
||||
### 3. 设备映射刷新策略已调整
|
||||
|
||||
最初计划里是“单元任务启动时加载一次设备映射”。这个方案已经被修正。
|
||||
|
||||
当前实现:
|
||||
|
||||
- 控制引擎在每轮单元循环中重新读取设备和角色映射
|
||||
- 设备或点位的控制相关配置发生变化后,会主动唤醒对应单元任务
|
||||
- 因此不再依赖“等下一个 supervisor 周期重启任务”来刷新映射
|
||||
|
||||
这解决了运行中的单元长期使用旧 `equipment.kind`、`signal_role`、`equipment_id` 绑定的问题。
|
||||
|
||||
## 当前引擎行为
|
||||
|
||||
### supervisor
|
||||
|
||||
- 周期性扫描已启用单元
|
||||
- 确保每个启用单元都有对应任务在运行
|
||||
- 单元被禁用或删除后,其任务会自行退出
|
||||
|
||||
### unit_task
|
||||
|
||||
每轮循环都会执行:
|
||||
|
||||
1. 重读单元配置
|
||||
2. 重读设备和角色映射
|
||||
3. 检查故障与通信状态
|
||||
4. 根据 `auto_enabled`、锁定状态和当前状态机状态推进流程
|
||||
|
||||
### wait_phase
|
||||
|
||||
- 使用 `sleep_until(deadline)` 控制阶段结束时间
|
||||
- 中途通过 `fault_tick` 和 `notify` 打断
|
||||
- 打断后重新检查故障、通信和自动控制开关
|
||||
|
||||
## 运行时结构
|
||||
|
||||
当前 `UnitRuntime` 字段:
|
||||
|
||||
```rust
|
||||
// src/control/runtime.rs
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UnitRuntime {
|
||||
pub unit_id: Uuid,
|
||||
pub state: UnitRuntimeState,
|
||||
pub auto_enabled: bool,
|
||||
pub accumulated_run_sec: i64, // 内部累加器(毫秒),不直接用于显示
|
||||
pub display_acc_sec: i64, // 状态转换时的快照,前端展示用此字段
|
||||
pub accumulated_run_sec: i64,
|
||||
pub display_acc_sec: i64,
|
||||
pub fault_locked: bool,
|
||||
pub flt_active: bool,
|
||||
pub comm_locked: bool,
|
||||
pub manual_ack_required: bool,
|
||||
}
|
||||
// 注意:elapsed 字段(current_run_elapsed_sec、current_stop_elapsed_sec、
|
||||
// distributor_run_elapsed_sec、last_tick_at)已在事件驱动重构中移除。
|
||||
// 计时完全由单元任务内部的 tokio::time::sleep_until 管理,请勿重新添加。
|
||||
```
|
||||
|
||||
`ControlRuntimeStore` 额外包含:
|
||||
说明:
|
||||
|
||||
```rust
|
||||
notifiers: Arc<RwLock<HashMap<Uuid, Arc<Notify>>>>,
|
||||
- `accumulated_run_sec` 和 `display_acc_sec` 单位都是毫秒
|
||||
- 运行时状态保存在内存中,不持久化
|
||||
|
||||
// 方法:
|
||||
pub async fn get_or_create_notify(&self, unit_id: Uuid) -> Arc<Notify>
|
||||
pub async fn notify_unit(&self, unit_id: Uuid) // 每次状态变更后调用
|
||||
```
|
||||
## 配置变更后的唤醒链路
|
||||
|
||||
---
|
||||
以下操作现在都会通知相关控制单元尽快刷新配置:
|
||||
|
||||
## 引擎架构(事件驱动,2026-03-26)
|
||||
- 更新设备
|
||||
- 批量调整设备所属单元
|
||||
- 删除设备
|
||||
- 更新点位的 `equipment_id` / `signal_role`
|
||||
- 批量调整点位设备绑定
|
||||
- 删除点位
|
||||
|
||||
```
|
||||
start()
|
||||
└─ supervise() —— 10秒间隔,为每个启用单元启动 unit_task
|
||||
## 相关文件
|
||||
|
||||
unit_task(unit_id)
|
||||
├─ load_equipment_maps —— 任务启动时加载一次(缓存至任务生命周期结束)
|
||||
├─ fault_tick —— 500ms 间隔,在 wait_phase 内部使用
|
||||
└─ loop:
|
||||
├─ 重新加载单元配置(检查是否仍启用)
|
||||
├─ check_fault_comm → 有变化则推送 WS
|
||||
├─ 若 !auto || fault || comm → select!(fault_tick | notify),continue
|
||||
└─ 按状态分支:
|
||||
Stopped → wait_phase(stop_time_sec) → 启动给煤机 → 状态=Running → 推送 WS
|
||||
Running → wait_phase(run_time_sec) → 停止给煤机 → acc += run_time_sec
|
||||
→ 若 acc >= acc_time_sec:启动布料机,状态=DistributorRunning
|
||||
→ 否则:状态=Stopped → 推送 WS
|
||||
DistributorRunning → wait_phase(bl_time_sec) → 停止布料机 → acc=0 → 状态=Stopped → 推送 WS
|
||||
FaultLocked|CommLocked → select!(fault_tick | notify)
|
||||
- `src/control/runtime.rs`
|
||||
- `src/control/engine.rs`
|
||||
- `src/control/validator.rs`
|
||||
- `src/handler/control.rs`
|
||||
- `src/handler/equipment.rs`
|
||||
- `src/handler/point.rs`
|
||||
- `src/service/control.rs`
|
||||
- `src/event.rs`
|
||||
- `src/websocket.rs`
|
||||
|
||||
wait_phase(secs):
|
||||
deadline = now + secs
|
||||
loop:
|
||||
select! { sleep_until(deadline) => 返回 true(阶段正常完成)
|
||||
fault_tick.tick() => 重新检查故障/通信;若中断返回 false
|
||||
notify.notified() => 重新检查故障/通信;若中断返回 false }
|
||||
```
|
||||
## 验证状态
|
||||
|
||||
**关键不变量:**
|
||||
- `accumulated_run_sec` 每个完成周期**精确**增加 `run_time_sec * 1000`(无 delta 漂移)。
|
||||
- `display_acc_sec` 仅在 Running→Stopped 或 Running→DistributorRunning 转换时从 `accumulated_run_sec` 复制快照,前端始终读取 `display_acc_sec`。
|
||||
- WS **仅在状态变化时**推送,无周期性推送。
|
||||
- `unit.state_changed` 事件仅用于日志记录,**不写入**数据库事件表(频率过高)。
|
||||
当前仓库内已补的相关测试包括:
|
||||
|
||||
---
|
||||
- 时长字段校验测试
|
||||
- `comm_locked` 阻止自动启动测试
|
||||
- 设备映射重载反映最新角色绑定测试
|
||||
|
||||
## 任务一:扩展 UnitRuntime ✅ 已完成
|
||||
|
||||
**文件:** `src/control/runtime.rs`
|
||||
|
||||
字段如上方"UnitRuntime 结构体"所示。`ControlRuntimeStore` 包含 `notifiers` 映射,提供 `get_or_create_notify` 和 `notify_unit` 方法。
|
||||
|
||||
---
|
||||
|
||||
## 任务二:创建共享脉冲指令辅助函数 ✅ 已完成
|
||||
|
||||
**文件:** `src/control/command.rs`、`src/control/mod.rs`、`src/handler/control.rs`
|
||||
|
||||
`send_pulse_command(connection_manager, point_id, value_type, pulse_ms)` 写入高→延迟→低电平序列。
|
||||
|
||||
`simulate_run_feedback(state, eq_id, running)` 在模拟模式下写入虚拟运行反馈值(用于无真实 OPC UA 设备时的调试)。
|
||||
|
||||
---
|
||||
|
||||
## 任务三:在 validator.rs 添加运行时状态检查 ✅ 已完成
|
||||
|
||||
**文件:** `src/control/validator.rs`
|
||||
|
||||
在 `validate_manual_control` 的现有 REM/FLT/quality 检查之后添加:
|
||||
|
||||
```rust
|
||||
if let Some(unit_id) = equipment.unit_id {
|
||||
if let Some(runtime) = state.control_runtime.get(unit_id).await {
|
||||
if runtime.auto_enabled {
|
||||
return Err(ApiErr::Forbidden("自动控制已激活,请先停止自动控制", ...));
|
||||
}
|
||||
if runtime.comm_locked {
|
||||
return Err(ApiErr::Forbidden("单元通信已锁定", ...));
|
||||
}
|
||||
if runtime.fault_locked {
|
||||
return Err(ApiErr::Forbidden("单元处于故障锁定状态", ...));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务四:扩展 AppEvent 业务事件 ✅ 已完成
|
||||
|
||||
**文件:** `src/event.rs`
|
||||
|
||||
新增 7 个变体:
|
||||
|
||||
```rust
|
||||
AutoControlStarted { unit_id: Uuid },
|
||||
AutoControlStopped { unit_id: Uuid },
|
||||
FaultLocked { unit_id: Uuid, equipment_id: Uuid },
|
||||
FaultAcked { unit_id: Uuid },
|
||||
CommLocked { unit_id: Uuid },
|
||||
CommRecovered { unit_id: Uuid },
|
||||
UnitStateChanged { unit_id: Uuid, from_state: String, to_state: String },
|
||||
```
|
||||
|
||||
**`persist_event_if_needed` 映射:**
|
||||
|
||||
| 变体 | 写库? | event_type |
|
||||
|------|--------|-----------|
|
||||
| `AutoControlStarted` | ✅ | `unit.auto_control_started` |
|
||||
| `AutoControlStopped` | ✅ | `unit.auto_control_stopped` |
|
||||
| `FaultLocked` | ✅ | `unit.fault_locked`(level: error)|
|
||||
| `FaultAcked` | ✅ | `unit.fault_acked` |
|
||||
| `CommLocked` | ✅ | `unit.comm_locked`(level: warn)|
|
||||
| `CommRecovered` | ✅ | `unit.comm_recovered` |
|
||||
| `UnitStateChanged` | ❌ | —(频率过高,每周期触发)|
|
||||
|
||||
---
|
||||
|
||||
## 任务五:添加 WsMessage::UnitRuntimeChanged ✅ 已完成
|
||||
|
||||
**文件:** `src/websocket.rs`
|
||||
|
||||
```rust
|
||||
UnitRuntimeChanged(crate::control::runtime::UnitRuntime),
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务六:添加 service 辅助函数 ✅ 已完成
|
||||
|
||||
**文件:** `src/service/control.rs`
|
||||
|
||||
```rust
|
||||
pub async fn get_all_enabled_units(pool: &PgPool) -> Result<Vec<ControlUnit>, sqlx::Error>
|
||||
pub async fn get_equipment_by_unit_id(pool: &PgPool, unit_id: Uuid) -> Result<Vec<Equipment>, sqlx::Error>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务七:实现 control/engine.rs ✅ 已完成(事件驱动,2026-03-26)
|
||||
|
||||
**文件:** `src/control/engine.rs`
|
||||
|
||||
完整设计参见上方"引擎架构"章节。
|
||||
|
||||
**后续修改的关键规则:**
|
||||
- 除状态转换或故障/通信变化外,不得推送 `WsMessage::UnitRuntimeChanged`。
|
||||
- `wait_phase` 必须使用 `sleep_until(deadline)` 而非 `sleep(duration)`——deadline 在阶段开始时固定,故障 tick 重检不会重置计时器。
|
||||
- 在 `wait_phase` 内处理 `notify.notified()` 时,必须从 store 重新读取运行时(handler 可能已修改 `auto_enabled`)。
|
||||
- 设备映射在每次任务调用时加载一次;若设备配置变更,监督器将在下次扫描(≤10秒)时重启该任务。
|
||||
|
||||
---
|
||||
|
||||
## 任务八:新增 API 端点 ✅ 已完成
|
||||
|
||||
**文件:** `src/handler/control.rs`、`src/main.rs`
|
||||
|
||||
| 方法 | 路径 | Handler |
|
||||
|------|------|---------|
|
||||
| POST | `/api/unit/:id/start-auto` | `start_auto_unit` |
|
||||
| POST | `/api/unit/:id/stop-auto` | `stop_auto_unit` |
|
||||
| POST | `/api/unit/:id/ack-fault` | `ack_fault_unit` |
|
||||
| POST | `/api/unit/batch-start-auto` | `batch_start_auto` |
|
||||
| POST | `/api/unit/batch-stop-auto` | `batch_stop_auto` |
|
||||
| GET | `/api/unit/:id/runtime` | `get_unit_runtime` |
|
||||
|
||||
**Notify 规约:** 每个修改 `auto_enabled` 或 `fault_locked` 的 handler,在 upsert 运行时后**必须**调用 `state.control_runtime.notify_unit(unit_id).await`,以立即唤醒休眠中的单元任务。
|
||||
|
||||
```rust
|
||||
// 每个 auto/fault handler 必须遵循此模式:
|
||||
state.control_runtime.upsert(runtime).await;
|
||||
state.control_runtime.notify_unit(unit_id).await; // ← 不可省略
|
||||
let _ = state.event_manager.send(AppEvent::...);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务九:前端运行时集成 ✅ 已完成
|
||||
|
||||
**文件:** `web/js/state.js`、`web/js/units.js`、`web/js/ops.js`、`web/js/app.js`
|
||||
|
||||
**app.js 中的 WS 处理器:**
|
||||
```js
|
||||
case "UnitRuntimeChanged":
|
||||
state.runtimes.set(payload.data.unit_id, payload.data);
|
||||
renderUnits(); // 重新渲染单元卡片(更新徽章和按钮)
|
||||
renderOpsUnits(); // 更新运维视图单元列表
|
||||
syncEquipmentButtonsForUnit(runtime.unit_id, runtime.auto_enabled);
|
||||
break;
|
||||
```
|
||||
|
||||
**显示规则:** 前端始终使用 `runtime.display_acc_sec` 显示累计时间,不使用 `runtime.accumulated_run_sec`。
|
||||
|
||||
```js
|
||||
// ✅ 正确
|
||||
`Acc ${Math.floor(runtime.display_acc_sec / 1000)}s`
|
||||
|
||||
// ❌ 错误——会显示周期中途的抖动值
|
||||
`Acc ${Math.floor(runtime.accumulated_run_sec / 1000)}s`
|
||||
```
|
||||
|
||||
**事件列表 CSS:** `.event-card` 必须设置 `flex-shrink: 0`(在 `web/styles.css` 中),防止 flex 列高度压缩导致文字重叠。
|
||||
|
||||
---
|
||||
|
||||
## 任务十:将引擎接入 AppState ✅ 已完成
|
||||
|
||||
**文件:** `src/main.rs`
|
||||
|
||||
```rust
|
||||
let control_runtime = Arc::new(control::runtime::ControlRuntimeStore::new());
|
||||
// ... 构建 AppState ...
|
||||
control::engine::start(state.clone(), control_runtime);
|
||||
```
|
||||
最近一次相关修改完成后,`cargo test` 与 `cargo check` 均已通过。
|
||||
|
|
|
|||
|
|
@ -1,358 +1,115 @@
|
|||
# 双视图 Web UI 实现计划
|
||||
|
||||
> **适用于代理执行:** 必须使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务执行。步骤使用复选框(`- [ ]`)语法跟踪进度。
|
||||
## 当前状态
|
||||
|
||||
**目标:** 在顶部添加 **运维视图** 和 **配置视图** 两个标签页切换。运维视图以设备为核心,展示实时信号点状态(彩色信号点)及底部系统事件面板;配置视图在原有布局基础上,将底部中间面板替换为实时 SSE 日志流。
|
||||
该计划对应的双视图页面已经完成,本文档同步到当前实现,并补充最近的日志流轮转修复。
|
||||
|
||||
**架构:** `<main>` 元素通过 CSS 类名(`grid-ops` / `grid-config`)控制面板显示。新建 `ops.js` 模块负责运维视图:加载所有单元的设备详情并渲染设备卡片,每张卡片包含 REM/RUN/FLT 三个信号点(彩色小圆点),卡片中的 DOM 元素注册到 `state.opsPointEls`(`Map<point_id, { dotEl }>`),WebSocket 处理器通过 `sigDotClass()` 实时更新信号点颜色。SSE 日志流(`/api/logs/stream`)仅在配置视图中启动,切换标签时启停。
|
||||
## 已完成范围
|
||||
|
||||
**技术栈:** Vanilla JS ES 模块、CSS Grid、SSE(`EventSource`)、现有 WebSocket 基础设施、`/api/unit/{id}/detail` 端点。
|
||||
- 已实现顶部 `Ops / Config` 视图切换
|
||||
- 已实现运维视图单元侧栏和设备卡片区
|
||||
- 已实现配置视图日志流面板
|
||||
- 已实现基于 WebSocket 的信号灯和运行时状态更新
|
||||
- 已实现批量启动 / 批量停止自动控制按钮
|
||||
- 已实现 SSE 日志流接入
|
||||
|
||||
---
|
||||
## 当前页面结构
|
||||
|
||||
## 当前布局(参考)
|
||||
### 运维视图
|
||||
|
||||
```
|
||||
grid(3列 × 2行):
|
||||
左上 → equipment-panel.html (第1列,第1行)
|
||||
右上 → points-panel.html (第2-3列,第1行)
|
||||
左下 → source-panel.html (第1列,第2行)—— 单元 + 数据源
|
||||
中下 → logs-panel.html (第2列,第2行)—— 系统事件
|
||||
右下 → chart-panel.html (第3列,第2行)
|
||||
- 左侧: 控制单元列表
|
||||
- 右侧: 设备卡片区
|
||||
- 底部: 系统事件面板
|
||||
|
||||
### 配置视图
|
||||
|
||||
- 保留原有配置面板布局
|
||||
- 底部中间区域显示实时日志流
|
||||
|
||||
## 关键实现说明
|
||||
|
||||
### 1. 单元与设备视图
|
||||
|
||||
前端在 `ops` 视图中:
|
||||
|
||||
- 展示单元运行状态、启用状态、累计时长
|
||||
- 展示设备的 `REM / RUN / FLT` 信号状态
|
||||
- 为 `coal_feeder` 和 `distributor` 提供手动 `Start / Stop`
|
||||
- 自动控制启用时,禁用对应设备卡片上的手动按钮
|
||||
|
||||
相关文件:
|
||||
|
||||
- `web/js/ops.js`
|
||||
- `web/js/units.js`
|
||||
- `web/js/app.js`
|
||||
- `web/js/state.js`
|
||||
- `web/styles.css`
|
||||
|
||||
### 2. 运行时更新
|
||||
|
||||
`UnitRuntimeChanged` WebSocket 消息会驱动:
|
||||
|
||||
- 单元列表状态刷新
|
||||
- 运维视图单元卡片刷新
|
||||
- 设备手动按钮禁用状态同步
|
||||
|
||||
点位实时值更新会驱动:
|
||||
|
||||
- 配置视图点位列表刷新
|
||||
- 运维视图信号灯颜色刷新
|
||||
- 图表数据追加
|
||||
|
||||
### 3. 日志流面板
|
||||
|
||||
配置视图使用 SSE 订阅 `/api/logs/stream`。
|
||||
|
||||
当前行为:
|
||||
|
||||
- 默认跟随最新的 `app.log*`
|
||||
- 后端每 800ms 检查一次增量内容
|
||||
- 如果日志轮转到了新文件,SSE 流会自动切换到新文件
|
||||
- 前端会插入一条分隔提示,例如:
|
||||
|
||||
```text
|
||||
[log switched to app.log.1]
|
||||
```
|
||||
|
||||
## 目标布局
|
||||
然后继续追加新文件内容,不清空旧内容。
|
||||
|
||||
```
|
||||
grid-config(与原布局一致):
|
||||
左上 → equipment-panel (第1列,第1行)
|
||||
右上 → points-panel (第2-3列,第1行)
|
||||
左下 → source-panel (第1列,第2行)
|
||||
中下 → log-stream-panel【新建】 (第2列,第2行)—— SSE 日志
|
||||
右下 → chart-panel (第3列,第2行)
|
||||
相关文件:
|
||||
|
||||
grid-ops(新布局):
|
||||
上方 → ops-panel【新建】 (第1-2列,第1行)—— 单元侧栏 + 设备卡片
|
||||
下方 → logs-panel (第1-2列,第2行)—— 系统事件(全宽)
|
||||
```
|
||||
- `src/handler/log.rs`
|
||||
- `web/js/logs.js`
|
||||
|
||||
## 文件清单
|
||||
## 最近补充修复
|
||||
|
||||
| 文件 | 操作 | 用途 |
|
||||
|---|---|---|
|
||||
| `web/html/topbar.html` | 修改 | 添加 `#tabOps` / `#tabConfig` 标签按钮及批量自动按钮 |
|
||||
| `web/html/ops-panel.html` | **新建** | 运维视图:`#opsUnitList` 侧栏 + `#opsEquipmentArea` 设备卡片区 |
|
||||
| `web/html/log-stream-panel.html` | **新建** | 配置视图底部中间:SSE 日志流(`#logView`)|
|
||||
| `web/index.html` | 修改 | 引入新 partial、更新版本号 |
|
||||
| `web/js/ops.js` | **新建** | 加载设备详情、渲染设备卡片、`sigDotClass()`、`syncEquipmentButtonsForUnit()` |
|
||||
| `web/js/state.js` | 修改 | 添加 `activeView`、`opsPointEls`、`logSource`、`selectedOpsUnitId` |
|
||||
| `web/js/dom.js` | 修改 | 添加引用:`tabOps`、`tabConfig`、`batchStartAutoBtn`、`batchStopAutoBtn`、`opsUnitList`、`opsEquipmentArea`、`logView` |
|
||||
| `web/js/logs.js` | 修改 | 添加 `startLogs` / `stopLogs`;在 WS 处理器中更新 `opsPointEls` 信号点 |
|
||||
| `web/js/app.js` | 修改 | 标签切换逻辑、监听 `units-loaded` 事件、启动 ops 视图 |
|
||||
| `web/styles.css` | 修改 | 标签样式、`grid-ops`、`grid-config`、设备卡片与信号点样式 |
|
||||
### 日志轮转跟随
|
||||
|
||||
---
|
||||
新增修复后:
|
||||
|
||||
## 任务一:标签脚手架 + CSS 布局切换 ✅ 已完成
|
||||
- `GET /api/logs/stream` 在未指定 `file` 时,会持续跟随最新日志文件
|
||||
- 一旦文件切换,后端返回 `reset = true` 和新的 `file`
|
||||
- 前端用分隔提示标识轮转点
|
||||
|
||||
**涉及文件:**
|
||||
- 修改:`web/html/topbar.html`
|
||||
- 修改:`web/index.html`
|
||||
- 修改:`web/js/state.js`
|
||||
- 修改:`web/js/dom.js`
|
||||
- 修改:`web/js/app.js`
|
||||
- 修改:`web/styles.css`
|
||||
这解决了旧实现只盯住启动时选中文件、轮转后不再显示新日志的问题。
|
||||
|
||||
- [x] **步骤 1:在顶栏添加标签按钮**
|
||||
## 相关文件
|
||||
|
||||
`web/html/topbar.html` 中添加 `.tab-bar`(含 `#tabOps` / `#tabConfig`)及批量自动控制按钮(`#batchStartAutoBtn` / `#batchStopAutoBtn`)。
|
||||
- `web/index.html`
|
||||
- `web/html/topbar.html`
|
||||
- `web/html/ops-panel.html`
|
||||
- `web/html/log-stream-panel.html`
|
||||
- `web/js/app.js`
|
||||
- `web/js/ops.js`
|
||||
- `web/js/logs.js`
|
||||
- `web/js/state.js`
|
||||
- `web/js/dom.js`
|
||||
- `web/styles.css`
|
||||
|
||||
- [x] **步骤 2:向 `web/styles.css` 添加标签与网格 CSS**
|
||||
## 验证状态
|
||||
|
||||
添加 `.tab-bar`、`.tab-btn`、`.tab-btn.active` 样式;将原有 `.grid` 替换为 `.grid-ops` 和 `.grid-config`,分别定义列、行及面板 `grid-column/row` 赋值。
|
||||
最近一次日志流相关修复完成后,已确认:
|
||||
|
||||
- [x] **步骤 3:向 `web/js/state.js` 添加新字段**
|
||||
|
||||
```js
|
||||
activeView: "ops", // "ops" | "config"
|
||||
opsPointEls: new Map(), // point_id -> { dotEl }
|
||||
logSource: null,
|
||||
selectedOpsUnitId: null,
|
||||
```
|
||||
|
||||
- [x] **步骤 4:向 `web/js/dom.js` 添加 DOM 引用**
|
||||
|
||||
```js
|
||||
tabOps: byId("tabOps"),
|
||||
tabConfig: byId("tabConfig"),
|
||||
batchStartAutoBtn: byId("batchStartAutoBtn"),
|
||||
batchStopAutoBtn: byId("batchStopAutoBtn"),
|
||||
opsUnitList: byId("opsUnitList"),
|
||||
opsEquipmentArea: byId("opsEquipmentArea"),
|
||||
logView: byId("logView"),
|
||||
```
|
||||
|
||||
- [x] **步骤 5:在 `web/js/app.js` 中添加 `switchView` 函数并绑定事件**
|
||||
|
||||
```js
|
||||
function switchView(view) {
|
||||
state.activeView = view;
|
||||
const main = document.querySelector("main");
|
||||
main.className = view === "ops" ? "grid-ops" : "grid-config";
|
||||
dom.tabOps.classList.toggle("active", view === "ops");
|
||||
dom.tabConfig.classList.toggle("active", view === "config");
|
||||
// 显示/隐藏配置视图专属面板(top-left/top-right/bottom-left/bottom-right/bottom-mid)
|
||||
// 显示/隐藏运维视图专属面板(ops-main/ops-bottom)
|
||||
if (view === "config") startLogs(); else stopLogs();
|
||||
}
|
||||
```
|
||||
|
||||
`bindEvents` 中添加:
|
||||
```js
|
||||
dom.tabOps.addEventListener("click", () => switchView("ops"));
|
||||
dom.tabConfig.addEventListener("click", () => switchView("config"));
|
||||
```
|
||||
|
||||
`bootstrap` 中调用:
|
||||
```js
|
||||
switchView("ops"); // 默认进入运维视图
|
||||
```
|
||||
|
||||
- [x] **步骤 6:更新 `web/index.html`**
|
||||
|
||||
`<main class="grid-ops">` 中引入所有 partial(含新建的 ops-panel.html、log-stream-panel.html),并更新 CSS/JS 版本号。
|
||||
|
||||
---
|
||||
|
||||
## 任务二:运维面板 HTML + CSS 骨架 ✅ 已完成
|
||||
|
||||
**涉及文件:**
|
||||
- 新建:`web/html/ops-panel.html`
|
||||
- 修改:`web/styles.css`
|
||||
|
||||
- [x] **步骤 1:新建 `web/html/ops-panel.html`**
|
||||
|
||||
```html
|
||||
<section class="panel ops-main">
|
||||
<div class="ops-layout">
|
||||
<aside class="ops-unit-sidebar">
|
||||
<div class="panel-head"><h2>控制单元</h2></div>
|
||||
<div class="list ops-unit-list" id="opsUnitList"></div>
|
||||
</aside>
|
||||
<div class="ops-equipment-area" id="opsEquipmentArea">
|
||||
<div class="muted ops-placeholder">← 选择控制单元</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
`web/html/logs-panel.html` 增加 `ops-bottom` class,使其在运维视图中作为底部全宽面板。
|
||||
|
||||
- [x] **步骤 2:向 `web/styles.css` 添加运维视图 CSS**
|
||||
|
||||
`.ops-layout`(flex 横向)、`.ops-unit-sidebar`(固定宽度)、`.ops-unit-list`(可滚动)、`.ops-equipment-area`(flex-wrap 卡片区)。
|
||||
|
||||
设备卡片相关类:`.ops-eq-card`、`.ops-eq-card-head`、`.ops-signal-rows`、`.ops-signal-row`、`.ops-signal-label`、`.ops-eq-card-actions`。
|
||||
|
||||
单元列表项相关类:`.ops-unit-item`、`.ops-unit-item-name`、`.ops-unit-item-meta`、`.ops-unit-item-actions`。
|
||||
|
||||
信号点相关类:`.sig-dot`(灰色默认)、`.sig-dot.sig-on`(绿色)、`.sig-dot.sig-fault`(红色)、`.sig-dot.sig-warn`(黄色)。
|
||||
|
||||
---
|
||||
|
||||
## 任务三:ops.js —— 单元列表 + 设备卡片渲染 ✅ 已完成
|
||||
|
||||
**涉及文件:**
|
||||
- 新建:`web/js/ops.js`
|
||||
- 修改:`web/js/app.js`、`web/js/units.js`
|
||||
|
||||
### 实际实现说明
|
||||
|
||||
运维视图在**初始加载时一次性加载所有单元的所有设备卡片**(`loadAllEquipmentCards`),而非等待点击单元后再加载。点击某个单元会过滤只展示该单元的设备;再次点击同一单元则取消过滤并恢复全部展示。
|
||||
|
||||
信号点使用**彩色小圆点**(`sig-dot` 类)而非文字值+质量徽章。
|
||||
|
||||
#### 核心函数
|
||||
|
||||
**`sigDotClass(role, quality, valueText) → string`**(导出)
|
||||
|
||||
根据信号质量与值计算 CSS 类名:
|
||||
- `quality !== "good"` → `"sig-dot sig-warn"`(黄色)
|
||||
- 值为 `"1"` / `"true"` / `"on"`:
|
||||
- `role === "flt"` → `"sig-dot sig-fault"`(红色)
|
||||
- 其他 → `"sig-dot sig-on"`(绿色)
|
||||
- 其他 → `"sig-dot"`(灰色)
|
||||
|
||||
**`renderOpsUnits()`**(导出)
|
||||
|
||||
遍历 `state.units`,为每个单元渲染列表项,包含:
|
||||
- 运行状态徽章(`runtimeBadge`)、启用/禁用徽章、累计时间
|
||||
- "Start Auto" / "Stop Auto" 按钮(调用 `/api/control/unit/:id/start-auto` 或 `stop-auto`)
|
||||
- 若 `runtime.manual_ack_required` 为真,显示 "Ack Fault" 按钮
|
||||
|
||||
**`loadAllEquipmentCards()`**(导出)
|
||||
|
||||
并发请求所有单元的 `/api/unit/{id}/detail`,将全部设备合并后调用 `renderOpsEquipments()`。
|
||||
|
||||
**`selectOpsUnit(unitId)`**(私有)
|
||||
|
||||
切换 `state.selectedOpsUnitId`。若取消选择,调用 `loadAllEquipmentCards()` 恢复全部展示;若选中某单元,加载该单元详情并渲染其设备。
|
||||
|
||||
**`renderOpsEquipments(equipments)`**(私有)
|
||||
|
||||
为每台设备渲染一张卡片,包含:
|
||||
- 卡片头:设备编码 + 类型徽章
|
||||
- 信号行:REM / RUN / FLT 三个角色,每行一个 `<span class="sig-dot ...">` 元素(`data-ops-dot` + `data-ops-role` 属性)
|
||||
- 控制按钮(仅 `coal_feeder` / `distributor`):Start / Stop,`auto_enabled` 时禁用
|
||||
- 注册 DOM 元素:`state.opsPointEls.set(pointId, { dotEl })`
|
||||
- 若缓存中有 `point.point_monitor`,立即根据缓存值初始化信号点颜色
|
||||
|
||||
**`startOps()`**(导出)
|
||||
|
||||
```js
|
||||
export function startOps() {
|
||||
renderOpsUnits();
|
||||
loadAllEquipmentCards();
|
||||
dom.batchStartAutoBtn?.addEventListener("click", () => {
|
||||
apiFetch("/api/control/unit/batch-start-auto", { method: "POST" }).then(() => loadUnits()).catch(() => {});
|
||||
});
|
||||
dom.batchStopAutoBtn?.addEventListener("click", () => {
|
||||
apiFetch("/api/control/unit/batch-stop-auto", { method: "POST" }).then(() => loadUnits()).catch(() => {});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**`syncEquipmentButtonsForUnit(unitId, autoEnabled)`**(导出)
|
||||
|
||||
WS 收到 `UnitRuntimeChanged` 时调用,同步设备卡片中 Start/Stop 按钮的 `disabled` 状态(避免重新渲染整个卡片区):
|
||||
|
||||
```js
|
||||
export function syncEquipmentButtonsForUnit(unitId, autoEnabled) {
|
||||
dom.opsEquipmentArea
|
||||
.querySelectorAll(`.ops-eq-card-actions[data-unit-id="${unitId}"]`)
|
||||
.forEach((actions) => {
|
||||
actions.querySelectorAll("button").forEach((btn) => {
|
||||
btn.disabled = autoEnabled;
|
||||
btn.title = autoEnabled ? "自动控制运行中,请先停止自动" : "";
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### app.js 接入
|
||||
|
||||
```js
|
||||
import { startOps, renderOpsUnits, loadAllEquipmentCards } from "./ops.js";
|
||||
|
||||
// bootstrap 中:
|
||||
await withStatus(loadUnits());
|
||||
startOps();
|
||||
|
||||
// 事件监听:
|
||||
document.addEventListener("equipments-updated", () => {
|
||||
renderUnits();
|
||||
renderOpsUnits();
|
||||
});
|
||||
document.addEventListener("units-loaded", () => {
|
||||
renderOpsUnits();
|
||||
if (!state.selectedOpsUnitId) loadAllEquipmentCards();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务四:运维卡片信号点实时更新 ✅ 已完成
|
||||
|
||||
**涉及文件:**
|
||||
- 修改:`web/js/logs.js`
|
||||
|
||||
在 `startPointSocket` 的 WebSocket `PointNewValue` 分支中,添加运维视图信号点更新逻辑:
|
||||
|
||||
```js
|
||||
// 运维视图信号点
|
||||
const opsEntry = state.opsPointEls.get(data.point_id);
|
||||
if (opsEntry) {
|
||||
const { dotEl } = opsEntry;
|
||||
const role = dotEl.dataset.opsRole;
|
||||
import("./ops.js").then(({ sigDotClass }) => {
|
||||
dotEl.className = sigDotClass(role, data.quality, data.value_text);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`UnitRuntimeChanged` 分支同步更新运维单元列表和设备按钮状态:
|
||||
|
||||
```js
|
||||
if (payload.type === "UnitRuntimeChanged") {
|
||||
const runtime = payload.data;
|
||||
state.runtimes.set(runtime.unit_id, runtime);
|
||||
renderUnits();
|
||||
import("./ops.js").then(({ renderOpsUnits, syncEquipmentButtonsForUnit }) => {
|
||||
renderOpsUnits();
|
||||
syncEquipmentButtonsForUnit(runtime.unit_id, runtime.auto_enabled);
|
||||
});
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:使用动态 `import("./ops.js")` 避免循环依赖(`ops.js` → `logs.js` → `ops.js`)。
|
||||
|
||||
---
|
||||
|
||||
## 任务五:配置视图的日志流面板 ✅ 已完成
|
||||
|
||||
**涉及文件:**
|
||||
- 新建:`web/html/log-stream-panel.html`
|
||||
- 修改:`web/js/logs.js`
|
||||
- 修改:`web/js/dom.js`
|
||||
|
||||
- [x] **步骤 1:新建 `web/html/log-stream-panel.html`**
|
||||
|
||||
```html
|
||||
<section class="panel bottom-mid">
|
||||
<div class="panel-head"><h2>实时日志</h2></div>
|
||||
<div class="log" id="logView"></div>
|
||||
</section>
|
||||
```
|
||||
|
||||
- [x] **步骤 2:在 `web/js/logs.js` 中实现 `startLogs` / `stopLogs`**
|
||||
|
||||
```js
|
||||
export function startLogs() {
|
||||
if (state.logSource) return;
|
||||
state.logSource = new EventSource("/api/logs/stream");
|
||||
state.logSource.addEventListener("log", (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
(data.lines || []).forEach(appendLog);
|
||||
});
|
||||
state.logSource.addEventListener("error", () => appendLog("[log stream error]"));
|
||||
}
|
||||
|
||||
export function stopLogs() {
|
||||
if (state.logSource) {
|
||||
state.logSource.close();
|
||||
state.logSource = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`startLogs()` 是幂等的(有 `if (state.logSource) return` 守卫),可安全重复调用。
|
||||
|
||||
---
|
||||
|
||||
## 任务六:收尾、清理与样式完善 ✅ 已完成
|
||||
|
||||
- [x] 补充日志面板 CSS(`.log`、`.log-line`、`.level-info/warn/error`)
|
||||
- [x] `web/js/units.js` 在 `loadUnits()` 末尾派发 `units-loaded` 事件
|
||||
- [x] 更新 `web/index.html` 版本号
|
||||
- [x] 最终验证:标签切换、信号点实时更新、Start/Stop 控制按钮、SSE 日志流
|
||||
|
||||
---
|
||||
|
||||
## 实现者注意事项
|
||||
|
||||
- `state.opsPointEls` 在每次重新渲染设备卡片时清空重建,不存在陈旧引用。
|
||||
- `syncEquipmentButtonsForUnit` 仅更新按钮的 `disabled` 状态,避免每次运行时更新都重渲染整个卡片区。
|
||||
- 运维视图默认展示**所有单元的所有设备**,点击单元后过滤;取消选择后恢复全部展示。
|
||||
- 设备卡片头部 `data-unit-id` 属性供 `syncEquipmentButtonsForUnit` 精确定位按钮。
|
||||
- 后端 `/api/unit/{id}/detail` 响应中 `point.point_monitor` 字段包含最新缓存值,可用于初始渲染信号点颜色,无需等待 WebSocket 推送。
|
||||
- `cargo check` 通过
|
||||
- 日志轮转切换逻辑的后端测试通过
|
||||
- `node --check web/js/logs.js` 通过
|
||||
|
|
|
|||
Loading…
Reference in New Issue