Add executable business blueprint workflow
This commit is contained in:
@@ -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