# 广寒基地拓扑图表现层数据库化设计 日期: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 仓库。