diff --git a/RuoYi-Vue/docs/superpowers/plans/2026-07-07-use-case-diagram-center.md b/RuoYi-Vue/docs/superpowers/plans/2026-07-07-use-case-diagram-center.md new file mode 100644 index 0000000..09e9b34 --- /dev/null +++ b/RuoYi-Vue/docs/superpowers/plans/2026-07-07-use-case-diagram-center.md @@ -0,0 +1,1150 @@ +# Use Case 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 dedicated `用例图` tab to EasyCode's diagram center with text DSL editing, UML SVG preview, project draft save/restore, and SVG/PNG export. + +**Architecture:** Add a pure `useCaseDiagram.js` utility for DSL parsing, deterministic layout, and standalone SVG export. Integrate that utility into `DiagramCenterView.vue` using the same left-DSL/right-preview pattern already used by flowcharts and architecture diagrams, while persisting drafts through the existing project diagram API as `diagramType: 'use_case'`. + +**Tech Stack:** Vue 3 Composition API, Element Plus, browser SVG/canvas export, Node `node:test` for focused frontend utility/view string tests. + +--- + +## File Structure + +- Create: `easycode-web/src/utils/useCaseDiagram.js` + - Owns default DSL generation, DSL parsing, graph layout, SVG escaping, and export SVG serialization. +- Create: `easycode-web/src/utils/useCaseDiagram.test.mjs` + - Verifies parser, default DSL, layout, relation semantics, canvas growth, and export SVG behavior. +- Modify: `easycode-web/src/views/DiagramCenterView.vue` + - Adds `用例图` tab, editor UI, preview SVG, save/restore logic, and export handlers. +- Modify: `easycode-web/src/views/diagramCenterView.test.mjs` + - Verifies tab routing, save/restore wiring, and export handler wiring with source-level tests. + +Do not change backend schema or project diagram APIs. + +--- + +### Task 1: Utility Tests First + +**Files:** +- Create: `easycode-web/src/utils/useCaseDiagram.test.mjs` +- Test: `easycode-web/src/utils/useCaseDiagram.test.mjs` + +- [ ] **Step 1: Write the failing utility test file** + +Create `easycode-web/src/utils/useCaseDiagram.test.mjs` with: + +```js +import test from 'node:test' +import assert from 'node:assert/strict' +import { + USE_CASE_EXAMPLE_DSL, + buildDefaultUseCaseDsl, + buildUseCaseDiagram, + buildUseCaseExportSvg, + parseUseCaseDsl +} from './useCaseDiagram.js' + +const shoppingDsl = `用户 +浏览商品 + 搜索商品 + 查看商品详情 + 筛选商品 +加入购物车 + 选择规格 + 修改数量 +提交订单 + 选择收货地址 + 选择优惠券 +评价商品 + 上传图片? + 打分` + +test('parseUseCaseDsl parses screenshot-style actor, use cases, and include relations', () => { + const parsed = parseUseCaseDsl(shoppingDsl) + + assert.equal(parsed.actors.length, 1) + assert.equal(parsed.actors[0].name, '用户') + assert.equal(parsed.primaryUseCases.length, 4) + assert.equal(parsed.useCases.length, 13) + assert.equal(parsed.relations.filter((relation) => relation.type === 'association').length, 4) + assert.equal(parsed.relations.filter((relation) => relation.type === 'include').length, 8) + assert.equal(parsed.relations.filter((relation) => relation.type === 'extend').length, 1) + assert.equal(parsed.warnings.length, 0) + assert.deepEqual( + parsed.primaryUseCases.map((item) => item.name), + ['浏览商品', '加入购物车', '提交订单', '评价商品'] + ) +}) + +test('parseUseCaseDsl normalizes tabs to two-space indentation', () => { + const parsed = parseUseCaseDsl('用户\n浏览商品\n\t搜索商品') + + assert.equal(parsed.primaryUseCases.length, 1) + assert.equal(parsed.useCases.find((item) => item.name === '搜索商品').parentId, 'use_case_1') + assert.equal(parsed.relations.some((relation) => relation.type === 'include'), true) +}) + +test('parseUseCaseDsl marks question-suffixed children as extend relations', () => { + const parsed = parseUseCaseDsl('用户\n支付订单\n 支付失败?') + + assert.equal(parsed.relations.find((relation) => relation.type === 'extend').target, 'use_case_1') + assert.equal(parsed.useCases.find((item) => item.name === '支付失败').optional, true) +}) + +test('parseUseCaseDsl returns warnings for empty DSL and falls back to actor when missing', () => { + const empty = parseUseCaseDsl('') + assert.equal(empty.actors[0].name, '用户') + assert.equal(empty.useCases.length, 0) + assert.ok(empty.warnings.some((warning) => warning.includes('至少添加一个用例'))) + + const missingActor = parseUseCaseDsl(' 搜索商品') + assert.equal(missingActor.actors[0].name, '用户') + assert.equal(missingActor.primaryUseCases.length, 1) + assert.ok(missingActor.warnings.some((warning) => warning.includes('缺少参与者'))) +}) + +test('buildDefaultUseCaseDsl creates project-aware starter content from blueprint menus', () => { + const dsl = buildDefaultUseCaseDsl({ + projectName: '图书借阅系统', + appBlueprint: JSON.stringify({ + frontendMenus: [{ name: '图书浏览' }, { title: '我的借阅' }], + adminMenus: [{ name: '图书管理' }, { code: 'borrowAudit' }] + }) + }) + + assert.match(dsl, /用户/) + assert.match(dsl, /图书浏览/) + assert.match(dsl, /我的借阅/) + assert.match(dsl, /图书管理/) + assert.match(dsl, /borrowAudit/) +}) + +test('buildUseCaseDiagram lays out actor, ovals, relations, and grows canvas', () => { + const small = buildUseCaseDiagram(parseUseCaseDsl('用户\n浏览商品')) + const large = buildUseCaseDiagram(parseUseCaseDsl(shoppingDsl), { title: '商城用例图' }) + + assert.equal(large.title, '商城用例图') + assert.equal(large.stats.actorCount, 1) + assert.equal(large.stats.primaryUseCaseCount, 4) + assert.equal(large.stats.useCaseCount, 13) + assert.ok(large.canvas.height > small.canvas.height) + assert.ok(large.actors[0].x < large.useCases[0].x) + assert.ok(large.useCases.some((item) => item.kind === 'child')) + assert.ok(large.relations.some((relation) => relation.label === '<>')) + assert.ok(large.relations.some((relation) => relation.label === '<>')) +}) + +test('buildUseCaseExportSvg serializes safe UML SVG without foreignObject', () => { + const diagram = buildUseCaseDiagram(parseUseCaseDsl('用户\n浏览