Skip to content

Commit 085dd65

Browse files
alswlcursoragent
andcommitted
feat: ER 图仅表名视图切换与关系线吸附
- 新增 ViewMode 常量与 parseDatabaseToER(database, { tableOnly }) - 仅表名时节点不渲染 list 端口,边 source/target 仅 cell 以吸附表名节点 - Viewer 增加「仅表名」Switch、viewMode 状态与 localStorage 记忆 - 新增 E2E 场景:切换仅表名 → 关系线保留 → 切回完整视图 - 规格与任务:specs/003-toggle-columns-table-names Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f030310 commit 085dd65

File tree

11 files changed

+704
-29
lines changed

11 files changed

+704
-29
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Viewer 组件与 ER 服务契约
2+
3+
**Feature**: 003-toggle-columns-table-names
4+
**Phase**: 1
5+
6+
本功能为纯前端;以下为组件与服务的**接口契约**(等价于 API 契约)。
7+
8+
---
9+
10+
## 1. Viewer 组件 (src/components/viewer/viewer.tsx)
11+
12+
### Props(入参)
13+
14+
| 属性 | 类型 | 必填 | 说明 |
15+
| --- | --- | --- | --- |
16+
| database | `import('@dbml/core').Database` 或项目内使用的 Database 类型 || 当前解析得到的 DBML 数据库结构,用于生成 ER 图。 |
17+
18+
### 新增内部状态(本功能)
19+
20+
| 状态 | 类型 | 说明 |
21+
| --- | --- | --- |
22+
| viewMode | `'full' \| 'tableOnly'` | 当前视图模式;默认 `'full'`。可选从 localStorage 初始化。 |
23+
24+
### 用户动作 → 行为
25+
26+
| 用户动作 | 行为 |
27+
| --- | --- |
28+
| 点击「仅表名」开关(设为 tableOnly) | 设置 viewMode 为 `'tableOnly'`;使用 `parseDatabaseToER(database, { tableOnly: true })` 得到 model,执行 layout 后 `graph.fromJSON(models)`|
29+
| 点击「完整」开关(设为 full) | 设置 viewMode 为 `'full'`;使用 `parseDatabaseToER(database, { tableOnly: false })` 得到 model,执行 layout 后 `graph.fromJSON(models)`|
30+
| database 变更(由父组件传入) | 使用当前 viewMode 重新调用 `parseDatabaseToER(database, { tableOnly: viewMode === 'tableOnly' })`,layout 后更新图。 |
31+
32+
### 工具栏新增
33+
34+
- 一个切换控件(Button 或 Switch),标签如「仅表名」/「表名」;切换时更新 viewMode 并触发上述数据流。
35+
36+
---
37+
38+
## 2. ER 服务 (src/services/er/index.ts)
39+
40+
### parseDatabaseToER
41+
42+
**签名(扩展后)**
43+
44+
```ts
45+
function parseDatabaseToER(
46+
database: Database,
47+
options?: { tableOnly?: boolean },
48+
): { nodes: NodeData[]; edges: EdgeData[] };
49+
```
50+
51+
**参数**
52+
53+
| 参数 | 类型 | 说明 |
54+
| --- | --- | --- |
55+
| database | Database | 与现有一致。 |
56+
| options.tableOnly | boolean | 可选,默认 false。为 true 时:节点不包含 list 组端口(仅表头);边的 source/target 不包含 port,仅 cell。 |
57+
58+
**返回值**:与现有一致,`nodes``edges` 符合 X6 `Model.FromJSONData` 的节点/边结构。
59+
60+
### parseTableToNode(内部或导出)
61+
62+
`options?.tableOnly === true` 时:
63+
64+
- 返回的节点 `ports` 不包含 `list` 组(或 list 组为空)。
65+
- 节点 `height` 为单行高度(如 24)。
66+
67+
### parseRef(内部或导出)
68+
69+
`options?.tableOnly === true` 时:
70+
71+
- 返回的 edge 的 `source` 仅包含 `cell`,不包含 `port`
72+
- 返回的 edge 的 `target` 仅包含 `cell`,不包含 `port`
73+
74+
---
75+
76+
## 3. 常量(可选)
77+
78+
若集中管理枚举,可新增:
79+
80+
```ts
81+
// src/constants/viewMode.ts 或等效路径
82+
export type ViewMode = 'full' | 'tableOnly';
83+
export const VIEW_MODE = { FULL: 'full', TABLE_ONLY: 'tableOnly' } as const;
84+
```
85+
86+
以上契约在实现时须保持;测试与 E2E 可依赖这些接口编写。
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Data Model: 003-toggle-columns-table-names
2+
3+
**Feature**: ER 图列切换与仅表名视图
4+
**Phase**: 1
5+
6+
本功能不引入新的持久化数据或后端实体;仅扩展前端**视图状态**与 ER 图生成时的**参数**
7+
8+
---
9+
10+
## 1. 视图模式(ViewMode)
11+
12+
| 字段/概念 | 类型 | 说明 |
13+
| --- | --- | --- |
14+
| ViewMode | `'full' \| 'tableOnly'` | 当前 ER 图展示模式:完整(表名+列)或仅表名。 |
15+
| 持久化 | 可选 | 可存入 `localStorage`(如 key: `dbml-editor-er-view-mode`),会话间记忆用户选择。 |
16+
17+
**校验规则**:仅允许枚举值;默认 `'full'`
18+
19+
---
20+
21+
## 2. 现有实体(本功能中的使用方式)
22+
23+
- **Database**@dbml/core):不变;仍为解析结果,Viewer 的 `props.database`
24+
- **ER 节点(er-rect)**:由 `parseTableToNode(table, schemaName, options?)` 生成;当 `options.tableOnly === true` 时,不向 `ports` 添加 `list` 组,仅保留表头,节点 `height` 为单行(如 24px)。
25+
- **ER 边(Ref)**:由 `parseRef(ref, options?)` 生成;当 `options.tableOnly === true` 时,返回的 edge 的 `source`/`target` 只包含 `cell`,不包含 `port`,以便 X6 使用节点锚点吸附。
26+
27+
---
28+
29+
## 3. 状态转换
30+
31+
- **用户点击「仅表名」**:ViewMode `full``tableOnly`;触发重新 `parseDatabaseToER(database, { tableOnly: true })` + layout + fromJSON。
32+
- **用户点击「完整」**:ViewMode `tableOnly``full`;触发重新 `parseDatabaseToER(database, { tableOnly: false })` + layout + fromJSON。
33+
- **DBML 变更**`database` 更新,当前 ViewMode 不变,按当前模式重新生成图并更新。
34+
35+
无其他状态机;无服务端同步。
36+
37+
---
38+
39+
## 4. 与章程的一致性
40+
41+
- 数据与解析均在客户端;ViewMode 不离开浏览器(可选 localStorage)。
42+
- 无新增数据库表或 API 契约;仅前端组件状态与 ER 转换参数。
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Implementation Plan: ER 图列切换与仅表名视图
2+
3+
**Branch**: `003-toggle-columns-table-names` | **Date**: 2026-02-09 | **Spec**: [spec.md](./spec.md)
4+
**Input**: Feature specification from `/specs/003-toggle-columns-table-names/spec.md`
5+
6+
## Summary
7+
8+
在 ER 图工具栏增加「仅表名」视图切换:隐藏列、只显示表名块,关系线保留并吸附到表名节点;切换回完整视图时恢复列与端口绑定。技术上通过视图模式状态 + X6 节点/边数据或渲染分支实现,不改变 DBML 或 @dbml/core 解析结果。
9+
10+
## Technical Context
11+
12+
**Language/Version**: TypeScript 5.x, Node.js 18
13+
**Primary Dependencies**: Umi 4, Ant Design 5.x, AntV X6, @antv/layout (Dagre), @dbml/core, Monaco Editor
14+
**Storage**: N/A(纯前端;可选 localStorage 记忆视图模式)
15+
**Testing**: Jest(单元), Playwright(E2E)
16+
**Target Platform**: 现代浏览器(客户端 Web);**Project Type**: single(前端单体)
17+
**Performance Goals**: 100 表以内 schema 下视图切换与图同步 &lt;500ms(章程要求)
18+
**Constraints**: 隐私优先(全部客户端)、TypeScript strict、无 any/ts-ignore(章程)
19+
**Scale/Scope**: 单页 ER 编辑与可视化,表数量级 10² 以内
20+
21+
## Constitution Check
22+
23+
_GATE: Must pass before Phase 0 research. Re-check after Phase 1 design._
24+
25+
| 章程条款 | 状态 | 说明 |
26+
| --- | --- | --- |
27+
| I. 隐私优先 || 视图模式与渲染仅在前端,无数据上传。 |
28+
| II. 实时可视化 || 切换为展示层状态,不改变解析;DBML 变更后仍按现有流程同步,模式保持。 |
29+
| III. 类型安全 || 新增 ViewMode 等类型,无 any;X6/Monaco 等已有例外在章程允许范围内。 |
30+
| IV. 测试纪律 || 计划新增 E2E 覆盖「仅表名」切换与关系线吸附;单元测试覆盖 viewMode 与 ER 转换逻辑(若有新函数)。 |
31+
| V. 浏览器优先 || 功能纯前端,无服务端依赖。 |
32+
| 质量门禁 || TypeScript strict、lint、单元/E2E 通过方可合并。 |
33+
34+
**结论**: 无违规,通过门禁。
35+
36+
## Project Structure
37+
38+
### Documentation (this feature)
39+
40+
```text
41+
specs/003-toggle-columns-table-names/
42+
├── plan.md # This file
43+
├── research.md # Phase 0 output
44+
├── data-model.md # Phase 1 output
45+
├── quickstart.md # Phase 1 output
46+
├── contracts/ # Phase 1 output (组件/状态契约)
47+
└── tasks.md # Phase 2 output (/speckit.tasks - NOT created by plan)
48+
```
49+
50+
### Source Code (repository root)
51+
52+
```text
53+
src/
54+
├── components/
55+
│ ├── editor/
56+
│ └── viewer/ # ER 图:Viewer.tsx,视图模式状态与工具栏开关
57+
├── constants/ # 可选:ViewMode 枚举
58+
├── models/
59+
├── nodes/
60+
│ └── er.ts # er-rect 注册;可能扩展「仅表头」或端口可见性
61+
├── pages/
62+
│ └── Home/
63+
├── services/
64+
│ ├── dbml/
65+
│ └── er/ # parseDatabaseToER;可能支持 tableOnly 的 model 形态
66+
└── utils/
67+
68+
tests/
69+
├── e2e/ # 新增:仅表名切换与关系线吸附场景
70+
├── setup.ts
71+
└── (unit under src/**/__tests__)
72+
```
73+
74+
**Structure Decision**: 单项目结构(Umi 4 前端),ER 相关逻辑在 `src/components/viewer``src/services/er``src/nodes/er.ts`;本功能不新增后端或新包。
75+
76+
## Complexity Tracking
77+
78+
> **Fill ONLY if Constitution Check has violations that must be justified**
79+
80+
(无违规,本节留空。)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Quickstart: 003-toggle-columns-table-names
2+
3+
**Feature**: ER 图列切换与仅表名视图
4+
**Branch**: `003-toggle-columns-table-names`
5+
6+
## 前置条件
7+
8+
- Node.js 18+
9+
- pnpm
10+
- 仓库根目录已执行 `pnpm install`
11+
12+
## 本地运行
13+
14+
```bash
15+
cd /Users/alswl/dev/my/dbml-editor
16+
pnpm run dev
17+
```
18+
19+
在浏览器中打开控制台输出的本地地址(通常为 http://localhost:8000)。在编辑器中输入或加载包含多表与 Ref 的 DBML,右侧 ER 图会显示。
20+
21+
## 本功能验证步骤
22+
23+
1. **打开应用**:确保 ER 图已渲染(有至少 2 个表及 1 条关系线)。
24+
2. **找切换控件**:在 ER 图区域旁的缩放工具栏附近,应有「仅表名」或类似开关/按钮。
25+
3. **切换到仅表名**:点击后,图中每个表应只显示表名矩形,不显示列;关系线仍存在且两端吸附在表名节点上。
26+
4. **切回完整视图**:再次点击切换,列重新显示,关系线连接到对应列。
27+
5. **编辑 DBML**:在左侧编辑器中修改表或 Ref,确认图在约 500ms 内更新,且当前视图模式(仅表名/完整)保持正确。
28+
29+
## 测试
30+
31+
```bash
32+
# 单元测试
33+
pnpm test
34+
35+
# E2E(需先启动或使用 playwright 默认)
36+
pnpm run test:e2e
37+
```
38+
39+
本功能完成后,应至少有一条 E2E 场景覆盖:进入页面 → 等待 ER 图加载 → 点击仅表名 → 断言关系线存在且连接节点 → 切回完整 → 断言列显示。
40+
41+
## 相关文件
42+
43+
- 规格与计划:`specs/003-toggle-columns-table-names/spec.md``plan.md`
44+
- 研究与数据模型:`research.md``data-model.md`
45+
- 契约:`contracts/viewer-component-api.md`
46+
- 实现涉及:`src/components/viewer/viewer.tsx``src/services/er/index.ts`、可选 `src/constants/``src/nodes/er.ts`(若需微调节点)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Research: 仅表名视图与关系线吸附
2+
3+
**Feature**: 003-toggle-columns-table-names
4+
**Phase**: 0
5+
6+
## 1. 仅表名视图在 AntV X6 中的实现方式
7+
8+
### Decision(决策)
9+
10+
采用 **同一节点类型 `er-rect` + 视图模式控制「是否渲染 list 组端口」** 的方案:在 `tableOnly` 模式下,生成/更新到画布的节点不包含 `list` 组端口(或端口数量为 0),节点高度仅保留表头一行;关系线在 tableOnly 模式下改为连接**节点主体**(或每个表一个虚拟的「表级」端口),通过 X6 的 `source/target: { cell, port? }` 在 tableOnly 时只指定 `cell` 并使用节点锚点(如 `midSide`)实现吸附。
11+
12+
### Rationale(理由)
13+
14+
- 现有 `parseDatabaseToER` 已按「表 → 节点 + 列 → ports」生成数据;边通过 `source.port` / `target.port` 连到具体列。若在 tableOnly 时改为不传 ports(或传空 list 组),节点自然只渲染表头;边若仍带 port 会失效,故需在 tableOnly 下改为仅指定 `cell` 并用节点默认锚点,这样关系线会吸附到表名节点边框/中心,满足「snap to table names」。
15+
- 不新增节点类型可避免重复维护两套 markup/attrs,且切换时只需用同一套 `parseTableToNode` 的「是否包含列」分支即可。
16+
17+
### Alternatives considered(考虑的备选)
18+
19+
- **备选 A**:注册第二种节点 `er-rect-table-only`,无 list 端口,边连节点。缺点:两套节点样式需同步,切换时需整图替换 shape,复杂度更高。
20+
- **备选 B**:保留所有端口但用 CSS/attrs 隐藏端口视觉。缺点:布局上节点仍占列高,无法实现「只显示表名」的紧凑布局;边仍连到隐藏端口,对「吸附到表名」的语义不直观。
21+
22+
---
23+
24+
## 2. 关系线在仅表名视图下的锚点策略
25+
26+
### Decision(决策)
27+
28+
**tableOnly** 模式下,边的 `source`/`target` 只设置 `cell`,不设置 `port`;依赖 Graph 的 `connecting.anchor` 或边的 `anchor` 使用 **`midSide`(或 `left`/`right` 按方向)**,使连线吸附在表名节点的侧面中点,保持与 Dagre 水平布局(LR)一致。
29+
30+
### Rationale(理由)
31+
32+
- X6 当 target/source 不指定 port 时,会使用节点的默认锚点;`midSide` 已在本项目 Graph 配置中使用,表名节点在 tableOnly 下为单行矩形,侧面中点即表名块边缘,语义清晰。
33+
- Dagre 的 `rankdir: 'LR'` 下,边从节点左右两侧连出,用水平方向的 midSide 可避免连线穿过节点。
34+
35+
### Alternatives considered(考虑的备选)
36+
37+
- **备选 A**:为每个表在 tableOnly 下暴露一个虚拟 port(如 `table-only`),边仍连到该 port。实现略复杂,且效果与「不设 port + 节点锚点」等价。
38+
- **备选 B**:使用 `anchor: 'center'`。缺点:多条边时都从中心连出,易重叠,可读性差。
39+
40+
---
41+
42+
## 3. 视图模式状态与数据流
43+
44+
### Decision(决策)
45+
46+
-**Viewer 组件** 内用 `useState<'full'|'tableOnly'>` 保存视图模式;工具栏增加一个 Switch/Button 切换。
47+
- **parseDatabaseToER** 增加可选参数 `viewMode`(或等价的 `tableOnly: boolean`):为 `tableOnly` 时,`parseTableToNode` 不添加 list 组端口;`parseRef` 在 tableOnly 时返回的 edge 不包含 `source.port`/`target.port`,仅包含 `source.cell`/`target.cell`
48+
-`props.database` 或视图模式变化时,重新执行 `parseDatabaseToER(..., viewMode)``setModels(layout.layout(...))`,保证图与模式同步。
49+
50+
### Rationale(理由)
51+
52+
- 单一数据源:ER 图数据仍由 `database + viewMode` 推导,不保留「两套 model」;切换时重算 model 并 fromJSON,逻辑简单且与现有 `useEffect([props.database])` 一致。
53+
- 可选:将 viewMode 写入 localStorage,在 Viewer 初始化时读回,实现「会话内记忆」。
54+
55+
### Alternatives considered(考虑的备选)
56+
57+
- **备选 A**:在 X6 层动态显示/隐藏端口。需要遍历节点改 attrs 或 port 的 visible,且边仍连到 port,需同时改边的 source/target,与「吸附到表名」目标不符。
58+
- **备选 B**:维护两套 JSON model 并切换时替换。冗余且易不同步,不采用。
59+
60+
---
61+
62+
## 4. 布局(Dagre)与节点尺寸
63+
64+
### Decision(决策)
65+
66+
- tableOnly 下节点 `height` 为单行(如 24px),`width` 可与完整视图一致(如 150px);Dagre 布局在每次 `layout.layout(m)` 时按当前节点尺寸计算,因此切换视图后重新执行一次 layout,避免重叠或留白过大。
67+
- 不改变 Dagre 的 `rankdir`/`ranksep`/`nodesep` 等配置;仅因节点变小,整体图会更紧凑。
68+
69+
### Rationale(理由)
70+
71+
- 现有代码已在 `props.database` 变化时执行 `setModels(layout.layout(m))`;扩展为 `viewMode` 变化也触发同样流程即可,Dagre 会根据新尺寸重新排布,关系线由 router 重算,自然「snap」到新位置。
72+
73+
### Alternatives considered(考虑的备选)
74+
75+
- **备选 A**:不重新 layout,只改节点高度。可能导致节点重叠(原为多行高度,现为单行),不采用。
76+
- **备选 B**:tableOnly 使用不同 ranksep/nodesep。可后续做 UX 微调,非必须。
77+
78+
---
79+
80+
## 5. 小结(Phase 0 输出)
81+
82+
- **实现路径**:Viewer 内 viewMode 状态 + 工具栏开关;`parseDatabaseToER(database, { tableOnly })` 在 tableOnly 时不输出列端口、边不带 port;切换或 database 变化时重新 layout 并 fromJSON。
83+
- **锚点**:tableOnly 下边仅指定 cell,用节点默认锚点(midSide)实现吸附到表名节点。
84+
- **无未决项**:技术上下文无 NEEDS CLARIFICATION,可直接进入 Phase 1 设计与契约。

0 commit comments

Comments
 (0)