Add EasyCode front web app
This commit is contained in:
12
RuoYi-Vue/easycode-web/src/App.vue
Normal file
12
RuoYi-Vue/easycode-web/src/App.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<AppHeader />
|
||||
<main class="app-main">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AppHeader from '@/components/AppHeader.vue'
|
||||
</script>
|
||||
31
RuoYi-Vue/easycode-web/src/api/auth.js
Normal file
31
RuoYi-Vue/easycode-web/src/api/auth.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import request from './request'
|
||||
|
||||
export function login(data) {
|
||||
return request({
|
||||
url: '/front/auth/login',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function register(data) {
|
||||
return request({
|
||||
url: '/front/auth/register',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function profile() {
|
||||
return request({
|
||||
url: '/front/auth/profile',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return request({
|
||||
url: '/front/auth/logout',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
59
RuoYi-Vue/easycode-web/src/api/project.js
Normal file
59
RuoYi-Vue/easycode-web/src/api/project.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import request from './request'
|
||||
|
||||
function unwrap(res) {
|
||||
return res && Object.prototype.hasOwnProperty.call(res, 'data') ? res.data : res
|
||||
}
|
||||
|
||||
export function createProject(data) {
|
||||
return request({
|
||||
url: '/front/project/create',
|
||||
method: 'post',
|
||||
data
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function generateDatabase(projectId, data) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/generate-database`,
|
||||
method: 'post',
|
||||
data
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function getDatabase(projectId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/database`,
|
||||
method: 'get'
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function saveDatabase(projectId, data) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/database`,
|
||||
method: 'put',
|
||||
data
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function generateProject(projectId) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/preview`,
|
||||
method: 'post'
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function getProjectStructure(projectId, templateType) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/structure`,
|
||||
method: 'get',
|
||||
params: { templateType }
|
||||
}).then(unwrap)
|
||||
}
|
||||
|
||||
export function getFileContent(projectId, params) {
|
||||
return request({
|
||||
url: `/front/project/${projectId}/file-content`,
|
||||
method: 'get',
|
||||
params
|
||||
}).then(unwrap)
|
||||
}
|
||||
51
RuoYi-Vue/easycode-web/src/api/request.js
Normal file
51
RuoYi-Vue/easycode-web/src/api/request.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getToken, removeToken } from '@/utils/auth'
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: '',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
service.interceptors.request.use((config) => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
const result = response.data
|
||||
|
||||
if (result && Object.prototype.hasOwnProperty.call(result, 'code')) {
|
||||
if (result.code === 200) {
|
||||
return result
|
||||
}
|
||||
|
||||
if (result.code === 401) {
|
||||
removeToken()
|
||||
ElMessage.error('登录状态已过期,请重新登录')
|
||||
} else {
|
||||
ElMessage.error(result.msg || '请求处理失败')
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(result.msg || '请求处理失败'))
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
removeToken()
|
||||
ElMessage.error('登录状态已过期,请重新登录')
|
||||
} else {
|
||||
ElMessage.error(error.message || '网络请求失败,请稍后重试')
|
||||
}
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
||||
123
RuoYi-Vue/easycode-web/src/components/AppHeader.vue
Normal file
123
RuoYi-Vue/easycode-web/src/components/AppHeader.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<header class="app-header">
|
||||
<router-link class="brand" to="/">
|
||||
<span class="brand-mark">EC</span>
|
||||
<span>EasyCode</span>
|
||||
</router-link>
|
||||
|
||||
<nav class="nav">
|
||||
<router-link to="/">工作台</router-link>
|
||||
<router-link to="/generate">生成项目</router-link>
|
||||
</nav>
|
||||
|
||||
<div class="actions">
|
||||
<template v-if="token">
|
||||
<span class="user-name">{{ displayName }}</span>
|
||||
<el-button :icon="SwitchButton" @click="handleLogout">退出</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button @click="router.push('/login')">登录</el-button>
|
||||
<el-button type="primary" @click="router.push('/register')">注册</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { SwitchButton } from '@element-plus/icons-vue'
|
||||
import { getToken, getUser, removeToken } from '@/utils/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const token = computed(() => {
|
||||
route.fullPath
|
||||
return getToken()
|
||||
})
|
||||
const user = computed(() => {
|
||||
route.fullPath
|
||||
return getUser()
|
||||
})
|
||||
const displayName = computed(() => user.value?.nickName || user.value?.userName || '已登录')
|
||||
|
||||
function handleLogout() {
|
||||
removeToken()
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 28px;
|
||||
height: 64px;
|
||||
padding: 0 28px;
|
||||
border-bottom: 1px solid #dfe5ee;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #111827;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: inline-grid;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
color: #ffffff;
|
||||
background: #2563eb;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
color: #4b5563;
|
||||
font-size: 14px;
|
||||
|
||||
.router-link-active {
|
||||
color: #1d4ed8;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: #475467;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.app-header {
|
||||
grid-template-columns: 1fr auto;
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
order: 3;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
RuoYi-Vue/easycode-web/src/components/CodePreview.vue
Normal file
56
RuoYi-Vue/easycode-web/src/components/CodePreview.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<section class="code-preview">
|
||||
<div class="preview-head">
|
||||
<span>{{ fileName || '代码预览' }}</span>
|
||||
</div>
|
||||
<el-empty v-if="!code" description="请选择左侧文件查看代码" />
|
||||
<pre v-else><code>{{ code }}</code></pre>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
fileName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
code: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.code-preview {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
min-height: 560px;
|
||||
border-left: 1px solid #e3e8f0;
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.preview-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 48px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid #243044;
|
||||
color: #dbe4f0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
pre {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 18px;
|
||||
overflow: auto;
|
||||
color: #d8dee9;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
:deep(.el-empty__description p) {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
</style>
|
||||
293
RuoYi-Vue/easycode-web/src/components/DatabaseDesigner.vue
Normal file
293
RuoYi-Vue/easycode-web/src/components/DatabaseDesigner.vue
Normal file
@@ -0,0 +1,293 @@
|
||||
<template>
|
||||
<div class="designer">
|
||||
<aside class="table-list">
|
||||
<div class="list-head">
|
||||
<strong>数据表</strong>
|
||||
<el-button :icon="Plus" circle @click="addTable" />
|
||||
</div>
|
||||
<el-empty v-if="!tables.length" description="暂无表结构" :image-size="88" />
|
||||
<button
|
||||
v-for="(table, index) in tables"
|
||||
v-else
|
||||
:key="table.tableId || table.id || index"
|
||||
class="table-item"
|
||||
:class="{ active: selectedIndex === index }"
|
||||
type="button"
|
||||
@click="selectedIndex = index"
|
||||
>
|
||||
<span>{{ table.tableName || '未命名表' }}</span>
|
||||
<small>{{ table.tableComment || '未填写备注' }}</small>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<section class="table-editor">
|
||||
<el-empty v-if="!currentTable" description="添加或选择一张数据表" />
|
||||
<template v-else>
|
||||
<div class="table-form">
|
||||
<el-form label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :md="10">
|
||||
<el-form-item label="表名">
|
||||
<el-input v-model="currentTable.tableName" placeholder="例如 sys_customer" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="10">
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="currentTable.tableComment" placeholder="例如 客户信息表" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="4" class="delete-col">
|
||||
<el-button :icon="Delete" type="danger" plain @click="removeTable(selectedIndex)">
|
||||
删除表
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="field-head">
|
||||
<h3>字段设计</h3>
|
||||
<el-button type="primary" :icon="Plus" @click="addField">添加字段</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="currentColumns" border>
|
||||
<el-table-column label="字段名" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.columnName" placeholder="字段名" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="160">
|
||||
<template #default="{ row }">
|
||||
<el-select v-model="row.columnType" placeholder="字段类型">
|
||||
<el-option label="varchar(255)" value="varchar(255)" />
|
||||
<el-option label="bigint(20)" value="bigint(20)" />
|
||||
<el-option label="int(11)" value="int(11)" />
|
||||
<el-option label="decimal(10,2)" value="decimal(10,2)" />
|
||||
<el-option label="datetime" value="datetime" />
|
||||
<el-option label="text" value="text" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.columnComment" placeholder="字段备注" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="主键" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-checkbox :model-value="row.isPk === '1'" @change="(checked) => row.isPk = checked ? '1' : '0'" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="必填" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-checkbox :model-value="row.isRequired === '1'" @change="(checked) => row.isRequired = checked ? '1' : '0'" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button :icon="Delete" circle text type="danger" @click="removeField($index)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({ tables: [], sql: '' })
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const selectedIndex = ref(0)
|
||||
|
||||
const database = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const tables = computed(() => database.value.tables || [])
|
||||
const currentTable = computed(() => tables.value[selectedIndex.value])
|
||||
const currentColumns = computed(() => {
|
||||
if (!currentTable.value) return []
|
||||
if (!Array.isArray(currentTable.value.columns)) {
|
||||
currentTable.value.columns = []
|
||||
}
|
||||
return currentTable.value.columns
|
||||
})
|
||||
|
||||
watch(
|
||||
() => tables.value.length,
|
||||
(length) => {
|
||||
if (selectedIndex.value >= length) {
|
||||
selectedIndex.value = Math.max(length - 1, 0)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function uid(prefix) {
|
||||
return `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||||
}
|
||||
|
||||
function updateTables(nextTables) {
|
||||
database.value = { ...database.value, tables: nextTables }
|
||||
}
|
||||
|
||||
function addTable() {
|
||||
const nextTables = [
|
||||
...tables.value,
|
||||
{
|
||||
id: uid('table'),
|
||||
tableName: 'new_table',
|
||||
tableComment: '新建数据表',
|
||||
className: 'NewTable',
|
||||
moduleName: 'system',
|
||||
businessName: 'newTable',
|
||||
functionName: '新建数据',
|
||||
tplCategory: 'crud',
|
||||
tplWebType: 'element-plus',
|
||||
columns: [
|
||||
{
|
||||
id: uid('field'),
|
||||
columnName: 'id',
|
||||
columnType: 'bigint(20)',
|
||||
columnComment: '主键',
|
||||
javaType: 'Long',
|
||||
javaField: 'id',
|
||||
isPk: '1',
|
||||
isIncrement: '1',
|
||||
isRequired: '1',
|
||||
isInsert: '1',
|
||||
isEdit: '1',
|
||||
isList: '1',
|
||||
isQuery: '0',
|
||||
queryType: 'EQ',
|
||||
htmlType: 'input'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
updateTables(nextTables)
|
||||
selectedIndex.value = nextTables.length - 1
|
||||
}
|
||||
|
||||
function removeTable(index) {
|
||||
updateTables(tables.value.filter((_, itemIndex) => itemIndex !== index))
|
||||
}
|
||||
|
||||
function addField() {
|
||||
if (!currentTable.value) return
|
||||
currentColumns.value.push({
|
||||
id: uid('field'),
|
||||
columnName: '',
|
||||
columnType: 'varchar(255)',
|
||||
columnComment: '',
|
||||
javaType: 'String',
|
||||
javaField: '',
|
||||
isPk: '0',
|
||||
isIncrement: '0',
|
||||
isRequired: '0',
|
||||
isInsert: '1',
|
||||
isEdit: '1',
|
||||
isList: '1',
|
||||
isQuery: '0',
|
||||
queryType: 'EQ',
|
||||
htmlType: 'input'
|
||||
})
|
||||
}
|
||||
|
||||
function removeField(index) {
|
||||
if (!currentTable.value) return
|
||||
currentColumns.value.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.designer {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
min-height: 430px;
|
||||
}
|
||||
|
||||
.table-list {
|
||||
border-right: 1px solid #e3e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.list-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e3e8f0;
|
||||
}
|
||||
|
||||
.table-item {
|
||||
display: flex;
|
||||
width: calc(100% - 20px);
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
color: #344054;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #9bbcf5;
|
||||
background: #eef5ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
}
|
||||
|
||||
.table-editor {
|
||||
min-width: 0;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.delete-col {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.field-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 4px 0 14px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.designer {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table-list {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #e3e8f0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<el-tree
|
||||
class="structure-tree"
|
||||
:data="data"
|
||||
:props="{ label: 'label', children: 'children' }"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ node, data: nodeData }">
|
||||
<span class="tree-node">
|
||||
<el-icon>
|
||||
<Document v-if="isFile(nodeData)" />
|
||||
<Folder v-else />
|
||||
</el-icon>
|
||||
<span>{{ node.label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Document, Folder } from '@element-plus/icons-vue'
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['file-select'])
|
||||
|
||||
function isFile(node) {
|
||||
if (node?.type === 'folder' || node?.type === 'directory') return false
|
||||
if (node?.type === 'file' || node?.isFile) return true
|
||||
return !node?.children?.length
|
||||
}
|
||||
|
||||
function handleNodeClick(node) {
|
||||
if (isFile(node)) {
|
||||
emit('file-select', node)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.structure-tree {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
17
RuoYi-Vue/easycode-web/src/main.js
Normal file
17
RuoYi-Vue/easycode-web/src/main.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/global.scss'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(ElementPlus)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
37
RuoYi-Vue/easycode-web/src/router/index.js
Normal file
37
RuoYi-Vue/easycode-web/src/router/index.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import HomeView from '@/views/HomeView.vue'
|
||||
import LoginView from '@/views/LoginView.vue'
|
||||
import RegisterView from '@/views/RegisterView.vue'
|
||||
import GenerateView from '@/views/GenerateView.vue'
|
||||
import PreviewView from '@/views/PreviewView.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', name: 'home', component: HomeView },
|
||||
{ path: '/login', name: 'login', component: LoginView },
|
||||
{ path: '/register', name: 'register', component: RegisterView },
|
||||
{ path: '/generate', name: 'generate', component: GenerateView, meta: { requiresAuth: true } },
|
||||
{
|
||||
path: '/project/:projectId/preview',
|
||||
name: 'preview',
|
||||
component: PreviewView,
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
if (to.meta.requiresAuth && !getToken()) {
|
||||
ElMessage.warning('请先登录后再使用生成工作台')
|
||||
return { path: '/login', query: { redirect: to.fullPath } }
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
export default router
|
||||
80
RuoYi-Vue/easycode-web/src/styles/global.scss
Normal file
80
RuoYi-Vue/easycode-web/src/styles/global.scss
Normal file
@@ -0,0 +1,80 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
color: #1f2937;
|
||||
background: #f5f7fb;
|
||||
font-family:
|
||||
Inter,
|
||||
"PingFang SC",
|
||||
"Microsoft YaHei",
|
||||
Arial,
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
|
||||
.page {
|
||||
width: min(1180px, calc(100% - 40px));
|
||||
margin: 0 auto;
|
||||
padding: 28px 0 40px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid #d9e0ea;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #e7ecf3;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0;
|
||||
color: #172033;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.page {
|
||||
width: min(100% - 24px, 1180px);
|
||||
padding-top: 18px;
|
||||
}
|
||||
}
|
||||
31
RuoYi-Vue/easycode-web/src/utils/auth.js
Normal file
31
RuoYi-Vue/easycode-web/src/utils/auth.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const TOKEN_KEY = 'easycode_web_token'
|
||||
const USER_KEY = 'easycode_web_user'
|
||||
|
||||
export function getToken() {
|
||||
return localStorage.getItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(USER_KEY)
|
||||
}
|
||||
|
||||
export function getUser() {
|
||||
const raw = localStorage.getItem(USER_KEY)
|
||||
if (!raw) return null
|
||||
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch (error) {
|
||||
localStorage.removeItem(USER_KEY)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function setUser(user) {
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user || {}))
|
||||
}
|
||||
235
RuoYi-Vue/easycode-web/src/views/GenerateView.vue
Normal file
235
RuoYi-Vue/easycode-web/src/views/GenerateView.vue
Normal 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>
|
||||
142
RuoYi-Vue/easycode-web/src/views/HomeView.vue
Normal file
142
RuoYi-Vue/easycode-web/src/views/HomeView.vue
Normal 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>
|
||||
83
RuoYi-Vue/easycode-web/src/views/LoginView.vue
Normal file
83
RuoYi-Vue/easycode-web/src/views/LoginView.vue
Normal 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>
|
||||
141
RuoYi-Vue/easycode-web/src/views/PreviewView.vue
Normal file
141
RuoYi-Vue/easycode-web/src/views/PreviewView.vue
Normal 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>
|
||||
78
RuoYi-Vue/easycode-web/src/views/RegisterView.vue
Normal file
78
RuoYi-Vue/easycode-web/src/views/RegisterView.vue
Normal 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>
|
||||
Reference in New Issue
Block a user