18 KiB
广寒基地拓扑图表现层数据库化设计
日期:2026-07-09 状态:待用户审核 范围:广寒基地工程总图的视觉表现层数据设计,不包含本轮代码实现
1. 背景
当前 guanghan_topology_demo.html 已经从后端快照读取大部分拓扑数据:
- 任务与时间点决定快照时间;
- 组件、端口、连接、状态来自 PostgreSQL;
- SVG 节点坐标、尺寸、分组、标签覆盖、显示提示和散热器标记来自
diagram_layouts、diagram_nodes、diagram_node_members; - 页面会按快照隐藏尚未出现的组件和连接。
但页面仍然保留了一部分视觉语义:
core-card、power-card、vehicle-card等样式键对应什么颜色,由前端 CSS 决定;structural_mount、pressurized_passage、fuel、power等连接类型如何映射成线条颜色、线宽、虚实线,由前端 JS/CSS 决定;- 图例项由 HTML 硬编码;
- 散热器虽然由数据库标记是否显示,但具体形状、颜色、标签仍由前端决定。
因此当前状态是“半数据库驱动”。本设计把它推进到 B1 级别:数据库决定工程图的主要视觉语义,前端只保留通用渲染和交互壳。
2. 目标
本设计的目标是让后端数据库决定以下内容:
- 节点矩形的位置、尺寸、圆角、填充色、描边色、描边宽度。
- 连接线的颜色、线宽、虚实线、图例文案。
- 装饰件的基础视觉参数,例如散热器的颜色、描边、标签。
- 工程图图例的项目、顺序、显示名称和示例样式。
- 节点
style_key、连接connection_type、装饰件decoration_key到具体视觉参数的映射。
验收时应能做到:
- 修改数据库中
pressurized_passage的颜色后,页面加压走道颜色随之变化,不需要改 HTML/CSS; - 修改数据库中
core-card的填充/描边后,核心舱视觉随之变化; - 图例完全由 API 返回,不再在页面硬编码;
- GHC-01 到 GHC-05 的历史快照仍按数据库事实正确显示;
- 当前已确认的空间结构不变。
3. 非目标
B1 方案不把整个页面变成数据库控制的 UI 系统。以下内容仍属于前端通用壳:
- 页面字体族、整体背景、工具栏、弹窗、按钮、hover/focus 效果;
- 文本截断算法、SVG 元素创建方式、键盘可访问性;
- 响应式断点和滚动容器;
- 模块详情弹窗的排版;
- 业务时态表的结构重写;
- 数据库直接存储完整 SVG 或 HTML。
这条边界很重要:数据库掌握“工程图是什么样”,前端掌握“页面如何交互”。
4. 设计原则
4.1 表现层不是业务事实
组件、任务、连接、状态仍然是业务事实。布局和样式是对这些事实的可视化表达。
因此:
- 快照时间继续只决定哪些组件、连接、状态存在;
- 默认情况下,视觉主题不随历史时间变化;
- 早期任务快照可以使用同一张当前工程总图布局,只隐藏当时尚不存在的组件。
这能保持不同时刻的空间比较稳定,避免 GHC-01、GHC-03、GHC-05 每次切换都“重排整张图”。
4.2 数据库保存语义映射,不保存前端实现
数据库可以保存:
stroke_color = '#51e89c'stroke_width = 9dash_array = nulllegend_label = '加压走道'
数据库不保存:
- CSS 选择器;
- JavaScript 函数名;
- HTML 片段;
- 浏览器专用的布局技巧。
4.3 可迁移、可扩展、可回退
新增表现层表应与现有 diagram_layouts 兼容。实现时可以分阶段上线:
- 先建表并 seed 当前样式;
- API 同时返回旧字段和新
presentation; - 前端切换到新字段;
- 测试通过后,保留旧 CSS 类作为安全兜底,但不再作为事实来源。
5. 数据模型
5.1 diagram_themes
主题表表示一套工程图视觉风格。广寒基地初期只需要一个主题。
建议字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uuid |
主键 |
base_id |
uuid |
所属基地 |
theme_key |
text |
例如 guanghan_dark_engineering_v1 |
name |
text |
显示名称 |
description |
text |
可空 |
tokens |
jsonb |
少量通用色值,例如文字色、弱文字色;不放完整 CSS |
is_default |
boolean |
是否默认主题 |
created_at / updated_at |
timestamptz |
审计字段 |
约束:
UNIQUE (base_id, theme_key)
tokens 示例:
{
"textColor": "#eaf2ff",
"mutedTextColor": "#91a3bd",
"labelColor": "#f4df7b"
}
5.2 diagram_node_styles
节点样式表把现有 diagram_nodes.style_key 从“前端 CSS 类名”变成“数据库样式键”。
建议字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uuid |
主键 |
theme_id |
uuid |
所属主题 |
style_key |
text |
例如 core-card、power-card |
display_name |
text |
管理用途 |
fill_color |
text |
SVG 填充色 |
stroke_color |
text |
SVG 描边色 |
stroke_width |
numeric |
SVG 描边宽度 |
corner_radius |
numeric |
矩形圆角 |
title_color |
text |
可空,默认使用主题文字色 |
meta_color |
text |
可空 |
hint_color |
text |
可空 |
created_at / updated_at |
timestamptz |
审计字段 |
约束:
UNIQUE (theme_id, style_key)
现有样式键继续保留:
node-cardcore-cardpower-cardpower-node-cardvehicle-card
这样数据迁移最小,页面也容易渐进切换。
5.3 diagram_edge_styles
连接样式表按业务连接类型定义线条表现。
建议字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uuid |
主键 |
theme_id |
uuid |
所属主题 |
connection_type |
text |
对应 topology_connections.connection_type |
display_name |
text |
例如 加压走道 |
stroke_color |
text |
线条颜色 |
stroke_width |
numeric |
线宽 |
dash_array |
text |
可空,例如 13 10 |
line_cap |
text |
例如 square |
legend_group |
text |
可空,用于多个连接类型共用一个图例 |
legend_order |
integer |
图例排序辅助 |
created_at / updated_at |
timestamptz |
审计字段 |
约束:
UNIQUE (theme_id, connection_type)
当前建议映射:
connection_type |
默认图例 | 默认表现 |
|---|---|---|
structural_mount |
结构直连 | 灰蓝色实线 |
pressurized_passage |
加压走道 | 绿色实线 |
fuel |
燃料 / 供电 | 紫色虚线 |
power |
燃料 / 供电 | 紫色虚线 |
cooling |
冷却 | 预留,可暂不显示图例 |
docking |
对接口 | 预留 |
如果 fuel 和 power 继续共用图例,可以让两条记录使用同一个 legend_group = 'utility'。
5.4 diagram_decoration_styles
装饰件样式表定义散热器等非组件元素的表现。装饰件是否出现仍由 diagram_nodes.decorations 决定。
建议字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uuid |
主键 |
theme_id |
uuid |
所属主题 |
decoration_key |
text |
例如 radiator |
display_name |
text |
例如 大型折叠散热面板 |
fill_color |
text |
主体填充色 |
stroke_color |
text |
主体描边色 |
stroke_width |
numeric |
主体描边宽度 |
label |
text |
图上短标签,例如 散热器 |
label_color |
text |
标签颜色 |
render_params |
jsonb |
小规模绘制参数 |
legend_order |
integer |
图例排序辅助 |
created_at / updated_at |
timestamptz |
审计字段 |
约束:
UNIQUE (theme_id, decoration_key)
render_params 示例:
{
"widthRatio": 0.55,
"maxWidth": 130,
"height": 16,
"offsetXRatio": 0.55,
"offsetY": -18,
"ribCount": 3
}
render_params 只用于装饰件的小型形状参数。它不承载任意 SVG,也不允许注入脚本或 HTML。
5.5 diagram_legend_items
图例可以从节点、连接和装饰件样式自动推导,但为了保证顺序和文案稳定,建议单独建表。
建议字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
uuid |
主键 |
theme_id |
uuid |
所属主题 |
legend_key |
text |
例如 pressurized |
label |
text |
例如 加压走道 |
item_type |
text |
edge、decoration、node |
style_ref |
text |
引用 connection_type、decoration_key 或 style_key |
display_order |
integer |
显示顺序 |
is_visible |
boolean |
是否显示 |
约束:
UNIQUE (theme_id, legend_key)
CHECK (item_type IN ('edge', 'decoration', 'node'))
当前图例建议:
structural:结构直连,引用structural_mountpressurized:加压走道,引用pressurized_passageutility:燃料 / 供电,引用fuel或power中任一代表样式radiator:大型折叠散热面板,引用radiator
5.6 diagram_layouts 扩展
为布局绑定默认主题:
| 字段 | 类型 | 说明 |
|---|---|---|
theme_id |
uuid |
引用 diagram_themes(id),可空;为空时使用基地默认主题 |
建议约束:
ALTER TABLE topology.diagram_layouts
ADD COLUMN theme_id uuid REFERENCES topology.diagram_themes(id);
5.7 diagram_nodes 扩展
现有字段基本足够,但建议补一个可选字段:
| 字段 | 类型 | 说明 |
|---|---|---|
render_meta |
jsonb |
节点级小规模渲染参数,例如特殊文本锚点;首期可不使用 |
首期可以不新增 render_meta,只让 style_key 指向 diagram_node_styles。如果后续发现某个节点需要特殊文本位置,再引入该字段。
6. API 设计
6.1 快照响应结构
现有 /api/v1/bases/guanghan/snapshot 继续返回 base、query、components、connections、layout。新增 layout.presentation。
建议结构:
{
"layout": {
"layout": {
"key": "engineering_overview_v1",
"name": "工程总图 v1",
"canvasWidth": 1500,
"canvasHeight": 1900,
"themeKey": "guanghan_dark_engineering_v1"
},
"nodes": [
{
"nodeKey": "core_cabin",
"styleKey": "core-card",
"x": 630,
"y": 470,
"width": 240,
"height": 200,
"memberKeys": ["core_cabin"]
}
],
"presentation": {
"theme": {
"key": "guanghan_dark_engineering_v1",
"name": "广寒深色工程图",
"tokens": {
"textColor": "#eaf2ff",
"mutedTextColor": "#91a3bd"
}
},
"nodeStyles": {
"core-card": {
"fillColor": "#3b250d",
"strokeColor": "#ffad42",
"strokeWidth": 5,
"cornerRadius": 16,
"titleColor": "#eaf2ff",
"metaColor": "#a6b5c9",
"hintColor": "#71849e"
}
},
"edgeStyles": {
"pressurized_passage": {
"displayName": "加压走道",
"strokeColor": "#51e89c",
"strokeWidth": 9,
"dashArray": null,
"lineCap": "square",
"legendGroup": "pressurized"
}
},
"decorationStyles": {
"radiator": {
"displayName": "大型折叠散热面板",
"fillColor": "#6b5814",
"strokeColor": "#f0d869",
"strokeWidth": 2,
"label": "散热器",
"labelColor": "#f4df7b",
"renderParams": {
"widthRatio": 0.55,
"maxWidth": 130,
"height": 16,
"offsetXRatio": 0.55,
"offsetY": -18,
"ribCount": 3
}
}
},
"legendItems": [
{
"key": "pressurized",
"label": "加压走道",
"itemType": "edge",
"styleRef": "pressurized_passage",
"displayOrder": 20
}
]
}
}
}
6.2 缺省与错误处理
API 查询布局时:
- 使用默认布局;
- 如果布局绑定了主题,使用该主题;
- 如果布局未绑定主题,使用基地默认主题;
- 如果节点引用的
style_key不存在,API 应返回明确错误或在响应中标记presentationWarnings。
建议首期采用“API 返回 500/配置错误,不静默降级”的策略。拓扑图是工程资料,缺失样式不应伪装成正确页面。
前端可以有开发兜底色,但只用于防止页面彻底空白,不能作为测试验收依据。
6.3 样式值安全
API 应只返回白名单字段。颜色值应校验为:
#RGB#RRGGBBrgb(...)rgba(...)- 少量受控命名色如
transparent
首期建议只使用 #RRGGBB,简单、稳定、可测试。
dash_array 只允许数字和空格,例如 13 10。
line_cap 只允许 butt、round、square。
render_params 只允许预定义键,不允许任意 SVG 属性透传。
7. 前端渲染设计
7.1 节点
当前逻辑:
rect.class = node.styleKey
CSS 决定 fill/stroke/stroke-width
目标逻辑:
style = presentation.nodeStyles[node.styleKey]
rect.setAttribute('fill', style.fillColor)
rect.setAttribute('stroke', style.strokeColor)
rect.setAttribute('stroke-width', style.strokeWidth)
rect.setAttribute('rx', style.cornerRadius)
文本颜色同理从节点样式或主题 token 获取。
7.2 连接
当前逻辑:
connection.type -> connectionClass(type)
CSS 决定颜色和虚实线
目标逻辑:
style = presentation.edgeStyles[connection.type]
line.stroke = style.strokeColor
line.strokeWidth = style.strokeWidth
line.strokeDasharray = style.dashArray
line.strokeLinecap = style.lineCap
连接合并逻辑保留:多个数据库连接如果落在同一对视觉节点之间,并且使用同一种连接类型或同一个视觉样式,可以合并成一条线。
7.3 装饰件
当前逻辑:
if node.decorations.radiator:
createRadiator(node)
目标逻辑:
for each decorationKey in node.decorations:
style = presentation.decorationStyles[decorationKey]
createKnownDecoration(decorationKey, node, style)
首期只实现 radiator 这一种已知装饰件。后续新增装饰件时,应先在前端注册一个安全的已知 renderer,再允许数据库使用该 decoration_key。
7.4 图例
当前图例由 HTML 写死。目标是:
- 页面保留空的
<div id="legend">; - 前端遍历
presentation.legendItems; - 根据
itemType和styleRef找到对应样式; - 创建线段或色块示例。
这样图例顺序、文案和显示/隐藏由数据库决定。
8. Seed 数据建议
首期 seed 应把当前用户已认可的视觉效果完整落库:
8.1 节点样式
style_key |
用途 |
|---|---|
core-card |
广寒核心舱 |
node-card |
普通舱段 |
power-card |
电力模块分支 |
power-node-card |
电力模块中心节点 |
vehicle-card |
吴刚/冰轮等车辆 |
8.2 连接样式
connection_type |
图例 | 要求 |
|---|---|---|
structural_mount |
结构直连 | 灰蓝实线 |
pressurized_passage |
加压走道 | 绿色实线 |
fuel |
燃料 / 供电 | 紫色虚线 |
power |
燃料 / 供电 | 紫色虚线 |
8.3 装饰件样式
decoration_key |
用途 |
|---|---|
radiator |
电力模块四个大分支顶部的大型折叠散热面板 |
散热器继续不是独立组件,不出现在弹窗里,不覆盖加压走道。
9. 测试策略
9.1 API 测试
新增测试应覆盖:
- snapshot 响应包含
layout.presentation; nodeStyles包含所有diagram_nodes.style_key;edgeStyles包含当前快照里所有connections.type;legendItems按displayOrder排序;radiator装饰件样式存在;- 缺失样式时返回明确配置错误。
9.2 前端源码测试
源码测试应确认:
- 页面不再硬编码图例文字;
- 页面不再通过
.core-card、.pressurized等 CSS 类决定业务颜色; - 页面读取
presentation.nodeStyles、presentation.edgeStyles、presentation.decorationStyles、presentation.legendItems。
9.3 浏览器端到端测试
E2E 应覆盖:
- GHC-01、GHC-05 正常渲染;
- GHC-05 有两条绿色加压走道;
- 图例来自 API;
- 打开模块弹窗正常;
- 修改测试数据库中的
pressurized_passage.stroke_color后,浏览器渲染颜色变化; - 修改
core-card.fill_color后,核心舱填充颜色变化; - 历史任务节点/连接/散热器数量不回退。
9.4 截图验收
实现阶段每次页面修改后仍遵循现有要求:必须自己截图检查效果。
至少保留:
- GHC-05 顶部/中部/底部截图;
- GHC-01 早期快照截图;
- 图例截图;
- 修改测试主题颜色后的验证截图。
10. 实施分期建议
Phase 1:数据库与 API
- 新增表现层迁移;
- seed 当前已认可样式;
- API 返回
layout.presentation; - 增加 API 单元测试。
Phase 2:前端切换
- 图例改为从
presentation.legendItems生成; - 节点、连接、散热器改为使用 API 样式;
- 移除业务颜色 CSS 的事实来源角色;
- 保留通用 CSS 和交互样式。
Phase 3:验证与清理
- 跑完整单元测试和 E2E;
- 截图核对 GHC-01 到 GHC-05;
- 用测试数据临时改颜色,验证前端无代码修改即可变化;
- 更新验收报告。
11. 与未来扩展的关系
未来核心舱北、东、西三方向会继续扩展十字结构,北部成为多台吴刚着陆器连接点,西部成为多个 rover 连接点。B1 表现层设计支持这种扩展:
- 新节点继续写入
diagram_nodes; - 新节点使用已有或新增
style_key; - 新连接类型如果需要新视觉表现,只新增
diagram_edge_styles和diagram_legend_items; - 未来扩展锚点仍不在页面显示,只有实际组件在快照时间存在后才渲染。
如果将来需要同时支持“工程总图”“简化拓扑图”“施工阶段图”等多个视图,可以新增不同 diagram_layouts.layout_key,并让 API 支持 layout= 参数。但这不是本轮 B1 的首期目标。
12. 决策摘要
本设计采用 B1:表现层数据库化。
明确放入数据库:
- 节点位置和尺寸;
- 节点颜色、描边、圆角;
- 连接线颜色、线宽、虚实线;
- 散热器基础视觉参数;
- 图例项。
明确留在前端:
- 页面壳、弹窗、工具栏、响应式;
- hover/focus;
- 文本截断;
- SVG 创建和交互事件;
- 安全的已知装饰件 renderer。
这个边界能满足“由后台数据库决定各组件渲染位置、颜色等信息”,同时避免把数据库变成一个难维护的 HTML/CSS 仓库。