Add EasyCode front web app

This commit is contained in:
王鹏
2026-05-22 16:26:33 +08:00
parent ca3242915b
commit d39eeca8a9
24 changed files with 2821 additions and 0 deletions

View File

@@ -0,0 +1,235 @@
<template>
<section class="page generate-page">
<div class="panel">
<div class="panel-header">
<div>
<h1 class="panel-title">项目生成工作台</h1>
<p class="muted">先生成数据库结构确认后再进入项目预览</p>
</div>
<el-steps class="steps" :active="activeStep" finish-status="success" simple>
<el-step title="生成数据库" />
<el-step title="预览项目" />
</el-steps>
</div>
<div class="content">
<el-form :model="projectForm" label-position="top" class="project-form">
<el-row :gutter="16">
<el-col :xs="24" :md="8">
<el-form-item label="项目名称">
<el-input v-model="projectForm.projectName" placeholder="例如 客户中心" />
</el-form-item>
</el-col>
<el-col :xs="24" :md="16">
<el-form-item label="需求关键词">
<el-input
v-model="projectForm.keyword"
placeholder="描述业务对象、流程或功能范围"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="补充说明">
<el-input
v-model="projectForm.projectDesc"
type="textarea"
:rows="3"
placeholder="例如字段偏好、模块边界、业务规则"
/>
</el-form-item>
</el-form>
<el-alert
v-if="errorMessage"
class="error-alert"
type="warning"
:title="errorMessage"
show-icon
:closable="false"
/>
<div class="toolbar">
<el-button type="primary" :icon="DataAnalysis" :loading="generating" @click="handleGenerateDatabase">
生成数据库
</el-button>
<el-button :disabled="!projectId || !database.tables.length" :loading="saving" @click="handleSaveDatabase">
保存表结构
</el-button>
<el-button
type="success"
:icon="View"
:disabled="!projectId || !database.tables.length"
:loading="previewing"
@click="handlePreview"
>
预览项目
</el-button>
</div>
</div>
</div>
<div class="panel designer-panel">
<div class="panel-header">
<h2 class="panel-title">数据库设计</h2>
<span class="muted">表数量{{ database.tables.length }}</span>
</div>
<DatabaseDesigner v-if="database.tables.length" v-model="database" />
<el-empty v-else description="点击生成数据库后,这里会展示可编辑表结构" />
</div>
</section>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { DataAnalysis, View } from '@element-plus/icons-vue'
import DatabaseDesigner from '@/components/DatabaseDesigner.vue'
import { createProject, generateDatabase, generateProject, saveDatabase } from '@/api/project'
const route = useRoute()
const router = useRouter()
const activeStep = ref(0)
const generating = ref(false)
const saving = ref(false)
const previewing = ref(false)
const projectId = ref(null)
const errorMessage = ref('')
const database = ref({ tables: [], sql: '' })
const projectForm = reactive({
projectName: '',
keyword: String(route.query.keyword || ''),
projectDesc: '',
industryTemplate: ''
})
function unwrap(result) {
return result?.data || result || {}
}
function normalizeDatabase(payload) {
const data = unwrap(payload)
return {
tables: Array.isArray(data.tables) ? data.tables : [],
sql: data.sql || ''
}
}
async function ensureProject() {
if (projectId.value) return projectId.value
const result = await createProject({
projectName: projectForm.projectName,
projectDesc: projectForm.projectDesc,
industryTemplate: projectForm.industryTemplate
})
const data = unwrap(result)
projectId.value = data.projectId || data.id
if (!projectId.value) {
throw new Error('创建项目成功但未返回项目 ID')
}
return projectId.value
}
async function handleGenerateDatabase() {
if (!projectForm.projectName.trim()) {
ElMessage.warning('请输入项目名称')
return
}
if (!projectForm.keyword.trim() && !projectForm.projectDesc.trim()) {
ElMessage.warning('请输入需求关键词或补充说明')
return
}
generating.value = true
errorMessage.value = ''
try {
const id = await ensureProject()
const result = await generateDatabase(id, {
projectName: projectForm.projectName,
projectDesc: projectForm.projectDesc || projectForm.keyword,
industryTemplate: projectForm.industryTemplate,
extraRequirements: projectForm.keyword
})
database.value = normalizeDatabase(result)
if (!database.value.tables.length) {
errorMessage.value = '后端暂未返回表结构,请稍后重试或手动添加表。'
}
activeStep.value = 1
ElMessage.success('数据库结构生成完成')
} catch (error) {
errorMessage.value = error.message || '生成数据库失败,请确认后端服务已启动。'
} finally {
generating.value = false
}
}
async function handleSaveDatabase() {
if (!projectId.value) return
saving.value = true
try {
await saveDatabase(projectId.value, database.value)
ElMessage.success('表结构已保存')
} finally {
saving.value = false
}
}
async function handlePreview() {
if (!projectId.value) return
previewing.value = true
try {
await saveDatabase(projectId.value, database.value)
await generateProject(projectId.value)
router.push(`/project/${projectId.value}/preview`)
} catch (error) {
errorMessage.value = error.message || '生成项目预览失败,请稍后重试。'
} finally {
previewing.value = false
}
}
</script>
<style scoped lang="scss">
.generate-page {
display: grid;
gap: 18px;
}
.steps {
width: min(430px, 100%);
}
.content {
display: grid;
gap: 16px;
padding: 20px;
}
.project-form {
max-width: 980px;
}
.error-alert {
max-width: 980px;
}
.designer-panel {
overflow: hidden;
}
@media (max-width: 860px) {
.panel-header {
align-items: stretch;
flex-direction: column;
}
}
</style>

View File

@@ -0,0 +1,142 @@
<template>
<section class="home page">
<div class="workbench panel">
<div class="intro">
<p class="eyebrow">EasyCode 前台生成器</p>
<h1>输入业务关键词开始生成你的项目骨架</h1>
<p class="muted">从业务需求到数据库结构再到前后端项目预览流程都在前台完成</p>
</div>
<el-form class="quick-form" @submit.prevent="handleGenerate">
<el-input
v-model="keyword"
size="large"
placeholder="例如:客户管理、设备巡检、合同审批"
clearable
@keyup.enter="handleGenerate"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-button size="large" type="primary" :icon="MagicStick" @click="handleGenerate">
立即生成
</el-button>
<el-button size="large" @click="router.push('/login')">登录</el-button>
</el-form>
<div class="status-strip">
<div>
<strong>第一步</strong>
<span>生成数据库并编辑表字段</span>
</div>
<div>
<strong>第二步</strong>
<span>预览后端前台和后台前端代码</span>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { MagicStick, Search } from '@element-plus/icons-vue'
const router = useRouter()
const keyword = ref('')
function handleGenerate() {
router.push({
path: '/generate',
query: keyword.value.trim() ? { keyword: keyword.value.trim() } : {}
})
}
</script>
<style scoped lang="scss">
.home {
display: flex;
align-items: stretch;
}
.workbench {
display: grid;
gap: 28px;
width: 100%;
padding: 34px;
}
.intro {
max-width: 760px;
.eyebrow {
margin: 0 0 10px;
color: #1d4ed8;
font-weight: 700;
}
h1 {
max-width: 760px;
margin: 0;
color: #111827;
font-size: 38px;
line-height: 1.2;
letter-spacing: 0;
}
.muted {
max-width: 620px;
margin: 14px 0 0;
font-size: 16px;
}
}
.quick-form {
display: grid;
grid-template-columns: minmax(240px, 1fr) auto auto;
gap: 12px;
}
.status-strip {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1px;
overflow: hidden;
border: 1px solid #dfe5ee;
border-radius: 8px;
background: #dfe5ee;
div {
display: flex;
flex-direction: column;
gap: 6px;
padding: 18px;
background: #f8fafc;
}
strong {
color: #111827;
}
span {
color: #667085;
}
}
@media (max-width: 760px) {
.workbench {
padding: 22px;
}
.intro h1 {
font-size: 28px;
}
.quick-form,
.status-strip {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<section class="auth-page page">
<div class="auth-panel panel">
<div class="panel-header">
<h1 class="panel-title">登录 EasyCode</h1>
</div>
<el-form :model="form" label-position="top" class="auth-form">
<el-form-item label="用户名">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
<el-button type="primary" :loading="loading" @click="handleLogin">登录</el-button>
<el-button text @click="router.push('/register')">没有账号去注册</el-button>
</el-form>
</div>
</section>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { login } from '@/api/auth'
import { setToken, setUser } from '@/utils/auth'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const form = reactive({
username: '',
password: ''
})
function pickPayload(result) {
return result?.data || result || {}
}
async function handleLogin() {
if (!form.username || !form.password) {
ElMessage.warning('请输入用户名和密码')
return
}
loading.value = true
try {
const result = await login(form)
const payload = pickPayload(result)
const token = payload.token || payload.access_token
if (!token) {
throw new Error('登录成功但未返回 token')
}
setToken(token)
setUser(payload.user || { userName: form.username })
ElMessage.success('登录成功')
router.push(route.query.redirect || '/generate')
} catch (error) {
ElMessage.error(error.message || '登录失败')
} finally {
loading.value = false
}
}
</script>
<style scoped lang="scss">
.auth-page {
display: grid;
place-items: start center;
}
.auth-panel {
width: min(440px, 100%);
}
.auth-form {
display: grid;
gap: 4px;
padding: 22px;
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<section class="page preview-page">
<div class="panel preview-layout">
<aside class="structure-pane">
<div class="panel-header">
<h1 class="panel-title">项目结构</h1>
<el-button :icon="Refresh" :loading="loading" @click="loadStructures">刷新</el-button>
</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-tabs>
<div class="tree-wrap">
<el-empty v-if="!currentTree.length && !loading" description="暂无项目结构" />
<ProjectStructureTree v-else :data="currentTree" @file-select="handleFileSelect" />
</div>
</aside>
<CodePreview :file-name="selectedFileName" :code="code" />
</div>
</section>
</template>
<script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import CodePreview from '@/components/CodePreview.vue'
import ProjectStructureTree from '@/components/ProjectStructureTree.vue'
import { getFileContent, getProjectStructure } from '@/api/project'
const route = useRoute()
const projectId = computed(() => route.params.projectId)
const activeType = ref('backend')
const loading = ref(false)
const codeLoading = ref(false)
const code = ref('')
const selectedFileName = ref('')
const structures = reactive({
backend: [],
frontend: [],
admin_frontend: []
})
const currentTree = computed(() => structures[activeType.value] || [])
function unwrap(result) {
return result?.data || result || []
}
function normalizeNodes(nodes, 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}/`)
return {
...node,
id: node.id || `${activeType.value}:${path}`,
label,
path,
type: node.type || (children.length ? 'folder' : 'file'),
children
}
})
}
async function loadType(type) {
const result = await getProjectStructure(projectId.value, type)
const data = unwrap(result)
structures[type] = normalizeNodes(data)
}
async function loadStructures() {
loading.value = true
try {
await Promise.all(['backend', 'frontend', 'admin_frontend'].map(loadType))
} catch (error) {
ElMessage.error(error.message || '加载项目结构失败')
} finally {
loading.value = false
}
}
async function handleFileSelect(file) {
codeLoading.value = true
selectedFileName.value = file.path || file.label
code.value = ''
try {
const result = await getFileContent(projectId.value, {
templateType: activeType.value,
nodeId: file.id,
tableId: file.tableId,
category: file.category || file.path || file.label
})
const data = unwrap(result)
code.value = typeof data === 'string' ? data : data.content || ''
} catch (error) {
ElMessage.error(error.message || '加载文件内容失败')
} finally {
codeLoading.value = false
}
}
watch(activeType, () => {
code.value = ''
selectedFileName.value = ''
})
onMounted(loadStructures)
</script>
<style scoped lang="scss">
.preview-layout {
display: grid;
grid-template-columns: 360px minmax(0, 1fr);
min-height: 620px;
overflow: hidden;
}
.structure-pane {
min-width: 0;
background: #ffffff;
}
.type-tabs {
padding: 0 16px;
}
.tree-wrap {
padding: 0 12px 18px;
}
@media (max-width: 900px) {
.preview-layout {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<section class="auth-page page">
<div class="auth-panel panel">
<div class="panel-header">
<h1 class="panel-title">注册 EasyCode</h1>
</div>
<el-form :model="form" label-position="top" class="auth-form">
<el-form-item label="用户名">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="form.confirmPassword" type="password" placeholder="请再次输入密码" show-password />
</el-form-item>
<el-button type="primary" :loading="loading" @click="handleRegister">注册</el-button>
<el-button text @click="router.push('/login')">已有账号去登录</el-button>
</el-form>
</div>
</section>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { register } from '@/api/auth'
const router = useRouter()
const loading = ref(false)
const form = reactive({
username: '',
password: '',
confirmPassword: ''
})
async function handleRegister() {
if (!form.username || !form.password) {
ElMessage.warning('请填写用户名和密码')
return
}
if (form.password !== form.confirmPassword) {
ElMessage.warning('两次输入的密码不一致')
return
}
loading.value = true
try {
await register({
username: form.username,
password: form.password
})
ElMessage.success('注册成功,请登录')
router.push('/login')
} finally {
loading.value = false
}
}
</script>
<style scoped lang="scss">
.auth-page {
display: grid;
place-items: start center;
}
.auth-panel {
width: min(440px, 100%);
}
.auth-form {
display: grid;
gap: 4px;
padding: 22px;
}
</style>