From 67e0335f0151a7c035b532d7a8d314d6b1ad10bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Tue, 7 Jul 2026 14:24:19 +0800 Subject: [PATCH] feat: add standalone module diagram editor --- RuoYi-Vue/easycode-web/src/router/index.js | 35 +++ .../src/views/ModuleDiagramEditorView.vue | 270 ++++++++++++++++++ .../src/views/ProjectListView.vue | 111 ++++--- .../views/moduleDiagramEditorView.test.mjs | 48 ++++ .../src/views/projectListView.test.mjs | 54 ++++ 5 files changed, 473 insertions(+), 45 deletions(-) create mode 100644 RuoYi-Vue/easycode-web/src/views/ModuleDiagramEditorView.vue create mode 100644 RuoYi-Vue/easycode-web/src/views/moduleDiagramEditorView.test.mjs create mode 100644 RuoYi-Vue/easycode-web/src/views/projectListView.test.mjs diff --git a/RuoYi-Vue/easycode-web/src/router/index.js b/RuoYi-Vue/easycode-web/src/router/index.js index 9b7b6f9..dff561f 100644 --- a/RuoYi-Vue/easycode-web/src/router/index.js +++ b/RuoYi-Vue/easycode-web/src/router/index.js @@ -5,8 +5,13 @@ import HomeView from '@/views/HomeView.vue' import LoginView from '@/views/LoginView.vue' import RegisterView from '@/views/RegisterView.vue' import GenerateView from '@/views/GenerateView.vue' +import DiagramCenterView from '@/views/DiagramCenterView.vue' +import ModuleDiagramEditorView from '@/views/ModuleDiagramEditorView.vue' +import PageDesignerView from '@/views/PageDesignerView.vue' +import PaperDraftView from '@/views/PaperDraftView.vue' import ProjectListView from '@/views/ProjectListView.vue' import PreviewView from '@/views/PreviewView.vue' +import ProfileView from '@/views/ProfileView.vue' import SourceDetailView from '@/views/SourceDetailView.vue' import SourceStoreView from '@/views/SourceStoreView.vue' @@ -16,8 +21,38 @@ const routes = [ { path: '/register', name: 'register', component: RegisterView }, { path: '/source-store', name: 'source-store', component: SourceStoreView }, { path: '/source-store/:sourceId', name: 'source-detail', component: SourceDetailView }, + { path: '/profile', name: 'profile', component: ProfileView, meta: { requiresAuth: true } }, { path: '/projects', name: 'projects', component: ProjectListView, meta: { requiresAuth: true } }, { path: '/generate', name: 'generate', component: GenerateView, meta: { requiresAuth: true } }, + { + path: '/project/:projectId/page-designer', + name: 'page-designer', + component: PageDesignerView, + meta: { requiresAuth: true } + }, + { + path: '/project/:projectId/diagrams', + name: 'diagram-center', + component: DiagramCenterView, + meta: { requiresAuth: true } + }, + { + path: '/module-diagram', + name: 'module-diagram-editor-local', + component: ModuleDiagramEditorView + }, + { + path: '/project/:projectId/module-diagram', + name: 'module-diagram-editor', + component: ModuleDiagramEditorView, + meta: { requiresAuth: true } + }, + { + path: '/project/:projectId/paper-draft', + name: 'paper-draft', + component: PaperDraftView, + meta: { requiresAuth: true } + }, { path: '/project/:projectId/preview', name: 'preview', diff --git a/RuoYi-Vue/easycode-web/src/views/ModuleDiagramEditorView.vue b/RuoYi-Vue/easycode-web/src/views/ModuleDiagramEditorView.vue new file mode 100644 index 0000000..e9b16ea --- /dev/null +++ b/RuoYi-Vue/easycode-web/src/views/ModuleDiagramEditorView.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/RuoYi-Vue/easycode-web/src/views/ProjectListView.vue b/RuoYi-Vue/easycode-web/src/views/ProjectListView.vue index 9fa8b9a..7a3a30c 100644 --- a/RuoYi-Vue/easycode-web/src/views/ProjectListView.vue +++ b/RuoYi-Vue/easycode-web/src/views/ProjectListView.vue @@ -8,7 +8,7 @@
刷新 - 新建项目 + 新建项目
@@ -17,7 +17,7 @@ @@ -32,34 +32,30 @@ {{ row.updateTime || row.createTime || '-' }} - + @@ -68,9 +64,7 @@ - 新建项目 - + /> @@ -80,7 +74,7 @@ import { onMounted, ref } from 'vue' import { useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' -import { Delete, Download, EditPen, Plus, Refresh, View } from '@element-plus/icons-vue' +import { Connection, DataAnalysis, Delete, Document, Download, EditPen, Plus, Refresh, View } from '@element-plus/icons-vue' import { deleteProject, downloadProject, listProjects } from '@/api/project' import { saveBlob, sourceZipName } from '@/utils/download' @@ -121,7 +115,34 @@ function continueProject(project) { ElMessage.warning('项目 ID 不存在,无法继续') return } - router.push({ path: '/generate', query: { projectId } }) + router.push({ path: '/generate', query: { projectId, mode: 'expert' } }) +} + +function openDiagrams(project) { + const projectId = projectIdOf(project) + if (!projectId) { + ElMessage.warning('项目 ID 不存在,无法打开图表中心') + return + } + router.push(`/project/${projectId}/diagrams`) +} + +function openModuleDiagramEditor(project) { + const projectId = projectIdOf(project) + if (!projectId) { + ElMessage.warning('项目 ID 不存在,无法打开模块图') + return + } + router.push(`/project/${projectId}/module-diagram`) +} + +function openPaperDraft(project) { + const projectId = projectIdOf(project) + if (!projectId) { + ElMessage.warning('项目 ID 不存在,无法打开论文初稿') + return + } + router.push(`/project/${projectId}/paper-draft`) } async function downloadSource(project) { @@ -149,7 +170,7 @@ async function removeProject(project) { if (!projectId) return try { - await ElMessageBox.confirm(`确认删除项目「${project.projectName || projectId}」?`, '删除项目', { + await ElMessageBox.confirm(`确认删除项目“${project.projectName || projectId}”?`, '删除项目', { type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' @@ -187,9 +208,16 @@ onMounted(loadProjects) } .project-desc { + display: -webkit-box; + max-width: 540px; margin-top: 4px; + overflow: hidden; color: #667085; font-size: 13px; + line-height: 1.6; + overflow-wrap: anywhere; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; } .row-actions { @@ -203,17 +231,10 @@ onMounted(loadProjects) margin-left: 0; } -.action-button { - min-width: 72px; - height: 32px; - padding: 7px 10px; - border-color: #d8e0eb; - color: #475467; - background: #ffffff; -} - -.action-button-wide { - min-width: 94px; +.project-icon-button { + width: 36px; + height: 36px; + padding: 0; } @media (max-width: 860px) { diff --git a/RuoYi-Vue/easycode-web/src/views/moduleDiagramEditorView.test.mjs b/RuoYi-Vue/easycode-web/src/views/moduleDiagramEditorView.test.mjs new file mode 100644 index 0000000..569d4ce --- /dev/null +++ b/RuoYi-Vue/easycode-web/src/views/moduleDiagramEditorView.test.mjs @@ -0,0 +1,48 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { existsSync, readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, resolve } from 'node:path' + +const currentDir = dirname(fileURLToPath(import.meta.url)) +const srcDir = resolve(currentDir, '..') + +function readSource(relativePath) { + return readFileSync(resolve(srcDir, relativePath), 'utf8') +} + +test('standalone module diagram editor page exists and embeds the shared component', () => { + assert.equal(existsSync(resolve(srcDir, 'views/ModuleDiagramEditorView.vue')), true) + const source = readSource('views/ModuleDiagramEditorView.vue') + + assert.equal(source.includes("import AppModuleDiagramView from '@/components/AppModuleDiagramView.vue'"), true) + assert.equal(source.includes('module-editor-layout'), true) + assert.equal(source.includes('module-editor-side'), true) + assert.equal(source.includes('module-editor-canvas'), true) + assert.equal(source.includes('v-model:source-mode="sourceMode"'), true) + assert.equal(source.includes('v-model:outline-text="outlineText"'), true) + assert.equal(source.includes('v-model:layout-options="layoutOptions"'), true) + assert.equal(source.includes(':editable="false"'), true) +}) + +test('standalone module diagram editor supports optional project loading and saving', () => { + const source = readSource('views/ModuleDiagramEditorView.vue') + + assert.equal(source.includes('const projectId = computed'), true) + assert.equal(source.includes('getProject(projectId.value)'), true) + assert.equal(source.includes('listProjectDiagrams(projectId.value)'), true) + assert.equal(source.includes('saveProjectDiagram(projectId.value'), true) + assert.equal(source.includes('function editorDiagramPayload'), true) + assert.equal(source.includes('sourceMode: sourceMode.value'), true) + assert.equal(source.includes('outlineText: outlineText.value'), true) + assert.equal(source.includes('layoutOptions: layoutOptions.value'), true) +}) + +test('router exposes project and local module diagram editor routes', () => { + const source = readSource('router/index.js') + + assert.equal(source.includes("import ModuleDiagramEditorView from '@/views/ModuleDiagramEditorView.vue'"), true) + assert.equal(source.includes("path: '/module-diagram'"), true) + assert.equal(source.includes("path: '/project/:projectId/module-diagram'"), true) + assert.equal(source.includes("name: 'module-diagram-editor'"), true) +}) diff --git a/RuoYi-Vue/easycode-web/src/views/projectListView.test.mjs b/RuoYi-Vue/easycode-web/src/views/projectListView.test.mjs new file mode 100644 index 0000000..af6a305 --- /dev/null +++ b/RuoYi-Vue/easycode-web/src/views/projectListView.test.mjs @@ -0,0 +1,54 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, resolve } from 'node:path' + +const currentDir = dirname(fileURLToPath(import.meta.url)) + +function readView(name) { + return readFileSync(resolve(currentDir, name), 'utf8') +} + +test('project list row actions use compact icon buttons like profile project actions', () => { + const source = readView('ProjectListView.vue') + + assert.equal(source.includes('project-icon-button'), true) + assert.equal(source.includes('el-tooltip content="专家编辑"'), true) + assert.equal(source.includes('aria-label="专家编辑"'), true) + assert.equal(source.includes('aria-label="预览"'), true) + assert.equal(source.includes('aria-label="下载"'), true) + assert.equal(source.includes('aria-label="删除"'), true) + assert.match(source, //) + assert.doesNotMatch(source, /]*>\s*继续生成\s*<\/el-button>/) + assert.doesNotMatch(source, /]*>\s*预览\s*<\/el-button>/) + assert.doesNotMatch(source, /]*>\s*下载\s*<\/el-button>/) + assert.doesNotMatch(source, /]*>\s*删除\s*<\/el-button>/) +}) + +test('empty project state does not include a duplicate create project button', () => { + const source = readView('ProjectListView.vue') + + assert.equal(source.includes("router.push({ path: '/generate', query: { mode: 'one-click' } })"), true) + assert.doesNotMatch(source, //) +}) + +test('project list truncates long project descriptions after two lines', () => { + const source = readView('ProjectListView.vue') + + const styleMatch = source.match(/\.project-desc\s*\{([\s\S]*?)\n\}/) + assert.notEqual(styleMatch, null) + assert.match(styleMatch[1], /display:\s*-webkit-box;/) + assert.match(styleMatch[1], /-webkit-box-orient:\s*vertical;/) + assert.match(styleMatch[1], /-webkit-line-clamp:\s*2;/) + assert.match(styleMatch[1], /overflow:\s*hidden;/) +}) + +test('project list exposes compact module diagram action', () => { + const source = readView('ProjectListView.vue') + + assert.equal(source.includes('el-tooltip content="模块图"'), true) + assert.equal(source.includes('aria-label="模块图"'), true) + assert.equal(source.includes('openModuleDiagramEditor'), true) + assert.equal(source.includes('`/project/${projectId}/module-diagram`'), true) +})