# UML Class Diagram Center Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a project-aware UML class diagram editor to the EasyCode diagram center, launched from `更多图表 > 生成 UML 类图`. **Architecture:** Implement a focused `classDiagram` utility that converts project database tables into editable DSL, parses that DSL into a deterministic graph, lays out class boxes and UML relations, and exports SVG. Wire the graph into `DiagramCenterView.vue` using the same DSL editor, save/restore, and SVG/PNG export pattern already used by use case, sequence, activity, state, organization, and data-flow diagrams. **Tech Stack:** Vue 3 Composition API, Element Plus, plain SVG, Node `node:test`, no new runtime dependencies. --- ## File Structure - Create `easycode-web/src/utils/classDiagram.js`: all class diagram DSL parsing, default DSL generation, deterministic layout, and SVG export. - Create `easycode-web/src/utils/classDiagram.test.mjs`: behavior tests for parsing, default generation, layout, warnings, and SVG export safety. - Modify `easycode-web/src/views/DiagramCenterView.vue`: import the utility, add `class_diagram` state/computed/template branch/save/load/export handlers, and reuse existing diagram workbench styles. - Modify `easycode-web/src/views/diagramCenterView.test.mjs`: source-level integration tests for dropdown exposure, direct route, save/restore payload, and export handlers. ## Task 1: Class Diagram Utility Tests **Files:** - Create: `easycode-web/src/utils/classDiagram.test.mjs` - Later implementation target: `easycode-web/src/utils/classDiagram.js` - [ ] **Step 1: Write the failing utility test** Create `easycode-web/src/utils/classDiagram.test.mjs` with: ```js import test from 'node:test' import assert from 'node:assert/strict' import { CLASS_DIAGRAM_EXAMPLE_DSL, buildClassDiagram, buildClassDiagramExportSvg, buildDefaultClassDiagramDsl, parseClassDiagramDsl } from './classDiagram.js' const bookstoreDsl = `标题: 在线书店 UML 类图 class 用户 <> - id: Long (PK) - username: String - password: String + login(): Boolean class 客户 extends 用户 - level: Integer - balance: BigDecimal + placeOrder(items: List): Order class 支付接口 <> + pay(): Boolean class 微信支付 implements 支付接口 - appId: String + pay(): Boolean class 订单 - id: Long (PK) - totalAmount: BigDecimal + cancel(): void class 订单明细 - id: Long (PK) - quantity: Integer - price: BigDecimal 客户 "1" -- "0..*" 订单 : 下单 订单 ..> 微信支付 : 使用 订单 o-- 支付接口 : 支付方式 订单 *-- 订单明细 : 明细` test('parseClassDiagramDsl recognizes classes, members, stereotypes, and UML relations', () => { const parsed = parseClassDiagramDsl(bookstoreDsl) assert.equal(parsed.title, '在线书店 UML 类图') assert.equal(parsed.warnings.length, 0) assert.equal(parsed.classes.length, 6) assert.deepEqual( parsed.classes.slice(0, 4).map((item) => ({ name: item.name, stereotype: item.stereotype, extendsName: item.extendsName, implementsNames: item.implementsNames })), [ { name: '用户', stereotype: 'abstract', extendsName: '', implementsNames: [] }, { name: '客户', stereotype: '', extendsName: '用户', implementsNames: [] }, { name: '支付接口', stereotype: 'interface', extendsName: '', implementsNames: [] }, { name: '微信支付', stereotype: '', extendsName: '', implementsNames: ['支付接口'] } ] ) assert.deepEqual( parsed.classes.find((item) => item.name === '用户').attributes.map((item) => `${item.visibility} ${item.name}: ${item.type} ${item.meta}`), ['- id: Long PK', '- username: String ', '- password: String '] ) assert.deepEqual( parsed.classes.find((item) => item.name === '客户').methods.map((item) => `${item.visibility} ${item.signature}`), ['+ placeOrder(items: List): Order'] ) assert.ok(parsed.relations.some((item) => item.type === 'inheritance' && item.source === '客户' && item.target === '用户')) assert.ok(parsed.relations.some((item) => item.type === 'implementation' && item.source === '微信支付' && item.target === '支付接口')) assert.ok(parsed.relations.some((item) => item.type === 'association' && item.source === '客户' && item.target === '订单' && item.sourceMultiplicity === '1' && item.targetMultiplicity === '0..*')) assert.ok(parsed.relations.some((item) => item.type === 'dependency' && item.label === '使用')) assert.ok(parsed.relations.some((item) => item.type === 'aggregation' && item.label === '支付方式')) assert.ok(parsed.relations.some((item) => item.type === 'composition' && item.label === '明细')) }) test('buildDefaultClassDiagramDsl creates classes and inferred associations from database tables', () => { const dsl = buildDefaultClassDiagramDsl( { projectName: '在线书店' }, { tables: [ { tableName: 'book', tableComment: '图书', columns: [ { columnName: 'id', columnType: 'bigint', columnComment: '图书ID', isPk: '1', isRequired: '1' }, { columnName: 'title', columnType: 'varchar(100)', columnComment: '标题' }, { columnName: 'category_id', columnType: 'bigint', columnComment: '分类ID' }, { columnName: 'price', columnType: 'decimal(10,2)', columnComment: '价格' }, { columnName: 'create_time', columnType: 'datetime', columnComment: '创建时间' } ] }, { tableName: 'book_category', tableComment: '图书分类', columns: [ { columnName: 'id', columnType: 'bigint', columnComment: '分类ID', isPk: '1' }, { columnName: 'name', columnType: 'varchar(50)', columnComment: '名称' } ] } ], erDiagram: { relations: [ { source: 'book', target: 'book_category', label: '所属分类', type: 'many-to-one' } ] } } ) assert.match(dsl, /标题: 在线书店 UML 类图/) assert.match(dsl, /class 图书/) assert.match(dsl, /- id: Long \(PK\)/) assert.match(dsl, /- title: String/) assert.match(dsl, /- price: BigDecimal/) assert.match(dsl, /- createTime: LocalDateTime/) assert.match(dsl, /图书 "0\.\.\*" -- "1" 图书分类 : 所属分类/) }) test('parseClassDiagramDsl keeps valid content while reporting malformed lines and missing relation targets', () => { const parsed = parseClassDiagramDsl(`标题: 错误类图 class 用户 - id Long 坏行 用户 -- 订单 : 下单`) assert.equal(parsed.classes.length, 1) assert.ok(parsed.warnings.some((warning) => warning.includes('第 3 行'))) assert.ok(parsed.warnings.some((warning) => warning.includes('第 4 行'))) assert.ok(parsed.warnings.some((warning) => warning.includes('关系目标不存在:订单'))) }) test('buildClassDiagram lays out class compartments and relation paths', () => { const diagram = buildClassDiagram(parseClassDiagramDsl(bookstoreDsl), { title: '在线书店类图' }) assert.equal(diagram.title, '在线书店类图') assert.equal(diagram.stats.classCount, 6) assert.ok(diagram.stats.relationCount >= 6) assert.ok(diagram.canvas.width >= 1100) assert.ok(diagram.canvas.height >= 620) assert.ok(diagram.classes.every((item) => item.width >= 210 && item.height >= 112)) assert.ok(diagram.relations.every((item) => item.path)) }) test('buildClassDiagramExportSvg serializes safe UML class SVG without foreignObject', () => { const diagram = buildClassDiagram(parseClassDiagramDsl(`标题: 安全类图 class 用户