Add executable business blueprint workflow
This commit is contained in:
@@ -27,6 +27,14 @@ export function getProject(projectId) {
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function updateProject(projectId, data) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}`,
|
||||
method: 'put',
|
||||
data
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function deleteProject(projectId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}`,
|
||||
@@ -34,6 +42,13 @@ export function deleteProject(projectId) {
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function listAvailableModules() {
|
||||
return request({
|
||||
url: '/front/project/options/modules',
|
||||
method: 'get'
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function generateDatabase(projectId, data) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/generate-database`,
|
||||
@@ -52,6 +67,15 @@ export function generateAppBlueprint(projectId, data) {
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function generateBusinessBlueprint(projectId, data) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/generate-business-blueprint`,
|
||||
method: 'post',
|
||||
timeout: GENERATE_REQUEST_TIMEOUT,
|
||||
data
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function getDatabase(projectId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/database`,
|
||||
|
||||
327
RuoYi-Vue/easycode-web/src/components/AppModuleDiagramView.vue
Normal file
327
RuoYi-Vue/easycode-web/src/components/AppModuleDiagramView.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<div class="app-module-diagram">
|
||||
<el-empty v-if="!graph.nodes.length" description="生成应用蓝图后可查看系统功能模块图" />
|
||||
<template v-else>
|
||||
<div class="module-toolbar">
|
||||
<div class="module-summary">
|
||||
<span>节点:{{ graph.nodes.length }}</span>
|
||||
<span>连线:{{ graph.edges.length }}</span>
|
||||
</div>
|
||||
<el-button :icon="Picture" @click="exportPng">导出图片</el-button>
|
||||
</div>
|
||||
|
||||
<div class="module-scroll">
|
||||
<svg ref="svgRef" class="module-canvas" :viewBox="`0 0 ${canvas.width} ${canvas.height}`" :style="canvasStyle">
|
||||
<defs>
|
||||
<marker
|
||||
id="app-module-arrow"
|
||||
markerWidth="10"
|
||||
markerHeight="10"
|
||||
refX="8"
|
||||
refY="3"
|
||||
orient="auto"
|
||||
markerUnits="strokeWidth"
|
||||
>
|
||||
<path d="M0,0 L0,6 L8,3 z" fill="#8aa0bc" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<g class="lane-layer">
|
||||
<g v-for="lane in graph.lanes" :key="lane.key">
|
||||
<rect class="lane-bg" :x="lane.x - 16" y="22" :width="lane.width + 32" :height="canvas.height - 44" rx="10" />
|
||||
<text class="lane-title" :x="lane.x" y="46">{{ lane.title }}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g class="edge-layer">
|
||||
<g v-for="edge in edgeViews" :key="edge.id">
|
||||
<path class="module-edge" :d="edge.path" marker-end="url(#app-module-arrow)" />
|
||||
<text class="edge-label" :x="edge.labelX" :y="edge.labelY">{{ edge.label }}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g class="node-layer">
|
||||
<g
|
||||
v-for="node in graph.nodes"
|
||||
:key="node.id"
|
||||
class="module-node"
|
||||
:class="[node.kind, node.lane]"
|
||||
:transform="`translate(${node.x}, ${node.y})`"
|
||||
>
|
||||
<rect class="module-node-box" :width="node.width" :height="node.height" rx="8" />
|
||||
<rect class="module-node-head" :width="node.width" height="52" rx="8" />
|
||||
<rect class="module-node-head-fill" y="44" :width="node.width" height="8" />
|
||||
<text class="node-title" x="14" y="27">{{ shortText(node.title, 18) }}</text>
|
||||
<text class="node-subtitle" x="14" y="46">{{ shortText(node.subtitle, 28) }}</text>
|
||||
<g v-for="(badge, badgeIndex) in node.badges" :key="badge" :transform="`translate(${badgeX(node, badgeIndex)}, 72)`">
|
||||
<rect class="node-badge-bg" :width="badgeWidth(badge)" height="22" rx="5" />
|
||||
<text class="node-badge-text" :x="badgeWidth(badge) / 2" y="15">{{ shortText(badge, 14) }}</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Picture } from '@element-plus/icons-vue'
|
||||
import { buildAppModuleGraph } from '@/utils/appModuleGraph'
|
||||
import { saveBlob } from '@/utils/download'
|
||||
|
||||
const props = defineProps({
|
||||
blueprint: {
|
||||
type: Object,
|
||||
default: () => ({ roles: [], frontendMenus: [], adminMenus: [] })
|
||||
}
|
||||
})
|
||||
|
||||
const svgRef = ref(null)
|
||||
const graph = computed(() => buildAppModuleGraph(props.blueprint))
|
||||
|
||||
const nodeMap = computed(() => {
|
||||
const map = new Map()
|
||||
graph.value.nodes.forEach((node) => map.set(node.id, node))
|
||||
return map
|
||||
})
|
||||
|
||||
const canvas = computed(() => {
|
||||
const nodes = graph.value.nodes
|
||||
const lanes = graph.value.lanes || []
|
||||
const width = Math.max(980, ...lanes.map((lane) => lane.x + lane.width + 48), ...nodes.map((node) => node.x + node.width + 48))
|
||||
const height = Math.max(430, ...nodes.map((node) => node.y + node.height + 48))
|
||||
return { width, height }
|
||||
})
|
||||
|
||||
const canvasStyle = computed(() => ({
|
||||
width: `${canvas.value.width}px`,
|
||||
height: `${canvas.value.height}px`
|
||||
}))
|
||||
|
||||
const edgeViews = computed(() => graph.value.edges
|
||||
.map((edge) => {
|
||||
const source = nodeMap.value.get(edge.source)
|
||||
const target = nodeMap.value.get(edge.target)
|
||||
if (!source || !target) return null
|
||||
|
||||
const sourceX = source.x + source.width
|
||||
const sourceY = source.y + source.height / 2
|
||||
const targetX = target.x
|
||||
const targetY = target.y + target.height / 2
|
||||
const curve = Math.max(64, Math.abs(targetX - sourceX) / 2)
|
||||
|
||||
return {
|
||||
...edge,
|
||||
path: `M ${sourceX} ${sourceY} C ${sourceX + curve} ${sourceY}, ${targetX - curve} ${targetY}, ${targetX} ${targetY}`,
|
||||
labelX: (sourceX + targetX) / 2,
|
||||
labelY: (sourceY + targetY) / 2 - 9
|
||||
}
|
||||
})
|
||||
.filter(Boolean))
|
||||
|
||||
function shortText(value, maxLength) {
|
||||
const text = String(value ?? '')
|
||||
return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text
|
||||
}
|
||||
|
||||
function badgeWidth(value) {
|
||||
const text = String(value ?? '')
|
||||
return Math.min(112, Math.max(48, text.length * 8 + 20))
|
||||
}
|
||||
|
||||
function badgeX(node, index) {
|
||||
return node.badges.slice(0, index).reduce((x, badge) => x + badgeWidth(badge) + 6, 14)
|
||||
}
|
||||
|
||||
function exportStyle() {
|
||||
return `
|
||||
.lane-bg{fill:#f8fafc;stroke:#e2e8f0;stroke-width:1}
|
||||
.lane-title{fill:#475569;font-family:Arial,sans-serif;font-size:13px;font-weight:700}
|
||||
.module-edge{fill:none;stroke:#8aa0bc;stroke-width:1.7}
|
||||
.edge-label{fill:#64748b;font-family:Arial,sans-serif;font-size:12px;paint-order:stroke;stroke:#f8fafc;stroke-width:4px;text-anchor:middle}
|
||||
.module-node-box{fill:#fff;stroke:#d6e0ee;stroke-width:1}
|
||||
.module-node-head{fill:#eef5ff}
|
||||
.module-node-head-fill{fill:#eef5ff}
|
||||
.module-node.access .module-node-head,.module-node.access .module-node-head-fill{fill:#ecfdf5}
|
||||
.module-node.role .module-node-head,.module-node.role .module-node-head-fill{fill:#eff6ff}
|
||||
.module-node.frontend .module-node-head,.module-node.frontend .module-node-head-fill{fill:#f0fdf4}
|
||||
.module-node.admin .module-node-head,.module-node.admin .module-node-head-fill{fill:#fff7ed}
|
||||
.node-title{fill:#0f172a;font-family:Arial,sans-serif;font-size:15px;font-weight:700}
|
||||
.node-subtitle{fill:#64748b;font-family:Arial,sans-serif;font-size:12px}
|
||||
.node-badge-bg{fill:#f8fafc;stroke:#e2e8f0;stroke-width:1}
|
||||
.node-badge-text{fill:#475569;font-family:Arial,sans-serif;font-size:11px;font-weight:700;text-anchor:middle}`
|
||||
}
|
||||
|
||||
async function exportPng() {
|
||||
if (!svgRef.value) return
|
||||
|
||||
const svg = svgRef.value.cloneNode(true)
|
||||
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
|
||||
style.textContent = exportStyle()
|
||||
svg.insertBefore(style, svg.firstChild)
|
||||
const svgText = `<?xml version="1.0" encoding="UTF-8"?>${svg.outerHTML}`
|
||||
const svgBlob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' })
|
||||
const url = URL.createObjectURL(svgBlob)
|
||||
const image = new Image()
|
||||
image.decoding = 'async'
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
image.onload = resolve
|
||||
image.onerror = reject
|
||||
image.src = url
|
||||
})
|
||||
const canvasElement = document.createElement('canvas')
|
||||
canvasElement.width = canvas.value.width
|
||||
canvasElement.height = canvas.value.height
|
||||
const context = canvasElement.getContext('2d')
|
||||
context.fillStyle = '#f8fafc'
|
||||
context.fillRect(0, 0, canvasElement.width, canvasElement.height)
|
||||
context.drawImage(image, 0, 0)
|
||||
const blob = await new Promise((resolve) => canvasElement.toBlob(resolve, 'image/png'))
|
||||
if (blob) {
|
||||
saveBlob(blob, 'app-module-diagram.png')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.warning('导出图片失败,请稍后重试')
|
||||
} finally {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-module-diagram {
|
||||
background: #f8fafc;
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.module-toolbar {
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e3e8f0;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
.module-summary {
|
||||
color: #64748b;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
font-size: 13px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.module-scroll {
|
||||
min-height: 430px;
|
||||
overflow: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.module-canvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lane-bg {
|
||||
fill: #f8fafc;
|
||||
stroke: #e2e8f0;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.lane-title {
|
||||
fill: #475569;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.module-edge {
|
||||
fill: none;
|
||||
stroke: #8aa0bc;
|
||||
stroke-width: 1.7;
|
||||
}
|
||||
|
||||
.edge-label {
|
||||
fill: #64748b;
|
||||
font-size: 12px;
|
||||
paint-order: stroke;
|
||||
stroke: #f8fafc;
|
||||
stroke-width: 4px;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.module-node-box {
|
||||
fill: #ffffff;
|
||||
filter: drop-shadow(0 8px 18px rgba(15, 23, 42, 0.08));
|
||||
stroke: #d6e0ee;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.module-node-head,
|
||||
.module-node-head-fill {
|
||||
fill: #eef5ff;
|
||||
}
|
||||
|
||||
.module-node.access {
|
||||
.module-node-head,
|
||||
.module-node-head-fill {
|
||||
fill: #ecfdf5;
|
||||
}
|
||||
}
|
||||
|
||||
.module-node.role {
|
||||
.module-node-head,
|
||||
.module-node-head-fill {
|
||||
fill: #eff6ff;
|
||||
}
|
||||
}
|
||||
|
||||
.module-node.frontend {
|
||||
.module-node-head,
|
||||
.module-node-head-fill {
|
||||
fill: #f0fdf4;
|
||||
}
|
||||
}
|
||||
|
||||
.module-node.admin {
|
||||
.module-node-head,
|
||||
.module-node-head-fill {
|
||||
fill: #fff7ed;
|
||||
}
|
||||
}
|
||||
|
||||
.node-title {
|
||||
fill: #0f172a;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.node-subtitle {
|
||||
fill: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.node-badge-bg {
|
||||
fill: #f8fafc;
|
||||
stroke: #e2e8f0;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.node-badge-text {
|
||||
fill: #475569;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.module-toolbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
200
RuoYi-Vue/easycode-web/src/utils/appModuleGraph.js
Normal file
200
RuoYi-Vue/easycode-web/src/utils/appModuleGraph.js
Normal file
@@ -0,0 +1,200 @@
|
||||
const SOURCE_X = 32
|
||||
const FRONTEND_X = 360
|
||||
const ADMIN_X = 688
|
||||
const START_Y = 72
|
||||
const NODE_WIDTH = 252
|
||||
const NODE_HEIGHT = 108
|
||||
const NODE_GAP_Y = 24
|
||||
|
||||
function cleanText(value, fallback = '') {
|
||||
const text = String(value ?? '').trim()
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
function flagOn(value) {
|
||||
return value === true || value === 1 || value === '1' || value === 'true'
|
||||
}
|
||||
|
||||
function normalizeKey(value, fallback) {
|
||||
const text = cleanText(value, fallback)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]+/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
return text || fallback
|
||||
}
|
||||
|
||||
function uniqueKey(value, fallbackPrefix, index, used) {
|
||||
const base = normalizeKey(value, `${fallbackPrefix}_${index + 1}`)
|
||||
let candidate = base
|
||||
let suffix = 2
|
||||
while (used.has(candidate)) {
|
||||
candidate = `${base}_${suffix}`
|
||||
suffix += 1
|
||||
}
|
||||
used.add(candidate)
|
||||
return candidate
|
||||
}
|
||||
|
||||
function normalizeRoles(roles) {
|
||||
const used = new Set()
|
||||
return (Array.isArray(roles) ? roles : []).filter(Boolean).map((role, index) => {
|
||||
const code = uniqueKey(role.code, 'role', index, used)
|
||||
return {
|
||||
code,
|
||||
id: `role:${code}`,
|
||||
title: cleanText(role.name, code),
|
||||
subtitle: cleanText(role.description, '角色'),
|
||||
kind: 'role',
|
||||
lane: 'role',
|
||||
badges: ['角色']
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeMenus(menus, lane, fallbackPrefix) {
|
||||
const used = new Set()
|
||||
return (Array.isArray(menus) ? menus : []).filter(Boolean).map((menu, index) => {
|
||||
const code = uniqueKey(menu.code, fallbackPrefix, index, used)
|
||||
const requiresLogin = flagOn(menu.requiresLogin)
|
||||
const dataScope = cleanText(menu.dataScope, requiresLogin ? 'CURRENT_USER' : 'PUBLIC').toUpperCase()
|
||||
const visibleRoles = Array.isArray(menu.visibleRoles)
|
||||
? menu.visibleRoles.map((role) => normalizeKey(role, '')).filter(Boolean)
|
||||
: []
|
||||
|
||||
return {
|
||||
code,
|
||||
id: `${lane}:${code}`,
|
||||
title: cleanText(menu.name, code),
|
||||
subtitle: cleanText(menu.path, '/'),
|
||||
kind: 'menu',
|
||||
lane,
|
||||
requiresLogin,
|
||||
visibleRoles,
|
||||
dataScope,
|
||||
badges: [
|
||||
lane === 'frontend' ? '前台' : '后台',
|
||||
requiresLogin ? '需登录' : '公开',
|
||||
dataScope
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function shouldUsePublic(menu) {
|
||||
return !menu.requiresLogin || menu.dataScope === 'PUBLIC'
|
||||
}
|
||||
|
||||
function needsPublicAccess(menus) {
|
||||
return menus.some(shouldUsePublic)
|
||||
}
|
||||
|
||||
function needsLoggedInAccess(menus, roleIds) {
|
||||
return menus.some((menu) => {
|
||||
if (!menu.requiresLogin) return false
|
||||
return !menu.visibleRoles.some((roleCode) => roleIds.has(`role:${roleCode}`))
|
||||
})
|
||||
}
|
||||
|
||||
function buildAccessNodes(menus, roleIds) {
|
||||
const nodes = []
|
||||
if (needsPublicAccess(menus)) {
|
||||
nodes.push({
|
||||
code: 'public',
|
||||
id: 'access:public',
|
||||
title: '公开访问',
|
||||
subtitle: '无需登录',
|
||||
kind: 'access',
|
||||
lane: 'access',
|
||||
badges: ['PUBLIC']
|
||||
})
|
||||
}
|
||||
if (needsLoggedInAccess(menus, roleIds)) {
|
||||
nodes.push({
|
||||
code: 'logged_in',
|
||||
id: 'access:logged_in',
|
||||
title: '登录用户',
|
||||
subtitle: '未限定角色',
|
||||
kind: 'access',
|
||||
lane: 'access',
|
||||
badges: ['LOGIN']
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
function applyLayout(nodes) {
|
||||
const laneIndexes = new Map()
|
||||
return nodes.map((node) => {
|
||||
const layoutLane = node.lane === 'admin' ? 'admin' : node.lane === 'frontend' ? 'frontend' : 'source'
|
||||
const index = laneIndexes.get(layoutLane) || 0
|
||||
laneIndexes.set(layoutLane, index + 1)
|
||||
const x = layoutLane === 'admin' ? ADMIN_X : layoutLane === 'frontend' ? FRONTEND_X : SOURCE_X
|
||||
|
||||
return {
|
||||
...node,
|
||||
x,
|
||||
y: START_Y + index * (NODE_HEIGHT + NODE_GAP_Y),
|
||||
width: NODE_WIDTH,
|
||||
height: NODE_HEIGHT
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function edgeLabel(menu) {
|
||||
return menu.dataScope || (menu.requiresLogin ? 'LOGIN' : 'PUBLIC')
|
||||
}
|
||||
|
||||
function edgeId(source, target) {
|
||||
return `${source}->${target}`
|
||||
}
|
||||
|
||||
function buildEdges(menus, roleIds) {
|
||||
const edges = []
|
||||
menus.forEach((menu) => {
|
||||
const validRoleIds = menu.visibleRoles
|
||||
.map((roleCode) => `role:${roleCode}`)
|
||||
.filter((roleId) => roleIds.has(roleId))
|
||||
|
||||
if (validRoleIds.length) {
|
||||
validRoleIds.forEach((roleId) => {
|
||||
edges.push({
|
||||
id: edgeId(roleId, menu.id),
|
||||
source: roleId,
|
||||
target: menu.id,
|
||||
label: edgeLabel(menu)
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const source = shouldUsePublic(menu) ? 'access:public' : 'access:logged_in'
|
||||
edges.push({
|
||||
id: edgeId(source, menu.id),
|
||||
source,
|
||||
target: menu.id,
|
||||
label: edgeLabel(menu)
|
||||
})
|
||||
})
|
||||
return edges
|
||||
}
|
||||
|
||||
export function buildAppModuleGraph(blueprint = {}) {
|
||||
const roles = normalizeRoles(blueprint.roles)
|
||||
const frontendMenus = normalizeMenus(blueprint.frontendMenus, 'frontend', 'frontend_menu')
|
||||
const adminMenus = normalizeMenus(blueprint.adminMenus, 'admin', 'admin_menu')
|
||||
const menus = [...frontendMenus, ...adminMenus]
|
||||
const roleIds = new Set(roles.map((role) => role.id))
|
||||
const accessNodes = buildAccessNodes(menus, roleIds)
|
||||
const nodes = applyLayout([...accessNodes, ...roles, ...frontendMenus, ...adminMenus])
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges: buildEdges(menus, roleIds),
|
||||
lanes: [
|
||||
{ key: 'source', title: '访问角色', x: SOURCE_X, width: NODE_WIDTH },
|
||||
{ key: 'frontend', title: '前台功能模块', x: FRONTEND_X, width: NODE_WIDTH },
|
||||
{ key: 'admin', title: '后台功能模块', x: ADMIN_X, width: NODE_WIDTH }
|
||||
]
|
||||
}
|
||||
}
|
||||
82
RuoYi-Vue/easycode-web/src/utils/appModuleGraph.test.mjs
Normal file
82
RuoYi-Vue/easycode-web/src/utils/appModuleGraph.test.mjs
Normal file
@@ -0,0 +1,82 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { buildAppModuleGraph } from './appModuleGraph.js'
|
||||
|
||||
test('buildAppModuleGraph creates role and menu module nodes with access edges', () => {
|
||||
const graph = buildAppModuleGraph({
|
||||
roles: [
|
||||
{ code: 'reader', name: '读者', description: '前台用户' },
|
||||
{ code: 'admin', name: '管理员', description: '后台运营' }
|
||||
],
|
||||
frontendMenus: [
|
||||
{
|
||||
code: 'catalog',
|
||||
name: '图书目录',
|
||||
path: '/catalog',
|
||||
requiresLogin: false,
|
||||
visibleRoles: [],
|
||||
dataScope: 'PUBLIC'
|
||||
},
|
||||
{
|
||||
code: 'my_borrows',
|
||||
name: '我的借阅',
|
||||
path: '/my-borrows',
|
||||
requiresLogin: true,
|
||||
visibleRoles: ['reader'],
|
||||
dataScope: 'CURRENT_USER'
|
||||
}
|
||||
],
|
||||
adminMenus: [
|
||||
{
|
||||
code: 'book_admin',
|
||||
name: '图书管理',
|
||||
path: '/admin/books',
|
||||
requiresLogin: true,
|
||||
visibleRoles: ['admin'],
|
||||
dataScope: 'ROLE'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
assert.deepEqual(
|
||||
graph.nodes.map((node) => ({ id: node.id, title: node.title, kind: node.kind, lane: node.lane })),
|
||||
[
|
||||
{ id: 'access:public', title: '公开访问', kind: 'access', lane: 'access' },
|
||||
{ id: 'role:reader', title: '读者', kind: 'role', lane: 'role' },
|
||||
{ id: 'role:admin', title: '管理员', kind: 'role', lane: 'role' },
|
||||
{ id: 'frontend:catalog', title: '图书目录', kind: 'menu', lane: 'frontend' },
|
||||
{ id: 'frontend:my_borrows', title: '我的借阅', kind: 'menu', lane: 'frontend' },
|
||||
{ id: 'admin:book_admin', title: '图书管理', kind: 'menu', lane: 'admin' }
|
||||
]
|
||||
)
|
||||
|
||||
assert.deepEqual(
|
||||
graph.edges.map((edge) => ({ source: edge.source, target: edge.target, label: edge.label })),
|
||||
[
|
||||
{ source: 'access:public', target: 'frontend:catalog', label: 'PUBLIC' },
|
||||
{ source: 'role:reader', target: 'frontend:my_borrows', label: 'CURRENT_USER' },
|
||||
{ source: 'role:admin', target: 'admin:book_admin', label: 'ROLE' }
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
test('buildAppModuleGraph creates a logged-in access node for menus without explicit visible roles', () => {
|
||||
const graph = buildAppModuleGraph({
|
||||
roles: [],
|
||||
frontendMenus: [
|
||||
{
|
||||
code: 'profile',
|
||||
name: '个人中心',
|
||||
path: '/profile',
|
||||
requiresLogin: true,
|
||||
visibleRoles: [],
|
||||
dataScope: 'CURRENT_USER'
|
||||
}
|
||||
],
|
||||
adminMenus: []
|
||||
})
|
||||
|
||||
assert.equal(graph.nodes[0].id, 'access:logged_in')
|
||||
assert.equal(graph.edges[0].source, 'access:logged_in')
|
||||
assert.equal(graph.edges[0].target, 'frontend:profile')
|
||||
})
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h1 class="panel-title">项目生成工作台</h1>
|
||||
<p class="muted">先生成数据库结构,确认后再进入项目预览。</p>
|
||||
<p class="muted">先生成应用蓝图,再生成数据库结构,保存修改后生成业务蓝图。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,39 @@
|
||||
placeholder="例如字段偏好、模块边界、业务规则"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :md="8">
|
||||
<el-form-item label="项目形态">
|
||||
<el-switch
|
||||
v-model="projectForm.frontendEnabled"
|
||||
active-text="生成用户前台"
|
||||
inactive-text="仅后台管理"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="16">
|
||||
<el-form-item label="后台功能模块">
|
||||
<el-select
|
||||
v-model="projectForm.moduleIds"
|
||||
multiple
|
||||
clearable
|
||||
filterable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="选择后台维护的功能模块"
|
||||
class="module-select"
|
||||
:loading="loadingModules"
|
||||
>
|
||||
<el-option
|
||||
v-for="module in availableModules"
|
||||
:key="module.moduleId"
|
||||
:label="moduleLabel(module)"
|
||||
:value="module.moduleId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<el-alert
|
||||
@@ -54,6 +87,14 @@
|
||||
<el-button :disabled="!projectId || !database.tables.length" :loading="saving" @click="handleSaveDatabase">
|
||||
保存表结构
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!projectId || !database.tables.length"
|
||||
:loading="generatingBusinessBlueprint"
|
||||
@click="handleGenerateBusinessBlueprint"
|
||||
>
|
||||
生成业务蓝图
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
:icon="View"
|
||||
@@ -67,17 +108,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel app-blueprint-panel">
|
||||
<div v-if="hasAppBlueprint" class="panel app-blueprint-panel">
|
||||
<div class="panel-header database-header">
|
||||
<h2 class="panel-title">应用蓝图</h2>
|
||||
<div class="database-header-actions">
|
||||
<el-radio-group v-model="appBlueprintView" size="large">
|
||||
<el-radio-button label="summary">摘要</el-radio-button>
|
||||
<el-radio-button label="diagram">模块图</el-radio-button>
|
||||
<el-radio-button label="json">JSON</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="muted">角色:{{ appBlueprint.roles.length }}</span>
|
||||
<span class="muted">前台菜单:{{ appBlueprint.frontendMenus.length }}</span>
|
||||
<span class="muted">后台菜单:{{ appBlueprint.adminMenus.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasAppBlueprint" class="app-blueprint-content">
|
||||
<div class="app-blueprint-grid">
|
||||
<div class="app-blueprint-content">
|
||||
<div v-show="appBlueprintView === 'summary'" class="app-blueprint-grid">
|
||||
<section>
|
||||
<h3>角色</h3>
|
||||
<el-tag v-for="role in appBlueprint.roles" :key="role.code" effect="plain">
|
||||
@@ -97,12 +143,14 @@
|
||||
</el-tag>
|
||||
</section>
|
||||
</div>
|
||||
<el-input v-model="appBlueprintJson" type="textarea" :rows="12" spellcheck="false" />
|
||||
<div class="toolbar compact-toolbar">
|
||||
<el-button @click="applyAppBlueprintJson">应用调整</el-button>
|
||||
</div>
|
||||
<AppModuleDiagramView v-show="appBlueprintView === 'diagram'" :blueprint="appBlueprint" />
|
||||
<template v-if="appBlueprintView === 'json'">
|
||||
<el-input v-model="appBlueprintJson" type="textarea" :rows="12" spellcheck="false" />
|
||||
<div class="toolbar compact-toolbar">
|
||||
<el-button @click="applyAppBlueprintJson">应用调整</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<el-empty v-else description="生成应用蓝图后,这里会展示角色和前后台菜单" />
|
||||
</div>
|
||||
|
||||
<div class="panel designer-panel">
|
||||
@@ -148,25 +196,25 @@
|
||||
入参:{{ action.requestFields.join('、') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="Array.isArray(action.rules) && action.rules.length" class="blueprint-section">
|
||||
<div v-if="actionRuleItems(action).length" class="blueprint-section">
|
||||
<strong>规则</strong>
|
||||
<ul>
|
||||
<li v-for="rule in action.rules" :key="rule">{{ rule }}</li>
|
||||
<li v-for="(rule, ruleIndex) in actionRuleItems(action)" :key="`rule-${ruleIndex}-${rule}`">
|
||||
{{ rule }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="Array.isArray(action.effects) && action.effects.length" class="blueprint-section">
|
||||
<strong>影响</strong>
|
||||
<ul>
|
||||
<li v-for="effect in action.effects" :key="`${effect.type}-${effect.targetTable}-${effect.targetField}`">
|
||||
{{ effect.description || effect.type }}
|
||||
<span v-if="effect.targetTable"> / {{ effect.targetTable }}</span>
|
||||
<span v-if="effect.targetField">.{{ effect.targetField }}</span>
|
||||
<li v-for="(effect, effectIndex) in action.effects" :key="blueprintEffectKey(effect, effectIndex)">
|
||||
{{ formatEffect(effect) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<el-empty v-else description="生成数据库后,这里会展示 AI 识别出的业务动作草稿" />
|
||||
<el-empty v-else description="保存表结构后点击生成业务蓝图,这里会展示非 CRUD 业务动作" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -176,29 +224,36 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { DataAnalysis, View } from '@element-plus/icons-vue'
|
||||
import AppModuleDiagramView from '@/components/AppModuleDiagramView.vue'
|
||||
import DatabaseDesigner from '@/components/DatabaseDesigner.vue'
|
||||
import ErDiagramView from '@/components/ErDiagramView.vue'
|
||||
import { createProject, generateAppBlueprint, generateDatabase, generateProject, getDatabase, getProject, saveDatabase } from '@/api/project'
|
||||
import { createProject, generateAppBlueprint, generateBusinessBlueprint, generateDatabase, generateProject, getDatabase, getProject, listAvailableModules, saveDatabase, updateProject } from '@/api/project'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const generating = ref(false)
|
||||
const generatingAppBlueprint = ref(false)
|
||||
const generatingBusinessBlueprint = ref(false)
|
||||
const saving = ref(false)
|
||||
const previewing = ref(false)
|
||||
const loadingProject = ref(false)
|
||||
const loadingModules = ref(false)
|
||||
const databaseView = ref('editor')
|
||||
const appBlueprintView = ref('summary')
|
||||
const projectId = ref(normalizeProjectId(route.query.projectId))
|
||||
const errorMessage = ref('')
|
||||
const appBlueprint = ref(emptyAppBlueprint())
|
||||
const appBlueprintJson = ref(formatJson(emptyAppBlueprint()))
|
||||
const database = ref({ tables: [], sql: '', erDiagram: emptyErDiagram(), businessActions: [] })
|
||||
const availableModules = ref([])
|
||||
const projectForm = reactive({
|
||||
projectName: '',
|
||||
keyword: String(route.query.keyword || ''),
|
||||
projectDesc: '',
|
||||
industryTemplate: ''
|
||||
industryTemplate: '',
|
||||
frontendEnabled: true,
|
||||
moduleIds: []
|
||||
})
|
||||
|
||||
const LAST_PROJECT_KEY = 'easycode_last_project_id'
|
||||
@@ -272,12 +327,127 @@ function normalizeDatabase(payload) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBusinessActions(payload) {
|
||||
const data = unwrap(payload)
|
||||
return Array.isArray(data) ? data : []
|
||||
}
|
||||
|
||||
function normalizeProjectId(value) {
|
||||
const raw = Array.isArray(value) ? value[0] : value
|
||||
const id = Number(raw)
|
||||
return Number.isFinite(id) && id > 0 ? String(id) : ''
|
||||
}
|
||||
|
||||
function normalizeModuleIds(value) {
|
||||
return (Array.isArray(value) ? value : [])
|
||||
.map((item) => Number(item))
|
||||
.filter((item) => Number.isFinite(item) && item > 0)
|
||||
}
|
||||
|
||||
function moduleLabel(module) {
|
||||
const alias = module.moduleAlias ? ` / ${module.moduleAlias}` : ''
|
||||
return `${module.moduleName || module.moduleId}${alias}`
|
||||
}
|
||||
|
||||
const ruleTypeLabels = {
|
||||
EXISTS: '必须存在',
|
||||
NOT_EXISTS: '必须不存在',
|
||||
FIELD_EQUALS: '字段等于',
|
||||
FIELD_NOT_EQUALS: '字段不等于',
|
||||
FIELD_IN: '字段属于',
|
||||
NUMBER_GTE: '数值不小于'
|
||||
}
|
||||
|
||||
const effectTypeLabels = {
|
||||
UPDATE_FIELD: '更新字段',
|
||||
INSERT_ROW: '新增记录',
|
||||
UPDATE_FIELDS: '更新多字段',
|
||||
SET_STATUS: '设置状态',
|
||||
INCREASE_NUMBER: '数值增加',
|
||||
DECREASE_NUMBER: '数值减少'
|
||||
}
|
||||
|
||||
function actionRuleItems(action) {
|
||||
const rules = []
|
||||
if (Array.isArray(action?.rules)) {
|
||||
rules.push(...action.rules.map(formatRule).filter(Boolean))
|
||||
}
|
||||
if (Array.isArray(action?.ruleChecks)) {
|
||||
rules.push(...action.ruleChecks.map(formatRule).filter(Boolean))
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
function formatRule(rule) {
|
||||
if (!rule) return ''
|
||||
if (typeof rule === 'string') return rule
|
||||
|
||||
const parts = [ruleTypeLabels[rule.type] || rule.type || '规则']
|
||||
const target = formatTarget(rule.targetTable, rule.targetField)
|
||||
if (target) parts.push(target)
|
||||
if (Array.isArray(rule.conditionFields) && rule.conditionFields.length) {
|
||||
parts.push(`条件 ${formatFieldList(rule.conditionFields)}`)
|
||||
}
|
||||
if (rule.expectedValue) parts.push(`期望 ${rule.expectedValue}`)
|
||||
if (Array.isArray(rule.expectedValues) && rule.expectedValues.length) {
|
||||
parts.push(`可选 ${formatFieldList(rule.expectedValues)}`)
|
||||
}
|
||||
if (rule.compareValue) parts.push(`比较 ${rule.compareValue}`)
|
||||
if (rule.message) parts.push(`提示 ${rule.message}`)
|
||||
return parts.join(' / ')
|
||||
}
|
||||
|
||||
function formatEffect(effect) {
|
||||
if (!effect) return ''
|
||||
if (typeof effect === 'string') return effect
|
||||
|
||||
const parts = [effect.description || effectTypeLabels[effect.type] || effect.type || '影响']
|
||||
const target = formatTarget(effect.targetTable, effect.targetField)
|
||||
const values = formatValueMap(effect.values)
|
||||
if (target) parts.push(target)
|
||||
if (values) parts.push(values)
|
||||
if (effect.value) parts.push(`值 ${effect.value}`)
|
||||
if (effect.amount) parts.push(`数量 ${effect.amount}`)
|
||||
if (effect.minValue) parts.push(`下限 ${effect.minValue}`)
|
||||
if (Array.isArray(effect.conditionFields) && effect.conditionFields.length) {
|
||||
parts.push(`条件 ${formatFieldList(effect.conditionFields)}`)
|
||||
}
|
||||
if (effect.message) parts.push(`提示 ${effect.message}`)
|
||||
return parts.join(' / ')
|
||||
}
|
||||
|
||||
function blueprintEffectKey(effect, index) {
|
||||
if (!effect || typeof effect === 'string') return `effect-${index}-${effect || ''}`
|
||||
return `effect-${index}-${effect.type || ''}-${effect.targetTable || ''}-${effect.targetField || ''}`
|
||||
}
|
||||
|
||||
function formatTarget(table, field) {
|
||||
if (table && field) return `${table}.${field}`
|
||||
return table || field || ''
|
||||
}
|
||||
|
||||
function formatValueMap(values) {
|
||||
if (!values || typeof values !== 'object' || Array.isArray(values)) return ''
|
||||
return Object.entries(values)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
function formatFieldList(values) {
|
||||
return values.filter(Boolean).join('、')
|
||||
}
|
||||
|
||||
function projectPayload() {
|
||||
return {
|
||||
projectName: projectForm.projectName,
|
||||
projectDesc: projectForm.projectDesc,
|
||||
requirementKeyword: projectForm.keyword,
|
||||
industryTemplate: projectForm.industryTemplate,
|
||||
frontendEnabled: projectForm.frontendEnabled,
|
||||
moduleIds: normalizeModuleIds(projectForm.moduleIds)
|
||||
}
|
||||
}
|
||||
|
||||
function rememberProject(id) {
|
||||
if (id) {
|
||||
localStorage.setItem(LAST_PROJECT_KEY, String(id))
|
||||
@@ -298,6 +468,8 @@ async function loadProjectDraft(value) {
|
||||
projectForm.projectDesc = project.projectDesc || ''
|
||||
projectForm.keyword = project.requirementKeyword || projectForm.keyword || ''
|
||||
projectForm.industryTemplate = project.industryTemplate || ''
|
||||
projectForm.frontendEnabled = project.frontendEnabled !== '0'
|
||||
projectForm.moduleIds = normalizeModuleIds(project.moduleIds)
|
||||
appBlueprint.value = normalizeAppBlueprint(project.appBlueprint)
|
||||
syncAppBlueprintJson()
|
||||
database.value = normalizeDatabase(databaseResult)
|
||||
@@ -310,14 +482,12 @@ async function loadProjectDraft(value) {
|
||||
}
|
||||
|
||||
async function ensureProject() {
|
||||
if (projectId.value) return projectId.value
|
||||
if (projectId.value) {
|
||||
await updateProject(projectId.value, projectPayload())
|
||||
return projectId.value
|
||||
}
|
||||
|
||||
const result = await createProject({
|
||||
projectName: projectForm.projectName,
|
||||
projectDesc: projectForm.projectDesc,
|
||||
requirementKeyword: projectForm.keyword,
|
||||
industryTemplate: projectForm.industryTemplate
|
||||
})
|
||||
const result = await createProject(projectPayload())
|
||||
const data = unwrap(result)
|
||||
projectId.value = data.projectId || data.id
|
||||
|
||||
@@ -337,6 +507,18 @@ async function ensureProject() {
|
||||
return projectId.value
|
||||
}
|
||||
|
||||
async function loadModuleOptions() {
|
||||
loadingModules.value = true
|
||||
try {
|
||||
const result = await listAvailableModules()
|
||||
availableModules.value = Array.isArray(result) ? result : []
|
||||
} catch (error) {
|
||||
ElMessage.warning(error.message || '后台功能模块加载失败')
|
||||
} finally {
|
||||
loadingModules.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function validateRequirementInput() {
|
||||
if (!projectForm.projectName.trim()) {
|
||||
ElMessage.warning('请输入项目名称')
|
||||
@@ -421,6 +603,39 @@ async function handleGenerateDatabase() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGenerateBusinessBlueprint() {
|
||||
if (!validateRequirementInput()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!database.value.tables.length) {
|
||||
ElMessage.warning('请先生成并确认数据库设计')
|
||||
return
|
||||
}
|
||||
|
||||
generatingBusinessBlueprint.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const id = await ensureProject()
|
||||
await saveDatabase(id, database.value)
|
||||
const result = await generateBusinessBlueprint(id, {
|
||||
projectName: projectForm.projectName,
|
||||
projectDesc: projectForm.projectDesc,
|
||||
industryTemplate: projectForm.industryTemplate,
|
||||
extraRequirements: projectForm.keyword
|
||||
})
|
||||
database.value = {
|
||||
...database.value,
|
||||
businessActions: normalizeBusinessActions(result)
|
||||
}
|
||||
ElMessage.success('业务蓝图已生成')
|
||||
} catch (error) {
|
||||
errorMessage.value = error.message || '生成业务蓝图失败,请确认表结构已保存后重试。'
|
||||
} finally {
|
||||
generatingBusinessBlueprint.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveDatabase() {
|
||||
if (!projectId.value) return
|
||||
|
||||
@@ -459,6 +674,7 @@ watch(
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
loadModuleOptions()
|
||||
if (projectId.value) {
|
||||
loadProjectDraft(projectId.value)
|
||||
}
|
||||
@@ -481,6 +697,10 @@ onMounted(() => {
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
.module-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-alert {
|
||||
max-width: 980px;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<el-tabs v-model="activeType" class="type-tabs">
|
||||
<el-tab-pane label="后端" name="backend" />
|
||||
<el-tab-pane label="前台" name="frontend" />
|
||||
<el-tab-pane label="后台前端" name="admin_frontend" />
|
||||
<el-tab-pane
|
||||
v-for="type in templateTypes"
|
||||
:key="type.value"
|
||||
:label="type.label"
|
||||
:name="type.value"
|
||||
/>
|
||||
</el-tabs>
|
||||
<div class="tree-wrap">
|
||||
<el-empty v-if="!currentTree.length && !loading" description="暂无项目结构" />
|
||||
@@ -32,7 +35,7 @@ import { ElMessage } from 'element-plus'
|
||||
import { Download, Refresh } from '@element-plus/icons-vue'
|
||||
import CodePreview from '@/components/CodePreview.vue'
|
||||
import ProjectStructureTree from '@/components/ProjectStructureTree.vue'
|
||||
import { downloadProject, getFileContent, getProjectStructure } from '@/api/project'
|
||||
import { downloadProject, getFileContent, getProject, getProjectStructure } from '@/api/project'
|
||||
import { saveBlob, sourceZipName } from '@/utils/download'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -43,27 +46,37 @@ const codeLoading = ref(false)
|
||||
const downloading = ref(false)
|
||||
const code = ref('')
|
||||
const selectedFileName = ref('')
|
||||
const projectConfig = ref({})
|
||||
const structures = reactive({
|
||||
backend: [],
|
||||
frontend: [],
|
||||
admin_frontend: []
|
||||
})
|
||||
|
||||
const templateTypes = computed(() => {
|
||||
const types = [{ label: '后端', value: 'backend' }]
|
||||
if (projectConfig.value.frontendEnabled !== '0') {
|
||||
types.push({ label: '前台', value: 'frontend' })
|
||||
}
|
||||
types.push({ label: '后台前端', value: 'admin_frontend' })
|
||||
return types
|
||||
})
|
||||
|
||||
const currentTree = computed(() => structures[activeType.value] || [])
|
||||
|
||||
function unwrap(result) {
|
||||
return result?.data || result || []
|
||||
}
|
||||
|
||||
function normalizeNodes(nodes, prefix = '') {
|
||||
function normalizeNodes(nodes, type, prefix = '') {
|
||||
return (Array.isArray(nodes) ? nodes : []).map((node, index) => {
|
||||
const label = node.label || node.name || node.path || `文件 ${index + 1}`
|
||||
const path = node.path || `${prefix}${label}`
|
||||
const children = normalizeNodes(node.children, `${path}/`)
|
||||
const children = normalizeNodes(node.children, type, `${path}/`)
|
||||
|
||||
return {
|
||||
...node,
|
||||
id: node.id || `${activeType.value}:${path}`,
|
||||
id: node.id || `${type}:${path}`,
|
||||
label,
|
||||
path,
|
||||
type: node.type || (children.length ? 'folder' : 'file'),
|
||||
@@ -83,13 +96,25 @@ function pickCodeContent(data) {
|
||||
async function loadType(type) {
|
||||
const result = await getProjectStructure(projectId.value, type)
|
||||
const data = unwrap(result)
|
||||
structures[type] = normalizeNodes(data)
|
||||
structures[type] = normalizeNodes(data, type)
|
||||
}
|
||||
|
||||
async function loadProjectConfig() {
|
||||
const result = await getProject(projectId.value)
|
||||
projectConfig.value = unwrap(result) || {}
|
||||
if (!templateTypes.value.some((type) => type.value === activeType.value)) {
|
||||
activeType.value = 'backend'
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStructures() {
|
||||
loading.value = true
|
||||
try {
|
||||
await Promise.all(['backend', 'frontend', 'admin_frontend'].map(loadType))
|
||||
await loadProjectConfig()
|
||||
structures.backend = []
|
||||
structures.frontend = []
|
||||
structures.admin_frontend = []
|
||||
await Promise.all(templateTypes.value.map((type) => loadType(type.value)))
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '加载项目结构失败')
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user