feat: add naive infra

This commit is contained in:
xingyu4j
2025-10-16 18:04:17 +08:00
parent f6ad13b4f1
commit dd1528d45a
68 changed files with 9959 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
<script lang="ts" setup>
import type { InfraCodegenApi } from '#/api/infra/codegen';
import { watch } from 'vue';
import { useVbenForm } from '#/adapter/form';
import { useBasicInfoFormSchema } from '../data';
const props = defineProps<{
table: InfraCodegenApi.CodegenTable;
}>();
/** 表单实例 */
const [Form, formApi] = useVbenForm({
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-4', // 配置表单布局为两列
schema: useBasicInfoFormSchema(),
layout: 'horizontal',
showDefaultActions: false,
});
/** 动态更新表单值 */
watch(
() => props.table,
(val: any) => {
if (!val) {
return;
}
formApi.setValues(val);
},
{ immediate: true },
);
/** 暴露出表单校验方法和表单值获取方法 */
defineExpose({
validate: async () => {
const { valid } = await formApi.validate();
return valid;
},
getValues: formApi.getValues,
});
</script>
<template>
<Form />
</template>

View File

@@ -0,0 +1,153 @@
<script lang="ts" setup>
import type { SelectOption } from 'naive-ui';
import type { InfraCodegenApi } from '#/api/infra/codegen';
import { nextTick, onMounted, ref, watch } from 'vue';
import { NCheckbox, NInput, NSelect } from 'naive-ui';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSimpleDictTypeList } from '#/api/system/dict/type';
import { useCodegenColumnTableColumns } from '../data';
const props = defineProps<{
columns?: InfraCodegenApi.CodegenColumn[];
}>();
/** 表格配置 */
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: useCodegenColumnTableColumns(),
border: true,
showOverflow: true,
autoResize: true,
keepSource: true,
rowConfig: {
keyField: 'id',
isHover: true,
},
pagerConfig: {
enabled: false,
},
toolbarConfig: {
enabled: false,
},
},
});
/** 监听外部传入的列数据 */
watch(
() => props.columns,
async (columns) => {
if (!columns) {
return;
}
await nextTick();
gridApi.grid?.loadData(columns);
},
{
immediate: true,
},
);
/** 提供获取表格数据的方法供父组件调用 */
defineExpose({
getData: (): InfraCodegenApi.CodegenColumn[] => gridApi.grid.getData(),
});
/** 初始化 */
const dictTypeOptions = ref<SelectOption[]>([]); // 字典类型选项
async function initDictTypeOptions() {
const res = await getSimpleDictTypeList();
dictTypeOptions.value = res.map((item) => {
return {
label: item.name,
value: item.type,
};
});
}
onMounted(async () => {
await initDictTypeOptions();
});
</script>
<template>
<Grid>
<!-- 字段描述 -->
<template #columnComment="{ row }">
<NInput v-model:value="row.columnComment" />
</template>
<!-- Java 类型 -->
<template #javaType="{ row, column }">
<NSelect
v-model:value="row.javaType"
:options="column.params.options"
class="w-full"
/>
</template>
<!-- Java 属性 -->
<template #javaField="{ row }">
<NInput v-model:value="row.javaField" />
</template>
<!-- 插入 -->
<template #createOperation="{ row }">
<NCheckbox v-model:checked="row.createOperation" />
</template>
<!-- 编辑 -->
<template #updateOperation="{ row }">
<NCheckbox v-model:checked="row.updateOperation" />
</template>
<!-- 列表 -->
<template #listOperationResult="{ row }">
<NCheckbox v-model:checked="row.listOperationResult" />
</template>
<!-- 查询 -->
<template #listOperation="{ row }">
<NCheckbox v-model:checked="row.listOperation" />
</template>
<!-- 查询方式 -->
<template #listOperationCondition="{ row, column }">
<NSelect
v-model:value="row.listOperationCondition"
:options="column.params.options"
class="w-full"
/>
</template>
<!-- 允许空 -->
<template #nullable="{ row }">
<NCheckbox v-model:checked="row.nullable" />
</template>
<!-- 显示类型 -->
<template #htmlType="{ row, column }">
<NSelect
v-model:value="row.htmlType"
:options="column.params.options"
class="w-full"
/>
</template>
<!-- 字典类型 -->
<template #dictType="{ row }">
<NSelect
v-model:value="row.dictType"
:options="dictTypeOptions"
class="w-full"
allow-clear
show-search
/>
</template>
<!-- 示例 -->
<template #example="{ row }">
<NInput v-model:value="row.example" />
</template>
</Grid>
</template>

View File

@@ -0,0 +1,176 @@
<script lang="ts" setup>
import type { InfraCodegenApi } from '#/api/infra/codegen';
import { computed, ref, watch } from 'vue';
import { InfraCodegenTemplateTypeEnum } from '@vben/constants';
import { isEmpty } from '@vben/utils';
import { useVbenForm } from '#/adapter/form';
import { getCodegenTableList } from '#/api/infra/codegen';
import {
useGenerationInfoBaseFormSchema,
useGenerationInfoSubTableFormSchema,
useGenerationInfoTreeFormSchema,
} from '../data';
const props = defineProps<{
columns?: InfraCodegenApi.CodegenColumn[];
table?: InfraCodegenApi.CodegenTable;
}>();
const tables = ref<InfraCodegenApi.CodegenTable[]>([]);
/** 计算当前模板类型 */
const currentTemplateType = ref<number>();
const isTreeTable = computed(
() => currentTemplateType.value === InfraCodegenTemplateTypeEnum.TREE,
);
const isSubTable = computed(
() => currentTemplateType.value === InfraCodegenTemplateTypeEnum.SUB,
);
/** 基础表单实例 */
const [BaseForm, baseFormApi] = useVbenForm({
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-4', // 配置表单布局为两列
layout: 'horizontal',
showDefaultActions: false,
schema: useGenerationInfoBaseFormSchema(),
handleValuesChange: (values) => {
// 监听模板类型变化
if (
values.templateType !== undefined &&
values.templateType !== currentTemplateType.value
) {
currentTemplateType.value = values.templateType;
}
},
});
/** 树表信息表单实例 */
const [TreeForm, treeFormApi] = useVbenForm({
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-4', // 配置表单布局为两列
layout: 'horizontal',
showDefaultActions: false,
schema: [],
});
/** 主子表信息表单实例 */
const [SubForm, subFormApi] = useVbenForm({
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-4', // 配置表单布局为两列
layout: 'horizontal',
showDefaultActions: false,
schema: [],
});
/** 更新树表信息表单 schema */
function updateTreeSchema(): void {
treeFormApi.setState({
schema: useGenerationInfoTreeFormSchema(props.columns),
});
// 树表信息回显
treeFormApi.setValues(props.table as any);
}
/** 更新主子表信息表单 schema */
function updateSubSchema(): void {
subFormApi.setState({
schema: useGenerationInfoSubTableFormSchema(props.columns, tables.value),
});
// 主子表信息回显
subFormApi.setValues(props.table as any);
}
/** 获取合并的表单值 */
async function getAllFormValues(): Promise<Record<string, any>> {
// 基础表单值
const baseValues = await baseFormApi.getValues();
// 根据模板类型获取对应的额外表单值
let extraValues = {};
if (isTreeTable.value) {
extraValues = await treeFormApi.getValues();
} else if (isSubTable.value) {
extraValues = await subFormApi.getValues();
}
// 合并表单值
return { ...baseValues, ...extraValues };
}
/** 验证所有表单 */
async function validateAllForms() {
// 验证基础表单
const { valid: baseFormValid } = await baseFormApi.validate();
// 根据模板类型验证对应的额外表单
let extraValid = true;
if (isTreeTable.value) {
const { valid: treeFormValid } = await treeFormApi.validate();
extraValid = treeFormValid;
} else if (isSubTable.value) {
const { valid: subFormValid } = await subFormApi.validate();
extraValid = subFormValid;
}
return baseFormValid && extraValid;
}
/** 设置表单值 */
function setAllFormValues(values: Record<string, any>): void {
if (!values) {
return;
}
// 记录模板类型
currentTemplateType.value = values.templateType;
// 设置基础表单值
baseFormApi.setValues(values);
// 根据模板类型设置对应的额外表单值
if (isTreeTable.value) {
treeFormApi.setValues(values);
} else if (isSubTable.value) {
subFormApi.setValues(values);
}
}
/** 监听表格数据变化 */
watch(
() => props.table,
async (val) => {
if (!val || isEmpty(val)) {
return;
}
const table = val as InfraCodegenApi.CodegenTable;
// 初始化树表的 schema
updateTreeSchema();
// 设置表单值
setAllFormValues(table);
// 获取表数据,用于主子表选择
const dataSourceConfigId = table.dataSourceConfigId;
if (dataSourceConfigId === undefined) {
return;
}
tables.value = await getCodegenTableList(dataSourceConfigId);
// 初始化子表 schema
updateSubSchema();
},
{ immediate: true },
);
/** 暴露出表单校验方法和表单值获取方法 */
defineExpose({
validate: validateAllForms,
getValues: getAllFormValues,
});
</script>
<template>
<div>
<!-- 基础表单 -->
<BaseForm />
<!-- 树表信息表单 -->
<TreeForm v-if="isTreeTable" />
<!-- 主子表信息表单 -->
<SubForm v-if="isSubTable" />
</div>
</template>

View File

@@ -0,0 +1,118 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { InfraCodegenApi } from '#/api/infra/codegen';
import { reactive } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message } from '#/adapter/naive';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { createCodegenList, getSchemaTableList } from '#/api/infra/codegen';
import { $t } from '#/locales';
import {
useImportTableColumns,
useImportTableFormSchema,
} from '#/views/infra/codegen/data';
/** 定义组件事件 */
const emit = defineEmits<{
(e: 'success'): void;
}>();
const formData = reactive<InfraCodegenApi.CodegenCreateListReqVO>({
dataSourceConfigId: 0,
tableNames: [], // 已选择的表列表
});
/** 表格实例 */
const [Grid] = useVbenVxeGrid({
formOptions: {
schema: useImportTableFormSchema(),
submitOnChange: true,
},
gridOptions: {
columns: useImportTableColumns(),
height: 600,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
if (formValues.dataSourceConfigId === undefined) {
return [];
}
formData.dataSourceConfigId = formValues.dataSourceConfigId;
return await getSchemaTableList({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'name',
isHover: true,
},
toolbarConfig: {
enabled: false,
},
checkboxConfig: {
highlight: true,
range: true,
},
pagerConfig: {
enabled: false,
},
} as VxeTableGridOptions<InfraCodegenApi.DatabaseTable>,
gridEvents: {
checkboxChange: ({
records,
}: {
records: InfraCodegenApi.DatabaseTable[];
}) => {
formData.tableNames = records.map((item) => item.name);
},
},
});
/** 模态框实例 */
const [Modal, modalApi] = useVbenModal({
title: '导入表',
class: 'w-1/2',
async onConfirm() {
modalApi.lock();
// 1.1 获取表单值
if (formData?.dataSourceConfigId === undefined) {
message.error('请选择数据源');
return;
}
// 1.2 校验是否选择了表
if (formData.tableNames.length === 0) {
message.error('请选择需要导入的表');
return;
}
// 2. 提交请求
const hideLoading = message.loading('导入中...', {
duration: 0,
});
try {
await createCodegenList(formData);
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
hideLoading.destroy();
modalApi.unlock();
}
},
});
</script>
<template>
<Modal>
<Grid />
</Modal>
</template>

View File

@@ -0,0 +1,268 @@
<script lang="ts" setup>
import type { TreeOption, TreeOverrideNodeClickBehaviorReturn } from 'naive-ui';
import type { InfraCodegenApi } from '#/api/infra/codegen';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { CodeEditor } from '@vben/plugins/code-editor';
import { useClipboard } from '@vueuse/core';
import { NButton, NTabPane, NTabs, NTree } from 'naive-ui';
import { message } from '#/adapter/naive';
import { previewCodegen } from '#/api/infra/codegen';
/** 文件树类型 */
interface FileNode {
key: string;
title: string;
parentKey: string;
isLeaf?: boolean;
children?: FileNode[];
}
/** 组件状态 */
const loading = ref(false);
const fileTree = ref<FileNode[]>([]);
const previewFiles = ref<InfraCodegenApi.CodegenPreview[]>([]);
const activeKey = ref<string>('');
/** 代码地图 */
const codeMap = ref<Map<string, string>>(new Map<string, string>());
function setCodeMap(key: string, code: string) {
// 处理可能的缩进问题特别是对Java文件
const trimmedCode = code.trimStart();
// 如果已有缓存则不重新构建
if (codeMap.value.has(key)) {
return;
}
codeMap.value.set(key, trimmedCode);
}
/** 删除代码地图 */
function removeCodeMapKey(targetKey: any) {
// 只有一个代码视图时不允许删除
if (codeMap.value.size === 1) {
return;
}
if (codeMap.value.has(targetKey)) {
codeMap.value.delete(targetKey);
}
}
/** 复制代码 */
async function copyCode() {
const { copy } = useClipboard();
const file = previewFiles.value.find(
(item) => item.filePath === activeKey.value,
);
if (file) {
await copy(file.code);
message.success('复制成功');
}
}
/** 文件节点点击事件 */
function handleNodeClick({
option,
}: {
option: TreeOption;
}): TreeOverrideNodeClickBehaviorReturn {
if (!option.isLeaf) {
return 'default';
}
activeKey.value = option.key?.toString() || '';
const file = previewFiles.value.find((item) => {
const list = activeKey.value.split('.');
// 特殊处理 - 包合并
if (list.length > 2) {
const lang = list.pop();
return item.filePath === `${list.join('/')}.${lang}`;
}
return item.filePath === activeKey.value;
});
if (!file) {
return 'default';
}
setCodeMap(activeKey.value, file.code);
return 'default';
}
/** 处理文件树 */
function handleFiles(data: InfraCodegenApi.CodegenPreview[]): FileNode[] {
const exists: Record<string, boolean> = {};
const files: FileNode[] = [];
// 处理文件路径
for (const item of data) {
const paths = item.filePath.split('/');
let cursor = 0;
let fullPath = '';
while (cursor < paths.length) {
const path = paths[cursor] || '';
const oldFullPath = fullPath;
// 处理 Java 包路径特殊情况
if (path === 'java' && cursor + 1 < paths.length) {
fullPath = fullPath ? `${fullPath}/${path}` : path;
cursor++;
// 合并包路径
let packagePath = '';
while (cursor < paths.length) {
const nextPath = paths[cursor] || '';
if (
[
'controller',
'convert',
'dal',
'dataobject',
'enums',
'mysql',
'service',
'vo',
].includes(nextPath)
) {
break;
}
packagePath = packagePath ? `${packagePath}.${nextPath}` : nextPath;
cursor++;
}
if (packagePath) {
const newFullPath = `${fullPath}/${packagePath}`;
if (!exists[newFullPath]) {
exists[newFullPath] = true;
files.push({
key: newFullPath,
title: packagePath,
parentKey: oldFullPath || '/',
isLeaf: cursor === paths.length,
});
}
fullPath = newFullPath;
}
continue;
}
// 处理普通路径
fullPath = fullPath ? `${fullPath}/${path}` : path;
if (!exists[fullPath]) {
exists[fullPath] = true;
files.push({
key: fullPath,
title: path,
parentKey: oldFullPath || '/',
isLeaf: cursor === paths.length - 1,
});
}
cursor++;
}
}
/** 构建树形结构 */
function buildTree(parentKey: string): FileNode[] {
return files
.filter((file) => file.parentKey === parentKey)
.map((file) => ({
...file,
children: buildTree(file.key),
}));
}
return buildTree('/');
}
/** 模态框实例 */
const [Modal, modalApi] = useVbenModal({
footer: false,
fullscreen: true,
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
// 关闭时清除代码视图缓存
codeMap.value.clear();
return;
}
const row = modalApi.getData<InfraCodegenApi.CodegenTable>();
if (!row) {
return;
}
// 加载预览数据
loading.value = true;
try {
const data = await previewCodegen(row.id);
previewFiles.value = data;
// 构建代码树,并默认选中第一个文件
fileTree.value = handleFiles(data);
if (data.length > 0) {
activeKey.value = data[0]?.filePath || '';
const code = data[0]?.code || '';
setCodeMap(activeKey.value, code);
}
} finally {
loading.value = false;
}
},
});
</script>
<template>
<Modal title="代码预览">
<div class="flex h-full" v-loading="loading">
<!-- 文件树 -->
<div
class="h-full w-1/3 overflow-auto border-r border-gray-200 pr-4 dark:border-gray-700"
>
<NTree
v-if="fileTree.length > 0"
:data="fileTree"
default-expand-all
key-field="key"
label-field="title"
children-field="children"
:checked-keys="[activeKey]"
:override-default-node-click-behavior="handleNodeClick"
/>
</div>
<!-- 代码预览 -->
<div class="h-full w-2/3 overflow-auto pl-4">
<NTabs v-model:value="activeKey" type="card" @close="removeCodeMapKey">
<NTabPane
v-for="key in codeMap.keys()"
:name="key"
:key="key"
:tab="key.split('/').pop()"
>
<div
class="h-full rounded-md bg-gray-50 !p-0 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
>
<CodeEditor
class="max-h-200"
:value="codeMap.get(activeKey)"
mode="application/json"
:readonly="true"
:bordered="true"
:auto-format="false"
/>
</div>
</NTabPane>
<template #suffix>
<NButton type="primary" ghost @click="copyCode">
<IconifyIcon icon="lucide:copy" />
复制代码
</NButton>
</template>
</NTabs>
</div>
</div>
</Modal>
</template>