feat: add standalone module diagram editor
This commit is contained in:
@@ -5,8 +5,13 @@ import HomeView from '@/views/HomeView.vue'
|
|||||||
import LoginView from '@/views/LoginView.vue'
|
import LoginView from '@/views/LoginView.vue'
|
||||||
import RegisterView from '@/views/RegisterView.vue'
|
import RegisterView from '@/views/RegisterView.vue'
|
||||||
import GenerateView from '@/views/GenerateView.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 ProjectListView from '@/views/ProjectListView.vue'
|
||||||
import PreviewView from '@/views/PreviewView.vue'
|
import PreviewView from '@/views/PreviewView.vue'
|
||||||
|
import ProfileView from '@/views/ProfileView.vue'
|
||||||
import SourceDetailView from '@/views/SourceDetailView.vue'
|
import SourceDetailView from '@/views/SourceDetailView.vue'
|
||||||
import SourceStoreView from '@/views/SourceStoreView.vue'
|
import SourceStoreView from '@/views/SourceStoreView.vue'
|
||||||
|
|
||||||
@@ -16,8 +21,38 @@ const routes = [
|
|||||||
{ path: '/register', name: 'register', component: RegisterView },
|
{ path: '/register', name: 'register', component: RegisterView },
|
||||||
{ path: '/source-store', name: 'source-store', component: SourceStoreView },
|
{ path: '/source-store', name: 'source-store', component: SourceStoreView },
|
||||||
{ path: '/source-store/:sourceId', name: 'source-detail', component: SourceDetailView },
|
{ 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: '/projects', name: 'projects', component: ProjectListView, meta: { requiresAuth: true } },
|
||||||
{ path: '/generate', name: 'generate', component: GenerateView, 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',
|
path: '/project/:projectId/preview',
|
||||||
name: 'preview',
|
name: 'preview',
|
||||||
|
|||||||
270
RuoYi-Vue/easycode-web/src/views/ModuleDiagramEditorView.vue
Normal file
270
RuoYi-Vue/easycode-web/src/views/ModuleDiagramEditorView.vue
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
<template>
|
||||||
|
<section class="page module-diagram-editor-page" v-loading="loading">
|
||||||
|
<div class="panel module-editor-shell">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div>
|
||||||
|
<h1 class="panel-title">功能模块图编辑器</h1>
|
||||||
|
<p class="muted">{{ project.projectName || '输入缩进结构,生成论文风格功能模块图。' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar">
|
||||||
|
<el-button v-if="projectId" @click="router.push(`/project/${projectId}/diagrams`)">返回图表中心</el-button>
|
||||||
|
<el-button v-if="projectId" type="primary" :loading="saving" @click="saveEditorDiagram">保存到项目</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="module-editor-layout">
|
||||||
|
<aside class="module-editor-side">
|
||||||
|
<div class="module-editor-side-header">
|
||||||
|
<strong>缩进结构</strong>
|
||||||
|
<div class="module-editor-side-actions">
|
||||||
|
<el-button text type="primary" @click="loadExampleOutline">示例</el-button>
|
||||||
|
<el-button v-if="projectId" text type="primary" @click="useBlueprintOutline">使用项目蓝图</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-input
|
||||||
|
v-model="editableOutlineText"
|
||||||
|
type="textarea"
|
||||||
|
:rows="22"
|
||||||
|
resize="none"
|
||||||
|
spellcheck="false"
|
||||||
|
placeholder="第一行写系统名称;第二层缩进 2 个空格;第三层及以下作为功能节点。"
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="module-editor-canvas">
|
||||||
|
<AppModuleDiagramView
|
||||||
|
v-model:source-mode="sourceMode"
|
||||||
|
v-model:outline-text="outlineText"
|
||||||
|
v-model:layout-options="layoutOptions"
|
||||||
|
:blueprint="appBlueprint"
|
||||||
|
:project-name="project.projectName"
|
||||||
|
:editable="false"
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import AppModuleDiagramView from '@/components/AppModuleDiagramView.vue'
|
||||||
|
import { getProject, listProjectDiagrams, saveProjectDiagram } from '@/api/project'
|
||||||
|
import { blueprintToModuleOutline, moduleOutlineToText, parseModuleOutlineText } from '@/utils/appModuleOutline'
|
||||||
|
import { buildAppModuleGraph } from '@/utils/appModuleGraph'
|
||||||
|
|
||||||
|
const EXAMPLE_OUTLINE = `学生社团管理系统
|
||||||
|
管理员端
|
||||||
|
注册登录
|
||||||
|
用户管理
|
||||||
|
社团管理
|
||||||
|
成员管理
|
||||||
|
活动管理
|
||||||
|
公告管理
|
||||||
|
系统管理
|
||||||
|
普通用户端
|
||||||
|
注册登录
|
||||||
|
社团浏览
|
||||||
|
社团申请
|
||||||
|
活动浏览
|
||||||
|
活动报名
|
||||||
|
个人中心`
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const projectId = computed(() => route.params.projectId || route.query.projectId || '')
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const project = reactive({})
|
||||||
|
const savedDiagrams = ref([])
|
||||||
|
const sourceMode = ref('outline')
|
||||||
|
const outlineText = ref(EXAMPLE_OUTLINE)
|
||||||
|
const layoutOptions = ref({ density: 'standard', direction: 'horizontal', fontSize: 'standard' })
|
||||||
|
|
||||||
|
const appBlueprint = computed(() => parseJson(project.appBlueprint, { roles: [], frontendMenus: [], adminMenus: [] }))
|
||||||
|
const blueprintOutlineText = computed(() => moduleOutlineToText(blueprintToModuleOutline(appBlueprint.value, { projectName: project.projectName })))
|
||||||
|
const activeOutlineText = computed(() => sourceMode.value === 'blueprint' ? blueprintOutlineText.value : outlineText.value)
|
||||||
|
const currentGraph = computed(() => buildAppModuleGraph(parseModuleOutlineText(activeOutlineText.value).outline, layoutOptions.value))
|
||||||
|
const editableOutlineText = computed({
|
||||||
|
get: () => outlineText.value,
|
||||||
|
set: (value) => {
|
||||||
|
outlineText.value = value
|
||||||
|
sourceMode.value = 'outline'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function parseJson(value, fallback) {
|
||||||
|
if (!value) return fallback
|
||||||
|
if (typeof value === 'object') return value
|
||||||
|
try {
|
||||||
|
return JSON.parse(value)
|
||||||
|
} catch (error) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadExampleOutline() {
|
||||||
|
outlineText.value = EXAMPLE_OUTLINE
|
||||||
|
sourceMode.value = 'outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
function useBlueprintOutline() {
|
||||||
|
const text = blueprintOutlineText.value
|
||||||
|
outlineText.value = text || EXAMPLE_OUTLINE
|
||||||
|
sourceMode.value = 'outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLayoutOptions(value = {}) {
|
||||||
|
return {
|
||||||
|
density: value.density || 'standard',
|
||||||
|
direction: value.direction || 'horizontal',
|
||||||
|
fontSize: value.fontSize || 'standard'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editorDiagramPayload() {
|
||||||
|
return {
|
||||||
|
sourceMode: sourceMode.value,
|
||||||
|
outlineText: outlineText.value,
|
||||||
|
layoutOptions: layoutOptions.value,
|
||||||
|
graph: currentGraph.value,
|
||||||
|
projectName: project.projectName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySavedModuleDiagram(saved) {
|
||||||
|
const payload = parseJson(saved?.diagramJson, null)
|
||||||
|
if (!payload || typeof payload !== 'object') {
|
||||||
|
useBlueprintOutline()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sourceMode.value = payload.sourceMode === 'blueprint' ? 'blueprint' : 'outline'
|
||||||
|
outlineText.value = typeof payload.outlineText === 'string' ? payload.outlineText : (blueprintOutlineText.value || EXAMPLE_OUTLINE)
|
||||||
|
layoutOptions.value = normalizeLayoutOptions(payload.layoutOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEditorData() {
|
||||||
|
if (!projectId.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [projectResult, diagramsResult] = await Promise.all([
|
||||||
|
getProject(projectId.value),
|
||||||
|
listProjectDiagrams(projectId.value)
|
||||||
|
])
|
||||||
|
Object.assign(project, projectResult || {})
|
||||||
|
savedDiagrams.value = Array.isArray(diagramsResult) ? diagramsResult : []
|
||||||
|
const saved = savedDiagrams.value.find((item) => item.diagramType === 'module')
|
||||||
|
if (saved) {
|
||||||
|
applySavedModuleDiagram(saved)
|
||||||
|
} else {
|
||||||
|
useBlueprintOutline()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.message || '加载模块图失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEditorDiagram() {
|
||||||
|
if (!projectId.value) return
|
||||||
|
const existing = savedDiagrams.value.find((item) => item.diagramType === 'module')
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const saved = await saveProjectDiagram(projectId.value, {
|
||||||
|
diagramId: existing?.diagramId,
|
||||||
|
diagramType: 'module',
|
||||||
|
title: '功能模块图',
|
||||||
|
description: '系统功能模块图草稿',
|
||||||
|
diagramJson: JSON.stringify(editorDiagramPayload())
|
||||||
|
})
|
||||||
|
if (saved?.diagramId) {
|
||||||
|
const index = savedDiagrams.value.findIndex((item) => item.diagramId === saved.diagramId)
|
||||||
|
if (index >= 0) {
|
||||||
|
savedDiagrams.value.splice(index, 1, saved)
|
||||||
|
} else {
|
||||||
|
savedDiagrams.value.unshift(saved)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElMessage.success('功能模块图已保存')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.message || '保存模块图失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadEditorData)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.module-diagram-editor-page {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-editor-shell {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-editor-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(300px, 360px) minmax(0, 1fr);
|
||||||
|
min-height: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-editor-side {
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 18px;
|
||||||
|
border-right: 1px solid #e7ecf3;
|
||||||
|
background: #fbfdff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-editor-side-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-editor-side-header strong {
|
||||||
|
color: #172033;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-editor-side-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-editor-side :deep(.el-textarea__inner) {
|
||||||
|
min-height: 560px !important;
|
||||||
|
border-color: #d9e0ea;
|
||||||
|
color: #172033;
|
||||||
|
font-family: Consolas, "Courier New", "Microsoft YaHei", monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-editor-canvas {
|
||||||
|
min-width: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.module-editor-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-editor-side {
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid #e7ecf3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<el-button :icon="Refresh" :loading="loading" @click="loadProjects">刷新</el-button>
|
<el-button :icon="Refresh" :loading="loading" @click="loadProjects">刷新</el-button>
|
||||||
<el-button type="primary" :icon="Plus" @click="router.push('/generate')">新建项目</el-button>
|
<el-button type="primary" :icon="Plus" @click="router.push({ path: '/generate', query: { mode: 'one-click' } })">新建项目</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<el-table-column prop="projectName" label="项目名称" min-width="190">
|
<el-table-column prop="projectName" label="项目名称" min-width="190">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="project-name">{{ row.projectName || '未命名项目' }}</div>
|
<div class="project-name">{{ row.projectName || '未命名项目' }}</div>
|
||||||
<div class="project-desc">{{ row.projectDesc || row.requirementKeyword || row.industryTemplate || '暂无描述' }}</div>
|
<div class="project-desc">{{ row.projectDesc || row.requirementKeyword || '暂无描述' }}</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="140">
|
<el-table-column label="状态" width="140">
|
||||||
@@ -32,34 +32,30 @@
|
|||||||
{{ row.updateTime || row.createTime || '-' }}
|
{{ row.updateTime || row.createTime || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="390" fixed="right">
|
<el-table-column label="操作" width="328" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="row-actions">
|
<div class="row-actions">
|
||||||
<el-button class="action-button action-button-wide" :icon="EditPen" plain @click="continueProject(row)">
|
<el-tooltip content="专家编辑" placement="top">
|
||||||
继续生成
|
<el-button class="project-icon-button" :icon="EditPen" circle plain aria-label="专家编辑" @click="continueProject(row)" />
|
||||||
</el-button>
|
</el-tooltip>
|
||||||
<el-button
|
<el-tooltip content="图表中心" placement="top">
|
||||||
class="action-button"
|
<el-button class="project-icon-button" :icon="DataAnalysis" circle plain aria-label="图表中心" @click="openDiagrams(row)" />
|
||||||
:icon="View"
|
</el-tooltip>
|
||||||
plain
|
<el-tooltip content="模块图" placement="top">
|
||||||
:disabled="row.previewStatus !== '1'"
|
<el-button class="project-icon-button" :icon="Connection" circle plain aria-label="模块图" @click="openModuleDiagramEditor(row)" />
|
||||||
@click="router.push(`/project/${projectIdOf(row)}/preview`)"
|
</el-tooltip>
|
||||||
>
|
<el-tooltip content="论文初稿" placement="top">
|
||||||
预览
|
<el-button class="project-icon-button" :icon="Document" circle plain aria-label="论文初稿" @click="openPaperDraft(row)" />
|
||||||
</el-button>
|
</el-tooltip>
|
||||||
<el-button
|
<el-tooltip content="预览" placement="top">
|
||||||
class="action-button"
|
<el-button class="project-icon-button" :icon="View" circle plain aria-label="预览" :disabled="row.previewStatus !== '1'" @click="router.push(`/project/${projectIdOf(row)}/preview`)" />
|
||||||
:icon="Download"
|
</el-tooltip>
|
||||||
plain
|
<el-tooltip content="下载" placement="top">
|
||||||
:disabled="row.previewStatus !== '1'"
|
<el-button class="project-icon-button" :icon="Download" circle plain aria-label="下载" :disabled="row.previewStatus !== '1'" :loading="downloadingProjectId === projectIdOf(row)" @click="downloadSource(row)" />
|
||||||
:loading="downloadingProjectId === projectIdOf(row)"
|
</el-tooltip>
|
||||||
@click="downloadSource(row)"
|
<el-tooltip content="删除" placement="top">
|
||||||
>
|
<el-button class="project-icon-button" :icon="Delete" circle plain type="danger" aria-label="删除" @click="removeProject(row)" />
|
||||||
下载
|
</el-tooltip>
|
||||||
</el-button>
|
|
||||||
<el-button class="action-button" :icon="Delete" type="danger" plain @click="removeProject(row)">
|
|
||||||
删除
|
|
||||||
</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -68,9 +64,7 @@
|
|||||||
<el-empty
|
<el-empty
|
||||||
v-if="!loading && !projects.length"
|
v-if="!loading && !projects.length"
|
||||||
description="暂无项目,创建一个新的项目草稿吧"
|
description="暂无项目,创建一个新的项目草稿吧"
|
||||||
>
|
/>
|
||||||
<el-button type="primary" :icon="Plus" @click="router.push('/generate')">新建项目</el-button>
|
|
||||||
</el-empty>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -80,7 +74,7 @@
|
|||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
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 { deleteProject, downloadProject, listProjects } from '@/api/project'
|
||||||
import { saveBlob, sourceZipName } from '@/utils/download'
|
import { saveBlob, sourceZipName } from '@/utils/download'
|
||||||
|
|
||||||
@@ -121,7 +115,34 @@ function continueProject(project) {
|
|||||||
ElMessage.warning('项目 ID 不存在,无法继续')
|
ElMessage.warning('项目 ID 不存在,无法继续')
|
||||||
return
|
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) {
|
async function downloadSource(project) {
|
||||||
@@ -149,7 +170,7 @@ async function removeProject(project) {
|
|||||||
if (!projectId) return
|
if (!projectId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(`确认删除项目「${project.projectName || projectId}」?`, '删除项目', {
|
await ElMessageBox.confirm(`确认删除项目“${project.projectName || projectId}”?`, '删除项目', {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
confirmButtonText: '删除',
|
confirmButtonText: '删除',
|
||||||
cancelButtonText: '取消'
|
cancelButtonText: '取消'
|
||||||
@@ -187,9 +208,16 @@ onMounted(loadProjects)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.project-desc {
|
.project-desc {
|
||||||
|
display: -webkit-box;
|
||||||
|
max-width: 540px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
color: #667085;
|
color: #667085;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-actions {
|
.row-actions {
|
||||||
@@ -203,17 +231,10 @@ onMounted(loadProjects)
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button {
|
.project-icon-button {
|
||||||
min-width: 72px;
|
width: 36px;
|
||||||
height: 32px;
|
height: 36px;
|
||||||
padding: 7px 10px;
|
padding: 0;
|
||||||
border-color: #d8e0eb;
|
|
||||||
color: #475467;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button-wide {
|
|
||||||
min-width: 94px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
54
RuoYi-Vue/easycode-web/src/views/projectListView.test.mjs
Normal file
54
RuoYi-Vue/easycode-web/src/views/projectListView.test.mjs
Normal file
@@ -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, /<el-table-column label="操作" width="328" fixed="right">/)
|
||||||
|
assert.doesNotMatch(source, /<el-button[^>]*>\s*继续生成\s*<\/el-button>/)
|
||||||
|
assert.doesNotMatch(source, /<el-button[^>]*>\s*预览\s*<\/el-button>/)
|
||||||
|
assert.doesNotMatch(source, /<el-button[^>]*>\s*下载\s*<\/el-button>/)
|
||||||
|
assert.doesNotMatch(source, /<el-button[^>]*>\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, /<el-empty[\s\S]*?<el-button[\s\S]*?<\/el-empty>/)
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user