refactor: 完善代码生成器

pull/69/head
puhui999 2025-04-09 17:33:25 +08:00
parent 8a5f7ede42
commit 0f9ccbb955
5 changed files with 203 additions and 148 deletions

View File

@ -3,12 +3,19 @@ import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraCodegenApi } from '#/api/infra/codegen';
import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config';
import type { SystemMenuApi } from '#/api/system/menu';
import type { Recordable } from '@vben/types';
import { IconifyIcon } from '@vben/icons';
import { z } from '#/adapter/form';
import { getMenuList } from '#/api/system/menu';
import { getRangePickerDefaultProps } from '#/utils/date';
import { DICT_TYPE, getDictOptions } from '#/utils/dict';
import { handleTree } from '#/utils/tree';
import { h } from 'vue';
import { useAccess } from '@vben/access';
import { $t } from '@vben/locales';
const { hasAccessByCodes } = useAccess();
@ -99,21 +106,14 @@ export function useBasicInfoFormSchema(): VbenFormSchema[] {
componentProps: {
rows: 3,
},
// 使用Tailwind的col-span-2让元素跨越两列
// 使用 Tailwind col-span-2 让元素跨越两列
formItemClass: 'md:col-span-2',
},
];
}
/** 生成信息表单基础 schema */
export function useGenerationInfoBaseFormSchema(menus: SystemMenuApi.SystemMenu[]): VbenFormSchema[] {
/** 菜单树选项 */
const menuTreeProps = {
value: 'id',
title: 'name',
children: 'children',
};
export function useGenerationInfoBaseFormSchema(): VbenFormSchema[] {
return [
{
component: 'Select',
@ -146,19 +146,49 @@ export function useGenerationInfoBaseFormSchema(menus: SystemMenuApi.SystemMenu[
rules: z.number().min(1, { message: '生成场景不能为空' }),
},
{
component: 'TreeSelect',
fieldName: 'parentMenuId',
label: '上级菜单',
help: '分配到指定菜单下,例如 系统管理',
component: 'ApiTreeSelect',
componentProps: {
allowClear: true,
api: async () => {
const data = await getMenuList();
data.unshift({
id: 0,
name: '顶级菜单',
} as SystemMenuApi.SystemMenu);
return handleTree(data);
},
class: 'w-full',
labelField: 'name',
valueField: 'id',
childrenField: 'children',
placeholder: '请选择上级菜单',
filterTreeNode(input: string, node: Recordable<any>) {
if (!input || input.length === 0) {
return true;
}
const name: string = node.label ?? '';
if (!name) return false;
return name.includes(input) || $t(name).includes(input);
},
showSearch: true,
treeData: menus,
fieldNames: menuTreeProps,
treeNodeFilterProp: 'title',
treeDefaultExpandAll: false,
treeCheckable: false,
placeholder: '请选择系统菜单',
treeDefaultExpandedKeys: [0],
},
rules: 'selectRequired',
renderComponentContent() {
return {
title({ label, icon }: { icon: string; label: string }) {
const components = [];
if (!label) return '';
if (icon) {
components.push(h(IconifyIcon, { class: 'size-4', icon }));
}
components.push(h('span', { class: '' }, $t(label || '')));
return h('div', { class: 'flex items-center gap-1' }, components);
},
};
},
},
{

View File

@ -6,31 +6,31 @@ import ColumnInfo from './modules/column-info.vue';
import GenerationInfo from './modules/generation-info.vue';
import { Page } from '@vben/common-ui';
import { ChevronsLeft } from '@vben/icons';
import { Button, message, Tabs } from 'ant-design-vue';
import { Button, message, Steps } from 'ant-design-vue';
import { getCodegenTable, updateCodegenTable } from '#/api/infra/codegen';
import { $t } from '#/locales';
import { ref } from 'vue';
import { ref, unref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const loading = ref(false);
const activeKey = ref('colum');
const currentStep = ref(0);
const formData = ref<InfraCodegenApi.CodegenDetail>({
table: {},
table: {} as InfraCodegenApi.CodegenTable,
columns: [],
});
//
const basicInfoRef = ref();
const columnInfoRef = ref();
const generateInfoRef = ref();
const basicInfoRef = ref<InstanceType<typeof BasicInfo>>();
const columnInfoRef = ref<InstanceType<typeof ColumnInfo>>();
const generateInfoRef = ref<InstanceType<typeof GenerationInfo>>();
//
const getDetail = async () => {
const id = Number(route.query.id);
const id = route.query.id as any;
if (!id) return;
loading.value = true;
@ -43,26 +43,35 @@ const getDetail = async () => {
//
const submitForm = async () => {
if (!formData.value) return;
//
await basicInfoRef.value?.validate();
await generateInfoRef.value?.validate();
try {
//
await basicInfoRef.value?.validate();
await generateInfoRef.value?.validate();
} catch {
message.warn('保存失败,原因:存在表单校验失败请检查!!!');
return;
}
const hideLoading = message.loading({
content: $t('ui.actionMessage.saving'),
content: $t('ui.actionMessage.updating'),
duration: 0,
key: 'action_process_msg',
});
try {
await updateCodegenTable(formData.value);
debugger;
//
const basicInfo = await basicInfoRef.value?.getValues();
const columns = columnInfoRef.value?.getData() || unref(formData).columns;
const generateInfo = await generateInfoRef.value?.getValues();
await updateCodegenTable({ table: { ...unref(formData).table, ...basicInfo, ...generateInfo }, columns });
message.success({
content: $t('ui.actionMessage.saveSuccess'),
content: $t('ui.actionMessage.operationSuccess'),
key: 'action_process_msg',
});
close();
} catch (error) {
message.warn('保存失败!!!');
console.error('保存失败', error);
} finally {
hideLoading();
@ -74,28 +83,61 @@ const close = () => {
router.push('/infra/codegen');
};
//
const nextStep = async () => {
currentStep.value += 1;
};
//
const prevStep = () => {
if (currentStep.value > 0) {
currentStep.value -= 1;
}
};
//
const steps = [
{
title: '基本信息',
},
{
title: '字段信息',
},
{
title: '生成信息',
},
];
//
getDetail();
</script>
<template>
<Page auto-content-height>
<div v-loading="loading">
<Tabs v-model:active-key="activeKey">
<Tabs.TabPane key="basicInfo" tab="基本信息">
<BasicInfo ref="basicInfoRef" :table="formData.table" />
</Tabs.TabPane>
<Tabs.TabPane key="colum" tab="字段信息">
<ColumnInfo ref="columnInfoRef" :columns="formData.columns" />
</Tabs.TabPane>
<Tabs.TabPane key="generateInfo" tab="生成信息">
<GenerationInfo ref="generateInfoRef" :table="formData.table" :columns="formData.columns" />
</Tabs.TabPane>
</Tabs>
<Page auto-content-height v-loading="loading">
<div class="flex h-[95%] flex-col rounded-md bg-white p-4 dark:bg-[#1f1f1f] dark:text-gray-300">
<Steps type="navigation" :current="currentStep" class="mb-8 rounded shadow-sm dark:bg-[#141414]">
<Steps.Step v-for="(step, index) in steps" :key="index" :title="step.title" />
</Steps>
<div class="mt-4 flex justify-end">
<Button type="primary" :loading="loading" @click="submitForm"></Button>
<Button class="ml-2" @click="close">
<div class="flex-1 overflow-auto py-4">
<!-- 根据当前步骤显示对应的组件 -->
<BasicInfo v-show="currentStep === 0" ref="basicInfoRef" :table="formData.table" />
<ColumnInfo v-show="currentStep === 1" ref="columnInfoRef" :columns="formData.columns" />
<GenerationInfo
v-show="currentStep === 2"
ref="generateInfoRef"
:table="formData.table"
:columns="formData.columns"
/>
</div>
<div class="mt-4 flex justify-end space-x-2">
<Button v-show="currentStep > 0" @click="prevStep"></Button>
<Button v-show="currentStep < steps.length - 1" type="primary" @click="nextStep"></Button>
<Button v-show="currentStep === steps.length - 1" type="primary" :loading="loading" @click="submitForm">
保存
</Button>
<Button @click="close">
<ChevronsLeft class="mr-1" />
返回
</Button>

View File

@ -23,6 +23,7 @@ const [Grid, extendedApi] = useVbenVxeGrid({
border: true,
showOverflow: true,
height: 'auto',
autoResize: true,
keepSource: true,
rowConfig: {
keyField: 'id',
@ -54,7 +55,7 @@ watch(
//
defineExpose({
getData: extendedApi.grid?.getData,
getData: (): InfraCodegenApi.CodegenColumn[] => extendedApi.grid.getData(),
});
//
@ -71,83 +72,81 @@ loadDictTypeOptions();
</script>
<template>
<div class="h-[80vh] w-full">
<Grid>
<!-- 字段描述 -->
<template #columnComment="{ row }">
<Input v-model:value="row.columnComment" />
</template>
<Grid>
<!-- 字段描述 -->
<template #columnComment="{ row }">
<Input v-model:value="row.columnComment" />
</template>
<!-- Java类型 -->
<template #javaType="{ row, column }">
<Select v-model:value="row.javaType" style="width: 100%">
<Select.Option v-for="option in column.params.options" :key="option.value" :value="option.value">
{{ option.label }}
</Select.Option>
</Select>
</template>
<!-- Java类型 -->
<template #javaType="{ row, column }">
<Select v-model:value="row.javaType" style="width: 100%">
<Select.Option v-for="option in column.params.options" :key="option.value" :value="option.value">
{{ option.label }}
</Select.Option>
</Select>
</template>
<!-- Java属性 -->
<template #javaField="{ row }">
<Input v-model:value="row.javaField" />
</template>
<!-- Java属性 -->
<template #javaField="{ row }">
<Input v-model:value="row.javaField" />
</template>
<!-- 插入 -->
<template #createOperation="{ row }">
<Checkbox v-model:checked="row.createOperation" />
</template>
<!-- 插入 -->
<template #createOperation="{ row }">
<Checkbox v-model:checked="row.createOperation" />
</template>
<!-- 编辑 -->
<template #updateOperation="{ row }">
<Checkbox v-model:checked="row.updateOperation" />
</template>
<!-- 编辑 -->
<template #updateOperation="{ row }">
<Checkbox v-model:checked="row.updateOperation" />
</template>
<!-- 列表 -->
<template #listOperationResult="{ row }">
<Checkbox v-model:checked="row.listOperationResult" />
</template>
<!-- 列表 -->
<template #listOperationResult="{ row }">
<Checkbox v-model:checked="row.listOperationResult" />
</template>
<!-- 查询 -->
<template #listOperation="{ row }">
<Checkbox v-model:checked="row.listOperation" />
</template>
<!-- 查询 -->
<template #listOperation="{ row }">
<Checkbox v-model:checked="row.listOperation" />
</template>
<!-- 查询方式 -->
<template #listOperationCondition="{ row, column }">
<Select v-model:value="row.listOperationCondition" style="width: 100%">
<Select.Option v-for="option in column.params.options" :key="option.value" :value="option.value">
{{ option.label }}
</Select.Option>
</Select>
</template>
<!-- 查询方式 -->
<template #listOperationCondition="{ row, column }">
<Select v-model:value="row.listOperationCondition" style="width: 100%">
<Select.Option v-for="option in column.params.options" :key="option.value" :value="option.value">
{{ option.label }}
</Select.Option>
</Select>
</template>
<!-- 允许空 -->
<template #nullable="{ row }">
<Checkbox v-model:checked="row.nullable" />
</template>
<!-- 允许空 -->
<template #nullable="{ row }">
<Checkbox v-model:checked="row.nullable" />
</template>
<!-- 显示类型 -->
<template #htmlType="{ row, column }">
<Select v-model:value="row.htmlType" style="width: 100%">
<Select.Option v-for="option in column.params.options" :key="option.value" :value="option.value">
{{ option.label }}
</Select.Option>
</Select>
</template>
<!-- 显示类型 -->
<template #htmlType="{ row, column }">
<Select v-model:value="row.htmlType" style="width: 100%">
<Select.Option v-for="option in column.params.options" :key="option.value" :value="option.value">
{{ option.label }}
</Select.Option>
</Select>
</template>
<!-- 字典类型 -->
<template #dictType="{ row }">
<Select v-model:value="row.dictType" style="width: 100%" allow-clear show-search>
<Select.Option v-for="option in dictTypeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</Select.Option>
</Select>
</template>
<!-- 字典类型 -->
<template #dictType="{ row }">
<Select v-model:value="row.dictType" style="width: 100%" allow-clear show-search>
<Select.Option v-for="option in dictTypeOptions" :key="option.value" :value="option.value">
{{ option.label }}
</Select.Option>
</Select>
</template>
<!-- 示例 -->
<template #example="{ row }">
<Input v-model:value="row.example" />
</template>
</Grid>
</div>
<!-- 示例 -->
<template #example="{ row }">
<Input v-model:value="row.example" />
</template>
</Grid>
</template>

View File

@ -1,12 +1,12 @@
<script lang="ts" setup>
import type { InfraCodegenApi } from '#/api/infra/codegen';
import type { SystemMenuApi } from '#/api/system/menu';
import { useVbenForm } from '#/adapter/form';
import { getCodegenTableList } from '#/api/infra/codegen';
import { getSimpleMenusList } from '#/api/system/menu';
import { InfraCodegenTemplateTypeEnum } from '#/utils/constants';
import { computed, onMounted, ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { isEmpty } from '@vben/utils';
import { useGenerationInfoBaseFormSchema, useSubTableFormSchema, useTreeTableFormSchema } from '../data';
@ -18,7 +18,6 @@ const props = defineProps<{
}>();
const tables = ref<InfraCodegenApi.CodegenTable[]>([]);
const menus = ref<SystemMenuApi.SystemMenu[]>([]);
const currentTemplateType = ref<number>();
const wrapperClass = 'grid grid-cols-1 md:grid-cols-2 gap-4 mb-4'; //
//
@ -30,7 +29,7 @@ const [BaseForm, baseFormApi] = useVbenForm({
wrapperClass,
layout: 'horizontal',
showDefaultActions: false,
schema: [],
schema: useGenerationInfoBaseFormSchema(),
handleValuesChange: (values) => {
//
if (values.templateType !== undefined && values.templateType !== currentTemplateType.value) {
@ -55,12 +54,6 @@ const [SubForm, subFormApi] = useVbenForm({
schema: [],
});
/** 更新基础表单 schema */
function updateBaseSchema(): void {
const schema = useGenerationInfoBaseFormSchema(menus.value);
baseFormApi.setState({ schema });
}
/** 更新树表信息表单 schema */
function updateTreeSchema(): void {
const schema = useTreeTableFormSchema(props.columns);
@ -124,29 +117,24 @@ function setAllFormValues(values: Record<string, any>): void {
watch(
() => props.table,
async (val) => {
if (!val) return;
if (!val || isEmpty(val)) {
return;
}
// schema
updateTreeSchema();
//
setAllFormValues(val);
//
if (typeof val.dataSourceConfigId !== undefined) {
tables.value = await getCodegenTableList(val.dataSourceConfigId);
// schema
updateSubSchema();
if (typeof val.dataSourceConfigId === undefined) {
return;
}
tables.value = await getCodegenTableList(val.dataSourceConfigId);
// schema
updateSubSchema();
},
{ immediate: true },
);
/** 初始化 */
onMounted(async () => {
//
menus.value = await getSimpleMenusList();
// schema
updateBaseSchema();
});
/** 暴露出表单校验方法和表单值获取方法 */
defineExpose({
validate: validateAllForms,

View File

@ -33,7 +33,7 @@ function isBoolean(value: unknown): value is boolean {
* @param {T} value
* @returns {boolean} truefalse
*/
function isEmpty<T = unknown>(value?: T): value is T {
function isEmpty<T = unknown>(value?: T): boolean {
if (value === null || value === undefined) {
return true;
}
@ -75,9 +75,7 @@ function isHttpUrl(url?: string): boolean {
* @returns {boolean} windowtruefalse
*/
function isWindow(value: any): value is Window {
return (
typeof window !== 'undefined' && value !== null && value === value.window
);
return typeof window !== 'undefined' && value !== null && value === value.window;
}
/**
@ -137,9 +135,7 @@ function isNumber(value: any): value is number {
* // Returns undefined because all values are either null or undefined.
* getFirstNonNullOrUndefined(undefined, null); // undefined
*/
function getFirstNonNullOrUndefined<T>(
...values: (null | T | undefined)[]
): T | undefined {
function getFirstNonNullOrUndefined<T>(...values: (null | T | undefined)[]): T | undefined {
for (const value of values) {
if (value !== undefined && value !== null) {
return value;