更新数据库管理
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
This commit is contained in:
@@ -39,6 +39,14 @@ export async function generationApi(data: Record<string, any>) {
|
|||||||
return requestClient.post<any>(`${prefix}generation`, data);
|
return requestClient.post<any>(`${prefix}generation`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量代码生成
|
||||||
|
* @param data 数组格式的代码生成数据
|
||||||
|
*/
|
||||||
|
export async function batchGenerationApi(data: Record<string, any>[]) {
|
||||||
|
return requestClient.post<any>(`${prefix}batch-generation`, data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下载压缩包信息
|
* 下载压缩包信息
|
||||||
*/
|
*/
|
||||||
@@ -52,3 +60,26 @@ export async function downloadInfoApi(id: any) {
|
|||||||
export async function codeGenerationDownloadApi(id: any) {
|
export async function codeGenerationDownloadApi(id: any) {
|
||||||
return requestClient.download(`${prefix}download?id=${id}`);
|
return requestClient.download(`${prefix}download?id=${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据库表列表
|
||||||
|
*/
|
||||||
|
export async function getTablesApi() {
|
||||||
|
return requestClient.get<any>(`${prefix}get-tables`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表结构
|
||||||
|
* @param tableName 表名
|
||||||
|
*/
|
||||||
|
export async function getTableStructureApi(tableName: string) {
|
||||||
|
return requestClient.get<any>(`${prefix}get-table-structure`, { params: { table_name: tableName } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库表生成代码生成数据
|
||||||
|
* @param data 包含表名和其他配置的数据
|
||||||
|
*/
|
||||||
|
export async function generateFromTableApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}generate-from-table`, data);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,422 @@
|
|||||||
|
<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>
|
||||||
@@ -3,7 +3,7 @@ import type { UploadChangeParam, UploadFile } from 'ant-design-vue';
|
|||||||
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { Button, Card, Input, message, Select, Tabs, Upload } from 'ant-design-vue';
|
import { Button, Card, Checkbox, Input, message, Select, Tabs, Upload } from 'ant-design-vue';
|
||||||
|
|
||||||
import { JsonViewer } from '@vben/common-ui';
|
import { JsonViewer } from '@vben/common-ui';
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
const mode: GenerationMode = 'json';
|
const mode: GenerationMode = 'json';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
generate: [data: CodeGenData];
|
generate: [data: CodeGenData | CodeGenData[]];
|
||||||
back: [];
|
back: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -34,7 +34,9 @@ const formData = ref<Record<string, any>>({});
|
|||||||
// JSON输入
|
// JSON输入
|
||||||
const jsonText = ref('');
|
const jsonText = ref('');
|
||||||
const jsonError = ref('');
|
const jsonError = ref('');
|
||||||
const parsedData = ref<CodeGenData | null>(null);
|
const parsedData = ref<CodeGenData | CodeGenData[] | null>(null);
|
||||||
|
const isBatchMode = ref(false); // 是否为批量模式
|
||||||
|
const selectedModules = ref<number[]>([]); // 批量模式下选中的模块索引
|
||||||
const activeTab = ref('text');
|
const activeTab = ref('text');
|
||||||
|
|
||||||
// 文件上传相关
|
// 文件上传相关
|
||||||
@@ -49,6 +51,9 @@ const selectedDraft = ref<string>('');
|
|||||||
// 预览数据
|
// 预览数据
|
||||||
const previewData = computed(() => {
|
const previewData = computed(() => {
|
||||||
if (parsedData.value) {
|
if (parsedData.value) {
|
||||||
|
if (Array.isArray(parsedData.value)) {
|
||||||
|
return parsedData.value;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...parsedData.value,
|
...parsedData.value,
|
||||||
field: parsedData.value.field || [],
|
field: parsedData.value.field || [],
|
||||||
@@ -57,6 +62,19 @@ const previewData = computed(() => {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 批量模式下的模块列表
|
||||||
|
const batchModules = computed(() => {
|
||||||
|
if (Array.isArray(parsedData.value)) {
|
||||||
|
return parsedData.value.map((item, index) => ({
|
||||||
|
index,
|
||||||
|
class_name: item.class_name,
|
||||||
|
class_comment: item.class_comment,
|
||||||
|
fieldCount: item.field?.length || 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
// 加载草稿列表
|
// 加载草稿列表
|
||||||
const loadDraftList = () => {
|
const loadDraftList = () => {
|
||||||
drafts.value = listDrafts(mode);
|
drafts.value = listDrafts(mode);
|
||||||
@@ -96,26 +114,42 @@ const parseJson = () => {
|
|||||||
jsonError.value = '';
|
jsonError.value = '';
|
||||||
if (!jsonText.value.trim()) {
|
if (!jsonText.value.trim()) {
|
||||||
parsedData.value = null;
|
parsedData.value = null;
|
||||||
|
isBatchMode.value = false;
|
||||||
|
selectedModules.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(jsonText.value);
|
const json = JSON.parse(jsonText.value);
|
||||||
const transformed = transformJsonToStandard(json);
|
const transformed = transformJsonToStandard(json);
|
||||||
parsedData.value = transformed;
|
|
||||||
|
|
||||||
// 更新表单数据
|
// 判断是否为数组(批量模式)
|
||||||
formData.value = {
|
if (Array.isArray(transformed)) {
|
||||||
class_name: transformed.class_name,
|
isBatchMode.value = true;
|
||||||
class_comment: transformed.class_comment,
|
parsedData.value = transformed;
|
||||||
icon: transformed.icon,
|
// 默认全选
|
||||||
sort: transformed.sort,
|
selectedModules.value = transformed.map((_, index) => index);
|
||||||
pid: transformed.pid,
|
formApi.resetFields();
|
||||||
};
|
} else {
|
||||||
formApi.setValues(formData.value);
|
isBatchMode.value = false;
|
||||||
|
parsedData.value = transformed;
|
||||||
|
selectedModules.value = [];
|
||||||
|
|
||||||
|
// 更新表单数据(单个模式)
|
||||||
|
formData.value = {
|
||||||
|
class_name: transformed.class_name,
|
||||||
|
class_comment: transformed.class_comment,
|
||||||
|
icon: transformed.icon,
|
||||||
|
sort: transformed.sort,
|
||||||
|
pid: transformed.pid,
|
||||||
|
};
|
||||||
|
formApi.setValues(formData.value);
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
jsonError.value = error.message || 'JSON格式错误';
|
jsonError.value = error.message || 'JSON格式错误';
|
||||||
parsedData.value = null;
|
parsedData.value = null;
|
||||||
|
isBatchMode.value = false;
|
||||||
|
selectedModules.value = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -223,6 +257,8 @@ const formatJson = () => {
|
|||||||
const clearJson = () => {
|
const clearJson = () => {
|
||||||
jsonText.value = '';
|
jsonText.value = '';
|
||||||
parsedData.value = null;
|
parsedData.value = null;
|
||||||
|
isBatchMode.value = false;
|
||||||
|
selectedModules.value = [];
|
||||||
formData.value = {};
|
formData.value = {};
|
||||||
formApi.resetFields();
|
formApi.resetFields();
|
||||||
draftId.value = null;
|
draftId.value = null;
|
||||||
@@ -238,25 +274,49 @@ const handleGenerate = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证数据
|
// 批量模式
|
||||||
const validation = validateData(parsedData.value);
|
if (isBatchMode.value && Array.isArray(parsedData.value)) {
|
||||||
if (!validation.valid) {
|
if (selectedModules.value.length === 0) {
|
||||||
message.error(validation.message);
|
message.warning('请至少选择一个模块进行生成');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证选中的模块
|
||||||
|
const selectedData = selectedModules.value.map(index => parsedData.value![index]);
|
||||||
|
for (const item of selectedData) {
|
||||||
|
const validation = validateData(item);
|
||||||
|
if (!validation.valid) {
|
||||||
|
message.error(`模块 "${item.class_comment || item.class_name}" 验证失败:${validation.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('generate', selectedData);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合并表单数据(用户可能修改了基础信息)
|
// 单个模式
|
||||||
const formValues = await formApi.getValues();
|
if (!isBatchMode.value && !Array.isArray(parsedData.value)) {
|
||||||
const finalData: CodeGenData = {
|
// 验证数据
|
||||||
...parsedData.value,
|
const validation = validateData(parsedData.value);
|
||||||
class_name: formValues.class_name || parsedData.value.class_name,
|
if (!validation.valid) {
|
||||||
class_comment: formValues.class_comment || parsedData.value.class_comment,
|
message.error(validation.message);
|
||||||
icon: formValues.icon || parsedData.value.icon,
|
return;
|
||||||
sort: formValues.sort ?? parsedData.value.sort,
|
}
|
||||||
pid: formValues.pid ?? parsedData.value.pid,
|
|
||||||
};
|
|
||||||
|
|
||||||
emit('generate', finalData);
|
// 合并表单数据(用户可能修改了基础信息)
|
||||||
|
const formValues = await formApi.getValues();
|
||||||
|
const finalData: CodeGenData = {
|
||||||
|
...parsedData.value,
|
||||||
|
class_name: formValues.class_name || parsedData.value.class_name,
|
||||||
|
class_comment: formValues.class_comment || parsedData.value.class_comment,
|
||||||
|
icon: formValues.icon || parsedData.value.icon,
|
||||||
|
sort: formValues.sort ?? parsedData.value.sort,
|
||||||
|
pid: formValues.pid ?? parsedData.value.pid,
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('generate', finalData);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除草稿
|
// 删除草稿
|
||||||
@@ -387,7 +447,68 @@ onMounted(() => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div v-if="parsedData" class="content-area mb-24 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<!-- 批量模式 -->
|
||||||
|
<div v-if="isBatchMode && Array.isArray(parsedData)" class="content-area mb-24">
|
||||||
|
<Card title="批量生成模块列表" class="shadow-sm mb-4">
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
共 {{ batchModules.length }} 个模块,已选择 {{ selectedModules.length }} 个
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button size="small" @click="selectedModules = batchModules.map(m => m.index)">
|
||||||
|
全选
|
||||||
|
</Button>
|
||||||
|
<Button size="small" @click="selectedModules = []">
|
||||||
|
取消全选
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2 max-h-[500px] overflow-auto">
|
||||||
|
<div
|
||||||
|
v-for="module in batchModules"
|
||||||
|
:key="module.index"
|
||||||
|
class="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||||
|
:class="selectedModules.includes(module.index) ? 'border-primary bg-primary/5' : 'border-gray-200 dark:border-gray-700'"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 flex-1">
|
||||||
|
<Checkbox
|
||||||
|
:checked="selectedModules.includes(module.index)"
|
||||||
|
@change="(e: any) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
if (!selectedModules.includes(module.index)) {
|
||||||
|
selectedModules.push(module.index);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedModules = selectedModules.filter(i => i !== module.index);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{{ module.class_comment || module.class_name || `模块 ${module.index + 1}` }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
类名: {{ module.class_name }} | 字段数: {{ module.fieldCount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card title="模块预览" class="shadow-sm">
|
||||||
|
<div class="max-h-[500px] overflow-auto">
|
||||||
|
<JsonViewer
|
||||||
|
:value="previewData"
|
||||||
|
:expand-depth="2"
|
||||||
|
copyable
|
||||||
|
boxed
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 单个模式 -->
|
||||||
|
<div v-else-if="parsedData && !isBatchMode" class="content-area mb-24 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
<Card title="基础信息" class="shadow-sm">
|
<Card title="基础信息" class="shadow-sm">
|
||||||
<Form />
|
<Form />
|
||||||
</Card>
|
</Card>
|
||||||
@@ -403,7 +524,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-4 flex items-center gap-2 text-sm text-gray-500">
|
<div class="mt-4 flex items-center gap-2 text-sm text-gray-500">
|
||||||
<i class="fa-solid fa-list mr-1"></i>
|
<i class="fa-solid fa-list mr-1"></i>
|
||||||
<span>共 {{ parsedData.field?.length || 0 }} 个字段</span>
|
<span>共 {{ (parsedData as CodeGenData).field?.length || 0 }} 个字段</span>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -436,12 +557,30 @@ onMounted(() => {
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<span v-if="parsedData">
|
<span v-if="parsedData">
|
||||||
已解析 <span class="text-primary font-semibold">{{ parsedData.field?.length || 0 }}</span> 个字段
|
<template v-if="isBatchMode">
|
||||||
|
已解析 <span class="text-primary font-semibold">{{ batchModules.length }}</span> 个模块
|
||||||
|
<span v-if="selectedModules.length > 0" class="ml-2">
|
||||||
|
(已选择 {{ selectedModules.length }} 个)
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
已解析 <span class="text-primary font-semibold">{{ (parsedData as CodeGenData).field?.length || 0 }}</span> 个字段
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>请先导入JSON数据</span>
|
<span v-else>请先导入JSON数据</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
{{ parsedData ? '数据已就绪,可以生成代码' : '等待数据导入...' }}
|
<template v-if="parsedData">
|
||||||
|
<template v-if="isBatchMode">
|
||||||
|
{{ selectedModules.length > 0 ? `已选择 ${selectedModules.length} 个模块,可以批量生成` : '请选择要生成的模块' }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
数据已就绪,可以生成代码
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
等待数据导入...
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -467,10 +606,11 @@ onMounted(() => {
|
|||||||
type="primary"
|
type="primary"
|
||||||
size="large"
|
size="large"
|
||||||
class="action-btn-primary"
|
class="action-btn-primary"
|
||||||
:disabled="!parsedData"
|
:disabled="!parsedData || (isBatchMode && selectedModules.length === 0)"
|
||||||
@click="handleGenerate"
|
@click="handleGenerate"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-code mr-2"></i>生成代码
|
<i class="fa-solid fa-code mr-2"></i>
|
||||||
|
{{ isBatchMode ? `批量生成 (${selectedModules.length})` : '生成代码' }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
export type GenerationMode = 'json' | 'manual' | 'ai';
|
export type GenerationMode = 'json' | 'manual' | 'ai' | 'database';
|
||||||
|
|
||||||
interface ModeOption {
|
interface ModeOption {
|
||||||
mode: GenerationMode;
|
mode: GenerationMode;
|
||||||
@@ -26,6 +26,14 @@ const modes: ModeOption[] = [
|
|||||||
borderGradient:
|
borderGradient:
|
||||||
'linear-gradient(90deg, #f59e0b, #f97316, #fb923c, #f97316, #f59e0b)',
|
'linear-gradient(90deg, #f59e0b, #f97316, #fb923c, #f97316, #f59e0b)',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
mode: 'database',
|
||||||
|
title: '数据库表',
|
||||||
|
description: '从现有数据库表自动生成CRUD代码',
|
||||||
|
iconColor: '#3b82f6',
|
||||||
|
borderGradient:
|
||||||
|
'linear-gradient(90deg, #3b82f6, #2563eb, #1d4ed8, #2563eb, #3b82f6)',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
mode: 'ai',
|
mode: 'ai',
|
||||||
title: 'AI提示词',
|
title: 'AI提示词',
|
||||||
@@ -47,7 +55,7 @@ const handleSelect = (mode: GenerationMode) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mode-selector">
|
<div class="mode-selector">
|
||||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-3">
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in modes"
|
v-for="(item, index) in modes"
|
||||||
:key="item.mode"
|
:key="item.mode"
|
||||||
@@ -221,6 +229,57 @@ const handleSelect = (mode: GenerationMode) => {
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
<!-- 数据库表图标 -->
|
||||||
|
<svg
|
||||||
|
v-else-if="item.mode === 'database'"
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
:style="{ color: item.iconColor }"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="8"
|
||||||
|
y="12"
|
||||||
|
width="32"
|
||||||
|
height="24"
|
||||||
|
rx="2"
|
||||||
|
fill="currentColor"
|
||||||
|
fill-opacity="0.1"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="8"
|
||||||
|
y="12"
|
||||||
|
width="32"
|
||||||
|
height="24"
|
||||||
|
rx="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="8"
|
||||||
|
y1="20"
|
||||||
|
x2="40"
|
||||||
|
y2="20"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="8"
|
||||||
|
y1="28"
|
||||||
|
x2="40"
|
||||||
|
y2="28"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<circle cx="16" cy="16" r="2" fill="currentColor" />
|
||||||
|
<circle cx="16" cy="24" r="2" fill="currentColor" />
|
||||||
|
<circle cx="16" cy="32" r="2" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
<!-- AI图标 -->
|
<!-- AI图标 -->
|
||||||
<svg
|
<svg
|
||||||
v-else-if="item.mode === 'ai'"
|
v-else-if="item.mode === 'ai'"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { JsonViewer } from '@vben/common-ui';
|
|||||||
|
|
||||||
import { downloadByData } from '#/util/tool';
|
import { downloadByData } from '#/util/tool';
|
||||||
import {
|
import {
|
||||||
|
batchGenerationApi,
|
||||||
codeGenerationDownloadApi,
|
codeGenerationDownloadApi,
|
||||||
generationApi,
|
generationApi,
|
||||||
getCodeGenerationListApi,
|
getCodeGenerationListApi,
|
||||||
@@ -18,6 +19,7 @@ import type { CodeGenData } from './utils/data-transformer';
|
|||||||
import type { GenerationMode } from './utils/draft-manager';
|
import type { GenerationMode } from './utils/draft-manager';
|
||||||
|
|
||||||
import AiPrompt from './components/ai-prompt.vue';
|
import AiPrompt from './components/ai-prompt.vue';
|
||||||
|
import DatabaseSelect from './components/database-select.vue';
|
||||||
import JsonImport from './components/json-import.vue';
|
import JsonImport from './components/json-import.vue';
|
||||||
import ManualForm from './components/manual-form.vue';
|
import ManualForm from './components/manual-form.vue';
|
||||||
import ModeSelector from './components/mode-selector.vue';
|
import ModeSelector from './components/mode-selector.vue';
|
||||||
@@ -51,6 +53,9 @@ const JsonImportComponent = defineAsyncComponent(() =>
|
|||||||
const ManualFormComponent = defineAsyncComponent(() =>
|
const ManualFormComponent = defineAsyncComponent(() =>
|
||||||
Promise.resolve(ManualForm),
|
Promise.resolve(ManualForm),
|
||||||
);
|
);
|
||||||
|
const DatabaseSelectComponent = defineAsyncComponent(() =>
|
||||||
|
Promise.resolve(DatabaseSelect),
|
||||||
|
);
|
||||||
const AiPromptComponent = defineAsyncComponent(() =>
|
const AiPromptComponent = defineAsyncComponent(() =>
|
||||||
Promise.resolve(AiPrompt),
|
Promise.resolve(AiPrompt),
|
||||||
);
|
);
|
||||||
@@ -62,6 +67,7 @@ const currentMode = ref<GenerationMode | null>(null);
|
|||||||
const modeTitles: Record<GenerationMode, string> = {
|
const modeTitles: Record<GenerationMode, string> = {
|
||||||
json: 'JSON导入模式',
|
json: 'JSON导入模式',
|
||||||
manual: '手动填写模式',
|
manual: '手动填写模式',
|
||||||
|
database: '数据库表模式',
|
||||||
ai: 'AI提示词模式',
|
ai: 'AI提示词模式',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,25 +81,51 @@ const handleBack = () => {
|
|||||||
currentMode.value = null;
|
currentMode.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 生成代码
|
// 生成代码(支持单个或批量)
|
||||||
const handleGenerate = async (data: CodeGenData) => {
|
const handleGenerate = async (data: CodeGenData | CodeGenData[]) => {
|
||||||
try {
|
try {
|
||||||
const res = await generationApi(data);
|
// 判断是否为数组(批量生成)
|
||||||
confirm({
|
if (Array.isArray(data)) {
|
||||||
cancelText: '迟点下载',
|
const res = await batchGenerationApi(data);
|
||||||
confirmText: '是的,马上下载',
|
confirm({
|
||||||
content:
|
cancelText: '迟点下载',
|
||||||
'您的模块已经生成成功,是否立即下载?\r\n' +
|
confirmText: '查看生成结果',
|
||||||
'后端代码已经自动生成:\r\n' +
|
content:
|
||||||
'(Controller、Service、Model、Route、Mysql数据表),无需您操作。\r\n' +
|
`已成功生成 ${res.length} 个模块!\r\n` +
|
||||||
'您只需要把代码黏贴至 "您的根目录/apps/web-antd/src/views/my-gen"文件夹下即可使用',
|
'后端代码已经自动生成:\r\n' +
|
||||||
icon: 'success',
|
'(Controller、Service、Model、Route、Mysql数据表),无需您操作。\r\n' +
|
||||||
title: '代码生成成功!',
|
'您只需要把代码黏贴至 "您的根目录/apps/web-antd/src/views/my-gen"文件夹下即可使用',
|
||||||
}).then(() => {
|
icon: 'success',
|
||||||
codeGenerationDownloadApi(res.id).then((blob) => {
|
title: '批量代码生成成功!',
|
||||||
downloadByData(blob, res.file_name, 'application/zip');
|
}).then(() => {
|
||||||
|
// 批量下载所有生成的模块
|
||||||
|
res.forEach((item: any, index: number) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
codeGenerationDownloadApi(item.id).then((blob) => {
|
||||||
|
downloadByData(blob, item.file_name, 'application/zip');
|
||||||
|
});
|
||||||
|
}, index * 500); // 延迟下载避免浏览器阻止
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
} else {
|
||||||
|
// 单个生成
|
||||||
|
const res = await generationApi(data);
|
||||||
|
confirm({
|
||||||
|
cancelText: '迟点下载',
|
||||||
|
confirmText: '是的,马上下载',
|
||||||
|
content:
|
||||||
|
'您的模块已经生成成功,是否立即下载?\r\n' +
|
||||||
|
'后端代码已经自动生成:\r\n' +
|
||||||
|
'(Controller、Service、Model、Route、Mysql数据表),无需您操作。\r\n' +
|
||||||
|
'您只需要把代码黏贴至 "您的根目录/apps/web-antd/src/views/my-gen"文件夹下即可使用',
|
||||||
|
icon: 'success',
|
||||||
|
title: '代码生成成功!',
|
||||||
|
}).then(() => {
|
||||||
|
codeGenerationDownloadApi(res.id).then((blob) => {
|
||||||
|
downloadByData(blob, res.file_name, 'application/zip');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
// 生成成功后刷新历史记录
|
// 生成成功后刷新历史记录
|
||||||
if (historyDrawerVisible.value) {
|
if (historyDrawerVisible.value) {
|
||||||
loadHistoryList();
|
loadHistoryList();
|
||||||
@@ -201,6 +233,14 @@ const handleViewJson = (item: GenerationHistory) => {
|
|||||||
@back="handleBack"
|
@back="handleBack"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 数据库表模式 -->
|
||||||
|
<DatabaseSelectComponent
|
||||||
|
v-else-if="currentMode === 'database'"
|
||||||
|
key="database"
|
||||||
|
@generate="handleGenerate"
|
||||||
|
@back="handleBack"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- AI提示词模式 -->
|
<!-- AI提示词模式 -->
|
||||||
<AiPromptComponent
|
<AiPromptComponent
|
||||||
v-else-if="currentMode === 'ai'"
|
v-else-if="currentMode === 'ai'"
|
||||||
|
|||||||
@@ -45,9 +45,62 @@ const createDefaultField = (): FieldType => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON模式数据转换
|
* JSON模式数据转换
|
||||||
|
* 支持单个对象或对象数组(批量生成)
|
||||||
*/
|
*/
|
||||||
export function transformJsonToStandard(json: any): CodeGenData {
|
export function transformJsonToStandard(json: any): CodeGenData | CodeGenData[] {
|
||||||
// 如果JSON包含完整结构,直接使用
|
// 如果输入是数组(批量生成多个模块)
|
||||||
|
if (Array.isArray(json) && json.length > 0) {
|
||||||
|
// 检查第一个元素是否是完整的模块配置
|
||||||
|
if (json[0] && json[0].class_name && json[0].field) {
|
||||||
|
// 数组中的每个元素都是完整的模块配置
|
||||||
|
return json.map((item: any) => ({
|
||||||
|
class_name: item.class_name || '',
|
||||||
|
class_comment: item.class_comment || '',
|
||||||
|
icon: item.icon || '',
|
||||||
|
sort: item.sort ?? 9999,
|
||||||
|
pid: item.pid ?? 0,
|
||||||
|
field: Array.isArray(item.field)
|
||||||
|
? item.field.map((f: any) => ({
|
||||||
|
name: f.name || '',
|
||||||
|
type: f.type || 'varchar',
|
||||||
|
type_length: f.type_length || '',
|
||||||
|
default: f.default || '',
|
||||||
|
comment: f.comment || '',
|
||||||
|
not_null: f.not_null !== undefined ? f.not_null : true,
|
||||||
|
formShow: f.formShow !== undefined ? f.formShow : true,
|
||||||
|
tableShow: f.tableShow !== undefined ? f.tableShow : true,
|
||||||
|
formType: f.formType || 'VbenInput',
|
||||||
|
search: f.search !== undefined ? f.search : true,
|
||||||
|
searchValue: f.searchValue || '=',
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// 数组中的元素只是字段数组,需要用户补充基础信息
|
||||||
|
return {
|
||||||
|
class_name: '',
|
||||||
|
class_comment: '',
|
||||||
|
icon: '',
|
||||||
|
sort: 9999,
|
||||||
|
pid: 0,
|
||||||
|
field: json.map((f: any) => ({
|
||||||
|
name: f.name || '',
|
||||||
|
type: f.type || 'varchar',
|
||||||
|
type_length: f.type_length || '',
|
||||||
|
default: f.default || '',
|
||||||
|
comment: f.comment || '',
|
||||||
|
not_null: f.not_null !== undefined ? f.not_null : true,
|
||||||
|
formShow: f.formShow !== undefined ? f.formShow : true,
|
||||||
|
tableShow: f.tableShow !== undefined ? f.tableShow : true,
|
||||||
|
formType: f.formType || 'VbenInput',
|
||||||
|
search: f.search !== undefined ? f.search : true,
|
||||||
|
searchValue: f.searchValue || '=',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果JSON包含完整结构,直接使用(单个模块)
|
||||||
if (json.class_name && json.field) {
|
if (json.class_name && json.field) {
|
||||||
return {
|
return {
|
||||||
class_name: json.class_name || '',
|
class_name: json.class_name || '',
|
||||||
@@ -73,30 +126,6 @@ export function transformJsonToStandard(json: any): CodeGenData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果只有字段数组,需要用户补充基础信息
|
|
||||||
if (Array.isArray(json)) {
|
|
||||||
return {
|
|
||||||
class_name: '',
|
|
||||||
class_comment: '',
|
|
||||||
icon: '',
|
|
||||||
sort: 9999,
|
|
||||||
pid: 0,
|
|
||||||
field: json.map((f: any) => ({
|
|
||||||
name: f.name || '',
|
|
||||||
type: f.type || 'varchar',
|
|
||||||
type_length: f.type_length || '',
|
|
||||||
default: f.default || '',
|
|
||||||
comment: f.comment || '',
|
|
||||||
not_null: f.not_null !== undefined ? f.not_null : true,
|
|
||||||
formShow: f.formShow !== undefined ? f.formShow : true,
|
|
||||||
tableShow: f.tableShow !== undefined ? f.tableShow : true,
|
|
||||||
formType: f.formType || 'VbenInput',
|
|
||||||
search: f.search !== undefined ? f.search : true,
|
|
||||||
searchValue: f.searchValue || '=',
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认返回空结构
|
// 默认返回空结构
|
||||||
return {
|
return {
|
||||||
class_name: '',
|
class_name: '',
|
||||||
|
|||||||
114
apps/web-antd/src/views/system/database/api/index.ts
Normal file
114
apps/web-antd/src/views/system/database/api/index.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
|
const prefix = 'database/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表列表
|
||||||
|
*/
|
||||||
|
export async function getTableListApi() {
|
||||||
|
return requestClient.get<any>(`${prefix}list-tables`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表详细信息
|
||||||
|
* @param tableName 表名
|
||||||
|
*/
|
||||||
|
export async function getTableInfoApi(tableName: string) {
|
||||||
|
return requestClient.get<any>(`${prefix}get-table-info`, { params: { table_name: tableName } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表数据(分页)
|
||||||
|
* @param data 查询参数
|
||||||
|
*/
|
||||||
|
export async function getTableDataApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}get-table-data`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新表数据
|
||||||
|
* @param data 更新参数
|
||||||
|
*/
|
||||||
|
export async function updateTableDataApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update-table-data`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除表数据
|
||||||
|
* @param data 删除参数
|
||||||
|
*/
|
||||||
|
export async function deleteTableDataApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}delete-table-data`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入表数据
|
||||||
|
* @param data 插入参数
|
||||||
|
*/
|
||||||
|
export async function insertTableDataApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}insert-table-data`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新表注释
|
||||||
|
* @param data 更新参数
|
||||||
|
*/
|
||||||
|
export async function updateTableCommentApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update-table-comment`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新字段注释
|
||||||
|
* @param data 更新参数
|
||||||
|
*/
|
||||||
|
export async function updateColumnCommentApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update-column-comment`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加索引
|
||||||
|
* @param data 添加参数
|
||||||
|
*/
|
||||||
|
export async function addIndexApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}add-index`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除索引
|
||||||
|
* @param data 删除参数
|
||||||
|
*/
|
||||||
|
export async function dropIndexApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}drop-index`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改表结构
|
||||||
|
* @param data 修改参数
|
||||||
|
*/
|
||||||
|
export async function updateTableStructureApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}update-table-structure`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除表数据
|
||||||
|
* @param data 删除参数
|
||||||
|
*/
|
||||||
|
export async function batchDeleteTableDataApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}batch-delete-table-data`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行SQL查询(仅SELECT)
|
||||||
|
* @param data 查询参数
|
||||||
|
*/
|
||||||
|
export async function executeSqlQueryApi(data: Record<string, any>) {
|
||||||
|
return requestClient.post<any>(`${prefix}execute-sql-query`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取变更记录列表
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
export async function getChangeLogApi(params: Record<string, any>) {
|
||||||
|
return requestClient.get<any>(`${prefix}get-change-log`, { params });
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { updateTableStructureApi } from '../api';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'AddColumnModal',
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableName = ref('');
|
||||||
|
const columns = ref<any[]>([]);
|
||||||
|
const reloadCallback = ref<(() => void) | null>(null);
|
||||||
|
|
||||||
|
// 字段类型选项
|
||||||
|
const fieldTypeOptions = [
|
||||||
|
{ label: 'VARCHAR', value: 'VARCHAR' },
|
||||||
|
{ label: 'CHAR', value: 'CHAR' },
|
||||||
|
{ label: 'TEXT', value: 'TEXT' },
|
||||||
|
{ label: 'LONGTEXT', value: 'LONGTEXT' },
|
||||||
|
{ label: 'INT', value: 'INT' },
|
||||||
|
{ label: 'BIGINT', value: 'BIGINT' },
|
||||||
|
{ label: 'TINYINT', value: 'TINYINT' },
|
||||||
|
{ label: 'SMALLINT', value: 'SMALLINT' },
|
||||||
|
{ label: 'DECIMAL', value: 'DECIMAL' },
|
||||||
|
{ label: 'FLOAT', value: 'FLOAT' },
|
||||||
|
{ label: 'DOUBLE', value: 'DOUBLE' },
|
||||||
|
{ label: 'DATETIME', value: 'DATETIME' },
|
||||||
|
{ label: 'DATE', value: 'DATE' },
|
||||||
|
{ label: 'TIMESTAMP', value: 'TIMESTAMP' },
|
||||||
|
{ label: 'TIME', value: 'TIME' },
|
||||||
|
{ label: 'YEAR', value: 'YEAR' },
|
||||||
|
{ label: 'BOOLEAN', value: 'TINYINT' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 需要长度的类型
|
||||||
|
const typesNeedLength = ['VARCHAR', 'CHAR', 'INT', 'BIGINT', 'TINYINT', 'SMALLINT', 'DECIMAL', 'FLOAT', 'DOUBLE'];
|
||||||
|
|
||||||
|
// 位置选项(AFTER字段)
|
||||||
|
const positionOptions = computed(() => {
|
||||||
|
if (!columns.value) return [];
|
||||||
|
return columns.value.map((col) => ({
|
||||||
|
label: `${col.name}${col.comment ? ` (${col.comment})` : ''}`,
|
||||||
|
value: col.name,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单schema
|
||||||
|
const formSchema = computed(() => {
|
||||||
|
const baseSchema = [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '字段名',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入字段名(小写字母、数字、下划线)',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '字段类型',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
options: fieldTypeOptions,
|
||||||
|
placeholder: '请选择字段类型',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 根据类型动态添加长度字段
|
||||||
|
const lengthField = {
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'length',
|
||||||
|
label: '长度/精度',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '例如: 255 或 10,2 (DECIMAL类型)',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
show: {
|
||||||
|
triggerFields: ['type'],
|
||||||
|
condition: (values: any) => {
|
||||||
|
const type = values.type || '';
|
||||||
|
return typesNeedLength.includes(type.toUpperCase());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const afterField = {
|
||||||
|
component: 'Select',
|
||||||
|
fieldName: 'after',
|
||||||
|
label: '位置(AFTER字段)',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
options: positionOptions.value,
|
||||||
|
placeholder: '选择字段位置,留空则添加到表末尾',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
...baseSchema,
|
||||||
|
lengthField,
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'default_value',
|
||||||
|
label: '默认值',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入默认值,留空表示无默认值',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Switch',
|
||||||
|
fieldName: 'nullable',
|
||||||
|
label: '允许NULL',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Textarea',
|
||||||
|
fieldName: 'comment',
|
||||||
|
label: '字段注释',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
rows: 3,
|
||||||
|
placeholder: '请输入字段注释',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
afterField,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
wrapperClass: 'grid-cols-12',
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: formSchema,
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
const valid = await formApi.validate();
|
||||||
|
if (valid.valid) {
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
try {
|
||||||
|
// 构建column_info
|
||||||
|
const columnInfo: any = {
|
||||||
|
name: values.name,
|
||||||
|
type: values.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理长度
|
||||||
|
if (values.length && typesNeedLength.includes(values.type.toUpperCase())) {
|
||||||
|
columnInfo.length = values.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理默认值
|
||||||
|
if (values.default_value !== null && values.default_value !== undefined && values.default_value !== '') {
|
||||||
|
columnInfo.default_value = values.default_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理NULL
|
||||||
|
columnInfo.nullable = values.nullable || false;
|
||||||
|
|
||||||
|
// 处理注释
|
||||||
|
if (values.comment) {
|
||||||
|
columnInfo.comment = values.comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理位置
|
||||||
|
if (values.after) {
|
||||||
|
columnInfo.after = values.after;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateTableStructureApi({
|
||||||
|
table_name: tableName.value,
|
||||||
|
action: 'add',
|
||||||
|
column_info: columnInfo,
|
||||||
|
});
|
||||||
|
message.success('添加成功');
|
||||||
|
reloadCallback.value?.();
|
||||||
|
modalApi.close();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '添加失败');
|
||||||
|
} finally {
|
||||||
|
modalApi.setState({ loading: false, confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const data = modalApi.getData<{
|
||||||
|
tableName: string;
|
||||||
|
columns: any[];
|
||||||
|
reload: () => void;
|
||||||
|
}>();
|
||||||
|
if (data) {
|
||||||
|
tableName.value = data.tableName;
|
||||||
|
columns.value = data.columns;
|
||||||
|
reloadCallback.value = data.reload;
|
||||||
|
formApi.resetFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="添加字段" class="w-[600px]">
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { addIndexApi } from '../api';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'AddIndexModal',
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableName = ref('');
|
||||||
|
const columns = ref<any[]>([]);
|
||||||
|
const reloadCallback = ref<(() => void) | null>(null);
|
||||||
|
|
||||||
|
const columnOptions = computed(() => {
|
||||||
|
return columns.value.map((col) => ({
|
||||||
|
label: col.comment || col.name,
|
||||||
|
value: col.name,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 索引类型选项
|
||||||
|
const indexTypeOptions = [
|
||||||
|
{ label: 'BTREE', value: 'BTREE' },
|
||||||
|
{ label: 'HASH', value: 'HASH' },
|
||||||
|
{ label: 'FULLTEXT', value: 'FULLTEXT' },
|
||||||
|
{ label: 'SPATIAL', value: 'SPATIAL' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
wrapperClass: 'grid-cols-12',
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '索引名',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入索引名',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
fieldName: 'columns',
|
||||||
|
label: '选择字段',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
mode: 'multiple',
|
||||||
|
placeholder: '请选择字段',
|
||||||
|
options: columnOptions,
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Switch',
|
||||||
|
fieldName: 'unique',
|
||||||
|
label: '唯一索引',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '索引类型',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
options: indexTypeOptions,
|
||||||
|
placeholder: '请选择索引类型(默认BTREE)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
const valid = await formApi.validate();
|
||||||
|
if (valid.valid) {
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
if (!values.columns || values.columns.length === 0) {
|
||||||
|
message.warning('请选择字段');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
try {
|
||||||
|
await addIndexApi({
|
||||||
|
table_name: tableName.value,
|
||||||
|
index_name: values.name,
|
||||||
|
columns: values.columns,
|
||||||
|
unique: values.unique || false,
|
||||||
|
type: values.type || 'BTREE',
|
||||||
|
});
|
||||||
|
message.success('添加成功');
|
||||||
|
reloadCallback.value?.();
|
||||||
|
modalApi.close();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '添加失败');
|
||||||
|
} finally {
|
||||||
|
modalApi.setState({ loading: false, confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const data = modalApi.getData<{
|
||||||
|
tableName: string;
|
||||||
|
columns: any[];
|
||||||
|
reload: () => void;
|
||||||
|
}>();
|
||||||
|
if (data) {
|
||||||
|
tableName.value = data.tableName;
|
||||||
|
columns.value = data.columns;
|
||||||
|
reloadCallback.value = data.reload;
|
||||||
|
formApi.resetFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="添加索引" class="w-[500px]">
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
265
apps/web-antd/src/views/system/database/components/ChangeLog.vue
Normal file
265
apps/web-antd/src/views/system/database/components/ChangeLog.vue
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { h, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Button, Card, message, Table, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getChangeLogApi } from '../api';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ChangeLog',
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const changeLogList = ref<any[]>([]);
|
||||||
|
const pagination = ref({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filters = ref({
|
||||||
|
table_name: '',
|
||||||
|
operation_type: '',
|
||||||
|
start_time: '',
|
||||||
|
end_time: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 操作类型选项
|
||||||
|
const operationTypeOptions = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '结构变更', value: 'structure_change' },
|
||||||
|
{ label: '数据变更', value: 'data_change' },
|
||||||
|
{ label: 'SQL查询', value: 'sql_query' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 表列表(从筛选条件获取,实际应该从API获取)
|
||||||
|
const tableList = ref<any[]>([]);
|
||||||
|
|
||||||
|
// 加载变更记录
|
||||||
|
const loadChangeLog = async (page = 1, pageSize = 20) => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params: any = {
|
||||||
|
page,
|
||||||
|
page_size: pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filters.value.table_name) {
|
||||||
|
params.table_name = filters.value.table_name;
|
||||||
|
}
|
||||||
|
if (filters.value.operation_type) {
|
||||||
|
params.operation_type = filters.value.operation_type;
|
||||||
|
}
|
||||||
|
if (filters.value.start_time) {
|
||||||
|
params.start_time = filters.value.start_time;
|
||||||
|
}
|
||||||
|
if (filters.value.end_time) {
|
||||||
|
params.end_time = filters.value.end_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await getChangeLogApi(params);
|
||||||
|
changeLogList.value = res.items || [];
|
||||||
|
pagination.value = {
|
||||||
|
current: res.page || 1,
|
||||||
|
pageSize: res.size || 20,
|
||||||
|
total: res.total || 0,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '加载变更记录失败');
|
||||||
|
changeLogList.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 操作类型标签颜色
|
||||||
|
const getOperationTypeColor = (type: string) => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
structure_change: 'blue',
|
||||||
|
data_change: 'green',
|
||||||
|
sql_query: 'orange',
|
||||||
|
};
|
||||||
|
return colorMap[type] || 'default';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 操作类型标签文本
|
||||||
|
const getOperationTypeText = (type: string) => {
|
||||||
|
const textMap: Record<string, string> = {
|
||||||
|
structure_change: '结构变更',
|
||||||
|
data_change: '数据变更',
|
||||||
|
sql_query: 'SQL查询',
|
||||||
|
};
|
||||||
|
return textMap[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (timestamp: number) => {
|
||||||
|
if (!timestamp) return '-';
|
||||||
|
return new Date(timestamp * 1000).toLocaleString('zh-CN');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
loadChangeLog(1, pagination.value.pageSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
filters.value = {
|
||||||
|
table_name: '',
|
||||||
|
operation_type: '',
|
||||||
|
start_time: '',
|
||||||
|
end_time: '',
|
||||||
|
};
|
||||||
|
loadChangeLog(1, pagination.value.pageSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 展开详情
|
||||||
|
const expandedRowKeys = ref<number[]>([]);
|
||||||
|
|
||||||
|
const getActionText = (action: string) => {
|
||||||
|
const actionText: Record<string, string> = {
|
||||||
|
update_table_comment: '更新表注释',
|
||||||
|
update_column_comment: '更新字段注释',
|
||||||
|
add_index: '添加索引',
|
||||||
|
drop_index: '删除索引',
|
||||||
|
add_column: '添加字段',
|
||||||
|
modify_column: '修改字段',
|
||||||
|
drop_column: '删除字段',
|
||||||
|
insert: '插入数据',
|
||||||
|
update: '更新数据',
|
||||||
|
delete: '删除数据',
|
||||||
|
batch_delete: '批量删除',
|
||||||
|
select_query: 'SELECT查询',
|
||||||
|
};
|
||||||
|
return actionText[action] || action;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||||||
|
{ title: '表名', dataIndex: 'table_name', key: 'table_name', width: 150 },
|
||||||
|
{
|
||||||
|
title: '操作类型',
|
||||||
|
dataIndex: 'operation_type',
|
||||||
|
key: 'operation_type',
|
||||||
|
width: 120,
|
||||||
|
customRender: ({ text }: any) =>
|
||||||
|
h(Tag, { color: getOperationTypeColor(text) }, () => getOperationTypeText(text)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作详情',
|
||||||
|
dataIndex: 'operation_detail',
|
||||||
|
key: 'operation_detail',
|
||||||
|
customRender: ({ record }: any) => {
|
||||||
|
const detail = record.operation_detail || {};
|
||||||
|
const action = detail.action || '';
|
||||||
|
return getActionText(action);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ title: '操作用户', dataIndex: 'user_id', key: 'user_id', width: 100 },
|
||||||
|
{
|
||||||
|
title: '操作时间',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
key: 'created_at',
|
||||||
|
width: 180,
|
||||||
|
customRender: ({ text }: any) => formatTime(text),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadChangeLog();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="change-log">
|
||||||
|
<Card title="变更记录" class="mb-4">
|
||||||
|
<div class="mb-4 flex flex-wrap items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm">表名:</span>
|
||||||
|
<input
|
||||||
|
v-model="filters.table_name"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入表名"
|
||||||
|
class="w-40 rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm">操作类型:</span>
|
||||||
|
<select
|
||||||
|
v-model="filters.operation_type"
|
||||||
|
class="w-32 rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<option v-for="option in operationTypeOptions" :key="option.value" :value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm">开始时间:</span>
|
||||||
|
<input
|
||||||
|
v-model="filters.start_time"
|
||||||
|
type="date"
|
||||||
|
class="w-40 rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm">结束时间:</span>
|
||||||
|
<input
|
||||||
|
v-model="filters.end_time"
|
||||||
|
type="date"
|
||||||
|
class="w-40 rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button type="primary" @click="handleSearch">搜索</Button>
|
||||||
|
<Button @click="handleReset">重置</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="changeLogList"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="{
|
||||||
|
current: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
total: pagination.total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
}"
|
||||||
|
:expanded-row-keys="expandedRowKeys"
|
||||||
|
@change="(p: any) => loadChangeLog(p.current, p.pageSize)"
|
||||||
|
>
|
||||||
|
<template #expandedRowRender="{ record }">
|
||||||
|
<div class="p-4 space-y-4">
|
||||||
|
<div v-if="record.sql_statement">
|
||||||
|
<div class="mb-2 font-semibold">SQL语句:</div>
|
||||||
|
<pre class="rounded bg-gray-100 p-3 text-sm dark:bg-gray-800">{{ record.sql_statement }}</pre>
|
||||||
|
</div>
|
||||||
|
<div v-if="record.operation_detail">
|
||||||
|
<div class="mb-2 font-semibold">操作详情:</div>
|
||||||
|
<pre class="rounded bg-gray-100 p-3 text-sm dark:bg-gray-800">{{ JSON.stringify(record.operation_detail, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
<div v-if="record.before_data" class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 font-semibold">变更前:</div>
|
||||||
|
<pre class="max-h-64 overflow-auto rounded bg-gray-100 p-3 text-sm dark:bg-gray-800">{{ JSON.stringify(record.before_data, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 font-semibold">变更后:</div>
|
||||||
|
<pre class="max-h-64 overflow-auto rounded bg-gray-100 p-3 text-sm dark:bg-gray-800">{{ JSON.stringify(record.after_data, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.space-y-4 > * + * {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Button, Card, Space } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'DataSearch',
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
columns: any[];
|
||||||
|
modelValue: any[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: any[]];
|
||||||
|
'search': [];
|
||||||
|
'reset': [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const searchConditions = ref<any[]>([]);
|
||||||
|
|
||||||
|
// 操作符选项
|
||||||
|
const operatorOptions = [
|
||||||
|
{ label: '等于', value: '=' },
|
||||||
|
{ label: '不等于', value: '!=' },
|
||||||
|
{ label: '大于', value: '>' },
|
||||||
|
{ label: '大于等于', value: '>=' },
|
||||||
|
{ label: '小于', value: '<' },
|
||||||
|
{ label: '小于等于', value: '<=' },
|
||||||
|
{ label: '包含', value: 'like' },
|
||||||
|
{ label: '不包含', value: 'not like' },
|
||||||
|
{ label: '为空', value: 'is null' },
|
||||||
|
{ label: '不为空', value: 'is not null' },
|
||||||
|
{ label: '在范围内', value: 'in' },
|
||||||
|
{ label: '不在范围内', value: 'not in' },
|
||||||
|
{ label: '之间', value: 'between' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 字段选项
|
||||||
|
const fieldOptions = computed(() => {
|
||||||
|
return props.columns.map((col) => ({
|
||||||
|
label: `${col.name}${col.comment ? ` (${col.comment})` : ''}`,
|
||||||
|
value: col.name,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加查询条件
|
||||||
|
const addCondition = () => {
|
||||||
|
searchConditions.value.push({
|
||||||
|
field: '',
|
||||||
|
operator: '=',
|
||||||
|
value: '',
|
||||||
|
value2: '', // for BETWEEN
|
||||||
|
logic: searchConditions.value.length > 0 ? 'AND' : 'AND',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除查询条件
|
||||||
|
const removeCondition = (index: number) => {
|
||||||
|
searchConditions.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
const conditions = searchConditions.value
|
||||||
|
.filter((cond) => cond.field && cond.operator)
|
||||||
|
.map((cond) => {
|
||||||
|
const condition: any = {
|
||||||
|
field: cond.field,
|
||||||
|
operator: cond.operator,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理不需要值的操作符
|
||||||
|
if (['is null', 'is not null'].includes(cond.operator)) {
|
||||||
|
// 这些操作符不需要值
|
||||||
|
} else if (cond.operator === 'between') {
|
||||||
|
condition.value = [cond.value, cond.value2];
|
||||||
|
} else if (['in', 'not in'].includes(cond.operator)) {
|
||||||
|
// 将逗号分隔的字符串转换为数组
|
||||||
|
condition.value = cond.value
|
||||||
|
? cond.value
|
||||||
|
.split(',')
|
||||||
|
.map((v: string) => v.trim())
|
||||||
|
.filter((v: string) => v)
|
||||||
|
: [];
|
||||||
|
} else {
|
||||||
|
condition.value = cond.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cond.logic) {
|
||||||
|
condition.logic = cond.logic;
|
||||||
|
}
|
||||||
|
|
||||||
|
return condition;
|
||||||
|
});
|
||||||
|
|
||||||
|
emit('update:modelValue', conditions);
|
||||||
|
emit('search');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
searchConditions.value = [];
|
||||||
|
emit('update:modelValue', []);
|
||||||
|
emit('reset');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断操作符是否需要值
|
||||||
|
const needsValue = (operator: string) => {
|
||||||
|
return !['is null', 'is not null'].includes(operator);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断操作符是否需要两个值(BETWEEN)
|
||||||
|
const needsTwoValues = (operator: string) => {
|
||||||
|
return operator === 'between';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断操作符是否需要数组值(IN)
|
||||||
|
const needsArrayValue = (operator: string) => {
|
||||||
|
return ['in', 'not in'].includes(operator);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card title="高级搜索" class="mb-4" size="small">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div v-for="(condition, index) in searchConditions" :key="index" class="flex items-start gap-2">
|
||||||
|
<!-- 逻辑连接符 -->
|
||||||
|
<div v-if="index > 0" class="w-16 pt-1">
|
||||||
|
<select
|
||||||
|
v-model="condition.logic"
|
||||||
|
class="w-full rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<option value="AND">AND</option>
|
||||||
|
<option value="OR">OR</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 字段选择 -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<select
|
||||||
|
v-model="condition.field"
|
||||||
|
class="w-full rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<option value="">请选择字段</option>
|
||||||
|
<option v-for="field in fieldOptions" :key="field.value" :value="field.value">
|
||||||
|
{{ field.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作符选择 -->
|
||||||
|
<div class="w-32">
|
||||||
|
<select
|
||||||
|
v-model="condition.operator"
|
||||||
|
class="w-full rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<option v-for="op in operatorOptions" :key="op.value" :value="op.value">
|
||||||
|
{{ op.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 值输入 -->
|
||||||
|
<div v-if="needsValue(condition.operator)" class="flex-1">
|
||||||
|
<template v-if="needsTwoValues(condition.operator)">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input
|
||||||
|
v-model="condition.value"
|
||||||
|
type="text"
|
||||||
|
placeholder="起始值"
|
||||||
|
class="flex-1 rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
/>
|
||||||
|
<span class="py-1 text-sm">至</span>
|
||||||
|
<input
|
||||||
|
v-model="condition.value2"
|
||||||
|
type="text"
|
||||||
|
placeholder="结束值"
|
||||||
|
class="flex-1 rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="needsArrayValue(condition.operator)">
|
||||||
|
<input
|
||||||
|
v-model="condition.value"
|
||||||
|
type="text"
|
||||||
|
placeholder="多个值用逗号分隔,如: 1,2,3"
|
||||||
|
class="w-full rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<input
|
||||||
|
v-model="condition.value"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入值"
|
||||||
|
class="w-full rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<Button type="text" danger size="small" @click="removeCondition(index)">
|
||||||
|
<i class="fa-solid fa-trash"></i>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Button type="dashed" size="small" @click="addCondition">
|
||||||
|
<i class="fa-solid fa-plus mr-2"></i>添加条件
|
||||||
|
</Button>
|
||||||
|
<Space>
|
||||||
|
<Button size="small" @click="handleReset">重置</Button>
|
||||||
|
<Button type="primary" size="small" @click="handleSearch">搜索</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.space-y-3 > * + * {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { updateColumnCommentApi } from '../api';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'EditColumnCommentModal',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
wrapperClass: 'grid-cols-12',
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'columnName',
|
||||||
|
label: '字段名',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Textarea',
|
||||||
|
fieldName: 'comment',
|
||||||
|
label: '字段注释',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
rows: 4,
|
||||||
|
placeholder: '请输入字段注释',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableName = ref('');
|
||||||
|
const columnName = ref('');
|
||||||
|
const reloadCallback = ref<(() => void) | null>(null);
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
const valid = await formApi.validate();
|
||||||
|
if (valid.valid) {
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
try {
|
||||||
|
await updateColumnCommentApi({
|
||||||
|
table_name: tableName.value,
|
||||||
|
column_name: columnName.value,
|
||||||
|
comment: values.comment || '',
|
||||||
|
});
|
||||||
|
message.success('更新成功');
|
||||||
|
reloadCallback.value?.();
|
||||||
|
modalApi.close();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '更新失败');
|
||||||
|
} finally {
|
||||||
|
modalApi.setState({ loading: false, confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const data = modalApi.getData<{
|
||||||
|
tableName: string;
|
||||||
|
column: any;
|
||||||
|
reload: () => void;
|
||||||
|
}>();
|
||||||
|
if (data) {
|
||||||
|
tableName.value = data.tableName;
|
||||||
|
columnName.value = data.column.name;
|
||||||
|
reloadCallback.value = data.reload;
|
||||||
|
formApi.setValues({
|
||||||
|
columnName: data.column.name,
|
||||||
|
comment: data.column.comment || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="编辑字段注释" class="w-[500px]">
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { updateTableStructureApi } from '../api';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'EditColumnModal',
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableName = ref('');
|
||||||
|
const columns = ref<any[]>([]);
|
||||||
|
const currentColumn = ref<any>(null);
|
||||||
|
const reloadCallback = ref<(() => void) | null>(null);
|
||||||
|
|
||||||
|
// 字段类型选项
|
||||||
|
const fieldTypeOptions = [
|
||||||
|
{ label: 'VARCHAR', value: 'VARCHAR' },
|
||||||
|
{ label: 'CHAR', value: 'CHAR' },
|
||||||
|
{ label: 'TEXT', value: 'TEXT' },
|
||||||
|
{ label: 'LONGTEXT', value: 'LONGTEXT' },
|
||||||
|
{ label: 'INT', value: 'INT' },
|
||||||
|
{ label: 'BIGINT', value: 'BIGINT' },
|
||||||
|
{ label: 'TINYINT', value: 'TINYINT' },
|
||||||
|
{ label: 'SMALLINT', value: 'SMALLINT' },
|
||||||
|
{ label: 'DECIMAL', value: 'DECIMAL' },
|
||||||
|
{ label: 'FLOAT', value: 'FLOAT' },
|
||||||
|
{ label: 'DOUBLE', value: 'DOUBLE' },
|
||||||
|
{ label: 'DATETIME', value: 'DATETIME' },
|
||||||
|
{ label: 'DATE', value: 'DATE' },
|
||||||
|
{ label: 'TIMESTAMP', value: 'TIMESTAMP' },
|
||||||
|
{ label: 'TIME', value: 'TIME' },
|
||||||
|
{ label: 'YEAR', value: 'YEAR' },
|
||||||
|
{ label: 'BOOLEAN', value: 'TINYINT' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 需要长度的类型
|
||||||
|
const typesNeedLength = ['VARCHAR', 'CHAR', 'INT', 'BIGINT', 'TINYINT', 'SMALLINT', 'DECIMAL', 'FLOAT', 'DOUBLE'];
|
||||||
|
|
||||||
|
// 位置选项(AFTER字段)
|
||||||
|
const positionOptions = computed(() => {
|
||||||
|
if (!columns.value || !currentColumn.value) return [];
|
||||||
|
return columns.value
|
||||||
|
.filter((col) => col.name !== currentColumn.value.name)
|
||||||
|
.map((col) => ({
|
||||||
|
label: `${col.name}${col.comment ? ` (${col.comment})` : ''}`,
|
||||||
|
value: col.name,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 解析字段类型和长度
|
||||||
|
const parseColumnType = (columnType: string) => {
|
||||||
|
const match = columnType.match(/^(\w+)(?:\((\d+(?:,\d+)?)\))?/i);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
type: match[1].toUpperCase(),
|
||||||
|
length: match[2] || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { type: columnType.toUpperCase(), length: '' };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表单schema
|
||||||
|
const formSchema = computed(() => {
|
||||||
|
const baseSchema = [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '字段名',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Select',
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '字段类型',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
options: fieldTypeOptions,
|
||||||
|
placeholder: '请选择字段类型',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 根据类型动态添加长度字段
|
||||||
|
const lengthField = {
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'length',
|
||||||
|
label: '长度/精度',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '例如: 255 或 10,2 (DECIMAL类型)',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
show: {
|
||||||
|
triggerFields: ['type'],
|
||||||
|
condition: (values: any) => {
|
||||||
|
const type = values.type || '';
|
||||||
|
return typesNeedLength.includes(type.toUpperCase());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const afterField = {
|
||||||
|
component: 'Select',
|
||||||
|
fieldName: 'after',
|
||||||
|
label: '位置(AFTER字段)',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
options: positionOptions.value,
|
||||||
|
placeholder: '选择字段位置,留空则保持当前位置',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
...baseSchema,
|
||||||
|
lengthField,
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'default_value',
|
||||||
|
label: '默认值',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入默认值,留空表示无默认值',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Switch',
|
||||||
|
fieldName: 'nullable',
|
||||||
|
label: '允许NULL',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: 'Textarea',
|
||||||
|
fieldName: 'comment',
|
||||||
|
label: '字段注释',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
rows: 3,
|
||||||
|
placeholder: '请输入字段注释',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
afterField,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
wrapperClass: 'grid-cols-12',
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: formSchema,
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
const valid = await formApi.validate();
|
||||||
|
if (valid.valid) {
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
try {
|
||||||
|
// 构建column_info
|
||||||
|
const columnInfo: any = {
|
||||||
|
name: values.name,
|
||||||
|
type: values.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理长度
|
||||||
|
if (values.length && typesNeedLength.includes(values.type.toUpperCase())) {
|
||||||
|
columnInfo.length = values.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理默认值
|
||||||
|
if (values.default_value !== null && values.default_value !== undefined && values.default_value !== '') {
|
||||||
|
columnInfo.default_value = values.default_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理NULL
|
||||||
|
columnInfo.nullable = values.nullable || false;
|
||||||
|
|
||||||
|
// 处理注释
|
||||||
|
if (values.comment) {
|
||||||
|
columnInfo.comment = values.comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理位置
|
||||||
|
if (values.after) {
|
||||||
|
columnInfo.after = values.after;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateTableStructureApi({
|
||||||
|
table_name: tableName.value,
|
||||||
|
action: 'modify',
|
||||||
|
column_info: columnInfo,
|
||||||
|
});
|
||||||
|
message.success('更新成功');
|
||||||
|
reloadCallback.value?.();
|
||||||
|
modalApi.close();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '更新失败');
|
||||||
|
} finally {
|
||||||
|
modalApi.setState({ loading: false, confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const data = modalApi.getData<{
|
||||||
|
tableName: string;
|
||||||
|
column: any;
|
||||||
|
columns: any[];
|
||||||
|
reload: () => void;
|
||||||
|
}>();
|
||||||
|
if (data) {
|
||||||
|
tableName.value = data.tableName;
|
||||||
|
currentColumn.value = data.column;
|
||||||
|
columns.value = data.columns;
|
||||||
|
reloadCallback.value = data.reload;
|
||||||
|
|
||||||
|
// 解析字段类型和长度
|
||||||
|
const { type, length } = parseColumnType(data.column.column_type || data.column.type || 'VARCHAR');
|
||||||
|
|
||||||
|
// 设置表单值
|
||||||
|
formApi.setValues({
|
||||||
|
name: data.column.name,
|
||||||
|
type: type,
|
||||||
|
length: length || '',
|
||||||
|
default_value: data.column.default_value ?? '',
|
||||||
|
nullable: data.column.nullable || false,
|
||||||
|
comment: data.column.comment || '',
|
||||||
|
after: '', // 位置需要用户重新选择
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="编辑字段" class="w-[600px]">
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { insertTableDataApi, updateTableDataApi } from '../api';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'EditDataModal',
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableName = ref('');
|
||||||
|
const columns = ref<any[]>([]);
|
||||||
|
const isUpdate = ref(false);
|
||||||
|
const primaryKey = ref('');
|
||||||
|
const reloadCallback = ref<(() => void) | null>(null);
|
||||||
|
|
||||||
|
const formSchema = computed(() => {
|
||||||
|
return columns.value.map((col) => {
|
||||||
|
const isTextType = col.type === 'text' || col.type === 'longtext';
|
||||||
|
return {
|
||||||
|
component: isTextType ? 'Textarea' : 'Input',
|
||||||
|
fieldName: col.name,
|
||||||
|
label: col.comment || col.name,
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: `请输入${col.comment || col.name}`,
|
||||||
|
...(isTextType ? { rows: 3 } : {}),
|
||||||
|
...(col.name === primaryKey.value && isUpdate.value ? { disabled: true } : {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
wrapperClass: 'grid-cols-12',
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: formSchema,
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
const valid = await formApi.validate();
|
||||||
|
if (valid.valid) {
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
try {
|
||||||
|
if (isUpdate.value) {
|
||||||
|
// 构建WHERE条件
|
||||||
|
const where = [
|
||||||
|
{
|
||||||
|
field: primaryKey.value,
|
||||||
|
operator: '=',
|
||||||
|
value: values[primaryKey.value],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const data = { ...values };
|
||||||
|
delete data[primaryKey.value];
|
||||||
|
|
||||||
|
await updateTableDataApi({
|
||||||
|
table_name: tableName.value,
|
||||||
|
data,
|
||||||
|
where,
|
||||||
|
});
|
||||||
|
message.success('更新成功');
|
||||||
|
} else {
|
||||||
|
await insertTableDataApi({
|
||||||
|
table_name: tableName.value,
|
||||||
|
data: values,
|
||||||
|
});
|
||||||
|
message.success('添加成功');
|
||||||
|
}
|
||||||
|
reloadCallback.value?.();
|
||||||
|
modalApi.close();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '操作失败');
|
||||||
|
} finally {
|
||||||
|
modalApi.setState({ loading: false, confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const data = modalApi.getData<{
|
||||||
|
tableName: string;
|
||||||
|
columns: any[];
|
||||||
|
values?: any;
|
||||||
|
update?: boolean;
|
||||||
|
reload: () => void;
|
||||||
|
}>();
|
||||||
|
if (data) {
|
||||||
|
tableName.value = data.tableName;
|
||||||
|
columns.value = data.columns;
|
||||||
|
isUpdate.value = data.update || false;
|
||||||
|
reloadCallback.value = data.reload;
|
||||||
|
|
||||||
|
// 查找主键
|
||||||
|
const pkColumn = data.columns.find((col) => col.key_type === 'PRI');
|
||||||
|
primaryKey.value = pkColumn ? pkColumn.name : data.columns[0]?.name || 'id';
|
||||||
|
|
||||||
|
// 设置表单值
|
||||||
|
if (data.values) {
|
||||||
|
formApi.setValues(data.values);
|
||||||
|
} else {
|
||||||
|
formApi.resetFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :title="`${isUpdate ? '编辑' : '添加'}数据`" class="w-[800px]">
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { updateTableCommentApi } from '../api';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'EditTableCommentModal',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
wrapperClass: 'grid-cols-12',
|
||||||
|
commonConfig: {
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: [
|
||||||
|
{
|
||||||
|
component: 'Textarea',
|
||||||
|
fieldName: 'comment',
|
||||||
|
label: '表注释',
|
||||||
|
formItemClass: 'col-span-12',
|
||||||
|
componentProps: {
|
||||||
|
rows: 4,
|
||||||
|
placeholder: '请输入表注释',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableName = ref('');
|
||||||
|
const reloadCallback = ref<(() => void) | null>(null);
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
fullscreenButton: false,
|
||||||
|
draggable: true,
|
||||||
|
onCancel() {
|
||||||
|
modalApi.close();
|
||||||
|
},
|
||||||
|
onConfirm: async () => {
|
||||||
|
const valid = await formApi.validate();
|
||||||
|
if (valid.valid) {
|
||||||
|
const values = await formApi.getValues();
|
||||||
|
modalApi.setState({ loading: true, confirmLoading: true });
|
||||||
|
try {
|
||||||
|
await updateTableCommentApi({
|
||||||
|
table_name: tableName.value,
|
||||||
|
comment: values.comment || '',
|
||||||
|
});
|
||||||
|
message.success('更新成功');
|
||||||
|
reloadCallback.value?.();
|
||||||
|
modalApi.close();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '更新失败');
|
||||||
|
} finally {
|
||||||
|
modalApi.setState({ loading: false, confirmLoading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onOpenChange(isOpen: boolean) {
|
||||||
|
if (isOpen) {
|
||||||
|
const data = modalApi.getData<{
|
||||||
|
tableName: string;
|
||||||
|
comment: string;
|
||||||
|
reload: () => void;
|
||||||
|
}>();
|
||||||
|
if (data) {
|
||||||
|
tableName.value = data.tableName;
|
||||||
|
reloadCallback.value = data.reload;
|
||||||
|
formApi.setValues({ comment: data.comment || '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal title="编辑表注释" class="w-[500px]">
|
||||||
|
<Form />
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
273
apps/web-antd/src/views/system/database/components/SqlQuery.vue
Normal file
273
apps/web-antd/src/views/system/database/components/SqlQuery.vue
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { Button, Card, message, Select } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { executeSqlQueryApi } from '../api';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'SqlQuery',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 接收外部传递的表名和表列表
|
||||||
|
const props = defineProps<{
|
||||||
|
tableName?: string;
|
||||||
|
tableList?: any[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const sqlText = ref('');
|
||||||
|
const queryResult = ref<any[]>([]);
|
||||||
|
const queryColumns = ref<string[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const queryHistory = ref<string[]>([]);
|
||||||
|
const selectedTableSelect = ref<string | undefined>(undefined);
|
||||||
|
|
||||||
|
// 验证SQL是否为SELECT语句
|
||||||
|
const validateSql = (sql: string): boolean => {
|
||||||
|
const trimmedSql = sql.trim().toUpperCase();
|
||||||
|
|
||||||
|
// 检查是否以SELECT开头
|
||||||
|
if (!trimmedSql.startsWith('SELECT')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁止的危险关键字
|
||||||
|
const dangerousKeywords = [
|
||||||
|
'DROP', 'DELETE', 'UPDATE', 'INSERT', 'ALTER', 'CREATE', 'TRUNCATE',
|
||||||
|
'REPLACE', 'GRANT', 'REVOKE', 'EXEC', 'EXECUTE', 'CALL', 'PROCEDURE',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const keyword of dangerousKeywords) {
|
||||||
|
if (trimmedSql.includes(keyword)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行SQL查询
|
||||||
|
const handleExecute = async () => {
|
||||||
|
if (!sqlText.value.trim()) {
|
||||||
|
message.warning('请输入SQL语句');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateSql(sqlText.value)) {
|
||||||
|
message.error('仅支持SELECT查询语句,且不能包含危险关键字');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await executeSqlQueryApi({
|
||||||
|
sql: sqlText.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 直接判断 res 是否为数组
|
||||||
|
if (Array.isArray(res)) {
|
||||||
|
queryResult.value = res;
|
||||||
|
|
||||||
|
if (res.length > 0) {
|
||||||
|
// 动态获取列名
|
||||||
|
const firstRow = res[0];
|
||||||
|
queryColumns.value = Object.keys(firstRow);
|
||||||
|
|
||||||
|
message.success(`查询成功,返回 ${res.length} 条记录`);
|
||||||
|
} else {
|
||||||
|
queryColumns.value = [];
|
||||||
|
message.info('查询成功,但未返回数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到历史记录
|
||||||
|
if (!queryHistory.value.includes(sqlText.value)) {
|
||||||
|
queryHistory.value.unshift(sqlText.value);
|
||||||
|
if (queryHistory.value.length > 10) {
|
||||||
|
queryHistory.value = queryHistory.value.slice(0, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('数据格式错误,期望数组,实际返回:', res);
|
||||||
|
queryResult.value = [];
|
||||||
|
queryColumns.value = [];
|
||||||
|
message.warning('查询返回了非数组格式的数据');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('SQL执行错误:', error);
|
||||||
|
message.error(error.message || '查询失败');
|
||||||
|
queryResult.value = [];
|
||||||
|
queryColumns.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 tableName 变化,设置默认值(从详情页进入时)
|
||||||
|
watch(
|
||||||
|
() => props.tableName,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
selectedTableSelect.value = newVal;
|
||||||
|
// 自动生成查询语句
|
||||||
|
sqlText.value = `SELECT * FROM ${newVal} LIMIT 20;`;
|
||||||
|
// 自动执行查询
|
||||||
|
handleExecute();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 处理下拉框切换表名
|
||||||
|
const handleTableSelectChange = (value: string) => {
|
||||||
|
selectedTableSelect.value = value;
|
||||||
|
if (value) {
|
||||||
|
sqlText.value = `SELECT * FROM ${value} LIMIT 20;`;
|
||||||
|
handleExecute();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清空SQL
|
||||||
|
const handleClear = () => {
|
||||||
|
sqlText.value = '';
|
||||||
|
queryResult.value = [];
|
||||||
|
queryColumns.value = [];
|
||||||
|
selectedTableSelect.value = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择历史记录
|
||||||
|
const handleSelectHistory = (sql: string) => {
|
||||||
|
sqlText.value = sql;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sql-query h-full flex flex-col">
|
||||||
|
<Card title="SQL查询" class="mb-4 flex-shrink-0">
|
||||||
|
<div class="mb-4 space-y-3">
|
||||||
|
<!-- 顶部工具栏:选择表 -->
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="flex items-center gap-2 flex-1">
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">快捷查询表:</span>
|
||||||
|
<Select
|
||||||
|
v-model:value="selectedTableSelect"
|
||||||
|
show-search
|
||||||
|
placeholder="选择数据表自动生成查询..."
|
||||||
|
class="w-64"
|
||||||
|
:options="props.tableList?.map(t => ({ label: t.name + (t.comment ? ` (${t.comment})` : ''), value: t.name }))"
|
||||||
|
@change="handleTableSelectChange"
|
||||||
|
:filter-option="(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="queryHistory.length > 0" class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-500">历史记录:</span>
|
||||||
|
<select
|
||||||
|
class="rounded border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-800 outline-none focus:border-primary max-w-[200px]"
|
||||||
|
@change="(e: any) => handleSelectHistory(e.target.value)"
|
||||||
|
>
|
||||||
|
<option value="">选择历史记录</option>
|
||||||
|
<option v-for="(sql, index) in queryHistory" :key="index" :value="sql">
|
||||||
|
{{ sql.substring(0, 30) }}{{ sql.length > 30 ? '...' : '' }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
v-model="sqlText"
|
||||||
|
class="w-full rounded border border-gray-300 p-3 font-mono text-sm dark:border-gray-600 dark:bg-gray-800 focus:ring-2 focus:ring-primary/50 focus:border-primary outline-none transition-all"
|
||||||
|
rows="5"
|
||||||
|
placeholder="请输入SELECT查询语句,例如:SELECT * FROM nl_admin LIMIT 10;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button type="primary" :loading="loading" @click="handleExecute">
|
||||||
|
<i class="fa-solid fa-play mr-2"></i>执行查询
|
||||||
|
</Button>
|
||||||
|
<Button @click="handleClear">
|
||||||
|
<i class="fa-solid fa-eraser mr-2"></i>清空
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 自定义表格区域 -->
|
||||||
|
<Card
|
||||||
|
v-if="queryResult.length > 0 || loading"
|
||||||
|
title="查询结果"
|
||||||
|
class="flex-1 overflow-hidden flex flex-col"
|
||||||
|
:body-style="{ padding: 0, flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }"
|
||||||
|
>
|
||||||
|
<div class="flex-1 overflow-auto">
|
||||||
|
<table class="w-full border-collapse text-left text-sm whitespace-nowrap">
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-800 sticky top-0 z-10 shadow-sm">
|
||||||
|
<tr>
|
||||||
|
<!-- 序号列 -->
|
||||||
|
<th class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-200 border-b border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 w-16 text-center">
|
||||||
|
#
|
||||||
|
</th>
|
||||||
|
<!-- 动态列头 -->
|
||||||
|
<th
|
||||||
|
v-for="col in queryColumns"
|
||||||
|
:key="col"
|
||||||
|
class="px-4 py-3 font-semibold text-gray-700 dark:text-gray-200 border-b border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
{{ col }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-900">
|
||||||
|
<tr
|
||||||
|
v-for="(row, rowIndex) in queryResult"
|
||||||
|
:key="rowIndex"
|
||||||
|
class="hover:bg-blue-50/50 dark:hover:bg-blue-900/10 transition-colors"
|
||||||
|
>
|
||||||
|
<!-- 序号 -->
|
||||||
|
<td class="px-4 py-3 text-gray-500 border-r border-gray-100 dark:border-gray-800 text-center font-mono text-xs bg-gray-50/30">
|
||||||
|
{{ rowIndex + 1 }}
|
||||||
|
</td>
|
||||||
|
<!-- 数据单元格 -->
|
||||||
|
<td
|
||||||
|
v-for="col in queryColumns"
|
||||||
|
:key="col"
|
||||||
|
class="px-4 py-3 text-gray-600 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
<div class="max-w-xs truncate" :title="String(row[col])">
|
||||||
|
{{ row[col] !== null ? row[col] : '(NULL)' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- 底部统计栏 -->
|
||||||
|
<div class="border-t border-gray-200 dark:border-gray-700 p-2 bg-gray-50 dark:bg-gray-800 text-xs text-gray-500 text-right">
|
||||||
|
共显示 {{ queryResult.length }} 条记录
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 滚动条美化 */
|
||||||
|
.overflow-auto {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
|
||||||
|
}
|
||||||
|
.overflow-auto::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
.overflow-auto::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.overflow-auto::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(156, 163, 175, 0.5);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.overflow-auto::-webkit-scrollbar-corner {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
179
apps/web-antd/src/views/system/database/components/TableData.vue
Normal file
179
apps/web-antd/src/views/system/database/components/TableData.vue
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, h, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Button, Popconfirm, Table } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import DataSearch from './DataSearch.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tableInfo: any;
|
||||||
|
tableData: any[];
|
||||||
|
loading: boolean;
|
||||||
|
pagination: {
|
||||||
|
current: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-data': [];
|
||||||
|
'edit-data': [record: any];
|
||||||
|
'delete-data': [record: any];
|
||||||
|
'batch-delete': [records: any[]];
|
||||||
|
'page-change': [page: number, pageSize: number];
|
||||||
|
'search': [conditions: any[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedRowKeys = ref<any[]>([]);
|
||||||
|
const searchConditions = ref<any[]>([]);
|
||||||
|
|
||||||
|
const tableColumns = computed(() => {
|
||||||
|
if (!props.tableInfo?.columns) return [];
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
key: 'selection',
|
||||||
|
width: 50,
|
||||||
|
fixed: 'left',
|
||||||
|
customRender: ({ record }: any) => {
|
||||||
|
const key = getRowKey(record);
|
||||||
|
return h('input', {
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: selectedRowKeys.value.includes(key),
|
||||||
|
onChange: (e: any) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
selectedRowKeys.value.push(key);
|
||||||
|
} else {
|
||||||
|
const index = selectedRowKeys.value.indexOf(key);
|
||||||
|
if (index > -1) {
|
||||||
|
selectedRowKeys.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...props.tableInfo.columns.map((col: any) => ({
|
||||||
|
title: col.comment || col.name,
|
||||||
|
dataIndex: col.name,
|
||||||
|
key: col.name,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 150,
|
||||||
|
fixed: 'right',
|
||||||
|
customRender: ({ record }: any) =>
|
||||||
|
h('div', { class: 'flex gap-2' }, [
|
||||||
|
h(Button, {
|
||||||
|
type: 'link',
|
||||||
|
size: 'small',
|
||||||
|
onClick: () => emit('edit-data', record),
|
||||||
|
}, () => '编辑'),
|
||||||
|
h(Popconfirm, {
|
||||||
|
title: '确定删除这条数据吗?',
|
||||||
|
onConfirm: () => emit('delete-data', record),
|
||||||
|
}, {
|
||||||
|
default: () =>
|
||||||
|
h(Button, {
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
size: 'small',
|
||||||
|
}, () => '删除'),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取行的唯一标识
|
||||||
|
const getRowKey = (record: any) => {
|
||||||
|
// 尝试使用id字段,如果没有则使用第一个字段
|
||||||
|
if (record.id !== undefined) {
|
||||||
|
return record.id;
|
||||||
|
}
|
||||||
|
const firstKey = Object.keys(record)[0];
|
||||||
|
return record[firstKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全选/取消全选
|
||||||
|
const handleSelectAll = (checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
selectedRowKeys.value = props.tableData.map((record) => getRowKey(record));
|
||||||
|
} else {
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
const handleBatchDelete = () => {
|
||||||
|
if (selectedRowKeys.value.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectedRecords = props.tableData.filter((record) =>
|
||||||
|
selectedRowKeys.value.includes(getRowKey(record)),
|
||||||
|
);
|
||||||
|
emit('batch-delete', selectedRecords);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
emit('search', searchConditions.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const handleResetSearch = () => {
|
||||||
|
searchConditions.value = [];
|
||||||
|
emit('search', []);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 高级搜索 -->
|
||||||
|
<DataSearch
|
||||||
|
:columns="tableInfo?.columns || []"
|
||||||
|
v-model="searchConditions"
|
||||||
|
@search="handleSearch"
|
||||||
|
@reset="handleResetSearch"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 操作栏 -->
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button type="primary" @click="emit('add-data')">
|
||||||
|
<i class="fa-solid fa-plus mr-2"></i>添加数据
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
v-if="selectedRowKeys.length > 0"
|
||||||
|
:title="`确定删除选中的 ${selectedRowKeys.length} 条数据吗?`"
|
||||||
|
@confirm="handleBatchDelete"
|
||||||
|
>
|
||||||
|
<Button type="primary" danger>
|
||||||
|
<i class="fa-solid fa-trash mr-2"></i>批量删除 ({{ selectedRowKeys.length }})
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedRowKeys.length > 0" class="text-sm text-gray-500">
|
||||||
|
已选择 {{ selectedRowKeys.length }} 条
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<Table
|
||||||
|
:columns="tableColumns"
|
||||||
|
:data-source="tableData"
|
||||||
|
:loading="loading"
|
||||||
|
:row-key="getRowKey"
|
||||||
|
:pagination="{
|
||||||
|
current: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
total: pagination.total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
}"
|
||||||
|
@change="(p: any) => emit('page-change', p.current, p.pageSize)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { Button, Popconfirm, Table, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tableInfo: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'add-index': [];
|
||||||
|
'delete-index': [indexName: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: '索引名', dataIndex: 'name', key: 'name' },
|
||||||
|
{
|
||||||
|
title: '字段',
|
||||||
|
dataIndex: 'columns',
|
||||||
|
key: 'columns',
|
||||||
|
customRender: ({ text }: any) => text.join(', '),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '唯一',
|
||||||
|
dataIndex: 'unique',
|
||||||
|
key: 'unique',
|
||||||
|
customRender: ({ text }: any) =>
|
||||||
|
h(Tag, { color: text ? 'green' : 'default' }, () => text ? '是' : '否'),
|
||||||
|
},
|
||||||
|
{ title: '类型', dataIndex: 'type', key: 'type' },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 120,
|
||||||
|
customRender: ({ record }: any) =>
|
||||||
|
h(Popconfirm, {
|
||||||
|
title: '确定删除此索引吗?',
|
||||||
|
onConfirm: () => emit('delete-index', record.name),
|
||||||
|
}, {
|
||||||
|
default: () =>
|
||||||
|
h(Button, {
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
size: 'small',
|
||||||
|
}, () => '删除'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<Button type="primary" @click="emit('add-index')">
|
||||||
|
<i class="fa-solid fa-plus mr-2"></i>添加索引
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="tableInfo?.indexes || []"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
190
apps/web-antd/src/views/system/database/components/TableList.vue
Normal file
190
apps/web-antd/src/views/system/database/components/TableList.vue
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Input, Empty, Spin, Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tableList: any[];
|
||||||
|
loading: boolean;
|
||||||
|
selectedTable: string;
|
||||||
|
searchKeyword: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:searchKeyword': [value: string];
|
||||||
|
'select-table': [tableName: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 根据表名生成一个固定的装饰色,让界面不那么单调
|
||||||
|
const getIconColor = (name: string) => {
|
||||||
|
const colors = ['text-blue-500', 'text-green-500', 'text-purple-500', 'text-orange-500', 'text-pink-500', 'text-cyan-500'];
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < name.length; i++) {
|
||||||
|
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
return colors[Math.abs(hash) % colors.length];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatSize = (bytes: number) => {
|
||||||
|
if (!bytes && bytes !== 0) return '-';
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化日期,只显示年月日
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
return dateStr.split(' ')[0];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex flex-col bg-white dark:bg-gray-900 rounded-lg shadow-sm">
|
||||||
|
<!-- 头部区域:标题与搜索 -->
|
||||||
|
<div class="p-5 border-b border-gray-100 dark:border-gray-800 flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||||
|
<div class="flex items-center gap-3 self-start sm:self-center">
|
||||||
|
<div class="w-10 h-10 bg-primary/10 rounded-xl flex items-center justify-center">
|
||||||
|
<!-- 替换原有 fa-database 图标 -->
|
||||||
|
<span class="icon-[material-symbols--database] text-primary text-xl"></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-bold m-0 leading-tight text-gray-800 dark:text-gray-100">数据库表管理</h2>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 m-0 mt-1">共 {{ tableList.length }} 张数据表</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full sm:w-80">
|
||||||
|
<Input
|
||||||
|
:value="searchKeyword"
|
||||||
|
placeholder="搜索表名、注释..."
|
||||||
|
allow-clear
|
||||||
|
size="large"
|
||||||
|
class="!rounded-lg"
|
||||||
|
@update:value="(val) => emit('update:searchKeyword', val)"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<i class="fa-solid fa-search text-gray-400 mr-1"></i>
|
||||||
|
</template>
|
||||||
|
</Input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<div class="flex-1 overflow-hidden relative bg-gray-50/50 dark:bg-gray-900">
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<div v-if="loading" class="absolute inset-0 z-10 flex justify-center items-center bg-white/60 dark:bg-gray-900/60 backdrop-blur-sm">
|
||||||
|
<div class="text-center">
|
||||||
|
<Spin size="large" />
|
||||||
|
<p class="mt-4 text-gray-500">正在加载数据表...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else-if="tableList.length === 0" class="h-full flex flex-col justify-center items-center text-gray-400">
|
||||||
|
<Empty :image="Empty.PRESENTED_IMAGE_SIMPLE" description="未找到匹配的数据表" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片网格列表 -->
|
||||||
|
<div v-else class="h-full overflow-y-auto p-4 sm:p-6">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-5">
|
||||||
|
<div
|
||||||
|
v-for="table in tableList"
|
||||||
|
:key="table.name"
|
||||||
|
class="group relative flex flex-col bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 cursor-pointer transition-all duration-300 hover:shadow-lg hover:-translate-y-1 hover:border-primary/30"
|
||||||
|
@click="emit('select-table', table.name)"
|
||||||
|
>
|
||||||
|
<!-- 卡片顶部:图标与名称 -->
|
||||||
|
<div class="flex items-start justify-between mb-3">
|
||||||
|
<div class="flex items-center gap-3 overflow-hidden flex-1">
|
||||||
|
<div class="w-10 h-10 rounded-lg bg-gray-50 dark:bg-gray-700 flex items-center justify-center flex-shrink-0 group-hover:bg-primary/5 transition-colors">
|
||||||
|
<span class="icon-[material-symbols--database] text-primary text-xl"></span>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden flex-1">
|
||||||
|
<h3 class="font-bold text-gray-800 dark:text-gray-100 truncate text-base mb-0.5" :title="table.name">
|
||||||
|
{{ table.name }}
|
||||||
|
</h3>
|
||||||
|
<!-- 标签行:显示引擎和字符集 -->
|
||||||
|
<div class="flex items-center gap-2 overflow-hidden">
|
||||||
|
<Tooltip :title="`存储引擎: ${table.engine}`">
|
||||||
|
<span class="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-500 font-mono whitespace-nowrap">
|
||||||
|
{{ table.engine || 'InnoDB' }}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip v-if="table.collation" :title="`字符集: ${table.collation}`">
|
||||||
|
<span class="text-[10px] px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-500 font-mono truncate max-w-[80px]">
|
||||||
|
{{ table.collation.split('_')[0] }}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片中间:注释 -->
|
||||||
|
<div class="flex-1 mb-4 min-h-[2.5rem]">
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 line-clamp-2 m-0 leading-relaxed" :title="table.comment">
|
||||||
|
{{ table.comment || '暂无表注释信息...' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片底部:统计与操作 -->
|
||||||
|
<div class="pt-3 border-t border-gray-100 dark:border-gray-700 flex items-center justify-between text-xs text-gray-400">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<!-- 行数 -->
|
||||||
|
<Tooltip title="数据行数">
|
||||||
|
<div class="flex items-center gap-1.5 hover:text-gray-600 dark:hover:text-gray-300">
|
||||||
|
<i class="fa-solid fa-list-ol"></i>
|
||||||
|
<span>{{ (table.rows || 0).toLocaleString() }}</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<!-- 大小 (数据+索引) -->
|
||||||
|
<Tooltip title="数据+索引大小">
|
||||||
|
<div class="flex items-center gap-1.5 hover:text-gray-600 dark:hover:text-gray-300">
|
||||||
|
<i class="fa-solid fa-hard-drive"></i>
|
||||||
|
<span>{{ formatSize((table.data_length || 0) + (table.index_length || 0)) }}</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右下角:默认显示时间,Hover显示管理按钮 -->
|
||||||
|
<div class="relative">
|
||||||
|
<Tooltip :title="`创建时间: ${table.create_time || '-'}`">
|
||||||
|
<div class="group-hover:opacity-0 transition-opacity duration-300 flex items-center gap-1">
|
||||||
|
<i class="fa-regular fa-clock"></i>
|
||||||
|
<span>{{ formatDate(table.create_time) }}</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div class="absolute right-0 top-0 flex items-center gap-1 text-primary opacity-0 group-hover:opacity-100 transform translate-x-2 group-hover:translate-x-0 transition-all duration-300 font-medium whitespace-nowrap bg-white dark:bg-gray-800 pl-2">
|
||||||
|
<span>管理</span>
|
||||||
|
<i class="fa-solid fa-arrow-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 优化滚动条样式 */
|
||||||
|
.overflow-y-auto {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(0, 0, 0, 0.1) transparent;
|
||||||
|
}
|
||||||
|
.overflow-y-auto::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.overflow-y-auto::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.overflow-y-auto::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
import { Button, Popconfirm, Table } from 'ant-design-vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
tableInfo: any;
|
||||||
|
loading: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'edit-table-comment': [];
|
||||||
|
'edit-column-comment': [column: any];
|
||||||
|
'edit-column': [column: any];
|
||||||
|
'add-column': [];
|
||||||
|
'delete-column': [column: any];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ title: '字段名', dataIndex: 'name', key: 'name' },
|
||||||
|
{ title: '类型', dataIndex: 'column_type', key: 'column_type' },
|
||||||
|
{
|
||||||
|
title: '允许NULL',
|
||||||
|
dataIndex: 'nullable',
|
||||||
|
key: 'nullable',
|
||||||
|
customRender: ({ text }: any) => (text ? '是' : '否'),
|
||||||
|
},
|
||||||
|
{ title: '默认值', dataIndex: 'default_value', key: 'default_value' },
|
||||||
|
{ title: '键', dataIndex: 'key_type', key: 'key_type' },
|
||||||
|
{ title: '注释', dataIndex: 'comment', key: 'comment' },
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 200,
|
||||||
|
fixed: 'right',
|
||||||
|
customRender: ({ record }: any) =>
|
||||||
|
h('div', { class: 'flex gap-2' }, [
|
||||||
|
h(Button, {
|
||||||
|
type: 'link',
|
||||||
|
size: 'small',
|
||||||
|
onClick: () => emit('edit-column', record),
|
||||||
|
}, () => '编辑'),
|
||||||
|
h(Button, {
|
||||||
|
type: 'link',
|
||||||
|
size: 'small',
|
||||||
|
onClick: () => emit('edit-column-comment', record),
|
||||||
|
}, () => '注释'),
|
||||||
|
h(Popconfirm, {
|
||||||
|
title: '确定删除此字段吗?此操作不可恢复!',
|
||||||
|
onConfirm: () => emit('delete-column', record),
|
||||||
|
}, {
|
||||||
|
default: () =>
|
||||||
|
h(Button, {
|
||||||
|
type: 'link',
|
||||||
|
danger: true,
|
||||||
|
size: 'small',
|
||||||
|
}, () => '删除'),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="loading" class="flex h-64 items-center justify-center">
|
||||||
|
<i class="fa-solid fa-spinner fa-spin text-2xl text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="tableInfo">
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||||
|
表注释:
|
||||||
|
<span class="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{{ tableInfo.comment || '无' }}
|
||||||
|
</span>
|
||||||
|
<Button type="link" size="small" @click="emit('edit-table-comment')">
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500">
|
||||||
|
引擎: {{ tableInfo.engine }} | 字符集: {{ tableInfo.collation }} | 记录数: {{ tableInfo.rows }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button type="primary" @click="emit('add-column')">
|
||||||
|
<i class="fa-solid fa-plus mr-2"></i>添加字段
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="tableInfo.columns"
|
||||||
|
:pagination="false"
|
||||||
|
size="small"
|
||||||
|
:scroll="{ x: 1000 }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
455
apps/web-antd/src/views/system/database/index.vue
Normal file
455
apps/web-antd/src/views/system/database/index.vue
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Button, Card, message, Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
batchDeleteTableDataApi,
|
||||||
|
deleteTableDataApi,
|
||||||
|
dropIndexApi,
|
||||||
|
getTableDataApi,
|
||||||
|
getTableInfoApi,
|
||||||
|
getTableListApi,
|
||||||
|
updateTableStructureApi,
|
||||||
|
} from './api';
|
||||||
|
import AddColumnModal from './components/AddColumnModal.vue';
|
||||||
|
import AddIndexModal from './components/AddIndexModal.vue';
|
||||||
|
import EditColumnCommentModal from './components/EditColumnCommentModal.vue';
|
||||||
|
import EditColumnModal from './components/EditColumnModal.vue';
|
||||||
|
import EditDataModal from './components/EditDataModal.vue';
|
||||||
|
import EditTableCommentModal from './components/EditTableCommentModal.vue';
|
||||||
|
import ChangeLog from './components/ChangeLog.vue';
|
||||||
|
import SqlQuery from './components/SqlQuery.vue';
|
||||||
|
import TableData from './components/TableData.vue';
|
||||||
|
import TableIndex from './components/TableIndex.vue';
|
||||||
|
import TableList from './components/TableList.vue';
|
||||||
|
import TableStructure from './components/TableStructure.vue';
|
||||||
|
|
||||||
|
// 表列表
|
||||||
|
const tableList = ref<any[]>([]);
|
||||||
|
const tableLoading = ref(false);
|
||||||
|
const searchKeyword = ref('');
|
||||||
|
const selectedTable = ref<string>('');
|
||||||
|
|
||||||
|
// 主页标签页 (列表/SQL)
|
||||||
|
const activeMainTab = ref('list');
|
||||||
|
|
||||||
|
// 表信息
|
||||||
|
const tableInfo = ref<any>(null);
|
||||||
|
const tableInfoLoading = ref(false);
|
||||||
|
|
||||||
|
// 表数据
|
||||||
|
const tableData = ref<any[]>([]);
|
||||||
|
const tableDataLoading = ref(false);
|
||||||
|
const tableDataPagination = ref({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当前标签页
|
||||||
|
const activeTab = ref('structure');
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
const [EditTableCommentModalComponent, editTableCommentModalApi] = useVbenModal({
|
||||||
|
connectedComponent: EditTableCommentModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [EditColumnCommentModalComponent, editColumnCommentModalApi] = useVbenModal({
|
||||||
|
connectedComponent: EditColumnCommentModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [EditColumnModalComponent, editColumnModalApi] = useVbenModal({
|
||||||
|
connectedComponent: EditColumnModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [AddColumnModalComponent, addColumnModalApi] = useVbenModal({
|
||||||
|
connectedComponent: AddColumnModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [EditDataModalComponent, editDataModalApi] = useVbenModal({
|
||||||
|
connectedComponent: EditDataModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [AddIndexModalComponent, addIndexModalApi] = useVbenModal({
|
||||||
|
connectedComponent: AddIndexModal,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 过滤后的表列表
|
||||||
|
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 getTableListApi();
|
||||||
|
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 = '';
|
||||||
|
tableInfo.value = null;
|
||||||
|
tableData.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedTable.value = tableName;
|
||||||
|
activeTab.value = 'structure';
|
||||||
|
await loadTableInfo();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回列表
|
||||||
|
const handleBack = () => {
|
||||||
|
selectedTable.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载表信息
|
||||||
|
const loadTableInfo = async () => {
|
||||||
|
if (!selectedTable.value) return;
|
||||||
|
|
||||||
|
tableInfoLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getTableInfoApi(selectedTable.value);
|
||||||
|
tableInfo.value = res;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '加载表信息失败');
|
||||||
|
tableInfo.value = null;
|
||||||
|
} finally {
|
||||||
|
tableInfoLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索条件
|
||||||
|
const searchConditions = ref<any[]>([]);
|
||||||
|
|
||||||
|
// 加载表数据
|
||||||
|
const loadTableData = async (page = 1, pageSize = 20, where: any[] = []) => {
|
||||||
|
if (!selectedTable.value) return;
|
||||||
|
|
||||||
|
tableDataLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getTableDataApi({
|
||||||
|
table_name: selectedTable.value,
|
||||||
|
page,
|
||||||
|
page_size: pageSize,
|
||||||
|
where: where.length > 0 ? where : searchConditions.value,
|
||||||
|
});
|
||||||
|
tableData.value = res.items || [];
|
||||||
|
tableDataPagination.value = {
|
||||||
|
current: res.page || 1,
|
||||||
|
pageSize: res.size || 20,
|
||||||
|
total: res.total || 0,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '加载表数据失败');
|
||||||
|
tableData.value = [];
|
||||||
|
} finally {
|
||||||
|
tableDataLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
const handleSearch = (conditions: any[]) => {
|
||||||
|
searchConditions.value = conditions;
|
||||||
|
loadTableData(1, tableDataPagination.value.pageSize, conditions);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换标签页
|
||||||
|
const handleTabChange = (key: string) => {
|
||||||
|
if (key === 'data' && tableData.value.length === 0) {
|
||||||
|
loadTableData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑表注释
|
||||||
|
const handleEditTableComment = () => {
|
||||||
|
if (!tableInfo.value) return;
|
||||||
|
editTableCommentModalApi.setData({
|
||||||
|
tableName: selectedTable.value,
|
||||||
|
comment: tableInfo.value.comment || '',
|
||||||
|
reload: loadTableInfo,
|
||||||
|
}).open();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑字段
|
||||||
|
const handleEditColumn = (column: any) => {
|
||||||
|
editColumnModalApi.setData({
|
||||||
|
tableName: selectedTable.value,
|
||||||
|
column,
|
||||||
|
columns: tableInfo.value?.columns || [],
|
||||||
|
reload: loadTableInfo,
|
||||||
|
}).open();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑字段注释
|
||||||
|
const handleEditColumnComment = (column: any) => {
|
||||||
|
editColumnCommentModalApi.setData({
|
||||||
|
tableName: selectedTable.value,
|
||||||
|
column,
|
||||||
|
reload: loadTableInfo,
|
||||||
|
}).open();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加字段
|
||||||
|
const handleAddColumn = () => {
|
||||||
|
addColumnModalApi.setData({
|
||||||
|
tableName: selectedTable.value,
|
||||||
|
columns: tableInfo.value?.columns || [],
|
||||||
|
reload: loadTableInfo,
|
||||||
|
}).open();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除字段
|
||||||
|
const handleDeleteColumn = async (column: any) => {
|
||||||
|
try {
|
||||||
|
await updateTableStructureApi({
|
||||||
|
table_name: selectedTable.value,
|
||||||
|
action: 'drop',
|
||||||
|
column_info: {
|
||||||
|
name: column.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
message.success('删除成功');
|
||||||
|
await loadTableInfo();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加数据
|
||||||
|
const handleAddData = () => {
|
||||||
|
editDataModalApi.setData({
|
||||||
|
tableName: selectedTable.value,
|
||||||
|
columns: tableInfo.value?.columns || [],
|
||||||
|
values: {},
|
||||||
|
update: false,
|
||||||
|
reload: () => loadTableData(tableDataPagination.value.current, tableDataPagination.value.pageSize),
|
||||||
|
}).open();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑数据
|
||||||
|
const handleEditData = (record: any) => {
|
||||||
|
editDataModalApi.setData({
|
||||||
|
tableName: selectedTable.value,
|
||||||
|
columns: tableInfo.value?.columns || [],
|
||||||
|
values: record,
|
||||||
|
update: true,
|
||||||
|
reload: () => loadTableData(tableDataPagination.value.current, tableDataPagination.value.pageSize),
|
||||||
|
}).open();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除数据
|
||||||
|
const handleDeleteData = async (record: any) => {
|
||||||
|
try {
|
||||||
|
const firstKey = Object.keys(record)[0];
|
||||||
|
await deleteTableDataApi({
|
||||||
|
table_name: selectedTable.value,
|
||||||
|
where: [
|
||||||
|
{
|
||||||
|
field: firstKey,
|
||||||
|
operator: '=',
|
||||||
|
value: record[firstKey],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
message.success('删除成功');
|
||||||
|
await loadTableData(tableDataPagination.value.current, tableDataPagination.value.pageSize);
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量删除数据
|
||||||
|
const handleBatchDeleteData = async (records: any[]) => {
|
||||||
|
try {
|
||||||
|
// 构建批量删除的WHERE条件
|
||||||
|
const whereConditions = records.map((record) => {
|
||||||
|
const firstKey = Object.keys(record)[0];
|
||||||
|
return {
|
||||||
|
field: firstKey,
|
||||||
|
operator: '=',
|
||||||
|
value: record[firstKey],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用OR连接多个条件
|
||||||
|
await batchDeleteTableDataApi({
|
||||||
|
table_name: selectedTable.value,
|
||||||
|
where: whereConditions,
|
||||||
|
});
|
||||||
|
message.success(`成功删除 ${records.length} 条数据`);
|
||||||
|
await loadTableData(tableDataPagination.value.current, tableDataPagination.value.pageSize);
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '批量删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加索引
|
||||||
|
const handleAddIndex = () => {
|
||||||
|
addIndexModalApi.setData({
|
||||||
|
tableName: selectedTable.value,
|
||||||
|
columns: tableInfo.value?.columns || [],
|
||||||
|
reload: loadTableInfo,
|
||||||
|
}).open();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除索引
|
||||||
|
const handleDeleteIndex = async (indexName: string) => {
|
||||||
|
try {
|
||||||
|
await dropIndexApi({
|
||||||
|
table_name: selectedTable.value,
|
||||||
|
index_name: indexName,
|
||||||
|
});
|
||||||
|
message.success('删除成功');
|
||||||
|
await loadTableInfo();
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error.message || '删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadTableList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height title="数据库管理">
|
||||||
|
<div class="database-management">
|
||||||
|
|
||||||
|
<!-- 列表页 / 主页 -->
|
||||||
|
<div v-show="!selectedTable" class="h-full bg-white dark:bg-gray-900 rounded-lg p-2">
|
||||||
|
<!-- 增加 Tabs 切换:数据表列表 / 全局 SQL 查询 -->
|
||||||
|
<Tabs v-model:activeKey="activeMainTab" class="h-full flex flex-col [&>.ant-tabs-content]:flex-1 [&>.ant-tabs-content]:overflow-auto">
|
||||||
|
<Tabs.TabPane key="list" tab="数据表列表">
|
||||||
|
<TableList
|
||||||
|
:table-list="filteredTableList"
|
||||||
|
:loading="tableLoading"
|
||||||
|
:selected-table="selectedTable"
|
||||||
|
:search-keyword="searchKeyword"
|
||||||
|
@update:search-keyword="searchKeyword = $event"
|
||||||
|
@select-table="handleSelectTable"
|
||||||
|
/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane key="sql" tab="全局 SQL 查询">
|
||||||
|
<!-- 主页面的 SQL 查询组件,传入表列表 -->
|
||||||
|
<div class="h-full p-2">
|
||||||
|
<SqlQuery :table-list="tableList" />
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 详情页:选择表后显示 -->
|
||||||
|
<div v-if="selectedTable" class="h-full">
|
||||||
|
<Card class="h-full flex flex-col" :body-style="{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button type="link" size="small" @click="handleBack" class="!px-0 !mr-2 text-gray-600 hover:text-primary">
|
||||||
|
<i class="fa-solid fa-arrow-left mr-1"></i>
|
||||||
|
返回
|
||||||
|
</Button>
|
||||||
|
<span class="font-bold text-lg">{{ selectedTable }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-hidden flex flex-col">
|
||||||
|
<Tabs v-model:activeKey="activeTab" @change="handleTabChange" class="h-full flex flex-col [&>.ant-tabs-content]:flex-1 [&>.ant-tabs-content]:overflow-auto">
|
||||||
|
<!-- 表结构 -->
|
||||||
|
<Tabs.TabPane key="structure" tab="表结构">
|
||||||
|
<TableStructure
|
||||||
|
:table-info="tableInfo"
|
||||||
|
:loading="tableInfoLoading"
|
||||||
|
@edit-table-comment="handleEditTableComment"
|
||||||
|
@edit-column-comment="handleEditColumnComment"
|
||||||
|
@edit-column="handleEditColumn"
|
||||||
|
@add-column="handleAddColumn"
|
||||||
|
@delete-column="handleDeleteColumn"
|
||||||
|
/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<!-- 表数据 -->
|
||||||
|
<Tabs.TabPane key="data" tab="表数据">
|
||||||
|
<TableData
|
||||||
|
:table-info="tableInfo"
|
||||||
|
:table-data="tableData"
|
||||||
|
:loading="tableDataLoading"
|
||||||
|
:pagination="tableDataPagination"
|
||||||
|
@add-data="handleAddData"
|
||||||
|
@edit-data="handleEditData"
|
||||||
|
@delete-data="handleDeleteData"
|
||||||
|
@batch-delete="handleBatchDeleteData"
|
||||||
|
@search="handleSearch"
|
||||||
|
@page-change="loadTableData"
|
||||||
|
/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<!-- 索引管理 -->
|
||||||
|
<Tabs.TabPane key="index" tab="索引管理">
|
||||||
|
<TableIndex
|
||||||
|
:table-info="tableInfo"
|
||||||
|
@add-index="handleAddIndex"
|
||||||
|
@delete-index="handleDeleteIndex"
|
||||||
|
/>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<!-- SQL查询 -->
|
||||||
|
<Tabs.TabPane key="sql" tab="SQL查询">
|
||||||
|
<!-- 详情页 SQL 组件,同时传入 tableName 和 tableList -->
|
||||||
|
<SqlQuery :table-name="selectedTable" :table-list="tableList" />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<!-- 变更记录 -->
|
||||||
|
<Tabs.TabPane key="changelog" tab="变更记录">
|
||||||
|
<ChangeLog />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modals -->
|
||||||
|
<EditTableCommentModalComponent />
|
||||||
|
<EditColumnCommentModalComponent />
|
||||||
|
<EditColumnModalComponent />
|
||||||
|
<AddColumnModalComponent />
|
||||||
|
<EditDataModalComponent />
|
||||||
|
<AddIndexModalComponent />
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.database-management {
|
||||||
|
min-height: calc(100vh - 200px);
|
||||||
|
}
|
||||||
|
/* 确保 Tabs 内容区域能撑满剩余高度,防止滚动条问题 */
|
||||||
|
:deep(.ant-tabs) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
:deep(.ant-tabs-content) {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,14 +5,14 @@ export default defineConfig(async () => {
|
|||||||
application: {},
|
application: {},
|
||||||
vite: {
|
vite: {
|
||||||
server: {
|
server: {
|
||||||
port: 8866,
|
port: 14002,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
// mock代理目标地址
|
// mock代理目标地址
|
||||||
// target: 'http://localhost:5320/api',
|
// target: 'http://localhost:5320/api',
|
||||||
target: 'http://127.0.0.1:18009/api/',
|
target: 'http://127.0.0.1:15002/api/',
|
||||||
ws: true,
|
ws: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user