docs(topology): design database-driven presentation layer

This commit is contained in:
2026-07-09 22:51:14 +08:00
parent f5cbe1b028
commit 479e37e00f
@@ -0,0 +1,627 @@
# 广寒基地拓扑图表现层数据库化设计
日期: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. 目标
本设计的目标是让后端数据库决定以下内容:
1. 节点矩形的位置、尺寸、圆角、填充色、描边色、描边宽度。
2. 连接线的颜色、线宽、虚实线、图例文案。
3. 装饰件的基础视觉参数,例如散热器的颜色、描边、标签。
4. 工程图图例的项目、顺序、显示名称和示例样式。
5. 节点 `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 = 9`
- `dash_array = null`
- `legend_label = '加压走道'`
数据库不保存:
- CSS 选择器;
- JavaScript 函数名;
- HTML 片段;
- 浏览器专用的布局技巧。
### 4.3 可迁移、可扩展、可回退
新增表现层表应与现有 `diagram_layouts` 兼容。实现时可以分阶段上线:
1. 先建表并 seed 当前样式;
2. API 同时返回旧字段和新 `presentation`
3. 前端切换到新字段;
4. 测试通过后,保留旧 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` | 审计字段 |
约束:
```sql
UNIQUE (base_id, theme_key)
```
`tokens` 示例:
```json
{
"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` | 审计字段 |
约束:
```sql
UNIQUE (theme_id, style_key)
```
现有样式键继续保留:
- `node-card`
- `core-card`
- `power-card`
- `power-node-card`
- `vehicle-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` | 审计字段 |
约束:
```sql
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` | 审计字段 |
约束:
```sql
UNIQUE (theme_id, decoration_key)
```
`render_params` 示例:
```json
{
"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` | 是否显示 |
约束:
```sql
UNIQUE (theme_id, legend_key)
CHECK (item_type IN ('edge', 'decoration', 'node'))
```
当前图例建议:
1. `structural`:结构直连,引用 `structural_mount`
2. `pressurized`:加压走道,引用 `pressurized_passage`
3. `utility`:燃料 / 供电,引用 `fuel``power` 中任一代表样式
4. `radiator`:大型折叠散热面板,引用 `radiator`
### 5.6 `diagram_layouts` 扩展
为布局绑定默认主题:
| 字段 | 类型 | 说明 |
|---|---|---|
| `theme_id` | `uuid` | 引用 `diagram_themes(id)`,可空;为空时使用基地默认主题 |
建议约束:
```sql
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`
建议结构:
```json
{
"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 查询布局时:
1. 使用默认布局;
2. 如果布局绑定了主题,使用该主题;
3. 如果布局未绑定主题,使用基地默认主题;
4. 如果节点引用的 `style_key` 不存在,API 应返回明确错误或在响应中标记 `presentationWarnings`
建议首期采用“API 返回 500/配置错误,不静默降级”的策略。拓扑图是工程资料,缺失样式不应伪装成正确页面。
前端可以有开发兜底色,但只用于防止页面彻底空白,不能作为测试验收依据。
### 6.3 样式值安全
API 应只返回白名单字段。颜色值应校验为:
- `#RGB`
- `#RRGGBB`
- `rgb(...)`
- `rgba(...)`
- 少量受控命名色如 `transparent`
首期建议只使用 `#RRGGBB`,简单、稳定、可测试。
`dash_array` 只允许数字和空格,例如 `13 10`
`line_cap` 只允许 `butt``round``square`
`render_params` 只允许预定义键,不允许任意 SVG 属性透传。
## 7. 前端渲染设计
### 7.1 节点
当前逻辑:
```text
rect.class = node.styleKey
CSS 决定 fill/stroke/stroke-width
```
目标逻辑:
```text
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 连接
当前逻辑:
```text
connection.type -> connectionClass(type)
CSS 决定颜色和虚实线
```
目标逻辑:
```text
style = presentation.edgeStyles[connection.type]
line.stroke = style.strokeColor
line.strokeWidth = style.strokeWidth
line.strokeDasharray = style.dashArray
line.strokeLinecap = style.lineCap
```
连接合并逻辑保留:多个数据库连接如果落在同一对视觉节点之间,并且使用同一种连接类型或同一个视觉样式,可以合并成一条线。
### 7.3 装饰件
当前逻辑:
```text
if node.decorations.radiator:
createRadiator(node)
```
目标逻辑:
```text
for each decorationKey in node.decorations:
style = presentation.decorationStyles[decorationKey]
createKnownDecoration(decorationKey, node, style)
```
首期只实现 `radiator` 这一种已知装饰件。后续新增装饰件时,应先在前端注册一个安全的已知 renderer,再允许数据库使用该 `decoration_key`
### 7.4 图例
当前图例由 HTML 写死。目标是:
1. 页面保留空的 `<div id="legend">`
2. 前端遍历 `presentation.legendItems`
3. 根据 `itemType``styleRef` 找到对应样式;
4. 创建线段或色块示例。
这样图例顺序、文案和显示/隐藏由数据库决定。
## 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 仓库。