Add executable business blueprint workflow

This commit is contained in:
王鹏
2026-05-24 13:29:42 +08:00
parent ecdc015c2f
commit 6682a44a43
40 changed files with 4756 additions and 113 deletions

View File

@@ -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;
}

View File

@@ -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 {