物业代码生成

This commit is contained in:
2025-06-18 11:03:42 +08:00
commit 1262d4c745
1881 changed files with 249599 additions and 0 deletions

View File

@@ -0,0 +1,218 @@
<script setup lang="ts">
import type { Key } from 'ant-design-vue/es/vc-tree/interface';
import type { Component } from 'vue';
import type { LanguageSupport } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { markRaw, ref } from 'vue';
import { CodeMirror, useVbenModal } from '@vben/common-ui';
import {
DefaultFileIcon,
FolderIcon,
JavaIcon,
SqlIcon,
TsIcon,
VueIcon,
XmlIcon,
} from '@vben/icons';
import { useClipboard } from '@vueuse/core';
import { Alert, Skeleton, Tree } from 'ant-design-vue';
import { previewCode } from '#/api/tool/gen';
interface TreeNode {
children: TreeNode[];
title: string;
key: string;
icon: Component; // 树左边图标
}
const treeData = ref<TreeNode[]>([]);
/** modal标题 */
const modalTitle = ref('代码预览');
/** 代码内容 */
const codeContent = ref('点击左侧树节点查看代码');
/** code */
const currentCodeData = ref<null | Recordable<any>>(null);
const [BasicModal, modalApi] = useVbenModal({
async onOpenChange(isOpen) {
if (!isOpen) {
handleClose();
return null;
}
modalApi.modalLoading(true);
const { tableId } = modalApi.getData() as { tableId: string };
const data = await previewCode(tableId);
currentCodeData.value = data;
const tree = convertToTree(Object.keys(data));
treeData.value = tree;
modalApi.modalLoading(false);
},
});
/**
* 文件路径数组转树结构
* @param paths 文件路径数组
*/
function convertToTree(paths: string[]): TreeNode[] {
const tree: TreeNode[] = [];
for (const path of paths) {
const segments = path.split('/');
let currentNode = tree;
let currentPath = '';
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
currentPath += `${segment}`;
if (i !== segments.length - 1) {
currentPath += '/';
}
const existingNode = currentNode.find((node) => node.title === segment);
if (existingNode) {
currentNode = existingNode.children || [];
} else {
const title = (segment ?? '').replace('.vm', '');
const newNode: TreeNode = {
icon: findIcon(currentPath),
key: currentPath,
title,
children: [],
};
currentNode.push(newNode);
currentNode = newNode.children;
}
}
}
return tree;
}
const iconMap = [
{ key: 'java', value: markRaw(JavaIcon) },
{ key: 'xml', value: markRaw(XmlIcon) },
{ key: 'sql', value: markRaw(SqlIcon) },
{ key: 'ts', value: markRaw(TsIcon) },
{ key: 'vue', value: markRaw(VueIcon) },
{ key: 'folder', value: markRaw(FolderIcon) },
];
function findIcon(path: string) {
const defaultFileIcon = DefaultFileIcon;
const defaultFolderIcon = FolderIcon;
if (path.endsWith('.vm')) {
const realPath = path.slice(0, -3);
// 是否为指定拓展名
const icon = iconMap.find((item) => realPath.endsWith(item.key));
if (icon) {
return icon.value;
}
return defaultFileIcon;
}
// 其他的为文件夹
return defaultFolderIcon;
}
const language = ref<LanguageSupport>('html');
function changeLanguageType(filename: string) {
const typeList: { language: LanguageSupport; type: string }[] = [
{ language: 'ts', type: '.ts' },
{ language: 'java', type: '.java' },
{ language: 'xml', type: '.xml' },
{ language: 'sql', type: 'sql' },
{ language: 'vue', type: '.vue' },
];
const type = typeList.find((item) => filename.includes(item.type));
language.value = type ? type.language : 'html';
}
function handleSelect(selectedKeys: Key[]) {
const [currentFile = ''] = selectedKeys as string[];
if (!currentCodeData.value) {
return;
}
const currentCode =
currentCodeData.value[currentFile as keyof typeof currentCodeData.value];
if (currentCode) {
// 设置代码type
changeLanguageType(currentFile);
// 内容
codeContent.value = currentCode;
// 修改标题
modalTitle.value = `代码预览: ${currentFile.replace('.vm', '')}`;
}
}
function handleClose() {
currentCodeData.value = null;
codeContent.value = '点击左侧树节点查看代码';
modalTitle.value = '代码预览';
language.value = 'html';
}
const { copy } = useClipboard({ legacy: true });
</script>
<template>
<BasicModal
:footer="false"
:fullscreen="true"
:fullscreen-button="false"
:title="modalTitle"
>
<div v-if="currentCodeData" class="flex gap-[8px]">
<div class="h-[calc(100vh-80px)] w-[300px] overflow-y-scroll">
<Tree
v-if="treeData.length > 0"
:show-line="{ showLeafIcon: false }"
:tree-data="treeData"
:virtual="false"
default-expand-all
@select="handleSelect"
>
<template #title="{ title, icon }">
<div class="flex items-center gap-[16px]">
<component :is="icon" />
<span>{{ title }}</span>
</div>
</template>
</Tree>
<Alert
class="mt-2"
show-icon
message="👆显示的名称为模板的文件名,非最终下载文件名..."
/>
</div>
<CodeMirror
v-model="codeContent"
:language="language"
class="h-[calc(100vh-80px)] w-full overflow-y-scroll text-[16px]"
readonly
/>
<div class="fixed right-20 top-20">
<a-button @click="copy(codeContent)">复制</a-button>
</div>
</div>
<Skeleton v-if="!currentCodeData" active />
</BasicModal>
</template>
<style lang="scss" scoped>
:deep(.ant-tree .ant-tree-switcher) {
display: flex;
align-items: center;
}
/** codeMirror 占满容器高度 即calc计算的高度 */
:deep(.cm-editor) {
height: 100%;
}
</style>

View File

@@ -0,0 +1,60 @@
import type { FormSchemaGetter } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table';
export const querySchema: FormSchemaGetter = () => [
{
component: 'Select',
fieldName: 'dataName',
label: '数据源',
defaultValue: '',
componentProps: {
allowClear: false,
},
},
{
component: 'Input',
fieldName: 'tableName',
label: '表名称',
},
{
component: 'Input',
fieldName: 'tableComment',
label: '表描述',
},
{
component: 'RangePicker',
fieldName: 'createTime',
label: '创建时间',
},
];
export const columns: VxeGridProps['columns'] = [
{ type: 'checkbox', width: 60 },
{
field: 'tableName',
title: '表名称',
},
{
field: 'tableComment',
title: '表描述',
},
{
field: 'className',
title: '实体类',
},
{
field: 'createTime',
title: '创建时间',
},
{
field: 'updateTime',
title: '更新时间',
},
{
field: 'action',
fixed: 'right',
slots: { default: 'action' },
title: '操作',
width: 300,
},
];

View File

@@ -0,0 +1,120 @@
<script setup lang="ts">
import type { GenInfo } from '#/api/tool/gen/model';
import { onMounted, provide, ref, unref, useTemplateRef } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { useTabs } from '@vben/hooks';
import { cloneDeep, safeParseNumber } from '@vben/utils';
import { Card, Skeleton, TabPane, Tabs } from 'ant-design-vue';
import { editSave, genInfo } from '#/api/tool/gen';
import { BasicSetting, GenConfig } from './edit-steps';
const { setTabTitle, closeCurrentTab } = useTabs();
const routes = useRoute();
// 获取路由参数
const tableId = routes.params.tableId as string;
const genInfoData = ref<GenInfo['info']>();
provide('genInfoData', genInfoData);
onMounted(async () => {
const resp = await genInfo(tableId);
// 需要做菜单转换 严格相等 才能选中回显
resp.info.parentMenuId = safeParseNumber(resp.info.parentMenuId);
genInfoData.value = resp.info;
setTabTitle(`生成配置: ${resp.info.tableName}`);
});
const currentTab = ref<'fields' | 'setting'>('setting');
const basicSettingRef = useTemplateRef('basicSettingRef');
const genConfigRef = useTemplateRef('genConfigRef');
const router = useRouter();
async function handleSave() {
try {
// 校验tab1
const settingValidate = await basicSettingRef.value?.validateForm();
if (!settingValidate) {
currentTab.value = 'setting';
return;
}
// 校验tab2
const genConfigValidate = await genConfigRef.value?.validateTable();
if (!genConfigValidate) {
currentTab.value = 'fields';
return;
}
const requestData = cloneDeep(unref(genInfoData)!);
// 获取表单数据
const formValues = await basicSettingRef.value?.getFormValues();
// 合并
Object.assign(requestData, formValues);
// 从表格获取最新的
requestData.columns = genConfigRef.value?.getTableRecords() ?? [];
// 树表需要添加这个参数
if (requestData && requestData.tplCategory === 'tree') {
const { treeCode, treeName, treeParentCode } = requestData;
requestData.params = {
treeCode,
treeName,
treeParentCode,
};
}
// 需要进行参数转化
if (requestData) {
const transform = (ret: boolean) => (ret ? '1' : '0');
requestData.columns.forEach((column) => {
const { edit, insert, query, required, list } = column;
column.isInsert = transform(insert);
column.isEdit = transform(edit);
column.isList = transform(list);
column.isQuery = transform(query);
column.isRequired = transform(required);
});
// 需要手动添加父级菜单 弹窗类型
requestData.params = {
...requestData.params,
parentMenuId: requestData.parentMenuId,
popupComponent: requestData.popupComponent,
formComponent: requestData.formComponent,
};
}
// 保存
await editSave(requestData);
// 关闭 & 跳转
await closeCurrentTab();
router.push({ path: '/tool/gen', replace: true });
} catch (error) {
console.error(error);
}
}
</script>
<template>
<Page :auto-content-height="true">
<Card
class="h-full"
v-if="genInfoData"
:body-style="{ padding: '0 16px 16px' }"
>
<Tabs v-model:active-key="currentTab" size="middle">
<template #rightExtra>
<a-button type="primary" @click="handleSave">保存配置</a-button>
</template>
<TabPane key="setting" tab="生成信息" :force-render="true">
<BasicSetting ref="basicSettingRef" />
</TabPane>
<TabPane key="fields" tab="字段信息" :force-render="true">
<GenConfig ref="genConfigRef" />
</TabPane>
</Tabs>
</Card>
<Skeleton v-else :active="true" />
</Page>
</template>

View File

@@ -0,0 +1,153 @@
<script setup lang="ts">
import type { Ref } from 'vue';
import type { Column, GenInfo } from '#/api/tool/gen/model';
import { inject, onMounted } from 'vue';
import { useVbenForm } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { addFullName, listToTree } from '@vben/utils';
import { Col, Row } from 'ant-design-vue';
import { menuList } from '#/api/system/menu';
import { formSchema } from './basic';
/**
* 从父组件注入
*/
const genInfoData = inject('genInfoData') as Ref<GenInfo['info']>;
const [BasicForm, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
formItemClass: 'col-span-1',
},
labelWidth: 150,
},
schema: formSchema(),
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
});
/**
* 树表需要用到的数据
*/
async function initTreeSelect(columns: Column[]) {
const options = columns.map((item) => {
const label = `${item.columnName} | ${item.columnComment}`;
return { label, value: item.columnName };
});
formApi.updateSchema([
{
componentProps: {
options,
},
fieldName: 'treeCode',
},
{
componentProps: {
options,
},
fieldName: 'treeParentCode',
},
{
componentProps: {
options,
},
fieldName: 'treeName',
},
]);
}
/**
* 加载菜单选择
*/
async function initMenuSelect() {
const list = await menuList();
// support i18n
list.forEach((item) => {
item.menuName = $t(item.menuName);
});
const tree = listToTree(list, { id: 'menuId', pid: 'parentId' });
const treeData = [
{
fullName: $t('menu.root'),
menuId: 0,
menuName: $t('menu.root'),
children: tree,
},
];
addFullName(treeData, 'menuName', ' / ');
formApi.updateSchema([
{
componentProps: {
fieldNames: {
label: 'menuName',
value: 'menuId',
},
// 设置弹窗滚动高度 默认256
listHeight: 300,
treeData,
treeDefaultExpandAll: false,
// 默认展开的树节点
treeDefaultExpandedKeys: [0],
treeLine: { showLeafIcon: false },
treeNodeLabelProp: 'fullName',
},
fieldName: 'parentMenuId',
},
]);
}
onMounted(async () => {
const info = genInfoData.value;
await formApi.setValues(info);
// 弹出框类型需要手动赋值
if (info.options) {
const { popupComponent, formComponent } = JSON.parse(info.options);
if (popupComponent) {
formApi.setFieldValue('popupComponent', popupComponent);
}
if (formComponent) {
formApi.setFieldValue('formComponent', formComponent);
}
}
await Promise.all([initTreeSelect(info.columns), initMenuSelect()]);
});
/**
* 校验表单
*/
async function validateForm() {
const { valid } = await formApi.validate();
if (!valid) {
return false;
}
return true;
}
/**
* 获取表单值
*/
async function getFormValues() {
return await formApi.getValues();
}
defineExpose({
validateForm,
getFormValues,
});
</script>
<template>
<Row justify="center">
<Col v-bind="{ xs: 24, sm: 24, md: 20, lg: 16, xl: 16 }">
<BasicForm />
</Col>
</Row>
</template>

View File

@@ -0,0 +1,212 @@
import type { FormSchemaGetter } from '#/adapter/form';
import { getPopupContainer } from '@vben/utils';
import { z } from '#/adapter/form';
export const formSchema: FormSchemaGetter = () => [
{
component: 'Divider',
componentProps: {
orientation: 'left',
},
fieldName: 'divider1',
formItemClass: 'col-span-2',
label: '基本信息',
},
{
component: 'Input',
fieldName: 'tableName',
label: '表名称',
rules: 'required',
},
{
component: 'Input',
fieldName: 'tableComment',
label: '表描述',
rules: 'required',
},
{
component: 'Input',
fieldName: 'className',
label: '实体类名称',
rules: 'required',
},
{
component: 'Input',
fieldName: 'functionAuthor',
label: '作者',
rules: 'required',
},
{
component: 'Divider',
componentProps: {
orientation: 'left',
},
fieldName: 'divider2',
formItemClass: 'col-span-2',
label: '生成信息',
},
{
component: 'Select',
componentProps: {
allowClear: false,
getPopupContainer,
options: [
{ label: '单表(增删改查)', value: 'crud' },
{ label: '树表(增删改查)', value: 'tree' },
],
},
defaultValue: 'crud',
fieldName: 'tplCategory',
label: '模板类型',
rules: 'selectRequired',
},
{
component: 'Select',
componentProps: {
getPopupContainer,
},
dependencies: {
show: (values) => values.tplCategory === 'tree',
triggerFields: ['tplCategory'],
},
fieldName: 'treeCode',
helpMessage: '树节点显示的编码字段名, 如: dept_id (相当于id)',
label: '树编码字段',
rules: 'selectRequired',
},
{
component: 'Select',
componentProps: {
allowClear: false,
},
dependencies: {
show: (values) => values.tplCategory === 'tree',
triggerFields: ['tplCategory'],
},
fieldName: 'treeParentCode',
help: '树节点显示的父编码字段名, 如: parent_Id (相当于parentId)',
label: '树父编码字段',
rules: 'selectRequired',
},
{
component: 'Select',
componentProps: {
allowClear: false,
},
dependencies: {
show: (values) => values.tplCategory === 'tree',
triggerFields: ['tplCategory'],
},
fieldName: 'treeName',
help: '树节点的显示名称字段名, 如: dept_name (相当于label)',
label: '树名称字段',
rules: 'selectRequired',
},
{
component: 'Input',
fieldName: 'packageName',
help: '生成在哪个java包下, 例如 com.ruoyi.system',
label: '生成包路径',
rules: 'required',
},
{
component: 'Input',
fieldName: 'moduleName',
help: '可理解为子系统名,例如 system',
label: '生成模块名',
rules: 'required',
},
{
component: 'Input',
fieldName: 'businessName',
help: '可理解为功能英文名,例如 user',
label: '生成业务名',
rules: 'required',
},
{
component: 'Input',
fieldName: 'functionName',
help: '用作类描述,例如 用户',
label: '生成功能名',
rules: 'required',
},
{
component: 'TreeSelect',
componentProps: {
allowClear: false,
getPopupContainer,
},
defaultValue: 0,
fieldName: 'parentMenuId',
label: '上级菜单',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: 'modal弹窗', value: 'modal' },
{ label: 'drawer抽屉', value: 'drawer' },
],
optionType: 'button',
},
help: '自定义功能, 需要后端支持',
defaultValue: 'modal',
fieldName: 'popupComponent',
label: '弹窗组件类型',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: 'useVbenForm', value: 'useForm' },
{ label: 'antd原生表单', value: 'native' },
],
optionType: 'button',
},
help: '自定义功能, 需要后端支持\n复杂(布局, 联动等)表单建议用antd原生表单',
defaultValue: 'useForm',
fieldName: 'formComponent',
label: '生成表单类型',
},
{
component: 'RadioGroup',
componentProps: {
buttonStyle: 'solid',
options: [
{ label: 'zip压缩包', value: '0' },
{ label: '自定义路径', value: '1' },
],
optionType: 'button',
},
defaultValue: '0',
fieldName: 'genType',
help: '默认为zip压缩包下载, 也可以自定义生成路径',
label: '生成代码方式',
},
{
component: 'Input',
defaultValue: '/',
dependencies: {
show: (model) => model.genType === '1',
triggerFields: ['genType'],
},
fieldName: 'genPath',
help: '输入绝对路径, 不支持"./"相对路径',
label: '代码生成路径',
rules: z
.string()
.regex(/^(?:[a-z]:)?(?:\/|(?:\\|\/)[^\\/:*?"<>|\r\n]+)*(?:\\|\/)?$/i, {
message: '请输入合法的路径',
}),
},
{
component: 'Textarea',
fieldName: 'remark',
formItemClass: 'col-span-2 items-baseline',
label: '备注',
},
];

View File

@@ -0,0 +1,72 @@
<script setup lang="ts">
import type { Ref } from 'vue';
import type { VxeGridProps } from '#/adapter/vxe-table';
import type { GenInfo } from '#/api/tool/gen/model';
import { inject } from 'vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { validRules, vxeTableColumns } from './gen-data';
/**
* 从父组件注入
*/
const genInfoData = inject('genInfoData') as Ref<GenInfo['info']>;
const gridOptions: VxeGridProps = {
columns: vxeTableColumns,
keepSource: true,
editConfig: { trigger: 'click', mode: 'cell', showStatus: true },
editRules: validRules,
rowConfig: {
keyField: 'id',
isCurrent: true, // 高亮当前行
},
columnConfig: {
resizable: true,
},
proxyConfig: {
enabled: true,
},
toolbarConfig: {
enabled: false,
},
height: 'auto',
pagerConfig: {
enabled: false,
},
data: genInfoData.value.columns,
};
const [BasicTable, tableApi] = useVbenVxeGrid({ gridOptions });
/**
* 校验表格数据
*/
async function validateTable() {
const hasError = await tableApi.grid.validate();
return !hasError;
}
/**
* 获取表格数据
*/
function getTableRecords() {
return tableApi?.grid?.getData?.() ?? [];
}
defineExpose({
validateTable,
getTableRecords,
});
</script>
<template>
<div class="flex flex-col gap-[16px]">
<div class="h-[calc(100vh-200px)] overflow-y-hidden">
<BasicTable />
</div>
</div>
</template>

View File

@@ -0,0 +1,326 @@
import type { Recordable } from '@vben/types';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { reactive } from 'vue';
import { getPopupContainer } from '@vben/utils';
import { Checkbox, Input, Select } from 'ant-design-vue';
import { dictOptionSelectList } from '#/api/system/dict/dict-type';
const JavaTypes: string[] = [
'Long',
'String',
'Integer',
'Double',
'BigDecimal',
'Date',
'Boolean',
'LocalDate',
'LocalDateTime',
];
const queryTypeOptions = [
{ label: '=', value: 'EQ' },
{ label: '!=', value: 'NE' },
{ label: '>', value: 'GT' },
{ label: '>=', value: 'GE' },
{ label: '<', value: 'LT' },
{ label: '<=', value: 'LE' },
{ label: 'LIKE', value: 'LIKE' },
{ label: 'BETWEEN', value: 'BETWEEN' },
];
const componentsOptions = [
{ label: '文本框', value: 'input' },
{ label: '文本域', value: 'textarea' },
{ label: '下拉框', value: 'select' },
{ label: '单选框', value: 'radio' },
{ label: '复选框', value: 'checkbox' },
{ label: '日期控件', value: 'datetime' },
{ label: '图片上传', value: 'imageUpload' },
{ label: '文件上传', value: 'fileUpload' },
{ label: '富文本', value: 'editor' },
];
const dictOptions = reactive<{ label: string; value: string }[]>([
{ label: '未设置', value: '' },
]);
/**
* 在这里初始化字典下拉框
*/
(async function init() {
const ret = await dictOptionSelectList();
ret.forEach((dict) => {
const option = {
label: `${dict.dictName} | ${dict.dictType}`,
value: dict.dictType,
};
dictOptions.push(option);
});
})();
function renderBooleanTag(row: Recordable<any>, field: string) {
const value = row[field] ? '是' : '否';
const className = row[field] ? 'text-green-500' : 'text-red-500';
return <span class={className}>{value}</span>;
}
function renderBooleanCheckbox(row: Recordable<any>, field: string) {
return <Checkbox v-model:checked={row[field]}></Checkbox>;
}
export const validRules: VxeGridProps['editRules'] = {
columnComment: [{ required: true, message: '请输入' }],
javaField: [{ required: true, message: '请输入' }],
};
export const vxeTableColumns: VxeGridProps['columns'] = [
{
title: '序号',
type: 'seq',
fixed: 'left',
width: '50',
align: 'center',
},
{
title: '字段列名',
field: 'columnName',
showOverflow: 'tooltip',
fixed: 'left',
minWidth: 150,
},
{
title: '字段描述',
field: 'columnComment',
minWidth: 150,
slots: {
edit: ({ row }) => {
return <Input v-model:value={row.columnComment}></Input>;
},
},
editRender: {},
},
{
title: 'db类型',
field: 'columnType',
minWidth: 120,
showOverflow: 'tooltip',
},
{
title: 'Java类型',
field: 'javaType',
minWidth: 150,
slots: {
edit: ({ row }) => {
const javaTypeOptions = JavaTypes.map((type) => ({
label: type,
value: type,
}));
return (
<Select
class="w-full"
getPopupContainer={getPopupContainer}
options={javaTypeOptions}
v-model:value={row.javaType}
></Select>
);
},
},
editRender: {},
},
{
title: 'Java属性名',
field: 'javaField',
minWidth: 150,
showOverflow: 'tooltip',
slots: {
edit: ({ row }) => {
return <Input v-model:value={row.javaField}></Input>;
},
},
editRender: {},
},
{
title: '插入',
field: 'insert',
minWidth: 80,
showOverflow: 'tooltip',
align: 'center',
slots: {
default: ({ row }) => {
return renderBooleanTag(row, 'insert');
},
edit: ({ row }) => {
return renderBooleanCheckbox(row, 'insert');
},
},
editRender: {},
},
{
title: '编辑',
field: 'edit',
showOverflow: 'tooltip',
align: 'center',
minWidth: 80,
slots: {
default: ({ row }) => {
return renderBooleanTag(row, 'edit');
},
edit: ({ row }) => {
return renderBooleanCheckbox(row, 'edit');
},
},
editRender: {},
},
{
title: '列表',
field: 'list',
showOverflow: 'tooltip',
align: 'center',
minWidth: 80,
slots: {
default: ({ row }) => {
return renderBooleanTag(row, 'list');
},
edit: ({ row }) => {
return renderBooleanCheckbox(row, 'list');
},
},
editRender: {},
},
{
title: '查询',
field: 'query',
showOverflow: 'tooltip',
align: 'center',
minWidth: 80,
slots: {
default: ({ row }) => {
return renderBooleanTag(row, 'query');
},
edit: ({ row }) => {
return renderBooleanCheckbox(row, 'query');
},
},
editRender: {},
},
{
title: '查询方式',
field: 'queryType',
showOverflow: 'tooltip',
align: 'center',
minWidth: 150,
slots: {
default: ({ row }) => {
const queryType = row.queryType;
const found = queryTypeOptions.find((item) => item.value === queryType);
if (found) {
return found.label;
}
return queryType;
},
edit: ({ row }) => {
return (
<Select
class="w-full"
getPopupContainer={getPopupContainer}
options={queryTypeOptions}
v-model:value={row.queryType}
></Select>
);
},
},
editRender: {},
},
{
title: '必填',
field: 'required',
showOverflow: 'tooltip',
align: 'center',
minWidth: 80,
slots: {
default: ({ row }) => {
return renderBooleanTag(row, 'required');
},
edit: ({ row }) => {
return renderBooleanCheckbox(row, 'required');
},
},
editRender: {},
},
{
title: '显示类型',
field: 'htmlType',
showOverflow: 'tooltip',
minWidth: 150,
align: 'center',
slots: {
default: ({ row }) => {
const htmlType = row.htmlType;
const found = componentsOptions.find((item) => item.value === htmlType);
if (found) {
return found.label;
}
return htmlType;
},
edit: ({ row }) => {
return (
<Select
class="w-full"
getPopupContainer={getPopupContainer}
options={componentsOptions}
v-model:value={row.htmlType}
></Select>
);
},
},
editRender: {},
},
{
title: '字典类型',
field: 'dictType',
showOverflow: 'tooltip',
minWidth: 230,
align: 'center',
titlePrefix: {
message: `仅'下拉框', '单选框', '复选框'支持字典类型`,
},
slots: {
default: ({ row }) => {
const dictType = row.dictType;
const found = dictOptions.find((item) => item.value === dictType);
if (found) {
return found.label;
}
return dictType;
},
edit: ({ row }) => {
// 清除的回调 需要设置为空字符串 否则不会提交
const onDeselect = () => {
row.dictType = '';
};
const disabled =
row.htmlType !== 'select' &&
row.htmlType !== 'radio' &&
row.htmlType !== 'checkbox';
return (
<Select
allowClear={true}
class="w-full"
disabled={disabled}
getPopupContainer={getPopupContainer}
onDeselect={onDeselect}
options={dictOptions}
placeholder="请选择字典类型"
v-model:value={row.dictType}
></Select>
);
},
},
editRender: {},
},
];

View File

@@ -0,0 +1,2 @@
export { default as BasicSetting } from './basic-setting.vue';
export { default as GenConfig } from './gen-config.vue';

View File

@@ -0,0 +1,10 @@
<!--
后端版本>=5.4.0 这个从本地路由变为从后台返回
-->
<script setup lang="ts">
import EditGenPage from './edit-gen.vue';
</script>
<template>
<EditGenPage />
</template>

View File

@@ -0,0 +1,291 @@
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { getVxePopupContainer } from '@vben/utils';
import { message, Modal, Popconfirm, Space } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
import {
batchGenCode,
generatedList,
genRemove,
genWithPath,
getDataSourceNames,
syncDb,
} from '#/api/tool/gen';
import { downloadByData } from '#/utils/file/download';
import codePreviewModal from './code-preview-modal.vue';
import { columns, querySchema } from './data';
import tableImportModal from './table-import-modal.vue';
const formOptions: VbenFormProps = {
schema: querySchema(),
commonConfig: {
labelWidth: 80,
componentProps: {
allowClear: true,
},
},
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
// 日期选择格式化
fieldMappingTime: [
[
'createTime',
['params[beginTime]', 'params[endTime]'],
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
],
],
};
const gridOptions: VxeGridProps = {
checkboxConfig: {
// 高亮
highlight: true,
// 翻页时保留选中状态
reserve: true,
// 点击行选中
trigger: 'row',
},
columns,
height: 'auto',
keepSource: true,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
return await generatedList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'tableId',
},
id: 'tool-gen-index',
};
const [BasicTable, tableApi] = useVbenVxeGrid({
formOptions,
gridOptions,
});
onMounted(async () => {
// 获取数据源
const ret = await getDataSourceNames();
const dataSourceOptions = [{ label: '全部', value: '' }];
const transOptions = ret.map((item) => ({ label: item, value: item }));
dataSourceOptions.push(...transOptions);
// 更新selectOptions
tableApi.formApi.updateSchema([
{
fieldName: 'dataName',
componentProps: {
options: dataSourceOptions,
},
},
]);
});
const [CodePreviewModal, previewModalApi] = useVbenModal({
connectedComponent: codePreviewModal,
});
function handlePreview(record: Recordable<any>) {
previewModalApi.setData({ tableId: record.tableId });
previewModalApi.open();
}
const router = useRouter();
function handleEdit(record: Recordable<any>) {
router.push(`/tool/gen-edit/index/${record.tableId}`);
}
async function handleSync(record: Recordable<any>) {
await syncDb(record.tableId);
await tableApi.query();
}
/**
* 批量生成代码
*/
async function handleBatchGen() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.tableId);
if (ids.length === 0) {
message.info('请选择需要生成代码的表');
return;
}
const hideLoading = message.loading('下载中...');
try {
const params = ids.join(',');
const data = await batchGenCode(params);
const timestamp = Date.now();
downloadByData(data, `批量代码生成_${timestamp}.zip`);
} finally {
hideLoading();
}
}
async function handleDownload(record: Recordable<any>) {
const hideLoading = message.loading('加载中...');
try {
// 路径生成
if (record.genType === '1' && record.genPath) {
await genWithPath(record.tableId);
message.success(`生成成功: ${record.genPath}`);
return;
}
// zip生成
const blob = await batchGenCode(record.tableId);
const filename = `代码生成_${record.tableName}_${dayjs().valueOf()}.zip`;
downloadByData(blob, filename);
} catch (error) {
console.error(error);
} finally {
hideLoading();
}
}
/**
* 删除
* @param record
*/
async function handleDelete(record: Recordable<any>) {
await genRemove(record.tableId);
await tableApi.query();
}
function handleMultiDelete() {
const rows = tableApi.grid.getCheckboxRecords();
const ids = rows.map((row: any) => row.tableId);
Modal.confirm({
title: '提示',
okType: 'danger',
content: `确认删除选中的${ids.length}条记录吗?`,
onOk: async () => {
await genRemove(ids);
await tableApi.query();
},
});
}
const [TableImportModal, tableImportModalApi] = useVbenModal({
connectedComponent: tableImportModal,
});
function handleImport() {
tableImportModalApi.open();
}
</script>
<template>
<Page :auto-content-height="true">
<BasicTable table-title="代码生成列表">
<template #toolbar-tools>
<a
class="text-primary mr-2"
href="https://dapdap.top/other/template.html"
target="_blank"
>👉关于代码生成模板
</a>
<Space>
<a-button
:disabled="!vxeCheckboxChecked(tableApi)"
danger
type="primary"
v-access:code="['tool:gen:remove']"
@click="handleMultiDelete"
>
{{ $t('pages.common.delete') }}
</a-button>
<a-button
:disabled="!vxeCheckboxChecked(tableApi)"
v-access:code="['tool:gen:code']"
@click="handleBatchGen"
>
{{ $t('pages.common.generate') }}
</a-button>
<a-button
type="primary"
v-access:code="['tool:gen:import']"
@click="handleImport"
>
{{ $t('pages.common.import') }}
</a-button>
</Space>
</template>
<template #action="{ row }">
<a-button
size="small"
type="link"
v-access:code="['tool:gen:preview']"
@click.stop="handlePreview(row)"
>
{{ $t('pages.common.preview') }}
</a-button>
<a-button
size="small"
type="link"
v-access:code="['tool:gen:edit']"
@click.stop="handleEdit(row)"
>
{{ $t('pages.common.edit') }}
</a-button>
<Popconfirm
:get-popup-container="getVxePopupContainer"
:title="`确认同步[${row.tableName}]?`"
placement="left"
@confirm="handleSync(row)"
>
<a-button
size="small"
type="link"
v-access:code="['tool:gen:edit']"
@click.stop=""
>
{{ $t('pages.common.sync') }}
</a-button>
</Popconfirm>
<a-button
size="small"
type="link"
v-access:code="['tool:gen:code']"
@click.stop="handleDownload(row)"
>
生成代码
</a-button>
<Popconfirm
:get-popup-container="getVxePopupContainer"
:title="`确认删除[${row.tableName}]?`"
placement="left"
@confirm="handleDelete(row)"
>
<a-button
danger
size="small"
type="link"
v-access:code="['tool:gen:remove']"
@click.stop=""
>
{{ $t('pages.common.delete') }}
</a-button>
</Popconfirm>
</template>
</BasicTable>
<CodePreviewModal />
<TableImportModal @reload="tableApi.query()" />
</Page>
</template>

View File

@@ -0,0 +1,143 @@
<script setup lang="ts">
import type { VbenFormProps } from '@vben/common-ui';
import type { VxeGridProps } from '#/adapter/vxe-table';
import { useVbenModal } from '@vben/common-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
getDataSourceNames,
importTable,
readyToGenList,
} from '#/api/tool/gen';
const emit = defineEmits<{ reload: [] }>();
const formOptions: VbenFormProps = {
schema: [
{
label: '数据源',
fieldName: 'dataName',
component: 'Select',
defaultValue: 'master',
},
{
label: '表名称',
fieldName: 'tableName',
component: 'Input',
},
{
label: '表描述',
fieldName: 'tableComment',
component: 'Input',
},
],
commonConfig: {
labelWidth: 60,
},
showCollapseButton: false,
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
};
const gridOptions: VxeGridProps = {
checkboxConfig: {
highlight: true,
reserve: true,
trigger: 'row',
},
columns: [
{
type: 'checkbox',
width: 60,
},
{
title: '表名称',
field: 'tableName',
align: 'left',
},
{
title: '表描述',
field: 'tableComment',
align: 'left',
},
{
title: '创建时间',
field: 'createTime',
},
{
title: '更新时间',
field: 'updateTime',
},
],
keepSource: true,
size: 'small',
minHeight: 400,
pagerConfig: {},
proxyConfig: {
ajax: {
query: async ({ page }, formValues = {}) => {
return await readyToGenList({
pageNum: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'tableId',
},
toolbarConfig: {
enabled: false,
},
};
const [BasicTable, tableApi] = useVbenVxeGrid({ formOptions, gridOptions });
const [BasicModal, modalApi] = useVbenModal({
onOpenChange: async (isOpen) => {
if (!isOpen) {
tableApi.grid.clearCheckboxRow();
return null;
}
const ret = await getDataSourceNames();
const dataSourceOptions = ret.map((item) => ({ label: item, value: item }));
tableApi.formApi.updateSchema([
{
fieldName: 'dataName',
componentProps: {
options: dataSourceOptions,
},
},
]);
},
onConfirm: handleSubmit,
});
async function handleSubmit() {
try {
const records = tableApi.grid.getCheckboxRecords();
const tables = records.map((item) => item.tableName);
if (tables.length === 0) {
modalApi.close();
return;
}
modalApi.modalLoading(true);
const { dataName } = await tableApi.formApi.getValues();
await importTable(tables.join(','), dataName);
emit('reload');
modalApi.close();
} catch (error) {
console.warn(error);
} finally {
modalApi.modalLoading(false);
}
}
</script>
<template>
<BasicModal class="w-[800px]" title="导入表">
<BasicTable />
</BasicModal>
</template>