From 479e37e00f18ee7ec532eceb2e9cfaa77949217a Mon Sep 17 00:00:00 2001 From: Armor00 <2654988228@qq.com> Date: Thu, 9 Jul 2026 22:51:14 +0800 Subject: [PATCH] docs(topology): design database-driven presentation layer --- ...uanghan-topology-presentation-db-design.md | 627 ++++++++++++++++++ 1 file changed, 627 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-09-guanghan-topology-presentation-db-design.md 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. 页面保留空的 `