Files
iAOP/core/cockpit/scripts/verify_renderer.py
T
bot_dev1 d23dfae776 feat(#51): 图表组件配置化渲染引擎(CockpitLayout→RenderPlan,PRD 5.5 配置化驾驶舱渲染层适配器)
在 #50 布局 JSON Schema 之上实现渲染层适配器,把 CockpitLayout 编译为与前端框架无关的 RenderPlan:

- 新增 core/cockpit/renderer.py:RenderPlan/WidgetRendererSpec/GridPlacement 渲染描述符,栅格整数坐标→CSS Grid 定位+百分比换算,widget type→前端组件映射(ProcessView/TrendChart/KpiCard/AlarmPanel/NlQuery)+ props 组装(四状态/趋势窗口/KPI标签兜底/告警订阅/NL占位),dark/light 两套主题 CSS 变量注入,组件数≥30 触发全局+逐组件性能策略(虚拟滚动/采样降频/WebWorker,PRD 5.5),render_layout 复用 #50 校验器防篡改。

- plan_to_dict/plan_to_json 序列化:切换行业模板=换 RenderPlan,前端组件注册表固定即可「切换模板零改码」。

- 28 个单元测试全通过 + verify_renderer.py sanity check 通过(氯化车间示例 + 树脂模板兼容 + 主题切换 + 序列化往返)。全 cockpit 模块 58 测试无回归。
2026-08-05 01:26:05 +08:00

152 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""图表组件渲染引擎验证脚本(issue #51,PRD 5.5 渲染层适配器验收)。
验证四个能力点:
1. **PRD 5.5 原始示例**(氯化车间)经布局校验后可渲染为合法 ``RenderPlan``,
且每类 widget 映射到正确前端组件 + props;
2. **现有树脂模板兼容**:与 ``templates/resin/dashboard/cockpit.resin.yaml``
等价的布局可渲染,且两个 trend 组件 id 唯一(虚拟滚动 key);
3. **主题 token 注入**:dark / light 两套 CSS 变量按主题切换;
4. **渲染计划可序列化**:``plan_to_json`` 输出可被 ``json.loads`` 还原,
且字段结构对齐前端 ``<WidgetHost>`` 约定。
用法(在 core/cockpit 目录下):
python scripts/verify_renderer.py
退出码:0 = 全部通过;1 = 存在未达标项。
说明:本脚本不依赖 PyYAML(与 #50 verify_layout_schema.py 一致,避免引入
运行时依赖),使用等价 dict 复现树脂驾驶舱布局;若装了 PyYAML 可直接解析
原 YAML 文件复现(见文末注释)。
"""
from __future__ import annotations
import os
import sys
# 本脚本位于 core/cockpit/scripts/,需要把 core/ 加入 sys.path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from cockpit import ( # noqa: E402
LAYOUT_SCHEMA_ID,
THEME_TOKENS,
WIDGET_COMPONENT,
plan_to_json,
render_layout,
validate_layout,
)
from cockpit.layout import load_layout # noqa: E402
def _ti_layout() -> dict:
"""PRD 5.5 氯化车间示例。"""
return {
"$schema": "iAOP-cockpit-layout-v1",
"title": "氯化车间驾驶舱",
"theme": "dark",
"widgets": [
{"type": "process_view", "src": "ti_four_state.svg", "x": 0, "y": 0, "w": 6, "h": 4},
{"type": "trend", "bind": "CLF-01.TEMP", "x": 6, "y": 0, "w": 6, "h": 2},
{"type": "kpi_card", "metric": "Ti_purity", "label": "Ti 纯度", "x": 6, "y": 2, "w": 3, "h": 2},
{"type": "alarm_panel", "x": 0, "y": 4, "w": 12, "h": 3},
{"type": "nl_query", "x": 9, "y": 2, "w": 3, "h": 2},
],
}
def _resin_like_layout() -> dict:
"""等价于 templates/resin/dashboard/cockpit.resin.yaml 的布局。"""
return {
"$schema": "iAOP-cockpit-layout-v1",
"title": "吸附树脂车间驾驶舱",
"theme": "dark",
"widgets": [
{"type": "process_view", "src": "resin_four_state.svg", "x": 0, "y": 0, "w": 12, "h": 4},
{"type": "trend", "bind": "R-801.TEMP", "x": 0, "y": 4, "w": 6, "h": 2},
{"type": "trend", "bind": "R-801.AGIT", "x": 6, "y": 4, "w": 6, "h": 2},
{"type": "kpi_card", "metric": "resin_exchange_capacity", "label": "交换容量",
"x": 0, "y": 6, "w": 3, "h": 2},
{"type": "alarm_panel", "x": 0, "y": 8, "w": 9, "h": 3},
{"type": "nl_query", "x": 9, "y": 8, "w": 3, "h": 3},
],
}
def _check_render(name: str, data: dict) -> bool:
"""校验 + 渲染一份布局,打印结果,返回是否通过。"""
res = validate_layout(data)
if not res.ok:
print(f"[FAIL] {name}: 布局校验未通过")
for e in res.errors:
print(f" - {e}")
return False
plan = render_layout(load_layout(data))
print(f"[PASS] {name}: 渲染 {plan.widget_count} 个组件,主题={plan.theme}")
for spec in plan.specs:
print(f" - {spec.id} → {spec.component} "
f"({spec.grid.width_pct}%, {spec.props})")
return True
def _check_themes() -> bool:
"""主题 token:dark / light 两套 CSS 变量都存在且不同。"""
ok = True
for theme in ("dark", "light"):
data = _ti_layout()
data["theme"] = theme
plan = render_layout(load_layout(data))
tokens = plan.theme_tokens
match = tokens == THEME_TOKENS[theme]
print(f"[{'PASS' if match else 'FAIL'}] 主题 token({theme}): {len(tokens)} 个 CSS 变量")
ok = ok and match
if THEME_TOKENS["dark"]["--cockpit-bg"] == THEME_TOKENS["light"]["--cockpit-bg"]:
print("[FAIL] dark/light 底色不应相同")
ok = False
else:
print("[PASS] dark/light 底色不同(确为两套配色)")
return ok
def _check_serialization() -> bool:
"""渲染计划可 JSON 序列化往返。"""
import json
plan = render_layout(load_layout(_resin_like_layout()))
text = plan_to_json(plan)
parsed = json.loads(text)
ok = parsed["title"] == "吸附树脂车间驾驶舱" and parsed["widgetCount"] == 6
# 字段结构对齐前端 <WidgetHost> 约定
fields_ok = all({"component", "grid", "props", "perf"} <= set(w) for w in parsed["widgets"])
print(f"[{'PASS' if ok and fields_ok else 'FAIL'}] 序列化往返: JSON 可解析、字段齐全")
return ok and fields_ok
def main() -> int:
print("=" * 70)
print("图表组件渲染引擎验证(issue #51 / PRD 5.5 渲染层适配器)")
print("=" * 70)
print(f"已注册前端组件映射: {WIDGET_COMPONENT}")
print(f"布局版本: {LAYOUT_SCHEMA_ID}")
print("-" * 70)
all_ok = True
all_ok &= _check_render("PRD 5.5 氯化车间示例", _ti_layout())
all_ok &= _check_render("树脂模板兼容", _resin_like_layout())
all_ok &= _check_themes()
all_ok &= _check_serialization()
print("-" * 70)
print(f"结果: {'全部通过 ✅' if all_ok else '存在未达标项 ❌'}")
return 0 if all_ok else 1
if __name__ == "__main__":
raise SystemExit(main())
# ---------------------------------------------------------------------------
# 附:若安装了 PyYAML,可直接解析树脂模板原文件复现渲染:
# import yaml
# with open("../../templates/resin/dashboard/cockpit.resin.yaml", encoding="utf-8") as f:
# data = yaml.safe_load(f)
# render_layout(load_layout(data))
# 字段语义与本脚本的 _resin_like_layout() 完全一致(iAOP-cockpit-layout-v1)。
# ---------------------------------------------------------------------------