diff --git a/docs/superpowers/specs/2026-07-09-guanghan-topology-presentation-db-design.md b/docs/superpowers/specs/2026-07-09-guanghan-topology-presentation-db-design.md new file mode 100644 index 0000000..553f795 --- /dev/null +++ b/docs/superpowers/specs/2026-07-09-guanghan-topology-presentation-db-design.md @@ -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. 页面保留空的 `
`; +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 仓库。