# -*- 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())