Bladeren bron

!734 代码生成:重构 vue2 代码生成模版,使用 async await 优化代码层次,适配树表和主子表
Merge pull request !734 from puhui999/feature/sub-table

芋道源码 1 jaar geleden
bovenliggende
commit
12bcee2a72
100 gewijzigde bestanden met toevoegingen van 9458 en 785 verwijderingen
  1. 17 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
  2. 97 5
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm
  3. 205 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm
  4. 2 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_inner.vue.vm
  5. 347 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm
  6. 165 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm
  7. 4 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_inner.vue.vm
  8. 320 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/form.vue.vm
  9. 171 199
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm
  10. 266 266
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm
  11. 317 314
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm
  12. 202 0
      yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineVue2Test.java
  13. 73 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/assert.json
  14. 6 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/ErrorCodeConstants_手动操作
  15. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentContactDO
  16. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentContactMapper
  17. 183 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentController
  18. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentDO
  19. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentMapper
  20. 34 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentPageReqVO
  21. 60 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentRespVO
  22. 52 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentSaveReqVO
  23. 139 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentService
  24. 180 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentServiceImpl
  25. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentServiceImplTest
  26. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentTeacherDO
  27. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentTeacherMapper
  28. 141 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/js/student
  29. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/sql/h2
  30. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/sql/sql
  31. 159 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentContactForm
  32. 134 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentContactList
  33. 164 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentForm
  34. 159 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentTeacherForm
  35. 134 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentTeacherList
  36. 241 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/index
  37. 12 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/xml/InfraStudentMapper
  38. 73 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/assert.json
  39. 3 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/ErrorCodeConstants_手动操作
  40. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentContactDO
  41. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentContactMapper
  42. 117 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentController
  43. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentDO
  44. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentMapper
  45. 34 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentPageReqVO
  46. 60 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentRespVO
  47. 58 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentSaveReqVO
  48. 77 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentService
  49. 147 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentServiceImpl
  50. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentServiceImplTest
  51. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentTeacherDO
  52. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentTeacherMapper
  53. 74 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/js/student
  54. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/sql/h2
  55. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/sql/sql
  56. 176 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentContactForm
  57. 91 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentContactList
  58. 221 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentForm
  59. 126 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentTeacherForm
  60. 95 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentTeacherList
  61. 230 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/index
  62. 12 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/xml/InfraStudentMapper
  63. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/assert.json
  64. 3 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/ErrorCodeConstants_手动操作
  65. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentContactDO
  66. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentContactMapper
  67. 117 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentController
  68. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentDO
  69. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentMapper
  70. 34 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentPageReqVO
  71. 60 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentRespVO
  72. 58 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentSaveReqVO
  73. 77 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentService
  74. 147 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentServiceImpl
  75. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentServiceImplTest
  76. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentTeacherDO
  77. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentTeacherMapper
  78. 74 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/js/student
  79. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/sql/h2
  80. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/sql/sql
  81. 176 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/StudentContactForm
  82. 221 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/StudentForm
  83. 126 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/StudentTeacherForm
  84. 213 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/index
  85. 12 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/xml/InfraStudentMapper
  86. 49 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/assert.json
  87. 3 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/ErrorCodeConstants_手动操作
  88. 95 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentController
  89. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentDO
  90. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentMapper
  91. 34 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentPageReqVO
  92. 60 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentRespVO
  93. 50 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentSaveReqVO
  94. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentService
  95. 74 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentServiceImpl
  96. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentServiceImplTest
  97. 53 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/js/student
  98. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/sql/h2
  99. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/sql/sql
  100. 164 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/vue/StudentForm

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

@@ -104,6 +104,18 @@ public class CodegenEngine {
                     vueFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
             .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("api/api.js"),
                     vueFilePath("api/${table.moduleName}/${classNameVar}.js"))
+            .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/form.vue"),
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
+            .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_normal.vue"),  // 特殊:主子表专属逻辑
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
+            .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_inner.vue"),  // 特殊:主子表专属逻辑
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
+            .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_erp.vue"),  // 特殊:主子表专属逻辑
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
+            .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/list_sub_inner.vue"),  // 特殊:主子表专属逻辑
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
+            .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/list_sub_erp.vue"),  // 特殊:主子表专属逻辑
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
             // Vue3 标准模版
             .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/index.vue"),
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
@@ -285,6 +297,10 @@ public class CodegenEngine {
         if (StrUtil.count(content, "dateFormatter") == 1) {
             content = StrUtils.removeLineContains(content, "dateFormatter");
         }
+        // Vue2 界面:修正 $refs
+        if (StrUtil.count(content, "this.refs") >= 1) {
+            content = content.replace("this.refs", "this.$refs");
+        }
         // Vue 界面:去除多的 dict 相关,只有一个的情况下,说明没使用到
         if (StrUtil.count(content, "getIntDictOptions") == 1) {
             content = content.replace("getIntDictOptions, ", "");
@@ -452,7 +468,7 @@ public class CodegenEngine {
     }
 
     private static String vueFilePath(String path) {
-        return "yudao-ui-${sceneEnum.basePackage}/" + // 顶级目录
+        return "yudao-ui-${sceneEnum.basePackage}-vue2/" + // 顶级目录
                 "src/" + path;
     }
 

+ 97 - 5
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm

@@ -35,21 +35,113 @@ export function get${simpleClassName}(id) {
   })
 }
 
+#if ( $table.templateType != 2 )
 // 获得${table.classComment}分页
-export function get${simpleClassName}Page(query) {
+export function get${simpleClassName}Page(params) {
   return request({
     url: '${baseURL}/page',
     method: 'get',
-    params: query
+    params
   })
 }
-
+#else
+// 获得${table.classComment}列表
+export function get${simpleClassName}List(params) {
+  return request({
+    url: '${baseURL}/list',
+    method: 'get',
+    params
+  })
+}
+#end
 // 导出${table.classComment} Excel
-export function export${simpleClassName}Excel(query) {
+export function export${simpleClassName}Excel(params) {
   return request({
     url: '${baseURL}/export-excel',
     method: 'get',
-    params: query,
+    params,
     responseType: 'blob'
   })
 }
+## 特殊:主子表专属逻辑
+#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) {
+    return request({
+      url: '${baseURL}/${subSimpleClassName_strikeCase}/page',
+      method: 'get',
+      params
+    })
+  }
+    ## 情况二:非 MASTER_ERP 时,需要列表查询子表
+  #else
+    #if ( $subTable.subJoinMany )
+
+    // 获得${subTable.classComment}列表
+    export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}) {
+      return request({
+        url: `${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField},
+        method: 'get'
+      })
+    }
+    #else
+
+    // 获得${subTable.classComment}
+    export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}) {
+      return request({
+        url: `${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField},
+        method: 'get'
+      })
+    }
+    #end
+  #end
+  ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作
+  #if ( $table.templateType == 11 )
+  // 新增${subTable.classComment}
+  export function create${subSimpleClassName}(data) {
+    return request({
+      url: `${baseURL}/${subSimpleClassName_strikeCase}/create`,
+      method: 'post',
+      data
+    })
+  }
+
+  // 修改${subTable.classComment}
+  export function update${subSimpleClassName}(data) {
+    return request({
+      url: `${baseURL}/${subSimpleClassName_strikeCase}/update`,
+      method: 'post',
+      data
+    })
+  }
+
+  // 删除${subTable.classComment}
+  export function delete${subSimpleClassName}(id) {
+    return request({
+      url: `${baseURL}/${subSimpleClassName_strikeCase}/delete?id=` + id,
+      method: 'delete'
+    })
+  }
+
+  // 获得${subTable.classComment}
+  export function get${subSimpleClassName}(id) {
+    return request({
+      url: `${baseURL}/${subSimpleClassName_strikeCase}/get?id=` + id,
+      method: 'get'
+    })
+  }
+  #end
+#end

+ 205 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm

@@ -0,0 +1,205 @@
+#set ($subTable = $subTables.get($subIndex))##当前表
+#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
+#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+          #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)
+                  #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
+                  #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-input v-model="formData.${javaField}" placeholder="请输入${comment}" />
+                    </el-form-item>
+                  #elseif($column.htmlType == "imageUpload")## 图片上传
+                      #set ($hasImageUploadColumn = true)
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <ImageUpload v-model="formData.${javaField}"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "fileUpload")## 文件上传
+                      #set ($hasFileUploadColumn = true)
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <FileUpload v-model="formData.${javaField}"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "editor")## 文本编辑器
+                      #set ($hasEditorColumn = true)
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <editor v-model="formData.${javaField}" :min-height="192"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "select")## 下拉框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-select v-model="formData.${javaField}" placeholder="请选择${comment}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                       :key="dict.value" :label="dict.label" #if ($column.javaType == "Integer" || $column.javaType == "Long"):value="parseInt(dict.value)"#else:value="dict.value"#end />
+                          #else##没数据字典
+                            <el-option label="请选择字典生成" value="" />
+                          #end
+                      </el-select>
+                    </el-form-item>
+                  #elseif($column.htmlType == "checkbox")## 多选框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-checkbox-group v-model="formData.${javaField}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-checkbox v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                         :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"#else:label="dict.value"#end>{{dict.label}}</el-checkbox>
+                          #else##没数据字典
+                            <el-checkbox>请选择字典生成</el-checkbox>
+                          #end
+                      </el-checkbox-group>
+                    </el-form-item>
+                  #elseif($column.htmlType == "radio")## 单选框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-radio-group v-model="formData.${javaField}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                      :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"
+                                      #else:label="dict.value"#end>{{dict.label}}</el-radio>
+                          #else##没数据字典
+                            <el-radio label="1">请选择字典生成</el-radio>
+                          #end
+                      </el-radio-group>
+                    </el-form-item>
+                  #elseif($column.htmlType == "datetime")## 时间框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-date-picker clearable v-model="formData.${javaField}" type="date" value-format="timestamp" placeholder="选择${comment}" />
+                    </el-form-item>
+                  #elseif($column.htmlType == "textarea")## 文本框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-input v-model="formData.${javaField}" type="textarea" placeholder="请输入内容" />
+                    </el-form-item>
+                  #end
+              #end
+          #end
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
+      #if ($hasImageUploadColumn)
+      import ImageUpload from '@/components/ImageUpload';
+      #end
+      #if ($hasFileUploadColumn)
+      import FileUpload from '@/components/FileUpload';
+      #end
+      #if ($hasEditorColumn)
+      import Editor from '@/components/Editor';
+      #end
+  export default {
+    name: "${subSimpleClassName}Form",
+    components: {
+        #if ($hasImageUploadColumn)
+          ImageUpload,
+        #end
+        #if ($hasFileUploadColumn)
+          FileUpload,
+        #end
+        #if ($hasEditorColumn)
+          Editor,
+        #end
+    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+            #foreach ($column in $subColumns)
+                #if ($column.createOperation || $column.updateOperation)
+                    #if ($column.htmlType == "checkbox")
+                            $column.javaField: [],
+                    #else
+                            $column.javaField: undefined,
+                    #end
+                #end
+            #end
+        },
+        // 表单校验
+        formRules: {
+            #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
+        },
+      };
+    },
+    methods: {
+      /** 打开弹窗 */
+      async open(id, ${subJoinColumn.javaField}) {
+        this.dialogVisible = true;
+        this.reset();
+        this.formData.${subJoinColumn.javaField} = ${subJoinColumn.javaField};
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            const res = await ${simpleClassName}Api.get${subSimpleClassName}(id);
+            this.formData = res.data;
+            this.dialogTitle = "修改${subTable.classComment}";
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.dialogTitle = "新增${subTable.classComment}";
+      },
+      /** 提交按钮 */
+      async submitForm() {
+        await this.#[[$]]#refs["formRef"].validate();
+        this.formLoading = true;
+        try {
+            const data = this.formData;
+            // 修改的提交
+            if (data.${primaryColumn.javaField}) {
+            await  ${simpleClassName}Api.update${subSimpleClassName}(data);
+            this.#[[$modal]]#.msgSuccess("修改成功");
+            this.dialogVisible = false;
+            this.#[[$]]#emit('success');
+              return;
+            }
+            // 添加的提交
+              await ${simpleClassName}Api.create${subSimpleClassName}(data);
+              this.#[[$modal]]#.msgSuccess("新增成功");
+              this.dialogVisible = false;
+              this.#[[$]]#emit('success');
+        }finally {
+          this.formLoading = false;
+        }
+      },
+      /** 表单重置 */
+      reset() {
+        this.formData = {
+            #foreach ($column in $subColumns)
+                #if ($column.createOperation || $column.updateOperation)
+                    #if ($column.htmlType == "checkbox")
+                            $column.javaField: [],
+                    #else
+                            $column.javaField: undefined,
+                    #end
+                #end
+            #end
+        };
+        this.resetForm("formRef");
+      },
+    }
+  };
+</script>

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

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

+ 347 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm

@@ -0,0 +1,347 @@
+#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>
+  <div class="app-container">
+    #if ( $subTable.subJoinMany )## 情况一:一对多,table + form
+      <el-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          v-loading="formLoading"
+          label-width="0px"
+          :inline-message="true"
+      >
+        <el-table :data="formData" class="-mt-10px">
+          <el-table-column label="序号" type="index" width="100" />
+            #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)
+                    #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
+                    #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+                      <el-table-column label="${comment}" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <el-input v-model="row.${javaField}" placeholder="请输入${comment}" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "imageUpload")## 图片上传
+                        #set ($hasImageUploadColumn = true)
+                      <el-table-column label="${comment}" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <ImageUpload v-model="row.${javaField}"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "fileUpload")## 文件上传
+                        #set ($hasFileUploadColumn = true)
+                      <el-table-column label="${comment}" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <FileUpload v-model="row.${javaField}"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "editor")## 文本编辑器
+                        #set ($hasEditorColumn = true)
+                      <el-table-column label="${comment}" min-width="400">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <Editor v-model="row.${javaField}" :min-height="192"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "select")## 下拉框
+                      <el-table-column label="${comment}" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <el-select v-model="row.${javaField}" placeholder="请选择${comment}">
+                                #if ("" != $dictType)## 有数据字典
+                                  <el-option v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                             :key="dict.value" :label="dict.label" #if ($column.javaType == "Integer" || $column.javaType == "Long"):value="parseInt(dict.value)"#else:value="dict.value"#end />
+                                #else##没数据字典
+                                  <el-option label="请选择字典生成" value="" />
+                                #end
+                            </el-select>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "checkbox")## 多选框
+                      <el-table-column label="${comment}" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <el-checkbox-group v-model="row.${javaField}">
+                                #if ("" != $dictType)## 有数据字典
+                                  <el-checkbox v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                               :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"#else:label="dict.value"#end>{{dict.label}}</el-checkbox>
+                                #else##没数据字典
+                                  <el-checkbox>请选择字典生成</el-checkbox>
+                                #end
+                            </el-checkbox-group>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "radio")## 单选框
+                      <el-table-column label="${comment}" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <el-radio-group v-model="row.${javaField}">
+                                #if ("" != $dictType)## 有数据字典
+                                  <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                            :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"
+                                            #else:label="dict.value"#end>{{dict.label}}</el-radio>
+                                #else##没数据字典
+                                  <el-radio label="1">请选择字典生成</el-radio>
+                                #end
+                            </el-radio-group>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "datetime")## 时间框
+                      <el-table-column label="${comment}" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <el-date-picker clearable v-model="row.${javaField}" type="date" value-format="timestamp" placeholder="选择${comment}" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "textarea")## 文本框
+                      <el-table-column label="${comment}" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <el-input v-model="row.${javaField}" type="textarea" placeholder="请输入${comment}" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #end
+                #end
+            #end
+          <el-table-column align="center" fixed="right" label="操作" width="60">
+            <template v-slot="{ $index }">
+              <el-link @click="handleDelete($index)">—</el-link>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-form>
+      <el-row justify="center" class="mt-3">
+        <el-button @click="handleAdd" round>+ 添加${subTable.classComment}</el-button>
+      </el-row>
+    #else## 情况二:一对一,form
+      <el-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          label-width="100px"
+          v-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)
+                  #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
+                  #elseif ($column.htmlType == "input" && !$column.primaryKey)
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-input v-model="formData.${javaField}" placeholder="请输入${comment}" />
+                    </el-form-item>
+                  #elseif($column.htmlType == "imageUpload")## 图片上传
+                      #set ($hasImageUploadColumn = true)
+                    <el-form-item label="${comment}">
+                      <ImageUpload v-model="formData.${javaField}"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "fileUpload")## 文件上传
+                      #set ($hasFileUploadColumn = true)
+                    <el-form-item label="${comment}">
+                      <FileUpload v-model="formData.${javaField}"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "editor")## 文本编辑器
+                      #set ($hasEditorColumn = true)
+                    <el-form-item label="${comment}">
+                      <Editor v-model="formData.${javaField}" :min-height="192"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "select")## 下拉框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-select v-model="formData.${javaField}" placeholder="请选择${comment}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                       :key="dict.value" :label="dict.label" #if ($column.javaType == "Integer" || $column.javaType == "Long"):value="parseInt(dict.value)"#else:value="dict.value"#end />
+                          #else##没数据字典
+                            <el-option label="请选择字典生成" value="" />
+                          #end
+                      </el-select>
+                    </el-form-item>
+                  #elseif($column.htmlType == "checkbox")## 多选框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-checkbox-group v-model="formData.${javaField}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-checkbox v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                         :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"#else:label="dict.value"#end>{{dict.label}}</el-checkbox>
+                          #else##没数据字典
+                            <el-checkbox>请选择字典生成</el-checkbox>
+                          #end
+                      </el-checkbox-group>
+                    </el-form-item>
+                  #elseif($column.htmlType == "radio")## 单选框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-radio-group v-model="formData.${javaField}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                      :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"
+                                      #else:label="dict.value"#end>{{dict.label}}</el-radio>
+                          #else##没数据字典
+                            <el-radio label="1">请选择字典生成</el-radio>
+                          #end
+                      </el-radio-group>
+                    </el-form-item>
+                  #elseif($column.htmlType == "datetime")## 时间框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-date-picker clearable v-model="formData.${javaField}" type="date" value-format="timestamp" placeholder="选择${comment}" />
+                    </el-form-item>
+                  #elseif($column.htmlType == "textarea")## 文本框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-input v-model="formData.${javaField}" type="textarea" placeholder="请输入${comment}" />
+                    </el-form-item>
+                  #end
+              #end
+          #end
+      </el-form>
+    #end
+  </div>
+</template>
+
+<script>
+  import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
+      #if ($hasImageUploadColumn)
+      import ImageUpload from '@/components/ImageUpload';
+      #end
+      #if ($hasFileUploadColumn)
+      import FileUpload from '@/components/FileUpload';
+      #end
+      #if ($hasEditorColumn)
+      import Editor from '@/components/Editor';
+      #end
+  export default {
+    name: "${subSimpleClassName}Form",
+    components: {
+        #if ($hasImageUploadColumn)
+          ImageUpload,
+        #end
+        #if ($hasFileUploadColumn)
+          FileUpload,
+        #end
+        #if ($hasEditorColumn)
+          Editor,
+        #end
+    },
+    props:[
+      '${subJoinColumn.javaField}'
+    ],// ${subJoinColumn.columnComment}(主表的关联字段)
+    data() {
+      return {
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: [],
+        // 表单校验
+        formRules: {
+            #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
+        },
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+      ${subJoinColumn.javaField}:{
+        handler(val) {
+          // 1. 重置表单
+            #if ( $subTable.subJoinMany )
+              this.formData = []
+            #else
+              this.formData = {
+                  #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 {
+            this.formLoading = true;
+            // 这里还是需要获取一下 this 的不然取不到 formData
+            const that = this;
+            #if ( $subTable.subJoinMany )
+            ${simpleClassName}Api.get${subSimpleClassName}ListBy${SubJoinColumnName}(val).then(function (res){
+              that.formData = res.data;
+            })
+            #else
+            ${simpleClassName}Api.get${subSimpleClassName}By${SubJoinColumnName}(val).then(function (res){
+              const data = res.data;
+              if (!data) {
+                return
+              }
+              that.formData = data;
+            })
+            #end
+          } finally {
+            this.formLoading = false;
+          }
+        },
+        immediate: true
+      }
+    },
+    methods: {
+        #if ( $subTable.subJoinMany )
+          /** 新增按钮操作 */
+          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} = this.${subJoinColumn.javaField};
+            this.formData.push(row);
+          },
+          /** 删除按钮操作 */
+          handleDelete(index) {
+            this.formData.splice(index, 1);
+          },
+        #end
+      /** 表单校验 */
+      validate(){
+        return this.#[[$]]#refs["formRef"].validate();
+      },
+      /** 表单值 */
+      getData(){
+        return this.formData;
+      }
+    }
+  };
+</script>

+ 165 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm

@@ -0,0 +1,165 @@
+#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>
+  <div class="app-container">
+#if ($table.templateType == 11)
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['${permissionPrefix}:create']">新增</el-button>
+      </el-col>
+    </el-row>
+#end
+      ## 列表
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="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")## 时间类型
+                <el-table-column label="${comment}" align="center" prop="${javaField}" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.${javaField}) }}</span>
+                  </template>
+                </el-table-column>
+                  #elseif($column.dictType && "" != $column.dictType)## 数据字典
+                <el-table-column label="${comment}" align="center" prop="${javaField}">
+                  <template v-slot="scope">
+                    <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.${column.javaField}" />
+                  </template>
+                </el-table-column>
+              #else
+                <el-table-column label="${comment}" align="center" prop="${javaField}" />
+              #end
+          #end
+      #end
+    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <template v-slot="scope">
+        <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.${primaryColumn.javaField})"
+                   v-hasPermi="['${permissionPrefix}:update']">修改</el-button>
+        <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                   v-hasPermi="['${permissionPrefix}:delete']">删除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+#if ($table.templateType == 11)
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+  <!-- 对话框(添加 / 修改) -->
+  <${subSimpleClassName}Form ref="formRef" @success="getList" />
+#end
+  </div>
+</template>
+
+<script>
+  import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
+  #if ($table.templateType == 11)
+  import ${subSimpleClassName}Form from './${subSimpleClassName}Form.vue'
+  #end
+  export default {
+    name: "${subSimpleClassName}List",
+#if ($table.templateType == 11)
+    components: {
+       ${subSimpleClassName}Form
+    },
+#end
+    props:[
+      '${subJoinColumn.javaField}'
+    ],// ${subJoinColumn.columnComment}(主表的关联字段)
+    data() {
+      return {
+        // 遮罩层
+        loading: true,
+        // 列表的数据
+        list: [],
+#if ($table.templateType == 11)
+        // 列表的总页数
+        total: 0,
+        // 查询参数
+        queryParams: {
+          pageNo: 1,
+          pageSize: 10,
+          ${subJoinColumn.javaField}: undefined
+        }
+#end
+      };
+    },
+#if ($table.templateType != 11)
+    created() {
+      this.getList();
+    },
+#end
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+        ${subJoinColumn.javaField}:{
+            handler(val) {
+              this.queryParams.${subJoinColumn.javaField} = val;
+              if (val){
+                this.handleQuery();
+              }
+            },
+            immediate: true
+      }
+    },
+    methods: {
+      /** 查询列表 */
+      async getList() {
+        try {
+          this.loading = true;
+          #if ($table.templateType == 11)
+            const res = await ${simpleClassName}Api.get${subSimpleClassName}Page(this.queryParams);
+            this.list = res.data.list;
+            this.total = res.data.total;
+          #else
+              #if ( $subTable.subJoinMany )
+                const res = await ${simpleClassName}Api.get${subSimpleClassName}ListBy${SubJoinColumnName}(this.${subJoinColumn.javaField});
+                this.list = res.data;
+              #else
+                const res = await  ${simpleClassName}Api.get${subSimpleClassName}By${SubJoinColumnName}(this.${subJoinColumn.javaField});
+                const data = res.data;
+                if (!data) {
+                  return;
+                }
+                this.list.push(data);
+              #end
+          #end
+        } finally {
+          this.loading = false;
+        }
+      },
+      /** 搜索按钮操作 */
+      handleQuery() {
+        this.queryParams.pageNo = 1;
+        this.getList();
+      },
+#if ($table.templateType == 11)
+      /** 添加/修改操作 */
+      openForm(id) {
+        if (!this.${subJoinColumn.javaField}) {
+          this.#[[$modal]]#.msgError('请选择一个${table.classComment}');
+          return;
+        }
+        this.#[[$]]#refs["formRef"].open(id, this.${subJoinColumn.javaField});
+      },
+      /** 删除按钮操作 */
+      async handleDelete(row) {
+        const ${primaryColumn.javaField} = row.${primaryColumn.javaField};
+        await this.#[[$modal]]#.confirm('是否确认删除${table.classComment}编号为"' + ${primaryColumn.javaField} + '"的数据项?');
+        try {
+          await ${simpleClassName}Api.delete${subSimpleClassName}(${primaryColumn.javaField});
+          await this.getList();
+          this.#[[$modal]]#.msgSuccess("删除成功");
+        } catch {}
+      },
+#end
+    }
+  };
+</script>

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

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

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

@@ -0,0 +1,320 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+          #foreach($column in $columns)
+              #if ($column.createOperation || $column.updateOperation)
+                  #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 ( $table.templateType == 2 && $column.id == $treeParentColumn.id )
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <TreeSelect
+                          v-model="formData.${javaField}"
+                          :options="${classNameVar}Tree"
+                          :normalizer="normalizer"
+                          placeholder="请选择${comment}"
+                      />
+                    </el-form-item>
+                  #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-input v-model="formData.${javaField}" placeholder="请输入${comment}" />
+                    </el-form-item>
+                  #elseif($column.htmlType == "imageUpload")## 图片上传
+                      #set ($hasImageUploadColumn = true)
+                    <el-form-item label="${comment}">
+                      <ImageUpload v-model="formData.${javaField}"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "fileUpload")## 文件上传
+                      #set ($hasFileUploadColumn = true)
+                    <el-form-item label="${comment}">
+                      <FileUpload v-model="formData.${javaField}"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "editor")## 文本编辑器
+                      #set ($hasEditorColumn = true)
+                    <el-form-item label="${comment}">
+                      <Editor v-model="formData.${javaField}" :min-height="192"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "select")## 下拉框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-select v-model="formData.${javaField}" placeholder="请选择${comment}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                       :key="dict.value" :label="dict.label" #if ($column.javaType == "Integer" || $column.javaType == "Long"):value="parseInt(dict.value)"#else:value="dict.value"#end />
+                          #else##没数据字典
+                            <el-option label="请选择字典生成" value="" />
+                          #end
+                      </el-select>
+                    </el-form-item>
+                  #elseif($column.htmlType == "checkbox")## 多选框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-checkbox-group v-model="formData.${javaField}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-checkbox v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                         :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"#else:label="dict.value"#end>{{dict.label}}</el-checkbox>
+                          #else##没数据字典
+                            <el-checkbox>请选择字典生成</el-checkbox>
+                          #end
+                      </el-checkbox-group>
+                    </el-form-item>
+                  #elseif($column.htmlType == "radio")## 单选框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-radio-group v-model="formData.${javaField}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                      :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"
+                                      #else:label="dict.value"#end>{{dict.label}}</el-radio>
+                          #else##没数据字典
+                            <el-radio label="1">请选择字典生成</el-radio>
+                          #end
+                      </el-radio-group>
+                    </el-form-item>
+                  #elseif($column.htmlType == "datetime")## 时间框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-date-picker clearable v-model="formData.${javaField}" type="date" value-format="timestamp" placeholder="选择${comment}" />
+                    </el-form-item>
+                  #elseif($column.htmlType == "textarea")## 文本框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-input v-model="formData.${javaField}" type="textarea" placeholder="请输入内容" />
+                    </el-form-item>
+                  #end
+              #end
+          #end
+      </el-form>
+        ## 特殊:主子表专属逻辑
+        #if ( $table.templateType == 10 || $table.templateType == 12 )
+          <!-- 子表的表单 -->
+          <el-tabs v-model="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))
+                <el-tab-pane label="${subTable.classComment}" name="$subClassNameVar">
+                  <${subSimpleClassName}Form ref="${subClassNameVar}FormRef" :${subJoinColumn_strikeCase}="formData.id" />
+                </el-tab-pane>
+              #end
+          </el-tabs>
+        #end
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
+  #if ($hasImageUploadColumn)
+  import ImageUpload from '@/components/ImageUpload';
+  #end
+  #if ($hasFileUploadColumn)
+  import FileUpload from '@/components/FileUpload';
+  #end
+  #if ($hasEditorColumn)
+  import Editor from '@/components/Editor';
+  #end
+  ## 特殊:树表专属逻辑
+  #if ( $table.templateType == 2 )
+  import TreeSelect from "@riophae/vue-treeselect";
+  import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+  #end
+  ## 特殊:主子表专属逻辑
+  #if ( $table.templateType == 10 || $table.templateType == 12 )
+      #foreach ($subSimpleClassName in $subSimpleClassNames)
+      import ${subSimpleClassName}Form from './components/${subSimpleClassName}Form.vue'
+      #end
+  #end
+  export default {
+    name: "${simpleClassName}Form",
+    components: {
+        #if ($hasImageUploadColumn)
+          ImageUpload,
+        #end
+        #if ($hasFileUploadColumn)
+          FileUpload,
+        #end
+        #if ($hasEditorColumn)
+          Editor,
+        #end
+        ## 特殊:树表专属逻辑
+        #if ( $table.templateType == 2 )
+          TreeSelect,
+        #end
+        ## 特殊:主子表专属逻辑
+        #if ( $table.templateType == 10 || $table.templateType == 12 )
+            #foreach ($subSimpleClassName in $subSimpleClassNames)
+               ${subSimpleClassName}Form,
+            #end
+        #end
+    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+            #foreach ($column in $columns)
+                #if ($column.createOperation || $column.updateOperation)
+                    #if ($column.htmlType == "checkbox")
+                            $column.javaField: [],
+                    #else
+                            $column.javaField: undefined,
+                    #end
+                #end
+            #end
+        },
+        // 表单校验
+        formRules: {
+            #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
+        },
+          ## 特殊:树表专属逻辑
+          #if ( $table.templateType == 2 )
+             ${classNameVar}Tree: [], // 树形结构
+          #end
+        ## 特殊:主子表专属逻辑
+        #if ( $table.templateType == 10 || $table.templateType == 12 )
+        #if ( $subTables && $subTables.size() > 0 )
+            /** 子表的表单 */
+             subTabsName: '$subClassNameVars.get(0)'
+        #end
+        #end
+      };
+    },
+    methods: {
+      /** 打开弹窗 */
+     async open(id) {
+        this.dialogVisible = true;
+        this.reset();
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            const res = await ${simpleClassName}Api.get${simpleClassName}(id);
+            this.formData = res.data;
+            this.title = "修改${table.classComment}";
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.title = "新增${table.classComment}";
+        ## 特殊:树表专属逻辑
+        #if ( $table.templateType == 2 )
+            this.get${simpleClassName}Tree();
+        #end
+      },
+      /** 提交按钮 */
+      async submitForm() {
+        // 校验主表
+        await this.$refs["formRef"].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 {
+                      ## 代码生成后会替换为正确的 refs
+                      await this.refs['${subClassNameVar}FormRef'].validate();
+                    } catch (e) {
+                      this.subTabsName = '${subClassNameVar}';
+                      return;
+                    }
+                  #end
+              #end
+          #end
+        this.formLoading = true;
+        try {
+          const data = this.formData;
+        ## 特殊:主子表专属逻辑
+        #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 = this.refs['${subClassNameVar}FormRef'].getData();
+            #end
+        #end
+        #end
+          // 修改的提交
+          if (data.${primaryColumn.javaField}) {
+            await ${simpleClassName}Api.update${simpleClassName}(data);
+            this.#[[$modal]]#.msgSuccess("修改成功");
+            this.dialogVisible = false;
+            this.#[[$]]#emit('success');
+            return;
+          }
+          // 添加的提交
+          await ${simpleClassName}Api.create${simpleClassName}(data);
+          this.#[[$modal]]#.msgSuccess("新增成功");
+          this.dialogVisible = false;
+          this.#[[$]]#emit('success');
+        }finally {
+          this.formLoading = false;
+        }
+      },
+        ## 特殊:树表专属逻辑
+        #if ( $table.templateType == 2 )
+          /** 获得${table.classComment}树 */
+         async get${simpleClassName}Tree() {
+            this.${classNameVar}Tree = [];
+            const res = await ${simpleClassName}Api.get${simpleClassName}List();
+            const root = { id: 0, name: '顶级${table.classComment}', children: [] };
+            root.children = this.handleTree(res.data, 'id', '${treeParentColumn.javaField}')
+            this.${classNameVar}Tree.push(root)
+          },
+        #end
+        ## 特殊:树表专属逻辑
+        #if ( $table.templateType == 2 )
+          /** 转换${table.classComment}数据结构 */
+          normalizer(node) {
+            if (node.children && !node.children.length) {
+              delete node.children;
+            }
+              #if ($treeNameColumn.javaField == "name")
+                return {
+                  id: node.id,
+                  label: node.name,
+                  children: node.children
+                };
+              #else
+                return {
+                  id: node.id,
+                  label: node['$treeNameColumn.javaField'],
+                  children: node.children
+                };
+              #end
+          },
+        #end
+      /** 表单重置 */
+      reset() {
+        this.formData = {
+            #foreach ($column in $columns)
+                #if ($column.createOperation || $column.updateOperation)
+                    #if ($column.htmlType == "checkbox")
+                            $column.javaField: [],
+                    #else
+                            $column.javaField: undefined,
+                    #end
+                #end
+            #end
+        };
+        this.resetForm("formRef");
+      }
+    }
+  };
+</script>

+ 171 - 199
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm

@@ -47,18 +47,68 @@
     <!-- 操作工具栏 -->
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
                    v-hasPermi="['${permissionPrefix}:create']">新增</el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
                    v-hasPermi="['${permissionPrefix}:export']">导出</el-button>
       </el-col>
+        ## 特殊:树表专属逻辑
+        #if ( $table.templateType == 2 )
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="el-icon-sort" size="mini" @click="toggleExpandAll">
+              展开/折叠
+            </el-button>
+          </el-col>
+        #end
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <!-- 列表 -->
-    <el-table v-loading="loading" :data="list">
+      ## 特殊:主子表专属逻辑
+      #if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
+      <el-table
+          v-loading="loading"
+          :data="list"
+          :stripe="true"
+          :highlight-current-row="true"
+          :show-overflow-tooltip="true"
+          @current-change="handleCurrentChange"
+      >
+          ## 特殊:树表专属逻辑
+      #elseif ( $table.templateType == 2 )
+      <el-table
+          v-loading="loading"
+          :data="list"
+          :stripe="true"
+          :show-overflow-tooltip="true"
+          v-if="refreshTable"
+          row-key="id"
+          :default-expand-all="isExpandAll"
+          :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+      >
+      #else
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      #end
+      ## 特殊:主子表专属逻辑
+      #if ( $table.templateType == 12 && $subTables && $subTables.size() > 0 )
+        <!-- 子表的列表 -->
+        <el-table-column type="expand">
+          <template #default="scope">
+            <el-tabs value="$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))
+                  <el-tab-pane label="${subTable.classComment}" name="$subClassNameVar">
+                    <${subSimpleClassName}List :${subJoinColumn_strikeCase}="scope.row.id" />
+                  </el-tab-pane>
+                #end
+            </el-tabs>
+          </template>
+        </el-table-column>
+      #end
 #foreach($column in $columns)
 #if ($column.listOperationResult)
     #set ($dictType=$column.dictType)
@@ -84,102 +134,42 @@
 #end
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template v-slot="scope">
-          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.${primaryColumn.javaField})"
                      v-hasPermi="['${permissionPrefix}:update']">修改</el-button>
           <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
                      v-hasPermi="['${permissionPrefix}:delete']">删除</el-button>
         </template>
       </el-table-column>
     </el-table>
+## 特殊:树表专属逻辑(树不需要分页)
+#if ( $table.templateType != 2 )
     <!-- 分页组件 -->
     <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
                 @pagination="getList"/>
-
+#end
     <!-- 对话框(添加 / 修改) -->
-    <el-dialog :title="title" :visible.sync="open" width="500px" v-dialogDrag append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-#foreach($column in $columns)
-#if ($column.createOperation || $column.updateOperation)
-    #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.htmlType == "input")
-  #if (!$column.primaryKey)## 忽略主键,不用在表单里
-        <el-form-item label="${comment}" prop="${javaField}">
-          <el-input v-model="form.${javaField}" placeholder="请输入${comment}" />
-        </el-form-item>
+    <${simpleClassName}Form ref="formRef" @success="getList" />
+  ## 特殊:主子表专属逻辑
+  #if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
+    <!-- 子表的列表 -->
+      <el-tabs v-model="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))
+            <el-tab-pane label="${subTable.classComment}" name="$subClassNameVar">
+              <${subSimpleClassName}List v-if="currentRow.id" :${subJoinColumn_strikeCase}="currentRow.id" />
+            </el-tab-pane>
+          #end
+      </el-tabs>
   #end
-#elseif($column.htmlType == "imageUpload")## 图片上传
-        #set ($hasImageUploadColumn = true)
-        <el-form-item label="${comment}">
-          <imageUpload v-model="form.${javaField}"/>
-        </el-form-item>
-#elseif($column.htmlType == "fileUpload")## 文件上传
-        #set ($hasFileUploadColumn = true)
-        <el-form-item label="${comment}">
-          <fileUpload v-model="form.${javaField}"/>
-        </el-form-item>
-#elseif($column.htmlType == "editor")## 文本编辑器
-        #set ($hasEditorColumn = true)
-        <el-form-item label="${comment}">
-          <editor v-model="form.${javaField}" :min-height="192"/>
-        </el-form-item>
-#elseif($column.htmlType == "select")## 下拉框
-        <el-form-item label="${comment}" prop="${javaField}">
-          <el-select v-model="form.${javaField}" placeholder="请选择${comment}">
-    #if ("" != $dictType)## 有数据字典
-            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
-                       :key="dict.value" :label="dict.label" #if ($column.javaType == "Integer" || $column.javaType == "Long"):value="parseInt(dict.value)"#else:value="dict.value"#end />
-    #else##没数据字典
-            <el-option label="请选择字典生成" value="" />
-    #end
-          </el-select>
-        </el-form-item>
-#elseif($column.htmlType == "checkbox")## 多选框
-        <el-form-item label="${comment}" prop="${javaField}">
-          <el-checkbox-group v-model="form.${javaField}">
-    #if ("" != $dictType)## 有数据字典
-            <el-checkbox v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
-                         :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"#else:label="dict.value"#end>{{dict.label}}</el-checkbox>
-    #else##没数据字典
-            <el-checkbox>请选择字典生成</el-checkbox>
-    #end
-          </el-checkbox-group>
-        </el-form-item>
-#elseif($column.htmlType == "radio")## 单选框
-        <el-form-item label="${comment}" prop="${javaField}">
-          <el-radio-group v-model="form.${javaField}">
-    #if ("" != $dictType)## 有数据字典
-            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
-                      :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"#else:label="dict.value"#end>{{dict.label}}</el-radio>
-    #else##没数据字典
-            <el-radio label="1">请选择字典生成</el-radio>
-    #end
-          </el-radio-group>
-        </el-form-item>
-#elseif($column.htmlType == "datetime")## 时间框
-        <el-form-item label="${comment}" prop="${javaField}">
-          <el-date-picker clearable v-model="form.${javaField}" type="date" value-format="timestamp" placeholder="选择${comment}" />
-        </el-form-item>
-#elseif($column.htmlType == "textarea")## 文本框
-        <el-form-item label="${comment}" prop="${javaField}">
-          <el-input v-model="form.${javaField}" type="textarea" placeholder="请输入内容" />
-        </el-form-item>
-#end
-#end
-#end
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </div>
-    </el-dialog>
   </div>
 </template>
 
 <script>
-import { create${simpleClassName}, update${simpleClassName}, delete${simpleClassName}, get${simpleClassName}, get${simpleClassName}Page, export${simpleClassName}Excel } from "@/api/${table.moduleName}/${classNameVar}";
+import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}';
+import ${simpleClassName}Form from './${simpleClassName}Form.vue';
 #if ($hasImageUploadColumn)
 import ImageUpload from '@/components/ImageUpload';
 #end
@@ -189,10 +179,26 @@ import FileUpload from '@/components/FileUpload';
 #if ($hasEditorColumn)
 import Editor from '@/components/Editor';
 #end
-
+## 特殊:主子表专属逻辑
+#if ( $table.templateType != 10 )
+#if ( $subTables && $subTables.size() > 0 )
+    #foreach ($subSimpleClassName in $subSimpleClassNames)
+    import ${subSimpleClassName}List from './components/${subSimpleClassName}List.vue'
+    #end
+#end
+#end
 export default {
   name: "${simpleClassName}",
   components: {
+          ${simpleClassName}Form,
+## 特殊:主子表专属逻辑
+#if ( $table.templateType != 10 )
+#if ( $subTables && $subTables.size() > 0 )
+      #foreach ($subSimpleClassName in $subSimpleClassNames)
+          ${subSimpleClassName}List,
+      #end
+#end
+#end
 #if ($hasImageUploadColumn)
     ImageUpload,
 #end
@@ -211,40 +217,44 @@ export default {
       exportLoading: false,
       // 显示搜索条件
       showSearch: true,
-      // 总条数
-      total: 0,
+      ## 特殊:树表专属逻辑(树不需要分页接口)
+      #if ( $table.templateType != 2 )
+        // 总条数
+        total: 0,
+      #end
       // ${table.classComment}列表
       list: [],
-      // 弹出层标题
-      title: "",
-      // 是否显示弹出层
-      open: false,
+      // 是否展开,默认全部展开
+      isExpandAll: true,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 选中行
+      currentRow: {},
       // 查询参数
       queryParams: {
-        pageNo: 1,
-        pageSize: 10,
+        ## 特殊:树表专属逻辑(树不需要分页接口)
+        #if ( $table.templateType != 2 )
+            pageNo: 1,
+            pageSize: 10,
+        #end
         #foreach ($column in $columns)
         #if ($column.listOperation)
         #if ($column.listOperationCondition != 'BETWEEN')
         $column.javaField: null,
         #end
-        #if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
+        #if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
         $column.javaField: [],
         #end
         #end
         #end
       },
-      // 表单参数
-      form: {},
-      // 表单校验
-      rules: {
-      #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
-      }
+        ## 特殊:主子表专属逻辑-erp
+        #if ( $table.templateType == 11)
+            #if ( $subTables && $subTables.size() > 0 )
+              /** 子表的列表 */
+              subTabsName: '$subClassNameVars.get(0)'
+            #end
+        #end
     };
   },
   created() {
@@ -252,34 +262,21 @@ export default {
   },
   methods: {
     /** 查询列表 */
-    getList() {
+    async getList() {
+      try {
       this.loading = true;
-      // 执行查询
-      get${simpleClassName}Page(this.queryParams).then(response => {
-        this.list = response.data.list;
-        this.total = response.data.total;
+      ## 特殊:树表专属逻辑(树不需要分页接口)
+      #if ( $table.templateType == 2 )
+       const res = await ${simpleClassName}Api.get${simpleClassName}List(this.queryParams);
+       this.list = this.handleTree(res.data, 'id', '${treeParentColumn.javaField}');
+      #else
+        const res = await ${simpleClassName}Api.get${simpleClassName}Page(this.queryParams);
+        this.list = res.data.list;
+        this.total = res.data.total;
+      #end
+      } finally {
         this.loading = false;
-      });
-    },
-    /** 取消按钮 */
-    cancel() {
-      this.open = false;
-      this.reset();
-    },
-    /** 表单重置 */
-    reset() {
-      this.form = {
-        #foreach ($column in $columns)
-        #if ($column.createOperation || $column.updateOperation)
-        #if ($column.htmlType == "checkbox")
-        $column.javaField: [],
-        #else
-        $column.javaField: undefined,
-        #end
-        #end
-        #end
-      };
-      this.resetForm("form");
+      }
     },
     /** 搜索按钮操作 */
     handleQuery() {
@@ -291,79 +288,54 @@ export default {
       this.resetForm("queryForm");
       this.handleQuery();
     },
-    /** 新增按钮操作 */
-    handleAdd() {
-      this.reset();
-      this.open = true;
-      this.title = "添加${table.classComment}";
-    },
-    /** 修改按钮操作 */
-    handleUpdate(row) {
-      this.reset();
-      const ${primaryColumn.javaField} = row.${primaryColumn.javaField};
-      get${simpleClassName}(${primaryColumn.javaField}).then(response => {
-        this.form = response.data;
-        #foreach ($column in $columns)
-        #if($column.htmlType == "checkbox")## checkbox 特殊处理
-        this.form.$column.javaField = this.form.${column.javaField}.split(",");
-        #end
-        #end
-        this.open = true;
-        this.title = "修改${table.classComment}";
-      });
-    },
-    /** 提交按钮 */
-    submitForm() {
-      this.#[[$]]#refs["form"].validate(valid => {
-        if (!valid) {
-          return;
-        }
-        #foreach ($column in $columns)
-        #if($column.htmlType == "checkbox")
-        this.form.$column.javaField = this.form.${column.javaField}.join(",");
-        #end
-        #end
-        // 修改的提交
-        if (this.form.${primaryColumn.javaField} != null) {
-          update${simpleClassName}(this.form).then(response => {
-            this.#[[$modal]]#.msgSuccess("修改成功");
-            this.open = false;
-            this.getList();
-          });
-          return;
-        }
-        // 添加的提交
-        create${simpleClassName}(this.form).then(response => {
-          this.#[[$modal]]#.msgSuccess("新增成功");
-          this.open = false;
-          this.getList();
-        });
-      });
+    /** 添加/修改操作 */
+    openForm(id) {
+      this.#[[$]]#refs["formRef"].open(id);
     },
     /** 删除按钮操作 */
-    handleDelete(row) {
+    async handleDelete(row) {
       const ${primaryColumn.javaField} = row.${primaryColumn.javaField};
-      this.#[[$modal]]#.confirm('是否确认删除${table.classComment}编号为"' + ${primaryColumn.javaField} + '"的数据项?').then(function() {
-          return delete${simpleClassName}(${primaryColumn.javaField});
-        }).then(() => {
-          this.getList();
-          this.#[[$modal]]#.msgSuccess("删除成功");
-        }).catch(() => {});
+      await this.#[[$modal]]#.confirm('是否确认删除${table.classComment}编号为"' + ${primaryColumn.javaField} + '"的数据项?')
+      try {
+       await ${simpleClassName}Api.delete${simpleClassName}(${primaryColumn.javaField});
+       this.getList();
+       this.#[[$modal]]#.msgSuccess("删除成功");
+      } catch {}
     },
     /** 导出按钮操作 */
-    handleExport() {
-      // 处理查询参数
-      let params = {...this.queryParams};
-      params.pageNo = undefined;
-      params.pageSize = undefined;
-      this.#[[$modal]]#.confirm('是否确认导出所有${table.classComment}数据项?').then(() => {
-          this.exportLoading = true;
-          return export${simpleClassName}Excel(params);
-        }).then(response => {
-          this.#[[$]]#download.excel(response, '${table.classComment}.xls');
-          this.exportLoading = false;
-        }).catch(() => {});
-    }
+    async handleExport() {
+      await this.#[[$modal]]#.confirm('是否确认导出所有${table.classComment}数据项?');
+      try {
+        this.exportLoading = true;
+        const res = await ${simpleClassName}Api.export${simpleClassName}Excel(this.queryParams);
+        this.#[[$]]#download.excel(res.data, '${table.classComment}.xls');
+      } catch {
+      } finally {
+        this.exportLoading = false;
+      }
+    },
+      ## 特殊:主子表专属逻辑
+      #if ( $table.templateType == 11 )
+        /** 选中行操作 */
+        handleCurrentChange(row) {
+         this.currentRow = row;
+        #if ( $subTables && $subTables.size() > 0 )
+          /** 子表的列表 */
+          this.subTabsName = '$subClassNameVars.get(0)';
+        #end
+        },
+      #end
+      ## 特殊:树表专属逻辑
+      #if ( $table.templateType == 2 )
+        /** 展开/折叠操作 */
+        toggleExpandAll() {
+          this.refreshTable = false
+          this.isExpandAll = !this.isExpandAll
+          this.$nextTick(function () {
+            this.refreshTable = true
+          })
+        }
+      #end
   }
 };
 </script>

+ 266 - 266
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm

@@ -1,137 +1,137 @@
 <template>
   <Dialog :title="dialogTitle" v-model="dialogVisible">
     <el-form
-      ref="formRef"
-      :model="formData"
-      :rules="formRules"
-      label-width="100px"
-      v-loading="formLoading"
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-width="100px"
+        v-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 )
-      <el-form-item label="${comment}" prop="${javaField}">
-        <el-tree-select
-          v-model="formData.${javaField}"
-          :data="${classNameVar}Tree"
-          #if ($treeNameColumn.javaField == "name")
-          :props="defaultProps"
-          #else
-          :props="{...defaultProps, label: '$treeNameColumn.javaField'}"
-          #end
-          check-strictly
-          default-expand-all
-          placeholder="请选择${comment}"
-        />
-      </el-form-item>
-        #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
-      <el-form-item label="${comment}" prop="${javaField}">
-        <el-input v-model="formData.${javaField}" placeholder="请输入${comment}" />
-      </el-form-item>
-        #elseif($column.htmlType == "imageUpload")## 图片上传
-      <el-form-item label="${comment}" prop="${javaField}">
-        <UploadImg v-model="formData.${javaField}" />
-      </el-form-item>
-        #elseif($column.htmlType == "fileUpload")## 文件上传
-      <el-form-item label="${comment}" prop="${javaField}">
-        <UploadFile v-model="formData.${javaField}" />
-      </el-form-item>
-        #elseif($column.htmlType == "editor")## 文本编辑器
-      <el-form-item label="${comment}" prop="${javaField}">
-        <Editor v-model="formData.${javaField}" height="150px" />
-      </el-form-item>
-        #elseif($column.htmlType == "select")## 下拉框
-      <el-form-item label="${comment}" prop="${javaField}">
-        <el-select v-model="formData.${javaField}" placeholder="请选择${comment}">
-                #if ("" != $dictType)## 有数据字典
-          <el-option
-            v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-                #else##没数据字典
-          <el-option label="请选择字典生成" value="" />
+        #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
-        </el-select>
-      </el-form-item>
-        #elseif($column.htmlType == "checkbox")## 多选框
-      <el-form-item label="${comment}" prop="${javaField}">
-        <el-checkbox-group v-model="formData.${javaField}">
-                #if ("" != $dictType)## 有数据字典
-          <el-checkbox
-            v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-checkbox>
-                #else##没数据字典
-          <el-checkbox>请选择字典生成</el-checkbox>
+                #if ( $table.templateType == 2 && $column.id == $treeParentColumn.id )
+                  <el-form-item label="${comment}" prop="${javaField}">
+                    <el-tree-select
+                        v-model="formData.${javaField}"
+                        :data="${classNameVar}Tree"
+                        #if ($treeNameColumn.javaField == "name")
+                        :props="defaultProps"
+                        #else
+                        :props="{...defaultProps, label: '$treeNameColumn.javaField'}"
+                        #end
+                        check-strictly
+                        default-expand-all
+                        placeholder="请选择${comment}"
+                    />
+                  </el-form-item>
+                #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+                  <el-form-item label="${comment}" prop="${javaField}">
+                    <el-input v-model="formData.${javaField}" placeholder="请输入${comment}" />
+                  </el-form-item>
+                #elseif($column.htmlType == "imageUpload")## 图片上传
+                  <el-form-item label="${comment}" prop="${javaField}">
+                    <UploadImg v-model="formData.${javaField}" />
+                  </el-form-item>
+                #elseif($column.htmlType == "fileUpload")## 文件上传
+                  <el-form-item label="${comment}" prop="${javaField}">
+                    <UploadFile v-model="formData.${javaField}" />
+                  </el-form-item>
+                #elseif($column.htmlType == "editor")## 文本编辑器
+                  <el-form-item label="${comment}" prop="${javaField}">
+                    <Editor v-model="formData.${javaField}" height="150px" />
+                  </el-form-item>
+                #elseif($column.htmlType == "select")## 下拉框
+                  <el-form-item label="${comment}" prop="${javaField}">
+                    <el-select v-model="formData.${javaField}" placeholder="请选择${comment}">
+                        #if ("" != $dictType)## 有数据字典
+                          <el-option
+                              v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+                              :key="dict.value"
+                              :label="dict.label"
+                              :value="dict.value"
+                          />
+                        #else##没数据字典
+                          <el-option label="请选择字典生成" value="" />
+                        #end
+                    </el-select>
+                  </el-form-item>
+                #elseif($column.htmlType == "checkbox")## 多选框
+                  <el-form-item label="${comment}" prop="${javaField}">
+                    <el-checkbox-group v-model="formData.${javaField}">
+                        #if ("" != $dictType)## 有数据字典
+                          <el-checkbox
+                              v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+                              :key="dict.value"
+                              :label="dict.value"
+                          >
+                            {{ dict.label }}
+                          </el-checkbox>
+                        #else##没数据字典
+                          <el-checkbox>请选择字典生成</el-checkbox>
+                        #end
+                    </el-checkbox-group>
+                  </el-form-item>
+                #elseif($column.htmlType == "radio")## 单选框
+                  <el-form-item label="${comment}" prop="${javaField}">
+                    <el-radio-group v-model="formData.${javaField}">
+                        #if ("" != $dictType)## 有数据字典
+                          <el-radio
+                              v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+                              :key="dict.value"
+                              :label="dict.value"
+                          >
+                            {{ dict.label }}
+                          </el-radio>
+                        #else##没数据字典
+                          <el-radio label="1">请选择字典生成</el-radio>
+                        #end
+                    </el-radio-group>
+                  </el-form-item>
+                #elseif($column.htmlType == "datetime")## 时间框
+                  <el-form-item label="${comment}" prop="${javaField}">
+                    <el-date-picker
+                        v-model="formData.${javaField}"
+                        type="date"
+                        value-format="x"
+                        placeholder="选择${comment}"
+                    />
+                  </el-form-item>
+                #elseif($column.htmlType == "textarea")## 文本框
+                  <el-form-item label="${comment}" prop="${javaField}">
+                    <el-input v-model="formData.${javaField}" type="textarea" placeholder="请输入${comment}" />
+                  </el-form-item>
                 #end
-        </el-checkbox-group>
-      </el-form-item>
-        #elseif($column.htmlType == "radio")## 单选框
-      <el-form-item label="${comment}" prop="${javaField}">
-        <el-radio-group v-model="formData.${javaField}">
-                #if ("" != $dictType)## 有数据字典
-          <el-radio
-            v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-radio>
-                #else##没数据字典
-          <el-radio label="1">请选择字典生成</el-radio>
-                #end
-        </el-radio-group>
-      </el-form-item>
-        #elseif($column.htmlType == "datetime")## 时间框
-      <el-form-item label="${comment}" prop="${javaField}">
-        <el-date-picker
-          v-model="formData.${javaField}"
-          type="date"
-          value-format="x"
-          placeholder="选择${comment}"
-        />
-      </el-form-item>
-        #elseif($column.htmlType == "textarea")## 文本框
-      <el-form-item label="${comment}" prop="${javaField}">
-        <el-input v-model="formData.${javaField}" type="textarea" placeholder="请输入${comment}" />
-      </el-form-item>
+            #end
         #end
-    #end
-#end
     </el-form>
-## 特殊:主子表专属逻辑
-#if ( $table.templateType == 10 || $table.templateType == 12 )
-    <!-- 子表的表单 -->
-    <el-tabs v-model="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))
-      <el-tab-pane label="${subTable.classComment}" name="$subClassNameVar">
-        <${subSimpleClassName}Form ref="${subClassNameVar}FormRef" :${subJoinColumn_strikeCase}="formData.id" />
-      </el-tab-pane>
-    #end
-    </el-tabs>
-#end
+      ## 特殊:主子表专属逻辑
+      #if ( $table.templateType == 10 || $table.templateType == 12 )
+        <!-- 子表的表单 -->
+        <el-tabs v-model="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))
+              <el-tab-pane label="${subTable.classComment}" name="$subClassNameVar">
+                <${subSimpleClassName}Form ref="${subClassNameVar}FormRef" :${subJoinColumn_strikeCase}="formData.id" />
+              </el-tab-pane>
+            #end
+        </el-tabs>
+      #end
     <template #footer>
       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
@@ -139,160 +139,160 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
-import * as ${simpleClassName}Api 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
+  import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+  import * as ${simpleClassName}Api 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
 
-const { t } = useI18n() // 国际化
-const message = useMessage() // 消息弹窗
+  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 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,
+          /** 子表的表单 */
+          const subTabsName = ref('$subClassNameVars.get(0)')
+              #foreach ($subClassNameVar in $subClassNameVars)
+              const ${subClassNameVar}FormRef = ref()
+              #end
+          #end
       #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 open = async (type: string, id?: number) => {
-  dialogVisible.value = true
-  dialogTitle.value = t('action.' + type)
-  formType.value = type
-  resetForm()
-  // 修改时,设置数据
-  if (id) {
+  /** 提交表单 */
+  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 {
-      formData.value = await ${simpleClassName}Api.get${simpleClassName}(id)
+      const data = formData.value as unknown as ${simpleClassName}Api.${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
     }
   }
-## 特殊:树表专属逻辑
-#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}Api.${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'))
+  /** 重置表单 */
+  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
     }
-    dialogVisible.value = false
-    // 发送操作成功的事件
-    emit('success')
-  } finally {
-    formLoading.value = false
+    formRef.value?.resetFields()
   }
-}
+      ## 特殊:树表专属逻辑
+      #if ( $table.templateType == 2 )
 
-/** 重置表单 */
-const resetForm = () => {
-  formData.value = {
-#foreach ($column in $columns)
-  #if ($column.createOperation || $column.updateOperation)
-      #if ($column.htmlType == "checkbox")
-    $column.javaField: [],
-      #else
-    $column.javaField: undefined,
+      /** 获得${table.classComment}树 */
+      const get${simpleClassName}Tree = async () => {
+              ${classNameVar}Tree.value = []
+        const data = await ${simpleClassName}Api.get${simpleClassName}List()
+        const root: Tree = { id: 0, name: '顶级${table.classComment}', children: [] }
+        root.children = handleTree(data, 'id', '${treeParentColumn.javaField}')
+              ${classNameVar}Tree.value.push(root)
+      }
       #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: Tree = { id: 0, name: '顶级${table.classComment}', children: [] }
-  root.children = handleTree(data, 'id', '${treeParentColumn.javaField}')
-  ${classNameVar}Tree.value.push(root)
-}
-#end
 </script>

+ 317 - 314
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm

@@ -2,372 +2,375 @@
   <ContentWrap>
     <!-- 搜索工作栏 -->
     <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
+        class="-mb-15px"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="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")
-      <el-form-item label="${comment}" prop="${javaField}">
-        <el-input
-          v-model="queryParams.${javaField}"
-          placeholder="请输入${comment}"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-            #elseif ($column.htmlType == "select" || $column.htmlType == "radio")
-      <el-form-item label="${comment}" prop="${javaField}">
-        <el-select
-          v-model="queryParams.${javaField}"
-          placeholder="请选择${comment}"
-          clearable
-          class="!w-240px"
-        >
-                #if ("" != $dictType)## 设置了 dictType 数据字典的情况
-          <el-option
-            v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-                #else## 未设置 dictType 数据字典的情况
-          <el-option label="请选择字典生成" value="" />
+        #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
-        </el-select>
-      </el-form-item>
-    #elseif($column.htmlType == "datetime")
-      #if ($column.listOperationCondition != "BETWEEN")## 非范围
-      <el-form-item label="${comment}" prop="${javaField}">
-        <el-date-picker
-          v-model="queryParams.${javaField}"
-          value-format="YYYY-MM-DD"
-          type="date"
-          placeholder="选择${comment}"
-          clearable
-          class="!w-240px"
-        />
-      </el-form-item>
-      #else## 范围
-      <el-form-item label="${comment}" prop="${javaField}">
-        <el-date-picker
-          v-model="queryParams.${javaField}"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          type="daterange"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-240px"
-        />
-      </el-form-item>
-      #end
-    #end
-    #end
-    #end
+                #if ($column.htmlType == "input")
+                  <el-form-item label="${comment}" prop="${javaField}">
+                    <el-input
+                        v-model="queryParams.${javaField}"
+                        placeholder="请输入${comment}"
+                        clearable
+                        @keyup.enter="handleQuery"
+                        class="!w-240px"
+                    />
+                  </el-form-item>
+                #elseif ($column.htmlType == "select" || $column.htmlType == "radio")
+                  <el-form-item label="${comment}" prop="${javaField}">
+                    <el-select
+                        v-model="queryParams.${javaField}"
+                        placeholder="请选择${comment}"
+                        clearable
+                        class="!w-240px"
+                    >
+                        #if ("" != $dictType)## 设置了 dictType 数据字典的情况
+                          <el-option
+                              v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+                              :key="dict.value"
+                              :label="dict.label"
+                              :value="dict.value"
+                          />
+                        #else## 未设置 dictType 数据字典的情况
+                          <el-option label="请选择字典生成" value="" />
+                        #end
+                    </el-select>
+                  </el-form-item>
+                #elseif($column.htmlType == "datetime")
+                    #if ($column.listOperationCondition != "BETWEEN")## 非范围
+                      <el-form-item label="${comment}" prop="${javaField}">
+                        <el-date-picker
+                            v-model="queryParams.${javaField}"
+                            value-format="YYYY-MM-DD"
+                            type="date"
+                            placeholder="选择${comment}"
+                            clearable
+                            class="!w-240px"
+                        />
+                      </el-form-item>
+                    #else## 范围
+                      <el-form-item label="${comment}" prop="${javaField}">
+                        <el-date-picker
+                            v-model="queryParams.${javaField}"
+                            value-format="YYYY-MM-DD HH:mm:ss"
+                            type="daterange"
+                            start-placeholder="开始日期"
+                            end-placeholder="结束日期"
+                            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+                            class="!w-240px"
+                        />
+                      </el-form-item>
+                    #end
+                #end
+            #end
+        #end
       <el-form-item>
         <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
         <el-button
-          type="primary"
-          plain
-          @click="openForm('create')"
-          v-hasPermi="['${permissionPrefix}:create']"
+            type="primary"
+            plain
+            @click="openForm('create')"
+            v-hasPermi="['${permissionPrefix}:create']"
         >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['${permissionPrefix}:export']"
+            type="success"
+            plain
+            @click="handleExport"
+            :loading="exportLoading"
+            v-hasPermi="['${permissionPrefix}:export']"
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
-## 特殊:树表专属逻辑
-#if ( $table.templateType == 2 )
-        <el-button type="danger" plain @click="toggleExpandAll">
-          <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
-        </el-button>
-#end
+          ## 特殊:树表专属逻辑
+          #if ( $table.templateType == 2 )
+            <el-button type="danger" plain @click="toggleExpandAll">
+              <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
+            </el-button>
+          #end
       </el-form-item>
     </el-form>
   </ContentWrap>
 
   <!-- 列表 -->
   <ContentWrap>
-## 特殊:主子表专属逻辑
-#if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
-    <el-table
-      v-loading="loading"
-      :data="list"
-      :stripe="true"
-      :show-overflow-tooltip="true"
-      highlight-current-row
-      @current-change="handleCurrentChange"
-    >
-## 特殊:树表专属逻辑
-#elseif ( $table.templateType == 2 )
-    <el-table
-      v-loading="loading"
-      :data="list"
-      :stripe="true"
-      :show-overflow-tooltip="true"
-      row-key="id"
-      :default-expand-all="isExpandAll"
-      v-if="refreshTable"
-    >
-#else
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-#end
-## 特殊:主子表专属逻辑
-#if ( $table.templateType == 12 && $subTables && $subTables.size() > 0 )
-      <!-- 子表的列表 -->
-      <el-table-column type="expand">
-        <template #default="scope">
-          <el-tabs model-value="$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))
-            <el-tab-pane label="${subTable.classComment}" name="$subClassNameVar">
-              <${subSimpleClassName}List :${subJoinColumn_strikeCase}="scope.row.id" />
-            </el-tab-pane>
-            #end
-          </el-tabs>
-        </template>
-      </el-table-column>
-#end
+      ## 特殊:主子表专属逻辑
+      #if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
+      <el-table
+          v-loading="loading"
+          :data="list"
+          :stripe="true"
+          :show-overflow-tooltip="true"
+          highlight-current-row
+          @current-change="handleCurrentChange"
+      >
+          ## 特殊:树表专属逻辑
+      #elseif ( $table.templateType == 2 )
+      <el-table
+          v-loading="loading"
+          :data="list"
+          :stripe="true"
+          :show-overflow-tooltip="true"
+          row-key="id"
+          :default-expand-all="isExpandAll"
+          v-if="refreshTable"
+      >
+      #else
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      #end
+      ## 特殊:主子表专属逻辑
+      #if ( $table.templateType == 12 && $subTables && $subTables.size() > 0 )
+        <!-- 子表的列表 -->
+        <el-table-column type="expand">
+          <template #default="scope">
+            <el-tabs model-value="$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))
+                  <el-tab-pane label="${subTable.classComment}" name="$subClassNameVar">
+                    <${subSimpleClassName}List :${subJoinColumn_strikeCase}="scope.row.id" />
+                  </el-tab-pane>
+                #end
+            </el-tabs>
+          </template>
+        </el-table-column>
+      #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")## 时间类型
-      <el-table-column
-        label="${comment}"
-        align="center"
-        prop="${javaField}"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-        #elseif($column.dictType && "" != $column.dictType)## 数据字典
-      <el-table-column label="${comment}" align="center" prop="${javaField}">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.${column.javaField}" />
-        </template>
-      </el-table-column>
-        #else
-      <el-table-column label="${comment}" align="center" prop="${javaField}" />
-        #end
+          #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")## 时间类型
+                <el-table-column
+                    label="${comment}"
+                    align="center"
+                    prop="${javaField}"
+                    :formatter="dateFormatter"
+                    width="180px"
+                />
+              #elseif($column.dictType && "" != $column.dictType)## 数据字典
+                <el-table-column label="${comment}" align="center" prop="${javaField}">
+                  <template #default="scope">
+                    <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.${column.javaField}" />
+                  </template>
+                </el-table-column>
+              #else
+                <el-table-column label="${comment}" align="center" prop="${javaField}" />
+              #end
+          #end
       #end
-    #end
-      <el-table-column label="操作" align="center">
-        <template #default="scope">
-          <el-button
+    <el-table-column label="操作" align="center">
+      <template #default="scope">
+        <el-button
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
             v-hasPermi="['${permissionPrefix}:update']"
-          >
-            编辑
-          </el-button>
-          <el-button
+        >
+          编辑
+        </el-button>
+        <el-button
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
             v-hasPermi="['${permissionPrefix}:delete']"
-          >
-            删除
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
+        >
+          删除
+        </el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+## 特殊:树表专属逻辑(树不需要分页)
+#if ( $table.templateType != 2 )
     <!-- 分页 -->
     <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
     />
+#end
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
   <${simpleClassName}Form ref="formRef" @success="getList" />
-## 特殊:主子表专属逻辑
-#if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
-  <!-- 子表的列表 -->
-  <ContentWrap>
-    <el-tabs model-value="$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))
-      <el-tab-pane label="${subTable.classComment}" name="$subClassNameVar">
-        <${subSimpleClassName}List :${subJoinColumn_strikeCase}="currentRow.id" />
-      </el-tab-pane>
-      #end
-    </el-tabs>
-  </ContentWrap>
-#end
+    ## 特殊:主子表专属逻辑
+    #if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
+      <!-- 子表的列表 -->
+      <ContentWrap>
+        <el-tabs model-value="$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))
+              <el-tab-pane label="${subTable.classComment}" name="$subClassNameVar">
+                <${subSimpleClassName}List :${subJoinColumn_strikeCase}="currentRow.id" />
+              </el-tab-pane>
+            #end
+        </el-tabs>
+      </ContentWrap>
+    #end
 </template>
 
 <script setup lang="ts">
-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 * as ${simpleClassName}Api 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
+  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 * as ${simpleClassName}Api 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
 
-defineOptions({ name: '${table.className}' })
+  defineOptions({ name: '${table.className}' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
+  const message = useMessage() // 消息弹窗
+  const { t } = useI18n() // 国际化
 
-const loading = ref(true) // 列表的加载中
-const list = ref([]) // 列表的数据
-## 特殊:树表专属逻辑(树不需要分页接口)
-#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: null,
-  #end
-      #if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
-  $column.javaField: [],
+  const loading = ref(true) // 列表的加载中
+  const list = ref([]) // 列表的数据
+      ## 特殊:树表专属逻辑(树不需要分页接口)
+      #if ( $table.templateType != 2 )
+      const total = ref(0) // 列表的总页数
       #end
-    #end
-  #end
-})
-const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
+  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: null,
+              #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 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 handleQuery = () => {
+    queryParams.pageNo = 1
+    getList()
+  }
 
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  handleQuery()
-}
+  /** 重置按钮操作 */
+  const resetQuery = () => {
+    queryFormRef.value.resetFields()
+    handleQuery()
+  }
 
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
-}
+  /** 添加/修改操作 */
+  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 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
+  /** 导出按钮操作 */
+  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 )
+      ## 特殊:主子表专属逻辑
+      #if ( $table.templateType == 11 )
 
-/** 选中行操作 */
-const currentRow = ref({}) // 选中行
-const handleCurrentChange = (row) => {
-  currentRow.value = row
-}
-#end
-## 特殊:树表专属逻辑
-#if ( $table.templateType == 2 )
+      /** 选中行操作 */
+      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
+      /** 展开/折叠操作 */
+      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()
-})
+  /** 初始化 **/
+  onMounted(() => {
+    getList()
+  })
 </script>

+ 202 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineVue2Test.java

@@ -0,0 +1,202 @@
+package cn.iocoder.yudao.module.infra.service.codegen.inner;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.io.resource.ResourceUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.ZipUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
+import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
+import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
+import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Spy;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * {@link CodegenEngine} 的单元测试
+ *
+ * @author 芋道源码
+ */
+public class CodegenEngineVue2Test extends BaseMockitoUnitTest {
+
+    @InjectMocks
+    private CodegenEngine codegenEngine;
+
+    @Spy
+    private CodegenProperties codegenProperties = new CodegenProperties()
+            .setBasePackage("cn.iocoder.yudao");
+
+    @BeforeEach
+    public void setUp() {
+        codegenEngine.initGlobalBindingMap();
+    }
+
+    @Test
+    public void testExecute_vue2_one() {
+        // 准备参数
+        CodegenTableDO table = getTable("student")
+                .setFrontType(CodegenFrontTypeEnum.VUE2.getType())
+                .setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
+        List<CodegenColumnDO> columns = getColumnList("student");
+
+        // 调用
+        Map<String, String> result = codegenEngine.execute(table, columns, null, null);
+        // 断言
+        assertResult(result, "codegen/vue2_one");
+//        writeResult(result, "/root/ruoyi-vue-pro/yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one");
+    }
+
+    @Test
+    public void testExecute_vue2_tree() {
+        // 准备参数
+        CodegenTableDO table = getTable("category")
+                .setFrontType(CodegenFrontTypeEnum.VUE2.getType())
+                .setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
+        List<CodegenColumnDO> columns = getColumnList("category");
+
+        // 调用
+        Map<String, String> result = codegenEngine.execute(table, columns, null, null);
+        // 断言
+        assertResult(result, "codegen/vue2_tree");
+//        writeResult(result, "/root/ruoyi-vue-pro/yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_tree");
+//        writeFile(result, "/Users/yunai/test/demo66.zip");
+    }
+
+    @Test
+    public void testExecute_vue2_master_normal() {
+        testExecute_vue2_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "codegen/vue2_master_normal");
+    }
+
+    @Test
+    public void testExecute_vue2_master_erp() {
+        testExecute_vue2_master(CodegenTemplateTypeEnum.MASTER_ERP, "codegen/vue2_master_erp");
+    }
+
+    @Test
+    public void testExecute_vue2_master_inner() {
+        testExecute_vue2_master(CodegenTemplateTypeEnum.MASTER_INNER, "codegen/vue2_master_inner");
+    }
+
+    private void testExecute_vue2_master(CodegenTemplateTypeEnum templateType,
+                                         String path) {
+        // 准备参数
+        CodegenTableDO table = getTable("student")
+                .setFrontType(CodegenFrontTypeEnum.VUE2.getType())
+                .setTemplateType(templateType.getType());
+        List<CodegenColumnDO> columns = getColumnList("student");
+        // 准备参数(子表)
+        CodegenTableDO contactTable = getTable("contact")
+                .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
+                .setFrontType(CodegenFrontTypeEnum.VUE2.getType())
+                .setSubJoinColumnId(100L).setSubJoinMany(true);
+        List<CodegenColumnDO> contactColumns = getColumnList("contact");
+        // 准备参数(班主任)
+        CodegenTableDO teacherTable = getTable("teacher")
+                .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
+                .setFrontType(CodegenFrontTypeEnum.VUE2.getType())
+                .setSubJoinColumnId(200L).setSubJoinMany(false);
+        List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
+
+        // 调用
+        Map<String, String> result = codegenEngine.execute(table, columns,
+                Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
+        // 断言
+        assertResult(result, path);
+//        writeResult(result, "/root/ruoyi-vue-pro/yudao-module-infra/yudao-module-infra-biz/src/test/resources/" + path);
+//        writeFile(result, "/Users/yunai/test/demo11.zip");
+    }
+
+    private static CodegenTableDO getTable(String name) {
+        String content = ResourceUtil.readUtf8Str("codegen/table/" + name + ".json");
+        return JsonUtils.parseObject(content, "table", CodegenTableDO.class);
+    }
+
+    private static List<CodegenColumnDO> getColumnList(String name) {
+        String content = ResourceUtil.readUtf8Str("codegen/table/" + name + ".json");
+        List<CodegenColumnDO> list = JsonUtils.parseArray(content, "columns", CodegenColumnDO.class);
+        list.forEach(column -> {
+            if (column.getNullable() == null) {
+                column.setNullable(false);
+            }
+            if (column.getCreateOperation() == null) {
+                column.setCreateOperation(false);
+            }
+            if (column.getUpdateOperation() == null) {
+                column.setUpdateOperation(false);
+            }
+            if (column.getListOperation() == null) {
+                column.setListOperation(false);
+            }
+            if (column.getListOperationResult() == null) {
+                column.setListOperationResult(false);
+            }
+        });
+        return list;
+    }
+
+    @SuppressWarnings("rawtypes")
+    private static void assertResult(Map<String, String> result, String path) {
+        String assertContent = ResourceUtil.readUtf8Str(path + "/assert.json");
+        List<HashMap> asserts = JsonUtils.parseArray(assertContent, HashMap.class);
+        assertEquals(asserts.size(), result.size());
+        // 校验每个文件
+        asserts.forEach(assertMap -> {
+            String contentPath = (String) assertMap.get("contentPath");
+            String filePath = (String) assertMap.get("filePath");
+            String content = ResourceUtil.readUtf8Str(path + "/" + contentPath);
+            assertEquals(content, result.get(filePath), filePath + ":不匹配");
+        });
+    }
+
+    // ==================== 调试专用 ====================
+
+    /**
+     * 【调试使用】将生成的代码,写入到文件
+     *
+     * @param result 生成的代码
+     * @param path 写入文件的路径
+     */
+    private void writeFile(Map<String, String> result, String path) {
+        // 生成压缩包
+        String[] paths = result.keySet().toArray(new String[0]);
+        ByteArrayInputStream[] ins = result.values().stream().map(IoUtil::toUtf8Stream).toArray(ByteArrayInputStream[]::new);
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        ZipUtil.zip(outputStream, paths, ins);
+        // 写入文件
+        FileUtil.writeBytes(outputStream.toByteArray(), path);
+    }
+
+    /**
+     * 【调试使用】将生成的结果,写入到文件
+     *
+     * @param result 生成的代码
+     * @param basePath 写入文件的路径(绝对路径)
+     */
+    private void writeResult(Map<String, String> result, String basePath) {
+        // 写入文件内容
+        List<Map<String, String>> asserts = new ArrayList<>();
+        result.forEach((filePath, fileContent) -> {
+            String lastFilePath = StrUtil.subAfter(filePath, '/', true);
+            String contentPath = StrUtil.subAfter(lastFilePath, '.', true)
+                    + '/' + StrUtil.subBefore(lastFilePath, '.', true);
+            asserts.add(MapUtil.<String, String>builder().put("filePath", filePath)
+                    .put("contentPath", contentPath).build());
+            FileUtil.writeUtf8String(fileContent, basePath + "/" + contentPath);
+        });
+        // 写入 assert.json 文件
+        FileUtil.writeUtf8String(JsonUtils.toJsonPrettyString(asserts), basePath +"/assert.json");
+    }
+
+}

+ 73 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/assert.json

@@ -0,0 +1,73 @@
+[ {
+  "contentPath" : "java/InfraStudentPageReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentRespVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
+}, {
+  "contentPath" : "java/InfraStudentSaveReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentController",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
+}, {
+  "contentPath" : "java/InfraStudentDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
+}, {
+  "contentPath" : "java/InfraStudentContactDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
+}, {
+  "contentPath" : "java/InfraStudentTeacherDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
+}, {
+  "contentPath" : "java/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
+}, {
+  "contentPath" : "java/InfraStudentContactMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
+}, {
+  "contentPath" : "java/InfraStudentTeacherMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
+}, {
+  "contentPath" : "xml/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/demo/InfraStudentMapper.xml"
+}, {
+  "contentPath" : "java/InfraStudentServiceImpl",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
+}, {
+  "contentPath" : "java/InfraStudentService",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
+}, {
+  "contentPath" : "java/InfraStudentServiceImplTest",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
+}, {
+  "contentPath" : "java/ErrorCodeConstants_手动操作",
+  "filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
+}, {
+  "contentPath" : "sql/sql",
+  "filePath" : "sql/sql.sql"
+}, {
+  "contentPath" : "sql/h2",
+  "filePath" : "sql/h2.sql"
+}, {
+  "contentPath" : "vue/index",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/index.vue"
+}, {
+  "contentPath" : "js/student",
+  "filePath" : "yudao-ui-admin-vue2/src/api/infra/student.js"
+}, {
+  "contentPath" : "vue/StudentForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/StudentForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentContactForm.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentTeacherForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactList",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentContactList.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherList",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentTeacherList.vue"
+} ]

+ 6 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/ErrorCodeConstants_手动操作

@@ -0,0 +1,6 @@
+// TODO 待办:请将下面的错误码复制到 yudao-module-infra-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
+// ========== 学生 TODO 补充编号 ==========
+ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
+ErrorCode STUDENT_CONTACT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生联系人不存在");
+ErrorCode STUDENT_TEACHER_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任不存在");
+ErrorCode STUDENT_TEACHER_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任已存在");

+ 71 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentContactDO

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生联系人 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student_contact")
+@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentContactDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 30 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentContactMapper

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生联系人 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
+
+    default PageResult<InfraStudentContactDO> selectPage(PageParam reqVO, Long studentId) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentContactDO>()
+            .eq(InfraStudentContactDO::getStudentId, studentId)
+            .orderByDesc(InfraStudentContactDO::getId));
+    }
+
+    default int deleteByStudentId(Long studentId) {
+        return delete(InfraStudentContactDO::getStudentId, studentId);
+    }
+
+}

+ 183 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentController

@@ -0,0 +1,183 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
+
+@Tag(name = "管理后台 - 学生")
+@RestController
+@RequestMapping("/infra/student")
+@Validated
+public class InfraStudentController {
+
+    @Resource
+    private InfraStudentService studentService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:create')")
+    public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
+        return success(studentService.createStudent(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:update')")
+    public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
+        studentService.updateStudent(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除学生")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:student:delete')")
+    public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
+        studentService.deleteStudent(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得学生")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
+        InfraStudentDO student = studentService.getStudent(id);
+        return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得学生分页")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
+        PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出学生 Excel")
+    @PreAuthorize("@ss.hasPermission('infra:student:export')")
+    @OperateLog(type = EXPORT)
+    public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
+                        BeanUtils.toBean(list, InfraStudentRespVO.class));
+    }
+
+    // ==================== 子表(学生联系人) ====================
+
+    @GetMapping("/student-contact/page")
+    @Operation(summary = "获得学生联系人分页")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<PageResult<InfraStudentContactDO>> getStudentContactPage(PageParam pageReqVO,
+                                                                                        @RequestParam("studentId") Long studentId) {
+        return success(studentService.getStudentContactPage(pageReqVO, studentId));
+    }
+
+    @PostMapping("/student-contact/create")
+    @Operation(summary = "创建学生联系人")
+    @PreAuthorize("@ss.hasPermission('infra:student:create')")
+    public CommonResult<Long> createStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
+        return success(studentService.createStudentContact(studentContact));
+    }
+
+    @PutMapping("/student-contact/update")
+    @Operation(summary = "更新学生联系人")
+    @PreAuthorize("@ss.hasPermission('infra:student:update')")
+    public CommonResult<Boolean> updateStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
+        studentService.updateStudentContact(studentContact);
+        return success(true);
+    }
+
+    @DeleteMapping("/student-contact/delete")
+    @Parameter(name = "id", description = "编号", required = true)
+    @Operation(summary = "删除学生联系人")
+    @PreAuthorize("@ss.hasPermission('infra:student:delete')")
+    public CommonResult<Boolean> deleteStudentContact(@RequestParam("id") Long id) {
+        studentService.deleteStudentContact(id);
+        return success(true);
+    }
+
+	@GetMapping("/student-contact/get")
+	@Operation(summary = "获得学生联系人")
+	@Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+	public CommonResult<InfraStudentContactDO> getStudentContact(@RequestParam("id") Long id) {
+	    return success(studentService.getStudentContact(id));
+	}
+
+    // ==================== 子表(学生班主任) ====================
+
+    @GetMapping("/student-teacher/page")
+    @Operation(summary = "获得学生班主任分页")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<PageResult<InfraStudentTeacherDO>> getStudentTeacherPage(PageParam pageReqVO,
+                                                                                        @RequestParam("studentId") Long studentId) {
+        return success(studentService.getStudentTeacherPage(pageReqVO, studentId));
+    }
+
+    @PostMapping("/student-teacher/create")
+    @Operation(summary = "创建学生班主任")
+    @PreAuthorize("@ss.hasPermission('infra:student:create')")
+    public CommonResult<Long> createStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
+        return success(studentService.createStudentTeacher(studentTeacher));
+    }
+
+    @PutMapping("/student-teacher/update")
+    @Operation(summary = "更新学生班主任")
+    @PreAuthorize("@ss.hasPermission('infra:student:update')")
+    public CommonResult<Boolean> updateStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
+        studentService.updateStudentTeacher(studentTeacher);
+        return success(true);
+    }
+
+    @DeleteMapping("/student-teacher/delete")
+    @Parameter(name = "id", description = "编号", required = true)
+    @Operation(summary = "删除学生班主任")
+    @PreAuthorize("@ss.hasPermission('infra:student:delete')")
+    public CommonResult<Boolean> deleteStudentTeacher(@RequestParam("id") Long id) {
+        studentService.deleteStudentTeacher(id);
+        return success(true);
+    }
+
+	@GetMapping("/student-teacher/get")
+	@Operation(summary = "获得学生班主任")
+	@Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+	public CommonResult<InfraStudentTeacherDO> getStudentTeacher(@RequestParam("id") Long id) {
+	    return success(studentService.getStudentTeacher(id));
+	}
+
+}

+ 67 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentDO

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student")
+@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 30 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentMapper

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+
+/**
+ * 学生 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
+
+    default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
+                .likeIfPresent(InfraStudentDO::getName, reqVO.getName())
+                .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
+                .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
+                .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
+                .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(InfraStudentDO::getId));
+    }
+
+}

+ 34 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentPageReqVO

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 学生分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class InfraStudentPageReqVO extends PageParam {
+
+    @Schema(description = "名字", example = "芋头")
+    private String name;
+
+    @Schema(description = "出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", example = "1")
+    private Integer sex;
+
+    @Schema(description = "是否有效", example = "true")
+    private Boolean enabled;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 60 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentRespVO

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+@Schema(description = "管理后台 - 学生 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class InfraStudentRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @ExcelProperty("名字")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @ExcelProperty("简介")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty(value = "性别", converter = DictConvert.class)
+    @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @ExcelProperty(value = "是否有效", converter = DictConvert.class)
+    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @ExcelProperty("头像")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @ExcelProperty("附件")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @ExcelProperty("备注")
+    private String memo;
+
+    @Schema(description = "创建时间")
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 52 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentSaveReqVO

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+
+@Schema(description = "管理后台 - 学生新增/修改 Request VO")
+@Data
+public class InfraStudentSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @NotEmpty(message = "名字不能为空")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @NotEmpty(message = "简介不能为空")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "出生日期不能为空")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "性别不能为空")
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "是否有效不能为空")
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @NotEmpty(message = "头像不能为空")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @NotEmpty(message = "附件不能为空")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @NotEmpty(message = "备注不能为空")
+    private String memo;
+
+}

+ 139 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentService

@@ -0,0 +1,139 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+/**
+ * 学生 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface InfraStudentService {
+
+    /**
+     * 创建学生
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
+
+    /**
+     * 更新学生
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
+
+    /**
+     * 删除学生
+     *
+     * @param id 编号
+     */
+    void deleteStudent(Long id);
+
+    /**
+     * 获得学生
+     *
+     * @param id 编号
+     * @return 学生
+     */
+    InfraStudentDO getStudent(Long id);
+
+    /**
+     * 获得学生分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 学生分页
+     */
+    PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
+
+    // ==================== 子表(学生联系人) ====================
+
+    /**
+     * 获得学生联系人分页
+     *
+     * @param pageReqVO 分页查询
+     * @param studentId 学生编号
+     * @return 学生联系人分页
+     */
+    PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId);
+
+    /**
+     * 创建学生联系人
+     *
+     * @param studentContact 创建信息
+     * @return 编号
+     */
+    Long createStudentContact(@Valid InfraStudentContactDO studentContact);
+
+    /**
+     * 更新学生联系人
+     *
+     * @param studentContact 更新信息
+     */
+    void updateStudentContact(@Valid InfraStudentContactDO studentContact);
+
+    /**
+     * 删除学生联系人
+     *
+     * @param id 编号
+     */
+    void deleteStudentContact(Long id);
+
+	/**
+	 * 获得学生联系人
+	 *
+	 * @param id 编号
+     * @return 学生联系人
+	 */
+    InfraStudentContactDO getStudentContact(Long id);
+
+    // ==================== 子表(学生班主任) ====================
+
+    /**
+     * 获得学生班主任分页
+     *
+     * @param pageReqVO 分页查询
+     * @param studentId 学生编号
+     * @return 学生班主任分页
+     */
+    PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId);
+
+    /**
+     * 创建学生班主任
+     *
+     * @param studentTeacher 创建信息
+     * @return 编号
+     */
+    Long createStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
+
+    /**
+     * 更新学生班主任
+     *
+     * @param studentTeacher 更新信息
+     */
+    void updateStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
+
+    /**
+     * 删除学生班主任
+     *
+     * @param id 编号
+     */
+    void deleteStudentTeacher(Long id);
+
+	/**
+	 * 获得学生班主任
+	 *
+	 * @param id 编号
+     * @return 学生班主任
+	 */
+    InfraStudentTeacherDO getStudentTeacher(Long id);
+
+}

+ 180 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentServiceImpl

@@ -0,0 +1,180 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+
+/**
+ * 学生 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class InfraStudentServiceImpl implements InfraStudentService {
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+    @Resource
+    private InfraStudentContactMapper studentContactMapper;
+    @Resource
+    private InfraStudentTeacherMapper studentTeacherMapper;
+
+    @Override
+    public Long createStudent(InfraStudentSaveReqVO createReqVO) {
+        // 插入
+        InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
+        studentMapper.insert(student);
+        // 返回
+        return student.getId();
+    }
+
+    @Override
+    public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
+        // 校验存在
+        validateStudentExists(updateReqVO.getId());
+        // 更新
+        InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
+        studentMapper.updateById(updateObj);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteStudent(Long id) {
+        // 校验存在
+        validateStudentExists(id);
+        // 删除
+        studentMapper.deleteById(id);
+
+        // 删除子表
+        deleteStudentContactByStudentId(id);
+        deleteStudentTeacherByStudentId(id);
+    }
+
+    private void validateStudentExists(Long id) {
+        if (studentMapper.selectById(id) == null) {
+            throw exception(STUDENT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public InfraStudentDO getStudent(Long id) {
+        return studentMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
+        return studentMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 子表(学生联系人) ====================
+
+    @Override
+    public PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId) {
+        return studentContactMapper.selectPage(pageReqVO, studentId);
+    }
+
+    @Override
+    public Long createStudentContact(InfraStudentContactDO studentContact) {
+        studentContactMapper.insert(studentContact);
+        return studentContact.getId();
+    }
+
+    @Override
+    public void updateStudentContact(InfraStudentContactDO studentContact) {
+        // 校验存在
+        validateStudentContactExists(studentContact.getId());
+        // 更新
+        studentContactMapper.updateById(studentContact);
+    }
+
+    @Override
+    public void deleteStudentContact(Long id) {
+        // 校验存在
+        validateStudentContactExists(id);
+        // 删除
+        studentContactMapper.deleteById(id);
+    }
+
+    @Override
+    public InfraStudentContactDO getStudentContact(Long id) {
+        return studentContactMapper.selectById(id);
+    }
+
+    private void validateStudentContactExists(Long id) {
+        if (studentContactMapper.selectById(id) == null) {
+            throw exception(STUDENT_CONTACT_NOT_EXISTS);
+        }
+    }
+
+    private void deleteStudentContactByStudentId(Long studentId) {
+        studentContactMapper.deleteByStudentId(studentId);
+    }
+
+    // ==================== 子表(学生班主任) ====================
+
+    @Override
+    public PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId) {
+        return studentTeacherMapper.selectPage(pageReqVO, studentId);
+    }
+
+    @Override
+    public Long createStudentTeacher(InfraStudentTeacherDO studentTeacher) {
+        // 校验是否已经存在
+        if (studentTeacherMapper.selectByStudentId(studentTeacher.getStudentId()) != null) {
+            throw exception(STUDENT_TEACHER_EXISTS);
+        }
+        // 插入
+        studentTeacherMapper.insert(studentTeacher);
+        return studentTeacher.getId();
+    }
+
+    @Override
+    public void updateStudentTeacher(InfraStudentTeacherDO studentTeacher) {
+        // 校验存在
+        validateStudentTeacherExists(studentTeacher.getId());
+        // 更新
+        studentTeacherMapper.updateById(studentTeacher);
+    }
+
+    @Override
+    public void deleteStudentTeacher(Long id) {
+        // 校验存在
+        validateStudentTeacherExists(id);
+        // 删除
+        studentTeacherMapper.deleteById(id);
+    }
+
+    @Override
+    public InfraStudentTeacherDO getStudentTeacher(Long id) {
+        return studentTeacherMapper.selectById(id);
+    }
+
+    private void validateStudentTeacherExists(Long id) {
+        if (studentTeacherMapper.selectById(id) == null) {
+            throw exception(STUDENT_TEACHER_NOT_EXISTS);
+        }
+    }
+
+    private void deleteStudentTeacherByStudentId(Long studentId) {
+        studentTeacherMapper.deleteByStudentId(studentId);
+    }
+
+}

+ 146 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentServiceImplTest

@@ -0,0 +1,146 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link InfraStudentServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(InfraStudentServiceImpl.class)
+public class InfraStudentServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private InfraStudentServiceImpl studentService;
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+
+    @Test
+    public void testCreateStudent_success() {
+        // 准备参数
+        InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
+
+        // 调用
+        Long studentId = studentService.createStudent(createReqVO);
+        // 断言
+        assertNotNull(studentId);
+        // 校验记录的属性是否正确
+        InfraStudentDO student = studentMapper.selectById(studentId);
+        assertPojoEquals(createReqVO, student, "id");
+    }
+
+    @Test
+    public void testUpdateStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
+            o.setId(dbStudent.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        studentService.updateStudent(updateReqVO);
+        // 校验是否更新正确
+        InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
+        assertPojoEquals(updateReqVO, student);
+    }
+
+    @Test
+    public void testUpdateStudent_notExists() {
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbStudent.getId();
+
+        // 调用
+        studentService.deleteStudent(id);
+       // 校验数据不存在了
+       assertNull(studentMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteStudent_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetStudentPage() {
+       // mock 数据
+       InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
+           o.setName(null);
+           o.setBirthday(null);
+           o.setSex(null);
+           o.setEnabled(null);
+           o.setCreateTime(null);
+       });
+       studentMapper.insert(dbStudent);
+       // 测试 name 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
+       // 测试 birthday 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
+       // 测试 sex 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
+       // 测试 enabled 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
+       // 测试 createTime 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
+       // 准备参数
+       InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
+       reqVO.setName(null);
+       reqVO.setBirthday(null);
+       reqVO.setSex(null);
+       reqVO.setEnabled(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbStudent, pageResult.getList().get(0));
+    }
+
+}

+ 71 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentTeacherDO

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生班主任 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student_teacher")
+@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentTeacherDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 30 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentTeacherMapper

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生班主任 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
+
+    default PageResult<InfraStudentTeacherDO> selectPage(PageParam reqVO, Long studentId) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentTeacherDO>()
+            .eq(InfraStudentTeacherDO::getStudentId, studentId)
+            .orderByDesc(InfraStudentTeacherDO::getId));
+    }
+
+    default int deleteByStudentId(Long studentId) {
+        return delete(InfraStudentTeacherDO::getStudentId, studentId);
+    }
+
+}

+ 141 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/js/student

@@ -0,0 +1,141 @@
+import request from '@/utils/request'
+
+// 创建学生
+export function createStudent(data) {
+  return request({
+    url: '/infra/student/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新学生
+export function updateStudent(data) {
+  return request({
+    url: '/infra/student/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除学生
+export function deleteStudent(id) {
+  return request({
+    url: '/infra/student/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得学生
+export function getStudent(id) {
+  return request({
+    url: '/infra/student/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得学生分页
+export function getStudentPage(params) {
+  return request({
+    url: '/infra/student/page',
+    method: 'get',
+    params
+  })
+}
+// 导出学生 Excel
+export function exportStudentExcel(params) {
+  return request({
+    url: '/infra/student/export-excel',
+    method: 'get',
+    params,
+    responseType: 'blob'
+  })
+}
+
+// ==================== 子表(学生联系人) ====================
+  
+  // 获得学生联系人分页
+  export function getStudentContactPage(params) {
+    return request({
+      url: '/infra/student/student-contact/page',
+      method: 'get',
+      params
+    })
+  }
+        // 新增学生联系人
+  export function createStudentContact(data) {
+    return request({
+      url: `/infra/student/student-contact/create`,
+      method: 'post',
+      data
+    })
+  }
+
+  // 修改学生联系人
+  export function updateStudentContact(data) {
+    return request({
+      url: `/infra/student/student-contact/update`,
+      method: 'post',
+      data
+    })
+  }
+
+  // 删除学生联系人
+  export function deleteStudentContact(id) {
+    return request({
+      url: `/infra/student/student-contact/delete?id=` + id,
+      method: 'delete'
+    })
+  }
+
+  // 获得学生联系人
+  export function getStudentContact(id) {
+    return request({
+      url: `/infra/student/student-contact/get?id=` + id,
+      method: 'get'
+    })
+  }
+
+// ==================== 子表(学生班主任) ====================
+  
+  // 获得学生班主任分页
+  export function getStudentTeacherPage(params) {
+    return request({
+      url: '/infra/student/student-teacher/page',
+      method: 'get',
+      params
+    })
+  }
+        // 新增学生班主任
+  export function createStudentTeacher(data) {
+    return request({
+      url: `/infra/student/student-teacher/create`,
+      method: 'post',
+      data
+    })
+  }
+
+  // 修改学生班主任
+  export function updateStudentTeacher(data) {
+    return request({
+      url: `/infra/student/student-teacher/update`,
+      method: 'post',
+      data
+    })
+  }
+
+  // 删除学生班主任
+  export function deleteStudentTeacher(id) {
+    return request({
+      url: `/infra/student/student-teacher/delete?id=` + id,
+      method: 'delete'
+    })
+  }
+
+  // 获得学生班主任
+  export function getStudentTeacher(id) {
+    return request({
+      url: `/infra/student/student-teacher/get?id=` + id,
+      method: 'get'
+    })
+  }

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/sql/h2

@@ -0,0 +1,17 @@
+-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
+CREATE TABLE IF NOT EXISTS "infra_student" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name" varchar NOT NULL,
+    "description" varchar NOT NULL,
+    "birthday" varchar NOT NULL,
+    "sex" int NOT NULL,
+    "enabled" bit NOT NULL,
+    "avatar" varchar NOT NULL,
+    "video" varchar NOT NULL,
+    "memo" varchar NOT NULL,
+    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY ("id")
+) COMMENT '学生表';
+
+-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
+DELETE FROM "infra_student";

+ 55 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/sql/sql

@@ -0,0 +1,55 @@
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+    '学生管理', '', 2, 0, 888,
+    'student', '', 'infra/demo/index', 0, 'InfraStudent'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生查询', 'infra:student:query', 3, 1, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生创建', 'infra:student:create', 3, 2, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生更新', 'infra:student:update', 3, 3, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生删除', 'infra:student:delete', 3, 4, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生导出', 'infra:student:export', 3, 5, @parentId,
+    '', '', '', 0
+);

+ 159 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentContactForm

@@ -0,0 +1,159 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+                     <el-form-item label="名字" prop="name">
+                      <el-input v-model="formData.name" placeholder="请输入名字" />
+                    </el-form-item>
+                    <el-form-item label="简介" prop="description">
+                      <el-input v-model="formData.description" type="textarea" placeholder="请输入内容" />
+                    </el-form-item>
+                    <el-form-item label="出生日期" prop="birthday">
+                      <el-date-picker clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item label="是否有效" prop="enabled">
+                      <el-radio-group v-model="formData.enabled">
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                                      :key="dict.value" :label="dict.value">{{dict.label}}</el-radio>
+                      </el-radio-group>
+                    </el-form-item>
+                    <el-form-item label="头像" prop="avatar">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件" prop="video">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注" prop="memo">
+                      <editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+      import ImageUpload from '@/components/ImageUpload';
+      import FileUpload from '@/components/FileUpload';
+      import Editor from '@/components/Editor';
+  export default {
+    name: "StudentContactForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+                            id: undefined,
+                            studentId: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        },
+        // 表单校验
+        formRules: {
+                        studentId: [{ required: true, message: "学生编号不能为空", trigger: "blur" }],
+                        name: [{ required: true, message: "名字不能为空", trigger: "blur" }],
+                        description: [{ required: true, message: "简介不能为空", trigger: "blur" }],
+                        birthday: [{ required: true, message: "出生日期不能为空", trigger: "blur" }],
+                        sex: [{ required: true, message: "性别不能为空", trigger: "change" }],
+                        enabled: [{ required: true, message: "是否有效不能为空", trigger: "blur" }],
+                        avatar: [{ required: true, message: "头像不能为空", trigger: "blur" }],
+                        memo: [{ required: true, message: "备注不能为空", trigger: "blur" }],
+        },
+      };
+    },
+    methods: {
+      /** 打开弹窗 */
+      open(id, studentId) {
+        this.dialogVisible = true;
+        this.reset();
+        const that = this;
+        this.formData.studentId = studentId;
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+              StudentApi.getStudentContact(id).then(res=>{
+              that.formData = res.data;
+              that.dialogTitle = "修改学生联系人";
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.dialogTitle = "新增学生联系人";
+      },
+      /** 提交按钮 */
+      submitForm() {
+        this.formLoading = true;
+        try {
+          let data = this.formData;
+          this.$refs["formRef"].validate(valid => {
+            if (!valid) {
+              return;
+            }
+            // 修改的提交
+            if (data.id) {
+              StudentApi.updateStudentContact(data).then(response => {
+                this.$modal.msgSuccess("修改成功");
+                this.dialogVisible = false;
+                this.$emit('success');
+              });
+              return;
+            }
+            // 添加的提交
+            StudentApi.createStudentContact(data).then(response => {
+              this.$modal.msgSuccess("新增成功");
+              this.dialogVisible = false;
+              this.$emit('success');
+            });
+          });
+        }finally {
+          this.formLoading = false
+        }
+      },
+      /** 表单重置 */
+      reset() {
+        this.formData = {
+                            id: undefined,
+                            studentId: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        };
+        this.resetForm("formRef");
+      },
+    }
+  };
+</script>

+ 134 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentContactList

@@ -0,0 +1,134 @@
+<template>
+  <div class="app-container">
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['infra:student:create']">新增</el-button>
+      </el-col>
+    </el-row>
+            <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+                <el-table-column label="编号" align="center" prop="id" />
+                 <el-table-column label="名字" align="center" prop="name" />
+                <el-table-column label="简介" align="center" prop="description" />
+                <el-table-column label="出生日期" align="center" prop="birthday" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.birthday) }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="性别" align="center" prop="sex">
+                  <template v-slot="scope">
+                    <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="是否有效" align="center" prop="enabled">
+                  <template v-slot="scope">
+                    <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="头像" align="center" prop="avatar" />
+                <el-table-column label="附件" align="center" prop="video" />
+                <el-table-column label="备注" align="center" prop="memo" />
+                <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.createTime) }}</span>
+                  </template>
+                </el-table-column>
+    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <template v-slot="scope">
+        <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                   v-hasPermi="['infra:student:update']">修改</el-button>
+        <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                   v-hasPermi="['infra:student:delete']">删除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+  <!-- 对话框(添加 / 修改) -->
+  <StudentContactForm ref="formRef" @success="getList" />
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+  import StudentContactForm from './StudentContactForm.vue'
+  export default {
+    name: "StudentContactList",
+    components: {
+       StudentContactForm
+    },
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 遮罩层
+        loading: true,
+        // 列表的数据
+        list: [],
+        // 列表的总页数
+        total: 0,
+        // 查询参数
+        queryParams: {
+          pageNo: 1,
+          pageSize: 10,
+          studentId: undefined
+        }
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+        studentId:{
+            handler(val) {
+              this.queryParams.studentId = val;
+              if (val){
+                this.handleQuery();
+              }
+            },
+            immediate: true
+      }
+    },
+    methods: {
+      /** 查询列表 */
+      getList() {
+        try {
+          this.loading = true;
+          const that = this;
+          StudentApi.getStudentContactPage(this.queryParams).then(response => {
+            that.list = response.data.list;
+            that.total = response.data.total;
+          });
+        } finally {
+          this.loading = false;
+        }
+      },
+      /** 搜索按钮操作 */
+      handleQuery() {
+        this.queryParams.pageNo = 1;
+        this.getList();
+      },
+      /** 添加/修改操作 */
+      openForm(id) {
+        if (!this.studentId) {
+          that.$modal.msgError('请选择一个学生');
+          return;
+        }
+        this.$refs["formRef"].open(id, this.studentId);
+      },
+      /** 删除按钮操作 */
+      handleDelete(row) {
+        const that = this;
+        try {
+          const id = row.id;
+          this.$modal.confirm('是否确认删除学生编号为"' + id + '"的数据项?').then(()=>{
+            return StudentApi.deleteStudentContact(id);
+          }).then(() => {
+            that.getList();
+            that.$modal.msgSuccess("删除成功");
+          }).catch(() => {});
+        } catch {}
+      },
+    }
+  };
+</script>

+ 164 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentForm

@@ -0,0 +1,164 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+                    <el-form-item label="名字" prop="name">
+                      <el-input v-model="formData.name" placeholder="请输入名字" />
+                    </el-form-item>
+                    <el-form-item label="简介" prop="description">
+                      <el-input v-model="formData.description" type="textarea" placeholder="请输入内容" />
+                    </el-form-item>
+                    <el-form-item label="出生日期" prop="birthday">
+                      <el-date-picker clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item label="是否有效" prop="enabled">
+                      <el-radio-group v-model="formData.enabled">
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                                      :key="dict.value" :label="dict.value">{{dict.label}}</el-radio>
+                      </el-radio-group>
+                    </el-form-item>
+                    <el-form-item label="头像">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注">
+                      <Editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+              <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+  import ImageUpload from '@/components/ImageUpload';
+  import FileUpload from '@/components/FileUpload';
+  import Editor from '@/components/Editor';
+      export default {
+    name: "StudentForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+                    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        },
+        // 表单校验
+        formRules: {
+                        name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+                        description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+                        birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+                        sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+                        enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+                        avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+                        video: [{ required: true, message: '附件不能为空', trigger: 'blur' }],
+                        memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
+        },
+                        };
+    },
+    methods: {
+      /** 打开弹窗 */
+     open(id) {
+        this.dialogVisible = true;
+        this.reset();
+        const that = this;
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            StudentApi.getStudent(id).then(res=>{
+              that.formData = res.data;
+              that.title = "修改学生";
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.title = "新增学生";
+              },
+      /** 提交按钮 */
+      submitForm() {
+        this.formLoading = true;
+        try {
+          const that = this;
+          let data = this.formData;
+          let validate = false;
+          // 校验主表
+          this.getRef("formRef").validate(valid => {
+            validate = valid;
+          });
+                  // 所有表单校验通过后方可提交
+          if (!validate) {
+            return;
+          }
+          // 修改的提交
+          if (data.id) {
+                  StudentApi.updateStudent(data).then(response => {
+              that.$modal.msgSuccess("修改成功");
+              that.dialogVisible = false;
+              that.$emit('success');
+            });
+            return;
+          }
+          // 添加的提交
+          StudentApi.createStudent(data).then(response => {
+            that.$modal.msgSuccess("新增成功");
+            that.dialogVisible = false;
+            that.$emit('success');
+          });
+        }finally {
+          this.formLoading = false;
+        }
+      },
+      getRef(refName){
+        return this.$refs[refName];
+      },
+                      /** 表单重置 */
+      reset() {
+        this.formData = {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        };
+        this.resetForm("formRef");
+      },
+    }
+  };
+</script>

+ 159 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentTeacherForm

@@ -0,0 +1,159 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+                     <el-form-item label="名字" prop="name">
+                      <el-input v-model="formData.name" placeholder="请输入名字" />
+                    </el-form-item>
+                    <el-form-item label="简介" prop="description">
+                      <el-input v-model="formData.description" type="textarea" placeholder="请输入内容" />
+                    </el-form-item>
+                    <el-form-item label="出生日期" prop="birthday">
+                      <el-date-picker clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item label="是否有效" prop="enabled">
+                      <el-radio-group v-model="formData.enabled">
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                                      :key="dict.value" :label="dict.value">{{dict.label}}</el-radio>
+                      </el-radio-group>
+                    </el-form-item>
+                    <el-form-item label="头像" prop="avatar">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件" prop="video">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注" prop="memo">
+                      <editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+      import ImageUpload from '@/components/ImageUpload';
+      import FileUpload from '@/components/FileUpload';
+      import Editor from '@/components/Editor';
+  export default {
+    name: "StudentTeacherForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+                            id: undefined,
+                            studentId: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        },
+        // 表单校验
+        formRules: {
+                        studentId: [{ required: true, message: "学生编号不能为空", trigger: "blur" }],
+                        name: [{ required: true, message: "名字不能为空", trigger: "blur" }],
+                        description: [{ required: true, message: "简介不能为空", trigger: "blur" }],
+                        birthday: [{ required: true, message: "出生日期不能为空", trigger: "blur" }],
+                        sex: [{ required: true, message: "性别不能为空", trigger: "change" }],
+                        enabled: [{ required: true, message: "是否有效不能为空", trigger: "blur" }],
+                        avatar: [{ required: true, message: "头像不能为空", trigger: "blur" }],
+                        memo: [{ required: true, message: "备注不能为空", trigger: "blur" }],
+        },
+      };
+    },
+    methods: {
+      /** 打开弹窗 */
+      open(id, studentId) {
+        this.dialogVisible = true;
+        this.reset();
+        const that = this;
+        this.formData.studentId = studentId;
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+              StudentApi.getStudentTeacher(id).then(res=>{
+              that.formData = res.data;
+              that.dialogTitle = "修改学生班主任";
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.dialogTitle = "新增学生班主任";
+      },
+      /** 提交按钮 */
+      submitForm() {
+        this.formLoading = true;
+        try {
+          let data = this.formData;
+          this.$refs["formRef"].validate(valid => {
+            if (!valid) {
+              return;
+            }
+            // 修改的提交
+            if (data.id) {
+              StudentApi.updateStudentTeacher(data).then(response => {
+                this.$modal.msgSuccess("修改成功");
+                this.dialogVisible = false;
+                this.$emit('success');
+              });
+              return;
+            }
+            // 添加的提交
+            StudentApi.createStudentTeacher(data).then(response => {
+              this.$modal.msgSuccess("新增成功");
+              this.dialogVisible = false;
+              this.$emit('success');
+            });
+          });
+        }finally {
+          this.formLoading = false
+        }
+      },
+      /** 表单重置 */
+      reset() {
+        this.formData = {
+                            id: undefined,
+                            studentId: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        };
+        this.resetForm("formRef");
+      },
+    }
+  };
+</script>

+ 134 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentTeacherList

@@ -0,0 +1,134 @@
+<template>
+  <div class="app-container">
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['infra:student:create']">新增</el-button>
+      </el-col>
+    </el-row>
+            <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+                <el-table-column label="编号" align="center" prop="id" />
+                 <el-table-column label="名字" align="center" prop="name" />
+                <el-table-column label="简介" align="center" prop="description" />
+                <el-table-column label="出生日期" align="center" prop="birthday" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.birthday) }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="性别" align="center" prop="sex">
+                  <template v-slot="scope">
+                    <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="是否有效" align="center" prop="enabled">
+                  <template v-slot="scope">
+                    <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="头像" align="center" prop="avatar" />
+                <el-table-column label="附件" align="center" prop="video" />
+                <el-table-column label="备注" align="center" prop="memo" />
+                <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.createTime) }}</span>
+                  </template>
+                </el-table-column>
+    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <template v-slot="scope">
+        <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                   v-hasPermi="['infra:student:update']">修改</el-button>
+        <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                   v-hasPermi="['infra:student:delete']">删除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+  <!-- 对话框(添加 / 修改) -->
+  <StudentTeacherForm ref="formRef" @success="getList" />
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+  import StudentTeacherForm from './StudentTeacherForm.vue'
+  export default {
+    name: "StudentTeacherList",
+    components: {
+       StudentTeacherForm
+    },
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 遮罩层
+        loading: true,
+        // 列表的数据
+        list: [],
+        // 列表的总页数
+        total: 0,
+        // 查询参数
+        queryParams: {
+          pageNo: 1,
+          pageSize: 10,
+          studentId: undefined
+        }
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+        studentId:{
+            handler(val) {
+              this.queryParams.studentId = val;
+              if (val){
+                this.handleQuery();
+              }
+            },
+            immediate: true
+      }
+    },
+    methods: {
+      /** 查询列表 */
+      getList() {
+        try {
+          this.loading = true;
+          const that = this;
+          StudentApi.getStudentTeacherPage(this.queryParams).then(response => {
+            that.list = response.data.list;
+            that.total = response.data.total;
+          });
+        } finally {
+          this.loading = false;
+        }
+      },
+      /** 搜索按钮操作 */
+      handleQuery() {
+        this.queryParams.pageNo = 1;
+        this.getList();
+      },
+      /** 添加/修改操作 */
+      openForm(id) {
+        if (!this.studentId) {
+          that.$modal.msgError('请选择一个学生');
+          return;
+        }
+        this.$refs["formRef"].open(id, this.studentId);
+      },
+      /** 删除按钮操作 */
+      handleDelete(row) {
+        const that = this;
+        try {
+          const id = row.id;
+          this.$modal.confirm('是否确认删除学生编号为"' + id + '"的数据项?').then(()=>{
+            return StudentApi.deleteStudentTeacher(id);
+          }).then(() => {
+            that.getList();
+            that.$modal.msgSuccess("删除成功");
+          }).catch(() => {});
+        } catch {}
+      },
+    }
+  };
+</script>

+ 241 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/index

@@ -0,0 +1,241 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="queryParams.name" placeholder="请输入名字" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker clearable v-model="queryParams.birthday" type="date" value-format="yyyy-MM-dd" placeholder="选择出生日期" />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="queryParams.sex" placeholder="请选择性别" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                       :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-select v-model="queryParams.enabled" placeholder="请选择是否有效" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                       :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker v-model="queryParams.createTime" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="['00:00:00', '23:59:59']" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['infra:student:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
+                   v-hasPermi="['infra:student:export']">导出</el-button>
+      </el-col>
+              <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+            <el-table
+          v-loading="loading"
+          :data="list"
+          :stripe="true"
+          :highlight-current-row="true"
+          :show-overflow-tooltip="true"
+          @current-change="handleCurrentChange"
+      >
+                      <el-table-column label="编号" align="center" prop="id">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.id" />
+        </template>
+      </el-table-column>
+      <el-table-column label="名字" align="center" prop="name">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.name" />
+        </template>
+      </el-table-column>
+      <el-table-column label="简介" align="center" prop="description">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.description" />
+        </template>
+      </el-table-column>
+      <el-table-column label="出生日期" align="center" prop="birthday" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.birthday) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" align="center" prop="sex">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.avatar" />
+        </template>
+      </el-table-column>
+      <el-table-column label="附件" align="center" prop="video">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.video" />
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="memo">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.memo" />
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template v-slot="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                     v-hasPermi="['infra:student:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['infra:student:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+    <!-- 对话框(添加 / 修改) -->
+    <StudentForm ref="formRef" @success="getList" />
+      <!-- 子表的列表 -->
+      <el-tabs v-model="subTabsName">
+            <el-tab-pane label="学生联系人" name="studentContact">
+              <StudentContactList v-if="currentRow.id" :student-id="currentRow.id" />
+            </el-tab-pane>
+            <el-tab-pane label="学生班主任" name="studentTeacher">
+              <StudentTeacherList v-if="currentRow.id" :student-id="currentRow.id" />
+            </el-tab-pane>
+      </el-tabs>
+  </div>
+</template>
+
+<script>
+import * as StudentApi from '@/api/infra/demo';
+import StudentForm from './StudentForm.vue';
+    import StudentContactList from './components/StudentContactList.vue'
+    import StudentTeacherList from './components/StudentTeacherList.vue'
+export default {
+  name: "Student",
+  components: {
+          StudentForm,
+          StudentContactList,
+          StudentTeacherList,
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+              // 总条数
+        total: 0,
+      // 学生列表
+      list: [],
+      // 是否展开,默认全部展开
+      isExpandAll: true,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 选中行
+      currentRow: {},
+      // 查询参数
+      queryParams: {
+                    pageNo: 1,
+            pageSize: 10,
+        name: null,
+        birthday: null,
+        sex: null,
+        enabled: null,
+        createTime: [],
+      },
+                      /** 子表的列表 */
+              subTabsName: 'studentContact'
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      try {
+      this.loading = true;
+            StudentApi.getStudentPage(this.queryParams).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+      });
+      } finally {
+        this.loading = false;
+      }
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 添加/修改操作 */
+    openForm(id) {
+      this.$refs["formRef"].open(id);
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const that = this;
+      try {
+      const id = row.id;
+      this.$modal.confirm('是否确认删除学生编号为"' + id + '"的数据项?').then(()=>{
+          return StudentApi.deleteStudent(id);
+        }).then(() => {
+        that.getList();
+        that.$modal.msgSuccess("删除成功");
+        }).catch(() => {});
+      } catch {}
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const that = this;
+      try {
+          this.$modal.confirm('是否确认导出所有学生数据项?').then(() => {
+              that.exportLoading = true;
+              return StudentApi.exportStudentExcel(params);
+            }).then(response => {
+              that.$download.excel(response, '学生.xls');
+            });
+      } catch {
+      } finally {
+        that.exportLoading = false;
+      }
+    },
+              /** 选中行操作 */
+        handleCurrentChange(row) {
+         this.currentRow = row;
+          /** 子表的列表 */
+          this.subTabsName = 'studentContact';
+        },
+        }
+};
+</script>

+ 12 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/xml/InfraStudentMapper

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 73 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/assert.json

@@ -0,0 +1,73 @@
+[ {
+  "contentPath" : "java/InfraStudentPageReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentRespVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
+}, {
+  "contentPath" : "java/InfraStudentSaveReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentController",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
+}, {
+  "contentPath" : "java/InfraStudentDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
+}, {
+  "contentPath" : "java/InfraStudentContactDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
+}, {
+  "contentPath" : "java/InfraStudentTeacherDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
+}, {
+  "contentPath" : "java/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
+}, {
+  "contentPath" : "java/InfraStudentContactMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
+}, {
+  "contentPath" : "java/InfraStudentTeacherMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
+}, {
+  "contentPath" : "xml/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/demo/InfraStudentMapper.xml"
+}, {
+  "contentPath" : "java/InfraStudentServiceImpl",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
+}, {
+  "contentPath" : "java/InfraStudentService",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
+}, {
+  "contentPath" : "java/InfraStudentServiceImplTest",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
+}, {
+  "contentPath" : "java/ErrorCodeConstants_手动操作",
+  "filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
+}, {
+  "contentPath" : "sql/sql",
+  "filePath" : "sql/sql.sql"
+}, {
+  "contentPath" : "sql/h2",
+  "filePath" : "sql/h2.sql"
+}, {
+  "contentPath" : "vue/index",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/index.vue"
+}, {
+  "contentPath" : "js/student",
+  "filePath" : "yudao-ui-admin-vue2/src/api/infra/student.js"
+}, {
+  "contentPath" : "vue/StudentForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/StudentForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentContactForm.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentTeacherForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactList",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentContactList.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherList",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentTeacherList.vue"
+} ]

+ 3 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/ErrorCodeConstants_手动操作

@@ -0,0 +1,3 @@
+// TODO 待办:请将下面的错误码复制到 yudao-module-infra-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
+// ========== 学生 TODO 补充编号 ==========
+ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");

+ 71 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentContactDO

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生联系人 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student_contact")
+@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentContactDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 28 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentContactMapper

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生联系人 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
+
+    default List<InfraStudentContactDO> selectListByStudentId(Long studentId) {
+        return selectList(InfraStudentContactDO::getStudentId, studentId);
+    }
+
+    default int deleteByStudentId(Long studentId) {
+        return delete(InfraStudentContactDO::getStudentId, studentId);
+    }
+
+}

+ 117 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentController

@@ -0,0 +1,117 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
+
+@Tag(name = "管理后台 - 学生")
+@RestController
+@RequestMapping("/infra/student")
+@Validated
+public class InfraStudentController {
+
+    @Resource
+    private InfraStudentService studentService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:create')")
+    public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
+        return success(studentService.createStudent(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:update')")
+    public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
+        studentService.updateStudent(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除学生")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:student:delete')")
+    public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
+        studentService.deleteStudent(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得学生")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
+        InfraStudentDO student = studentService.getStudent(id);
+        return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得学生分页")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
+        PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出学生 Excel")
+    @PreAuthorize("@ss.hasPermission('infra:student:export')")
+    @OperateLog(type = EXPORT)
+    public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
+                        BeanUtils.toBean(list, InfraStudentRespVO.class));
+    }
+
+    // ==================== 子表(学生联系人) ====================
+
+    @GetMapping("/student-contact/list-by-student-id")
+    @Operation(summary = "获得学生联系人列表")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<List<InfraStudentContactDO>> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) {
+        return success(studentService.getStudentContactListByStudentId(studentId));
+    }
+
+    // ==================== 子表(学生班主任) ====================
+
+    @GetMapping("/student-teacher/get-by-student-id")
+    @Operation(summary = "获得学生班主任")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<InfraStudentTeacherDO> getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) {
+        return success(studentService.getStudentTeacherByStudentId(studentId));
+    }
+
+}

+ 67 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentDO

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student")
+@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 30 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentMapper

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+
+/**
+ * 学生 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
+
+    default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
+                .likeIfPresent(InfraStudentDO::getName, reqVO.getName())
+                .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
+                .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
+                .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
+                .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(InfraStudentDO::getId));
+    }
+
+}

+ 34 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentPageReqVO

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 学生分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class InfraStudentPageReqVO extends PageParam {
+
+    @Schema(description = "名字", example = "芋头")
+    private String name;
+
+    @Schema(description = "出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", example = "1")
+    private Integer sex;
+
+    @Schema(description = "是否有效", example = "true")
+    private Boolean enabled;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 60 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentRespVO

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+@Schema(description = "管理后台 - 学生 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class InfraStudentRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @ExcelProperty("名字")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @ExcelProperty("简介")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty(value = "性别", converter = DictConvert.class)
+    @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @ExcelProperty(value = "是否有效", converter = DictConvert.class)
+    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @ExcelProperty("头像")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @ExcelProperty("附件")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @ExcelProperty("备注")
+    private String memo;
+
+    @Schema(description = "创建时间")
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 58 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentSaveReqVO

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+
+@Schema(description = "管理后台 - 学生新增/修改 Request VO")
+@Data
+public class InfraStudentSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @NotEmpty(message = "名字不能为空")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @NotEmpty(message = "简介不能为空")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "出生日期不能为空")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "性别不能为空")
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "是否有效不能为空")
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @NotEmpty(message = "头像不能为空")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @NotEmpty(message = "附件不能为空")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @NotEmpty(message = "备注不能为空")
+    private String memo;
+
+    @Schema(description = "学生联系人列表")
+    private List<InfraStudentContactDO> studentContacts;
+
+    @Schema(description = "学生班主任")
+    private InfraStudentTeacherDO studentTeacher;
+
+}

+ 77 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentService

@@ -0,0 +1,77 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+/**
+ * 学生 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface InfraStudentService {
+
+    /**
+     * 创建学生
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
+
+    /**
+     * 更新学生
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
+
+    /**
+     * 删除学生
+     *
+     * @param id 编号
+     */
+    void deleteStudent(Long id);
+
+    /**
+     * 获得学生
+     *
+     * @param id 编号
+     * @return 学生
+     */
+    InfraStudentDO getStudent(Long id);
+
+    /**
+     * 获得学生分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 学生分页
+     */
+    PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
+
+    // ==================== 子表(学生联系人) ====================
+
+    /**
+     * 获得学生联系人列表
+     *
+     * @param studentId 学生编号
+     * @return 学生联系人列表
+     */
+    List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId);
+
+    // ==================== 子表(学生班主任) ====================
+
+    /**
+     * 获得学生班主任
+     *
+     * @param studentId 学生编号
+     * @return 学生班主任
+     */
+    InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId);
+
+}

+ 147 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentServiceImpl

@@ -0,0 +1,147 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+
+/**
+ * 学生 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class InfraStudentServiceImpl implements InfraStudentService {
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+    @Resource
+    private InfraStudentContactMapper studentContactMapper;
+    @Resource
+    private InfraStudentTeacherMapper studentTeacherMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createStudent(InfraStudentSaveReqVO createReqVO) {
+        // 插入
+        InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
+        studentMapper.insert(student);
+
+        // 插入子表
+        createStudentContactList(student.getId(), createReqVO.getStudentContacts());
+        createStudentTeacher(student.getId(), createReqVO.getStudentTeacher());
+        // 返回
+        return student.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
+        // 校验存在
+        validateStudentExists(updateReqVO.getId());
+        // 更新
+        InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
+        studentMapper.updateById(updateObj);
+
+        // 更新子表
+        updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts());
+        updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteStudent(Long id) {
+        // 校验存在
+        validateStudentExists(id);
+        // 删除
+        studentMapper.deleteById(id);
+
+        // 删除子表
+        deleteStudentContactByStudentId(id);
+        deleteStudentTeacherByStudentId(id);
+    }
+
+    private void validateStudentExists(Long id) {
+        if (studentMapper.selectById(id) == null) {
+            throw exception(STUDENT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public InfraStudentDO getStudent(Long id) {
+        return studentMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
+        return studentMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 子表(学生联系人) ====================
+
+    @Override
+    public List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId) {
+        return studentContactMapper.selectListByStudentId(studentId);
+    }
+
+    private void createStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
+        list.forEach(o -> o.setStudentId(studentId));
+        studentContactMapper.insertBatch(list);
+    }
+
+    private void updateStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
+        deleteStudentContactByStudentId(studentId);
+		list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新
+        createStudentContactList(studentId, list);
+    }
+
+    private void deleteStudentContactByStudentId(Long studentId) {
+        studentContactMapper.deleteByStudentId(studentId);
+    }
+
+    // ==================== 子表(学生班主任) ====================
+
+    @Override
+    public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) {
+        return studentTeacherMapper.selectByStudentId(studentId);
+    }
+
+    private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
+        if (studentTeacher == null) {
+            return;
+        }
+        studentTeacher.setStudentId(studentId);
+        studentTeacherMapper.insert(studentTeacher);
+    }
+
+    private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
+        if (studentTeacher == null) {
+			return;
+        }
+        studentTeacher.setStudentId(studentId);
+        studentTeacher.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新
+        studentTeacherMapper.insertOrUpdate(studentTeacher);
+    }
+
+    private void deleteStudentTeacherByStudentId(Long studentId) {
+        studentTeacherMapper.deleteByStudentId(studentId);
+    }
+
+}

+ 146 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentServiceImplTest

@@ -0,0 +1,146 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link InfraStudentServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(InfraStudentServiceImpl.class)
+public class InfraStudentServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private InfraStudentServiceImpl studentService;
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+
+    @Test
+    public void testCreateStudent_success() {
+        // 准备参数
+        InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
+
+        // 调用
+        Long studentId = studentService.createStudent(createReqVO);
+        // 断言
+        assertNotNull(studentId);
+        // 校验记录的属性是否正确
+        InfraStudentDO student = studentMapper.selectById(studentId);
+        assertPojoEquals(createReqVO, student, "id");
+    }
+
+    @Test
+    public void testUpdateStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
+            o.setId(dbStudent.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        studentService.updateStudent(updateReqVO);
+        // 校验是否更新正确
+        InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
+        assertPojoEquals(updateReqVO, student);
+    }
+
+    @Test
+    public void testUpdateStudent_notExists() {
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbStudent.getId();
+
+        // 调用
+        studentService.deleteStudent(id);
+       // 校验数据不存在了
+       assertNull(studentMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteStudent_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetStudentPage() {
+       // mock 数据
+       InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
+           o.setName(null);
+           o.setBirthday(null);
+           o.setSex(null);
+           o.setEnabled(null);
+           o.setCreateTime(null);
+       });
+       studentMapper.insert(dbStudent);
+       // 测试 name 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
+       // 测试 birthday 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
+       // 测试 sex 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
+       // 测试 enabled 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
+       // 测试 createTime 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
+       // 准备参数
+       InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
+       reqVO.setName(null);
+       reqVO.setBirthday(null);
+       reqVO.setSex(null);
+       reqVO.setEnabled(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbStudent, pageResult.getList().get(0));
+    }
+
+}

+ 71 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentTeacherDO

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生班主任 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student_teacher")
+@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentTeacherDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 28 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentTeacherMapper

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生班主任 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
+
+    default InfraStudentTeacherDO selectByStudentId(Long studentId) {
+        return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
+    }
+
+    default int deleteByStudentId(Long studentId) {
+        return delete(InfraStudentTeacherDO::getStudentId, studentId);
+    }
+
+}

+ 74 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/js/student

@@ -0,0 +1,74 @@
+import request from '@/utils/request'
+
+// 创建学生
+export function createStudent(data) {
+  return request({
+    url: '/infra/student/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新学生
+export function updateStudent(data) {
+  return request({
+    url: '/infra/student/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除学生
+export function deleteStudent(id) {
+  return request({
+    url: '/infra/student/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得学生
+export function getStudent(id) {
+  return request({
+    url: '/infra/student/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得学生分页
+export function getStudentPage(params) {
+  return request({
+    url: '/infra/student/page',
+    method: 'get',
+    params
+  })
+}
+// 导出学生 Excel
+export function exportStudentExcel(params) {
+  return request({
+    url: '/infra/student/export-excel',
+    method: 'get',
+    params,
+    responseType: 'blob'
+  })
+}
+
+// ==================== 子表(学生联系人) ====================
+  
+    // 获得学生联系人列表
+    export function getStudentContactListByStudentId(studentId) {
+      return request({
+        url: `/infra/student/student-contact/list-by-student-id?studentId=` + studentId,
+        method: 'get'
+      })
+    }
+  
+// ==================== 子表(学生班主任) ====================
+  
+    // 获得学生班主任
+    export function getStudentTeacherByStudentId(studentId) {
+      return request({
+        url: `/infra/student/student-teacher/get-by-student-id?studentId=` + studentId,
+        method: 'get'
+      })
+    }
+  

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/sql/h2

@@ -0,0 +1,17 @@
+-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
+CREATE TABLE IF NOT EXISTS "infra_student" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name" varchar NOT NULL,
+    "description" varchar NOT NULL,
+    "birthday" varchar NOT NULL,
+    "sex" int NOT NULL,
+    "enabled" bit NOT NULL,
+    "avatar" varchar NOT NULL,
+    "video" varchar NOT NULL,
+    "memo" varchar NOT NULL,
+    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY ("id")
+) COMMENT '学生表';
+
+-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
+DELETE FROM "infra_student";

+ 55 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/sql/sql

@@ -0,0 +1,55 @@
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+    '学生管理', '', 2, 0, 888,
+    'student', '', 'infra/demo/index', 0, 'InfraStudent'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生查询', 'infra:student:query', 3, 1, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生创建', 'infra:student:create', 3, 2, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生更新', 'infra:student:update', 3, 3, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生删除', 'infra:student:delete', 3, 4, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生导出', 'infra:student:export', 3, 5, @parentId,
+    '', '', '', 0
+);

+ 176 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentContactForm

@@ -0,0 +1,176 @@
+<template>
+  <div class="app-container">
+      <el-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          v-loading="formLoading"
+          label-width="0px"
+          :inline-message="true"
+      >
+        <el-table :data="formData" class="-mt-10px">
+          <el-table-column label="序号" type="index" width="100" />
+                       <el-table-column label="名字" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
+                            <el-input v-model="row.name" placeholder="请输入名字" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="简介" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.description`" :rules="formRules.description" class="mb-0px!">
+                            <el-input v-model="row.description" type="textarea" placeholder="请输入简介" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="出生日期" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.birthday`" :rules="formRules.birthday" class="mb-0px!">
+                            <el-date-picker clearable v-model="row.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="性别" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.sex`" :rules="formRules.sex" class="mb-0px!">
+                            <el-select v-model="row.sex" placeholder="请选择性别">
+                                  <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                             :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                            </el-select>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="是否有效" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.enabled`" :rules="formRules.enabled" class="mb-0px!">
+                            <el-radio-group v-model="row.enabled">
+                                  <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                                            :key="dict.value" :label="dict.value">{{dict.label}}</el-radio>
+                            </el-radio-group>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="头像" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.avatar`" :rules="formRules.avatar" class="mb-0px!">
+                            <ImageUpload v-model="row.avatar"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="附件" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.video`" :rules="formRules.video" class="mb-0px!">
+                            <FileUpload v-model="row.video"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="备注" min-width="400">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.memo`" :rules="formRules.memo" class="mb-0px!">
+                            <Editor v-model="row.memo" :min-height="192"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+          <el-table-column align="center" fixed="right" label="操作" width="60">
+            <template v-slot="{ $index }">
+              <el-link @click="handleDelete($index)">—</el-link>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-form>
+      <el-row justify="center" class="mt-3">
+        <el-button @click="handleAdd" round>+ 添加学生联系人</el-button>
+      </el-row>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+      import ImageUpload from '@/components/ImageUpload';
+      import FileUpload from '@/components/FileUpload';
+      import Editor from '@/components/Editor';
+  export default {
+    name: "StudentContactForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+    },
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: [],
+        // 表单校验
+        formRules: {
+                        studentId: [{ required: true, message: "学生编号不能为空", trigger: "blur" }],
+                        name: [{ required: true, message: "名字不能为空", trigger: "blur" }],
+                        description: [{ required: true, message: "简介不能为空", trigger: "blur" }],
+                        birthday: [{ required: true, message: "出生日期不能为空", trigger: "blur" }],
+                        sex: [{ required: true, message: "性别不能为空", trigger: "change" }],
+                        enabled: [{ required: true, message: "是否有效不能为空", trigger: "blur" }],
+                        avatar: [{ required: true, message: "头像不能为空", trigger: "blur" }],
+                        memo: [{ required: true, message: "备注不能为空", trigger: "blur" }],
+        },
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+      studentId:{
+        handler(val) {
+          // 1. 重置表单
+              this.formData = []
+          // 2. val 非空,则加载数据
+          if (!val) {
+            return;
+          }
+          try {
+            this.formLoading = true;
+            const that = this;
+            StudentApi.getStudentContactListByStudentId(val).then(res=>{
+              that.formData = res.data;
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        },
+        immediate: true
+      }
+    },
+    methods: {
+          /** 新增按钮操作 */
+          handleAdd() {
+            const row = {
+                                id: undefined,
+                                studentId: undefined,
+                                name: undefined,
+                                description: undefined,
+                                birthday: undefined,
+                                sex: undefined,
+                                enabled: undefined,
+                                avatar: undefined,
+                                video: undefined,
+                                memo: undefined,
+            }
+            row.studentId = this.studentId
+            this.formData.push(row)
+          },
+          /** 删除按钮操作 */
+          handleDelete(index) {
+            this.formData.splice(index, 1)
+          },
+      /** 表单校验 */
+      validate(){
+        return this.$refs["formRef"].validate()
+      },
+      /** 表单值 */
+      getData(){
+        return this.formData
+      }
+    }
+  };
+</script>

+ 91 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentContactList

@@ -0,0 +1,91 @@
+<template>
+  <div class="app-container">
+            <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+                <el-table-column label="编号" align="center" prop="id" />
+                 <el-table-column label="名字" align="center" prop="name" />
+                <el-table-column label="简介" align="center" prop="description" />
+                <el-table-column label="出生日期" align="center" prop="birthday" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.birthday) }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="性别" align="center" prop="sex">
+                  <template v-slot="scope">
+                    <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="是否有效" align="center" prop="enabled">
+                  <template v-slot="scope">
+                    <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="头像" align="center" prop="avatar" />
+                <el-table-column label="附件" align="center" prop="video" />
+                <el-table-column label="备注" align="center" prop="memo" />
+                <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.createTime) }}</span>
+                  </template>
+                </el-table-column>
+    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <template v-slot="scope">
+        <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                   v-hasPermi="['infra:student:update']">修改</el-button>
+        <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                   v-hasPermi="['infra:student:delete']">删除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+  export default {
+    name: "StudentContactList",
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 遮罩层
+        loading: true,
+        // 列表的数据
+        list: [],
+      };
+    },
+    created() {
+      this.getList();
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+        studentId:{
+            handler(val) {
+              this.queryParams.studentId = val;
+              if (val){
+                this.handleQuery();
+              }
+            },
+            immediate: true
+      }
+    },
+    methods: {
+      /** 查询列表 */
+      getList() {
+        try {
+          this.loading = true;
+          const that = this;
+                StudentApi.getStudentContactListByStudentId(this.studentId).then(response=>{
+                  that.list = response.data;
+              })
+        } finally {
+          this.loading = false;
+        }
+      },
+      /** 搜索按钮操作 */
+      handleQuery() {
+        this.queryParams.pageNo = 1;
+        this.getList();
+      },
+    }
+  };
+</script>

+ 221 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentForm

@@ -0,0 +1,221 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+                    <el-form-item label="名字" prop="name">
+                      <el-input v-model="formData.name" placeholder="请输入名字" />
+                    </el-form-item>
+                    <el-form-item label="简介" prop="description">
+                      <el-input v-model="formData.description" type="textarea" placeholder="请输入内容" />
+                    </el-form-item>
+                    <el-form-item label="出生日期" prop="birthday">
+                      <el-date-picker clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item label="是否有效" prop="enabled">
+                      <el-radio-group v-model="formData.enabled">
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                                      :key="dict.value" :label="dict.value">{{dict.label}}</el-radio>
+                      </el-radio-group>
+                    </el-form-item>
+                    <el-form-item label="头像">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注">
+                      <Editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+                  <!-- 子表的表单 -->
+          <el-tabs v-model="subTabsName">
+                <el-tab-pane label="学生联系人" name="studentContact">
+                  <StudentContactForm ref="studentContactFormRef" :student-id="formData.id" />
+                </el-tab-pane>
+                <el-tab-pane label="学生班主任" name="studentTeacher">
+                  <StudentTeacherForm ref="studentTeacherFormRef" :student-id="formData.id" />
+                </el-tab-pane>
+          </el-tabs>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+  import ImageUpload from '@/components/ImageUpload';
+  import FileUpload from '@/components/FileUpload';
+  import Editor from '@/components/Editor';
+          import StudentContactForm from './components/StudentContactForm.vue'
+      import StudentTeacherForm from './components/StudentTeacherForm.vue'
+  export default {
+    name: "StudentForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+                               StudentContactForm,
+               StudentTeacherForm,
+    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        },
+        // 表单校验
+        formRules: {
+                        name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+                        description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+                        birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+                        sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+                        enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+                        avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+                        video: [{ required: true, message: '附件不能为空', trigger: 'blur' }],
+                        memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
+        },
+                              /** 子表的表单 */
+             subTabsName: 'studentContact'
+      };
+    },
+    methods: {
+      /** 打开弹窗 */
+     open(id) {
+        this.dialogVisible = true;
+        this.reset();
+        const that = this;
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            StudentApi.getStudent(id).then(res=>{
+              that.formData = res.data;
+              that.title = "修改学生";
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.title = "新增学生";
+              },
+      /** 提交按钮 */
+      submitForm() {
+        this.formLoading = true;
+        try {
+          const that = this;
+          let data = this.formData;
+          let validate = false;
+          // 校验主表
+          this.getRef("formRef").validate(valid => {
+            validate = valid;
+          });
+                    // 校验子表
+            this.validateSubFrom01().then(() => {
+            // 全部校验通过-拼接子表的数据
+            // 拼接子表的数据
+                data.studentContacts = that.getRef('studentContactFormRef').getData();
+                data.studentTeacher = that.getRef('studentTeacherFormRef').getData();
+            }).catch((err) => {
+                validate = false;
+                that.subTabsName = err.replace("FormRef", ""); // 定位到没有校验通过的子表单
+            })
+          // 所有表单校验通过后方可提交
+          if (!validate) {
+            return;
+          }
+          // 修改的提交
+          if (data.id) {
+                  StudentApi.updateStudent(data).then(response => {
+              that.$modal.msgSuccess("修改成功");
+              that.dialogVisible = false;
+              that.$emit('success');
+            });
+            return;
+          }
+          // 添加的提交
+          StudentApi.createStudent(data).then(response => {
+            that.$modal.msgSuccess("新增成功");
+            that.dialogVisible = false;
+            that.$emit('success');
+          });
+        }finally {
+          this.formLoading = false;
+        }
+      },
+      getRef(refName){
+        return this.$refs[refName];
+      },
+      /** 校验子表单 */
+      validateSubFrom(item) {
+        return new Promise((resolve, reject) => {
+          this.getRef(item).validate()
+          .then(() => {
+            resolve();
+          })
+          .catch(() => {
+            reject(item);
+          })
+        })
+      },
+      /** 校验所有子表单 */
+      validateSubFrom01() {
+        // 需要校验的表单 ref
+        const validFormRefArr = [
+              "studentContactFormRef",
+              "studentTeacherFormRef",
+        ];
+        const validArr = []; // 校验
+        for (const item of validFormRefArr) {
+          validArr.push(this.validateSubFrom(item));
+        }
+        return new Promise((resolve, reject) => {
+          // 校验所有
+          Promise.all(validArr).then(() => {
+            resolve();
+          }).catch((err) => {
+            reject(err);
+          })
+        })
+      },
+                      /** 表单重置 */
+      reset() {
+        this.formData = {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        };
+        this.resetForm("formRef");
+      },
+    }
+  };
+</script>

+ 126 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentTeacherForm

@@ -0,0 +1,126 @@
+<template>
+  <div class="app-container">
+      <el-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          label-width="100px"
+          v-loading="formLoading"
+      >
+                     <el-form-item label="名字" prop="name">
+                      <el-input v-model="formData.name" placeholder="请输入名字" />
+                    </el-form-item>
+                    <el-form-item label="简介" prop="description">
+                      <el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
+                    </el-form-item>
+                    <el-form-item label="出生日期" prop="birthday">
+                      <el-date-picker clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item label="是否有效" prop="enabled">
+                      <el-radio-group v-model="formData.enabled">
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                                      :key="dict.value" :label="dict.value">{{dict.label}}</el-radio>
+                      </el-radio-group>
+                    </el-form-item>
+                    <el-form-item label="头像">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注">
+                      <Editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+      import ImageUpload from '@/components/ImageUpload';
+      import FileUpload from '@/components/FileUpload';
+      import Editor from '@/components/Editor';
+  export default {
+    name: "StudentTeacherForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+    },
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: [],
+        // 表单校验
+        formRules: {
+                        studentId: [{ required: true, message: "学生编号不能为空", trigger: "blur" }],
+                        name: [{ required: true, message: "名字不能为空", trigger: "blur" }],
+                        description: [{ required: true, message: "简介不能为空", trigger: "blur" }],
+                        birthday: [{ required: true, message: "出生日期不能为空", trigger: "blur" }],
+                        sex: [{ required: true, message: "性别不能为空", trigger: "change" }],
+                        enabled: [{ required: true, message: "是否有效不能为空", trigger: "blur" }],
+                        avatar: [{ required: true, message: "头像不能为空", trigger: "blur" }],
+                        memo: [{ required: true, message: "备注不能为空", trigger: "blur" }],
+        },
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+      studentId:{
+        handler(val) {
+          // 1. 重置表单
+              this.formData = {
+                                  id: undefined,
+                                  studentId: undefined,
+                                  name: undefined,
+                                  description: undefined,
+                                  birthday: undefined,
+                                  sex: undefined,
+                                  enabled: undefined,
+                                  avatar: undefined,
+                                  video: undefined,
+                                  memo: undefined,
+              }
+          // 2. val 非空,则加载数据
+          if (!val) {
+            return;
+          }
+          try {
+            this.formLoading = true;
+            const that = this;
+            StudentApi.getStudentTeacherByStudentId(val).then(res=>{
+              const data = res.data;
+              if (!data) {
+                return
+              }
+              that.formData = data;
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        },
+        immediate: true
+      }
+    },
+    methods: {
+      /** 表单校验 */
+      validate(){
+        return this.$refs["formRef"].validate()
+      },
+      /** 表单值 */
+      getData(){
+        return this.formData
+      }
+    }
+  };
+</script>

+ 95 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentTeacherList

@@ -0,0 +1,95 @@
+<template>
+  <div class="app-container">
+            <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+                <el-table-column label="编号" align="center" prop="id" />
+                 <el-table-column label="名字" align="center" prop="name" />
+                <el-table-column label="简介" align="center" prop="description" />
+                <el-table-column label="出生日期" align="center" prop="birthday" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.birthday) }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="性别" align="center" prop="sex">
+                  <template v-slot="scope">
+                    <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="是否有效" align="center" prop="enabled">
+                  <template v-slot="scope">
+                    <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="头像" align="center" prop="avatar" />
+                <el-table-column label="附件" align="center" prop="video" />
+                <el-table-column label="备注" align="center" prop="memo" />
+                <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.createTime) }}</span>
+                  </template>
+                </el-table-column>
+    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <template v-slot="scope">
+        <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                   v-hasPermi="['infra:student:update']">修改</el-button>
+        <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                   v-hasPermi="['infra:student:delete']">删除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+  export default {
+    name: "StudentTeacherList",
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 遮罩层
+        loading: true,
+        // 列表的数据
+        list: [],
+      };
+    },
+    created() {
+      this.getList();
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+        studentId:{
+            handler(val) {
+              this.queryParams.studentId = val;
+              if (val){
+                this.handleQuery();
+              }
+            },
+            immediate: true
+      }
+    },
+    methods: {
+      /** 查询列表 */
+      getList() {
+        try {
+          this.loading = true;
+          const that = this;
+                StudentApi.getStudentTeacherByStudentId(this.studentId).then(response=>{
+                  const data = response.data;
+                  if (!data) {
+                    return
+                  }
+                  that.list.push(data)
+              })
+        } finally {
+          this.loading = false;
+        }
+      },
+      /** 搜索按钮操作 */
+      handleQuery() {
+        this.queryParams.pageNo = 1;
+        this.getList();
+      },
+    }
+  };
+</script>

+ 230 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/index

@@ -0,0 +1,230 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="queryParams.name" placeholder="请输入名字" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker clearable v-model="queryParams.birthday" type="date" value-format="yyyy-MM-dd" placeholder="选择出生日期" />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="queryParams.sex" placeholder="请选择性别" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                       :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-select v-model="queryParams.enabled" placeholder="请选择是否有效" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                       :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker v-model="queryParams.createTime" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="['00:00:00', '23:59:59']" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['infra:student:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
+                   v-hasPermi="['infra:student:export']">导出</el-button>
+      </el-col>
+              <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+            <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+              <!-- 子表的列表 -->
+        <el-table-column type="expand">
+          <template #default="scope">
+            <el-tabs value="studentContact">
+                  <el-tab-pane label="学生联系人" name="studentContact">
+                    <StudentContactList :student-id="scope.row.id" />
+                  </el-tab-pane>
+                  <el-tab-pane label="学生班主任" name="studentTeacher">
+                    <StudentTeacherList :student-id="scope.row.id" />
+                  </el-tab-pane>
+            </el-tabs>
+          </template>
+        </el-table-column>
+      <el-table-column label="编号" align="center" prop="id">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.id" />
+        </template>
+      </el-table-column>
+      <el-table-column label="名字" align="center" prop="name">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.name" />
+        </template>
+      </el-table-column>
+      <el-table-column label="简介" align="center" prop="description">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.description" />
+        </template>
+      </el-table-column>
+      <el-table-column label="出生日期" align="center" prop="birthday" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.birthday) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" align="center" prop="sex">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.avatar" />
+        </template>
+      </el-table-column>
+      <el-table-column label="附件" align="center" prop="video">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.video" />
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="memo">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.memo" />
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template v-slot="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                     v-hasPermi="['infra:student:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['infra:student:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+    <!-- 对话框(添加 / 修改) -->
+    <StudentForm ref="formRef" @success="getList" />
+    </div>
+</template>
+
+<script>
+import * as StudentApi from '@/api/infra/demo';
+import StudentForm from './StudentForm.vue';
+    import StudentContactList from './components/StudentContactList.vue'
+    import StudentTeacherList from './components/StudentTeacherList.vue'
+export default {
+  name: "Student",
+  components: {
+          StudentForm,
+          StudentContactList,
+          StudentTeacherList,
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+              // 总条数
+        total: 0,
+      // 学生列表
+      list: [],
+      // 是否展开,默认全部展开
+      isExpandAll: true,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 选中行
+      currentRow: {},
+      // 查询参数
+      queryParams: {
+                    pageNo: 1,
+            pageSize: 10,
+        name: null,
+        birthday: null,
+        sex: null,
+        enabled: null,
+        createTime: [],
+      },
+            };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      try {
+      this.loading = true;
+            StudentApi.getStudentPage(this.queryParams).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+      });
+      } finally {
+        this.loading = false;
+      }
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 添加/修改操作 */
+    openForm(id) {
+      this.$refs["formRef"].open(id);
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const that = this;
+      try {
+      const id = row.id;
+      this.$modal.confirm('是否确认删除学生编号为"' + id + '"的数据项?').then(()=>{
+          return StudentApi.deleteStudent(id);
+        }).then(() => {
+        that.getList();
+        that.$modal.msgSuccess("删除成功");
+        }).catch(() => {});
+      } catch {}
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const that = this;
+      try {
+          this.$modal.confirm('是否确认导出所有学生数据项?').then(() => {
+              that.exportLoading = true;
+              return StudentApi.exportStudentExcel(params);
+            }).then(response => {
+              that.$download.excel(response, '学生.xls');
+            });
+      } catch {
+      } finally {
+        that.exportLoading = false;
+      }
+    },
+              }
+};
+</script>

+ 12 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/xml/InfraStudentMapper

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 67 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/assert.json

@@ -0,0 +1,67 @@
+[ {
+  "contentPath" : "java/InfraStudentPageReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentRespVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
+}, {
+  "contentPath" : "java/InfraStudentSaveReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentController",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
+}, {
+  "contentPath" : "java/InfraStudentDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
+}, {
+  "contentPath" : "java/InfraStudentContactDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
+}, {
+  "contentPath" : "java/InfraStudentTeacherDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
+}, {
+  "contentPath" : "java/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
+}, {
+  "contentPath" : "java/InfraStudentContactMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
+}, {
+  "contentPath" : "java/InfraStudentTeacherMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
+}, {
+  "contentPath" : "xml/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/demo/InfraStudentMapper.xml"
+}, {
+  "contentPath" : "java/InfraStudentServiceImpl",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
+}, {
+  "contentPath" : "java/InfraStudentService",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
+}, {
+  "contentPath" : "java/InfraStudentServiceImplTest",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
+}, {
+  "contentPath" : "java/ErrorCodeConstants_手动操作",
+  "filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
+}, {
+  "contentPath" : "sql/sql",
+  "filePath" : "sql/sql.sql"
+}, {
+  "contentPath" : "sql/h2",
+  "filePath" : "sql/h2.sql"
+}, {
+  "contentPath" : "vue/index",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/index.vue"
+}, {
+  "contentPath" : "js/student",
+  "filePath" : "yudao-ui-admin-vue2/src/api/infra/student.js"
+}, {
+  "contentPath" : "vue/StudentForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/StudentForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentContactForm.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentTeacherForm.vue"
+} ]

+ 3 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/ErrorCodeConstants_手动操作

@@ -0,0 +1,3 @@
+// TODO 待办:请将下面的错误码复制到 yudao-module-infra-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
+// ========== 学生 TODO 补充编号 ==========
+ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");

+ 71 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentContactDO

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生联系人 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student_contact")
+@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentContactDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 28 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentContactMapper

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生联系人 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
+
+    default List<InfraStudentContactDO> selectListByStudentId(Long studentId) {
+        return selectList(InfraStudentContactDO::getStudentId, studentId);
+    }
+
+    default int deleteByStudentId(Long studentId) {
+        return delete(InfraStudentContactDO::getStudentId, studentId);
+    }
+
+}

+ 117 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentController

@@ -0,0 +1,117 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
+
+@Tag(name = "管理后台 - 学生")
+@RestController
+@RequestMapping("/infra/student")
+@Validated
+public class InfraStudentController {
+
+    @Resource
+    private InfraStudentService studentService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:create')")
+    public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
+        return success(studentService.createStudent(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:update')")
+    public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
+        studentService.updateStudent(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除学生")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:student:delete')")
+    public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
+        studentService.deleteStudent(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得学生")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
+        InfraStudentDO student = studentService.getStudent(id);
+        return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得学生分页")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
+        PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出学生 Excel")
+    @PreAuthorize("@ss.hasPermission('infra:student:export')")
+    @OperateLog(type = EXPORT)
+    public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
+                        BeanUtils.toBean(list, InfraStudentRespVO.class));
+    }
+
+    // ==================== 子表(学生联系人) ====================
+
+    @GetMapping("/student-contact/list-by-student-id")
+    @Operation(summary = "获得学生联系人列表")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<List<InfraStudentContactDO>> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) {
+        return success(studentService.getStudentContactListByStudentId(studentId));
+    }
+
+    // ==================== 子表(学生班主任) ====================
+
+    @GetMapping("/student-teacher/get-by-student-id")
+    @Operation(summary = "获得学生班主任")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<InfraStudentTeacherDO> getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) {
+        return success(studentService.getStudentTeacherByStudentId(studentId));
+    }
+
+}

+ 67 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentDO

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student")
+@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 30 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentMapper

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+
+/**
+ * 学生 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
+
+    default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
+                .likeIfPresent(InfraStudentDO::getName, reqVO.getName())
+                .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
+                .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
+                .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
+                .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(InfraStudentDO::getId));
+    }
+
+}

+ 34 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentPageReqVO

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 学生分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class InfraStudentPageReqVO extends PageParam {
+
+    @Schema(description = "名字", example = "芋头")
+    private String name;
+
+    @Schema(description = "出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", example = "1")
+    private Integer sex;
+
+    @Schema(description = "是否有效", example = "true")
+    private Boolean enabled;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 60 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentRespVO

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+@Schema(description = "管理后台 - 学生 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class InfraStudentRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @ExcelProperty("名字")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @ExcelProperty("简介")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty(value = "性别", converter = DictConvert.class)
+    @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @ExcelProperty(value = "是否有效", converter = DictConvert.class)
+    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @ExcelProperty("头像")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @ExcelProperty("附件")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @ExcelProperty("备注")
+    private String memo;
+
+    @Schema(description = "创建时间")
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 58 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentSaveReqVO

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+
+@Schema(description = "管理后台 - 学生新增/修改 Request VO")
+@Data
+public class InfraStudentSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @NotEmpty(message = "名字不能为空")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @NotEmpty(message = "简介不能为空")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "出生日期不能为空")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "性别不能为空")
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "是否有效不能为空")
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @NotEmpty(message = "头像不能为空")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @NotEmpty(message = "附件不能为空")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @NotEmpty(message = "备注不能为空")
+    private String memo;
+
+    @Schema(description = "学生联系人列表")
+    private List<InfraStudentContactDO> studentContacts;
+
+    @Schema(description = "学生班主任")
+    private InfraStudentTeacherDO studentTeacher;
+
+}

+ 77 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentService

@@ -0,0 +1,77 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+/**
+ * 学生 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface InfraStudentService {
+
+    /**
+     * 创建学生
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
+
+    /**
+     * 更新学生
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
+
+    /**
+     * 删除学生
+     *
+     * @param id 编号
+     */
+    void deleteStudent(Long id);
+
+    /**
+     * 获得学生
+     *
+     * @param id 编号
+     * @return 学生
+     */
+    InfraStudentDO getStudent(Long id);
+
+    /**
+     * 获得学生分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 学生分页
+     */
+    PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
+
+    // ==================== 子表(学生联系人) ====================
+
+    /**
+     * 获得学生联系人列表
+     *
+     * @param studentId 学生编号
+     * @return 学生联系人列表
+     */
+    List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId);
+
+    // ==================== 子表(学生班主任) ====================
+
+    /**
+     * 获得学生班主任
+     *
+     * @param studentId 学生编号
+     * @return 学生班主任
+     */
+    InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId);
+
+}

+ 147 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentServiceImpl

@@ -0,0 +1,147 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+
+/**
+ * 学生 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class InfraStudentServiceImpl implements InfraStudentService {
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+    @Resource
+    private InfraStudentContactMapper studentContactMapper;
+    @Resource
+    private InfraStudentTeacherMapper studentTeacherMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createStudent(InfraStudentSaveReqVO createReqVO) {
+        // 插入
+        InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
+        studentMapper.insert(student);
+
+        // 插入子表
+        createStudentContactList(student.getId(), createReqVO.getStudentContacts());
+        createStudentTeacher(student.getId(), createReqVO.getStudentTeacher());
+        // 返回
+        return student.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
+        // 校验存在
+        validateStudentExists(updateReqVO.getId());
+        // 更新
+        InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
+        studentMapper.updateById(updateObj);
+
+        // 更新子表
+        updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts());
+        updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteStudent(Long id) {
+        // 校验存在
+        validateStudentExists(id);
+        // 删除
+        studentMapper.deleteById(id);
+
+        // 删除子表
+        deleteStudentContactByStudentId(id);
+        deleteStudentTeacherByStudentId(id);
+    }
+
+    private void validateStudentExists(Long id) {
+        if (studentMapper.selectById(id) == null) {
+            throw exception(STUDENT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public InfraStudentDO getStudent(Long id) {
+        return studentMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
+        return studentMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 子表(学生联系人) ====================
+
+    @Override
+    public List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId) {
+        return studentContactMapper.selectListByStudentId(studentId);
+    }
+
+    private void createStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
+        list.forEach(o -> o.setStudentId(studentId));
+        studentContactMapper.insertBatch(list);
+    }
+
+    private void updateStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
+        deleteStudentContactByStudentId(studentId);
+		list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新
+        createStudentContactList(studentId, list);
+    }
+
+    private void deleteStudentContactByStudentId(Long studentId) {
+        studentContactMapper.deleteByStudentId(studentId);
+    }
+
+    // ==================== 子表(学生班主任) ====================
+
+    @Override
+    public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) {
+        return studentTeacherMapper.selectByStudentId(studentId);
+    }
+
+    private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
+        if (studentTeacher == null) {
+            return;
+        }
+        studentTeacher.setStudentId(studentId);
+        studentTeacherMapper.insert(studentTeacher);
+    }
+
+    private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
+        if (studentTeacher == null) {
+			return;
+        }
+        studentTeacher.setStudentId(studentId);
+        studentTeacher.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新
+        studentTeacherMapper.insertOrUpdate(studentTeacher);
+    }
+
+    private void deleteStudentTeacherByStudentId(Long studentId) {
+        studentTeacherMapper.deleteByStudentId(studentId);
+    }
+
+}

+ 146 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentServiceImplTest

@@ -0,0 +1,146 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link InfraStudentServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(InfraStudentServiceImpl.class)
+public class InfraStudentServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private InfraStudentServiceImpl studentService;
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+
+    @Test
+    public void testCreateStudent_success() {
+        // 准备参数
+        InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
+
+        // 调用
+        Long studentId = studentService.createStudent(createReqVO);
+        // 断言
+        assertNotNull(studentId);
+        // 校验记录的属性是否正确
+        InfraStudentDO student = studentMapper.selectById(studentId);
+        assertPojoEquals(createReqVO, student, "id");
+    }
+
+    @Test
+    public void testUpdateStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
+            o.setId(dbStudent.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        studentService.updateStudent(updateReqVO);
+        // 校验是否更新正确
+        InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
+        assertPojoEquals(updateReqVO, student);
+    }
+
+    @Test
+    public void testUpdateStudent_notExists() {
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbStudent.getId();
+
+        // 调用
+        studentService.deleteStudent(id);
+       // 校验数据不存在了
+       assertNull(studentMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteStudent_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetStudentPage() {
+       // mock 数据
+       InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
+           o.setName(null);
+           o.setBirthday(null);
+           o.setSex(null);
+           o.setEnabled(null);
+           o.setCreateTime(null);
+       });
+       studentMapper.insert(dbStudent);
+       // 测试 name 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
+       // 测试 birthday 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
+       // 测试 sex 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
+       // 测试 enabled 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
+       // 测试 createTime 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
+       // 准备参数
+       InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
+       reqVO.setName(null);
+       reqVO.setBirthday(null);
+       reqVO.setSex(null);
+       reqVO.setEnabled(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbStudent, pageResult.getList().get(0));
+    }
+
+}

+ 71 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentTeacherDO

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生班主任 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student_teacher")
+@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentTeacherDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 28 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentTeacherMapper

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生班主任 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
+
+    default InfraStudentTeacherDO selectByStudentId(Long studentId) {
+        return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
+    }
+
+    default int deleteByStudentId(Long studentId) {
+        return delete(InfraStudentTeacherDO::getStudentId, studentId);
+    }
+
+}

+ 74 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/js/student

@@ -0,0 +1,74 @@
+import request from '@/utils/request'
+
+// 创建学生
+export function createStudent(data) {
+  return request({
+    url: '/infra/student/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新学生
+export function updateStudent(data) {
+  return request({
+    url: '/infra/student/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除学生
+export function deleteStudent(id) {
+  return request({
+    url: '/infra/student/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得学生
+export function getStudent(id) {
+  return request({
+    url: '/infra/student/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得学生分页
+export function getStudentPage(params) {
+  return request({
+    url: '/infra/student/page',
+    method: 'get',
+    params
+  })
+}
+// 导出学生 Excel
+export function exportStudentExcel(params) {
+  return request({
+    url: '/infra/student/export-excel',
+    method: 'get',
+    params,
+    responseType: 'blob'
+  })
+}
+
+// ==================== 子表(学生联系人) ====================
+  
+    // 获得学生联系人列表
+    export function getStudentContactListByStudentId(studentId) {
+      return request({
+        url: `/infra/student/student-contact/list-by-student-id?studentId=` + studentId,
+        method: 'get'
+      })
+    }
+  
+// ==================== 子表(学生班主任) ====================
+  
+    // 获得学生班主任
+    export function getStudentTeacherByStudentId(studentId) {
+      return request({
+        url: `/infra/student/student-teacher/get-by-student-id?studentId=` + studentId,
+        method: 'get'
+      })
+    }
+  

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/sql/h2

@@ -0,0 +1,17 @@
+-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
+CREATE TABLE IF NOT EXISTS "infra_student" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name" varchar NOT NULL,
+    "description" varchar NOT NULL,
+    "birthday" varchar NOT NULL,
+    "sex" int NOT NULL,
+    "enabled" bit NOT NULL,
+    "avatar" varchar NOT NULL,
+    "video" varchar NOT NULL,
+    "memo" varchar NOT NULL,
+    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY ("id")
+) COMMENT '学生表';
+
+-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
+DELETE FROM "infra_student";

+ 55 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/sql/sql

@@ -0,0 +1,55 @@
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+    '学生管理', '', 2, 0, 888,
+    'student', '', 'infra/demo/index', 0, 'InfraStudent'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生查询', 'infra:student:query', 3, 1, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生创建', 'infra:student:create', 3, 2, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生更新', 'infra:student:update', 3, 3, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生删除', 'infra:student:delete', 3, 4, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生导出', 'infra:student:export', 3, 5, @parentId,
+    '', '', '', 0
+);

+ 176 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/StudentContactForm

@@ -0,0 +1,176 @@
+<template>
+  <div class="app-container">
+      <el-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          v-loading="formLoading"
+          label-width="0px"
+          :inline-message="true"
+      >
+        <el-table :data="formData" class="-mt-10px">
+          <el-table-column label="序号" type="index" width="100" />
+                       <el-table-column label="名字" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
+                            <el-input v-model="row.name" placeholder="请输入名字" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="简介" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.description`" :rules="formRules.description" class="mb-0px!">
+                            <el-input v-model="row.description" type="textarea" placeholder="请输入简介" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="出生日期" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.birthday`" :rules="formRules.birthday" class="mb-0px!">
+                            <el-date-picker clearable v-model="row.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="性别" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.sex`" :rules="formRules.sex" class="mb-0px!">
+                            <el-select v-model="row.sex" placeholder="请选择性别">
+                                  <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                             :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                            </el-select>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="是否有效" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.enabled`" :rules="formRules.enabled" class="mb-0px!">
+                            <el-radio-group v-model="row.enabled">
+                                  <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                                            :key="dict.value" :label="dict.value">{{dict.label}}</el-radio>
+                            </el-radio-group>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="头像" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.avatar`" :rules="formRules.avatar" class="mb-0px!">
+                            <ImageUpload v-model="row.avatar"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="附件" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.video`" :rules="formRules.video" class="mb-0px!">
+                            <FileUpload v-model="row.video"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="备注" min-width="400">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.memo`" :rules="formRules.memo" class="mb-0px!">
+                            <Editor v-model="row.memo" :min-height="192"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+          <el-table-column align="center" fixed="right" label="操作" width="60">
+            <template v-slot="{ $index }">
+              <el-link @click="handleDelete($index)">—</el-link>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-form>
+      <el-row justify="center" class="mt-3">
+        <el-button @click="handleAdd" round>+ 添加学生联系人</el-button>
+      </el-row>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+      import ImageUpload from '@/components/ImageUpload';
+      import FileUpload from '@/components/FileUpload';
+      import Editor from '@/components/Editor';
+  export default {
+    name: "StudentContactForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+    },
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: [],
+        // 表单校验
+        formRules: {
+                        studentId: [{ required: true, message: "学生编号不能为空", trigger: "blur" }],
+                        name: [{ required: true, message: "名字不能为空", trigger: "blur" }],
+                        description: [{ required: true, message: "简介不能为空", trigger: "blur" }],
+                        birthday: [{ required: true, message: "出生日期不能为空", trigger: "blur" }],
+                        sex: [{ required: true, message: "性别不能为空", trigger: "change" }],
+                        enabled: [{ required: true, message: "是否有效不能为空", trigger: "blur" }],
+                        avatar: [{ required: true, message: "头像不能为空", trigger: "blur" }],
+                        memo: [{ required: true, message: "备注不能为空", trigger: "blur" }],
+        },
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+      studentId:{
+        handler(val) {
+          // 1. 重置表单
+              this.formData = []
+          // 2. val 非空,则加载数据
+          if (!val) {
+            return;
+          }
+          try {
+            this.formLoading = true;
+            const that = this;
+            StudentApi.getStudentContactListByStudentId(val).then(res=>{
+              that.formData = res.data;
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        },
+        immediate: true
+      }
+    },
+    methods: {
+          /** 新增按钮操作 */
+          handleAdd() {
+            const row = {
+                                id: undefined,
+                                studentId: undefined,
+                                name: undefined,
+                                description: undefined,
+                                birthday: undefined,
+                                sex: undefined,
+                                enabled: undefined,
+                                avatar: undefined,
+                                video: undefined,
+                                memo: undefined,
+            }
+            row.studentId = this.studentId
+            this.formData.push(row)
+          },
+          /** 删除按钮操作 */
+          handleDelete(index) {
+            this.formData.splice(index, 1)
+          },
+      /** 表单校验 */
+      validate(){
+        return this.$refs["formRef"].validate()
+      },
+      /** 表单值 */
+      getData(){
+        return this.formData
+      }
+    }
+  };
+</script>

+ 221 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/StudentForm

@@ -0,0 +1,221 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+                    <el-form-item label="名字" prop="name">
+                      <el-input v-model="formData.name" placeholder="请输入名字" />
+                    </el-form-item>
+                    <el-form-item label="简介" prop="description">
+                      <el-input v-model="formData.description" type="textarea" placeholder="请输入内容" />
+                    </el-form-item>
+                    <el-form-item label="出生日期" prop="birthday">
+                      <el-date-picker clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item label="是否有效" prop="enabled">
+                      <el-radio-group v-model="formData.enabled">
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                                      :key="dict.value" :label="dict.value">{{dict.label}}</el-radio>
+                      </el-radio-group>
+                    </el-form-item>
+                    <el-form-item label="头像">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注">
+                      <Editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+                  <!-- 子表的表单 -->
+          <el-tabs v-model="subTabsName">
+                <el-tab-pane label="学生联系人" name="studentContact">
+                  <StudentContactForm ref="studentContactFormRef" :student-id="formData.id" />
+                </el-tab-pane>
+                <el-tab-pane label="学生班主任" name="studentTeacher">
+                  <StudentTeacherForm ref="studentTeacherFormRef" :student-id="formData.id" />
+                </el-tab-pane>
+          </el-tabs>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+  import ImageUpload from '@/components/ImageUpload';
+  import FileUpload from '@/components/FileUpload';
+  import Editor from '@/components/Editor';
+          import StudentContactForm from './components/StudentContactForm.vue'
+      import StudentTeacherForm from './components/StudentTeacherForm.vue'
+  export default {
+    name: "StudentForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+                               StudentContactForm,
+               StudentTeacherForm,
+    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        },
+        // 表单校验
+        formRules: {
+                        name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+                        description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+                        birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+                        sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+                        enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+                        avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+                        video: [{ required: true, message: '附件不能为空', trigger: 'blur' }],
+                        memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
+        },
+                              /** 子表的表单 */
+             subTabsName: 'studentContact'
+      };
+    },
+    methods: {
+      /** 打开弹窗 */
+     open(id) {
+        this.dialogVisible = true;
+        this.reset();
+        const that = this;
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            StudentApi.getStudent(id).then(res=>{
+              that.formData = res.data;
+              that.title = "修改学生";
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.title = "新增学生";
+              },
+      /** 提交按钮 */
+      submitForm() {
+        this.formLoading = true;
+        try {
+          const that = this;
+          let data = this.formData;
+          let validate = false;
+          // 校验主表
+          this.getRef("formRef").validate(valid => {
+            validate = valid;
+          });
+                    // 校验子表
+            this.validateSubFrom01().then(() => {
+            // 全部校验通过-拼接子表的数据
+            // 拼接子表的数据
+                data.studentContacts = that.getRef('studentContactFormRef').getData();
+                data.studentTeacher = that.getRef('studentTeacherFormRef').getData();
+            }).catch((err) => {
+                validate = false;
+                that.subTabsName = err.replace("FormRef", ""); // 定位到没有校验通过的子表单
+            })
+          // 所有表单校验通过后方可提交
+          if (!validate) {
+            return;
+          }
+          // 修改的提交
+          if (data.id) {
+                  StudentApi.updateStudent(data).then(response => {
+              that.$modal.msgSuccess("修改成功");
+              that.dialogVisible = false;
+              that.$emit('success');
+            });
+            return;
+          }
+          // 添加的提交
+          StudentApi.createStudent(data).then(response => {
+            that.$modal.msgSuccess("新增成功");
+            that.dialogVisible = false;
+            that.$emit('success');
+          });
+        }finally {
+          this.formLoading = false;
+        }
+      },
+      getRef(refName){
+        return this.$refs[refName];
+      },
+      /** 校验子表单 */
+      validateSubFrom(item) {
+        return new Promise((resolve, reject) => {
+          this.getRef(item).validate()
+          .then(() => {
+            resolve();
+          })
+          .catch(() => {
+            reject(item);
+          })
+        })
+      },
+      /** 校验所有子表单 */
+      validateSubFrom01() {
+        // 需要校验的表单 ref
+        const validFormRefArr = [
+              "studentContactFormRef",
+              "studentTeacherFormRef",
+        ];
+        const validArr = []; // 校验
+        for (const item of validFormRefArr) {
+          validArr.push(this.validateSubFrom(item));
+        }
+        return new Promise((resolve, reject) => {
+          // 校验所有
+          Promise.all(validArr).then(() => {
+            resolve();
+          }).catch((err) => {
+            reject(err);
+          })
+        })
+      },
+                      /** 表单重置 */
+      reset() {
+        this.formData = {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        };
+        this.resetForm("formRef");
+      },
+    }
+  };
+</script>

+ 126 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/StudentTeacherForm

@@ -0,0 +1,126 @@
+<template>
+  <div class="app-container">
+      <el-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          label-width="100px"
+          v-loading="formLoading"
+      >
+                     <el-form-item label="名字" prop="name">
+                      <el-input v-model="formData.name" placeholder="请输入名字" />
+                    </el-form-item>
+                    <el-form-item label="简介" prop="description">
+                      <el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
+                    </el-form-item>
+                    <el-form-item label="出生日期" prop="birthday">
+                      <el-date-picker clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item label="是否有效" prop="enabled">
+                      <el-radio-group v-model="formData.enabled">
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                                      :key="dict.value" :label="dict.value">{{dict.label}}</el-radio>
+                      </el-radio-group>
+                    </el-form-item>
+                    <el-form-item label="头像">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注">
+                      <Editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+      import ImageUpload from '@/components/ImageUpload';
+      import FileUpload from '@/components/FileUpload';
+      import Editor from '@/components/Editor';
+  export default {
+    name: "StudentTeacherForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+    },
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: [],
+        // 表单校验
+        formRules: {
+                        studentId: [{ required: true, message: "学生编号不能为空", trigger: "blur" }],
+                        name: [{ required: true, message: "名字不能为空", trigger: "blur" }],
+                        description: [{ required: true, message: "简介不能为空", trigger: "blur" }],
+                        birthday: [{ required: true, message: "出生日期不能为空", trigger: "blur" }],
+                        sex: [{ required: true, message: "性别不能为空", trigger: "change" }],
+                        enabled: [{ required: true, message: "是否有效不能为空", trigger: "blur" }],
+                        avatar: [{ required: true, message: "头像不能为空", trigger: "blur" }],
+                        memo: [{ required: true, message: "备注不能为空", trigger: "blur" }],
+        },
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+      studentId:{
+        handler(val) {
+          // 1. 重置表单
+              this.formData = {
+                                  id: undefined,
+                                  studentId: undefined,
+                                  name: undefined,
+                                  description: undefined,
+                                  birthday: undefined,
+                                  sex: undefined,
+                                  enabled: undefined,
+                                  avatar: undefined,
+                                  video: undefined,
+                                  memo: undefined,
+              }
+          // 2. val 非空,则加载数据
+          if (!val) {
+            return;
+          }
+          try {
+            this.formLoading = true;
+            const that = this;
+            StudentApi.getStudentTeacherByStudentId(val).then(res=>{
+              const data = res.data;
+              if (!data) {
+                return
+              }
+              that.formData = data;
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        },
+        immediate: true
+      }
+    },
+    methods: {
+      /** 表单校验 */
+      validate(){
+        return this.$refs["formRef"].validate()
+      },
+      /** 表单值 */
+      getData(){
+        return this.formData
+      }
+    }
+  };
+</script>

+ 213 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/index

@@ -0,0 +1,213 @@
+<template>
+  <div class="app-container">
+
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="queryParams.name" placeholder="请输入名字" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker clearable v-model="queryParams.birthday" type="date" value-format="yyyy-MM-dd" placeholder="选择出生日期" />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="queryParams.sex" placeholder="请选择性别" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                       :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-select v-model="queryParams.enabled" placeholder="请选择是否有效" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                       :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker v-model="queryParams.createTime" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="['00:00:00', '23:59:59']" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['infra:student:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
+                   v-hasPermi="['infra:student:export']">导出</el-button>
+      </el-col>
+              <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+            <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+            <el-table-column label="编号" align="center" prop="id">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.id" />
+        </template>
+      </el-table-column>
+      <el-table-column label="名字" align="center" prop="name">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.name" />
+        </template>
+      </el-table-column>
+      <el-table-column label="简介" align="center" prop="description">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.description" />
+        </template>
+      </el-table-column>
+      <el-table-column label="出生日期" align="center" prop="birthday" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.birthday) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" align="center" prop="sex">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.avatar" />
+        </template>
+      </el-table-column>
+      <el-table-column label="附件" align="center" prop="video">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.video" />
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="memo">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.memo" />
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template v-slot="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                     v-hasPermi="['infra:student:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['infra:student:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+    <!-- 对话框(添加 / 修改) -->
+    <StudentForm ref="formRef" @success="getList" />
+    </div>
+</template>
+
+<script>
+import * as StudentApi from '@/api/infra/demo';
+import StudentForm from './StudentForm.vue';
+export default {
+  name: "Student",
+  components: {
+          StudentForm,
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+              // 总条数
+        total: 0,
+      // 学生列表
+      list: [],
+      // 是否展开,默认全部展开
+      isExpandAll: true,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 选中行
+      currentRow: {},
+      // 查询参数
+      queryParams: {
+                    pageNo: 1,
+            pageSize: 10,
+        name: null,
+        birthday: null,
+        sex: null,
+        enabled: null,
+        createTime: [],
+      },
+            };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      try {
+      this.loading = true;
+            StudentApi.getStudentPage(this.queryParams).then(response => {
+        this.list = response.data.list;
+        this.total = response.data.total;
+      });
+      } finally {
+        this.loading = false;
+      }
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 添加/修改操作 */
+    openForm(id) {
+      this.$refs["formRef"].open(id);
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const that = this;
+      try {
+      const id = row.id;
+      this.$modal.confirm('是否确认删除学生编号为"' + id + '"的数据项?').then(()=>{
+          return StudentApi.deleteStudent(id);
+        }).then(() => {
+        that.getList();
+        that.$modal.msgSuccess("删除成功");
+        }).catch(() => {});
+      } catch {}
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const that = this;
+      try {
+          this.$modal.confirm('是否确认导出所有学生数据项?').then(() => {
+              that.exportLoading = true;
+              return StudentApi.exportStudentExcel(params);
+            }).then(response => {
+              that.$download.excel(response, '学生.xls');
+            });
+      } catch {
+      } finally {
+        that.exportLoading = false;
+      }
+    },
+              }
+};
+</script>

+ 12 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/xml/InfraStudentMapper

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 49 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/assert.json

@@ -0,0 +1,49 @@
+[ {
+  "contentPath" : "java/InfraStudentPageReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentRespVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
+}, {
+  "contentPath" : "java/InfraStudentSaveReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentController",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
+}, {
+  "contentPath" : "java/InfraStudentDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
+}, {
+  "contentPath" : "java/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
+}, {
+  "contentPath" : "xml/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/demo/InfraStudentMapper.xml"
+}, {
+  "contentPath" : "java/InfraStudentServiceImpl",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
+}, {
+  "contentPath" : "java/InfraStudentService",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
+}, {
+  "contentPath" : "java/InfraStudentServiceImplTest",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
+}, {
+  "contentPath" : "java/ErrorCodeConstants_手动操作",
+  "filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
+}, {
+  "contentPath" : "sql/sql",
+  "filePath" : "sql/sql.sql"
+}, {
+  "contentPath" : "sql/h2",
+  "filePath" : "sql/h2.sql"
+}, {
+  "contentPath" : "vue/index",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/index.vue"
+}, {
+  "contentPath" : "js/student",
+  "filePath" : "yudao-ui-admin-vue2/src/api/infra/student.js"
+}, {
+  "contentPath" : "vue/StudentForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/StudentForm.vue"
+} ]

+ 3 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/ErrorCodeConstants_手动操作

@@ -0,0 +1,3 @@
+// TODO 待办:请将下面的错误码复制到 yudao-module-infra-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
+// ========== 学生 TODO 补充编号 ==========
+ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");

+ 95 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentController

@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
+
+@Tag(name = "管理后台 - 学生")
+@RestController
+@RequestMapping("/infra/student")
+@Validated
+public class InfraStudentController {
+
+    @Resource
+    private InfraStudentService studentService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:create')")
+    public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
+        return success(studentService.createStudent(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:update')")
+    public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
+        studentService.updateStudent(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除学生")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:student:delete')")
+    public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
+        studentService.deleteStudent(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得学生")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
+        InfraStudentDO student = studentService.getStudent(id);
+        return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得学生分页")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
+        PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出学生 Excel")
+    @PreAuthorize("@ss.hasPermission('infra:student:export')")
+    @OperateLog(type = EXPORT)
+    public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
+                        BeanUtils.toBean(list, InfraStudentRespVO.class));
+    }
+
+}

+ 67 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentDO

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student")
+@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 30 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentMapper

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+
+/**
+ * 学生 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
+
+    default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
+                .likeIfPresent(InfraStudentDO::getName, reqVO.getName())
+                .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
+                .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
+                .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
+                .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(InfraStudentDO::getId));
+    }
+
+}

+ 34 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentPageReqVO

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 学生分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class InfraStudentPageReqVO extends PageParam {
+
+    @Schema(description = "名字", example = "芋头")
+    private String name;
+
+    @Schema(description = "出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", example = "1")
+    private Integer sex;
+
+    @Schema(description = "是否有效", example = "true")
+    private Boolean enabled;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 60 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentRespVO

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+@Schema(description = "管理后台 - 学生 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class InfraStudentRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @ExcelProperty("名字")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @ExcelProperty("简介")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty(value = "性别", converter = DictConvert.class)
+    @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @ExcelProperty(value = "是否有效", converter = DictConvert.class)
+    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @ExcelProperty("头像")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @ExcelProperty("附件")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @ExcelProperty("备注")
+    private String memo;
+
+    @Schema(description = "创建时间")
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 50 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentSaveReqVO

@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 学生新增/修改 Request VO")
+@Data
+public class InfraStudentSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @NotEmpty(message = "名字不能为空")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @NotEmpty(message = "简介不能为空")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "出生日期不能为空")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "性别不能为空")
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "是否有效不能为空")
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @NotEmpty(message = "头像不能为空")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @NotEmpty(message = "附件不能为空")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @NotEmpty(message = "备注不能为空")
+    private String memo;
+
+}

+ 55 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentService

@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+/**
+ * 学生 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface InfraStudentService {
+
+    /**
+     * 创建学生
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
+
+    /**
+     * 更新学生
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
+
+    /**
+     * 删除学生
+     *
+     * @param id 编号
+     */
+    void deleteStudent(Long id);
+
+    /**
+     * 获得学生
+     *
+     * @param id 编号
+     * @return 学生
+     */
+    InfraStudentDO getStudent(Long id);
+
+    /**
+     * 获得学生分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 学生分页
+     */
+    PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
+
+}

+ 74 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentServiceImpl

@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+
+/**
+ * 学生 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class InfraStudentServiceImpl implements InfraStudentService {
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+
+    @Override
+    public Long createStudent(InfraStudentSaveReqVO createReqVO) {
+        // 插入
+        InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
+        studentMapper.insert(student);
+        // 返回
+        return student.getId();
+    }
+
+    @Override
+    public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
+        // 校验存在
+        validateStudentExists(updateReqVO.getId());
+        // 更新
+        InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
+        studentMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteStudent(Long id) {
+        // 校验存在
+        validateStudentExists(id);
+        // 删除
+        studentMapper.deleteById(id);
+    }
+
+    private void validateStudentExists(Long id) {
+        if (studentMapper.selectById(id) == null) {
+            throw exception(STUDENT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public InfraStudentDO getStudent(Long id) {
+        return studentMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
+        return studentMapper.selectPage(pageReqVO);
+    }
+
+}

+ 146 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentServiceImplTest

@@ -0,0 +1,146 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link InfraStudentServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(InfraStudentServiceImpl.class)
+public class InfraStudentServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private InfraStudentServiceImpl studentService;
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+
+    @Test
+    public void testCreateStudent_success() {
+        // 准备参数
+        InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
+
+        // 调用
+        Long studentId = studentService.createStudent(createReqVO);
+        // 断言
+        assertNotNull(studentId);
+        // 校验记录的属性是否正确
+        InfraStudentDO student = studentMapper.selectById(studentId);
+        assertPojoEquals(createReqVO, student, "id");
+    }
+
+    @Test
+    public void testUpdateStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
+            o.setId(dbStudent.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        studentService.updateStudent(updateReqVO);
+        // 校验是否更新正确
+        InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
+        assertPojoEquals(updateReqVO, student);
+    }
+
+    @Test
+    public void testUpdateStudent_notExists() {
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbStudent.getId();
+
+        // 调用
+        studentService.deleteStudent(id);
+       // 校验数据不存在了
+       assertNull(studentMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteStudent_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetStudentPage() {
+       // mock 数据
+       InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
+           o.setName(null);
+           o.setBirthday(null);
+           o.setSex(null);
+           o.setEnabled(null);
+           o.setCreateTime(null);
+       });
+       studentMapper.insert(dbStudent);
+       // 测试 name 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
+       // 测试 birthday 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
+       // 测试 sex 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
+       // 测试 enabled 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
+       // 测试 createTime 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
+       // 准备参数
+       InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
+       reqVO.setName(null);
+       reqVO.setBirthday(null);
+       reqVO.setSex(null);
+       reqVO.setEnabled(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbStudent, pageResult.getList().get(0));
+    }
+
+}

+ 53 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/js/student

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 创建学生
+export function createStudent(data) {
+  return request({
+    url: '/infra/student/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新学生
+export function updateStudent(data) {
+  return request({
+    url: '/infra/student/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除学生
+export function deleteStudent(id) {
+  return request({
+    url: '/infra/student/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得学生
+export function getStudent(id) {
+  return request({
+    url: '/infra/student/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得学生分页
+export function getStudentPage(params) {
+  return request({
+    url: '/infra/student/page',
+    method: 'get',
+    params
+  })
+}
+// 导出学生 Excel
+export function exportStudentExcel(params) {
+  return request({
+    url: '/infra/student/export-excel',
+    method: 'get',
+    params,
+    responseType: 'blob'
+  })
+}

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/sql/h2

@@ -0,0 +1,17 @@
+-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
+CREATE TABLE IF NOT EXISTS "infra_student" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name" varchar NOT NULL,
+    "description" varchar NOT NULL,
+    "birthday" varchar NOT NULL,
+    "sex" int NOT NULL,
+    "enabled" bit NOT NULL,
+    "avatar" varchar NOT NULL,
+    "video" varchar NOT NULL,
+    "memo" varchar NOT NULL,
+    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY ("id")
+) COMMENT '学生表';
+
+-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
+DELETE FROM "infra_student";

+ 55 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/sql/sql

@@ -0,0 +1,55 @@
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+    '学生管理', '', 2, 0, 888,
+    'student', '', 'infra/demo/index', 0, 'InfraStudent'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生查询', 'infra:student:query', 3, 1, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生创建', 'infra:student:create', 3, 2, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生更新', 'infra:student:update', 3, 3, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生删除', 'infra:student:delete', 3, 4, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生导出', 'infra:student:export', 3, 5, @parentId,
+    '', '', '', 0
+);

+ 164 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/vue/StudentForm

@@ -0,0 +1,164 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+                    <el-form-item label="名字" prop="name">
+                      <el-input v-model="formData.name" placeholder="请输入名字" />
+                    </el-form-item>
+                    <el-form-item label="简介" prop="description">
+                      <el-input v-model="formData.description" type="textarea" placeholder="请输入内容" />
+                    </el-form-item>
+                    <el-form-item label="出生日期" prop="birthday">
+                      <el-date-picker clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item label="是否有效" prop="enabled">
+                      <el-radio-group v-model="formData.enabled">
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                                      :key="dict.value" :label="dict.value">{{dict.label}}</el-radio>
+                      </el-radio-group>
+                    </el-form-item>
+                    <el-form-item label="头像">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注">
+                      <Editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+              <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo'
+  import ImageUpload from '@/components/ImageUpload';
+  import FileUpload from '@/components/FileUpload';
+  import Editor from '@/components/Editor';
+      export default {
+    name: "StudentForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+                    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        },
+        // 表单校验
+        formRules: {
+                        name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+                        description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+                        birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+                        sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+                        enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+                        avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+                        video: [{ required: true, message: '附件不能为空', trigger: 'blur' }],
+                        memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
+        },
+                        };
+    },
+    methods: {
+      /** 打开弹窗 */
+     open(id) {
+        this.dialogVisible = true;
+        this.reset();
+        const that = this;
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            StudentApi.getStudent(id).then(res=>{
+              that.formData = res.data;
+              that.title = "修改学生";
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.title = "新增学生";
+              },
+      /** 提交按钮 */
+      submitForm() {
+        this.formLoading = true;
+        try {
+          const that = this;
+          let data = this.formData;
+          let validate = false;
+          // 校验主表
+          this.getRef("formRef").validate(valid => {
+            validate = valid;
+          });
+                  // 所有表单校验通过后方可提交
+          if (!validate) {
+            return;
+          }
+          // 修改的提交
+          if (data.id) {
+                  StudentApi.updateStudent(data).then(response => {
+              that.$modal.msgSuccess("修改成功");
+              that.dialogVisible = false;
+              that.$emit('success');
+            });
+            return;
+          }
+          // 添加的提交
+          StudentApi.createStudent(data).then(response => {
+            that.$modal.msgSuccess("新增成功");
+            that.dialogVisible = false;
+            that.$emit('success');
+          });
+        }finally {
+          this.formLoading = false;
+        }
+      },
+      getRef(refName){
+        return this.$refs[refName];
+      },
+                      /** 表单重置 */
+      reset() {
+        this.formData = {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        };
+        this.resetForm("formRef");
+      },
+    }
+  };
+</script>

Some files were not shown because too many files changed in this diff