Explorar el Código

【功能新增】INFRA:代码生成 vben5 antd 模版 50%

puhui999 hace 6 meses
padre
commit
c26a46da66
Se han modificado 11 ficheros con 1591 adiciones y 0 borrados
  1. 1 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java
  2. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
  3. 151 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/api/api.ts.vm
  4. 0 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/index.vue.vm
  5. 300 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm
  6. 367 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/index.vue.vm
  7. 204 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_erp.vue.vm
  8. 2 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_inner.vue.vm
  9. 362 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_normal.vue.vm
  10. 183 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/list_sub_erp.vue.vm
  11. 4 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/list_sub_inner.vue.vm

+ 1 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenFrontTypeEnum.java

@@ -16,6 +16,7 @@ public enum CodegenFrontTypeEnum {
     VUE3_ELEMENT_PLUS(20), // Vue3 Element Plus 标准模版
     VUE3_VBEN2_ANTD_SCHEMA(30), // Vue3 VBEN2 + ANTD + Schema 模版
     VUE3_VBEN5_ANTD_SCHEMA(40), // Vue3 VBEN5 + ANTD + schema 模版
+    VUE3_VBEN5_ANTD(50), // Vue3 VBEN5 + ANTD 模版
     ;
 
     /**

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java

@@ -163,6 +163,23 @@ public class CodegenEngine {
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
             .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/list_sub_erp.vue"),  // 特殊:主子表专属逻辑
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
+            // VUE3_VBEN5_ANTD
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/index.vue"),
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"),
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/form.vue"))
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"),  // 特殊:主子表专属逻辑
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue"))
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_inner.vue"),  // 特殊:主子表专属逻辑
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue"))
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_erp.vue"),  // 特殊:主子表专属逻辑
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue"))
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_inner.vue"),  // 特殊:主子表专属逻辑
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_erp.vue"),  // 特殊:主子表专属逻辑
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue"))
+            .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"),
+                    vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
             .build();
 
     @Resource

+ 151 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/api/api.ts.vm

@@ -0,0 +1,151 @@
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}")
+
+export namespace ${simpleClassName}Api {
+  ## 特殊:主子表专属逻辑
+  #foreach ($subTable in $subTables)
+    #set ($index = $foreach.count - 1)
+    #set ($subSimpleClassName = $subSimpleClassNames.get($index))
+    #set ($subColumns = $subColumnsList.get($index))##当前字段数组
+    /** ${subTable.classComment}信息 */
+    export interface ${subSimpleClassName} {
+      #foreach ($column in $subColumns)
+        #if ($column.createOperation || $column.updateOperation)
+          #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
+              ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
+          #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
+              ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: Date; // ${column.columnComment}
+          #else
+              ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
+          #end
+        #end
+      #end
+    }
+
+  #end
+  /** ${table.classComment}信息 */
+  export interface ${simpleClassName} {
+    #foreach ($column in $columns)
+      #if ($column.createOperation || $column.updateOperation)
+        #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal")
+            ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: number; // ${column.columnComment}
+        #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime")
+            ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: Date; // ${column.columnComment}
+        #else
+            ${column.javaField}#if($column.updateOperation && !$column.primaryKey && !$column.nullable)?#end: ${column.javaType.toLowerCase()}; // ${column.columnComment}
+        #end
+      #end
+    #end
+    #if ( $table.templateType == 2 )
+      children?: ${simpleClassName}[];
+    #end
+    ## 特殊:主子表专属逻辑
+    #if ( $table.templateType == 10 || $table.templateType == 12 )
+      #foreach ($subTable in $subTables)
+        #set ($index = $foreach.count - 1)
+        #set ($subSimpleClassName = $subSimpleClassNames.get($index))
+        #if ( $subTable.subJoinMany )
+            ${subSimpleClassName.toLowerCase()}s?: ${subSimpleClassName}[]
+        #else
+            ${subSimpleClassName.toLowerCase()}?: ${subSimpleClassName}
+        #end
+      #end
+    #end
+  }
+}
+
+#if ( $table.templateType != 2 )
+/** 查询${table.classComment}分页 */
+export function get${simpleClassName}Page(params: PageParam) {
+  return requestClient.get<PageResult<${simpleClassName}Api.${simpleClassName}>>('${baseURL}/page', { params });
+}
+#else
+/** 查询${table.classComment}列表 */
+export function get${simpleClassName}List(params: any) {
+  return requestClient.get<${simpleClassName}Api.${simpleClassName}[]>('${baseURL}/list', { params });
+}
+#end
+
+/** 查询${table.classComment}详情 */
+export function get${simpleClassName}(id: number) {
+  return requestClient.get<${simpleClassName}Api.${simpleClassName}>(`${baseURL}/get?id=${id}`);
+}
+
+/** 新增${table.classComment} */
+export function create${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
+  return requestClient.post('${baseURL}/create', data);
+}
+
+/** 修改${table.classComment} */
+export function update${simpleClassName}(data: ${simpleClassName}Api.${simpleClassName}) {
+  return requestClient.put('${baseURL}/update', data);
+}
+
+/** 删除${table.classComment} */
+export function delete${simpleClassName}(id: number) {
+  return requestClient.delete(`${baseURL}/delete?id=${id}`);
+}
+
+/** 导出${table.classComment} */
+export function export${simpleClassName}(params: any) {
+  return requestClient.download('${baseURL}/export-excel', params);
+}
+
+## 特殊:主子表专属逻辑
+#foreach ($subTable in $subTables)
+  #set ($index = $foreach.count - 1)
+  #set ($subSimpleClassName = $subSimpleClassNames.get($index))
+  #set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段
+  #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段
+  #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
+  #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
+  #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
+  #set ($subClassNameVar = $subClassNameVars.get($index))
+
+// ==================== 子表($subTable.classComment) ====================
+
+  ## 情况一:MASTER_ERP 时,需要分查询页子表
+  #if ( $table.templateType == 11 )
+  /** 获得${subTable.classComment}分页 */
+  export function get${subSimpleClassName}Page(params: PageParam) {
+    return requestClient.get<PageResult<${simpleClassName}Api.${subSimpleClassName}>>(`${baseURL}/${subSimpleClassName_strikeCase}/page`, { params });
+  }
+    ## 情况二:非 MASTER_ERP 时,需要列表查询子表
+  #else
+    #if ( $subTable.subJoinMany )
+    /** 获得${subTable.classComment}列表 */
+    export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
+      return requestClient.get<${simpleClassName}Api.${subSimpleClassName}[]>(`${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
+    }
+    #else
+    /** 获得${subTable.classComment} */
+    export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: number) {
+      return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=${${subJoinColumn.javaField}}`);
+    }
+    #end
+  #end
+  ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作
+  #if ( $table.templateType == 11 )
+  /** 新增${subTable.classComment} */
+  export function create${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
+    return requestClient.post(`${baseURL}/${subSimpleClassName_strikeCase}/create`, data);
+  }
+
+  /** 修改${subTable.classComment} */
+  export function update${subSimpleClassName}(data: ${simpleClassName}Api.${subSimpleClassName}) {
+    return requestClient.put(`${baseURL}/${subSimpleClassName_strikeCase}/update`, data);
+  }
+
+  /** 删除${subTable.classComment} */
+  export function delete${subSimpleClassName}(id: number) {
+    return requestClient.delete(`${baseURL}/${subSimpleClassName_strikeCase}/delete?id=${id}`);
+  }
+
+  /** 获得${subTable.classComment} */
+  export function get${subSimpleClassName}(id: number) {
+    return requestClient.get<${simpleClassName}Api.${subSimpleClassName}>(`${baseURL}/${subSimpleClassName_strikeCase}/get?id=${id}`);
+  }
+  #end
+#end

+ 0 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/index.vue.vm


+ 300 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/form.vue.vm

@@ -0,0 +1,300 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <Form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-col="{ span: 6 }"
+      :loading="formLoading"
+    >
+#foreach($column in $columns)
+    #if ($column.createOperation || $column.updateOperation)
+        #set ($dictType = $column.dictType)
+        #set ($javaField = $column.javaField)
+        #set ($javaType = $column.javaType)
+        #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+        #set ($comment = $column.columnComment)
+        #set ($dictMethod = "getDictOptions")## 计算使用哪个 dict 字典方法
+        #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+            #set ($dictMethod = "getIntDictOptions")
+        #elseif ($javaType == "String")
+            #set ($dictMethod = "getStrDictOptions")
+        #elseif ($javaType == "Boolean")
+            #set ($dictMethod = "getBoolDictOptions")
+        #end
+        #if ( $table.templateType == 2 && $column.id == $treeParentColumn.id )
+      <FormItem label="${comment}" name="${javaField}">
+        <TreeSelect
+          v-model:value="formData.${javaField}"
+          :treeData="${classNameVar}Tree"
+          #if ($treeNameColumn.javaField == "name")
+          :fieldNames="defaultProps"
+          #else
+          :fieldNames="{...defaultProps, label: '$treeNameColumn.javaField'}"
+          #end
+          checkable
+          treeDefaultExpandAll
+          placeholder="请选择${comment}"
+        />
+      </FormItem>
+        #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+      <FormItem label="${comment}" name="${javaField}">
+        <Input v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
+      </FormItem>
+        #elseif($column.htmlType == "imageUpload")## 图片上传
+      <FormItem label="${comment}" name="${javaField}">
+        <UploadImg v-model:value="formData.${javaField}" />
+      </FormItem>
+        #elseif($column.htmlType == "fileUpload")## 文件上传
+      <FormItem label="${comment}" name="${javaField}">
+        <UploadFile v-model:value="formData.${javaField}" />
+      </FormItem>
+        #elseif($column.htmlType == "editor")## 文本编辑器
+      <FormItem label="${comment}" name="${javaField}">
+        <Editor v-model:value="formData.${javaField}" height="150px" />
+      </FormItem>
+        #elseif($column.htmlType == "select")## 下拉框
+      <FormItem label="${comment}" name="${javaField}">
+        <Select v-model:value="formData.${javaField}" placeholder="请选择${comment}">
+                #if ("" != $dictType)## 有数据字典
+          <SelectOption
+            v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+                #else##没数据字典
+          <SelectOption label="请选择字典生成" value="" />
+                #end
+        </Select>
+      </FormItem>
+        #elseif($column.htmlType == "checkbox")## 多选框
+      <FormItem label="${comment}" name="${javaField}">
+        <CheckboxGroup v-model:value="formData.${javaField}">
+                #if ("" != $dictType)## 有数据字典
+          <Checkbox
+            v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+                #else##没数据字典
+          <Checkbox label="请选择字典生成" />
+                #end
+        </CheckboxGroup>
+      </FormItem>
+        #elseif($column.htmlType == "radio")## 单选框
+      <FormItem label="${comment}" name="${javaField}">
+        <RadioGroup v-model:value="formData.${javaField}">
+                #if ("" != $dictType)## 有数据字典
+          <Radio
+            v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+            :key="dict.value"
+            :value="dict.value"
+          >
+            {{ dict.label }}
+          </Radio>
+                #else##没数据字典
+          <Radio value="1">请选择字典生成</Radio>
+                #end
+        </RadioGroup>
+      </FormItem>
+        #elseif($column.htmlType == "datetime")## 时间框
+      <FormItem label="${comment}" name="${javaField}">
+        <DatePicker
+          v-model:value="formData.${javaField}"
+          valueFormat="x"
+          placeholder="选择${comment}"
+        />
+      </FormItem>
+        #elseif($column.htmlType == "textarea")## 文本框
+      <FormItem label="${comment}" name="${javaField}">
+        <Textarea v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
+      </FormItem>
+        #end
+    #end
+#end
+    </Form>
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 10 || $table.templateType == 12 )
+    <!-- 子表的表单 -->
+    <Tabs v-model:activeKey="subTabsName">
+    #foreach ($subTable in $subTables)
+      #set ($index = $foreach.count - 1)
+      #set ($subClassNameVar = $subClassNameVars.get($index))
+      #set ($subSimpleClassName = $subSimpleClassNames.get($index))
+      #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
+      <TabPane key="$subClassNameVar" tab="${subTable.classComment}">
+        <${subSimpleClassName}Form ref="${subClassNameVar}FormRef" :${subJoinColumn_strikeCase}="formData.id" />
+      </TabPane>
+    #end
+    </Tabs>
+#end
+    <template #footer>
+      <Button @click="submitForm" type="primary" :loading="formLoading">确 定</Button>
+      <Button @click="dialogVisible = false">取 消</Button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { Form, FormItem, Input, Button, Select, SelectOption, DatePicker, Textarea, Checkbox, CheckboxGroup, Radio, RadioGroup, Tabs, TabPane, TreeSelect } from 'ant-design-vue'
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ${simpleClassName}Api, ${simpleClassName}VO } from '@/api/${table.moduleName}/${table.businessName}'
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+import { defaultProps, handleTree } from '@/utils/tree'
+#end
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 10 || $table.templateType == 12 )
+#foreach ($subSimpleClassName in $subSimpleClassNames)
+import ${subSimpleClassName}Form from './components/${subSimpleClassName}Form.vue'
+#end
+#end
+
+/** ${table.classComment} 表单 */
+defineOptions({ name: '${simpleClassName}Form' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+#foreach ($column in $columns)
+    #if ($column.createOperation || $column.updateOperation)
+      #if ($column.htmlType == "checkbox")
+  $column.javaField: [],
+      #else
+  $column.javaField: undefined,
+      #end
+    #end
+#end
+})
+const formRules = reactive({
+#foreach ($column in $columns)
+    #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
+        #set($comment=$column.columnComment)
+  $column.javaField: [{ required: true, message: '${comment}不能为空', trigger: #if($column.htmlType == 'select')'change'#else'blur'#end }],
+    #end
+#end
+})
+const formRef = ref() // 表单 Ref
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+const ${classNameVar}Tree = ref([]) // 树形结构
+#end
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 10 || $table.templateType == 12 )
+#if ( $subTables && $subTables.size() > 0 )
+
+/** 子表的表单 */
+const subTabsName = ref('$subClassNameVars.get(0)')
+#foreach ($subClassNameVar in $subClassNameVars)
+const ${subClassNameVar}FormRef = ref()
+#end
+#end
+#end
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ${simpleClassName}Api.get${simpleClassName}(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+  await get${simpleClassName}Tree()
+#end
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 10 || $table.templateType == 12 )
+#if ( $subTables && $subTables.size() > 0 )
+  // 校验子表单
+  #foreach ($subTable in $subTables)
+  #set ($index = $foreach.count - 1)
+  #set ($subClassNameVar = $subClassNameVars.get($index))
+  try {
+    await ${subClassNameVar}FormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = '${subClassNameVar}'
+    return
+  }
+  #end
+#end
+#end
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as ${simpleClassName}VO
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 10 || $table.templateType == 12 )
+#if ( $subTables && $subTables.size() > 0 )
+    // 拼接子表的数据
+  #foreach ($subTable in $subTables)
+  #set ($index = $foreach.count - 1)
+  #set ($subClassNameVar = $subClassNameVars.get($index))
+    data.${subClassNameVar}#if ( $subTable.subJoinMany)s#end = ${subClassNameVar}FormRef.value.getData()
+  #end
+#end
+#end
+    if (formType.value === 'create') {
+      await ${simpleClassName}Api.create${simpleClassName}(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ${simpleClassName}Api.update${simpleClassName}(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+#foreach ($column in $columns)
+  #if ($column.createOperation || $column.updateOperation)
+      #if ($column.htmlType == "checkbox")
+    $column.javaField: [],
+      #else
+    $column.javaField: undefined,
+      #end
+  #end
+#end
+  }
+  formRef.value?.resetFields()
+}
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+
+/** 获得${table.classComment}树 */
+const get${simpleClassName}Tree = async () => {
+  ${classNameVar}Tree.value = []
+  const data = await ${simpleClassName}Api.get${simpleClassName}List()
+  const root = { id: 0, name: '顶级${table.classComment}', children: [] }
+  root.children = handleTree(data, 'id', '${treeParentColumn.javaField}')
+  ${classNameVar}Tree.value.push(root)
+}
+#end
+</script>

+ 367 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/index.vue.vm

@@ -0,0 +1,367 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <Form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      layout="inline"
+      label-col="68px"
+    >
+    #foreach($column in $columns)
+        #if ($column.listOperation)
+            #set ($dictType = $column.dictType)
+            #set ($javaField = $column.javaField)
+            #set ($javaType = $column.javaType)
+            #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+            #set ($comment = $column.columnComment)
+            #set ($dictMethod = "getDictOptions")## 计算使用哪个 dict 字典方法
+            #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+                #set ($dictMethod = "getIntDictOptions")
+            #elseif ($javaType == "String")
+                #set ($dictMethod = "getStrDictOptions")
+            #elseif ($javaType == "Boolean")
+                #set ($dictMethod = "getBoolDictOptions")
+            #end
+            #if ($column.htmlType == "input")
+      <FormItem label="${comment}" name="${javaField}">
+        <Input
+          v-model:value="queryParams.${javaField}"
+          placeholder="请输入${comment}"
+          allowClear
+          @pressEnter="handleQuery"
+          class="!w-240px"
+        />
+      </FormItem>
+            #elseif ($column.htmlType == "select" || $column.htmlType == "radio")
+      <FormItem label="${comment}" name="${javaField}">
+        <Select
+          v-model:value="queryParams.${javaField}"
+          placeholder="请选择${comment}"
+          allowClear
+          class="!w-240px"
+        >
+                #if ("" != $dictType)## 设置了 dictType 数据字典的情况
+          <SelectOption
+            v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+                #else## 未设置 dictType 数据字典的情况
+          <SelectOption label="请选择字典生成" value="" />
+                #end
+        </Select>
+      </FormItem>
+    #elseif($column.htmlType == "datetime")
+      #if ($column.listOperationCondition != "BETWEEN")## 非范围
+      <FormItem label="${comment}" name="${javaField}">
+        <DatePicker
+          v-model:value="queryParams.${javaField}"
+          valueFormat="YYYY-MM-DD"
+          placeholder="选择${comment}"
+          allowClear
+          class="!w-240px"
+        />
+      </FormItem>
+      #else## 范围
+      <FormItem label="${comment}" name="${javaField}">
+        <RangePicker
+          v-model:value="queryParams.${javaField}"
+          v-bind="getRangePickerDefaultProps()"
+          class="!w-220px"
+        />
+      </FormItem>
+      #end
+    #end
+    #end
+    #end
+      <FormItem>
+        <Button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</Button>
+        <Button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</Button>
+        <Button
+          type="primary"
+          ghost
+          @click="openForm('create')"
+          v-hasPermi="['${permissionPrefix}:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </Button>
+        <Button
+          type="success"
+          ghost
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['${permissionPrefix}:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </Button>
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+        <Button type="danger" ghost @click="toggleExpandAll">
+          <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
+        </Button>
+#end
+      </FormItem>
+    </Form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
+    <Table
+      :loading="loading"
+      :dataSource="list"
+      :bordered="true"
+      :rowClassName="() => 'editable-row'"
+      @change="handleCurrentChange"
+    >
+## 特殊:树表专属逻辑
+#elseif ( $table.templateType == 2 )
+    <Table
+      :loading="loading"
+      :dataSource="list"
+      :bordered="true"
+      rowKey="id"
+      :expandAllRows="isExpandAll"
+      v-if="refreshTable"
+    >
+#else
+    <Table :loading="loading" :dataSource="list" :bordered="true">
+#end
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 12 && $subTables && $subTables.size() > 0 )
+      <!-- 子表的列表 -->
+      <template #expandedRowRender="{ record }">
+        <Tabs defaultActiveKey="$subClassNameVars.get(0)">
+            #foreach ($subTable in $subTables)
+              #set ($index = $foreach.count - 1)
+              #set ($subClassNameVar = $subClassNameVars.get($index))
+              #set ($subSimpleClassName = $subSimpleClassNames.get($index))
+              #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
+            <TabPane key="$subClassNameVar" tab="${subTable.classComment}">
+              <${subSimpleClassName}List :${subJoinColumn_strikeCase}="record.id" />
+            </TabPane>
+            #end
+        </Tabs>
+      </template>
+#end
+      #foreach($column in $columns)
+      #if ($column.listOperationResult)
+        #set ($dictType=$column.dictType)
+        #set ($javaField = $column.javaField)
+        #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+        #set ($comment=$column.columnComment)
+        #if ($column.javaType == "LocalDateTime")## 时间类型
+      <Column
+        title="${comment}"
+        align="center"
+        dataIndex="${javaField}"
+        :customRender="({ text }) => dateFormatter(text)"
+        width="180px"
+      />
+        #elseif($column.dictType && "" != $column.dictType)## 数据字典
+      <Column title="${comment}" align="center" dataIndex="${javaField}">
+        <template #default="{ text, record }">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="record.${column.javaField}" />
+        </template>
+      </Column>
+        #else
+      <Column title="${comment}" align="center" dataIndex="${javaField}" />
+        #end
+      #end
+    #end
+      <Column title="操作" align="center" key="action" :width="120">
+        <template #default="{ record }">
+          <Button
+            type="link"
+            @click="openForm('update', record.id)"
+            v-hasPermi="['${permissionPrefix}:update']"
+          >
+            编辑
+          </Button>
+          <Button
+            type="link"
+            danger
+            @click="handleDelete(record.id)"
+            v-hasPermi="['${permissionPrefix}:delete']"
+          >
+            删除
+          </Button>
+        </template>
+      </Column>
+    </Table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:current="queryParams.pageNo"
+      v-model:pageSize="queryParams.pageSize"
+      @change="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <${simpleClassName}Form ref="formRef" @success="getList" />
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
+  <!-- 子表的列表 -->
+  <ContentWrap>
+    <Tabs defaultActiveKey="$subClassNameVars.get(0)">
+      #foreach ($subTable in $subTables)
+        #set ($index = $foreach.count - 1)
+        #set ($subClassNameVar = $subClassNameVars.get($index))
+        #set ($subSimpleClassName = $subSimpleClassNames.get($index))
+        #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
+      <TabPane key="$subClassNameVar" tab="${subTable.classComment}">
+        <${subSimpleClassName}List :${subJoinColumn_strikeCase}="currentRow.id" />
+      </TabPane>
+      #end
+    </Tabs>
+  </ContentWrap>
+#end
+</template>
+
+<script setup lang="ts">
+import { Form, FormItem, Input, Button, Table, Column, Select, SelectOption, DatePicker, RangePicker, Tabs, TabPane } from 'ant-design-vue'
+import dayjs from 'dayjs'
+import { getRangePickerDefaultProps } from '@vben/utils'
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+import { handleTree } from '@/utils/tree'
+#end
+import download from '@/utils/download'
+import { ${simpleClassName}Api, ${simpleClassName}VO } from '@/api/${table.moduleName}/${table.businessName}'
+import ${simpleClassName}Form from './${simpleClassName}Form.vue'
+## 特殊:主子表专属逻辑
+#if ( $table.templateType != 10 )
+#foreach ($subSimpleClassName in $subSimpleClassNames)
+import ${subSimpleClassName}List from './components/${subSimpleClassName}List.vue'
+#end
+#end
+
+/** ${table.classComment} 列表 */
+defineOptions({ name: '${table.className}' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<${simpleClassName}VO[]>([]) // 列表的数据
+## 特殊:树表专属逻辑(树不需要分页接口)
+#if ( $table.templateType != 2 )
+const total = ref(0) // 列表的总页数
+#end
+const queryParams = reactive({
+## 特殊:树表专属逻辑(树不需要分页接口)
+#if ( $table.templateType != 2 )
+  pageNo: 1,
+  pageSize: 10,
+#end
+  #foreach ($column in $columns)
+    #if ($column.listOperation)
+      #if ($column.listOperationCondition != 'BETWEEN')
+  $column.javaField: undefined,
+  #end
+      #if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
+  $column.javaField: [],
+      #end
+    #end
+  #end
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+## 特殊:树表专属逻辑(树不需要分页接口)
+  #if ( $table.templateType == 2 )
+    const data = await ${simpleClassName}Api.get${simpleClassName}List(queryParams)
+    list.value = handleTree(data, 'id', '${treeParentColumn.javaField}')
+  #else
+    const data = await ${simpleClassName}Api.get${simpleClassName}Page(queryParams)
+    list.value = data.list
+    total.value = data.total
+  #end
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ${simpleClassName}Api.delete${simpleClassName}(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await ${simpleClassName}Api.export${simpleClassName}(queryParams)
+    download.excel(data, '${table.classComment}.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 11 )
+
+/** 选中行操作 */
+const currentRow = ref({}) // 选中行
+const handleCurrentChange = (row) => {
+  currentRow.value = row
+}
+#end
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+
+/** 展开/折叠操作 */
+const isExpandAll = ref(true) // 是否展开,默认全部展开
+const refreshTable = ref(true) // 重新渲染表格状态
+const toggleExpandAll = async () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  await nextTick()
+  refreshTable.value = true
+}
+#end
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 204 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_erp.vue.vm

@@ -0,0 +1,204 @@
+#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
+#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <Form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-col="{ span: 6 }"
+      :loading="formLoading"
+    >
+#foreach($column in $subColumns)
+    #if ($column.createOperation || $column.updateOperation)
+        #set ($dictType = $column.dictType)
+        #set ($javaField = $column.javaField)
+        #set ($javaType = $column.javaType)
+        #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+        #set ($comment = $column.columnComment)
+        #set ($dictMethod = "getDictOptions")## 计算使用哪个 dict 字典方法
+        #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+            #set ($dictMethod = "getIntDictOptions")
+        #elseif ($javaType == "String")
+            #set ($dictMethod = "getStrDictOptions")
+        #elseif ($javaType == "Boolean")
+            #set ($dictMethod = "getBoolDictOptions")
+        #end
+        #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
+        #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+      <FormItem label="${comment}" name="${javaField}">
+        <Input v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
+      </FormItem>
+        #elseif($column.htmlType == "imageUpload")## 图片上传
+      <FormItem label="${comment}" name="${javaField}">
+        <UploadImg v-model:value="formData.${javaField}" />
+      </FormItem>
+        #elseif($column.htmlType == "fileUpload")## 文件上传
+      <FormItem label="${comment}" name="${javaField}">
+        <UploadFile v-model:value="formData.${javaField}" />
+      </FormItem>
+        #elseif($column.htmlType == "editor")## 文本编辑器
+      <FormItem label="${comment}" name="${javaField}">
+        <Editor v-model:value="formData.${javaField}" height="150px" />
+      </FormItem>
+        #elseif($column.htmlType == "select")## 下拉框
+      <FormItem label="${comment}" name="${javaField}">
+        <Select v-model:value="formData.${javaField}" placeholder="请选择${comment}">
+                #if ("" != $dictType)## 有数据字典
+          <SelectOption
+            v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+                #else##没数据字典
+          <SelectOption label="请选择字典生成" value="" />
+                #end
+        </Select>
+      </FormItem>
+        #elseif($column.htmlType == "checkbox")## 多选框
+      <FormItem label="${comment}" name="${javaField}">
+        <CheckboxGroup v-model:value="formData.${javaField}">
+                #if ("" != $dictType)## 有数据字典
+          <Checkbox
+            v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+                #else##没数据字典
+          <Checkbox label="请选择字典生成" />
+                #end
+        </CheckboxGroup>
+      </FormItem>
+        #elseif($column.htmlType == "radio")## 单选框
+      <FormItem label="${comment}" name="${javaField}">
+        <RadioGroup v-model:value="formData.${javaField}">
+                #if ("" != $dictType)## 有数据字典
+          <Radio
+            v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+            :key="dict.value"
+            :value="dict.value"
+          >
+            {{ dict.label }}
+          </Radio>
+                #else##没数据字典
+          <Radio value="1">请选择字典生成</Radio>
+                #end
+        </RadioGroup>
+      </FormItem>
+        #elseif($column.htmlType == "datetime")## 时间框
+      <FormItem label="${comment}" name="${javaField}">
+        <DatePicker
+          v-model:value="formData.${javaField}"
+          valueFormat="x"
+          placeholder="选择${comment}"
+        />
+      </FormItem>
+        #elseif($column.htmlType == "textarea")## 文本框
+      <FormItem label="${comment}" name="${javaField}">
+        <Textarea v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
+      </FormItem>
+        #end
+    #end
+#end
+    </Form>
+    <template #footer>
+      <Button @click="submitForm" type="primary" :loading="formLoading">确 定</Button>
+      <Button @click="dialogVisible = false">取 消</Button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { Form, FormItem, Input, Button, Select, SelectOption, DatePicker, Textarea, Checkbox, CheckboxGroup, Radio, RadioGroup } from 'ant-design-vue'
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ${simpleClassName}Api } from '@/api/${table.moduleName}/${table.businessName}'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+#foreach ($column in $subColumns)
+    #if ($column.createOperation || $column.updateOperation)
+      #if ($column.htmlType == "checkbox")
+  $column.javaField: [],
+      #else
+  $column.javaField: undefined,
+      #end
+    #end
+#end
+})
+const formRules = reactive({
+#foreach ($column in $subColumns)
+    #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
+        #set($comment=$column.columnComment)
+  $column.javaField: [{ required: true, message: '${comment}不能为空', trigger: #if($column.htmlType == 'select')'change'#else'blur'#end }],
+    #end
+#end
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, ${subJoinColumn.javaField}: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.${subJoinColumn.javaField} = ${subJoinColumn.javaField}
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ${simpleClassName}Api.get${subSimpleClassName}(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await ${simpleClassName}Api.create${subSimpleClassName}(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ${simpleClassName}Api.update${subSimpleClassName}(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+#foreach ($column in $subColumns)
+  #if ($column.createOperation || $column.updateOperation)
+      #if ($column.htmlType == "checkbox")
+    $column.javaField: [],
+      #else
+    $column.javaField: undefined,
+      #end
+  #end
+#end
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 2 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_inner.vue.vm

@@ -0,0 +1,2 @@
+## 主表的 normal 和 inner 使用相同的 form 表单
+#parse("codegen/vue3_vben5_antd/general/views/modules/form_sub_normal.vue.vm")

+ 362 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/form_sub_normal.vue.vm

@@ -0,0 +1,362 @@
+#set ($subTable = $subTables.get($subIndex))##当前表
+#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
+<template>
+#if ( $subTable.subJoinMany )## 情况一:一对多,table + form
+  <Form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    :loading="formLoading"
+    label-col="{ span: 0 }"
+  >
+    <Table :dataSource="formData" class="-mt-10px">
+      <Column title="序号" width="100">
+        <template #customRender="{ index }">
+          {{ index + 1 }}
+        </template>
+      </Column>
+#foreach($column in $subColumns)
+    #if ($column.createOperation || $column.updateOperation)
+        #set ($dictType = $column.dictType)
+        #set ($javaField = $column.javaField)
+        #set ($javaType = $column.javaType)
+        #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+        #set ($comment = $column.columnComment)
+        #set ($dictMethod = "getDictOptions")## 计算使用哪个 dict 字典方法
+        #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+            #set ($dictMethod = "getIntDictOptions")
+        #elseif ($javaType == "String")
+            #set ($dictMethod = "getStrDictOptions")
+        #elseif ($javaType == "Boolean")
+            #set ($dictMethod = "getBoolDictOptions")
+        #end
+        #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
+        #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+      <Column title="${comment}" :minWidth="150">
+        <template #customRender="{ record, index }">
+          <FormItem :name="[index, '${javaField}']" :rules="formRules.${javaField}" class="mb-0px!">
+            <Input v-model:value="record.${javaField}" placeholder="请输入${comment}" />
+          </FormItem>
+        </template>
+      </Column>
+        #elseif($column.htmlType == "imageUpload")## 图片上传
+      <Column title="${comment}" :minWidth="200">
+        <template #customRender="{ record, index }">
+          <FormItem :name="[index, '${javaField}']" :rules="formRules.${javaField}" class="mb-0px!">
+            <UploadImg v-model:value="record.${javaField}" />
+          </FormItem>
+        </template>
+      </Column>
+        #elseif($column.htmlType == "fileUpload")## 文件上传
+      <Column title="${comment}" :minWidth="200">
+        <template #customRender="{ record, index }">
+          <FormItem :name="[index, '${javaField}']" :rules="formRules.${javaField}" class="mb-0px!">
+            <UploadFile v-model:value="record.${javaField}" />
+          </FormItem>
+        </template>
+      </Column>
+        #elseif($column.htmlType == "editor")## 文本编辑器
+      <Column title="${comment}" :minWidth="400">
+        <template #customRender="{ record, index }">
+          <FormItem :name="[index, '${javaField}']" :rules="formRules.${javaField}" class="mb-0px!">
+            <Editor v-model:value="record.${javaField}" height="150px" />
+          </FormItem>
+        </template>
+      </Column>
+        #elseif($column.htmlType == "select")## 下拉框
+      <Column title="${comment}" :minWidth="150">
+        <template #customRender="{ record, index }">
+          <FormItem :name="[index, '${javaField}']" :rules="formRules.${javaField}" class="mb-0px!">
+            <Select v-model:value="record.${javaField}" placeholder="请选择${comment}">
+              #if ("" != $dictType)## 有数据字典
+                <SelectOption
+                  v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              #else##没数据字典
+                <SelectOption label="请选择字典生成" value="" />
+              #end
+            </Select>
+          </FormItem>
+        </template>
+      </Column>
+        #elseif($column.htmlType == "checkbox")## 多选框
+      <Column title="${comment}" :minWidth="150">
+        <template #customRender="{ record, index }">
+          <FormItem :name="[index, '${javaField}']" :rules="formRules.${javaField}" class="mb-0px!">
+            <CheckboxGroup v-model:value="record.${javaField}">
+              #if ("" != $dictType)## 有数据字典
+                <Checkbox
+                  v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              #else##没数据字典
+                <Checkbox label="请选择字典生成" />
+              #end
+            </CheckboxGroup>
+          </FormItem>
+        </template>
+      </Column>
+        #elseif($column.htmlType == "radio")## 单选框
+      <Column title="${comment}" :minWidth="150">
+        <template #customRender="{ record, index }">
+          <FormItem :name="[index, '${javaField}']" :rules="formRules.${javaField}" class="mb-0px!">
+            <RadioGroup v-model:value="record.${javaField}">
+              #if ("" != $dictType)## 有数据字典
+                <Radio
+                  v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+                  :key="dict.value"
+                  :value="dict.value"
+                >
+                  {{ dict.label }}
+                </Radio>
+              #else##没数据字典
+                <Radio value="1">请选择字典生成</Radio>
+              #end
+            </RadioGroup>
+          </FormItem>
+        </template>
+      </Column>
+        #elseif($column.htmlType == "datetime")## 时间框
+      <Column title="${comment}" :minWidth="150">
+        <template #customRender="{ record, index }">
+          <FormItem :name="[index, '${javaField}']" :rules="formRules.${javaField}" class="mb-0px!">
+            <DatePicker
+              v-model:value="record.${javaField}"
+              valueFormat="x"
+              placeholder="选择${comment}"
+            />
+          </FormItem>
+        </template>
+      </Column>
+        #elseif($column.htmlType == "textarea")## 文本框
+      <Column title="${comment}" :minWidth="200">
+        <template #customRender="{ record, index }">
+          <FormItem :name="[index, '${javaField}']" :rules="formRules.${javaField}" class="mb-0px!">
+            <Textarea v-model:value="record.${javaField}" placeholder="请输入${comment}" />
+          </FormItem>
+        </template>
+      </Column>
+        #end
+    #end
+#end
+      <Column align="center" fixed="right" title="操作" width="60">
+        <template #customRender="{ index }">
+          <Button @click="handleDelete(index)" type="link">—</Button>
+        </template>
+      </Column>
+    </Table>
+  </Form>
+  <Row justify="center" class="mt-3">
+    <Button @click="handleAdd" shape="round">+ 添加${subTable.classComment}</Button>
+  </Row>
+#else## 情况二:一对一,form
+  <Form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-col="{ span: 6 }"
+    :loading="formLoading"
+  >
+#foreach($column in $subColumns)
+  #if ($column.createOperation || $column.updateOperation)
+  #set ($dictType = $column.dictType)
+      #set ($javaField = $column.javaField)
+      #set ($javaType = $column.javaType)
+      #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+      #set ($comment = $column.columnComment)
+      #set ($dictMethod = "getDictOptions")## 计算使用哪个 dict 字典方法
+      #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+        #set ($dictMethod = "getIntDictOptions")
+      #elseif ($javaType == "String")
+          #set ($dictMethod = "getStrDictOptions")
+      #elseif ($javaType == "Boolean")
+          #set ($dictMethod = "getBoolDictOptions")
+      #end
+      #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
+      #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+    <FormItem label="${comment}" name="${javaField}">
+      <Input v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
+    </FormItem>
+      #elseif($column.htmlType == "imageUpload")## 图片上传
+    <FormItem label="${comment}" name="${javaField}">
+      <UploadImg v-model:value="formData.${javaField}" />
+    </FormItem>
+      #elseif($column.htmlType == "fileUpload")## 文件上传
+    <FormItem label="${comment}" name="${javaField}">
+      <UploadFile v-model:value="formData.${javaField}" />
+    </FormItem>
+      #elseif($column.htmlType == "editor")## 文本编辑器
+    <FormItem label="${comment}" name="${javaField}">
+      <Editor v-model:value="formData.${javaField}" height="150px" />
+    </FormItem>
+      #elseif($column.htmlType == "select")## 下拉框
+    <FormItem label="${comment}" name="${javaField}">
+      <Select v-model:value="formData.${javaField}" placeholder="请选择${comment}">
+              #if ("" != $dictType)## 有数据字典
+        <SelectOption
+          v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+              #else##没数据字典
+        <SelectOption label="请选择字典生成" value="" />
+              #end
+      </Select>
+    </FormItem>
+      #elseif($column.htmlType == "checkbox")## 多选框
+    <FormItem label="${comment}" name="${javaField}">
+      <CheckboxGroup v-model:value="formData.${javaField}">
+              #if ("" != $dictType)## 有数据字典
+        <Checkbox
+          v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+              #else##没数据字典
+        <Checkbox label="请选择字典生成" />
+              #end
+      </CheckboxGroup>
+    </FormItem>
+      #elseif($column.htmlType == "radio")## 单选框
+    <FormItem label="${comment}" name="${javaField}">
+      <RadioGroup v-model:value="formData.${javaField}">
+              #if ("" != $dictType)## 有数据字典
+        <Radio
+          v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+          :key="dict.value"
+          :value="dict.value"
+          >
+          {{ dict.label }}
+        </Radio>
+              #else##没数据字典
+        <Radio value="1">请选择字典生成</Radio>
+              #end
+      </RadioGroup>
+    </FormItem>
+      #elseif($column.htmlType == "datetime")## 时间框
+    <FormItem label="${comment}" name="${javaField}">
+      <DatePicker
+        v-model:value="formData.${javaField}"
+        valueFormat="x"
+        placeholder="选择${comment}"
+      />
+    </FormItem>
+      #elseif($column.htmlType == "textarea")## 文本框
+    <FormItem label="${comment}" name="${javaField}">
+      <Textarea v-model:value="formData.${javaField}" placeholder="请输入${comment}" />
+    </FormItem>
+      #end
+  #end
+#end
+  </Form>
+#end
+</template>
+<script setup lang="ts">
+import { Form, FormItem, Input, Button, Table, Column, Select, SelectOption, DatePicker, Textarea, Checkbox, CheckboxGroup, Radio, RadioGroup, Row } from 'ant-design-vue'
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ${simpleClassName}Api } from '@/api/${table.moduleName}/${table.businessName}'
+
+const props = defineProps<{
+  ${subJoinColumn.javaField}: undefined // ${subJoinColumn.columnComment}(主表的关联字段)
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+#foreach ($column in $subColumns)
+    #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
+        #set($comment=$column.columnComment)
+  $column.javaField: [{ required: true, message: '${comment}不能为空', trigger: #if($column.htmlType == 'select')'change'#else'blur'#end }],
+    #end
+#end
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.${subJoinColumn.javaField},
+  async (val) => {
+    // 1. 重置表单
+#if ( $subTable.subJoinMany )
+    formData.value = []
+#else
+    formData.value = {
+    #foreach ($column in $subColumns)
+      #if ($column.createOperation || $column.updateOperation)
+        #if ($column.htmlType == "checkbox")
+      $column.javaField: [],
+        #else
+      $column.javaField: undefined,
+        #end
+      #end
+    #end
+    }
+#end
+    // 2. val 非空,则加载数据
+    if (!val) {
+      return;
+    }
+    try {
+      formLoading.value = true
+#if ( $subTable.subJoinMany )
+      formData.value = await ${simpleClassName}Api.get${subSimpleClassName}ListBy${SubJoinColumnName}(val)
+#else
+      const data = await ${simpleClassName}Api.get${subSimpleClassName}By${SubJoinColumnName}(val)
+      if (!data) {
+        return
+      }
+      formData.value = data
+#end
+    } finally {
+      formLoading.value = false
+    }
+  },
+  { immediate: true }
+)
+#if ( $subTable.subJoinMany )
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  const row = {
+#foreach ($column in $subColumns)
+    #if ($column.createOperation || $column.updateOperation)
+      #if ($column.htmlType == "checkbox")
+    $column.javaField: [],
+      #else
+    $column.javaField: undefined,
+      #end
+  #end
+#end
+  }
+  row.${subJoinColumn.javaField} = props.${subJoinColumn.javaField}
+  formData.value.push(row)
+}
+
+/** 删除按钮操作 */
+const handleDelete = (index) => {
+  formData.value.splice(index, 1)
+}
+#end
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>

+ 183 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/list_sub_erp.vue.vm

@@ -0,0 +1,183 @@
+#set ($subTable = $subTables.get($subIndex))##当前表
+#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+#if ($table.templateType == 11)
+    <Button
+      type="primary"
+      @click="openForm('create')"
+      v-hasPermi="['${permissionPrefix}:create']"
+    >
+      <Icon icon="ep:plus" class="mr-5px" /> 新增
+    </Button>
+#end
+    <Table :loading="loading" :dataSource="list" :rowKey="(record) => record.id" :bordered="true">
+      #foreach($column in $subColumns)
+      #if ($column.listOperationResult)
+        #set ($dictType=$column.dictType)
+        #set ($javaField = $column.javaField)
+        #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+        #set ($comment=$column.columnComment)
+        #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
+        #elseif ($column.javaType == "LocalDateTime")## 时间类型
+      <Column
+        title="${comment}"
+        align="center"
+        dataIndex="${javaField}"
+        :customRender="({ text }) => dateFormatter(text)"
+        width="180px"
+      />
+        #elseif($column.dictType && "" != $column.dictType)## 数据字典
+      <Column title="${comment}" align="center" dataIndex="${javaField}">
+        <template #customRender="{ text }">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="text" />
+        </template>
+      </Column>
+        #else
+      <Column title="${comment}" align="center" dataIndex="${javaField}" />
+        #end
+      #end
+    #end
+    #if ($table.templateType == 11)
+      <Column title="操作" align="center">
+        <template #customRender="{ record }">
+          <Button
+            type="link"
+            @click="openForm('update', record.id)"
+            v-hasPermi="['${permissionPrefix}:update']"
+          >
+            编辑
+          </Button>
+          <Button
+            type="link"
+            danger
+            @click="handleDelete(record.id)"
+            v-hasPermi="['${permissionPrefix}:delete']"
+          >
+            删除
+          </Button>
+        </template>
+      </Column>
+    #end
+    </Table>
+    #if ($table.templateType == 11)
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:current="queryParams.pageNo"
+      v-model:pageSize="queryParams.pageSize"
+      @change="handleQuery"
+    />
+    #end
+  </ContentWrap>
+#if ($table.templateType == 11)
+    <!-- 表单弹窗:添加/修改 -->
+    <${subSimpleClassName}Form ref="formRef" @success="getList" />
+#end
+</template>
+<script setup lang="ts">
+import { Table, Column, Button, Pagination } from 'ant-design-vue'
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ${simpleClassName}Api } from '@/api/${table.moduleName}/${table.businessName}'
+#if ($table.templateType == 11)
+import ${subSimpleClassName}Form from './${subSimpleClassName}Form.vue'
+#end
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  ${subJoinColumn.javaField}?: number // ${subJoinColumn.columnComment}(主表的关联字段)
+}>()
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+#if ($table.templateType == 11)
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  ${subJoinColumn.javaField}: undefined as unknown
+})
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.${subJoinColumn.javaField},
+  (val: number) => {
+    if (!val) {
+      return
+    }
+    queryParams.${subJoinColumn.javaField} = val
+    handleQuery()
+  },
+    { immediate: true, deep: true }
+)
+#end
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+#if ($table.templateType == 11)
+    const data = await ${simpleClassName}Api.get${subSimpleClassName}Page(queryParams)
+    list.value = data.list
+    total.value = data.total
+#else
+  #if ( $subTable.subJoinMany )
+    list.value = await ${simpleClassName}Api.get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField})
+  #else
+    const data = await ${simpleClassName}Api.get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField})
+    if (!data) {
+      return
+    }
+    list.value.push(data)
+  #end
+#end
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+#if ($table.templateType == 11)
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  if (!props.${subJoinColumn.javaField}) {
+    message.error('请选择一个${table.classComment}')
+    return
+  }
+  formRef.value.open(type, id, props.${subJoinColumn.javaField})
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ${simpleClassName}Api.delete${subSimpleClassName}(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+#end
+#if ($table.templateType != 11)
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+#end
+</script>

+ 4 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben5_antd/general/views/modules/list_sub_inner.vue.vm

@@ -0,0 +1,4 @@
+## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点:
+## 1)inner 使用 list 不分页,erp 使用 page 分页
+## 2)erp 支持单个子表的新增、修改、删除,inner 不支持
+#parse("codegen/vue3_vben5_antd/general/views/modules/list_sub_erp.vue.vm")