feat: 完成 issue #131 配置化驾驶舱 Web 前端(web/cockpit)

This commit is contained in:
2026-08-05 09:32:10 +08:00
parent b673830859
commit 92e075c9f1
8 changed files with 1220 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
"""驾驶舱前端渲染计划编译脚本(issue #131 / PRD 5.5「⑤ 配置化驾驶舱」)。
把行业模板的布局 / 告警面板 **YAML 资产**编译成前端可直接 ``fetch`` 的
**RenderPlan JSON**,落到 ``web/cockpit/plans/``:
- ``plans/ti-cl4.json`` ← ``templates/ti-cl4/dashboard/cockpit.ti.yaml``
- ``plans/resin.json`` ← ``templates/resin/dashboard/cockpit.resin.yaml``
- ``plans/alarm_panel.ti.json``← ``templates/ti-cl4/dashboard/alarm_panel.ti.yaml``
切换行业模板 = 前端加载另一份 plan JSON,**前端代码零改动**(PRD 5.5 验收口径)。
编译在开发期离线完成,前端页面保持纯静态(无构建链、无后端依赖)。
本脚本是唯一依赖 PyYAML 的环节(开发期依赖,不进运行时);渲染逻辑全部复用
``core/cockpit``(#50 布局校验 / #51 渲染计划 / #52 告警面板 props),
保证前端看到的 JSON 与后端 Schema 严格一致。
用法(仓库根目录下):
python web/cockpit/scripts/build_plans.py
退出码:0 = 全部编译成功;1 = 存在失败项。
"""
from __future__ import annotations
import json
import os
import sys
import yaml # 开发期依赖(PyYAML),仅本脚本使用;前端运行时不依赖
# 仓库根目录加入 sys.path,以便 `from core.cockpit import ...`
_REPO_ROOT = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
)
sys.path.insert(0, _REPO_ROOT)
from core.cockpit import ( # noqa: E402
load_alarm_config,
load_layout,
plan_to_dict,
render_alarm_panel_props,
render_layout,
)
# 布局资产 → plan JSON 文件名
_LAYOUTS = {
"ti-cl4": "templates/ti-cl4/dashboard/cockpit.ti.yaml",
"resin": "templates/resin/dashboard/cockpit.resin.yaml",
}
# 告警面板配置资产 → plan JSON 文件名
_ALARM_PANELS = {
"alarm_panel.ti": "templates/ti-cl4/dashboard/alarm_panel.ti.yaml",
}
_OUT_DIR = os.path.join(_REPO_ROOT, "web", "cockpit", "plans")
def _load_yaml(path: str) -> dict:
with open(path, encoding="utf-8") as f:
return yaml.safe_load(f)
def _write_json(name: str, data: dict) -> str:
os.makedirs(_OUT_DIR, exist_ok=True)
out_path = os.path.join(_OUT_DIR, name + ".json")
with open(out_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return out_path
def _enrich_process_view_stages(plan_dict: dict, layout_data: dict) -> None:
"""把布局资产里 process_view 的 ``stages``(四状态工艺语义)注入 plan JSON。
#51 ``_build_props`` 只透传 ``src``/``states``,四状态清单(氯化→精制→
还原→蒸馏)留在布局资产里;前端 ProcessView 组件需要它来渲染流程节点,
因此在编译期按 widget 顺序补进 ``props.stages``(specs 与源 widgets 同序)。
"""
src_widgets = layout_data.get("widgets") or []
specs = plan_dict.get("widgets") or []
for spec, raw in zip(specs, src_widgets):
if spec.get("type") == "process_view" and isinstance(raw, dict):
stages = raw.get("stages")
if stages:
spec.setdefault("props", {})["stages"] = stages
def main() -> int:
failures = 0
# 1) 布局资产 → RenderPlan JSON(#50 校验 + #51 渲染计划)
for name, rel in _LAYOUTS.items():
try:
data = _load_yaml(os.path.join(_REPO_ROOT, rel))
plan = render_layout(load_layout(data))
plan_dict = plan_to_dict(plan)
_enrich_process_view_stages(plan_dict, data)
out = _write_json(name, plan_dict)
print(f"[OK] {rel} -> {os.path.relpath(out, _REPO_ROOT)} "
f"({plan.widget_count} widgets, theme={plan.theme})")
except Exception as exc: # noqa: BLE001 - 编译脚本需聚合全部失败
failures += 1
print(f"[FAIL] {rel}: {exc}")
# 2) 告警面板配置资产 → alarm_panel props JSON(#52)
for name, rel in _ALARM_PANELS.items():
try:
data = _load_yaml(os.path.join(_REPO_ROOT, rel))
props = render_alarm_panel_props(load_alarm_config(data))
out = _write_json(name, props)
print(f"[OK] {rel} -> {os.path.relpath(out, _REPO_ROOT)} "
f"(severities={sorted(props['severityStyles'])})")
except Exception as exc: # noqa: BLE001
failures += 1
print(f"[FAIL] {rel}: {exc}")
if failures:
print(f"共 {failures} 项编译失败")
return 1
print("全部渲染计划编译完成")
return 0
if __name__ == "__main__":
sys.exit(main())