423 lines
12 KiB
Vue
423 lines
12 KiB
Vue
|
|
<script setup lang="ts">
|
|||
|
|
import { computed, onMounted, ref } from 'vue';
|
|||
|
|
|
|||
|
|
import { Button, Card, Input, message, Select, Table } from 'ant-design-vue';
|
|||
|
|
|
|||
|
|
import { useVbenForm } from '#/adapter/form';
|
|||
|
|
import { codeGenerationForm } from '#/views/code-generation/config/form';
|
|||
|
|
|
|||
|
|
import type { CodeGenData } from '../utils/data-transformer';
|
|||
|
|
import { validateData } from '../utils/data-transformer';
|
|||
|
|
import {
|
|||
|
|
generateFromTableApi,
|
|||
|
|
getTableStructureApi,
|
|||
|
|
getTablesApi,
|
|||
|
|
} from '../api';
|
|||
|
|
|
|||
|
|
const emit = defineEmits<{
|
|||
|
|
generate: [data: CodeGenData];
|
|||
|
|
back: [];
|
|||
|
|
}>();
|
|||
|
|
|
|||
|
|
// 表单相关
|
|||
|
|
const [Form, formApi] = useVbenForm(codeGenerationForm);
|
|||
|
|
const formData = ref<Record<string, any>>({
|
|||
|
|
class_name: '',
|
|||
|
|
class_comment: '',
|
|||
|
|
icon: '',
|
|||
|
|
sort: 9999,
|
|||
|
|
pid: 0,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 表列表
|
|||
|
|
const tableList = ref<any[]>([]);
|
|||
|
|
const tableLoading = ref(false);
|
|||
|
|
const searchKeyword = ref('');
|
|||
|
|
const selectedTable = ref<string>('');
|
|||
|
|
|
|||
|
|
// 表结构数据
|
|||
|
|
const tableStructure = ref<CodeGenData | null>(null);
|
|||
|
|
const structureLoading = ref(false);
|
|||
|
|
|
|||
|
|
// 过滤后的表列表
|
|||
|
|
const filteredTableList = computed(() => {
|
|||
|
|
if (!searchKeyword.value) {
|
|||
|
|
return tableList.value;
|
|||
|
|
}
|
|||
|
|
return tableList.value.filter(
|
|||
|
|
(table) =>
|
|||
|
|
table.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
|||
|
|
(table.comment && table.comment.toLowerCase().includes(searchKeyword.value.toLowerCase())),
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 加载表列表
|
|||
|
|
const loadTableList = async () => {
|
|||
|
|
tableLoading.value = true;
|
|||
|
|
try {
|
|||
|
|
const res = await getTablesApi();
|
|||
|
|
tableList.value = res || [];
|
|||
|
|
} catch (error: any) {
|
|||
|
|
message.error(error.message || '加载表列表失败');
|
|||
|
|
tableList.value = [];
|
|||
|
|
} finally {
|
|||
|
|
tableLoading.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 选择表
|
|||
|
|
const handleSelectTable = async (tableName: string) => {
|
|||
|
|
if (!tableName) {
|
|||
|
|
selectedTable.value = '';
|
|||
|
|
tableStructure.value = null;
|
|||
|
|
formApi.resetFields();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
selectedTable.value = tableName;
|
|||
|
|
structureLoading.value = true;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 获取表结构
|
|||
|
|
const structureRes = await getTableStructureApi(tableName);
|
|||
|
|
const fields = structureRes || [];
|
|||
|
|
|
|||
|
|
// 生成类名(从表名转换)
|
|||
|
|
const prefix = 'nl_';
|
|||
|
|
let className = tableName.replace(prefix, '');
|
|||
|
|
className = className
|
|||
|
|
.split('_')
|
|||
|
|
.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1))
|
|||
|
|
.join('');
|
|||
|
|
|
|||
|
|
// 获取表注释作为中文名称
|
|||
|
|
const tableInfo = tableList.value.find((t) => t.name === tableName);
|
|||
|
|
const classComment = tableInfo?.comment || tableName;
|
|||
|
|
|
|||
|
|
// 构建代码生成数据
|
|||
|
|
tableStructure.value = {
|
|||
|
|
class_name: className,
|
|||
|
|
class_comment: classComment,
|
|||
|
|
icon: '',
|
|||
|
|
sort: 9999,
|
|||
|
|
pid: 0,
|
|||
|
|
field: fields,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 更新表单数据
|
|||
|
|
formData.value = {
|
|||
|
|
class_name: className,
|
|||
|
|
class_comment: classComment,
|
|||
|
|
icon: '',
|
|||
|
|
sort: 9999,
|
|||
|
|
pid: 0,
|
|||
|
|
};
|
|||
|
|
formApi.setValues(formData.value);
|
|||
|
|
} catch (error: any) {
|
|||
|
|
message.error(error.message || '获取表结构失败');
|
|||
|
|
tableStructure.value = null;
|
|||
|
|
} finally {
|
|||
|
|
structureLoading.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 生成代码
|
|||
|
|
const handleGenerate = async () => {
|
|||
|
|
if (!selectedTable.value) {
|
|||
|
|
message.warning('请先选择数据库表');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!tableStructure.value) {
|
|||
|
|
message.warning('表结构数据未加载,请重试');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证数据
|
|||
|
|
const validation = validateData(tableStructure.value);
|
|||
|
|
if (!validation.valid) {
|
|||
|
|
message.error(validation.message);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 合并表单数据(用户可能修改了基础信息)
|
|||
|
|
const formValues = await formApi.getValues();
|
|||
|
|
const finalData: CodeGenData = {
|
|||
|
|
...tableStructure.value,
|
|||
|
|
class_name: formValues.class_name || tableStructure.value.class_name,
|
|||
|
|
class_comment: formValues.class_comment || tableStructure.value.class_comment,
|
|||
|
|
icon: formValues.icon || tableStructure.value.icon,
|
|||
|
|
sort: formValues.sort ?? tableStructure.value.sort,
|
|||
|
|
pid: formValues.pid ?? tableStructure.value.pid,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
emit('generate', finalData);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 表列配置
|
|||
|
|
const tableColumns = [
|
|||
|
|
{
|
|||
|
|
title: '表名',
|
|||
|
|
dataIndex: 'name',
|
|||
|
|
key: 'name',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '注释',
|
|||
|
|
dataIndex: 'comment',
|
|||
|
|
key: 'comment',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '记录数',
|
|||
|
|
dataIndex: 'rows',
|
|||
|
|
key: 'rows',
|
|||
|
|
width: 100,
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
loadTableList();
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="database-select">
|
|||
|
|
<!-- 顶部操作栏 -->
|
|||
|
|
<Card class="mb-4">
|
|||
|
|
<template #title>
|
|||
|
|
<div class="flex items-center gap-2">
|
|||
|
|
<Button type="text" @click="emit('back')">
|
|||
|
|
<svg
|
|||
|
|
class="mr-2 inline-block"
|
|||
|
|
width="14"
|
|||
|
|
height="14"
|
|||
|
|
viewBox="0 0 14 14"
|
|||
|
|
fill="none"
|
|||
|
|
xmlns="http://www.w3.org/2000/svg"
|
|||
|
|
>
|
|||
|
|
<path
|
|||
|
|
d="M8.75 3.5L5.25 7L8.75 10.5"
|
|||
|
|
stroke="currentColor"
|
|||
|
|
stroke-width="1.5"
|
|||
|
|
stroke-linecap="round"
|
|||
|
|
stroke-linejoin="round"
|
|||
|
|
/>
|
|||
|
|
</svg>
|
|||
|
|
返回
|
|||
|
|
</Button>
|
|||
|
|
<span class="text-lg font-semibold">数据库表模式</span>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<div class="mb-4">
|
|||
|
|
<Input
|
|||
|
|
v-model:value="searchKeyword"
|
|||
|
|
placeholder="搜索表名或注释..."
|
|||
|
|
allow-clear
|
|||
|
|
class="mb-4"
|
|||
|
|
>
|
|||
|
|
<template #prefix>
|
|||
|
|
<i class="fa-solid fa-search"></i>
|
|||
|
|
</template>
|
|||
|
|
</Input>
|
|||
|
|
|
|||
|
|
<div class="mb-4">
|
|||
|
|
<div class="mb-2 text-sm text-gray-600 dark:text-gray-400">
|
|||
|
|
选择数据库表:
|
|||
|
|
</div>
|
|||
|
|
<Select
|
|||
|
|
v-model:value="selectedTable"
|
|||
|
|
placeholder="请选择数据库表"
|
|||
|
|
show-search
|
|||
|
|
:filter-option="false"
|
|||
|
|
style="width: 100%"
|
|||
|
|
:loading="tableLoading"
|
|||
|
|
@change="handleSelectTable"
|
|||
|
|
>
|
|||
|
|
<Select.Option
|
|||
|
|
v-for="table in filteredTableList"
|
|||
|
|
:key="table.name"
|
|||
|
|
:value="table.name"
|
|||
|
|
>
|
|||
|
|
<div class="flex items-center justify-between">
|
|||
|
|
<span>{{ table.name }}</span>
|
|||
|
|
<span class="ml-2 text-xs text-gray-400">
|
|||
|
|
{{ table.comment || '-' }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</Select.Option>
|
|||
|
|
</Select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div v-if="selectedTable" class="rounded-lg bg-blue-50 p-3 text-sm text-blue-700 dark:bg-blue-900/20 dark:text-blue-400">
|
|||
|
|
<i class="fa-solid fa-info-circle mr-2"></i>
|
|||
|
|
已选择表:<strong>{{ selectedTable }}</strong>
|
|||
|
|
<span v-if="tableStructure">
|
|||
|
|
,共 {{ tableStructure.field?.length || 0 }} 个字段
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<!-- 内容区域 -->
|
|||
|
|
<div v-if="tableStructure" class="content-area mb-24 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|||
|
|
<Card title="基础信息" class="shadow-sm">
|
|||
|
|
<Form />
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Card title="字段预览" class="shadow-sm">
|
|||
|
|
<div v-if="structureLoading" class="flex h-64 items-center justify-center">
|
|||
|
|
<div class="text-center">
|
|||
|
|
<i class="fa-solid fa-spinner fa-spin text-2xl text-primary mb-2"></i>
|
|||
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">加载表结构中...</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-else class="max-h-[500px] overflow-auto">
|
|||
|
|
<Table
|
|||
|
|
:columns="[
|
|||
|
|
{ title: '字段名', dataIndex: 'name', key: 'name' },
|
|||
|
|
{ title: '类型', dataIndex: 'type', key: 'type' },
|
|||
|
|
{ title: '长度', dataIndex: 'type_length', key: 'type_length' },
|
|||
|
|
{ title: '注释', dataIndex: 'comment', key: 'comment' },
|
|||
|
|
{ title: '必填', dataIndex: 'not_null', key: 'not_null', customRender: (text: any) => text ? '是' : '否' },
|
|||
|
|
]"
|
|||
|
|
:data-source="tableStructure.field"
|
|||
|
|
:pagination="false"
|
|||
|
|
size="small"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div class="mt-4 flex items-center gap-2 text-sm text-gray-500">
|
|||
|
|
<i class="fa-solid fa-list mr-1"></i>
|
|||
|
|
<span>共 {{ tableStructure.field?.length || 0 }} 个字段</span>
|
|||
|
|
</div>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 固定底部操作栏 -->
|
|||
|
|
<div class="fixed-bottom-bar">
|
|||
|
|
<div class="fixed-bottom-bar-wrapper">
|
|||
|
|
<div class="fixed-bottom-bar-content">
|
|||
|
|
<!-- 左侧状态信息 -->
|
|||
|
|
<div class="flex items-center gap-3">
|
|||
|
|
<div
|
|||
|
|
class="flex h-10 w-10 items-center justify-center rounded-full transition-all"
|
|||
|
|
:class="
|
|||
|
|
tableStructure
|
|||
|
|
? 'bg-primary/10 text-primary'
|
|||
|
|
: 'bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-600'
|
|||
|
|
"
|
|||
|
|
>
|
|||
|
|
<i
|
|||
|
|
:class="tableStructure ? 'fa-solid fa-check-circle' : 'fa-solid fa-circle-info'"
|
|||
|
|
></i>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div
|
|||
|
|
class="text-sm font-medium"
|
|||
|
|
:class="
|
|||
|
|
tableStructure
|
|||
|
|
? 'text-gray-900 dark:text-gray-100'
|
|||
|
|
: 'text-gray-500 dark:text-gray-400'
|
|||
|
|
"
|
|||
|
|
>
|
|||
|
|
<span v-if="tableStructure">
|
|||
|
|
已加载 <span class="text-primary font-semibold">{{ tableStructure.field?.length || 0 }}</span> 个字段
|
|||
|
|
</span>
|
|||
|
|
<span v-else>请先选择数据库表</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="text-xs text-gray-400 dark:text-gray-500">
|
|||
|
|
{{ tableStructure ? '数据已就绪,可以生成代码' : '等待选择表...' }}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 右侧操作按钮 -->
|
|||
|
|
<div class="flex items-center gap-2">
|
|||
|
|
<Button
|
|||
|
|
type="primary"
|
|||
|
|
size="large"
|
|||
|
|
class="action-btn-primary"
|
|||
|
|
:disabled="!tableStructure"
|
|||
|
|
@click="handleGenerate"
|
|||
|
|
>
|
|||
|
|
<i class="fa-solid fa-code mr-2"></i>生成代码
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.database-select {
|
|||
|
|
animation: fade-slide-enter 0.3s ease-out;
|
|||
|
|
padding-bottom: 120px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.content-area {
|
|||
|
|
min-height: 400px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 固定底部操作栏 */
|
|||
|
|
.fixed-bottom-bar {
|
|||
|
|
position: fixed;
|
|||
|
|
bottom: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
z-index: 100;
|
|||
|
|
padding: 0 24px 24px;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fixed-bottom-bar-wrapper {
|
|||
|
|
max-width: 1400px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
background: rgba(255, 255, 255, 0.98);
|
|||
|
|
backdrop-filter: blur(20px) saturate(180%);
|
|||
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|||
|
|
border-radius: 16px;
|
|||
|
|
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
|||
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|||
|
|
pointer-events: all;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .fixed-bottom-bar-wrapper {
|
|||
|
|
background: rgba(31, 41, 55, 0.98);
|
|||
|
|
border-color: rgba(255, 255, 255, 0.12);
|
|||
|
|
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.08);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fixed-bottom-bar-content {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
padding: 20px 24px;
|
|||
|
|
gap: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-btn-primary {
|
|||
|
|
border-radius: 8px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
padding: 0 24px;
|
|||
|
|
height: 44px;
|
|||
|
|
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.action-btn-primary:hover:not(:disabled) {
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.4);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes fade-slide-enter {
|
|||
|
|
from {
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateX(-20px);
|
|||
|
|
}
|
|||
|
|
to {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateX(0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|