开源贡献提案

DBX ER 图增强架构方案

面向 t8y2/dbx 项目的 ER 图模块重构与 ID 关联智能匹配方案,遵循 DBX 现有交互规范与存储模式

项目dbx v0.5.56
技术栈Tauri 2 + Vue 3 + Rust
协议Apache-2.0
日期2026-07-15
v4 — 遵循 DBX 操作规范与存储模式

方案审计与修订说明

#审计项v3 方案(原)v4 方案(修订)修订原因
1 工具栏交互 未对齐 DBX 现有按钮风格、布局结构和图标规范 严格遵循现有 shadcn-vue Button 规范、lucide 图标、工具栏布局 PR 需要与现有 UI 风格完全一致,否则会被 maintainer 要求修改
2 配置存储 新增 Rust 后端 match_rules.rs 持久化匹配规则 沿用 localStorage + dbx:diagram:... key 前缀 + safeLocalStorageGet/Set DBX 自定义关系已用此模式存储,不引入新的存储机制
3 后端改动 新增 match_rules.rs(Tauri Command)、修改 schema.rs 仅修改 schema.rs:新增 get_all_columnsColumnInfo.is_unique 删除存储相关后端改动,存储全部在前端 localStorage 完成

DBX 现有操作规范

在描述新方案之前,先明确需要遵循的现有规范。以下内容基于对 SchemaDiagramDialog.vue(v0.5.58)的源码分析。

Dialog 容器结构

<Dialog :open="open" @update:open="(v) => model = v">
  <DialogContent class="w-[94vw] h-[86vh] flex flex-col p-0">
    <DialogHeader class="px-4 py-3 border-b">
      <DialogTitle>Network图标 + "ER 图"标题</DialogTitle>
    </DialogHeader>
    <!-- 工具栏 -->
    <div class="flex items-center gap-2 border-b px-3 py-2 shrink-0 overflow-x-auto">
      ...按钮和选择器...
    </div>
    <!-- 可折叠面板(关系建模 / 匹配管理) -->
    <div v-if="showPanel" class="shrink-0 border-b">...</div>
    <!-- 画布 -->
    <div class="min-h-0 flex-1 bg-muted/20">...</div>
  </DialogContent>
</Dialog>

按钮规范

场景variantsize额外 class图标尺寸
带文字的操作按钮(建模关系、复制 SQL)outlinesmh-8 px-2 text-xsh-3.5 w-3.5 + mr-1
纯图标按钮(缩放、刷新、导出)ghosticonh-8 w-8h-4 w-4
模式切换按钮组(表模式/工程模式)ghostsmh-8 rounded-none px-2 text-xsh-3.5 w-3.5 + mr-1
面板内主要操作按钮(添加关系)defaultsmh-8 px-2 text-xsh-3.5 w-3.5 + mr-1

现有工具栏布局(改造前)

[Select:连接] [Select:数据库] [Select:Schema] | [🔍 搜索框] | [表模式][工程模式] | [🔗 建模关系] [📋 复制SQL] [关联表/全部表] | [Badge:表数] [Badge:关系数] [Badge:自定义关系数] | [⬇导出SVG] [🔄刷新] [➖缩小] [➕放大] [⧉重置布局]

现有存储模式

DBX 的自定义关系存储使用 原生 localStorage,key 格式为 dbx:diagram:relationships:v1:<connectionId>:<database>:<schema>。全项目另有一层安全封装 safeLocalStorageGet/Set/Remove(位于 lib/backend/safeStorage.ts),使用 globalThis.localStorage + try-catch。当前 ER 图模块直接使用原生 localStorage,本方案统一迁移到 safeLocalStorage 封装。

存储约束:不引入 Tauri plugin-store,不新增 Rust 后端存储命令。匹配规则的存储沿用 localStorage + dbx:diagram:... key 前缀,按"连接 + 数据库 + schema"粒度隔离,与自定义关系保持一致的存储范式。

现状分析与核心问题

DBX ER 图当前实现

DBX 的 ER 图功能完全自研,基于原生 HTML/CSS + SVG 渲染,没有引入任何第三方图可视化库。整个功能封装在一个约 1150 行的 SchemaDiagramDialog.vue 组件中。支持两种视图模式(Table View 和 Engineering View),正交折线连线路由(自动绕开中间表卡片),简单网格布局,缩放范围 0.6x - 1.5x,搜索过滤,聚焦模式,自定义关系建模(localStorage 持久化)和 JOIN SQL 自动生成。

与 DataGrip / Navicat 的差距

交互维度DataGripNavicatDBX 现状
自动关系推断物理外键 + 正则匹配虚拟外键仅物理外键仅物理外键 + 手动自定义 缺少智能匹配
布局算法多种可选布局 + 方向控制Auto-Layout 一键排列简单网格 无分层布局
缩放范围无硬限制无硬限制0.6x - 1.5x 范围过窄
框选框选复制搜索筛选完全缺失
撤销/重做Ctrl+Z/Y无限次 Undo/Redo完全缺失
连线交互显示/隐藏虚拟外键悬停高亮、编辑折点SVG 箭头连线不可交互 连线无交互

核心问题

关系发现能力弱

仅依赖物理外键。大量项目不建外键,或使用不支持外键的数据库(MongoDB、ClickHouse),ER 图上大量表呈现为孤岛。

布局与交互原始

网格布局无法体现表间逻辑关系,50+ 张表时连线交叉严重。缺少框选、撤销、连线交互等基本操作。

单体组件架构瓶颈

全部逻辑集中在单个 1150 行 Vue 组件中,渲染、布局、路由、交互、状态管理耦合,难以扩展和测试。

整体架构设计

技术选型

能力层DifyCozeDBX v4选型理由
画布框架ReactFlowFlowGram(Canvas)Vue FlowVue 3 项目;ReactFlow 忠实移植[6],gzip 49.8KB
布局引擎ELK.js(懒加载)自研ELK.js(打包)布局 + 正交边路由[7];打包确保离线可用
状态管理Zustand + ImmerMobXPinia(现有)DBX 已用 Pinia,撤销/重做内嵌到 store
配置存储localStorage + CRDT未知localStorage(现有)沿用 dbx:diagram:... key + safeLocalStorage

模块拆分

graph TB
    subgraph UI["UI 层 (Vue Components)"]
        A["SchemaDiagramDialog.vue
Dialog 容器 + 工具栏"] B["TableNode.vue
表卡片(Vue Flow 自定义节点)"] C["RelationshipEdge.vue
关系线(Vue Flow 自定义边)"] D["MatchPanel.vue
匹配规则管理面板"] end subgraph Adapter["适配层"] E["VueFlowAdapter
图数据 ↔ VueFlow 格式转换"] end subgraph Core["核心引擎 (lib/diagram/)"] F["GraphStore
Pinia + 撤销/重做历史栈"] G["LayoutManager
ELK.js 布局 + 正交边路由"] H["MatchEngine
ID 关联智能匹配"] I["MatchStorage
匹配规则 localStorage 读写"] end subgraph Backend["后端 (Rust) — 仅微调"] J["schema.rs
+get_all_columns
+ColumnInfo.is_unique"] end A --> E B --> E C --> E F --> E A --> G A --> H H --> I F --> J
图 1: v4 模块拆分架构

工具栏改造方案

新工具栏严格沿用 DBX 现有布局:选择器 → 搜索 → 模式切换 → 操作按钮 → Badge → 图标按钮。新增按钮遵循现有 Button 规范,新增图标使用 lucide-vue。Vue Flow 的 Controls 和 MiniMap 作为浮动组件叠加在画布上,不占用工具栏空间。

改造后工具栏布局

[Select:连接] [Select:数据库] [Select:Schema] | [🔍 搜索框] | [表模式][工程模式] | [🔗 建模关系] [🔍 自动匹配] [📊 自动布局 ▾] [📋 复制SQL] [关联表/全部表] | [Badge:表数] [Badge:关系数] [Badge:匹配关系数] [Badge:自定义关系数] | [⬇导出SVG] [🔄刷新] ➖缩小 ➕放大 [⧉重置布局]

新增/变更按钮明细

按钮位置规范图标说明
自动匹配新增 "建模关系"按钮右侧 variant="outline" size="sm" class="h-8 px-2 text-xs" ScanSearch(lucide,h-3.5 w-3.5) 切换打开/关闭 MatchPanel 面板,复用现有关系面板的条件渲染模式
自动布局新增 "自动匹配"按钮右侧 variant="outline" size="sm" class="h-8 px-2 text-xs" LayoutGrid(lucide,h-3.5 w-3.5) 点击触发 ELK 自动布局;下拉可选方向(LR / TB / RL / BT)
匹配关系 Badge新增 Badge 区域,自定义关系 Badge 前 variant="secondary" class="h-6 text-xs" 显示当前自动匹配推断的关系数,点击切换显示/隐藏
缩放按钮变更 工具栏右侧图标区 移除,由 Vue Flow Controls 替代 Vue Flow 的 <Controls /> 浮动在画布右下角,包含 +/-/fit/lock 按钮
重置布局变更 工具栏右侧 移除,由"自动布局"按钮替代 "自动布局"按钮已包含重排功能,无需单独的重置按钮
面板复用模式:MatchPanel 面板的交互模式完全复用现有关系建模面板的设计——通过 v-if="showMatchPanel" 条件渲染在工具栏和画布之间,包含 Select 选择器 + Button 操作 + Badge 列表。用户在 DBX 中看到的是一个与现有"建模关系"面板风格完全一致的"自动匹配"面板。

存储方案

所有匹配相关数据存储在前端 localStorage,使用 DBX 现有的 safeLocalStorageGet/Set/Remove 封装。key 格式与自定义关系保持一致的 dbx:diagram:... 前缀 + "连接 + 数据库 + schema"粒度。

存储 key 设计

数据类型key 格式现有/新增
自定义关系dbx:diagram:relationships:v1:<connId>:<db>:<schema>现有(保持不变)
匹配确认记录dbx:diagram:match-confirms:v1:<connId>:<db>:<schema>新增
匹配忽略记录dbx:diagram:match-ignores:v1:<connId>:<db>:<schema>新增
用户自定义正则规则dbx:diagram:match-rules:v1:<connId>:<db>:<schema>新增
匹配全局开关dbx:diagram:match-enabled新增

存储实现代码

// match-storage.ts
import { safeLocalStorageGet, safeLocalStorageSet, safeLocalStorageRemove }
  from "@/lib/backend/safeStorage";

function matchStorageKey(
  type: "match-confirms" | "match-ignores" | "match-rules",
  connectionId: string,
  database: string,
  schema: string,
): string {
  return ["dbx", "diagram", type, "v1", connectionId, database, schema].join(":");
}

// 加载已确认的匹配关系
export function loadMatchConfirms(
  connectionId: string, database: string, schema: string
): string[] {
  const key = matchStorageKey("match-confirms", connectionId, database, schema);
  try {
    return JSON.parse(safeLocalStorageGet(key) || "[]");
  } catch { return []; }
}

// 保存已确认的匹配关系
export function saveMatchConfirms(
  ids: string[], connectionId: string, database: string, schema: string
): void {
  const key = matchStorageKey("match-confirms", connectionId, database, schema);
  safeLocalStorageSet(key, JSON.stringify(ids));
}

// 加载已忽略的匹配关系
export function loadMatchIgnores(
  connectionId: string, database: string, schema: string
): string[] {
  const key = matchStorageKey("match-ignores", connectionId, database, schema);
  try {
    return JSON.parse(safeLocalStorageGet(key) || "[]");
  } catch { return []; }
}

// 保存已忽略的匹配关系
export function saveMatchIgnores(
  ids: string[], connectionId: string, database: string, schema: string
): void {
  const key = matchStorageKey("match-ignores", connectionId, database, schema);
  safeLocalStorageSet(key, JSON.stringify(ids));
}

// 匹配全局开关(跨连接共享)
export function isAutoMatchEnabled(): boolean {
  return safeLocalStorageGet("dbx:diagram:match-enabled") !== "false";
}

export function setAutoMatchEnabled(enabled: boolean): void {
  safeLocalStorageSet("dbx:diagram:match-enabled", String(enabled));
}
不新增 Rust 后端存储:v3 方案中的 match_rules.rs(Tauri Command: save_match_rules / load_match_rules)已删除。匹配规则的存储量很小(通常几十条 JSON),localStorage 完全胜任,且与 DBX 现有的自定义关系存储方式保持一致。

智能 ID 关联匹配引擎

本方案中最有价值的增量能力。DataGrip 通过正则表达式虚拟外键实现了类似功能[1],但需要用户手动配置。本方案内置开箱即用的自动匹配策略。

匹配策略分层

优先级策略规则置信度
P0物理外键读取 INFORMATION_SCHEMA 外键约束100%
P1命名约定{table}_id / {table}_uuid → 目标表主键,支持 snake_case / camelCase高(自动确认)
P2类型签名P1 + 源列与目标列类型兼容(如都是 bigint高(自动确认)
P3正则规则用户自定义正则,如 (.*)_id$1.id[1]中(需确认)

匹配算法核心逻辑

function inferRelationships(tables: TableMeta[]): InferredRelationship[] {
  const results: InferredRelationship[] = [];
  const tableNameSet = new Set(tables.map(t => t.name));
  const primaryKeys = buildPrimaryKeyIndex(tables);

  for (const table of tables) {
    for (const column of table.columns) {
      if (column.is_primary_key) continue;

      // P1: 命名约定匹配
      const match = column.name.match(/^(.+?)_(?:id|uuid|pk)$/i);
      if (!match) continue;

      const candidateTable = toSnakeCase(match[1]);
      if (!tableNameSet.has(candidateTable)) continue;

      const targetPK = primaryKeys.get(candidateTable);
      if (!targetPK) continue;

      // P2: 类型签名校验
      if (!isTypeCompatible(column.data_type, targetPK.data_type)) continue;

      results.push({
        sourceTable: table.name,
        sourceColumn: column.name,
        targetTable: candidateTable,
        targetColumn: targetPK.name,
        confidence: 'high',
        strategy: 'naming_convention',
      });
    }
  }
  return deduplicate(results);
}

匹配结果与存储交互

匹配引擎运行时需要与 match-storage.ts 交互,过滤已确认和已忽略的记录:

// match-engine.ts 中的过滤逻辑
function filterByStorage(
  inferred: InferredRelationship[],
  confirms: string[],   // 从 localStorage 加载
  ignores: string[],    // 从 localStorage 加载
): MatchResult {
  const confirmed = inferred.filter(r => confirms.includes(r.id));
  const pending = inferred.filter(r =>
    !confirms.includes(r.id) && !ignores.includes(r.id)
      && r.confidence === 'high'
  );
  const conflicts = pending.filter(r => hasMultipleTargets(r, pending));
  return {
    relationships: [...confirmed, ...pending.filter(r => !conflicts.includes(r))],
    conflicts,
    pending: conflicts,
    stats: { total: inferred.length, high: confirmed.length + pending.length, ... },
  };
}
视觉区分:物理外键实线高亮;自动匹配(高置信)虚线半透明;待确认关系点线灰色。工具栏 Badge 区的"匹配关系"Badge 点击可切换显示/隐藏。

Vue Flow + ELK.js 交互设计

Vue Flow 提供的开箱即用能力

能力Vue Flow 原生DBX v1 中
节点拖拽内置 draggable手动 mousedown/move/up
缩放与平移内置 viewport,无范围限制自研 diagramZoom.ts,0.6x-1.5x
框选多选SelectionMode.Partial缺失
MiniMap<MiniMap /> 浮动组件缺失
Controls<Controls /> 浮动组件(替代工具栏 +/- 按钮)手动按钮
背景网格<Background />CSS 背景
虚拟化onlyRenderVisibleElements缺失

ELK.js 布局配置

// elk-layout.ts
import ELK from 'elkjs/lib/elk.bundled.js';
const elk = new ELK();

export async function computeLayout(
  graph: DiagramGraph, options: LayoutOptions
): Promise<LayoutResult> {
  const elkGraph = buildElkGraph(graph, options);
  const result = await elk.layout(elkGraph);
  return extractLayoutResult(result);
}
配置项说明
elk.algorithmlayeredSugiyama 分层布局
elk.directionRIGHT / DOWN工具栏下拉切换
edgeRoutingORTHOGONAL正交折线,自动避开节点
nodePlacementBRANDES_KOEPF平衡对齐(同 Dify)
crossingMinimizationLAYER_SWEEP交叉最小化
layering.strategyNETWORK_SIMPLEX最小化边跨度
separateConnectedComponentstrue自动分离孤立子图

Pinia 原生撤销/重做

不引入第三方撤销库。在 GraphStore(Pinia)中手动维护 historyStack + redoStack,使用 lodash-escloneDeep 做快照(DBX 已通过 shadcn-vue 间接依赖 lodash-es,无新增依赖)。

// graph-store.ts(核心片段)
export const useGraphStore = defineStore('diagram-graph', () => {
  const nodes = ref<DiagramNode[]>([]);
  const edges = ref<DiagramEdge[]>([]);
  const historyStack = ref<HistorySnapshot[]>([]);
  const redoStack = ref<HistorySnapshot[]>([]);
  const maxHistorySize = 50;

  function pushHistory() {
    historyStack.value.push({
      nodes: cloneDeep(nodes.value),
      edges: cloneDeep(edges.value),
    });
    if (historyStack.value.length > maxHistorySize) historyStack.value.shift();
    redoStack.value = [];
  }

  function undo() {
    if (!historyStack.value.length) return;
    redoStack.value.push({ nodes: cloneDeep(nodes.value), edges: cloneDeep(edges.value) });
    const prev = historyStack.value.pop()!;
    nodes.value = prev.nodes;
    edges.value = prev.edges;
  }

  function redo() { /* 对称实现 */ }

  // 仅布局调整和关系操作记录历史,选择/缩放不记录
  function applyLayout(newNodes: DiagramNode[], newEdges: DiagramEdge[]) {
    pushHistory();
    nodes.value = newNodes;
    edges.value = newEdges;
  }
});

后端改动(最小化)

v4 方案的后端改动仅限于 schema.rs,不新增任何 Tauri Command 或存储模块:

改动点文件内容说明
批量列查询schema.rs新增 get_all_columns 命令一次返回 Schema 下所有表的列,避免匹配引擎逐表 IPC
列唯一键标识schema.rsColumnInfo 新增 is_unique 字段辅助匹配引擎判断目标列是否为主键或唯一键

文件结构规划

apps/desktop/src/
├── components/diagram/
│   ├── SchemaDiagramDialog.vue     (重构:拆分为容器 + 工具栏)
│   ├── **TableNode.vue**           (新增:Vue Flow 自定义节点)
│   ├── **RelationshipEdge.vue**    (新增:Vue Flow 自定义边)
│   ├── **MatchPanel.vue**          (新增:匹配管理面板)
│   └── **DiagramToolbar.vue**      (新增:工具栏提取)
│
├── lib/diagram/
│   ├── erDiagram.ts               (保留:核心数据模型)
│   ├── engineeringDiagram.ts      (保留:工程 ER 图)
│   ├── **vue-flow-adapter.ts**   (新增:Vue Flow 适配层)
│   ├── **graph-store.ts**         (新增:Pinia + 撤销/重做历史栈)
│   ├── **layout-manager.ts**      (新增:布局调度)
│   ├── **elk-layout.ts**          (新增:ELK.js 布局配置)
│   ├── **layout-grid.ts**         (新增:网格布局 fallback)
│   ├── **match-engine.ts**        (新增:智能匹配引擎)
│   ├── **match-strategies.ts**    (新增:匹配策略)
│   ├── **match-storage.ts**       (新增:localStorage 读写,复用 safeLocalStorage)
│   └── fieldLineage.ts            (保留)
│
├── types/
│   └── **diagram.ts**             (新增:类型定义)
│
└── tests/
    ├── unit/
    │   ├── **match-engine.test.ts**
    │   ├── **match-storage.test.ts**
    │   ├── **layout-manager.test.ts**
    │   ├── **vue-flow-adapter.test.ts**
    │   └── **graph-store.test.ts**
    └── e2e/
        └── **er-diagram.spec.ts**

src-tauri/src/commands/
└── schema.rs                      (微调:+get_all_columns, +is_unique)

// v3 中的 match_rules.rs 已删除
// v1 中的 edge-router.ts / interaction-manager.ts 已删除(Vue Flow / ELK 替代)

测试方案

单元测试(Vitest)

MatchEngine

TC-M1: 命名约定匹配 — user_id → users.id
输入 users(id PK)、orders(user_id)。期望匹配到 users.id,confidence='high'。
assert(result.relationships[0].targetTable === 'users')
TC-M2: 类型不匹配时拒绝
输入 users(id bigint)、orders(user_id varchar)。期望无匹配。
assert(result.relationships.length === 0)
TC-M3: 冲突检测 — type_id 指向多表
types 和 user_types 同时存在。期望标记冲突。
assert(result.conflicts.length > 0)
TC-M4: 物理外键优先不重复
物理外键 + 命名约定同时匹配。期望仅一条。
assert(result.relationships.length === 1)
TC-M5: camelCase 列名
Users(Id PK)、Orders(UserId)。期望匹配成功。
assert(result.relationships.length === 1)

MatchStorage

TC-MS1: confirms 存储与加载
保存 3 条 confirm ID 后加载。期望返回相同 3 条。
assert(loaded.length === 3 && loaded.every(id => saved.includes(id)))
TC-MS2: ignores 与 confirms 互不干扰
分别保存 confirms 和 ignores。期望各自加载正确。
assert(confirms !== ignores)
TC-MS3: 全局开关默认开启
未设置时调用 isAutoMatchEnabled()。期望 true。
assert(isAutoMatchEnabled() === true)

LayoutManager

TC-L1: ELK 输出无重叠
10 节点 + 9 边。期望无碰撞。
assert(!hasCollision(result.nodes))
TC-L2: 正交边路径含 L 命令
带端口的节点和边。期望 SVG path 包含 'L'。
assert(result.edges.every(e => e.path.includes('L')))
TC-L3: pinned 节点位置不变
2 个 pinned 节点。期望坐标不变。
assert(pinned.every(n => n.position === original))

GraphStore(撤销/重做)

TC-S1: 撤销恢复位置
拖拽 A 到 (100,100),undo。期望 A 回 (0,0)。
assert(nodes[0].position.x === 0)
TC-S2: redo 恢复新位置
拖拽,undo,redo。期望 A 在 (100,100)。
assert(nodes[0].position.x === 100)
TC-S3: 新操作清空 redo
拖拽 A,undo,拖拽 B。期望 canRedo = false。
assert(!canRedo)
TC-S4: 栈深度限制 50
连续 55 次 pushHistory。期望 historyStack.length === 50。
assert(historyStack.length === 50)

e2e 测试(Playwright)

TC-E1: 完整 ER 图加载
打开连接 → 选择数据库 → 点击"ER 图"。期望节点数 = 表数。
assert(locator('.vue-flow__node').count() === tableCount)
TC-E2: 无外键库的智能匹配
连接 SQLite → 打开 ER 图 → 点击"自动匹配"。期望虚线连线可见。
assert(locator('[data-kind="inferred"]').count() > 0)
TC-E3: ELK 自动布局
点击"自动布局"。期望节点按层级排列,连线无交叉。
assert(noOverlapping() && noIntersectingEdges())
TC-E4: 框选 + 批量拖拽
拖拽框选 3 节点 → 移动。期望 3 个同时移动。
assert(selectedCount === 3)
TC-E5: Ctrl+Z/Y 撤销重做
拖拽 → Ctrl+Z → Ctrl+Y。期望先回原位再回新位。
assert(posAfterRedo === draggedPos)
TC-E6: 匹配面板交互
点击"自动匹配"按钮 → 面板出现 → 点击确认一条 → 虚线变实线。
assert(confirmedEdge.getStyle().strokeDasharray === 'none')

分阶段实施计划

1

Phase 1: Vue Flow 迁移 + 智能匹配 + 撤销重做

目标:Vue Flow 替换自研渲染,智能匹配核心可用,Pinia 撤销/重做。

  • Vue Flow 集成:安装 @vue-flow/core + @vue-flow/minimap + @vue-flow/controls,实现适配层
  • ELK 集成:npm 安装 elkjs(直接打包),实现分层布局 + 正交边路由
  • GraphStore:Pinia + Immer,包含手动历史栈的撤销/重做
  • 匹配引擎:实现 P1/P2 策略,match-storage.ts 使用 safeLocalStorage
  • MatchPanel:复用现有关系面板的 UI 模式(Select + Button + Badge)
  • 工具栏:新增"自动匹配"和"自动布局"按钮,遵循现有 Button 规范
  • 后端schema.rs 新增 get_all_columnsis_unique
  • 单元测试:MatchEngine、MatchStorage、LayoutManager、GraphStore
2

Phase 2: 交互增强 + e2e 测试

目标:框选、连线交互、MiniMap、e2e 覆盖。

  • 框选:启用 SelectionMode.Partial
  • 连线交互RelationshipEdge.vue 悬停高亮
  • MiniMap:50+ 表时自动显示
  • 显示控制:工具栏开关控制列/注释/匹配关系
  • e2e 测试:Playwright 覆盖 6 个核心流程
3

Phase 3: 高级功能 + 性能优化

目标:大 Schema 支持、高级分析、P3 正则规则。

  • 虚拟化onlyRenderVisibleElements,支持 200+ 表
  • 路径过滤:选中两节点,仅显示关联路径
  • 正则规则:P3 用户自定义正则匹配,存储到 localStorage
  • 侧边栏拖入:Vue Flow DnD 增量添加表

关键设计决策

为什么工具栏新增按钮而非重新设计

DBX 的工具栏已有固定的布局节奏:选择器 → 搜索 → 模式切换 → 操作按钮 → Badge → 图标按钮。新增的"自动匹配"和"自动布局"按钮插入到操作按钮区域("建模关系"按钮右侧),遵循 variant="outline" size="sm" class="h-8 px-2 text-xs" + lucide 图标 h-3.5 w-3.5 + mr-1 的规范。缩放和重置按钮移除后由 Vue Flow 的浮动 Controls 组件替代,不占用工具栏空间。这种增量式改动与现有 UI 风格完全一致,PR 审查时不会因为"风格不统一"被要求返工。

为什么用 localStorage 而非新增后端存储

DBX 的自定义关系已使用 localStorage + dbx:diagram:relationships:v1:... key 模式存储。匹配规则的存储需求与自定义关系完全相同(按连接+数据库+schema 隔离、数据量小、JSON 序列化),没有必要引入新的存储机制。使用已有的 safeLocalStorageGet/Set/Remove 封装(而非直接 localStorage),可以统一错误处理,比现有自定义关系代码更健壮。

为什么 ELK.js 直接打包

DBA 常在无网络的内网环境使用数据库管理工具。懒加载在离线场景下会导致布局功能不可用。直接打包后 ELK.js 随安装包分发,增量约 300KB(gzip ~50KB),对 20MB 的 DBX 影响约 1.7%。

为什么不选 Coze 的 FlowGram

FlowGram 基于 Canvas 自研渲染引擎,定位是 AI 工作流编排(内置变量引擎、表单引擎)。Canvas 渲染文本排版远不如 HTML,不适合包含多行列信息的表卡片。对 ER 图来说严重过度设计。

测试用例索引

编号模块类型数量
TC-M1 ~ M5MatchEngine单元5
TC-MS1 ~ MS3MatchStorage单元3
TC-L1 ~ L3LayoutManager单元3
TC-S1 ~ S4GraphStore单元4
TC-E1 ~ E6ER 图全流程e2e6