Commit 2ef66566d4befc054a5657c475866e78702f0bea

Authored by YunaiV
1 parent 020fa9d5

feat(codegen): 完善 UniApp Wot 代码生成

- 支持普通、树表及多种主子表页面生成
- 完善主子表 API、表单、详情和字段控件
- 修复一对一空数据提交及树表搜索问题
- 统一日期时间戳和主键类型生成
- 修复 Wot 组件类型及生成代码格式问题
- 优化生成快照路径并增加文件冲突检测
- 补充 UniApp 代码生成单测和快照
Showing 105 changed files with 10321 additions and 190 deletions
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
... ... @@ -152,6 +152,16 @@ public class CodegenEngine {
152 152 vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/form/index.vue"))
153 153 .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail/index.vue"),
154 154 vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
  155 + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/index_tree.vue"),
  156 + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/index.vue"))
  157 + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("components/breadcrumb_tree.vue"),
  158 + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/components/breadcrumb.vue"))
  159 + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail_master.vue"),
  160 + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
  161 + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/detail_erp.vue"),
  162 + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/detail/index.vue"))
  163 + .put(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType(), vue3AdminUniappTemplatePath("views/form_sub_erp.vue"),
  164 + vue3UniappFilePath("pages-${table.moduleName}/${table.businessName}/${subSimpleClassName_strikeCase}/form/index.vue"))
155 165 // VUE3_VBEN2_ANTD_SCHEMA
156 166 .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"),
157 167 vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
... ... @@ -380,12 +390,17 @@ public class CodegenEngine {
380 390  
381 391 // 2. 执行生成
382 392 Map<String, String> result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序
  393 + Map<String, String> generatedSources = Maps.newHashMapWithExpectedSize(templates.size());
383 394 templates.forEach((vmPath, filePath) -> {
384   - // 2.1 特殊:主子表专属逻辑
  395 + // 2.1 特殊:主子表、Uniapp 树表使用独立页面模版
  396 + if (isVue3AdminUniappTemplate(vmPath) && !matchVue3AdminUniappTemplate(table, vmPath)) {
  397 + return;
  398 + }
  399 + // 2.2 特殊:主子表专属逻辑
385 400 if (isSubTemplate(vmPath)) {
386   - generateSubCode(table, subTables, result, vmPath, filePath, bindingMap);
  401 + generateSubCode(table, subTables, result, generatedSources, vmPath, filePath, bindingMap);
387 402 return;
388   - // 2.2 特殊:树表专属逻辑
  403 + // 2.3 特殊:树表专属逻辑
389 404 } else if (isPageReqVOTemplate(vmPath)) {
390 405 // 减少多余的类生成,例如说 PageVO.java 类
391 406 if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
... ... @@ -402,15 +417,21 @@ public class CodegenEngine {
402 417 return;
403 418 }
404 419 }
405   - // 2.3 默认生成
406   - generateCode(result, vmPath, filePath, bindingMap);
  420 + // 2.4 默认生成
  421 + generateCode(result, generatedSources, vmPath, filePath, bindingMap);
407 422 });
408 423 return result;
409 424 }
410 425  
411   - private void generateCode(Map<String, String> result, String vmPath,
  426 + private void generateCode(Map<String, String> result, Map<String, String> generatedSources, String vmPath,
412 427 String filePath, Map<String, Object> bindingMap) {
413 428 filePath = formatFilePath(filePath, bindingMap);
  429 + String source = vmPath + (bindingMap.containsKey("subIndex") ? "[subIndex=" + bindingMap.get("subIndex") + "]" : "");
  430 + String previousSource = generatedSources.putIfAbsent(filePath, source);
  431 + if (previousSource != null) {
  432 + throw new IllegalStateException(String.format("生成文件路径重复:%s,来源模板:%s、%s",
  433 + filePath, previousSource, source));
  434 + }
414 435 String content = templateEngine.getTemplate(vmPath).render(bindingMap);
415 436 // 格式化代码
416 437 content = prettyCode(content, vmPath);
... ... @@ -418,7 +439,7 @@ public class CodegenEngine {
418 439 }
419 440  
420 441 private void generateSubCode(CodegenTableDO table, List<CodegenTableDO> subTables,
421   - Map<String, String> result, String vmPath,
  442 + Map<String, String> result, Map<String, String> generatedSources, String vmPath,
422 443 String filePath, Map<String, Object> bindingMap) {
423 444 // 没有子表,所以不生成
424 445 if (CollUtil.isEmpty(subTables)) {
... ... @@ -441,7 +462,7 @@ public class CodegenEngine {
441 462 // 逐个生成
442 463 for (int i = 0; i < subTables.size(); i++) {
443 464 bindingMap.put("subIndex", i);
444   - generateCode(result, vmPath, filePath, bindingMap);
  465 + generateCode(result, generatedSources, vmPath, filePath, bindingMap);
445 466 }
446 467 bindingMap.remove("subIndex");
447 468 }
... ... @@ -482,6 +503,13 @@ public class CodegenEngine {
482 503 if (StrUtil.count(content, "DICT_TYPE.") == 0) {
483 504 content = StrUtils.removeLineContains(content, "DICT_TYPE");
484 505 }
  506 + // Vue3 Admin UniApp:清理空白行,并补充文件末尾换行,满足 ESLint 格式要求
  507 + if (vmPath.contains("vue3_admin_uniapp")) {
  508 + content = content.replaceAll("(?m)^[\\t ]+$", "");
  509 + if (!content.endsWith("\n")) {
  510 + content += "\n";
  511 + }
  512 + }
485 513 return content;
486 514 }
487 515  
... ... @@ -734,6 +762,33 @@ public class CodegenEngine {
734 762 return path.contains("_sub");
735 763 }
736 764  
  765 + private static boolean isVue3AdminUniappTemplate(String path) {
  766 + return path.contains("codegen/vue3_admin_uniapp/");
  767 + }
  768 +
  769 + /**
  770 + * 匹配 Uniapp 不同表类型的专用页面模版
  771 + */
  772 + private static boolean matchVue3AdminUniappTemplate(CodegenTableDO table, String path) {
  773 + boolean tree = CodegenTemplateTypeEnum.isTree(table.getTemplateType());
  774 + boolean masterSubmit = ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_NORMAL.getType())
  775 + || ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_INNER.getType());
  776 + boolean masterErp = ObjectUtil.equals(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_ERP.getType());
  777 + if (path.contains("_tree")) {
  778 + return tree;
  779 + }
  780 + if (path.contains("detail_master")) {
  781 + return masterSubmit;
  782 + }
  783 + if (path.contains("_erp")) {
  784 + return masterErp;
  785 + }
  786 + if (tree && path.endsWith("views/index.vue.vm")) {
  787 + return false;
  788 + }
  789 + return !(masterSubmit || masterErp) || !path.endsWith("views/detail/index.vue.vm");
  790 + }
  791 +
737 792 private static boolean isPageReqVOTemplate(String path) {
738 793 return path.contains("pageReqVO");
739 794 }
... ...
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/api/api.ts.vm
  1 +#if ($table.templateType != 2)
1 2 import type { PageParam, PageResult } from '@/http/types'
  3 +#end
2 4 import { http } from '@/http/http'
3 5  
4 6 #set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
... ... @@ -7,30 +9,72 @@ import { http } from &#39;@/http/http&#39;
7 9 #else
8 10 #set ($primaryTsType = "string")
9 11 #end
  12 +#foreach ($subTable in $subTables)
  13 +#set ($index = $foreach.count - 1)
  14 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  15 +#set ($subColumns = $subColumnsList.get($index))
  16 +#set ($subJoinColumn = $subJoinColumns.get($index))
  17 +/** ${subTable.classComment}信息 */
  18 +export interface ${subSimpleClassName} {
  19 +#foreach ($column in $subColumns)
  20 +#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
  21 +#set ($javaType = $column.javaType.toLowerCase())
  22 +#set ($optional = $column.nullable || $column.primaryKey || ($table.templateType != 11 && $column.id == $subJoinColumn.id))
  23 +#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
  24 + ${column.javaField}#if($optional)?#end: number
  25 +#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
  26 + ${column.javaField}#if($optional)?#end: number
  27 +#elseif(${javaType} == "boolean")
  28 + ${column.javaField}#if($optional)?#end: boolean
  29 +#else
  30 + ${column.javaField}#if($optional)?#end: string
  31 +#end
  32 +#end
  33 +#end
  34 +}
  35 +
  36 +#end
10 37 /** ${table.classComment}信息 */
11 38 export interface ${simpleClassName} {
12 39 #foreach ($column in $columns)
13   - #if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
14   - #set ($javaType = $column.javaType.toLowerCase())
15   - #set ($javaFieldLower = $column.javaField.toLowerCase())
16   - #set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")
17   - #if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
  40 +#if ($column.primaryKey || $column.createOperation || $column.updateOperation || $column.listOperationResult)
  41 +#set ($javaType = $column.javaType.toLowerCase())
  42 +#set ($optional = $column.nullable || $column.primaryKey)
  43 +#if(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
  44 + ${column.javaField}#if($optional)?#end: number
  45 +#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
18 46 ${column.javaField}#if($optional)?#end: number
19   - #elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
20   - ${column.javaField}#if($optional)?#end: Date
21   - #elseif(${javaType} == "boolean")
  47 +#elseif(${javaType} == "boolean")
22 48 ${column.javaField}#if($optional)?#end: boolean
23   - #else
  49 +#else
24 50 ${column.javaField}#if($optional)?#end: string
25   - #end
26   - #end
  51 +#end
  52 +#end
  53 +#end
  54 +#if ($table.templateType == 2)
  55 + children?: ${simpleClassName}[]
  56 +#end
  57 +#if ($table.templateType == 10 || $table.templateType == 12)
  58 +#foreach ($subTable in $subTables)
  59 +#set ($index = $foreach.count - 1)
  60 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  61 +#set ($subClassNameVar = $subClassNameVars.get($index))
  62 + ${subClassNameVar}#if($subTable.subJoinMany)s#end?: ${subSimpleClassName}#if($subTable.subJoinMany)[]#else | null#end
  63 +#end
27 64 #end
28 65 }
29 66  
  67 +#if ($table.templateType == 2)
  68 +/** 获取${table.classComment}列表 */
  69 +export function get${simpleClassName}List(params?: Record<string, any>) {
  70 + return http.get<${simpleClassName}[]>('/${table.moduleName}/${simpleClassName_strikeCase}/list', params)
  71 +}
  72 +#else
30 73 /** 获取${table.classComment}分页列表 */
31 74 export function get${simpleClassName}Page(params: PageParam) {
32 75 return http.get<PageResult<${simpleClassName}>>('/${table.moduleName}/${simpleClassName_strikeCase}/page', params)
33 76 }
  77 +#end
34 78  
35 79 /** 获取${table.classComment}详情 */
36 80 export function get${simpleClassName}(id: ${primaryTsType}) {
... ... @@ -39,7 +83,7 @@ export function get${simpleClassName}(id: ${primaryTsType}) {
39 83  
40 84 /** 创建${table.classComment} */
41 85 export function create${simpleClassName}(data: ${simpleClassName}) {
42   - return http.post<number>('/${table.moduleName}/${simpleClassName_strikeCase}/create', data)
  86 + return http.post<${primaryTsType}>('/${table.moduleName}/${simpleClassName_strikeCase}/create', data)
43 87 }
44 88  
45 89 /** 更新${table.classComment} */
... ... @@ -51,3 +95,65 @@ export function update${simpleClassName}(data: ${simpleClassName}) {
51 95 export function delete${simpleClassName}(id: ${primaryTsType}) {
52 96 return http.delete<boolean>(`/${table.moduleName}/${simpleClassName_strikeCase}/delete?id=${id}`)
53 97 }
  98 +#foreach ($subTable in $subTables)
  99 +#set ($index = $foreach.count - 1)
  100 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  101 +#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
  102 +#set ($subJoinColumn = $subJoinColumns.get($index))
  103 +#set ($subPrimaryJavaType = $subPrimaryColumn.javaType.toLowerCase())
  104 +#if(${subPrimaryJavaType} == "long" || ${subPrimaryJavaType} == "integer" || ${subPrimaryJavaType} == "short" || ${subPrimaryJavaType} == "double" || ${subPrimaryJavaType} == "bigdecimal" || ${subPrimaryJavaType} == "byte")
  105 +#set ($subPrimaryTsType = "number")
  106 +#else
  107 +#set ($subPrimaryTsType = "string")
  108 +#end
  109 +#set ($subJoinJavaType = $subJoinColumn.javaType.toLowerCase())
  110 +#if(${subJoinJavaType} == "long" || ${subJoinJavaType} == "integer" || ${subJoinJavaType} == "short" || ${subJoinJavaType} == "double" || ${subJoinJavaType} == "bigdecimal" || ${subJoinJavaType} == "byte")
  111 +#set ($subJoinTsType = "number")
  112 +#else
  113 +#set ($subJoinTsType = "string")
  114 +#end
  115 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
  116 +#set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
  117 +#set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
  118 +
  119 +// ==================== 子表:${subTable.classComment} ====================
  120 +#if ($table.templateType == 11)
  121 +
  122 +/** 获取${subTable.classComment}分页列表 */
  123 +export function get${subSimpleClassName}Page(params: PageParam & { ${subJoinColumn.javaField}: ${subJoinTsType} }) {
  124 + return http.get<PageResult<${subSimpleClassName}>>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/page', params)
  125 +}
  126 +
  127 +/** 获取${subTable.classComment}详情 */
  128 +export function get${subSimpleClassName}(id: ${subPrimaryTsType}) {
  129 + return http.get<${subSimpleClassName}>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/get?id=${id}`)
  130 +}
  131 +
  132 +/** 创建${subTable.classComment} */
  133 +export function create${subSimpleClassName}(data: ${subSimpleClassName}) {
  134 + return http.post<${subPrimaryTsType}>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/create', data)
  135 +}
  136 +
  137 +/** 更新${subTable.classComment} */
  138 +export function update${subSimpleClassName}(data: ${subSimpleClassName}) {
  139 + return http.put<boolean>('/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/update', data)
  140 +}
  141 +
  142 +/** 删除${subTable.classComment} */
  143 +export function delete${subSimpleClassName}(id: ${subPrimaryTsType}) {
  144 + return http.delete<boolean>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/delete?id=${id}`)
  145 +}
  146 +#elseif ($subTable.subJoinMany)
  147 +
  148 +/** 获取${subTable.classComment}列表 */
  149 +export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}: ${subJoinTsType}) {
  150 + return http.get<${subSimpleClassName}[]>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=#[[${]]#${subJoinColumn.javaField}#[[}]]#`)
  151 +}
  152 +#else
  153 +
  154 +/** 获取${subTable.classComment} */
  155 +export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}: ${subJoinTsType}) {
  156 + return http.get<${subSimpleClassName} | null>(`/${table.moduleName}/${simpleClassName_strikeCase}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=#[[${]]#${subJoinColumn.javaField}#[[}]]#`)
  157 +}
  158 +#end
  159 +#end
... ...
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/components/breadcrumb_tree.vue.vm 0 → 100644
  1 +<template>
  2 + <view class="bg-white px-24rpx py-16rpx">
  3 + <scroll-view scroll-x class="whitespace-nowrap">
  4 + <view class="inline-flex items-center">
  5 + <view
  6 + class="flex items-center text-28rpx"
  7 + :class="items.length > 0 ? 'text-[#1890ff]' : 'text-[#333]'"
  8 + @click="handleClick(-1)"
  9 + >
  10 + <text>全部${table.classComment}</text>
  11 + </view>
  12 + <template v-for="(item, index) in items" :key="item.id">
  13 + <wd-icon name="arrow-right" size="12px" color="#999" custom-class="mx-8rpx" />
  14 + <view
  15 + class="flex items-center text-28rpx"
  16 + :class="index < items.length - 1 ? 'text-[#1890ff]' : 'text-[#333]'"
  17 + @click="handleClick(index)"
  18 + >
  19 + <text>{{ item.name }}</text>
  20 + </view>
  21 + </template>
  22 + </view>
  23 + </scroll-view>
  24 + </view>
  25 +</template>
  26 +
  27 +<script lang="ts" setup>
  28 +import { ref, watch } from 'vue'
  29 +
  30 +interface BreadcrumbItem {
  31 + id: number
  32 + name: string
  33 +}
  34 +
  35 +const props = defineProps<{
  36 + modelValue: number
  37 +}>()
  38 +
  39 +const emit = defineEmits<{
  40 + 'update:modelValue': [value: number]
  41 +}>()
  42 +
  43 +const items = ref<BreadcrumbItem[]>([]) // 面包屑数据
  44 +
  45 +/** 监听外部值变化 */
  46 +watch(() => props.modelValue, (value) => {
  47 + if (value === 0) {
  48 + items.value = []
  49 + }
  50 +})
  51 +
  52 +/** 点击面包屑 */
  53 +function handleClick(index: number) {
  54 + if (index === -1) {
  55 + reset()
  56 + return
  57 + }
  58 + if (index < items.value.length - 1) {
  59 + items.value = items.value.slice(0, index + 1)
  60 + emit('update:modelValue', items.value[index].id)
  61 + }
  62 +}
  63 +
  64 +/** 进入子层级 */
  65 +function enter(item: BreadcrumbItem) {
  66 + items.value.push(item)
  67 + emit('update:modelValue', item.id)
  68 +}
  69 +
  70 +/** 返回上一层级 */
  71 +function back(): boolean {
  72 + if (items.value.length === 0) {
  73 + return false
  74 + }
  75 + items.value.pop()
  76 + emit('update:modelValue', items.value.at(-1)?.id ?? 0)
  77 + return true
  78 +}
  79 +
  80 +/** 重置面包屑 */
  81 +function reset() {
  82 + items.value = []
  83 + emit('update:modelValue', 0)
  84 +}
  85 +
  86 +defineExpose({ back, enter, reset })
  87 +</script>
... ...
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail/index.vue.vm
... ... @@ -39,7 +39,7 @@
39 39 </wd-button>
40 40 <wd-button
41 41 v-if="hasAccessByCodes(['${permissionPrefix}:delete'])"
42   - class="flex-1" type="error" :loading="deleting" @click="handleDelete"
  42 + class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
43 43 >
44 44 删除
45 45 </wd-button>
... ... @@ -49,6 +49,12 @@
49 49 </template>
50 50  
51 51 <script lang="ts" setup>
  52 +#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
  53 +#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
  54 +#set ($primaryTsType = "number")
  55 +#else
  56 +#set ($primaryTsType = "string")
  57 +#end
52 58 #set ($hasDict = 0)
53 59 #foreach($column in $columns)
54 60 #if ($hasDict == 0 && $column.dictType && "" != $column.dictType)
... ... @@ -64,6 +70,7 @@
64 70 import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
65 71 import { onUnload } from '@dcloudio/uni-app'
66 72 import { onMounted, ref } from 'vue'
  73 +import { useDialog } from '@wot-ui/ui/components/wd-dialog'
67 74 import { useToast } from '@wot-ui/ui/components/wd-toast'
68 75 import { delete${simpleClassName}, get${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
69 76 import { useAccess } from '@/hooks/useAccess'
... ... @@ -76,7 +83,7 @@ import { formatDateTime } from &#39;@/utils/date&#39;
76 83 #end
77 84  
78 85 const props = defineProps<{
79   - id?: number | any
  86 + id?: ${primaryTsType}
80 87 }>()
81 88  
82 89 definePage({
... ... @@ -87,6 +94,7 @@ definePage({
87 94 })
88 95  
89 96 const { hasAccessByCodes } = useAccess()
  97 +const dialog = useDialog()
90 98 const toast = useToast()
91 99 const formData = ref<${simpleClassName}>() // 详情数据
92 100 const deleting = ref(false) // 删除状态
... ... @@ -117,28 +125,27 @@ function handleEdit() {
117 125 }
118 126  
119 127 /** 删除${table.classComment} */
120   -function handleDelete() {
  128 +async function handleDelete() {
121 129 if (!props.id) {
122 130 return
123 131 }
124   - uni.showModal({
125   - title: '提示',
126   - content: '确定要删除该${table.classComment}吗?',
127   - success: async (res) => {
128   - if (!res.confirm) {
129   - return
130   - }
131   - deleting.value = true
132   - try {
133   - await delete${simpleClassName}(props.id)
134   - toast.success('删除成功')
135   - uni.$emit('${table.moduleName}:${table.businessName}:reload')
136   - delay(handleBack)
137   - } finally {
138   - deleting.value = false
139   - }
140   - },
141   - })
  132 + try {
  133 + await dialog.confirm({
  134 + title: '提示',
  135 + msg: '确定要删除该${table.classComment}吗?',
  136 + })
  137 + } catch {
  138 + return
  139 + }
  140 + deleting.value = true
  141 + try {
  142 + await delete${simpleClassName}(props.id)
  143 + toast.success('删除成功')
  144 + uni.$emit('${table.moduleName}:${table.businessName}:reload')
  145 + delay(handleBack)
  146 + } finally {
  147 + deleting.value = false
  148 + }
142 149 }
143 150  
144 151 /** 初始化 */
... ...
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail_erp.vue.vm 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar title="${table.classComment}详情" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
  5 +
  6 + <view class="pb-160rpx">
  7 + <!-- 主表详情 -->
  8 + <wd-cell-group border title="${table.classComment}信息">
  9 +#foreach($column in $columns)
  10 +#if ($column.primaryKey || $column.listOperationResult || $column.createOperation || $column.updateOperation)
  11 +#if ($column.dictType && "" != $column.dictType)
  12 + <wd-cell title="${column.columnComment}">
  13 + <dict-tag :type="DICT_TYPE.${column.dictType.toUpperCase()}" :value="formData?.${column.javaField}" />
  14 + </wd-cell>
  15 +#elseif ($column.javaType == "LocalDateTime")
  16 + <wd-cell title="${column.columnComment}" :value="formatDateTime(formData?.${column.javaField}) || '-'" />
  17 +#else
  18 + <wd-cell title="${column.columnComment}" :value="formData?.${column.javaField} ?? '-'" />
  19 +#end
  20 +#end
  21 +#end
  22 + </wd-cell-group>
  23 +
  24 +#foreach ($subTable in $subTables)
  25 +#set ($index = $foreach.count - 1)
  26 +#set ($subColumns = $subColumnsList.get($index))
  27 +#set ($subClassNameVar = $subClassNameVars.get($index))
  28 +#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
  29 +#set ($pageSize = 10)
  30 +#if (!$subTable.subJoinMany)
  31 +#set ($pageSize = 1)
  32 +#end
  33 + <!-- 子表:${subTable.classComment} -->
  34 + <view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
  35 + <text class="text-28rpx text-[#333] font-semibold">${subTable.classComment}({{ ${subClassNameVar}Total }})</text>
  36 + <wd-button
  37 +#if (!$subTable.subJoinMany)
  38 + v-if="${subClassNameVar}Total === 0 && hasAccessByCodes(['${permissionPrefix}:create'])"
  39 +#else
  40 + v-if="hasAccessByCodes(['${permissionPrefix}:create'])"
  41 +#end
  42 + size="small" type="primary" variant="plain" @click="open${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}()"
  43 + >
  44 + 添加
  45 + </wd-button>
  46 + </view>
  47 + <view class="px-24rpx">
  48 + <z-paging
  49 + ref="${subClassNameVar}PagingRef"
  50 + v-model="${subClassNameVar}List"
  51 + :fixed="false"
  52 + :auto="false"
  53 + height="560rpx"
  54 + :default-page-size="${pageSize}"
  55 + :refresher-enabled="false"
  56 + :inside-more="true"
  57 + :to-bottom-loading-more-enabled="false"
  58 + loading-more-default-text="点击加载更多"
  59 + empty-view-text="暂无${subTable.classComment}"
  60 + @query="query${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}List"
  61 + >
  62 + <view
  63 + v-for="item in ${subClassNameVar}List"
  64 + :key="item.${subPrimaryColumn.javaField}"
  65 + class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
  66 + >
  67 +#foreach ($column in $subColumns)
  68 +#if ($column.listOperationResult && !$column.primaryKey)
  69 + <view class="mb-8rpx text-28rpx text-[#666]">
  70 + ${column.columnComment}:{{ item.${column.javaField} ?? '-' }}
  71 + </view>
  72 +#end
  73 +#end
  74 + <view class="mt-16rpx flex justify-end gap-16rpx">
  75 + <wd-button size="small" variant="plain" @click="open${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item)">
  76 + 编辑
  77 + </wd-button>
  78 + <wd-button size="small" type="danger" variant="plain" @click="remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item)">
  79 + 删除
  80 + </wd-button>
  81 + </view>
  82 + </view>
  83 + </z-paging>
  84 + </view>
  85 +#end
  86 + </view>
  87 +
  88 + <!-- 底部操作按钮 -->
  89 + <view class="yd-detail-footer">
  90 + <view class="yd-detail-footer-actions">
  91 + <wd-button v-if="hasAccessByCodes(['${permissionPrefix}:update'])" class="flex-1" type="warning" @click="handleEdit">
  92 + 编辑
  93 + </wd-button>
  94 + <wd-button v-if="hasAccessByCodes(['${permissionPrefix}:delete'])" class="flex-1" type="danger" :loading="deleting" @click="handleDelete">
  95 + 删除
  96 + </wd-button>
  97 + </view>
  98 + </view>
  99 + </view>
  100 +</template>
  101 +
  102 +<script lang="ts" setup>
  103 +#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
  104 +#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
  105 +#set ($primaryTsType = "number")
  106 +#else
  107 +#set ($primaryTsType = "string")
  108 +#end
  109 +#set ($hasDict = 0)
  110 +#set ($hasDateTime = 0)
  111 +#foreach($column in $columns)
  112 +#if ($column.dictType && "" != $column.dictType)
  113 +#set ($hasDict = 1)
  114 +#end
  115 +#if ($column.javaType == "LocalDateTime")
  116 +#set ($hasDateTime = 1)
  117 +#end
  118 +#end
  119 +import type {
  120 + ${simpleClassName},
  121 +#foreach ($subSimpleClassName in $subSimpleClassNames)
  122 + ${subSimpleClassName},
  123 +#end
  124 +} from '@/api/${table.moduleName}/${table.businessName}'
  125 +import { onShow } from '@dcloudio/uni-app'
  126 +import { nextTick, ref } from 'vue'
  127 +import { useDialog } from '@wot-ui/ui/components/wd-dialog'
  128 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  129 +import {
  130 + delete${simpleClassName},
  131 +#foreach ($subSimpleClassName in $subSimpleClassNames)
  132 + delete${subSimpleClassName},
  133 +#end
  134 + get${simpleClassName},
  135 +#foreach ($subSimpleClassName in $subSimpleClassNames)
  136 + get${subSimpleClassName}Page,
  137 +#end
  138 +} from '@/api/${table.moduleName}/${table.businessName}'
  139 +import { useAccess } from '@/hooks/useAccess'
  140 +import { delay, navigateBackPlus } from '@/utils'
  141 +#if ($hasDict == 1)
  142 +import { DICT_TYPE } from '@/utils/constants'
  143 +#end
  144 +#if ($hasDateTime == 1)
  145 +import { formatDateTime } from '@/utils/date'
  146 +#end
  147 +
  148 +const props = defineProps<{ id?: ${primaryTsType} }>()
  149 +
  150 +definePage({
  151 + style: { navigationBarTitleText: '', navigationStyle: 'custom' },
  152 +})
  153 +
  154 +const { hasAccessByCodes } = useAccess()
  155 +const dialog = useDialog()
  156 +const toast = useToast()
  157 +const formData = ref<${simpleClassName}>() // 主表数据
  158 +#foreach ($subTable in $subTables)
  159 +#set ($index = $foreach.count - 1)
  160 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  161 +#set ($subClassNameVar = $subClassNameVars.get($index))
  162 +const ${subClassNameVar}List = ref<${subSimpleClassName}[]>([]) // ${subTable.classComment}列表
  163 +const ${subClassNameVar}Total = ref(0) // ${subTable.classComment}总数
  164 +const ${subClassNameVar}PagingRef = ref<any>() // ${subTable.classComment}分页组件引用
  165 +#end
  166 +const deleting = ref(false) // 主表删除状态
  167 +
  168 +/** 返回上一页 */
  169 +function handleBack() {
  170 + navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
  171 +}
  172 +
  173 +/** 加载主表 */
  174 +async function loadMain() {
  175 + if (props.id) {
  176 + formData.value = await get${simpleClassName}(props.id)
  177 + }
  178 +}
  179 +
  180 +#foreach ($subTable in $subTables)
  181 +#set ($index = $foreach.count - 1)
  182 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  183 +#set ($subClassNameVar = $subClassNameVars.get($index))
  184 +#set ($subJoinColumn = $subJoinColumns.get($index))
  185 +#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
  186 +/** 查询${subTable.classComment}列表 */
  187 +async function query${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}List(pageNo: number, pageSize: number) {
  188 + if (!props.id) {
  189 + ${subClassNameVar}PagingRef.value?.completeByTotal([], 0)
  190 + return
  191 + }
  192 + try {
  193 + const data = await get${subSimpleClassName}Page({ ${subJoinColumn.javaField}: props.id, pageNo, pageSize })
  194 + ${subClassNameVar}Total.value = data.total
  195 + ${subClassNameVar}PagingRef.value?.completeByTotal(data.list, data.total)
  196 + } catch {
  197 + ${subClassNameVar}PagingRef.value?.complete(false)
  198 + }
  199 +}
  200 +
  201 +/** 打开${subTable.classComment}表单 */
  202 +function open${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item?: ${subSimpleClassName}) {
  203 + const query = [
  204 + `${subJoinColumn.javaField}=#[[${]]#props.id#[[}]]#`,
  205 + item?.${subPrimaryColumn.javaField} ? `id=#[[${]]#item.${subPrimaryColumn.javaField}#[[}]]#` : '',
  206 + ].filter(Boolean).join('&')
  207 + uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/${subSimpleClassName_strikeCases.get($index)}/form/index?#[[${]]#query#[[}]]#` })
  208 +}
  209 +
  210 +/** 删除${subTable.classComment} */
  211 +async function remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(item: ${subSimpleClassName}) {
  212 + try {
  213 + await dialog.confirm({ title: '提示', msg: '确定要删除该${subTable.classComment}吗?' })
  214 + } catch {
  215 + return
  216 + }
  217 + await delete${subSimpleClassName}(item.${subPrimaryColumn.javaField}!)
  218 + toast.success('删除成功')
  219 + ${subClassNameVar}PagingRef.value?.reload()
  220 +}
  221 +
  222 +#end
  223 +/** 编辑主表 */
  224 +function handleEdit() {
  225 + uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/form/index?id=#[[${]]#props.id#[[}]]#` })
  226 +}
  227 +
  228 +/** 删除主表 */
  229 +async function handleDelete() {
  230 + if (!props.id) {
  231 + return
  232 + }
  233 + try {
  234 + await dialog.confirm({ title: '提示', msg: '确定要删除该${table.classComment}吗?' })
  235 + } catch {
  236 + return
  237 + }
  238 + deleting.value = true
  239 + try {
  240 + await delete${simpleClassName}(props.id)
  241 + toast.success('删除成功')
  242 + uni.$emit('${table.moduleName}:${table.businessName}:reload')
  243 + delay(handleBack)
  244 + } finally {
  245 + deleting.value = false
  246 + }
  247 +}
  248 +
  249 +/** 进入页面加载(含编辑和子表返回后刷新) */
  250 +onShow(() => {
  251 + loadMain()
  252 + nextTick(() => {
  253 +#foreach ($subClassNameVar in $subClassNameVars)
  254 + ${subClassNameVar}PagingRef.value?.reload()
  255 +#end
  256 + })
  257 +})
  258 +</script>
... ...
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/detail_master.vue.vm 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + title="${table.classComment}详情"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <view class="pb-160rpx">
  11 + <!-- 主表详情 -->
  12 + <wd-cell-group border title="${table.classComment}信息">
  13 +#foreach($column in $columns)
  14 +#if ($column.primaryKey || $column.listOperationResult || $column.createOperation || $column.updateOperation)
  15 +#if ($column.dictType && "" != $column.dictType)
  16 + <wd-cell title="${column.columnComment}">
  17 + <dict-tag :type="DICT_TYPE.${column.dictType.toUpperCase()}" :value="formData?.${column.javaField}" />
  18 + </wd-cell>
  19 +#elseif ($column.javaType == "LocalDateTime")
  20 + <wd-cell title="${column.columnComment}" :value="formatDateTime(formData?.${column.javaField}) || '-'" />
  21 +#else
  22 + <wd-cell title="${column.columnComment}" :value="formData?.${column.javaField} ?? '-'" />
  23 +#end
  24 +#end
  25 +#end
  26 + </wd-cell-group>
  27 +
  28 +#foreach ($subTable in $subTables)
  29 +#set ($index = $foreach.count - 1)
  30 +#set ($subColumns = $subColumnsList.get($index))
  31 +#set ($subClassNameVar = $subClassNameVars.get($index))
  32 +#set ($subPrimaryColumn = $subPrimaryColumns.get($index))
  33 + <!-- 子表:${subTable.classComment} -->
  34 + <view class="mt-20rpx px-24rpx py-16rpx text-28rpx text-[#999]">
  35 + ${subTable.classComment}#if($subTable.subJoinMany)({{ ${subClassNameVar}s.length }})#end
  36 + </view>
  37 + <view class="px-24rpx">
  38 +#if ($subTable.subJoinMany)
  39 + <view v-if="!${subClassNameVar}s.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
  40 + 暂无${subTable.classComment}
  41 + </view>
  42 + <view
  43 + v-for="item in ${subClassNameVar}s"
  44 + :key="item.${subPrimaryColumn.javaField}"
  45 + class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
  46 + >
  47 +#foreach ($column in $subColumns)
  48 +#if ($column.listOperationResult && !$column.primaryKey)
  49 + <view class="mb-8rpx text-28rpx text-[#666]">
  50 + ${column.columnComment}:{{ item.${column.javaField} ?? '-' }}
  51 + </view>
  52 +#end
  53 +#end
  54 + </view>
  55 +#else
  56 + <view class="rounded-12rpx bg-white p-24rpx shadow-sm">
  57 + <template v-if="${subClassNameVar}">
  58 +#foreach ($column in $subColumns)
  59 +#if ($column.listOperationResult && !$column.primaryKey)
  60 + <view class="mb-8rpx text-28rpx text-[#666]">
  61 + ${column.columnComment}:{{ ${subClassNameVar}.${column.javaField} ?? '-' }}
  62 + </view>
  63 +#end
  64 +#end
  65 + </template>
  66 + <view v-else class="text-center text-26rpx text-[#999]">
  67 + 暂无${subTable.classComment}
  68 + </view>
  69 + </view>
  70 +#end
  71 + </view>
  72 +#end
  73 + </view>
  74 +
  75 + <!-- 底部操作按钮 -->
  76 + <view class="yd-detail-footer">
  77 + <view class="yd-detail-footer-actions">
  78 + <wd-button
  79 + v-if="hasAccessByCodes(['${permissionPrefix}:update'])"
  80 + class="flex-1" type="warning" @click="handleEdit"
  81 + >
  82 + 编辑
  83 + </wd-button>
  84 + <wd-button
  85 + v-if="hasAccessByCodes(['${permissionPrefix}:delete'])"
  86 + class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
  87 + >
  88 + 删除
  89 + </wd-button>
  90 + </view>
  91 + </view>
  92 + </view>
  93 +</template>
  94 +
  95 +<script lang="ts" setup>
  96 +#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
  97 +#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
  98 +#set ($primaryTsType = "number")
  99 +#else
  100 +#set ($primaryTsType = "string")
  101 +#end
  102 +#set ($hasDict = 0)
  103 +#set ($hasDateTime = 0)
  104 +#foreach($column in $columns)
  105 +#if ($column.dictType && "" != $column.dictType)
  106 +#set ($hasDict = 1)
  107 +#end
  108 +#if ($column.javaType == "LocalDateTime")
  109 +#set ($hasDateTime = 1)
  110 +#end
  111 +#end
  112 +import type {
  113 + ${simpleClassName},
  114 +#foreach ($subSimpleClassName in $subSimpleClassNames)
  115 + ${subSimpleClassName},
  116 +#end
  117 +} from '@/api/${table.moduleName}/${table.businessName}'
  118 +import { onShow } from '@dcloudio/uni-app'
  119 +import { ref } from 'vue'
  120 +import { useDialog } from '@wot-ui/ui/components/wd-dialog'
  121 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  122 +import {
  123 + delete${simpleClassName},
  124 + get${simpleClassName},
  125 +#foreach ($subTable in $subTables)
  126 +#set ($index = $foreach.count - 1)
  127 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  128 +#set ($subJoinColumn = $subJoinColumns.get($index))
  129 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
  130 +#if ($subTable.subJoinMany)
  131 + get${subSimpleClassName}ListBy${SubJoinColumnName},
  132 +#else
  133 + get${subSimpleClassName}By${SubJoinColumnName},
  134 +#end
  135 +#end
  136 +} from '@/api/${table.moduleName}/${table.businessName}'
  137 +import { useAccess } from '@/hooks/useAccess'
  138 +import { delay, navigateBackPlus } from '@/utils'
  139 +#if ($hasDict == 1)
  140 +import { DICT_TYPE } from '@/utils/constants'
  141 +#end
  142 +#if ($hasDateTime == 1)
  143 +import { formatDateTime } from '@/utils/date'
  144 +#end
  145 +
  146 +const props = defineProps<{
  147 + id?: ${primaryTsType}
  148 +}>()
  149 +
  150 +definePage({
  151 + style: {
  152 + navigationBarTitleText: '',
  153 + navigationStyle: 'custom',
  154 + },
  155 +})
  156 +
  157 +const { hasAccessByCodes } = useAccess()
  158 +const dialog = useDialog()
  159 +const toast = useToast()
  160 +const formData = ref<${simpleClassName}>() // 主表数据
  161 +#foreach ($subTable in $subTables)
  162 +#set ($index = $foreach.count - 1)
  163 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  164 +#set ($subClassNameVar = $subClassNameVars.get($index))
  165 +#if ($subTable.subJoinMany)
  166 +const ${subClassNameVar}s = ref<${subSimpleClassName}[]>([]) // ${subTable.classComment}数据
  167 +#else
  168 +const ${subClassNameVar} = ref<${subSimpleClassName} | null>(null) // ${subTable.classComment}数据
  169 +#end
  170 +#end
  171 +const deleting = ref(false) // 删除状态
  172 +
  173 +/** 返回上一页 */
  174 +function handleBack() {
  175 + navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
  176 +}
  177 +
  178 +/** 加载主表和子表详情 */
  179 +async function getDetail() {
  180 + if (!props.id || deleting.value) {
  181 + return
  182 + }
  183 + const [data#foreach ($subTable in $subTables)#set ($index = $foreach.count - 1), ${subClassNameVars.get($index)}Data#end] = await Promise.all([
  184 + get${simpleClassName}(props.id),
  185 +#foreach ($subTable in $subTables)
  186 +#set ($index = $foreach.count - 1)
  187 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  188 +#set ($subJoinColumn = $subJoinColumns.get($index))
  189 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
  190 +#if ($subTable.subJoinMany)
  191 + get${subSimpleClassName}ListBy${SubJoinColumnName}(props.id),
  192 +#else
  193 + get${subSimpleClassName}By${SubJoinColumnName}(props.id),
  194 +#end
  195 +#end
  196 + ])
  197 + formData.value = data
  198 +#foreach ($subTable in $subTables)
  199 +#set ($index = $foreach.count - 1)
  200 +#set ($subClassNameVar = $subClassNameVars.get($index))
  201 +#if ($subTable.subJoinMany)
  202 + ${subClassNameVar}s.value = ${subClassNameVar}Data || []
  203 +#else
  204 + ${subClassNameVar}.value = ${subClassNameVar}Data || null
  205 +#end
  206 +#end
  207 +}
  208 +
  209 +/** 编辑${table.classComment} */
  210 +function handleEdit() {
  211 + uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/form/index?id=#[[${]]#props.id#[[}]]#` })
  212 +}
  213 +
  214 +/** 删除${table.classComment} */
  215 +async function handleDelete() {
  216 + if (!props.id) {
  217 + return
  218 + }
  219 + try {
  220 + await dialog.confirm({ title: '提示', msg: '确定要删除该${table.classComment}吗?' })
  221 + } catch {
  222 + return
  223 + }
  224 + deleting.value = true
  225 + try {
  226 + await delete${simpleClassName}(props.id)
  227 + toast.success('删除成功')
  228 + uni.$emit('${table.moduleName}:${table.businessName}:reload')
  229 + delay(handleBack)
  230 + } finally {
  231 + deleting.value = false
  232 + }
  233 +}
  234 +
  235 +/** 进入页面加载(含编辑返回后刷新) */
  236 +onShow(() => {
  237 + getDetail()
  238 +})
  239 +</script>
... ...
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form/index.vue.vm
... ... @@ -12,21 +12,31 @@
12 12 <wd-form ref="formRef" :model="formData" :schema="formSchema">
13 13 <wd-cell-group border>
14 14 #foreach($column in $columns)
15   - #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey)
16   - #set ($dictType = $column.dictType)
17   - #set ($javaField = $column.javaField)
18   - #set ($javaType = $column.javaType)
19   - #set ($comment = $column.columnComment)
20   - #set ($dictMethod = "getDictOptions")
21   - #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
22   - #set ($dictMethod = "getIntDictOptions")
23   - #elseif ($javaType == "String")
24   - #set ($dictMethod = "getStrDictOptions")
25   - #elseif ($javaType == "Boolean")
26   - #set ($dictMethod = "getBoolDictOptions")
27   - #end
28   - ## 下拉选择(字典)→ yd-form-picker,内部按 dictType è§£æžé€‰é¡¹ï¼Œé€‚åˆé€‰é¡¹è¾ƒå¤š
29   - #if ($column.htmlType == "select" && $dictType && "" != $dictType)
  15 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey)
  16 +#set ($dictType = $column.dictType)
  17 +#set ($javaField = $column.javaField)
  18 +#set ($javaType = $column.javaType)
  19 +#set ($comment = $column.columnComment)
  20 +#set ($dictMethod = "getDictOptions")
  21 +#if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
  22 +#set ($dictMethod = "getIntDictOptions")
  23 +#elseif ($javaType == "String")
  24 +#set ($dictMethod = "getStrDictOptions")
  25 +#elseif ($javaType == "Boolean")
  26 +#set ($dictMethod = "getBoolDictOptions")
  27 +#end
  28 +## 树表的父级选择
  29 +#if ($table.templateType == 2 && $column.id == $treeParentColumn.id)
  30 + <yd-tree-select
  31 + v-model="formData.${javaField}"
  32 + :data="treeData"
  33 + label="${comment}"
  34 + prop="${javaField}"
  35 + :props="{ label: '${treeNameColumn.javaField}', value: '${primaryColumn.javaField}' }"
  36 + placeholder="请选择${comment}"
  37 + />
  38 +## 下拉选择(字典)→ yd-form-picker,内部按 dictType è§£æžé€‰é¡¹ï¼Œé€‚åˆé€‰é¡¹è¾ƒå¤š
  39 +#elseif ($column.htmlType == "select" && $dictType && "" != $dictType)
30 40 <yd-form-picker
31 41 v-model="formData.${javaField}"
32 42 label="${comment}"
... ... @@ -38,8 +48,8 @@
38 48 #end
39 49 placeholder="请选择${comment}"
40 50 />
41   - ## å•选(字典)→ radio-group,适åˆé€‰é¡¹è¾ƒå°‘
42   - #elseif ($column.htmlType == "radio" && $dictType && "" != $dictType)
  51 +## å•选(字典)→ radio-group,适åˆé€‰é¡¹è¾ƒå°‘
  52 +#elseif ($column.htmlType == "radio" && $dictType && "" != $dictType)
43 53 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}" center>
44 54 <wd-radio-group v-model="formData.${javaField}" type="button">
45 55 <wd-radio
... ... @@ -51,11 +61,14 @@
51 61 </wd-radio>
52 62 </wd-radio-group>
53 63 </wd-form-item>
54   - ## 数字类型(无字典)
55   - #elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte")
  64 +## 数字类型(无字典)
  65 +#elseif (${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer" || ${javaType.toLowerCase()} == "short" || ${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal" || ${javaType.toLowerCase()} == "byte")
56 66 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
57 67 <wd-input-number
58 68 v-model="formData.${javaField}"
  69 +#if ($column.nullable)
  70 + allow-null
  71 +#end
59 72 #if (${javaType.toLowerCase()} == "double" || ${javaType.toLowerCase()} == "bigdecimal")
60 73 :precision="2"
61 74 input-type="number"
... ... @@ -63,37 +76,42 @@
63 76 :min="0"
64 77 />
65 78 </wd-form-item>
66   - ## 布尔类型
67   - #elseif (${javaType.toLowerCase()} == "boolean")
  79 +## 布尔类型
  80 +#elseif (${javaType.toLowerCase()} == "boolean")
68 81 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}" center>
69 82 <wd-switch v-model="formData.${javaField}" />
70 83 </wd-form-item>
71   - ## 日期时间类型
72   - #elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime")
73   - #set ($pickerType = "date")
74   - #if (${javaType.toLowerCase()} == "localdatetime")
75   - #set ($pickerType = "datetime")
76   - #end
  84 +## 日期时间类型
  85 +#elseif (${javaType.toLowerCase()} == "date" || ${javaType.toLowerCase()} == "localdate" || ${javaType.toLowerCase()} == "localdatetime")
  86 +#set ($pickerType = "date")
  87 +#if (${javaType.toLowerCase()} == "localdatetime")
  88 +#set ($pickerType = "datetime")
  89 +#end
77 90 <wd-form-item
78 91 title="${comment}"
79 92 title-width="180rpx"
80 93 prop="${javaField}"
81 94 is-link
82 95 #if ($pickerType == "datetime")
83   - :value="formatDateTime(formData.${javaField})"
  96 + :value="formData.${javaField} ? formatDateTime(formData.${javaField}) : ''"
84 97 #else
85   - :value="formatDate(formData.${javaField})"
  98 + :value="formData.${javaField} ? formatDate(formData.${javaField}) : ''"
86 99 #end
87 100 placeholder="请选择${comment}"
88 101 @click="${javaField}Visible = true"
89 102 />
90 103 <wd-datetime-picker
91   - v-model="formData.${javaField}"
92 104 v-model:visible="${javaField}Visible"
  105 + :model-value="formData.${javaField} || Date.now()"
93 106 type="${pickerType}"
  107 +#if ($pickerType == "datetime")
  108 + @update:model-value="value => formData.${javaField} = value"
  109 +#else
  110 + @update:model-value="value => formData.${javaField} = value"
  111 +#end
94 112 />
95   - ## 文本域
96   - #elseif ($column.htmlType == "textarea")
  113 +## 文本域
  114 +#elseif ($column.htmlType == "textarea")
97 115 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
98 116 <wd-textarea
99 117 v-model="formData.${javaField}"
... ... @@ -103,18 +121,18 @@
103 121 show-word-limit
104 122 />
105 123 </wd-form-item>
106   - ## 图片上传 → yd-upload-img(包 wd-form-item æ‰èƒ½æ³¨å†Œåˆ° wd-form åšæ ¡éªŒ/错误展示)
107   - #elseif ($column.htmlType == "imageUpload")
  124 +## 图片上传 → yd-upload-img(包 wd-form-item æ‰èƒ½æ³¨å†Œåˆ° wd-form åšæ ¡éªŒ/错误展示)
  125 +#elseif ($column.htmlType == "imageUpload")
108 126 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
109 127 <yd-upload-img v-model="formData.${javaField}" directory="${table.moduleName}/${table.businessName}" />
110 128 </wd-form-item>
111   - ## 文件上传 → yd-upload-file
112   - #elseif ($column.htmlType == "fileUpload")
  129 +## 文件上传 → yd-upload-file
  130 +#elseif ($column.htmlType == "fileUpload")
113 131 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
114 132 <yd-upload-file v-model="formData.${javaField}" directory="${table.moduleName}/${table.businessName}" />
115 133 </wd-form-item>
116   - ## 默认:文本输入
117   - #else
  134 +## 默认:文本输入
  135 +#else
118 136 <wd-form-item title="${comment}" title-width="180rpx" prop="${javaField}">
119 137 <wd-input
120 138 v-model="formData.${javaField}"
... ... @@ -122,11 +140,259 @@
122 140 placeholder="请输入${comment}"
123 141 />
124 142 </wd-form-item>
125   - #end
126   - #end
  143 +#end
  144 +#end
127 145 #end
128 146 </wd-cell-group>
129 147 </wd-form>
  148 +#if ($table.templateType == 10 || $table.templateType == 12)
  149 +#foreach ($subTable in $subTables)
  150 +#set ($index = $foreach.count - 1)
  151 +#set ($subColumns = $subColumnsList.get($index))
  152 +#set ($subJoinColumn = $subJoinColumns.get($index))
  153 +#set ($subClassNameVar = $subClassNameVars.get($index))
  154 +
  155 + <!-- å­è¡¨ï¼š${subTable.classComment} -->
  156 + <view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
  157 + <text class="text-28rpx text-[#333] font-semibold">${subTable.classComment}</text>
  158 +#if ($subTable.subJoinMany)
  159 + <wd-button size="small" type="primary" variant="plain" @click="add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}">
  160 + 添加
  161 + </wd-button>
  162 +#else
  163 + <wd-button
  164 + v-if="!${subClassNameVar}"
  165 + size="small"
  166 + type="primary"
  167 + variant="plain"
  168 + @click="add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}()"
  169 + >
  170 + 添加
  171 + </wd-button>
  172 +#end
  173 + </view>
  174 + <view class="px-24rpx">
  175 +#if ($subTable.subJoinMany)
  176 + <view v-if="!${subClassNameVar}s.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
  177 + 暂无${subTable.classComment}
  178 + </view>
  179 + <view
  180 + v-for="(item, index) in ${subClassNameVar}s"
  181 + :key="index"
  182 + class="mb-20rpx rounded-12rpx bg-white p-24rpx shadow-sm"
  183 + >
  184 + <view class="mb-16rpx flex justify-end">
  185 + <wd-button size="small" type="danger" variant="plain" @click="remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(index)">
  186 + 删除
  187 + </wd-button>
  188 + </view>
  189 +#foreach ($column in $subColumns)
  190 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
  191 +#set ($javaType = $column.javaType.toLowerCase())
  192 +#set ($dictMethod = "getDictOptions")
  193 +#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
  194 +#set ($dictMethod = "getIntDictOptions")
  195 +#elseif ($column.javaType == "String")
  196 +#set ($dictMethod = "getStrDictOptions")
  197 +#elseif ($column.javaType == "Boolean")
  198 +#set ($dictMethod = "getBoolDictOptions")
  199 +#end
  200 +#if ($column.htmlType == "select" && $column.dictType && "" != $column.dictType)
  201 + <yd-form-picker
  202 + v-model="item.${column.javaField}"
  203 + label="${column.columnComment}"
  204 + label-width="180rpx"
  205 + :dict-type="DICT_TYPE.${column.dictType.toUpperCase()}"
  206 +#if ($column.javaType == "String")
  207 + dict-kind="str"
  208 +#end
  209 + placeholder="请选择${column.columnComment}"
  210 + />
  211 +#elseif ($column.htmlType == "radio" && $column.dictType && "" != $column.dictType)
  212 + <wd-form-item title="${column.columnComment}" title-width="180rpx" center>
  213 + <wd-radio-group v-model="item.${column.javaField}" type="button">
  214 + <wd-radio
  215 + v-for="dict in $dictMethod(DICT_TYPE.${column.dictType.toUpperCase()})"
  216 + :key="dict.value"
  217 + :value="dict.value"
  218 + >
  219 + {{ dict.label }}
  220 + </wd-radio>
  221 + </wd-radio-group>
  222 + </wd-form-item>
  223 +#elseif ($javaType == "boolean")
  224 + <wd-form-item title="${column.columnComment}" title-width="180rpx" center>
  225 + <wd-switch v-model="item.${column.javaField}" />
  226 + </wd-form-item>
  227 +#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
  228 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  229 + <wd-input-number
  230 + v-model="item.${column.javaField}"
  231 +#if($column.nullable)
  232 + allow-null
  233 +#end
  234 +#if ($javaType == "double" || $javaType == "bigdecimal")
  235 + :precision="2"
  236 + input-type="number"
  237 +#end
  238 + :min="0"
  239 + />
  240 + </wd-form-item>
  241 +#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
  242 +#set ($subPickerType = "date")
  243 +#if ($javaType == "localdatetime")
  244 +#set ($subPickerType = "datetime")
  245 +#end
  246 + <wd-form-item
  247 + title="${column.columnComment}"
  248 + title-width="180rpx"
  249 + is-link
  250 +#if ($javaType == "localdatetime")
  251 + :value="item.${column.javaField} ? formatDateTime(item.${column.javaField}) : ''"
  252 +#else
  253 + :value="item.${column.javaField} ? formatDate(item.${column.javaField}) : ''"
  254 +#end
  255 + placeholder="请选择${column.columnComment}"
  256 + @click="subDateVisible[`${subClassNameVar}s.#[[${]]#index#[[}]]#.${column.javaField}`] = true"
  257 + />
  258 + <wd-datetime-picker
  259 + v-model:visible="subDateVisible[`${subClassNameVar}s.#[[${]]#index#[[}]]#.${column.javaField}`]"
  260 + :model-value="item.${column.javaField} || Date.now()"
  261 + type="${subPickerType}"
  262 +#if ($javaType == "localdatetime")
  263 + @update:model-value="value => item.${column.javaField} = value"
  264 +#else
  265 + @update:model-value="value => item.${column.javaField} = value"
  266 +#end
  267 + />
  268 +#elseif ($column.htmlType == "textarea")
  269 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  270 + <wd-textarea v-model="item.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
  271 + </wd-form-item>
  272 +#elseif ($column.htmlType == "imageUpload")
  273 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  274 + <yd-upload-img v-model="item.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
  275 + </wd-form-item>
  276 +#elseif ($column.htmlType == "fileUpload")
  277 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  278 + <yd-upload-file v-model="item.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
  279 + </wd-form-item>
  280 +#else
  281 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  282 + <wd-input v-model="item.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
  283 + </wd-form-item>
  284 +#end
  285 +#end
  286 +#end
  287 + </view>
  288 +#else
  289 + <view v-if="!${subClassNameVar}" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
  290 + 暂无${subTable.classComment}
  291 + </view>
  292 + <view v-else class="rounded-12rpx bg-white p-24rpx shadow-sm">
  293 +#foreach ($column in $subColumns)
  294 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
  295 +#set ($javaType = $column.javaType.toLowerCase())
  296 +#set ($dictMethod = "getDictOptions")
  297 +#if ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short")
  298 +#set ($dictMethod = "getIntDictOptions")
  299 +#elseif ($column.javaType == "String")
  300 +#set ($dictMethod = "getStrDictOptions")
  301 +#elseif ($column.javaType == "Boolean")
  302 +#set ($dictMethod = "getBoolDictOptions")
  303 +#end
  304 +#if ($column.htmlType == "select" && $column.dictType && "" != $column.dictType)
  305 + <yd-form-picker
  306 + v-model="${subClassNameVar}.${column.javaField}"
  307 + label="${column.columnComment}"
  308 + label-width="180rpx"
  309 + :dict-type="DICT_TYPE.${column.dictType.toUpperCase()}"
  310 +#if ($column.javaType == "String")
  311 + dict-kind="str"
  312 +#end
  313 + placeholder="请选择${column.columnComment}"
  314 + />
  315 +#elseif ($column.htmlType == "radio" && $column.dictType && "" != $column.dictType)
  316 + <wd-form-item title="${column.columnComment}" title-width="180rpx" center>
  317 + <wd-radio-group v-model="${subClassNameVar}.${column.javaField}" type="button">
  318 + <wd-radio
  319 + v-for="dict in $dictMethod(DICT_TYPE.${column.dictType.toUpperCase()})"
  320 + :key="dict.value"
  321 + :value="dict.value"
  322 + >
  323 + {{ dict.label }}
  324 + </wd-radio>
  325 + </wd-radio-group>
  326 + </wd-form-item>
  327 +#elseif ($javaType == "boolean")
  328 + <wd-form-item title="${column.columnComment}" title-width="180rpx" center>
  329 + <wd-switch v-model="${subClassNameVar}.${column.javaField}" />
  330 + </wd-form-item>
  331 +#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
  332 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  333 + <wd-input-number
  334 + v-model="${subClassNameVar}.${column.javaField}"
  335 +#if($column.nullable)
  336 + allow-null
  337 +#end
  338 +#if ($javaType == "double" || $javaType == "bigdecimal")
  339 + :precision="2"
  340 + input-type="number"
  341 +#end
  342 + :min="0"
  343 + />
  344 + </wd-form-item>
  345 +#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
  346 +#set ($subPickerType = "date")
  347 +#if ($javaType == "localdatetime")
  348 +#set ($subPickerType = "datetime")
  349 +#end
  350 + <wd-form-item
  351 + title="${column.columnComment}"
  352 + title-width="180rpx"
  353 + is-link
  354 +#if ($javaType == "localdatetime")
  355 + :value="${subClassNameVar}.${column.javaField} ? formatDateTime(${subClassNameVar}.${column.javaField}) : ''"
  356 +#else
  357 + :value="${subClassNameVar}.${column.javaField} ? formatDate(${subClassNameVar}.${column.javaField}) : ''"
  358 +#end
  359 + placeholder="请选择${column.columnComment}"
  360 + @click="subDateVisible['${subClassNameVar}.${column.javaField}'] = true"
  361 + />
  362 + <wd-datetime-picker
  363 + v-model:visible="subDateVisible['${subClassNameVar}.${column.javaField}']"
  364 + :model-value="${subClassNameVar}.${column.javaField} || Date.now()"
  365 + type="${subPickerType}"
  366 +#if ($javaType == "localdatetime")
  367 + @update:model-value="value => ${subClassNameVar}.${column.javaField} = value"
  368 +#else
  369 + @update:model-value="value => ${subClassNameVar}.${column.javaField} = value"
  370 +#end
  371 + />
  372 +#elseif ($column.htmlType == "textarea")
  373 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  374 + <wd-textarea v-model="${subClassNameVar}.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
  375 + </wd-form-item>
  376 +#elseif ($column.htmlType == "imageUpload")
  377 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  378 + <yd-upload-img v-model="${subClassNameVar}.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
  379 + </wd-form-item>
  380 +#elseif ($column.htmlType == "fileUpload")
  381 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  382 + <yd-upload-file v-model="${subClassNameVar}.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
  383 + </wd-form-item>
  384 +#else
  385 + <wd-form-item title="${column.columnComment}" title-width="180rpx">
  386 + <wd-input v-model="${subClassNameVar}.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
  387 + </wd-form-item>
  388 +#end
  389 +#end
  390 +#end
  391 + </view>
  392 +#end
  393 + </view>
  394 +#end
  395 +#end
130 396 </view>
131 397  
132 398 <!-- 底部ä¿å­˜æŒ‰é’® -->
... ... @@ -154,54 +420,113 @@ import type { FormInstance } from &#39;@wot-ui/ui/components/wd-form/types&#39;
154 420 #set ($hasDict = 0)
155 421 #set ($hasDate = 0)
156 422 #set ($hasDateTime = 0)
  423 +#set ($hasSubDate = 0)
157 424 #set ($hasGetDictOptions = 0)
158 425 #set ($hasGetIntDictOptions = 0)
159 426 #set ($hasGetStrDictOptions = 0)
160 427 #set ($hasGetBoolDictOptions = 0)
161 428 #foreach ($column in $columns)
162   - #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
  429 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
163 430 && ($column.htmlType == "select" || $column.htmlType == "radio")
164 431 && $column.dictType && "" != $column.dictType)
165   - #set ($hasDict = 1)
166   - #if ($column.htmlType == "radio" && ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short"))
167   - #set ($hasGetIntDictOptions = 1)
168   - #elseif ($column.htmlType == "radio" && $column.javaType == "String")
169   - #set ($hasGetStrDictOptions = 1)
170   - #elseif ($column.htmlType == "radio" && $column.javaType == "Boolean")
171   - #set ($hasGetBoolDictOptions = 1)
172   - #elseif ($column.htmlType == "radio")
173   - #set ($hasGetDictOptions = 1)
174   - #end
175   - #end
176   - #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
  432 +#set ($hasDict = 1)
  433 +#if ($column.htmlType == "radio" && ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short"))
  434 +#set ($hasGetIntDictOptions = 1)
  435 +#elseif ($column.htmlType == "radio" && $column.javaType == "String")
  436 +#set ($hasGetStrDictOptions = 1)
  437 +#elseif ($column.htmlType == "radio" && $column.javaType == "Boolean")
  438 +#set ($hasGetBoolDictOptions = 1)
  439 +#elseif ($column.htmlType == "radio")
  440 +#set ($hasGetDictOptions = 1)
  441 +#end
  442 +#end
  443 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
177 444 && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate"))
178   - #set ($hasDate = 1)
179   - #end
180   - #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
  445 +#set ($hasDate = 1)
  446 +#end
  447 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
181 448 && ${column.javaType.toLowerCase()} == "localdatetime")
182   - #set ($hasDateTime = 1)
183   - #end
  449 +#set ($hasDateTime = 1)
184 450 #end
185   -import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
  451 +#end
  452 +#if ($table.templateType == 10 || $table.templateType == 12)
  453 +#foreach ($subColumns in $subColumnsList)
  454 +#foreach ($column in $subColumns)
  455 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
  456 + && ($column.htmlType == "select" || $column.htmlType == "radio")
  457 + && $column.dictType && "" != $column.dictType)
  458 +#set ($hasDict = 1)
  459 +#if ($column.htmlType == "radio" && ($column.javaType == "Integer" || $column.javaType == "Long" || $column.javaType == "Byte" || $column.javaType == "Short"))
  460 +#set ($hasGetIntDictOptions = 1)
  461 +#elseif ($column.htmlType == "radio" && $column.javaType == "String")
  462 +#set ($hasGetStrDictOptions = 1)
  463 +#elseif ($column.htmlType == "radio" && $column.javaType == "Boolean")
  464 +#set ($hasGetBoolDictOptions = 1)
  465 +#elseif ($column.htmlType == "radio")
  466 +#set ($hasGetDictOptions = 1)
  467 +#end
  468 +#end
  469 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
  470 + && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate"))
  471 +#set ($hasDate = 1)
  472 +#set ($hasSubDate = 1)
  473 +#end
  474 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey
  475 + && ${column.javaType.toLowerCase()} == "localdatetime")
  476 +#set ($hasDateTime = 1)
  477 +#set ($hasSubDate = 1)
  478 +#end
  479 +#end
  480 +#end
  481 +#end
  482 +import type {
  483 + ${simpleClassName},
  484 +#if ($table.templateType == 10 || $table.templateType == 12)
  485 +#foreach ($subSimpleClassName in $subSimpleClassNames)
  486 + ${subSimpleClassName},
  487 +#end
  488 +#end
  489 +} from '@/api/${table.moduleName}/${table.businessName}'
186 490 import { computed, onMounted, ref } from 'vue'
187 491 import { useToast } from '@wot-ui/ui/components/wd-toast'
  492 +#if ($table.templateType == 10 || $table.templateType == 12)
  493 +import {
  494 + create${simpleClassName},
  495 + get${simpleClassName},
  496 +#foreach ($subTable in $subTables)
  497 +#set ($index = $foreach.count - 1)
  498 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  499 +#set ($subJoinColumn = $subJoinColumns.get($index))
  500 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
  501 +#if ($subTable.subJoinMany)
  502 + get${subSimpleClassName}ListBy${SubJoinColumnName},
  503 +#else
  504 + get${subSimpleClassName}By${SubJoinColumnName},
  505 +#end
  506 +#end
  507 + update${simpleClassName},
  508 +} from '@/api/${table.moduleName}/${table.businessName}'
  509 +#elseif ($table.templateType == 2)
  510 +import { create${simpleClassName}, get${simpleClassName}, get${simpleClassName}List, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
  511 +#else
188 512 import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
  513 +#end
189 514 #if ($hasGetDictOptions == 1 || $hasGetIntDictOptions == 1 || $hasGetStrDictOptions == 1 || $hasGetBoolDictOptions == 1)
190   - #set ($dictImportNames = "")
191   - #if ($hasGetDictOptions == 1)
192   - #set ($dictImportNames = "${dictImportNames}getDictOptions, ")
193   - #end
194   - #if ($hasGetIntDictOptions == 1)
195   - #set ($dictImportNames = "${dictImportNames}getIntDictOptions, ")
196   - #end
197   - #if ($hasGetStrDictOptions == 1)
198   - #set ($dictImportNames = "${dictImportNames}getStrDictOptions, ")
199   - #end
200   - #if ($hasGetBoolDictOptions == 1)
201   - #set ($dictImportNames = "${dictImportNames}getBoolDictOptions, ")
202   - #end
203   - #set ($dictImportNames = $dictImportNames.trim())
204   - #set ($dictImportNames = $dictImportNames.substring(0, $dictImportNames.length() - 1))
  515 +#set ($dictImportNames = "")
  516 +#if ($hasGetDictOptions == 1)
  517 +#set ($dictImportNames = "${dictImportNames}getDictOptions, ")
  518 +#end
  519 +#if ($hasGetIntDictOptions == 1)
  520 +#set ($dictImportNames = "${dictImportNames}getIntDictOptions, ")
  521 +#end
  522 +#if ($hasGetStrDictOptions == 1)
  523 +#set ($dictImportNames = "${dictImportNames}getStrDictOptions, ")
  524 +#end
  525 +#if ($hasGetBoolDictOptions == 1)
  526 +#set ($dictImportNames = "${dictImportNames}getBoolDictOptions, ")
  527 +#end
  528 +#set ($dictImportNames = $dictImportNames.trim())
  529 +#set ($dictImportNames = $dictImportNames.substring(0, $dictImportNames.length() - 1))
205 530 import { $dictImportNames } from '@/hooks/useDict'
206 531 #end
207 532 import { delay, navigateBackPlus } from '@/utils'
... ... @@ -209,21 +534,27 @@ import { delay, navigateBackPlus } from &#39;@/utils&#39;
209 534 import { DICT_TYPE } from '@/utils/constants'
210 535 #end
211 536 #if ($hasDate == 1 || $hasDateTime == 1)
212   - #set ($dateImportNames = "")
213   - #if ($hasDate == 1)
214   - #set ($dateImportNames = "${dateImportNames}formatDate, ")
215   - #end
216   - #if ($hasDateTime == 1)
217   - #set ($dateImportNames = "${dateImportNames}formatDateTime, ")
218   - #end
219   - #set ($dateImportNames = $dateImportNames.trim())
220   - #set ($dateImportNames = $dateImportNames.substring(0, $dateImportNames.length() - 1))
  537 +#set ($dateImportNames = "")
  538 +#if ($hasDate == 1)
  539 +#set ($dateImportNames = "${dateImportNames}formatDate, ")
  540 +#end
  541 +#if ($hasDateTime == 1)
  542 +#set ($dateImportNames = "${dateImportNames}formatDateTime, ")
  543 +#end
  544 +#set ($dateImportNames = $dateImportNames.trim())
  545 +#set ($dateImportNames = $dateImportNames.substring(0, $dateImportNames.length() - 1))
221 546 import { $dateImportNames } from '@/utils/date'
222 547 #end
223 548 import { createFormSchema } from '@/utils/wot'
  549 +#if ($table.templateType == 2)
  550 +import { handleTree } from '@/utils/tree'
  551 +#end
224 552  
225 553 const props = defineProps<{
226   - id?: ${primaryTsType} | any
  554 + id?: ${primaryTsType}
  555 +#if ($table.templateType == 2)
  556 + ${treeParentColumn.javaField}?: ${primaryTsType}
  557 +#end
227 558 }>()
228 559  
229 560 definePage({
... ... @@ -238,40 +569,153 @@ const getTitle = computed(() =&gt; props.id ? &#39;编辑${table.classComment}&#39; : &#39;æ–°å
238 569 const formLoading = ref(false) // è¡¨å•æäº¤çŠ¶æ€
239 570 const formData = ref<${simpleClassName}>({
240 571 #foreach($column in $columns)
241   - #if (($column.createOperation || $column.updateOperation) || $column.primaryKey)
242   - #set ($javaType = $column.javaType.toLowerCase())
243   - #set ($javaFieldLower = $column.javaField.toLowerCase())
244   - #set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")
245   - #if ($column.primaryKey)
  572 +#if (($column.createOperation || $column.updateOperation) || $column.primaryKey)
  573 +#set ($javaType = $column.javaType.toLowerCase())
  574 +#set ($javaFieldLower = $column.javaField.toLowerCase())
  575 +#set ($optional = $column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime")
  576 +#if ($column.primaryKey)
246 577 ${column.javaField}: undefined,
247   - #elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
  578 +#elseif($table.templateType == 2 && $column.id == $treeParentColumn.id)
  579 + ${column.javaField}: props.${column.javaField} ?? 0,
  580 +#elseif(${javaType} == "long" || ${javaType} == "integer" || ${javaType} == "short" || ${javaType} == "double" || ${javaType} == "bigdecimal" || ${javaType} == "byte")
  581 +#if($column.nullable)
  582 + ${column.javaField}: undefined,
  583 +#else
248 584 ${column.javaField}: 0,
249   - #elseif(${javaType} == "boolean")
  585 +#end
  586 +#elseif(${javaType} == "boolean")
250 587 ${column.javaField}: false,
251   - #elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
252   - ${column.javaField}: undefined,
253   - #else
  588 +#elseif(${javaType} == "date" || ${javaType} == "localdate" || ${javaType} == "localdatetime")
  589 + ${column.javaField}:#if($column.nullable || $column.primaryKey || $javaFieldLower == "createtime" || $javaFieldLower == "updatetime") undefined#else 0#end,
  590 +#else
254 591 ${column.javaField}: '',
255   - #end
256   - #end
  592 +#end
  593 +#end
257 594 #end
258 595 }) // è¡¨å•æ•°æ®
  596 +#if ($table.templateType == 10 || $table.templateType == 12)
  597 +#foreach ($subTable in $subTables)
  598 +#set ($index = $foreach.count - 1)
  599 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  600 +#set ($subClassNameVar = $subClassNameVars.get($index))
  601 +#set ($subColumns = $subColumnsList.get($index))
  602 +#set ($subJoinColumn = $subJoinColumns.get($index))
  603 +#if ($subTable.subJoinMany)
  604 +const ${subClassNameVar}s = ref<${subSimpleClassName}[]>([]) // ${subTable.classComment}æ•°æ®
  605 +#else
  606 +const ${subClassNameVar} = ref<${subSimpleClassName} | null>(null) // ${subTable.classComment}æ•°æ®
  607 +#end
  608 +#end
  609 +#end
259 610 #foreach($column in $columns)
260   - #if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime"))
  611 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime"))
261 612 const ${column.javaField}Visible = ref(false) // ${column.columnComment}选择器状æ€
262   - #end
  613 +#end
  614 +#end
  615 +#if ($hasSubDate == 1)
  616 +const subDateVisible = ref<Record<string, boolean>>({}) // å­è¡¨æ—¥æœŸé€‰æ‹©å™¨çжæ€
263 617 #end
264 618 const formSchema = createFormSchema({
265 619 #foreach($column in $columns)
266   - #set ($javaFieldLower = $column.javaField.toLowerCase())
267   - #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
  620 +#set ($javaFieldLower = $column.javaField.toLowerCase())
  621 +#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
268 622 && $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
269 623 ${column.javaField}: [{ required: true, message: '${column.columnComment}ä¸èƒ½ä¸ºç©º' }],
270   - #end
  624 +#end
271 625 #end
272 626 }) // è¡¨å•æ ¡éªŒè§„则
273 627 const formRef = ref<FormInstance>() // 表å•组件引用
  628 +#if ($table.templateType == 2)
  629 +const treeData = ref<${simpleClassName}[]>([]) // 父级树形选项
  630 +#end
  631 +
  632 +#if ($table.templateType == 10 || $table.templateType == 12)
  633 +#foreach ($subTable in $subTables)
  634 +#set ($index = $foreach.count - 1)
  635 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  636 +#set ($subClassNameVar = $subClassNameVars.get($index))
  637 +#set ($subColumns = $subColumnsList.get($index))
  638 +#set ($subJoinColumn = $subJoinColumns.get($index))
  639 +#if ($subTable.subJoinMany)
  640 +#set ($subClassNameVar = $subClassNameVars.get($index))
  641 +#set ($subColumns = $subColumnsList.get($index))
  642 +#set ($subJoinColumn = $subJoinColumns.get($index))
  643 +/** 添加${subTable.classComment} */
  644 +function add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}() {
  645 + ${subClassNameVar}s.value.push({
  646 +#foreach ($column in $subColumns)
  647 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
  648 +#set ($javaType = $column.javaType.toLowerCase())
  649 + ${column.javaField}:#if($column.nullable) undefined#elseif($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte" || $javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime") 0#elseif($javaType == "boolean") false#else ''#end,
  650 +#end
  651 +#end
  652 + })
  653 +}
274 654  
  655 +/** 删除${subTable.classComment} */
  656 +function remove${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}(index: number) {
  657 + ${subClassNameVar}s.value.splice(index, 1)
  658 +}
  659 +
  660 +#else
  661 +/** 添加${subTable.classComment} */
  662 +function add${subClassNameVar.substring(0,1).toUpperCase()}${subClassNameVar.substring(1)}() {
  663 + ${subClassNameVar}.value = {
  664 +#foreach ($column in $subColumns)
  665 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
  666 +#set ($javaType = $column.javaType.toLowerCase())
  667 + ${column.javaField}:#if($column.nullable) undefined#elseif($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte" || $javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime") 0#elseif($javaType == "boolean") false#else ''#end,
  668 +#end
  669 +#end
  670 + }
  671 +}
  672 +
  673 +#end
  674 +#end
  675 +/** 校验主å­è¡¨æ•°æ® */
  676 +function isEmptySubValue(value: unknown) {
  677 + return value === undefined || value === null || value === ''
  678 +}
  679 +
  680 +function validateSubTables() {
  681 +#foreach ($subTable in $subTables)
  682 +#set ($index = $foreach.count - 1)
  683 +#set ($subClassNameVar = $subClassNameVars.get($index))
  684 +#set ($subColumns = $subColumnsList.get($index))
  685 +#set ($subJoinColumn = $subJoinColumns.get($index))
  686 +#if ($subTable.subJoinMany)
  687 + for (let index = 0; index < ${subClassNameVar}s.value.length; index++) {
  688 + const item = ${subClassNameVar}s.value[index]!
  689 +#foreach ($column in $subColumns)
  690 +#set ($javaFieldLower = $column.javaField.toLowerCase())
  691 +#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
  692 + && $column.id != $subJoinColumn.id && $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
  693 + if (isEmptySubValue(item.${column.javaField})) {
  694 + toast.warning(`${subTable.classComment}第#[[${]]#index + 1#[[}]]#项的${column.columnComment}ä¸èƒ½ä¸ºç©º`)
  695 + return false
  696 + }
  697 +#end
  698 +#end
  699 + }
  700 +#else
  701 + if (${subClassNameVar}.value) {
  702 +#foreach ($column in $subColumns)
  703 +#set ($javaFieldLower = $column.javaField.toLowerCase())
  704 +#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey
  705 + && $column.id != $subJoinColumn.id && $javaFieldLower != "createtime" && $javaFieldLower != "updatetime")
  706 + if (isEmptySubValue(${subClassNameVar}.value.${column.javaField})) {
  707 + toast.warning('${subTable.classComment}çš„${column.columnComment}ä¸èƒ½ä¸ºç©º')
  708 + return false
  709 + }
  710 +#end
  711 +#end
  712 + }
  713 +#end
  714 +#end
  715 + return true
  716 +}
  717 +
  718 +#end
275 719 /** 返回上一页 */
276 720 function handleBack() {
277 721 navigateBackPlus('/pages-${table.moduleName}/${table.businessName}/index')
... ... @@ -282,7 +726,34 @@ async function getDetail() {
282 726 if (!props.id) {
283 727 return
284 728 }
  729 +#if ($table.templateType == 10 || $table.templateType == 12)
  730 + const [data#foreach ($subTable in $subTables)#set ($index = $foreach.count - 1), ${subClassNameVars.get($index)}Data#end] = await Promise.all([
  731 + get${simpleClassName}(props.id),
  732 +#foreach ($subTable in $subTables)
  733 +#set ($index = $foreach.count - 1)
  734 +#set ($subSimpleClassName = $subSimpleClassNames.get($index))
  735 +#set ($subJoinColumn = $subJoinColumns.get($index))
  736 +#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + $subJoinColumn.javaField.substring(1))
  737 +#if ($subTable.subJoinMany)
  738 + get${subSimpleClassName}ListBy${SubJoinColumnName}(props.id),
  739 +#else
  740 + get${subSimpleClassName}By${SubJoinColumnName}(props.id),
  741 +#end
  742 +#end
  743 + ])
  744 + formData.value = data
  745 +#foreach ($subTable in $subTables)
  746 +#set ($index = $foreach.count - 1)
  747 +#set ($subClassNameVar = $subClassNameVars.get($index))
  748 +#if ($subTable.subJoinMany)
  749 + ${subClassNameVar}s.value = ${subClassNameVar}Data || []
  750 +#else
  751 + ${subClassNameVar}.value = ${subClassNameVar}Data || null
  752 +#end
  753 +#end
  754 +#else
285 755 formData.value = await get${simpleClassName}(props.id)
  756 +#end
286 757 }
287 758  
288 759 /** æäº¤è¡¨å• */
... ... @@ -291,14 +762,31 @@ async function handleSubmit() {
291 762 if (!valid) {
292 763 return
293 764 }
  765 +#if ($table.templateType == 10 || $table.templateType == 12)
  766 + if (!validateSubTables()) {
  767 + return
  768 + }
  769 +#end
294 770  
295 771 formLoading.value = true
296 772 try {
  773 +#if ($table.templateType == 10 || $table.templateType == 12)
  774 + const data: ${simpleClassName} = {
  775 + ...formData.value,
  776 +#foreach ($subTable in $subTables)
  777 +#set ($index = $foreach.count - 1)
  778 +#set ($subClassNameVar = $subClassNameVars.get($index))
  779 + ${subClassNameVar}#if($subTable.subJoinMany)s#end: ${subClassNameVar}#if($subTable.subJoinMany)s#end.value,
  780 +#end
  781 + }
  782 +#else
  783 + const data = formData.value
  784 +#end
297 785 if (props.id) {
298   - await update${simpleClassName}(formData.value)
  786 + await update${simpleClassName}(data)
299 787 toast.success('修改æˆåŠŸ')
300 788 } else {
301   - await create${simpleClassName}(formData.value)
  789 + await create${simpleClassName}(data)
302 790 toast.success('新增æˆåŠŸ')
303 791 }
304 792 uni.$emit('${table.moduleName}:${table.businessName}:reload')
... ... @@ -309,7 +797,16 @@ async function handleSubmit() {
309 797 }
310 798  
311 799 /** åˆå§‹åŒ– */
312   -onMounted(() => {
  800 +onMounted(async () => {
  801 +#if ($table.templateType == 2)
  802 + const data = await get${simpleClassName}List()
  803 + treeData.value = [{
  804 + ${primaryColumn.javaField}: 0,
  805 + ${treeNameColumn.javaField}: '顶级${table.classComment}',
  806 + ${treeParentColumn.javaField}: 0,
  807 + children: handleTree(data, '${primaryColumn.javaField}', '${treeParentColumn.javaField}'),
  808 + } as ${simpleClassName}]
  809 +#end
313 810 getDetail()
314 811 })
315 812 </script>
... ...
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/form_sub_erp.vue.vm 0 → 100644
  1 +#set ($subTable = $subTables.get($subIndex))
  2 +#set ($subColumns = $subColumnsList.get($subIndex))
  3 +#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
  4 +#set ($subPrimaryColumn = $subPrimaryColumns.get($subIndex))
  5 +#set ($subJoinColumn = $subJoinColumns.get($subIndex))
  6 +#set ($subPrimaryJavaType = $subPrimaryColumn.javaType.toLowerCase())
  7 +#if(${subPrimaryJavaType} == "long" || ${subPrimaryJavaType} == "integer" || ${subPrimaryJavaType} == "short" || ${subPrimaryJavaType} == "double" || ${subPrimaryJavaType} == "bigdecimal" || ${subPrimaryJavaType} == "byte")
  8 +#set ($subPrimaryTsType = "number")
  9 +#else
  10 +#set ($subPrimaryTsType = "string")
  11 +#end
  12 +#set ($subJoinJavaType = $subJoinColumn.javaType.toLowerCase())
  13 +#if(${subJoinJavaType} == "long" || ${subJoinJavaType} == "integer" || ${subJoinJavaType} == "short" || ${subJoinJavaType} == "double" || ${subJoinJavaType} == "bigdecimal" || ${subJoinJavaType} == "byte")
  14 +#set ($subJoinTsType = "number")
  15 +#else
  16 +#set ($subJoinTsType = "string")
  17 +#end
  18 +#set ($hasDict = 0)
  19 +#set ($hasDate = 0)
  20 +#set ($hasDateTime = 0)
  21 +#foreach ($column in $subColumns)
  22 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
  23 +#if (($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType)
  24 +#set ($hasDict = 1)
  25 +#end
  26 +#if (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate")
  27 +#set ($hasDate = 1)
  28 +#end
  29 +#if (${column.javaType.toLowerCase()} == "localdatetime")
  30 +#set ($hasDateTime = 1)
  31 +#end
  32 +#end
  33 +#end
  34 +<template>
  35 + <view class="yd-page-container">
  36 + <!-- 顶部导航栏 -->
  37 + <wd-navbar :title="getTitle" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
  38 +
  39 + <!-- 表单区域 -->
  40 + <wd-form ref="formRef" :model="formData" :schema="formSchema">
  41 + <wd-cell-group border>
  42 +#foreach ($column in $subColumns)
  43 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id)
  44 +#set ($javaType = $column.javaType.toLowerCase())
  45 +#if (($column.htmlType == "select" || $column.htmlType == "radio") && $column.dictType && "" != $column.dictType)
  46 + <yd-form-picker
  47 + v-model="formData.${column.javaField}"
  48 + label="${column.columnComment}"
  49 + label-width="180rpx"
  50 + prop="${column.javaField}"
  51 + :dict-type="DICT_TYPE.${column.dictType.toUpperCase()}"
  52 +#if ($column.javaType == "String")
  53 + dict-kind="str"
  54 +#end
  55 + placeholder="请选择${column.columnComment}"
  56 + />
  57 +#elseif ($javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime")
  58 +#set ($pickerType = "date")
  59 +#if ($javaType == "localdatetime")
  60 +#set ($pickerType = "datetime")
  61 +#end
  62 + <wd-form-item
  63 + title="${column.columnComment}"
  64 + title-width="180rpx"
  65 + prop="${column.javaField}"
  66 + is-link
  67 +#if ($javaType == "localdatetime")
  68 + :value="formData.${column.javaField} ? formatDateTime(formData.${column.javaField}) : ''"
  69 +#else
  70 + :value="formData.${column.javaField} ? formatDate(formData.${column.javaField}) : ''"
  71 +#end
  72 + placeholder="请选择${column.columnComment}"
  73 + @click="${column.javaField}Visible = true"
  74 + />
  75 + <wd-datetime-picker
  76 + v-model:visible="${column.javaField}Visible"
  77 + :model-value="formData.${column.javaField} || Date.now()"
  78 + type="${pickerType}"
  79 +#if ($javaType == "localdatetime")
  80 + @update:model-value="value => formData.${column.javaField} = value"
  81 +#else
  82 + @update:model-value="value => formData.${column.javaField} = value"
  83 +#end
  84 + />
  85 +#else
  86 + <wd-form-item title="${column.columnComment}" title-width="180rpx" prop="${column.javaField}">
  87 +#if ($javaType == "boolean")
  88 + <wd-switch v-model="formData.${column.javaField}" />
  89 +#elseif ($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte")
  90 + <wd-input-number
  91 + v-model="formData.${column.javaField}"
  92 +#if($column.nullable)
  93 + allow-null
  94 +#end
  95 +#if ($javaType == "double" || $javaType == "bigdecimal")
  96 + :precision="2"
  97 + input-type="number"
  98 +#end
  99 + :min="0"
  100 + />
  101 +#elseif ($column.htmlType == "textarea")
  102 + <wd-textarea v-model="formData.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
  103 +#elseif ($column.htmlType == "imageUpload")
  104 + <yd-upload-img v-model="formData.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
  105 +#elseif ($column.htmlType == "fileUpload")
  106 + <yd-upload-file v-model="formData.${column.javaField}" directory="${table.moduleName}/${table.businessName}" />
  107 +#else
  108 + <wd-input v-model="formData.${column.javaField}" clearable placeholder="请输入${column.columnComment}" />
  109 +#end
  110 + </wd-form-item>
  111 +#end
  112 +#end
  113 +#end
  114 + </wd-cell-group>
  115 + </wd-form>
  116 +
  117 + <!-- 底部保存按钮 -->
  118 + <view class="yd-detail-footer">
  119 + <wd-button type="primary" block :loading="formLoading" @click="handleSubmit">
  120 + 保存
  121 + </wd-button>
  122 + </view>
  123 + </view>
  124 +</template>
  125 +
  126 +<script lang="ts" setup>
  127 +import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
  128 +import type { ${subSimpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
  129 +import { computed, onMounted, ref } from 'vue'
  130 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  131 +import { create${subSimpleClassName}, get${subSimpleClassName}, update${subSimpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
  132 +import { delay, navigateBackPlus } from '@/utils'
  133 +#if ($hasDict == 1)
  134 +import { DICT_TYPE } from '@/utils/constants'
  135 +#end
  136 +#if ($hasDate == 1 || $hasDateTime == 1)
  137 +#set ($dateImportNames = "")
  138 +#if ($hasDate == 1)
  139 +#set ($dateImportNames = "${dateImportNames}formatDate, ")
  140 +#end
  141 +#if ($hasDateTime == 1)
  142 +#set ($dateImportNames = "${dateImportNames}formatDateTime, ")
  143 +#end
  144 +#set ($dateImportNames = $dateImportNames.trim())
  145 +#set ($dateImportNames = $dateImportNames.substring(0, $dateImportNames.length() - 1))
  146 +import { $dateImportNames } from '@/utils/date'
  147 +#end
  148 +import { createFormSchema } from '@/utils/wot'
  149 +
  150 +const props = defineProps<{
  151 + id?: ${subPrimaryTsType}
  152 + ${subJoinColumn.javaField}: ${subJoinTsType}
  153 +}>()
  154 +
  155 +definePage({
  156 + style: { navigationBarTitleText: '', navigationStyle: 'custom' },
  157 +})
  158 +
  159 +const toast = useToast()
  160 +const getTitle = computed(() => props.id ? '编辑${subTable.classComment}' : '新增${subTable.classComment}')
  161 +const formLoading = ref(false) // 表单提交状态
  162 +const formData = ref<${subSimpleClassName}>({
  163 +#foreach ($column in $subColumns)
  164 +#if ($column.primaryKey)
  165 + ${column.javaField}: undefined,
  166 +#elseif ($column.id == $subJoinColumn.id)
  167 + ${column.javaField}: props.${column.javaField},
  168 +#elseif ($column.createOperation || $column.updateOperation)
  169 +#set ($javaType = $column.javaType.toLowerCase())
  170 + ${column.javaField}:#if($column.nullable) undefined#elseif($javaType == "long" || $javaType == "integer" || $javaType == "short" || $javaType == "double" || $javaType == "bigdecimal" || $javaType == "byte" || $javaType == "date" || $javaType == "localdate" || $javaType == "localdatetime") 0#elseif($javaType == "boolean") false#else ''#end,
  171 +#end
  172 +#end
  173 +}) // 表单数据
  174 +#foreach ($column in $subColumns)
  175 +#if (($column.createOperation || $column.updateOperation) && !$column.primaryKey && $column.id != $subJoinColumn.id
  176 + && (${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdate" || ${column.javaType.toLowerCase()} == "localdatetime"))
  177 +const ${column.javaField}Visible = ref(false) // ${column.columnComment}选择器状态
  178 +#end
  179 +#end
  180 +const formSchema = createFormSchema({
  181 +#foreach ($column in $subColumns)
  182 +#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !$column.primaryKey)
  183 + ${column.javaField}: [{ required: true, message: '${column.columnComment}不能为空' }],
  184 +#end
  185 +#end
  186 +}) // 表单校验规则
  187 +const formRef = ref<FormInstance>() // 表单组件引用
  188 +
  189 +/** 返回上一页 */
  190 +function handleBack() {
  191 + navigateBackPlus()
  192 +}
  193 +
  194 +/** 加载${subTable.classComment}详情 */
  195 +async function getDetail() {
  196 + if (props.id) {
  197 + formData.value = await get${subSimpleClassName}(props.id)
  198 + }
  199 +}
  200 +
  201 +/** 提交表单 */
  202 +async function handleSubmit() {
  203 + const { valid } = await formRef.value.validate()
  204 + if (!valid) {
  205 + return
  206 + }
  207 + formLoading.value = true
  208 + try {
  209 + const data = { ...formData.value, ${subJoinColumn.javaField}: props.${subJoinColumn.javaField} ?? formData.value.${subJoinColumn.javaField} }
  210 + if (props.id) {
  211 + await update${subSimpleClassName}(data)
  212 + toast.success('修改成功')
  213 + } else {
  214 + await create${subSimpleClassName}(data)
  215 + toast.success('新增成功')
  216 + }
  217 + delay(handleBack)
  218 + } finally {
  219 + formLoading.value = false
  220 + }
  221 +}
  222 +
  223 +/** 初始化 */
  224 +onMounted(() => {
  225 + getDetail()
  226 +})
  227 +</script>
... ...
yudao-module-infra/src/main/resources/codegen/vue3_admin_uniapp/views/index_tree.vue.vm 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + title="${table.classComment}管理"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <!-- 搜索组件 -->
  11 + <SearchForm @search="handleQuery" @reset="handleReset" />
  12 +
  13 + <!-- 面包屑导航 -->
  14 + <Breadcrumb ref="breadcrumbRef" v-model="currentParentId" />
  15 +
  16 + <!-- ${table.classComment}列表 -->
  17 + <view class="p-24rpx">
  18 + <view
  19 + v-for="item in currentList"
  20 + :key="item.${primaryColumn.javaField}"
  21 + class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
  22 + >
  23 + <view class="p-24rpx" @click="handleDetail(item)">
  24 + <view class="flex items-center justify-between">
  25 + <view class="flex min-w-0 items-center">
  26 + <view class="mr-16rpx h-48rpx w-48rpx shrink-0 flex items-center justify-center rounded-8rpx bg-[#1890ff]">
  27 + <wd-icon name="folder" size="20px" color="#fff" />
  28 + </view>
  29 + <view class="truncate text-32rpx text-[#333] font-semibold">
  30 + {{ item.${treeNameColumn.javaField} }}
  31 + </view>
  32 + </view>
  33 + <view
  34 + v-if="item.children?.length"
  35 + class="ml-16rpx shrink-0 flex items-center"
  36 + @click.stop="handleEnterChildren(item)"
  37 + >
  38 + <text class="text-24rpx text-[#1890ff]">子级 ({{ item.children.length }})</text>
  39 + <wd-icon name="arrow-right" size="12px" color="#1890ff" />
  40 + </view>
  41 + </view>
  42 + </view>
  43 + </view>
  44 +
  45 + <!-- 空状态 -->
  46 + <view v-if="!loading && currentList.length === 0" class="py-100rpx text-center">
  47 + <wd-empty icon="content" tip="暂无${table.classComment}数据" />
  48 + </view>
  49 + </view>
  50 +
  51 + <!-- 新增按钮 -->
  52 + <wd-fab
  53 + v-if="hasAccessByCodes(['${permissionPrefix}:create'])"
  54 + position="right-bottom"
  55 + type="primary"
  56 + :expandable="false"
  57 + @click="handleAdd"
  58 + />
  59 + </view>
  60 +</template>
  61 +
  62 +<script lang="ts" setup>
  63 +#set ($primaryJavaType = $primaryColumn.javaType.toLowerCase())
  64 +#if(${primaryJavaType} == "long" || ${primaryJavaType} == "integer" || ${primaryJavaType} == "short" || ${primaryJavaType} == "double" || ${primaryJavaType} == "bigdecimal" || ${primaryJavaType} == "byte")
  65 +#set ($primaryTsType = "number")
  66 +#else
  67 +#set ($primaryTsType = "string")
  68 +#end
  69 +import type { ${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
  70 +import { onUnload } from '@dcloudio/uni-app'
  71 +import { computed, onMounted, ref } from 'vue'
  72 +import { get${simpleClassName}List } from '@/api/${table.moduleName}/${table.businessName}'
  73 +import { useAccess } from '@/hooks/useAccess'
  74 +import { navigateBackPlus } from '@/utils'
  75 +import { findChildren, handleTree } from '@/utils/tree'
  76 +import Breadcrumb from './components/breadcrumb.vue'
  77 +import SearchForm from './components/search-form.vue'
  78 +
  79 +definePage({
  80 + style: {
  81 + navigationBarTitleText: '',
  82 + navigationStyle: 'custom',
  83 + },
  84 +})
  85 +
  86 +const { hasAccessByCodes } = useAccess()
  87 +const loading = ref(false) // 列表加载状态
  88 +const list = ref<${simpleClassName}[]>([]) // 完整树形列表
  89 +const currentParentId = ref<${primaryTsType}>(0) // 当前层级的父节点编号
  90 +const currentList = computed(() => currentParentId.value === 0
  91 + ? list.value
  92 + : findChildren(list.value, currentParentId.value, '${primaryColumn.javaField}')) // 当前层级列表
  93 +const breadcrumbRef = ref<InstanceType<typeof Breadcrumb>>() // 面包屑组件引用
  94 +const queryParams = ref<Record<string, any>>({}) // 查询参数
  95 +
  96 +/** 返回上一页或上一层级 */
  97 +function handleBack() {
  98 + if (!breadcrumbRef.value?.back()) {
  99 + navigateBackPlus()
  100 + }
  101 +}
  102 +
  103 +/** 进入子层级 */
  104 +function handleEnterChildren(item: ${simpleClassName}) {
  105 + breadcrumbRef.value?.enter({ id: item.${primaryColumn.javaField}!, name: item.${treeNameColumn.javaField} })
  106 +}
  107 +
  108 +/** 查询${table.classComment}列表 */
  109 +async function getList() {
  110 + loading.value = true
  111 + try {
  112 + list.value = handleTree(await get${simpleClassName}List(queryParams.value), '${primaryColumn.javaField}', '${treeParentColumn.javaField}')
  113 + } finally {
  114 + loading.value = false
  115 + }
  116 +}
  117 +
  118 +/** 搜索按钮操作 */
  119 +function handleQuery(data?: Record<string, any>) {
  120 + queryParams.value = { ...data }
  121 + currentParentId.value = 0
  122 + breadcrumbRef.value?.reset()
  123 + getList()
  124 +}
  125 +
  126 +/** 重置按钮操作 */
  127 +function handleReset() {
  128 + handleQuery()
  129 +}
  130 +
  131 +/** 新增${table.classComment} */
  132 +function handleAdd() {
  133 + uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/form/index?${treeParentColumn.javaField}=#[[${]]#currentParentId.value#[[}]]#` })
  134 +}
  135 +
  136 +/** 查看详情 */
  137 +function handleDetail(item: ${simpleClassName}) {
  138 + uni.navigateTo({ url: `/pages-${table.moduleName}/${table.businessName}/detail/index?id=#[[${]]#item.${primaryColumn.javaField}#[[}]]#` })
  139 +}
  140 +
  141 +/** 初始化 */
  142 +onMounted(() => {
  143 + getList()
  144 + uni.$on('${table.moduleName}:${table.businessName}:reload', getList)
  145 +})
  146 +
  147 +/** 卸载 */
  148 +onUnload(() => {
  149 + uni.$off('${table.moduleName}:${table.businessName}:reload', getList)
  150 +})
  151 +</script>
... ...
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineAbstractTest.java
... ... @@ -157,13 +157,17 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
157 157 String lastFilePath = StrUtil.subAfter(filePath, '/', true);
158 158 String ext = StrUtil.subAfter(lastFilePath, '.', true);
159 159 String name = StrUtil.subBefore(lastFilePath, '.', true);
160   - String contentPath = ext + '/' + name;
161   - // 同名文件(如 index.vue 同时出现在 列表/form/detail)会撞名,撞名时前缀补上级目录区分;仍撞则加序号兜底,避免快照互相覆盖
  160 + String parentPath = StrUtil.subBefore(filePath, '/' + lastFilePath, true);
  161 + String parentDir = StrUtil.subAfter(parentPath, '/', true);
  162 + // index.vue 按页面语义稳定归档,避免结果迭代顺序导致列表页和表单页的快照路径互换
  163 + String contentPath = "index".equals(name) && ("form".equals(parentDir) || "detail".equals(parentDir))
  164 + ? ext + '/' + parentDir + '/' + name : ext + '/' + name;
  165 + // ERP 多个子表的 form/index.vue 仍可能撞名,继续补充祖父目录区分
162 166 if (usedContentPaths.contains(contentPath)) {
163   - String parentDir = StrUtil.subAfter(StrUtil.subBefore(filePath, '/' + lastFilePath, true), '/', true);
164   - contentPath = ext + '/' + parentDir + '/' + name;
  167 + String grandParentDir = StrUtil.subAfter(StrUtil.subBefore(parentPath, '/' + parentDir, true), '/', true);
  168 + contentPath = ext + '/' + grandParentDir + '/' + parentDir + '/' + name;
165 169 for (int i = 2; usedContentPaths.contains(contentPath); i++) {
166   - contentPath = ext + '/' + parentDir + '/' + name + '-' + i;
  170 + contentPath = ext + '/' + grandParentDir + '/' + parentDir + '/' + name + '-' + i;
167 171 }
168 172 }
169 173 usedContentPaths.add(contentPath);
... ...
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineUniappTest.java
... ... @@ -7,10 +7,13 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
7 7 import com.baomidou.mybatisplus.annotation.DbType;
8 8 import org.junit.jupiter.api.Test;
9 9  
  10 +import java.util.Arrays;
10 11 import java.util.List;
11 12 import java.util.Map;
  13 +import java.util.Objects;
12 14  
13 15 import static org.junit.jupiter.api.Assertions.assertFalse;
  16 +import static org.junit.jupiter.api.Assertions.assertThrows;
14 17 import static org.junit.jupiter.api.Assertions.assertTrue;
15 18  
16 19 /**
... ... @@ -59,4 +62,182 @@ public class CodegenEngineUniappTest extends CodegenEngineAbstractTest {
59 62 assertTrue(searchForm.contains("sex: undefined as string | undefined,"));
60 63 }
61 64  
  65 + @Test
  66 + public void testExecute_tree() {
  67 + // 准备参数
  68 + CodegenTableDO table = getTable("category")
  69 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  70 + .setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
  71 + List<CodegenColumnDO> columns = getColumnList("category");
  72 +
  73 + // 调用
  74 + Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
  75 + // 断言
  76 + assertResult(result, "/vue3_admin_uniapp_tree");
  77 + }
  78 +
  79 + @Test
  80 + public void testExecute_semantics() {
  81 + // 准备参数
  82 + CodegenTableDO table = getTable("student")
  83 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  84 + .setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
  85 + List<CodegenColumnDO> columns = getColumnList("student");
  86 +
  87 + // 调用
  88 + Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
  89 + // 断言
  90 + String api = result.get("yudao-ui-admin-uniapp/src/api/infra/demo/index.ts");
  91 + assertTrue(api.contains("birthday: number"));
  92 + assertTrue(api.contains("createTime?: number"));
  93 + assertFalse(api.contains("createTime?: Date"));
  94 + String detail = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue");
  95 + assertTrue(detail.contains("type=\"danger\""));
  96 + assertFalse(detail.contains("type=\"error\""));
  97 + }
  98 +
  99 + @Test
  100 + public void testExecute_stringPrimaryKey() {
  101 + // 准备主表
  102 + CodegenTableDO table = getTable("student")
  103 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  104 + .setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType());
  105 + List<CodegenColumnDO> columns = getColumnList("student");
  106 + columns.stream().filter(CodegenColumnDO::getPrimaryKey).findFirst().orElseThrow().setJavaType("String");
  107 + // 准备子表
  108 + CodegenTableDO contactTable = getTable("contact")
  109 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  110 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  111 + .setSubJoinColumnId(100L).setSubJoinMany(true);
  112 + List<CodegenColumnDO> contactColumns = getColumnList("contact");
  113 + contactColumns.stream().filter(CodegenColumnDO::getPrimaryKey).findFirst().orElseThrow().setJavaType("String");
  114 + contactColumns.stream().filter(column -> Objects.equals(column.getId(), 100L))
  115 + .findFirst().orElseThrow().setJavaType("String");
  116 +
  117 + // 调用
  118 + Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
  119 + List.of(contactTable), List.of(contactColumns));
  120 + // 断言
  121 + String api = result.get("yudao-ui-admin-uniapp/src/api/infra/demo/index.ts");
  122 + assertTrue(api.contains("return http.post<string>('/infra/student/create', data)"));
  123 + assertTrue(api.contains("studentId: string"));
  124 + assertTrue(api.contains("getStudentContact(id: string)"));
  125 + assertTrue(api.contains("deleteStudentContact(id: string)"));
  126 + String detail = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue");
  127 + assertFalse(detail.contains("Number(props.id)"));
  128 + String subForm = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/student-contact/form/index.vue");
  129 + assertTrue(subForm.contains("id?: string"));
  130 + assertTrue(subForm.contains("studentId: string"));
  131 + }
  132 +
  133 + @Test
  134 + public void testExecute_treeSearch() {
  135 + // 准备参数
  136 + CodegenTableDO table = getTable("category")
  137 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  138 + .setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
  139 + List<CodegenColumnDO> columns = getColumnList("category");
  140 +
  141 + // 调用
  142 + Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
  143 + // 断言
  144 + String index = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue");
  145 + assertTrue(index.contains("currentParentId.value === 0\n ? list.value"));
  146 + assertFalse(index.contains("list.value.filter"));
  147 + String breadcrumb = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/components/breadcrumb.vue");
  148 + assertTrue(breadcrumb.contains("modelValue: number"));
  149 + assertFalse(breadcrumb.contains("TreeValue"));
  150 + }
  151 +
  152 + @Test
  153 + public void testExecute_duplicateSubPath() {
  154 + // 准备主表
  155 + CodegenTableDO table = getTable("student")
  156 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  157 + .setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType());
  158 + List<CodegenColumnDO> columns = getColumnList("student");
  159 + // 准备两个归一化名称相同的子表
  160 + CodegenTableDO crmItemTable = getTable("contact").setModuleName("crm").setClassName("CrmOrderItem")
  161 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  162 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  163 + .setSubJoinColumnId(100L).setSubJoinMany(true);
  164 + CodegenTableDO erpItemTable = getTable("contact").setModuleName("erp").setClassName("ErpOrderItem")
  165 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  166 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  167 + .setSubJoinColumnId(100L).setSubJoinMany(true);
  168 + List<CodegenColumnDO> crmItemColumns = getColumnList("contact");
  169 + List<CodegenColumnDO> erpItemColumns = getColumnList("contact");
  170 +
  171 + // 调用
  172 + IllegalStateException exception = assertThrows(IllegalStateException.class,
  173 + () -> codegenEngine.execute(DbType.MYSQL, table, columns,
  174 + Arrays.asList(crmItemTable, erpItemTable), Arrays.asList(crmItemColumns, erpItemColumns)));
  175 + // 断言
  176 + assertTrue(exception.getMessage().contains("生成文件路径重复"));
  177 + assertTrue(exception.getMessage().contains("subIndex=0"));
  178 + assertTrue(exception.getMessage().contains("subIndex=1"));
  179 + }
  180 +
  181 + @Test
  182 + public void testExecute_masterNormal() {
  183 + testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vue3_admin_uniapp_master_normal");
  184 + }
  185 +
  186 + @Test
  187 + public void testExecute_masterInner() {
  188 + testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vue3_admin_uniapp_master_inner");
  189 + }
  190 +
  191 + @Test
  192 + public void testExecute_masterErp() {
  193 + testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vue3_admin_uniapp_master_erp");
  194 + }
  195 +
  196 + private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
  197 + // 准备主表
  198 + CodegenTableDO table = getTable("student")
  199 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  200 + .setTemplateType(templateType.getType());
  201 + List<CodegenColumnDO> columns = getColumnList("student");
  202 + // 准备一对多子表
  203 + CodegenTableDO contactTable = getTable("contact")
  204 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  205 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  206 + .setSubJoinColumnId(100L).setSubJoinMany(true);
  207 + List<CodegenColumnDO> contactColumns = getColumnList("contact");
  208 + // 准备一对一子表
  209 + CodegenTableDO teacherTable = getTable("teacher")
  210 + .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
  211 + .setFrontType(CodegenFrontTypeEnum.VUE3_ADMIN_UNIAPP_WOT.getType())
  212 + .setSubJoinColumnId(200L).setSubJoinMany(false);
  213 + List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
  214 +
  215 + // 调用
  216 + Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
  217 + Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
  218 + // 断言
  219 + assertResult(result, path);
  220 + if (templateType != CodegenTemplateTypeEnum.MASTER_ERP) {
  221 + String api = result.get("yudao-ui-admin-uniapp/src/api/infra/demo/index.ts");
  222 + assertTrue(api.contains("studentTeacher?: StudentTeacher | null"));
  223 + String form = result.get("yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue");
  224 + assertTrue(form.contains("ref<StudentTeacher | null>(null)"));
  225 + assertTrue(form.contains("studentTeacher.value = studentTeacherData || null"));
  226 + assertTrue(form.contains(":model-value=\"item.birthday || Date.now()\""));
  227 + assertTrue(form.contains("v-model=\"item.sex\""));
  228 + assertTrue(form.contains("<yd-upload-img v-model=\"item.avatar\""));
  229 + assertTrue(form.contains("<yd-upload-file v-model=\"item.video\""));
  230 + assertTrue(form.contains("<wd-textarea v-model=\"item.description\""));
  231 + assertTrue(form.contains("function validateSubTables()"));
  232 + } else {
  233 + String contactForm = result.get(
  234 + "yudao-ui-admin-uniapp/src/pages-infra/demo/student-contact/form/index.vue");
  235 + assertTrue(contactForm.contains(":model-value=\"formData.birthday || Date.now()\""));
  236 + assertTrue(contactForm.contains("v-model=\"formData.sex\""));
  237 + assertTrue(contactForm.contains("<yd-upload-img v-model=\"formData.avatar\""));
  238 + assertTrue(contactForm.contains("<yd-upload-file v-model=\"formData.video\""));
  239 + assertTrue(contactForm.contains("<wd-textarea v-model=\"formData.description\""));
  240 + }
  241 + }
  242 +
62 243 }
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/assert.json 0 → 100644
  1 +[ {
  2 + "contentPath" : "java/InfraStudentPageReqVO",
  3 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
  4 +}, {
  5 + "contentPath" : "java/InfraStudentRespVO",
  6 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
  7 +}, {
  8 + "contentPath" : "java/InfraStudentSaveReqVO",
  9 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
  10 +}, {
  11 + "contentPath" : "java/InfraStudentController",
  12 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
  13 +}, {
  14 + "contentPath" : "java/InfraStudentDO",
  15 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
  16 +}, {
  17 + "contentPath" : "java/InfraStudentContactDO",
  18 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
  19 +}, {
  20 + "contentPath" : "java/InfraStudentTeacherDO",
  21 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
  22 +}, {
  23 + "contentPath" : "java/InfraStudentMapper",
  24 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
  25 +}, {
  26 + "contentPath" : "java/InfraStudentContactMapper",
  27 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
  28 +}, {
  29 + "contentPath" : "java/InfraStudentTeacherMapper",
  30 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
  31 +}, {
  32 + "contentPath" : "xml/InfraStudentMapper",
  33 + "filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml"
  34 +}, {
  35 + "contentPath" : "java/InfraStudentServiceImpl",
  36 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
  37 +}, {
  38 + "contentPath" : "java/InfraStudentService",
  39 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
  40 +}, {
  41 + "contentPath" : "java/InfraStudentServiceImplTest",
  42 + "filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
  43 +}, {
  44 + "contentPath" : "java/ErrorCodeConstants_手动操作",
  45 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
  46 +}, {
  47 + "contentPath" : "sql/sql",
  48 + "filePath" : "sql/sql.sql"
  49 +}, {
  50 + "contentPath" : "sql/h2",
  51 + "filePath" : "sql/h2.sql"
  52 +}, {
  53 + "contentPath" : "ts/index",
  54 + "filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
  55 +}, {
  56 + "contentPath" : "vue/index",
  57 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
  58 +}, {
  59 + "contentPath" : "vue/search-form",
  60 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
  61 +}, {
  62 + "contentPath" : "vue/form/index",
  63 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
  64 +}, {
  65 + "contentPath" : "vue/detail/index",
  66 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"
  67 +}, {
  68 + "contentPath" : "vue/student-contact/form/index",
  69 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/student-contact/form/index.vue"
  70 +}, {
  71 + "contentPath" : "vue/student-teacher/form/index",
  72 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/student-teacher/form/index.vue"
  73 +} ]
0 74 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/ErrorCodeConstants_手动操作 0 → 100644
  1 +// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
  2 +// ========== 学生 TODO 补充编号 ==========
  3 +ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
  4 +ErrorCode STUDENT_CONTACT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生联系人不存在");
  5 +ErrorCode STUDENT_TEACHER_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任不存在");
  6 +ErrorCode STUDENT_TEACHER_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任已存在");
0 7 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentContactDO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import java.time.LocalDateTime;
  6 +import java.time.LocalDateTime;
  7 +import com.baomidou.mybatisplus.annotation.*;
  8 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  9 +
  10 +/**
  11 + * 学生联系人 DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("infra_student_contact")
  16 +@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class InfraStudentContactDO extends BaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 学生编号
  32 + */
  33 + private Long studentId;
  34 + /**
  35 + * 名字
  36 + */
  37 + private String name;
  38 + /**
  39 + * 简介
  40 + */
  41 + private String description;
  42 + /**
  43 + * 出生日期
  44 + */
  45 + private LocalDateTime birthday;
  46 + /**
  47 + * 性别
  48 + *
  49 + * 枚举 {@link TODO system_user_sex 对应的类}
  50 + */
  51 + private Integer sex;
  52 + /**
  53 + * 是否有效
  54 + *
  55 + * 枚举 {@link TODO infra_boolean_string 对应的类}
  56 + */
  57 + private Boolean enabled;
  58 + /**
  59 + * 头像
  60 + */
  61 + private String avatar;
  62 + /**
  63 + * 附件
  64 + */
  65 + private String video;
  66 + /**
  67 + * 备注
  68 + */
  69 + private String memo;
  70 +
  71 +}
0 72 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentContactMapper 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.mysql.demo;
  2 +
  3 +import java.util.*;
  4 +
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  8 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  9 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  10 +import org.apache.ibatis.annotations.Mapper;
  11 +
  12 +/**
  13 + * 学生联系人 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
  19 +
  20 + default PageResult<InfraStudentContactDO> selectPage(PageParam reqVO, Long studentId) {
  21 + return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentContactDO>()
  22 + .eq(InfraStudentContactDO::getStudentId, studentId)
  23 + .orderByDesc(InfraStudentContactDO::getId));
  24 + }
  25 +
  26 + default int deleteByStudentId(Long studentId) {
  27 + return delete(InfraStudentContactDO::getStudentId, studentId);
  28 + }
  29 +
  30 + default int deleteByStudentIds(List<Long> studentIds) {
  31 + return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
  32 + }
  33 +
  34 +}
0 35 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentController 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo;
  2 +
  3 +import org.springframework.web.bind.annotation.*;
  4 +import jakarta.annotation.Resource;
  5 +import org.springframework.validation.annotation.Validated;
  6 +import org.springframework.security.access.prepost.PreAuthorize;
  7 +import io.swagger.v3.oas.annotations.tags.Tag;
  8 +import io.swagger.v3.oas.annotations.Parameter;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +
  11 +import jakarta.validation.constraints.*;
  12 +import jakarta.validation.*;
  13 +import jakarta.servlet.http.*;
  14 +import java.util.*;
  15 +import java.io.IOException;
  16 +
  17 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  18 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  19 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  20 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  21 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  22 +
  23 +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
  24 +
  25 +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
  26 +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
  27 +
  28 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  29 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  30 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  31 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  32 +import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
  33 +
  34 +@Tag(name = "管理后台 - 学生")
  35 +@RestController
  36 +@RequestMapping("/infra/student")
  37 +@Validated
  38 +public class InfraStudentController {
  39 +
  40 + @Resource
  41 + private InfraStudentService studentService;
  42 +
  43 + @PostMapping("/create")
  44 + @Operation(summary = "创建学生")
  45 + @PreAuthorize("@ss.hasPermission('infra:student:create')")
  46 + public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
  47 + return success(studentService.createStudent(createReqVO));
  48 + }
  49 +
  50 + @PutMapping("/update")
  51 + @Operation(summary = "更新学生")
  52 + @PreAuthorize("@ss.hasPermission('infra:student:update')")
  53 + public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
  54 + studentService.updateStudent(updateReqVO);
  55 + return success(true);
  56 + }
  57 +
  58 + @DeleteMapping("/delete")
  59 + @Operation(summary = "删除学生")
  60 + @Parameter(name = "id", description = "编号", required = true)
  61 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  62 + public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
  63 + studentService.deleteStudent(id);
  64 + return success(true);
  65 + }
  66 +
  67 + @DeleteMapping("/delete-list")
  68 + @Parameter(name = "ids", description = "编号", required = true)
  69 + @Operation(summary = "批量删除学生")
  70 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  71 + public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
  72 + studentService.deleteStudentListByIds(ids);
  73 + return success(true);
  74 + }
  75 +
  76 + @GetMapping("/get")
  77 + @Operation(summary = "获得学生")
  78 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  79 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  80 + public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
  81 + InfraStudentDO student = studentService.getStudent(id);
  82 + return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
  83 + }
  84 +
  85 + @GetMapping("/page")
  86 + @Operation(summary = "获得学生分页")
  87 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  88 + public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
  89 + PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
  90 + return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
  91 + }
  92 +
  93 + @GetMapping("/export-excel")
  94 + @Operation(summary = "导出学生 Excel")
  95 + @PreAuthorize("@ss.hasPermission('infra:student:export')")
  96 + @ApiAccessLog(operateType = EXPORT)
  97 + public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
  98 + HttpServletResponse response) throws IOException {
  99 + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
  100 + List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
  101 + // 导出 Excel
  102 + ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
  103 + BeanUtils.toBean(list, InfraStudentRespVO.class));
  104 + }
  105 +
  106 + // ==================== 子表(学生联系人) ====================
  107 +
  108 + @GetMapping("/student-contact/page")
  109 + @Operation(summary = "获得学生联系人分页")
  110 + @Parameter(name = "studentId", description = "学生编号")
  111 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  112 + public CommonResult<PageResult<InfraStudentContactDO>> getStudentContactPage(PageParam pageReqVO,
  113 + @RequestParam("studentId") Long studentId) {
  114 + return success(studentService.getStudentContactPage(pageReqVO, studentId));
  115 + }
  116 +
  117 + @PostMapping("/student-contact/create")
  118 + @Operation(summary = "创建学生联系人")
  119 + @PreAuthorize("@ss.hasPermission('infra:student:create')")
  120 + public CommonResult<Long> createStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
  121 + return success(studentService.createStudentContact(studentContact));
  122 + }
  123 +
  124 + @PutMapping("/student-contact/update")
  125 + @Operation(summary = "更新学生联系人")
  126 + @PreAuthorize("@ss.hasPermission('infra:student:update')")
  127 + public CommonResult<Boolean> updateStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
  128 + studentService.updateStudentContact(studentContact);
  129 + return success(true);
  130 + }
  131 +
  132 + @DeleteMapping("/student-contact/delete")
  133 + @Parameter(name = "id", description = "编号", required = true)
  134 + @Operation(summary = "删除学生联系人")
  135 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  136 + public CommonResult<Boolean> deleteStudentContact(@RequestParam("id") Long id) {
  137 + studentService.deleteStudentContact(id);
  138 + return success(true);
  139 + }
  140 +
  141 + @DeleteMapping("/student-contact/delete-list")
  142 + @Parameter(name = "ids", description = "编号", required = true)
  143 + @Operation(summary = "批量删除学生联系人")
  144 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  145 + public CommonResult<Boolean> deleteStudentContactList(@RequestParam("ids") List<Long> ids) {
  146 + studentService.deleteStudentContactListByIds(ids);
  147 + return success(true);
  148 + }
  149 +
  150 + @GetMapping("/student-contact/get")
  151 + @Operation(summary = "获得学生联系人")
  152 + @Parameter(name = "id", description = "编号", required = true)
  153 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  154 + public CommonResult<InfraStudentContactDO> getStudentContact(@RequestParam("id") Long id) {
  155 + return success(studentService.getStudentContact(id));
  156 + }
  157 +
  158 + // ==================== 子表(学生班主任) ====================
  159 +
  160 + @GetMapping("/student-teacher/page")
  161 + @Operation(summary = "获得学生班主任分页")
  162 + @Parameter(name = "studentId", description = "学生编号")
  163 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  164 + public CommonResult<PageResult<InfraStudentTeacherDO>> getStudentTeacherPage(PageParam pageReqVO,
  165 + @RequestParam("studentId") Long studentId) {
  166 + return success(studentService.getStudentTeacherPage(pageReqVO, studentId));
  167 + }
  168 +
  169 + @PostMapping("/student-teacher/create")
  170 + @Operation(summary = "创建学生班主任")
  171 + @PreAuthorize("@ss.hasPermission('infra:student:create')")
  172 + public CommonResult<Long> createStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
  173 + return success(studentService.createStudentTeacher(studentTeacher));
  174 + }
  175 +
  176 + @PutMapping("/student-teacher/update")
  177 + @Operation(summary = "更新学生班主任")
  178 + @PreAuthorize("@ss.hasPermission('infra:student:update')")
  179 + public CommonResult<Boolean> updateStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
  180 + studentService.updateStudentTeacher(studentTeacher);
  181 + return success(true);
  182 + }
  183 +
  184 + @DeleteMapping("/student-teacher/delete")
  185 + @Parameter(name = "id", description = "编号", required = true)
  186 + @Operation(summary = "删除学生班主任")
  187 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  188 + public CommonResult<Boolean> deleteStudentTeacher(@RequestParam("id") Long id) {
  189 + studentService.deleteStudentTeacher(id);
  190 + return success(true);
  191 + }
  192 +
  193 + @DeleteMapping("/student-teacher/delete-list")
  194 + @Parameter(name = "ids", description = "编号", required = true)
  195 + @Operation(summary = "批量删除学生班主任")
  196 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  197 + public CommonResult<Boolean> deleteStudentTeacherList(@RequestParam("ids") List<Long> ids) {
  198 + studentService.deleteStudentTeacherListByIds(ids);
  199 + return success(true);
  200 + }
  201 +
  202 + @GetMapping("/student-teacher/get")
  203 + @Operation(summary = "获得学生班主任")
  204 + @Parameter(name = "id", description = "编号", required = true)
  205 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  206 + public CommonResult<InfraStudentTeacherDO> getStudentTeacher(@RequestParam("id") Long id) {
  207 + return success(studentService.getStudentTeacher(id));
  208 + }
  209 +
  210 +}
0 211 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentDO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import java.time.LocalDateTime;
  6 +import java.time.LocalDateTime;
  7 +import com.baomidou.mybatisplus.annotation.*;
  8 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  9 +
  10 +/**
  11 + * 学生 DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("infra_student")
  16 +@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class InfraStudentDO extends BaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 名字
  32 + */
  33 + private String name;
  34 + /**
  35 + * 简介
  36 + */
  37 + private String description;
  38 + /**
  39 + * 出生日期
  40 + */
  41 + private LocalDateTime birthday;
  42 + /**
  43 + * 性别
  44 + *
  45 + * 枚举 {@link TODO system_user_sex 对应的类}
  46 + */
  47 + private Integer sex;
  48 + /**
  49 + * 是否有效
  50 + *
  51 + * 枚举 {@link TODO infra_boolean_string 对应的类}
  52 + */
  53 + private Boolean enabled;
  54 + /**
  55 + * 头像
  56 + */
  57 + private String avatar;
  58 + /**
  59 + * 附件
  60 + */
  61 + private String video;
  62 + /**
  63 + * 备注
  64 + */
  65 + private String memo;
  66 +
  67 +
  68 +}
0 69 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentMapper 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.mysql.demo;
  2 +
  3 +import java.util.*;
  4 +
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  7 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  8 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  9 +import org.apache.ibatis.annotations.Mapper;
  10 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  11 +
  12 +/**
  13 + * 学生 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
  19 +
  20 + default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
  21 + return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
  22 + .likeIfPresent(InfraStudentDO::getName, reqVO.getName())
  23 + .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
  24 + .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
  25 + .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
  26 + .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
  27 + .orderByDesc(InfraStudentDO::getId));
  28 + }
  29 +
  30 +}
0 31 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentPageReqVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +import java.time.LocalDateTime;
  9 +
  10 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  11 +
  12 +@Schema(description = "管理后台 - 学生分页 Request VO")
  13 +@Data
  14 +public class InfraStudentPageReqVO extends PageParam {
  15 +
  16 + @Schema(description = "名字", example = "芋头")
  17 + private String name;
  18 +
  19 + @Schema(description = "出生日期")
  20 + private LocalDateTime birthday;
  21 +
  22 + @Schema(description = "性别", example = "1")
  23 + private Integer sex;
  24 +
  25 + @Schema(description = "是否有效", example = "true")
  26 + private Boolean enabled;
  27 +
  28 + @Schema(description = "创建时间")
  29 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  30 + private LocalDateTime[] createTime;
  31 +
  32 +}
0 33 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentRespVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +import java.util.*;
  6 +import org.springframework.format.annotation.DateTimeFormat;
  7 +import java.time.LocalDateTime;
  8 +import cn.idev.excel.annotation.*;
  9 +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
  10 +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
  11 +
  12 +@Schema(description = "管理后台 - 学生 Response VO")
  13 +@Data
  14 +@ExcelIgnoreUnannotated
  15 +public class InfraStudentRespVO {
  16 +
  17 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  18 + @ExcelProperty("编号")
  19 + private Long id;
  20 +
  21 + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
  22 + @ExcelProperty("名字")
  23 + private String name;
  24 +
  25 + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
  26 + @ExcelProperty("简介")
  27 + private String description;
  28 +
  29 + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
  30 + @ExcelProperty("出生日期")
  31 + private LocalDateTime birthday;
  32 +
  33 + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  34 + @ExcelProperty(value = "性别", converter = DictConvert.class)
  35 + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
  36 + private Integer sex;
  37 +
  38 + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  39 + @ExcelProperty(value = "是否有效", converter = DictConvert.class)
  40 + @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
  41 + private Boolean enabled;
  42 +
  43 + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
  44 + @ExcelProperty("头像")
  45 + private String avatar;
  46 +
  47 + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
  48 + @ExcelProperty("附件")
  49 + private String video;
  50 +
  51 + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
  52 + @ExcelProperty("备注")
  53 + private String memo;
  54 +
  55 + @Schema(description = "创建时间")
  56 + @ExcelProperty("创建时间")
  57 + private LocalDateTime createTime;
  58 +
  59 +}
0 60 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentSaveReqVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +import java.util.*;
  6 +import jakarta.validation.constraints.*;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +import java.time.LocalDateTime;
  9 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  10 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  11 +
  12 +@Schema(description = "管理后台 - 学生新增/修改 Request VO")
  13 +@Data
  14 +public class InfraStudentSaveReqVO {
  15 +
  16 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  17 + private Long id;
  18 +
  19 + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
  20 + @NotEmpty(message = "名字不能为空")
  21 + private String name;
  22 +
  23 + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
  24 + @NotEmpty(message = "简介不能为空")
  25 + private String description;
  26 +
  27 + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
  28 + @NotNull(message = "出生日期不能为空")
  29 + private LocalDateTime birthday;
  30 +
  31 + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  32 + @NotNull(message = "性别不能为空")
  33 + private Integer sex;
  34 +
  35 + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  36 + @NotNull(message = "是否有效不能为空")
  37 + private Boolean enabled;
  38 +
  39 + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
  40 + @NotEmpty(message = "头像不能为空")
  41 + private String avatar;
  42 +
  43 + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
  44 + @NotEmpty(message = "附件不能为空")
  45 + private String video;
  46 +
  47 + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
  48 + @NotEmpty(message = "备注不能为空")
  49 + private String memo;
  50 +
  51 +}
0 52 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentService 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import java.util.*;
  4 +import jakarta.validation.*;
  5 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  6 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  7 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  8 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  9 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  10 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  11 +
  12 +/**
  13 + * 学生 Service 接口
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public interface InfraStudentService {
  18 +
  19 + /**
  20 + * 创建学生
  21 + *
  22 + * @param createReqVO 创建信息
  23 + * @return 编号
  24 + */
  25 + Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
  26 +
  27 + /**
  28 + * 更新学生
  29 + *
  30 + * @param updateReqVO 更新信息
  31 + */
  32 + void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
  33 +
  34 + /**
  35 + * 删除学生
  36 + *
  37 + * @param id 编号
  38 + */
  39 + void deleteStudent(Long id);
  40 +
  41 + /**
  42 + * 批量删除学生
  43 + *
  44 + * @param ids 编号
  45 + */
  46 + void deleteStudentListByIds(List<Long> ids);
  47 +
  48 + /**
  49 + * 获得学生
  50 + *
  51 + * @param id 编号
  52 + * @return 学生
  53 + */
  54 + InfraStudentDO getStudent(Long id);
  55 +
  56 + /**
  57 + * 获得学生分页
  58 + *
  59 + * @param pageReqVO 分页查询
  60 + * @return 学生分页
  61 + */
  62 + PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
  63 +
  64 + // ==================== 子表(学生联系人) ====================
  65 +
  66 + /**
  67 + * 获得学生联系人分页
  68 + *
  69 + * @param pageReqVO 分页查询
  70 + * @param studentId 学生编号
  71 + * @return 学生联系人分页
  72 + */
  73 + PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId);
  74 +
  75 + /**
  76 + * 创建学生联系人
  77 + *
  78 + * @param studentContact 创建信息
  79 + * @return 编号
  80 + */
  81 + Long createStudentContact(@Valid InfraStudentContactDO studentContact);
  82 +
  83 + /**
  84 + * 更新学生联系人
  85 + *
  86 + * @param studentContact 更新信息
  87 + */
  88 + void updateStudentContact(@Valid InfraStudentContactDO studentContact);
  89 +
  90 + /**
  91 + * 删除学生联系人
  92 + *
  93 + * @param id 编号
  94 + */
  95 + void deleteStudentContact(Long id);
  96 +
  97 + /**
  98 + * 批量删除学生联系人
  99 + *
  100 + * @param ids 编号
  101 + */
  102 + void deleteStudentContactListByIds(List<Long> ids);
  103 +
  104 + /**
  105 + * 获得学生联系人
  106 + *
  107 + * @param id 编号
  108 + * @return 学生联系人
  109 + */
  110 + InfraStudentContactDO getStudentContact(Long id);
  111 +
  112 + // ==================== 子表(学生班主任) ====================
  113 +
  114 + /**
  115 + * 获得学生班主任分页
  116 + *
  117 + * @param pageReqVO 分页查询
  118 + * @param studentId 学生编号
  119 + * @return 学生班主任分页
  120 + */
  121 + PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId);
  122 +
  123 + /**
  124 + * 创建学生班主任
  125 + *
  126 + * @param studentTeacher 创建信息
  127 + * @return 编号
  128 + */
  129 + Long createStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
  130 +
  131 + /**
  132 + * 更新学生班主任
  133 + *
  134 + * @param studentTeacher 更新信息
  135 + */
  136 + void updateStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
  137 +
  138 + /**
  139 + * 删除学生班主任
  140 + *
  141 + * @param id 编号
  142 + */
  143 + void deleteStudentTeacher(Long id);
  144 +
  145 + /**
  146 + * 批量删除学生班主任
  147 + *
  148 + * @param ids 编号
  149 + */
  150 + void deleteStudentTeacherListByIds(List<Long> ids);
  151 +
  152 + /**
  153 + * 获得学生班主任
  154 + *
  155 + * @param id 编号
  156 + * @return 学生班主任
  157 + */
  158 + InfraStudentTeacherDO getStudentTeacher(Long id);
  159 +
  160 +}
0 161 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentServiceImpl 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import org.springframework.stereotype.Service;
  5 +import jakarta.annotation.Resource;
  6 +import org.springframework.validation.annotation.Validated;
  7 +import org.springframework.transaction.annotation.Transactional;
  8 +
  9 +import java.util.*;
  10 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  11 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  12 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  13 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  14 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  15 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  16 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  17 +
  18 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
  19 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
  20 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
  21 +
  22 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  23 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  24 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
  25 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
  26 +
  27 +/**
  28 + * 学生 Service 实现类
  29 + *
  30 + * @author 芋道源码
  31 + */
  32 +@Service
  33 +@Validated
  34 +public class InfraStudentServiceImpl implements InfraStudentService {
  35 +
  36 + @Resource
  37 + private InfraStudentMapper studentMapper;
  38 + @Resource
  39 + private InfraStudentContactMapper studentContactMapper;
  40 + @Resource
  41 + private InfraStudentTeacherMapper studentTeacherMapper;
  42 +
  43 + @Override
  44 + public Long createStudent(InfraStudentSaveReqVO createReqVO) {
  45 + // 插入
  46 + InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
  47 + studentMapper.insert(student);
  48 +
  49 + // 返回
  50 + return student.getId();
  51 + }
  52 +
  53 + @Override
  54 + public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
  55 + // 校验存在
  56 + validateStudentExists(updateReqVO.getId());
  57 + // 更新
  58 + InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
  59 + studentMapper.updateById(updateObj);
  60 + }
  61 +
  62 + @Override
  63 + @Transactional(rollbackFor = Exception.class)
  64 + public void deleteStudent(Long id) {
  65 + // 校验存在
  66 + validateStudentExists(id);
  67 + // 删除
  68 + studentMapper.deleteById(id);
  69 +
  70 + // 删除子表
  71 + deleteStudentContactByStudentId(id);
  72 + deleteStudentTeacherByStudentId(id);
  73 + }
  74 +
  75 + @Override
  76 + @Transactional(rollbackFor = Exception.class)
  77 + public void deleteStudentListByIds(List<Long> ids) {
  78 + // 删除
  79 + studentMapper.deleteByIds(ids);
  80 +
  81 + // 删除子表
  82 + deleteStudentContactByStudentIds(ids);
  83 + deleteStudentTeacherByStudentIds(ids);
  84 + }
  85 +
  86 +
  87 + private void validateStudentExists(Long id) {
  88 + if (studentMapper.selectById(id) == null) {
  89 + throw exception(STUDENT_NOT_EXISTS);
  90 + }
  91 + }
  92 +
  93 + @Override
  94 + public InfraStudentDO getStudent(Long id) {
  95 + return studentMapper.selectById(id);
  96 + }
  97 +
  98 + @Override
  99 + public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
  100 + return studentMapper.selectPage(pageReqVO);
  101 + }
  102 +
  103 + // ==================== 子表(学生联系人) ====================
  104 +
  105 + @Override
  106 + public PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId) {
  107 + return studentContactMapper.selectPage(pageReqVO, studentId);
  108 + }
  109 +
  110 + @Override
  111 + public Long createStudentContact(InfraStudentContactDO studentContact) {
  112 + studentContact.clean(); // 清理掉创建、更新时间等相关属性值
  113 + studentContactMapper.insert(studentContact);
  114 + return studentContact.getId();
  115 + }
  116 +
  117 + @Override
  118 + public void updateStudentContact(InfraStudentContactDO studentContact) {
  119 + // 校验存在
  120 + validateStudentContactExists(studentContact.getId());
  121 + // 更新
  122 + studentContact.clean(); // 解决更新情况下:updateTime 不更新
  123 + studentContactMapper.updateById(studentContact);
  124 + }
  125 +
  126 + @Override
  127 + public void deleteStudentContact(Long id) {
  128 + // 删除
  129 + studentContactMapper.deleteById(id);
  130 + }
  131 +
  132 + @Override
  133 + public void deleteStudentContactListByIds(List<Long> ids) {
  134 + // 删除
  135 + studentContactMapper.deleteByIds(ids);
  136 + }
  137 +
  138 + @Override
  139 + public InfraStudentContactDO getStudentContact(Long id) {
  140 + return studentContactMapper.selectById(id);
  141 + }
  142 +
  143 + private void validateStudentContactExists(Long id) {
  144 + if (studentContactMapper.selectById(id) == null) {
  145 + throw exception(STUDENT_CONTACT_NOT_EXISTS);
  146 + }
  147 + }
  148 +
  149 + private void deleteStudentContactByStudentId(Long studentId) {
  150 + studentContactMapper.deleteByStudentId(studentId);
  151 + }
  152 +
  153 + private void deleteStudentContactByStudentIds(List<Long> studentIds) {
  154 + studentContactMapper.deleteByStudentIds(studentIds);
  155 + }
  156 +
  157 + // ==================== 子表(学生班主任) ====================
  158 +
  159 + @Override
  160 + public PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId) {
  161 + return studentTeacherMapper.selectPage(pageReqVO, studentId);
  162 + }
  163 +
  164 + @Override
  165 + public Long createStudentTeacher(InfraStudentTeacherDO studentTeacher) {
  166 + // 校验是否已经存在
  167 + if (studentTeacherMapper.selectByStudentId(studentTeacher.getStudentId()) != null) {
  168 + throw exception(STUDENT_TEACHER_EXISTS);
  169 + }
  170 + // 插入
  171 + studentTeacher.clean(); // 清理掉创建、更新时间等相关属性值
  172 + studentTeacherMapper.insert(studentTeacher);
  173 + return studentTeacher.getId();
  174 + }
  175 +
  176 + @Override
  177 + public void updateStudentTeacher(InfraStudentTeacherDO studentTeacher) {
  178 + // 校验存在
  179 + validateStudentTeacherExists(studentTeacher.getId());
  180 + // 更新
  181 + studentTeacher.clean(); // 解决更新情况下:updateTime 不更新
  182 + studentTeacherMapper.updateById(studentTeacher);
  183 + }
  184 +
  185 + @Override
  186 + public void deleteStudentTeacher(Long id) {
  187 + // 删除
  188 + studentTeacherMapper.deleteById(id);
  189 + }
  190 +
  191 + @Override
  192 + public void deleteStudentTeacherListByIds(List<Long> ids) {
  193 + // 删除
  194 + studentTeacherMapper.deleteByIds(ids);
  195 + }
  196 +
  197 + @Override
  198 + public InfraStudentTeacherDO getStudentTeacher(Long id) {
  199 + return studentTeacherMapper.selectById(id);
  200 + }
  201 +
  202 + private void validateStudentTeacherExists(Long id) {
  203 + if (studentTeacherMapper.selectById(id) == null) {
  204 + throw exception(STUDENT_TEACHER_NOT_EXISTS);
  205 + }
  206 + }
  207 +
  208 + private void deleteStudentTeacherByStudentId(Long studentId) {
  209 + studentTeacherMapper.deleteByStudentId(studentId);
  210 + }
  211 +
  212 + private void deleteStudentTeacherByStudentIds(List<Long> studentIds) {
  213 + studentTeacherMapper.deleteByStudentIds(studentIds);
  214 + }
  215 +
  216 +}
0 217 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentServiceImplTest 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import org.junit.jupiter.api.Disabled;
  4 +import org.junit.jupiter.api.Test;
  5 +
  6 +import jakarta.annotation.Resource;
  7 +
  8 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  9 +
  10 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  11 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  12 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
  13 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  14 +
  15 +import jakarta.annotation.Resource;
  16 +import org.springframework.context.annotation.Import;
  17 +import java.util.*;
  18 +import java.time.LocalDateTime;
  19 +
  20 +import static cn.hutool.core.util.RandomUtil.*;
  21 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
  22 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
  23 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
  24 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
  25 +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
  26 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
  27 +import static org.junit.jupiter.api.Assertions.*;
  28 +import static org.mockito.Mockito.*;
  29 +
  30 +/**
  31 + * {@link InfraStudentServiceImpl} 的单元测试类
  32 + *
  33 + * @author 芋道源码
  34 + */
  35 +@Import(InfraStudentServiceImpl.class)
  36 +public class InfraStudentServiceImplTest extends BaseDbUnitTest {
  37 +
  38 + @Resource
  39 + private InfraStudentServiceImpl studentService;
  40 +
  41 + @Resource
  42 + private InfraStudentMapper studentMapper;
  43 +
  44 + @Test
  45 + public void testCreateStudent_success() {
  46 + // 准备参数
  47 + InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
  48 +
  49 + // 调用
  50 + Long studentId = studentService.createStudent(createReqVO);
  51 + // 断言
  52 + assertNotNull(studentId);
  53 + // 校验记录的属性是否正确
  54 + InfraStudentDO student = studentMapper.selectById(studentId);
  55 + assertPojoEquals(createReqVO, student, "id");
  56 + }
  57 +
  58 + @Test
  59 + public void testUpdateStudent_success() {
  60 + // mock 数据
  61 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
  62 + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
  63 + // 准备参数
  64 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
  65 + o.setId(dbStudent.getId()); // 设置更新的 ID
  66 + });
  67 +
  68 + // 调用
  69 + studentService.updateStudent(updateReqVO);
  70 + // 校验是否更新正确
  71 + InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
  72 + assertPojoEquals(updateReqVO, student);
  73 + }
  74 +
  75 + @Test
  76 + public void testUpdateStudent_notExists() {
  77 + // 准备参数
  78 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
  79 +
  80 + // 调用, 并断言异常
  81 + assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
  82 + }
  83 +
  84 + @Test
  85 + public void testDeleteStudent_success() {
  86 + // mock 数据
  87 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
  88 + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
  89 + // 准备参数
  90 + Long id = dbStudent.getId();
  91 +
  92 + // 调用
  93 + studentService.deleteStudent(id);
  94 + // 校验数据不存在了
  95 + assertNull(studentMapper.selectById(id));
  96 + }
  97 +
  98 + @Test
  99 + public void testDeleteStudent_notExists() {
  100 + // 准备参数
  101 + Long id = randomLongId();
  102 +
  103 + // 调用, 并断言异常
  104 + assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
  105 + }
  106 +
  107 + @Test
  108 + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
  109 + public void testGetStudentPage() {
  110 + // mock 数据
  111 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
  112 + o.setName(null);
  113 + o.setBirthday(null);
  114 + o.setSex(null);
  115 + o.setEnabled(null);
  116 + o.setCreateTime(null);
  117 + });
  118 + studentMapper.insert(dbStudent);
  119 + // 测试 name 不匹配
  120 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
  121 + // 测试 birthday 不匹配
  122 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
  123 + // 测试 sex 不匹配
  124 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
  125 + // 测试 enabled 不匹配
  126 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
  127 + // 测试 createTime 不匹配
  128 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
  129 + // 准备参数
  130 + InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
  131 + reqVO.setName(null);
  132 + reqVO.setBirthday(null);
  133 + reqVO.setSex(null);
  134 + reqVO.setEnabled(null);
  135 + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
  136 +
  137 + // 调用
  138 + PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
  139 + // 断言
  140 + assertEquals(1, pageResult.getTotal());
  141 + assertEquals(1, pageResult.getList().size());
  142 + assertPojoEquals(dbStudent, pageResult.getList().get(0));
  143 + }
  144 +
  145 +}
0 146 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentTeacherDO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import java.time.LocalDateTime;
  6 +import java.time.LocalDateTime;
  7 +import com.baomidou.mybatisplus.annotation.*;
  8 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  9 +
  10 +/**
  11 + * 学生班主任 DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("infra_student_teacher")
  16 +@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class InfraStudentTeacherDO extends BaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 学生编号
  32 + */
  33 + private Long studentId;
  34 + /**
  35 + * 名字
  36 + */
  37 + private String name;
  38 + /**
  39 + * 简介
  40 + */
  41 + private String description;
  42 + /**
  43 + * 出生日期
  44 + */
  45 + private LocalDateTime birthday;
  46 + /**
  47 + * 性别
  48 + *
  49 + * 枚举 {@link TODO system_user_sex 对应的类}
  50 + */
  51 + private Integer sex;
  52 + /**
  53 + * 是否有效
  54 + *
  55 + * 枚举 {@link TODO infra_boolean_string 对应的类}
  56 + */
  57 + private Boolean enabled;
  58 + /**
  59 + * 头像
  60 + */
  61 + private String avatar;
  62 + /**
  63 + * 附件
  64 + */
  65 + private String video;
  66 + /**
  67 + * 备注
  68 + */
  69 + private String memo;
  70 +
  71 +}
0 72 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/java/InfraStudentTeacherMapper 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.mysql.demo;
  2 +
  3 +import java.util.*;
  4 +
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  8 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  9 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  10 +import org.apache.ibatis.annotations.Mapper;
  11 +
  12 +/**
  13 + * 学生班主任 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
  19 +
  20 + default PageResult<InfraStudentTeacherDO> selectPage(PageParam reqVO, Long studentId) {
  21 + return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentTeacherDO>()
  22 + .eq(InfraStudentTeacherDO::getStudentId, studentId)
  23 + .orderByDesc(InfraStudentTeacherDO::getId));
  24 + }
  25 + default InfraStudentTeacherDO selectByStudentId(Long studentId) {
  26 + return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
  27 + }
  28 +
  29 + default int deleteByStudentId(Long studentId) {
  30 + return delete(InfraStudentTeacherDO::getStudentId, studentId);
  31 + }
  32 +
  33 + default int deleteByStudentIds(List<Long> studentIds) {
  34 + return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds);
  35 + }
  36 +
  37 +}
0 38 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/sql/h2 0 → 100644
  1 +-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
  2 +CREATE TABLE IF NOT EXISTS "infra_student" (
  3 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  4 + "name" varchar NOT NULL,
  5 + "description" varchar NOT NULL,
  6 + "birthday" varchar NOT NULL,
  7 + "sex" int NOT NULL,
  8 + "enabled" bit NOT NULL,
  9 + "avatar" varchar NOT NULL,
  10 + "video" varchar NOT NULL,
  11 + "memo" varchar NOT NULL,
  12 + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  13 + PRIMARY KEY ("id")
  14 +) COMMENT '学生表';
  15 +
  16 +-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
  17 +DELETE FROM "infra_student";
0 18 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/sql/sql 0 → 100644
  1 +-- 菜单 SQL
  2 +INSERT INTO system_menu(
  3 + name, permission, type, sort, parent_id,
  4 + path, icon, component, status, component_name
  5 +)
  6 +VALUES (
  7 + '学生管理', '', 2, 0, 888,
  8 + 'student', '', 'infra/demo/index', 0, 'InfraStudent'
  9 +);
  10 +
  11 +-- 按钮父菜单ID
  12 +SELECT @parentId := LAST_INSERT_ID();
  13 +
  14 +-- 按钮 SQL
  15 +INSERT INTO system_menu(
  16 + name, permission, type, sort, parent_id,
  17 + path, icon, component, status
  18 +)
  19 +VALUES (
  20 + '学生查询', 'infra:student:query', 3, 1, @parentId,
  21 + '', '', '', 0
  22 +);
  23 +INSERT INTO system_menu(
  24 + name, permission, type, sort, parent_id,
  25 + path, icon, component, status
  26 +)
  27 +VALUES (
  28 + '学生创建', 'infra:student:create', 3, 2, @parentId,
  29 + '', '', '', 0
  30 +);
  31 +INSERT INTO system_menu(
  32 + name, permission, type, sort, parent_id,
  33 + path, icon, component, status
  34 +)
  35 +VALUES (
  36 + '学生更新', 'infra:student:update', 3, 3, @parentId,
  37 + '', '', '', 0
  38 +);
  39 +INSERT INTO system_menu(
  40 + name, permission, type, sort, parent_id,
  41 + path, icon, component, status
  42 +)
  43 +VALUES (
  44 + '学生删除', 'infra:student:delete', 3, 4, @parentId,
  45 + '', '', '', 0
  46 +);
  47 +INSERT INTO system_menu(
  48 + name, permission, type, sort, parent_id,
  49 + path, icon, component, status
  50 +)
  51 +VALUES (
  52 + '学生导出', 'infra:student:export', 3, 5, @parentId,
  53 + '', '', '', 0
  54 +);
0 55 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/ts/index 0 → 100644
  1 +import type { PageParam, PageResult } from '@/http/types'
  2 +import { http } from '@/http/http'
  3 +
  4 +/** 学生联系人信息 */
  5 +export interface StudentContact {
  6 + id?: number
  7 + studentId: number
  8 + name: string
  9 + description: string
  10 + birthday: number
  11 + sex: number
  12 + enabled: boolean
  13 + avatar: string
  14 + video?: string
  15 + memo: string
  16 + createTime?: number
  17 +}
  18 +
  19 +/** 学生班主任信息 */
  20 +export interface StudentTeacher {
  21 + id?: number
  22 + studentId: number
  23 + name: string
  24 + description: string
  25 + birthday: number
  26 + sex: number
  27 + enabled: boolean
  28 + avatar: string
  29 + video?: string
  30 + memo: string
  31 + createTime?: number
  32 +}
  33 +
  34 +/** 学生信息 */
  35 +export interface Student {
  36 + id?: number
  37 + name: string
  38 + description: string
  39 + birthday: number
  40 + sex: number
  41 + enabled: boolean
  42 + avatar: string
  43 + video: string
  44 + memo: string
  45 + createTime?: number
  46 +}
  47 +
  48 +/** 获取学生分页列表 */
  49 +export function getStudentPage(params: PageParam) {
  50 + return http.get<PageResult<Student>>('/infra/student/page', params)
  51 +}
  52 +
  53 +/** 获取学生详情 */
  54 +export function getStudent(id: number) {
  55 + return http.get<Student>(`/infra/student/get?id=${id}`)
  56 +}
  57 +
  58 +/** 创建学生 */
  59 +export function createStudent(data: Student) {
  60 + return http.post<number>('/infra/student/create', data)
  61 +}
  62 +
  63 +/** 更新学生 */
  64 +export function updateStudent(data: Student) {
  65 + return http.put<boolean>('/infra/student/update', data)
  66 +}
  67 +
  68 +/** 删除学生 */
  69 +export function deleteStudent(id: number) {
  70 + return http.delete<boolean>(`/infra/student/delete?id=${id}`)
  71 +}
  72 +
  73 +// ==================== 子表:学生联系人 ====================
  74 +
  75 +/** 获取学生联系人分页列表 */
  76 +export function getStudentContactPage(params: PageParam & { studentId: number }) {
  77 + return http.get<PageResult<StudentContact>>('/infra/student/student-contact/page', params)
  78 +}
  79 +
  80 +/** 获取学生联系人详情 */
  81 +export function getStudentContact(id: number) {
  82 + return http.get<StudentContact>(`/infra/student/student-contact/get?id=${id}`)
  83 +}
  84 +
  85 +/** 创建学生联系人 */
  86 +export function createStudentContact(data: StudentContact) {
  87 + return http.post<number>('/infra/student/student-contact/create', data)
  88 +}
  89 +
  90 +/** 更新学生联系人 */
  91 +export function updateStudentContact(data: StudentContact) {
  92 + return http.put<boolean>('/infra/student/student-contact/update', data)
  93 +}
  94 +
  95 +/** 删除学生联系人 */
  96 +export function deleteStudentContact(id: number) {
  97 + return http.delete<boolean>(`/infra/student/student-contact/delete?id=${id}`)
  98 +}
  99 +
  100 +// ==================== 子表:学生班主任 ====================
  101 +
  102 +/** 获取学生班主任分页列表 */
  103 +export function getStudentTeacherPage(params: PageParam & { studentId: number }) {
  104 + return http.get<PageResult<StudentTeacher>>('/infra/student/student-teacher/page', params)
  105 +}
  106 +
  107 +/** 获取学生班主任详情 */
  108 +export function getStudentTeacher(id: number) {
  109 + return http.get<StudentTeacher>(`/infra/student/student-teacher/get?id=${id}`)
  110 +}
  111 +
  112 +/** 创建学生班主任 */
  113 +export function createStudentTeacher(data: StudentTeacher) {
  114 + return http.post<number>('/infra/student/student-teacher/create', data)
  115 +}
  116 +
  117 +/** 更新学生班主任 */
  118 +export function updateStudentTeacher(data: StudentTeacher) {
  119 + return http.put<boolean>('/infra/student/student-teacher/update', data)
  120 +}
  121 +
  122 +/** 删除学生班主任 */
  123 +export function deleteStudentTeacher(id: number) {
  124 + return http.delete<boolean>(`/infra/student/student-teacher/delete?id=${id}`)
  125 +}
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/detail/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar title="学生详情" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
  5 +
  6 + <view class="pb-160rpx">
  7 + <!-- 主表详情 -->
  8 + <wd-cell-group border title="学生信息">
  9 + <wd-cell title="编号" :value="formData?.id ?? '-'" />
  10 + <wd-cell title="名字" :value="formData?.name ?? '-'" />
  11 + <wd-cell title="简介" :value="formData?.description ?? '-'" />
  12 + <wd-cell title="出生日期" :value="formatDateTime(formData?.birthday) || '-'" />
  13 + <wd-cell title="性别">
  14 + <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="formData?.sex" />
  15 + </wd-cell>
  16 + <wd-cell title="是否有效">
  17 + <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.enabled" />
  18 + </wd-cell>
  19 + <wd-cell title="头像" :value="formData?.avatar ?? '-'" />
  20 + <wd-cell title="附件" :value="formData?.video ?? '-'" />
  21 + <wd-cell title="备注" :value="formData?.memo ?? '-'" />
  22 + <wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
  23 + </wd-cell-group>
  24 +
  25 + <!-- 子表:学生联系人 -->
  26 + <view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
  27 + <text class="text-28rpx text-[#333] font-semibold">学生联系人({{ studentContactTotal }})</text>
  28 + <wd-button
  29 + v-if="hasAccessByCodes(['infra:student:create'])"
  30 + size="small" type="primary" variant="plain" @click="openStudentContact()"
  31 + >
  32 + 添加
  33 + </wd-button>
  34 + </view>
  35 + <view class="px-24rpx">
  36 + <z-paging
  37 + ref="studentContactPagingRef"
  38 + v-model="studentContactList"
  39 + :fixed="false"
  40 + :auto="false"
  41 + height="560rpx"
  42 + :default-page-size="10"
  43 + :refresher-enabled="false"
  44 + :inside-more="true"
  45 + :to-bottom-loading-more-enabled="false"
  46 + loading-more-default-text="点击加载更多"
  47 + empty-view-text="暂无学生联系人"
  48 + @query="queryStudentContactList"
  49 + >
  50 + <view
  51 + v-for="item in studentContactList"
  52 + :key="item.id"
  53 + class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
  54 + >
  55 + <view class="mb-8rpx text-28rpx text-[#666]">
  56 + 学生编号:{{ item.studentId ?? '-' }}
  57 + </view>
  58 + <view class="mb-8rpx text-28rpx text-[#666]">
  59 + 名字:{{ item.name ?? '-' }}
  60 + </view>
  61 + <view class="mb-8rpx text-28rpx text-[#666]">
  62 + 简介:{{ item.description ?? '-' }}
  63 + </view>
  64 + <view class="mb-8rpx text-28rpx text-[#666]">
  65 + 出生日期:{{ item.birthday ?? '-' }}
  66 + </view>
  67 + <view class="mb-8rpx text-28rpx text-[#666]">
  68 + 性别:{{ item.sex ?? '-' }}
  69 + </view>
  70 + <view class="mb-8rpx text-28rpx text-[#666]">
  71 + 是否有效:{{ item.enabled ?? '-' }}
  72 + </view>
  73 + <view class="mb-8rpx text-28rpx text-[#666]">
  74 + 头像:{{ item.avatar ?? '-' }}
  75 + </view>
  76 + <view class="mb-8rpx text-28rpx text-[#666]">
  77 + 附件:{{ item.video ?? '-' }}
  78 + </view>
  79 + <view class="mb-8rpx text-28rpx text-[#666]">
  80 + 备注:{{ item.memo ?? '-' }}
  81 + </view>
  82 + <view class="mb-8rpx text-28rpx text-[#666]">
  83 + 创建时间:{{ item.createTime ?? '-' }}
  84 + </view>
  85 + <view class="mt-16rpx flex justify-end gap-16rpx">
  86 + <wd-button size="small" variant="plain" @click="openStudentContact(item)">
  87 + 编辑
  88 + </wd-button>
  89 + <wd-button size="small" type="danger" variant="plain" @click="removeStudentContact(item)">
  90 + 删除
  91 + </wd-button>
  92 + </view>
  93 + </view>
  94 + </z-paging>
  95 + </view>
  96 + <!-- 子表:学生班主任 -->
  97 + <view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
  98 + <text class="text-28rpx text-[#333] font-semibold">学生班主任({{ studentTeacherTotal }})</text>
  99 + <wd-button
  100 + v-if="studentTeacherTotal === 0 && hasAccessByCodes(['infra:student:create'])"
  101 + size="small" type="primary" variant="plain" @click="openStudentTeacher()"
  102 + >
  103 + 添加
  104 + </wd-button>
  105 + </view>
  106 + <view class="px-24rpx">
  107 + <z-paging
  108 + ref="studentTeacherPagingRef"
  109 + v-model="studentTeacherList"
  110 + :fixed="false"
  111 + :auto="false"
  112 + height="560rpx"
  113 + :default-page-size="1"
  114 + :refresher-enabled="false"
  115 + :inside-more="true"
  116 + :to-bottom-loading-more-enabled="false"
  117 + loading-more-default-text="点击加载更多"
  118 + empty-view-text="暂无学生班主任"
  119 + @query="queryStudentTeacherList"
  120 + >
  121 + <view
  122 + v-for="item in studentTeacherList"
  123 + :key="item.id"
  124 + class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
  125 + >
  126 + <view class="mb-8rpx text-28rpx text-[#666]">
  127 + 学生编号:{{ item.studentId ?? '-' }}
  128 + </view>
  129 + <view class="mb-8rpx text-28rpx text-[#666]">
  130 + 名字:{{ item.name ?? '-' }}
  131 + </view>
  132 + <view class="mb-8rpx text-28rpx text-[#666]">
  133 + 简介:{{ item.description ?? '-' }}
  134 + </view>
  135 + <view class="mb-8rpx text-28rpx text-[#666]">
  136 + 出生日期:{{ item.birthday ?? '-' }}
  137 + </view>
  138 + <view class="mb-8rpx text-28rpx text-[#666]">
  139 + 性别:{{ item.sex ?? '-' }}
  140 + </view>
  141 + <view class="mb-8rpx text-28rpx text-[#666]">
  142 + 是否有效:{{ item.enabled ?? '-' }}
  143 + </view>
  144 + <view class="mb-8rpx text-28rpx text-[#666]">
  145 + 头像:{{ item.avatar ?? '-' }}
  146 + </view>
  147 + <view class="mb-8rpx text-28rpx text-[#666]">
  148 + 附件:{{ item.video ?? '-' }}
  149 + </view>
  150 + <view class="mb-8rpx text-28rpx text-[#666]">
  151 + 备注:{{ item.memo ?? '-' }}
  152 + </view>
  153 + <view class="mb-8rpx text-28rpx text-[#666]">
  154 + 创建时间:{{ item.createTime ?? '-' }}
  155 + </view>
  156 + <view class="mt-16rpx flex justify-end gap-16rpx">
  157 + <wd-button size="small" variant="plain" @click="openStudentTeacher(item)">
  158 + 编辑
  159 + </wd-button>
  160 + <wd-button size="small" type="danger" variant="plain" @click="removeStudentTeacher(item)">
  161 + 删除
  162 + </wd-button>
  163 + </view>
  164 + </view>
  165 + </z-paging>
  166 + </view>
  167 + </view>
  168 +
  169 + <!-- 底部操作按钮 -->
  170 + <view class="yd-detail-footer">
  171 + <view class="yd-detail-footer-actions">
  172 + <wd-button v-if="hasAccessByCodes(['infra:student:update'])" class="flex-1" type="warning" @click="handleEdit">
  173 + 编辑
  174 + </wd-button>
  175 + <wd-button v-if="hasAccessByCodes(['infra:student:delete'])" class="flex-1" type="danger" :loading="deleting" @click="handleDelete">
  176 + 删除
  177 + </wd-button>
  178 + </view>
  179 + </view>
  180 + </view>
  181 +</template>
  182 +
  183 +<script lang="ts" setup>
  184 +import type {
  185 + Student,
  186 + StudentContact,
  187 + StudentTeacher,
  188 +} from '@/api/infra/demo'
  189 +import { onShow } from '@dcloudio/uni-app'
  190 +import { nextTick, ref } from 'vue'
  191 +import { useDialog } from '@wot-ui/ui/components/wd-dialog'
  192 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  193 +import {
  194 + deleteStudent,
  195 + deleteStudentContact,
  196 + deleteStudentTeacher,
  197 + getStudent,
  198 + getStudentContactPage,
  199 + getStudentTeacherPage,
  200 +} from '@/api/infra/demo'
  201 +import { useAccess } from '@/hooks/useAccess'
  202 +import { delay, navigateBackPlus } from '@/utils'
  203 +import { DICT_TYPE } from '@/utils/constants'
  204 +import { formatDateTime } from '@/utils/date'
  205 +
  206 +const props = defineProps<{ id?: number }>()
  207 +
  208 +definePage({
  209 + style: { navigationBarTitleText: '', navigationStyle: 'custom' },
  210 +})
  211 +
  212 +const { hasAccessByCodes } = useAccess()
  213 +const dialog = useDialog()
  214 +const toast = useToast()
  215 +const formData = ref<Student>() // 主表数据
  216 +const studentContactList = ref<StudentContact[]>([]) // 学生联系人列表
  217 +const studentContactTotal = ref(0) // 学生联系人总数
  218 +const studentContactPagingRef = ref<any>() // 学生联系人分页组件引用
  219 +const studentTeacherList = ref<StudentTeacher[]>([]) // 学生班主任列表
  220 +const studentTeacherTotal = ref(0) // 学生班主任总数
  221 +const studentTeacherPagingRef = ref<any>() // 学生班主任分页组件引用
  222 +const deleting = ref(false) // 主表删除状态
  223 +
  224 +/** 返回上一页 */
  225 +function handleBack() {
  226 + navigateBackPlus('/pages-infra/demo/index')
  227 +}
  228 +
  229 +/** 加载主表 */
  230 +async function loadMain() {
  231 + if (props.id) {
  232 + formData.value = await getStudent(props.id)
  233 + }
  234 +}
  235 +
  236 +/** 查询学生联系人列表 */
  237 +async function queryStudentContactList(pageNo: number, pageSize: number) {
  238 + if (!props.id) {
  239 + studentContactPagingRef.value?.completeByTotal([], 0)
  240 + return
  241 + }
  242 + try {
  243 + const data = await getStudentContactPage({ studentId: props.id, pageNo, pageSize })
  244 + studentContactTotal.value = data.total
  245 + studentContactPagingRef.value?.completeByTotal(data.list, data.total)
  246 + } catch {
  247 + studentContactPagingRef.value?.complete(false)
  248 + }
  249 +}
  250 +
  251 +/** 打开学生联系人表单 */
  252 +function openStudentContact(item?: StudentContact) {
  253 + const query = [
  254 + `studentId=${props.id}`,
  255 + item?.id ? `id=${item.id}` : '',
  256 + ].filter(Boolean).join('&')
  257 + uni.navigateTo({ url: `/pages-infra/demo/student-contact/form/index?${query}` })
  258 +}
  259 +
  260 +/** 删除学生联系人 */
  261 +async function removeStudentContact(item: StudentContact) {
  262 + try {
  263 + await dialog.confirm({ title: '提示', msg: '确定要删除该学生联系人吗?' })
  264 + } catch {
  265 + return
  266 + }
  267 + await deleteStudentContact(item.id!)
  268 + toast.success('删除成功')
  269 + studentContactPagingRef.value?.reload()
  270 +}
  271 +
  272 +/** 查询学生班主任列表 */
  273 +async function queryStudentTeacherList(pageNo: number, pageSize: number) {
  274 + if (!props.id) {
  275 + studentTeacherPagingRef.value?.completeByTotal([], 0)
  276 + return
  277 + }
  278 + try {
  279 + const data = await getStudentTeacherPage({ studentId: props.id, pageNo, pageSize })
  280 + studentTeacherTotal.value = data.total
  281 + studentTeacherPagingRef.value?.completeByTotal(data.list, data.total)
  282 + } catch {
  283 + studentTeacherPagingRef.value?.complete(false)
  284 + }
  285 +}
  286 +
  287 +/** 打开学生班主任表单 */
  288 +function openStudentTeacher(item?: StudentTeacher) {
  289 + const query = [
  290 + `studentId=${props.id}`,
  291 + item?.id ? `id=${item.id}` : '',
  292 + ].filter(Boolean).join('&')
  293 + uni.navigateTo({ url: `/pages-infra/demo/student-teacher/form/index?${query}` })
  294 +}
  295 +
  296 +/** 删除学生班主任 */
  297 +async function removeStudentTeacher(item: StudentTeacher) {
  298 + try {
  299 + await dialog.confirm({ title: '提示', msg: '确定要删除该学生班主任吗?' })
  300 + } catch {
  301 + return
  302 + }
  303 + await deleteStudentTeacher(item.id!)
  304 + toast.success('删除成功')
  305 + studentTeacherPagingRef.value?.reload()
  306 +}
  307 +
  308 +/** 编辑主表 */
  309 +function handleEdit() {
  310 + uni.navigateTo({ url: `/pages-infra/demo/form/index?id=${props.id}` })
  311 +}
  312 +
  313 +/** 删除主表 */
  314 +async function handleDelete() {
  315 + if (!props.id) {
  316 + return
  317 + }
  318 + try {
  319 + await dialog.confirm({ title: '提示', msg: '确定要删除该学生吗?' })
  320 + } catch {
  321 + return
  322 + }
  323 + deleting.value = true
  324 + try {
  325 + await deleteStudent(props.id)
  326 + toast.success('删除成功')
  327 + uni.$emit('infra:demo:reload')
  328 + delay(handleBack)
  329 + } finally {
  330 + deleting.value = false
  331 + }
  332 +}
  333 +
  334 +/** 进入页面加载(含编辑和子表返回后刷新) */
  335 +onShow(() => {
  336 + loadMain()
  337 + nextTick(() => {
  338 + studentContactPagingRef.value?.reload()
  339 + studentTeacherPagingRef.value?.reload()
  340 + })
  341 +})
  342 +</script>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/form/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + :title="getTitle"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <!-- 表单区域 -->
  11 + <view>
  12 + <wd-form ref="formRef" :model="formData" :schema="formSchema">
  13 + <wd-cell-group border>
  14 + <wd-form-item title="名字" title-width="180rpx" prop="name">
  15 + <wd-input
  16 + v-model="formData.name"
  17 + clearable
  18 + placeholder="请输入名字"
  19 + />
  20 + </wd-form-item>
  21 + <wd-form-item title="简介" title-width="180rpx" prop="description">
  22 + <wd-textarea
  23 + v-model="formData.description"
  24 + clearable
  25 + placeholder="请输入简介"
  26 + :maxlength="200"
  27 + show-word-limit
  28 + />
  29 + </wd-form-item>
  30 + <wd-form-item
  31 + title="出生日期"
  32 + title-width="180rpx"
  33 + prop="birthday"
  34 + is-link
  35 + :value="formData.birthday ? formatDateTime(formData.birthday) : ''"
  36 + placeholder="请选择出生日期"
  37 + @click="birthdayVisible = true"
  38 + />
  39 + <wd-datetime-picker
  40 + v-model:visible="birthdayVisible"
  41 + :model-value="formData.birthday || Date.now()"
  42 + type="datetime"
  43 + @update:model-value="value => formData.birthday = value"
  44 + />
  45 + <yd-form-picker
  46 + v-model="formData.sex"
  47 + label="性别"
  48 + label-width="180rpx"
  49 + prop="sex"
  50 + :dict-type="DICT_TYPE.SYSTEM_USER_SEX"
  51 + placeholder="请选择性别"
  52 + />
  53 + <wd-form-item title="是否有效" title-width="180rpx" prop="enabled" center>
  54 + <wd-radio-group v-model="formData.enabled" type="button">
  55 + <wd-radio
  56 + v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  57 + :key="dict.value"
  58 + :value="dict.value"
  59 + >
  60 + {{ dict.label }}
  61 + </wd-radio>
  62 + </wd-radio-group>
  63 + </wd-form-item>
  64 + <wd-form-item title="头像" title-width="180rpx" prop="avatar">
  65 + <yd-upload-img v-model="formData.avatar" directory="infra/demo" />
  66 + </wd-form-item>
  67 + <wd-form-item title="附件" title-width="180rpx" prop="video">
  68 + <yd-upload-file v-model="formData.video" directory="infra/demo" />
  69 + </wd-form-item>
  70 + <wd-form-item title="备注" title-width="180rpx" prop="memo">
  71 + <wd-input
  72 + v-model="formData.memo"
  73 + clearable
  74 + placeholder="请输入备注"
  75 + />
  76 + </wd-form-item>
  77 + </wd-cell-group>
  78 + </wd-form>
  79 + </view>
  80 +
  81 + <!-- 底部保存按钮 -->
  82 + <view class="yd-detail-footer">
  83 + <wd-button
  84 + type="primary"
  85 + block
  86 + :loading="formLoading"
  87 + @click="handleSubmit"
  88 + >
  89 + 保存
  90 + </wd-button>
  91 + </view>
  92 + </view>
  93 +</template>
  94 +
  95 +<script lang="ts" setup>
  96 +import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
  97 +import type {
  98 + Student,
  99 +} from '@/api/infra/demo'
  100 +import { computed, onMounted, ref } from 'vue'
  101 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  102 +import { createStudent, getStudent, updateStudent } from '@/api/infra/demo'
  103 +import { getBoolDictOptions } from '@/hooks/useDict'
  104 +import { delay, navigateBackPlus } from '@/utils'
  105 +import { DICT_TYPE } from '@/utils/constants'
  106 +import { formatDateTime } from '@/utils/date'
  107 +import { createFormSchema } from '@/utils/wot'
  108 +
  109 +const props = defineProps<{
  110 + id?: number
  111 +}>()
  112 +
  113 +definePage({
  114 + style: {
  115 + navigationBarTitleText: '',
  116 + navigationStyle: 'custom',
  117 + },
  118 +})
  119 +
  120 +const toast = useToast()
  121 +const getTitle = computed(() => props.id ? '编辑学生' : '新增学生')
  122 +const formLoading = ref(false) // 表单提交状态
  123 +const formData = ref<Student>({
  124 + id: undefined,
  125 + name: '',
  126 + description: '',
  127 + birthday: 0,
  128 + sex: 0,
  129 + enabled: false,
  130 + avatar: '',
  131 + video: '',
  132 + memo: '',
  133 +}) // 表单数据
  134 +const birthdayVisible = ref(false) // 出生日期选择器状态
  135 +const formSchema = createFormSchema({
  136 + name: [{ required: true, message: '名字不能为空' }],
  137 + description: [{ required: true, message: '简介不能为空' }],
  138 + birthday: [{ required: true, message: '出生日期不能为空' }],
  139 + sex: [{ required: true, message: '性别不能为空' }],
  140 + enabled: [{ required: true, message: '是否有效不能为空' }],
  141 + avatar: [{ required: true, message: '头像不能为空' }],
  142 + video: [{ required: true, message: '附件不能为空' }],
  143 + memo: [{ required: true, message: '备注不能为空' }],
  144 +}) // 表单校验规则
  145 +const formRef = ref<FormInstance>() // 表单组件引用
  146 +
  147 +/** 返回上一页 */
  148 +function handleBack() {
  149 + navigateBackPlus('/pages-infra/demo/index')
  150 +}
  151 +
  152 +/** 加载学生详情 */
  153 +async function getDetail() {
  154 + if (!props.id) {
  155 + return
  156 + }
  157 + formData.value = await getStudent(props.id)
  158 +}
  159 +
  160 +/** 提交表单 */
  161 +async function handleSubmit() {
  162 + const { valid } = await formRef.value.validate()
  163 + if (!valid) {
  164 + return
  165 + }
  166 +
  167 + formLoading.value = true
  168 + try {
  169 + const data = formData.value
  170 + if (props.id) {
  171 + await updateStudent(data)
  172 + toast.success('修改成功')
  173 + } else {
  174 + await createStudent(data)
  175 + toast.success('新增成功')
  176 + }
  177 + uni.$emit('infra:demo:reload')
  178 + delay(handleBack)
  179 + } finally {
  180 + formLoading.value = false
  181 + }
  182 +}
  183 +
  184 +/** 初始化 */
  185 +onMounted(async () => {
  186 + getDetail()
  187 +})
  188 +</script>
  189 +
  190 +<style lang="scss" scoped>
  191 +</style>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container yd-page-container-paging">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + title="学生管理"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <!-- 搜索组件 -->
  11 + <SearchForm @search="handleQuery" @reset="handleReset" />
  12 +
  13 + <!-- 学生列表 -->
  14 + <z-paging
  15 + ref="pagingRef"
  16 + v-model="list"
  17 + :fixed="false"
  18 + class="min-h-0 flex-1"
  19 + :default-page-size="10"
  20 + :refresher-enabled="true"
  21 + :inside-more="true"
  22 + :loading-more-default-as-loading="true"
  23 + empty-view-text="暂无学生数据"
  24 + @query="queryList"
  25 + >
  26 + <view class="p-24rpx">
  27 + <view
  28 + v-for="item in list"
  29 + :key="item.id"
  30 + class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
  31 + @click="handleDetail(item)"
  32 + >
  33 + <view class="p-24rpx">
  34 + <view class="mb-16rpx flex items-center justify-between">
  35 + <view class="text-32rpx text-[#333] font-semibold">
  36 + {{ item.name }}
  37 + </view>
  38 + <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="item.sex" />
  39 + </view>
  40 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  41 + <text class="mr-8rpx text-[#999]">简介:</text>
  42 + <text class="line-clamp-1">{{ item.description }}</text>
  43 + </view>
  44 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  45 + <text class="mr-8rpx text-[#999]">出生日期:</text>
  46 + <text class="line-clamp-1">{{ formatDateTime(item.birthday) || '-' }}</text>
  47 + </view>
  48 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  49 + <text class="mr-8rpx text-[#999]">是否有效:</text>
  50 + <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="item.enabled" />
  51 + </view>
  52 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  53 + <text class="mr-8rpx text-[#999]">头像:</text>
  54 + <text class="line-clamp-1">{{ item.avatar }}</text>
  55 + </view>
  56 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  57 + <text class="mr-8rpx text-[#999]">附件:</text>
  58 + <text class="line-clamp-1">{{ item.video }}</text>
  59 + </view>
  60 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  61 + <text class="mr-8rpx text-[#999]">备注:</text>
  62 + <text class="line-clamp-1">{{ item.memo }}</text>
  63 + </view>
  64 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  65 + <text class="mr-8rpx text-[#999]">创建时间:</text>
  66 + <text class="line-clamp-1">{{ formatDateTime(item.createTime) || '-' }}</text>
  67 + </view>
  68 + </view>
  69 + </view>
  70 + </view>
  71 + </z-paging>
  72 +
  73 + <!-- 新增按钮 -->
  74 + <wd-fab
  75 + v-if="hasAccessByCodes(['infra:student:create'])"
  76 + position="right-bottom"
  77 + type="primary"
  78 + :expandable="false"
  79 + @click="handleAdd"
  80 + />
  81 + </view>
  82 +</template>
  83 +
  84 +<script lang="ts" setup>
  85 +import type { Student } from '@/api/infra/demo'
  86 +import { onUnload } from '@dcloudio/uni-app'
  87 +import { onMounted, ref } from 'vue'
  88 +import { getStudentPage } from '@/api/infra/demo'
  89 +import { useAccess } from '@/hooks/useAccess'
  90 +import { navigateBackPlus } from '@/utils'
  91 +import { DICT_TYPE } from '@/utils/constants'
  92 +import { formatDateTime } from '@/utils/date'
  93 +import SearchForm from './components/search-form.vue'
  94 +
  95 +definePage({
  96 + style: {
  97 + navigationBarTitleText: '',
  98 + navigationStyle: 'custom',
  99 + },
  100 +})
  101 +
  102 +const { hasAccessByCodes } = useAccess()
  103 +const list = ref<Student[]>([]) // 列表数据
  104 +const pagingRef = ref<any>() // 分页组件引用
  105 +const queryParams = ref<Record<string, any>>({}) // 查询参数
  106 +
  107 +/** 返回上一页 */
  108 +function handleBack() {
  109 + navigateBackPlus()
  110 +}
  111 +
  112 +/** 查询学生列表 */
  113 +async function queryList(pageNo: number, pageSize: number) {
  114 + try {
  115 + const params = {
  116 + ...queryParams.value,
  117 + pageNo,
  118 + pageSize,
  119 + }
  120 + const data = await getStudentPage(params)
  121 + pagingRef.value?.completeByTotal(data.list, data.total)
  122 + } catch {
  123 + pagingRef.value?.complete(false)
  124 + }
  125 +}
  126 +
  127 +/** 搜索按钮操作 */
  128 +function handleQuery(data?: Record<string, any>) {
  129 + queryParams.value = { ...data }
  130 + reload()
  131 +}
  132 +
  133 +/** 重置按钮操作 */
  134 +function handleReset() {
  135 + handleQuery()
  136 +}
  137 +
  138 +/** 重新加载 */
  139 +function reload() {
  140 + pagingRef.value?.reload()
  141 +}
  142 +
  143 +/** 新增学生 */
  144 +function handleAdd() {
  145 + uni.navigateTo({
  146 + url: '/pages-infra/demo/form/index',
  147 + })
  148 +}
  149 +
  150 +/** 查看详情 */
  151 +function handleDetail(item: Student) {
  152 + uni.navigateTo({
  153 + url: `/pages-infra/demo/detail/index?id=${item.id}`,
  154 + })
  155 +}
  156 +
  157 +/** 初始化 */
  158 +onMounted(() => {
  159 + uni.$on('infra:demo:reload', reload)
  160 +})
  161 +
  162 +/** 卸载 */
  163 +onUnload(() => {
  164 + uni.$off('infra:demo:reload', reload)
  165 +})
  166 +</script>
  167 +
  168 +<style lang="scss" scoped>
  169 +</style>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/search-form 0 → 100644
  1 +<template>
  2 + <!-- 搜索框入口 -->
  3 + <view @click="visible = true">
  4 + <wd-search :placeholder="placeholder" hide-cancel disabled />
  5 + </view>
  6 +
  7 + <!-- 搜索弹窗 -->
  8 + <wd-popup v-model="visible" position="top" @close="visible = false">
  9 + <view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
  10 + <view class="yd-search-form-item">
  11 + <view class="yd-search-form-label">
  12 + 名字
  13 + </view>
  14 + <wd-input
  15 + v-model="formData.name"
  16 + placeholder="请输入名字"
  17 + clearable
  18 + />
  19 + </view>
  20 + <view class="yd-search-form-item">
  21 + <view class="yd-search-form-label">
  22 + 出生日期
  23 + </view>
  24 + <wd-input
  25 + v-model="formData.birthday"
  26 + placeholder="请输入出生日期"
  27 + clearable
  28 + />
  29 + </view>
  30 + <yd-search-picker
  31 + v-model="formData.sex"
  32 + label="性别"
  33 + :columns="getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
  34 + all-option
  35 + />
  36 + <yd-search-picker
  37 + v-model="formData.enabled"
  38 + label="是否有效"
  39 + :columns="getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  40 + all-option
  41 + />
  42 + <yd-search-date-range v-model="formData.createTime" label="创建时间" />
  43 + <view class="yd-search-form-actions">
  44 + <wd-button class="flex-1" plain @click="handleReset">
  45 + 重置
  46 + </wd-button>
  47 + <wd-button class="flex-1" type="primary" @click="handleSearch">
  48 + 搜索
  49 + </wd-button>
  50 + </view>
  51 + </view>
  52 + </wd-popup>
  53 +</template>
  54 +
  55 +<script lang="ts" setup>
  56 +import { computed, reactive, ref } from 'vue'
  57 +import { getNavbarHeight } from '@/utils'
  58 +import { formatDate, formatDateRange } from '@/utils/date'
  59 +import { getDictLabel, getIntDictOptions, getBoolDictOptions } from '@/hooks/useDict'
  60 +import { DICT_TYPE } from '@/utils/constants'
  61 +
  62 +const emit = defineEmits<{
  63 + search: [data: Record<string, any>]
  64 + reset: []
  65 +}>()
  66 +
  67 +const visible = ref(false) // 搜索弹窗显示状态
  68 +const formData = reactive({
  69 + name: undefined as string | undefined,
  70 + birthday: undefined as string | undefined,
  71 + sex: undefined as number | undefined,
  72 + enabled: undefined as boolean | undefined,
  73 + createTime: [undefined, undefined] as [number | undefined, number | undefined],
  74 +}) // 搜索表单数据
  75 +
  76 +/** 搜索条件 placeholder 拼接 */
  77 +const placeholder = computed(() => {
  78 + const conditions: string[] = []
  79 + if (formData.name) {
  80 + conditions.push(`名字:${formData.name}`)
  81 + }
  82 + if (formData.birthday) {
  83 + conditions.push(`出生日期:${formData.birthday}`)
  84 + }
  85 + if (formData.sex !== undefined) {
  86 + conditions.push(`性别:${getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, formData.sex)}`)
  87 + }
  88 + if (formData.enabled !== undefined) {
  89 + conditions.push(`是否有效:${getDictLabel(DICT_TYPE.INFRA_BOOLEAN_STRING, formData.enabled)}`)
  90 + }
  91 + if (formData.createTime?.[0] && formData.createTime?.[1]) {
  92 + conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
  93 + }
  94 + return conditions.length > 0 ? conditions.join(' | ') : '搜索学生'
  95 +})
  96 +
  97 +/** 搜索按钮操作 */
  98 +function handleSearch() {
  99 + visible.value = false
  100 + emit('search', {
  101 + ...formData,
  102 + createTime: formatDateRange(formData.createTime),
  103 + })
  104 +}
  105 +
  106 +/** 重置按钮操作 */
  107 +function handleReset() {
  108 + formData.name = undefined
  109 + formData.birthday = undefined
  110 + formData.sex = undefined
  111 + formData.enabled = undefined
  112 + formData.createTime = [undefined, undefined]
  113 + visible.value = false
  114 + emit('reset')
  115 +}
  116 +</script>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-contact/form/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar :title="getTitle" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
  5 +
  6 + <!-- 表单区域 -->
  7 + <wd-form ref="formRef" :model="formData" :schema="formSchema">
  8 + <wd-cell-group border>
  9 + <wd-form-item title="名字" title-width="180rpx" prop="name">
  10 + <wd-input v-model="formData.name" clearable placeholder="请输入名字" />
  11 + </wd-form-item>
  12 + <wd-form-item title="简介" title-width="180rpx" prop="description">
  13 + <wd-textarea v-model="formData.description" clearable placeholder="请输入简介" />
  14 + </wd-form-item>
  15 + <wd-form-item
  16 + title="出生日期"
  17 + title-width="180rpx"
  18 + prop="birthday"
  19 + is-link
  20 + :value="formData.birthday ? formatDateTime(formData.birthday) : ''"
  21 + placeholder="请选择出生日期"
  22 + @click="birthdayVisible = true"
  23 + />
  24 + <wd-datetime-picker
  25 + v-model:visible="birthdayVisible"
  26 + :model-value="formData.birthday || Date.now()"
  27 + type="datetime"
  28 + @update:model-value="value => formData.birthday = value"
  29 + />
  30 + <yd-form-picker
  31 + v-model="formData.sex"
  32 + label="性别"
  33 + label-width="180rpx"
  34 + prop="sex"
  35 + :dict-type="DICT_TYPE.SYSTEM_USER_SEX"
  36 + placeholder="请选择性别"
  37 + />
  38 + <yd-form-picker
  39 + v-model="formData.enabled"
  40 + label="是否有效"
  41 + label-width="180rpx"
  42 + prop="enabled"
  43 + :dict-type="DICT_TYPE.INFRA_BOOLEAN_STRING"
  44 + placeholder="请选择是否有效"
  45 + />
  46 + <wd-form-item title="头像" title-width="180rpx" prop="avatar">
  47 + <yd-upload-img v-model="formData.avatar" directory="infra/demo" />
  48 + </wd-form-item>
  49 + <wd-form-item title="附件" title-width="180rpx" prop="video">
  50 + <yd-upload-file v-model="formData.video" directory="infra/demo" />
  51 + </wd-form-item>
  52 + <wd-form-item title="备注" title-width="180rpx" prop="memo">
  53 + <wd-input v-model="formData.memo" clearable placeholder="请输入备注" />
  54 + </wd-form-item>
  55 + </wd-cell-group>
  56 + </wd-form>
  57 +
  58 + <!-- 底部保存按钮 -->
  59 + <view class="yd-detail-footer">
  60 + <wd-button type="primary" block :loading="formLoading" @click="handleSubmit">
  61 + 保存
  62 + </wd-button>
  63 + </view>
  64 + </view>
  65 +</template>
  66 +
  67 +<script lang="ts" setup>
  68 +import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
  69 +import type { StudentContact } from '@/api/infra/demo'
  70 +import { computed, onMounted, ref } from 'vue'
  71 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  72 +import { createStudentContact, getStudentContact, updateStudentContact } from '@/api/infra/demo'
  73 +import { delay, navigateBackPlus } from '@/utils'
  74 +import { DICT_TYPE } from '@/utils/constants'
  75 +import { formatDateTime } from '@/utils/date'
  76 +import { createFormSchema } from '@/utils/wot'
  77 +
  78 +const props = defineProps<{
  79 + id?: number
  80 + studentId: number
  81 +}>()
  82 +
  83 +definePage({
  84 + style: { navigationBarTitleText: '', navigationStyle: 'custom' },
  85 +})
  86 +
  87 +const toast = useToast()
  88 +const getTitle = computed(() => props.id ? '编辑学生联系人' : '新增学生联系人')
  89 +const formLoading = ref(false) // 表单提交状态
  90 +const formData = ref<StudentContact>({
  91 + id: undefined,
  92 + studentId: props.studentId,
  93 + name: '',
  94 + description: '',
  95 + birthday: 0,
  96 + sex: 0,
  97 + enabled: false,
  98 + avatar: '',
  99 + video: undefined,
  100 + memo: '',
  101 +}) // 表单数据
  102 +const birthdayVisible = ref(false) // 出生日期选择器状态
  103 +const formSchema = createFormSchema({
  104 + studentId: [{ required: true, message: '学生编号不能为空' }],
  105 + name: [{ required: true, message: '名字不能为空' }],
  106 + description: [{ required: true, message: '简介不能为空' }],
  107 + birthday: [{ required: true, message: '出生日期不能为空' }],
  108 + sex: [{ required: true, message: '性别不能为空' }],
  109 + enabled: [{ required: true, message: '是否有效不能为空' }],
  110 + avatar: [{ required: true, message: '头像不能为空' }],
  111 + memo: [{ required: true, message: '备注不能为空' }],
  112 +}) // 表单校验规则
  113 +const formRef = ref<FormInstance>() // 表单组件引用
  114 +
  115 +/** 返回上一页 */
  116 +function handleBack() {
  117 + navigateBackPlus()
  118 +}
  119 +
  120 +/** 加载学生联系人详情 */
  121 +async function getDetail() {
  122 + if (props.id) {
  123 + formData.value = await getStudentContact(props.id)
  124 + }
  125 +}
  126 +
  127 +/** 提交表单 */
  128 +async function handleSubmit() {
  129 + const { valid } = await formRef.value.validate()
  130 + if (!valid) {
  131 + return
  132 + }
  133 + formLoading.value = true
  134 + try {
  135 + const data = { ...formData.value, studentId: props.studentId ?? formData.value.studentId }
  136 + if (props.id) {
  137 + await updateStudentContact(data)
  138 + toast.success('修改成功')
  139 + } else {
  140 + await createStudentContact(data)
  141 + toast.success('新增成功')
  142 + }
  143 + delay(handleBack)
  144 + } finally {
  145 + formLoading.value = false
  146 + }
  147 +}
  148 +
  149 +/** 初始化 */
  150 +onMounted(() => {
  151 + getDetail()
  152 +})
  153 +</script>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/vue/student-teacher/form/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar :title="getTitle" left-arrow placeholder safe-area-inset-top fixed @click-left="handleBack" />
  5 +
  6 + <!-- 表单区域 -->
  7 + <wd-form ref="formRef" :model="formData" :schema="formSchema">
  8 + <wd-cell-group border>
  9 + <wd-form-item title="名字" title-width="180rpx" prop="name">
  10 + <wd-input v-model="formData.name" clearable placeholder="请输入名字" />
  11 + </wd-form-item>
  12 + <wd-form-item title="简介" title-width="180rpx" prop="description">
  13 + <wd-textarea v-model="formData.description" clearable placeholder="请输入简介" />
  14 + </wd-form-item>
  15 + <wd-form-item
  16 + title="出生日期"
  17 + title-width="180rpx"
  18 + prop="birthday"
  19 + is-link
  20 + :value="formData.birthday ? formatDateTime(formData.birthday) : ''"
  21 + placeholder="请选择出生日期"
  22 + @click="birthdayVisible = true"
  23 + />
  24 + <wd-datetime-picker
  25 + v-model:visible="birthdayVisible"
  26 + :model-value="formData.birthday || Date.now()"
  27 + type="datetime"
  28 + @update:model-value="value => formData.birthday = value"
  29 + />
  30 + <yd-form-picker
  31 + v-model="formData.sex"
  32 + label="性别"
  33 + label-width="180rpx"
  34 + prop="sex"
  35 + :dict-type="DICT_TYPE.SYSTEM_USER_SEX"
  36 + placeholder="请选择性别"
  37 + />
  38 + <yd-form-picker
  39 + v-model="formData.enabled"
  40 + label="是否有效"
  41 + label-width="180rpx"
  42 + prop="enabled"
  43 + :dict-type="DICT_TYPE.INFRA_BOOLEAN_STRING"
  44 + placeholder="请选择是否有效"
  45 + />
  46 + <wd-form-item title="头像" title-width="180rpx" prop="avatar">
  47 + <yd-upload-img v-model="formData.avatar" directory="infra/demo" />
  48 + </wd-form-item>
  49 + <wd-form-item title="附件" title-width="180rpx" prop="video">
  50 + <yd-upload-file v-model="formData.video" directory="infra/demo" />
  51 + </wd-form-item>
  52 + <wd-form-item title="备注" title-width="180rpx" prop="memo">
  53 + <wd-input v-model="formData.memo" clearable placeholder="请输入备注" />
  54 + </wd-form-item>
  55 + </wd-cell-group>
  56 + </wd-form>
  57 +
  58 + <!-- 底部保存按钮 -->
  59 + <view class="yd-detail-footer">
  60 + <wd-button type="primary" block :loading="formLoading" @click="handleSubmit">
  61 + 保存
  62 + </wd-button>
  63 + </view>
  64 + </view>
  65 +</template>
  66 +
  67 +<script lang="ts" setup>
  68 +import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
  69 +import type { StudentTeacher } from '@/api/infra/demo'
  70 +import { computed, onMounted, ref } from 'vue'
  71 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  72 +import { createStudentTeacher, getStudentTeacher, updateStudentTeacher } from '@/api/infra/demo'
  73 +import { delay, navigateBackPlus } from '@/utils'
  74 +import { DICT_TYPE } from '@/utils/constants'
  75 +import { formatDateTime } from '@/utils/date'
  76 +import { createFormSchema } from '@/utils/wot'
  77 +
  78 +const props = defineProps<{
  79 + id?: number
  80 + studentId: number
  81 +}>()
  82 +
  83 +definePage({
  84 + style: { navigationBarTitleText: '', navigationStyle: 'custom' },
  85 +})
  86 +
  87 +const toast = useToast()
  88 +const getTitle = computed(() => props.id ? '编辑学生班主任' : '新增学生班主任')
  89 +const formLoading = ref(false) // 表单提交状态
  90 +const formData = ref<StudentTeacher>({
  91 + id: undefined,
  92 + studentId: props.studentId,
  93 + name: '',
  94 + description: '',
  95 + birthday: 0,
  96 + sex: 0,
  97 + enabled: false,
  98 + avatar: '',
  99 + video: undefined,
  100 + memo: '',
  101 +}) // 表单数据
  102 +const birthdayVisible = ref(false) // 出生日期选择器状态
  103 +const formSchema = createFormSchema({
  104 + studentId: [{ required: true, message: '学生编号不能为空' }],
  105 + name: [{ required: true, message: '名字不能为空' }],
  106 + description: [{ required: true, message: '简介不能为空' }],
  107 + birthday: [{ required: true, message: '出生日期不能为空' }],
  108 + sex: [{ required: true, message: '性别不能为空' }],
  109 + enabled: [{ required: true, message: '是否有效不能为空' }],
  110 + avatar: [{ required: true, message: '头像不能为空' }],
  111 + memo: [{ required: true, message: '备注不能为空' }],
  112 +}) // 表单校验规则
  113 +const formRef = ref<FormInstance>() // 表单组件引用
  114 +
  115 +/** 返回上一页 */
  116 +function handleBack() {
  117 + navigateBackPlus()
  118 +}
  119 +
  120 +/** 加载学生班主任详情 */
  121 +async function getDetail() {
  122 + if (props.id) {
  123 + formData.value = await getStudentTeacher(props.id)
  124 + }
  125 +}
  126 +
  127 +/** 提交表单 */
  128 +async function handleSubmit() {
  129 + const { valid } = await formRef.value.validate()
  130 + if (!valid) {
  131 + return
  132 + }
  133 + formLoading.value = true
  134 + try {
  135 + const data = { ...formData.value, studentId: props.studentId ?? formData.value.studentId }
  136 + if (props.id) {
  137 + await updateStudentTeacher(data)
  138 + toast.success('修改成功')
  139 + } else {
  140 + await createStudentTeacher(data)
  141 + toast.success('新增成功')
  142 + }
  143 + delay(handleBack)
  144 + } finally {
  145 + formLoading.value = false
  146 + }
  147 +}
  148 +
  149 +/** 初始化 */
  150 +onMounted(() => {
  151 + getDetail()
  152 +})
  153 +</script>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_erp/xml/InfraStudentMapper 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3 +<mapper namespace="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper">
  4 +
  5 + <!--
  6 + 一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
  7 + 无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
  8 + 代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
  9 + 文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
  10 + -->
  11 +
  12 +</mapper>
0 13 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/assert.json 0 → 100644
  1 +[ {
  2 + "contentPath" : "java/InfraStudentPageReqVO",
  3 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
  4 +}, {
  5 + "contentPath" : "java/InfraStudentRespVO",
  6 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
  7 +}, {
  8 + "contentPath" : "java/InfraStudentSaveReqVO",
  9 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
  10 +}, {
  11 + "contentPath" : "java/InfraStudentController",
  12 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
  13 +}, {
  14 + "contentPath" : "java/InfraStudentDO",
  15 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
  16 +}, {
  17 + "contentPath" : "java/InfraStudentContactDO",
  18 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
  19 +}, {
  20 + "contentPath" : "java/InfraStudentTeacherDO",
  21 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
  22 +}, {
  23 + "contentPath" : "java/InfraStudentMapper",
  24 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
  25 +}, {
  26 + "contentPath" : "java/InfraStudentContactMapper",
  27 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
  28 +}, {
  29 + "contentPath" : "java/InfraStudentTeacherMapper",
  30 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
  31 +}, {
  32 + "contentPath" : "xml/InfraStudentMapper",
  33 + "filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml"
  34 +}, {
  35 + "contentPath" : "java/InfraStudentServiceImpl",
  36 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
  37 +}, {
  38 + "contentPath" : "java/InfraStudentService",
  39 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
  40 +}, {
  41 + "contentPath" : "java/InfraStudentServiceImplTest",
  42 + "filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
  43 +}, {
  44 + "contentPath" : "java/ErrorCodeConstants_手动操作",
  45 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
  46 +}, {
  47 + "contentPath" : "sql/sql",
  48 + "filePath" : "sql/sql.sql"
  49 +}, {
  50 + "contentPath" : "sql/h2",
  51 + "filePath" : "sql/h2.sql"
  52 +}, {
  53 + "contentPath" : "ts/index",
  54 + "filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
  55 +}, {
  56 + "contentPath" : "vue/index",
  57 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
  58 +}, {
  59 + "contentPath" : "vue/search-form",
  60 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
  61 +}, {
  62 + "contentPath" : "vue/form/index",
  63 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
  64 +}, {
  65 + "contentPath" : "vue/detail/index",
  66 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"
  67 +} ]
0 68 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/ErrorCodeConstants_手动操作 0 → 100644
  1 +// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
  2 +// ========== 学生 TODO 补充编号 ==========
  3 +ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
0 4 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactDO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import java.time.LocalDateTime;
  6 +import java.time.LocalDateTime;
  7 +import com.baomidou.mybatisplus.annotation.*;
  8 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  9 +
  10 +/**
  11 + * 学生联系人 DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("infra_student_contact")
  16 +@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class InfraStudentContactDO extends BaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 学生编号
  32 + */
  33 + private Long studentId;
  34 + /**
  35 + * 名字
  36 + */
  37 + private String name;
  38 + /**
  39 + * 简介
  40 + */
  41 + private String description;
  42 + /**
  43 + * 出生日期
  44 + */
  45 + private LocalDateTime birthday;
  46 + /**
  47 + * 性别
  48 + *
  49 + * 枚举 {@link TODO system_user_sex 对应的类}
  50 + */
  51 + private Integer sex;
  52 + /**
  53 + * 是否有效
  54 + *
  55 + * 枚举 {@link TODO infra_boolean_string 对应的类}
  56 + */
  57 + private Boolean enabled;
  58 + /**
  59 + * 头像
  60 + */
  61 + private String avatar;
  62 + /**
  63 + * 附件
  64 + */
  65 + private String video;
  66 + /**
  67 + * 备注
  68 + */
  69 + private String memo;
  70 +
  71 +}
0 72 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentContactMapper 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.mysql.demo;
  2 +
  3 +import java.util.*;
  4 +
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  8 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  9 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  10 +import org.apache.ibatis.annotations.Mapper;
  11 +
  12 +/**
  13 + * 学生联系人 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
  19 +
  20 + default List<InfraStudentContactDO> selectListByStudentId(Long studentId) {
  21 + return selectList(InfraStudentContactDO::getStudentId, studentId);
  22 + }
  23 +
  24 + default int deleteByStudentId(Long studentId) {
  25 + return delete(InfraStudentContactDO::getStudentId, studentId);
  26 + }
  27 +
  28 + default int deleteByStudentIds(List<Long> studentIds) {
  29 + return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
  30 + }
  31 +
  32 +}
0 33 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentController 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo;
  2 +
  3 +import org.springframework.web.bind.annotation.*;
  4 +import jakarta.annotation.Resource;
  5 +import org.springframework.validation.annotation.Validated;
  6 +import org.springframework.security.access.prepost.PreAuthorize;
  7 +import io.swagger.v3.oas.annotations.tags.Tag;
  8 +import io.swagger.v3.oas.annotations.Parameter;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +
  11 +import jakarta.validation.constraints.*;
  12 +import jakarta.validation.*;
  13 +import jakarta.servlet.http.*;
  14 +import java.util.*;
  15 +import java.io.IOException;
  16 +
  17 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  18 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  19 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  20 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  21 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  22 +
  23 +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
  24 +
  25 +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
  26 +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
  27 +
  28 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  29 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  30 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  31 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  32 +import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
  33 +
  34 +@Tag(name = "管理后台 - 学生")
  35 +@RestController
  36 +@RequestMapping("/infra/student")
  37 +@Validated
  38 +public class InfraStudentController {
  39 +
  40 + @Resource
  41 + private InfraStudentService studentService;
  42 +
  43 + @PostMapping("/create")
  44 + @Operation(summary = "创建学生")
  45 + @PreAuthorize("@ss.hasPermission('infra:student:create')")
  46 + public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
  47 + return success(studentService.createStudent(createReqVO));
  48 + }
  49 +
  50 + @PutMapping("/update")
  51 + @Operation(summary = "更新学生")
  52 + @PreAuthorize("@ss.hasPermission('infra:student:update')")
  53 + public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
  54 + studentService.updateStudent(updateReqVO);
  55 + return success(true);
  56 + }
  57 +
  58 + @DeleteMapping("/delete")
  59 + @Operation(summary = "删除学生")
  60 + @Parameter(name = "id", description = "编号", required = true)
  61 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  62 + public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
  63 + studentService.deleteStudent(id);
  64 + return success(true);
  65 + }
  66 +
  67 + @DeleteMapping("/delete-list")
  68 + @Parameter(name = "ids", description = "编号", required = true)
  69 + @Operation(summary = "批量删除学生")
  70 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  71 + public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
  72 + studentService.deleteStudentListByIds(ids);
  73 + return success(true);
  74 + }
  75 +
  76 + @GetMapping("/get")
  77 + @Operation(summary = "获得学生")
  78 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  79 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  80 + public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
  81 + InfraStudentDO student = studentService.getStudent(id);
  82 + return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
  83 + }
  84 +
  85 + @GetMapping("/page")
  86 + @Operation(summary = "获得学生分页")
  87 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  88 + public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
  89 + PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
  90 + return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
  91 + }
  92 +
  93 + @GetMapping("/export-excel")
  94 + @Operation(summary = "导出学生 Excel")
  95 + @PreAuthorize("@ss.hasPermission('infra:student:export')")
  96 + @ApiAccessLog(operateType = EXPORT)
  97 + public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
  98 + HttpServletResponse response) throws IOException {
  99 + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
  100 + List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
  101 + // 导出 Excel
  102 + ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
  103 + BeanUtils.toBean(list, InfraStudentRespVO.class));
  104 + }
  105 +
  106 + // ==================== 子表(学生联系人) ====================
  107 +
  108 + @GetMapping("/student-contact/list-by-student-id")
  109 + @Operation(summary = "获得学生联系人列表")
  110 + @Parameter(name = "studentId", description = "学生编号")
  111 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  112 + public CommonResult<List<InfraStudentContactDO>> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) {
  113 + return success(studentService.getStudentContactListByStudentId(studentId));
  114 + }
  115 +
  116 + // ==================== 子表(学生班主任) ====================
  117 +
  118 + @GetMapping("/student-teacher/get-by-student-id")
  119 + @Operation(summary = "获得学生班主任")
  120 + @Parameter(name = "studentId", description = "学生编号")
  121 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  122 + public CommonResult<InfraStudentTeacherDO> getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) {
  123 + return success(studentService.getStudentTeacherByStudentId(studentId));
  124 + }
  125 +
  126 +}
0 127 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentDO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import java.time.LocalDateTime;
  6 +import java.time.LocalDateTime;
  7 +import com.baomidou.mybatisplus.annotation.*;
  8 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  9 +
  10 +/**
  11 + * 学生 DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("infra_student")
  16 +@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class InfraStudentDO extends BaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 名字
  32 + */
  33 + private String name;
  34 + /**
  35 + * 简介
  36 + */
  37 + private String description;
  38 + /**
  39 + * 出生日期
  40 + */
  41 + private LocalDateTime birthday;
  42 + /**
  43 + * 性别
  44 + *
  45 + * 枚举 {@link TODO system_user_sex 对应的类}
  46 + */
  47 + private Integer sex;
  48 + /**
  49 + * 是否有效
  50 + *
  51 + * 枚举 {@link TODO infra_boolean_string 对应的类}
  52 + */
  53 + private Boolean enabled;
  54 + /**
  55 + * 头像
  56 + */
  57 + private String avatar;
  58 + /**
  59 + * 附件
  60 + */
  61 + private String video;
  62 + /**
  63 + * 备注
  64 + */
  65 + private String memo;
  66 +
  67 +
  68 +}
0 69 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentMapper 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.mysql.demo;
  2 +
  3 +import java.util.*;
  4 +
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  7 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  8 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  9 +import org.apache.ibatis.annotations.Mapper;
  10 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  11 +
  12 +/**
  13 + * 学生 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
  19 +
  20 + default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
  21 + return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
  22 + .likeIfPresent(InfraStudentDO::getName, reqVO.getName())
  23 + .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
  24 + .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
  25 + .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
  26 + .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
  27 + .orderByDesc(InfraStudentDO::getId));
  28 + }
  29 +
  30 +}
0 31 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentPageReqVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +import java.time.LocalDateTime;
  9 +
  10 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  11 +
  12 +@Schema(description = "管理后台 - 学生分页 Request VO")
  13 +@Data
  14 +public class InfraStudentPageReqVO extends PageParam {
  15 +
  16 + @Schema(description = "名字", example = "芋头")
  17 + private String name;
  18 +
  19 + @Schema(description = "出生日期")
  20 + private LocalDateTime birthday;
  21 +
  22 + @Schema(description = "性别", example = "1")
  23 + private Integer sex;
  24 +
  25 + @Schema(description = "是否有效", example = "true")
  26 + private Boolean enabled;
  27 +
  28 + @Schema(description = "创建时间")
  29 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  30 + private LocalDateTime[] createTime;
  31 +
  32 +}
0 33 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentRespVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +import java.util.*;
  6 +import org.springframework.format.annotation.DateTimeFormat;
  7 +import java.time.LocalDateTime;
  8 +import cn.idev.excel.annotation.*;
  9 +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
  10 +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
  11 +
  12 +@Schema(description = "管理后台 - 学生 Response VO")
  13 +@Data
  14 +@ExcelIgnoreUnannotated
  15 +public class InfraStudentRespVO {
  16 +
  17 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  18 + @ExcelProperty("编号")
  19 + private Long id;
  20 +
  21 + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
  22 + @ExcelProperty("名字")
  23 + private String name;
  24 +
  25 + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
  26 + @ExcelProperty("简介")
  27 + private String description;
  28 +
  29 + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
  30 + @ExcelProperty("出生日期")
  31 + private LocalDateTime birthday;
  32 +
  33 + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  34 + @ExcelProperty(value = "性别", converter = DictConvert.class)
  35 + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
  36 + private Integer sex;
  37 +
  38 + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  39 + @ExcelProperty(value = "是否有效", converter = DictConvert.class)
  40 + @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
  41 + private Boolean enabled;
  42 +
  43 + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
  44 + @ExcelProperty("头像")
  45 + private String avatar;
  46 +
  47 + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
  48 + @ExcelProperty("附件")
  49 + private String video;
  50 +
  51 + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
  52 + @ExcelProperty("备注")
  53 + private String memo;
  54 +
  55 + @Schema(description = "创建时间")
  56 + @ExcelProperty("创建时间")
  57 + private LocalDateTime createTime;
  58 +
  59 +}
0 60 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentSaveReqVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +import java.util.*;
  6 +import jakarta.validation.constraints.*;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +import java.time.LocalDateTime;
  9 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  10 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  11 +
  12 +@Schema(description = "管理后台 - 学生新增/修改 Request VO")
  13 +@Data
  14 +public class InfraStudentSaveReqVO {
  15 +
  16 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  17 + private Long id;
  18 +
  19 + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
  20 + @NotEmpty(message = "名字不能为空")
  21 + private String name;
  22 +
  23 + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
  24 + @NotEmpty(message = "简介不能为空")
  25 + private String description;
  26 +
  27 + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
  28 + @NotNull(message = "出生日期不能为空")
  29 + private LocalDateTime birthday;
  30 +
  31 + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  32 + @NotNull(message = "性别不能为空")
  33 + private Integer sex;
  34 +
  35 + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  36 + @NotNull(message = "是否有效不能为空")
  37 + private Boolean enabled;
  38 +
  39 + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
  40 + @NotEmpty(message = "头像不能为空")
  41 + private String avatar;
  42 +
  43 + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
  44 + @NotEmpty(message = "附件不能为空")
  45 + private String video;
  46 +
  47 + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
  48 + @NotEmpty(message = "备注不能为空")
  49 + private String memo;
  50 +
  51 + @Schema(description = "学生联系人列表")
  52 + private List<InfraStudentContactDO> studentContacts;
  53 +
  54 + @Schema(description = "学生班主任")
  55 + private InfraStudentTeacherDO studentTeacher;
  56 +
  57 +}
0 58 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentService 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import java.util.*;
  4 +import jakarta.validation.*;
  5 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  6 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  7 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  8 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  9 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  10 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  11 +
  12 +/**
  13 + * 学生 Service 接口
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public interface InfraStudentService {
  18 +
  19 + /**
  20 + * 创建学生
  21 + *
  22 + * @param createReqVO 创建信息
  23 + * @return 编号
  24 + */
  25 + Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
  26 +
  27 + /**
  28 + * 更新学生
  29 + *
  30 + * @param updateReqVO 更新信息
  31 + */
  32 + void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
  33 +
  34 + /**
  35 + * 删除学生
  36 + *
  37 + * @param id 编号
  38 + */
  39 + void deleteStudent(Long id);
  40 +
  41 + /**
  42 + * 批量删除学生
  43 + *
  44 + * @param ids 编号
  45 + */
  46 + void deleteStudentListByIds(List<Long> ids);
  47 +
  48 + /**
  49 + * 获得学生
  50 + *
  51 + * @param id 编号
  52 + * @return 学生
  53 + */
  54 + InfraStudentDO getStudent(Long id);
  55 +
  56 + /**
  57 + * 获得学生分页
  58 + *
  59 + * @param pageReqVO 分页查询
  60 + * @return 学生分页
  61 + */
  62 + PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
  63 +
  64 + // ==================== 子表(学生联系人) ====================
  65 +
  66 + /**
  67 + * 获得学生联系人列表
  68 + *
  69 + * @param studentId 学生编号
  70 + * @return 学生联系人列表
  71 + */
  72 + List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId);
  73 +
  74 + // ==================== 子表(学生班主任) ====================
  75 +
  76 + /**
  77 + * 获得学生班主任
  78 + *
  79 + * @param studentId 学生编号
  80 + * @return 学生班主任
  81 + */
  82 + InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId);
  83 +
  84 +}
0 85 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImpl 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjectUtil;
  5 +import org.springframework.stereotype.Service;
  6 +import jakarta.annotation.Resource;
  7 +import org.springframework.validation.annotation.Validated;
  8 +import org.springframework.transaction.annotation.Transactional;
  9 +
  10 +import java.util.*;
  11 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  12 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  13 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  14 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  15 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  16 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  17 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  18 +
  19 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
  20 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
  21 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
  22 +
  23 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  24 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  25 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
  26 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
  27 +
  28 +/**
  29 + * 学生 Service 实现类
  30 + *
  31 + * @author 芋道源码
  32 + */
  33 +@Service
  34 +@Validated
  35 +public class InfraStudentServiceImpl implements InfraStudentService {
  36 +
  37 + @Resource
  38 + private InfraStudentMapper studentMapper;
  39 + @Resource
  40 + private InfraStudentContactMapper studentContactMapper;
  41 + @Resource
  42 + private InfraStudentTeacherMapper studentTeacherMapper;
  43 +
  44 + @Override
  45 + @Transactional(rollbackFor = Exception.class)
  46 + public Long createStudent(InfraStudentSaveReqVO createReqVO) {
  47 + // 插入
  48 + InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
  49 + studentMapper.insert(student);
  50 +
  51 +
  52 + // 插入子表
  53 + createStudentContactList(student.getId(), createReqVO.getStudentContacts());
  54 + createStudentTeacher(student.getId(), createReqVO.getStudentTeacher());
  55 + // 返回
  56 + return student.getId();
  57 + }
  58 +
  59 + @Override
  60 + @Transactional(rollbackFor = Exception.class)
  61 + public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
  62 + // 校验存在
  63 + validateStudentExists(updateReqVO.getId());
  64 + // 更新
  65 + InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
  66 + studentMapper.updateById(updateObj);
  67 +
  68 + // 更新子表
  69 + updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts());
  70 + updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher());
  71 + }
  72 +
  73 + @Override
  74 + @Transactional(rollbackFor = Exception.class)
  75 + public void deleteStudent(Long id) {
  76 + // 校验存在
  77 + validateStudentExists(id);
  78 + // 删除
  79 + studentMapper.deleteById(id);
  80 +
  81 + // 删除子表
  82 + deleteStudentContactByStudentId(id);
  83 + deleteStudentTeacherByStudentId(id);
  84 + }
  85 +
  86 + @Override
  87 + @Transactional(rollbackFor = Exception.class)
  88 + public void deleteStudentListByIds(List<Long> ids) {
  89 + // 删除
  90 + studentMapper.deleteByIds(ids);
  91 +
  92 + // 删除子表
  93 + deleteStudentContactByStudentIds(ids);
  94 + deleteStudentTeacherByStudentIds(ids);
  95 + }
  96 +
  97 +
  98 + private void validateStudentExists(Long id) {
  99 + if (studentMapper.selectById(id) == null) {
  100 + throw exception(STUDENT_NOT_EXISTS);
  101 + }
  102 + }
  103 +
  104 + @Override
  105 + public InfraStudentDO getStudent(Long id) {
  106 + return studentMapper.selectById(id);
  107 + }
  108 +
  109 + @Override
  110 + public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
  111 + return studentMapper.selectPage(pageReqVO);
  112 + }
  113 +
  114 + // ==================== 子表(学生联系人) ====================
  115 +
  116 + @Override
  117 + public List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId) {
  118 + return studentContactMapper.selectListByStudentId(studentId);
  119 + }
  120 +
  121 + private void createStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
  122 + if (CollUtil.isEmpty(list)) {
  123 + return;
  124 + }
  125 + list.forEach(o -> o.setStudentId(studentId).clean());
  126 + studentContactMapper.insertBatch(list);
  127 + }
  128 +
  129 + private void updateStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
  130 + list.forEach(o -> o.setStudentId(studentId).clean());
  131 + List<InfraStudentContactDO> oldList = studentContactMapper.selectListByStudentId(studentId);
  132 + List<List<InfraStudentContactDO>> diffList = diffList(oldList, list, (oldVal, newVal) -> {
  133 + boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId());
  134 + if (same) {
  135 + newVal.setId(oldVal.getId()).clean(); // 解决更新情况下:updateTime 不更新
  136 + }
  137 + return same;
  138 + });
  139 +
  140 + // 第二步,批量添加、修改、删除
  141 + if (CollUtil.isNotEmpty(diffList.get(0))) {
  142 + studentContactMapper.insertBatch(diffList.get(0));
  143 + }
  144 + if (CollUtil.isNotEmpty(diffList.get(1))) {
  145 + studentContactMapper.updateBatch(diffList.get(1));
  146 + }
  147 + if (CollUtil.isNotEmpty(diffList.get(2))) {
  148 + studentContactMapper.deleteByIds(convertList(diffList.get(2), InfraStudentContactDO::getId));
  149 + }
  150 + }
  151 +
  152 + private void deleteStudentContactByStudentId(Long studentId) {
  153 + studentContactMapper.deleteByStudentId(studentId);
  154 + }
  155 +
  156 + private void deleteStudentContactByStudentIds(List<Long> studentIds) {
  157 + studentContactMapper.deleteByStudentIds(studentIds);
  158 + }
  159 +
  160 + // ==================== 子表(学生班主任) ====================
  161 +
  162 + @Override
  163 + public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) {
  164 + return studentTeacherMapper.selectByStudentId(studentId);
  165 + }
  166 +
  167 + private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
  168 + if (studentTeacher == null) {
  169 + return;
  170 + }
  171 + studentTeacher.setStudentId(studentId);
  172 + studentTeacherMapper.insert(studentTeacher);
  173 + }
  174 +
  175 + private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
  176 + if (studentTeacher == null) {
  177 + return;
  178 + }
  179 + studentTeacher.setStudentId(studentId).clean();// 解决更新情况下:updateTime 不更新
  180 + studentTeacherMapper.insertOrUpdate(studentTeacher);
  181 + }
  182 +
  183 + private void deleteStudentTeacherByStudentId(Long studentId) {
  184 + studentTeacherMapper.deleteByStudentId(studentId);
  185 + }
  186 +
  187 + private void deleteStudentTeacherByStudentIds(List<Long> studentIds) {
  188 + studentTeacherMapper.deleteByStudentIds(studentIds);
  189 + }
  190 +
  191 +}
0 192 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentServiceImplTest 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import org.junit.jupiter.api.Disabled;
  4 +import org.junit.jupiter.api.Test;
  5 +
  6 +import jakarta.annotation.Resource;
  7 +
  8 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  9 +
  10 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  11 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  12 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
  13 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  14 +
  15 +import jakarta.annotation.Resource;
  16 +import org.springframework.context.annotation.Import;
  17 +import java.util.*;
  18 +import java.time.LocalDateTime;
  19 +
  20 +import static cn.hutool.core.util.RandomUtil.*;
  21 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
  22 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
  23 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
  24 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
  25 +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
  26 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
  27 +import static org.junit.jupiter.api.Assertions.*;
  28 +import static org.mockito.Mockito.*;
  29 +
  30 +/**
  31 + * {@link InfraStudentServiceImpl} 的单元测试类
  32 + *
  33 + * @author 芋道源码
  34 + */
  35 +@Import(InfraStudentServiceImpl.class)
  36 +public class InfraStudentServiceImplTest extends BaseDbUnitTest {
  37 +
  38 + @Resource
  39 + private InfraStudentServiceImpl studentService;
  40 +
  41 + @Resource
  42 + private InfraStudentMapper studentMapper;
  43 +
  44 + @Test
  45 + public void testCreateStudent_success() {
  46 + // 准备参数
  47 + InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
  48 +
  49 + // 调用
  50 + Long studentId = studentService.createStudent(createReqVO);
  51 + // 断言
  52 + assertNotNull(studentId);
  53 + // 校验记录的属性是否正确
  54 + InfraStudentDO student = studentMapper.selectById(studentId);
  55 + assertPojoEquals(createReqVO, student, "id");
  56 + }
  57 +
  58 + @Test
  59 + public void testUpdateStudent_success() {
  60 + // mock 数据
  61 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
  62 + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
  63 + // 准备参数
  64 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
  65 + o.setId(dbStudent.getId()); // 设置更新的 ID
  66 + });
  67 +
  68 + // 调用
  69 + studentService.updateStudent(updateReqVO);
  70 + // 校验是否更新正确
  71 + InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
  72 + assertPojoEquals(updateReqVO, student);
  73 + }
  74 +
  75 + @Test
  76 + public void testUpdateStudent_notExists() {
  77 + // 准备参数
  78 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
  79 +
  80 + // 调用, 并断言异常
  81 + assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
  82 + }
  83 +
  84 + @Test
  85 + public void testDeleteStudent_success() {
  86 + // mock 数据
  87 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
  88 + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
  89 + // 准备参数
  90 + Long id = dbStudent.getId();
  91 +
  92 + // 调用
  93 + studentService.deleteStudent(id);
  94 + // 校验数据不存在了
  95 + assertNull(studentMapper.selectById(id));
  96 + }
  97 +
  98 + @Test
  99 + public void testDeleteStudent_notExists() {
  100 + // 准备参数
  101 + Long id = randomLongId();
  102 +
  103 + // 调用, 并断言异常
  104 + assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
  105 + }
  106 +
  107 + @Test
  108 + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
  109 + public void testGetStudentPage() {
  110 + // mock 数据
  111 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
  112 + o.setName(null);
  113 + o.setBirthday(null);
  114 + o.setSex(null);
  115 + o.setEnabled(null);
  116 + o.setCreateTime(null);
  117 + });
  118 + studentMapper.insert(dbStudent);
  119 + // 测试 name 不匹配
  120 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
  121 + // 测试 birthday 不匹配
  122 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
  123 + // 测试 sex 不匹配
  124 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
  125 + // 测试 enabled 不匹配
  126 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
  127 + // 测试 createTime 不匹配
  128 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
  129 + // 准备参数
  130 + InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
  131 + reqVO.setName(null);
  132 + reqVO.setBirthday(null);
  133 + reqVO.setSex(null);
  134 + reqVO.setEnabled(null);
  135 + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
  136 +
  137 + // 调用
  138 + PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
  139 + // 断言
  140 + assertEquals(1, pageResult.getTotal());
  141 + assertEquals(1, pageResult.getList().size());
  142 + assertPojoEquals(dbStudent, pageResult.getList().get(0));
  143 + }
  144 +
  145 +}
0 146 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherDO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import java.time.LocalDateTime;
  6 +import java.time.LocalDateTime;
  7 +import com.baomidou.mybatisplus.annotation.*;
  8 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  9 +
  10 +/**
  11 + * 学生班主任 DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("infra_student_teacher")
  16 +@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class InfraStudentTeacherDO extends BaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 学生编号
  32 + */
  33 + private Long studentId;
  34 + /**
  35 + * 名字
  36 + */
  37 + private String name;
  38 + /**
  39 + * 简介
  40 + */
  41 + private String description;
  42 + /**
  43 + * 出生日期
  44 + */
  45 + private LocalDateTime birthday;
  46 + /**
  47 + * 性别
  48 + *
  49 + * 枚举 {@link TODO system_user_sex 对应的类}
  50 + */
  51 + private Integer sex;
  52 + /**
  53 + * 是否有效
  54 + *
  55 + * 枚举 {@link TODO infra_boolean_string 对应的类}
  56 + */
  57 + private Boolean enabled;
  58 + /**
  59 + * 头像
  60 + */
  61 + private String avatar;
  62 + /**
  63 + * 附件
  64 + */
  65 + private String video;
  66 + /**
  67 + * 备注
  68 + */
  69 + private String memo;
  70 +
  71 +}
0 72 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/java/InfraStudentTeacherMapper 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.mysql.demo;
  2 +
  3 +import java.util.*;
  4 +
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  8 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  9 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  10 +import org.apache.ibatis.annotations.Mapper;
  11 +
  12 +/**
  13 + * 学生班主任 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
  19 +
  20 + default InfraStudentTeacherDO selectByStudentId(Long studentId) {
  21 + return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
  22 + }
  23 +
  24 + default int deleteByStudentId(Long studentId) {
  25 + return delete(InfraStudentTeacherDO::getStudentId, studentId);
  26 + }
  27 +
  28 + default int deleteByStudentIds(List<Long> studentIds) {
  29 + return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds);
  30 + }
  31 +
  32 +}
0 33 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/h2 0 → 100644
  1 +-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
  2 +CREATE TABLE IF NOT EXISTS "infra_student" (
  3 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  4 + "name" varchar NOT NULL,
  5 + "description" varchar NOT NULL,
  6 + "birthday" varchar NOT NULL,
  7 + "sex" int NOT NULL,
  8 + "enabled" bit NOT NULL,
  9 + "avatar" varchar NOT NULL,
  10 + "video" varchar NOT NULL,
  11 + "memo" varchar NOT NULL,
  12 + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  13 + PRIMARY KEY ("id")
  14 +) COMMENT '学生表';
  15 +
  16 +-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
  17 +DELETE FROM "infra_student";
0 18 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/sql/sql 0 → 100644
  1 +-- 菜单 SQL
  2 +INSERT INTO system_menu(
  3 + name, permission, type, sort, parent_id,
  4 + path, icon, component, status, component_name
  5 +)
  6 +VALUES (
  7 + '学生管理', '', 2, 0, 888,
  8 + 'student', '', 'infra/demo/index', 0, 'InfraStudent'
  9 +);
  10 +
  11 +-- 按钮父菜单ID
  12 +SELECT @parentId := LAST_INSERT_ID();
  13 +
  14 +-- 按钮 SQL
  15 +INSERT INTO system_menu(
  16 + name, permission, type, sort, parent_id,
  17 + path, icon, component, status
  18 +)
  19 +VALUES (
  20 + '学生查询', 'infra:student:query', 3, 1, @parentId,
  21 + '', '', '', 0
  22 +);
  23 +INSERT INTO system_menu(
  24 + name, permission, type, sort, parent_id,
  25 + path, icon, component, status
  26 +)
  27 +VALUES (
  28 + '学生创建', 'infra:student:create', 3, 2, @parentId,
  29 + '', '', '', 0
  30 +);
  31 +INSERT INTO system_menu(
  32 + name, permission, type, sort, parent_id,
  33 + path, icon, component, status
  34 +)
  35 +VALUES (
  36 + '学生更新', 'infra:student:update', 3, 3, @parentId,
  37 + '', '', '', 0
  38 +);
  39 +INSERT INTO system_menu(
  40 + name, permission, type, sort, parent_id,
  41 + path, icon, component, status
  42 +)
  43 +VALUES (
  44 + '学生删除', 'infra:student:delete', 3, 4, @parentId,
  45 + '', '', '', 0
  46 +);
  47 +INSERT INTO system_menu(
  48 + name, permission, type, sort, parent_id,
  49 + path, icon, component, status
  50 +)
  51 +VALUES (
  52 + '学生导出', 'infra:student:export', 3, 5, @parentId,
  53 + '', '', '', 0
  54 +);
0 55 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/ts/index 0 → 100644
  1 +import type { PageParam, PageResult } from '@/http/types'
  2 +import { http } from '@/http/http'
  3 +
  4 +/** 学生联系人信息 */
  5 +export interface StudentContact {
  6 + id?: number
  7 + studentId?: number
  8 + name: string
  9 + description: string
  10 + birthday: number
  11 + sex: number
  12 + enabled: boolean
  13 + avatar: string
  14 + video?: string
  15 + memo: string
  16 + createTime?: number
  17 +}
  18 +
  19 +/** 学生班主任信息 */
  20 +export interface StudentTeacher {
  21 + id?: number
  22 + studentId?: number
  23 + name: string
  24 + description: string
  25 + birthday: number
  26 + sex: number
  27 + enabled: boolean
  28 + avatar: string
  29 + video?: string
  30 + memo: string
  31 + createTime?: number
  32 +}
  33 +
  34 +/** 学生信息 */
  35 +export interface Student {
  36 + id?: number
  37 + name: string
  38 + description: string
  39 + birthday: number
  40 + sex: number
  41 + enabled: boolean
  42 + avatar: string
  43 + video: string
  44 + memo: string
  45 + createTime?: number
  46 + studentContacts?: StudentContact[]
  47 + studentTeacher?: StudentTeacher | null
  48 +}
  49 +
  50 +/** 获取学生分页列表 */
  51 +export function getStudentPage(params: PageParam) {
  52 + return http.get<PageResult<Student>>('/infra/student/page', params)
  53 +}
  54 +
  55 +/** 获取学生详情 */
  56 +export function getStudent(id: number) {
  57 + return http.get<Student>(`/infra/student/get?id=${id}`)
  58 +}
  59 +
  60 +/** 创建学生 */
  61 +export function createStudent(data: Student) {
  62 + return http.post<number>('/infra/student/create', data)
  63 +}
  64 +
  65 +/** 更新学生 */
  66 +export function updateStudent(data: Student) {
  67 + return http.put<boolean>('/infra/student/update', data)
  68 +}
  69 +
  70 +/** 删除学生 */
  71 +export function deleteStudent(id: number) {
  72 + return http.delete<boolean>(`/infra/student/delete?id=${id}`)
  73 +}
  74 +
  75 +// ==================== 子表:学生联系人 ====================
  76 +
  77 +/** 获取学生联系人列表 */
  78 +export function getStudentContactListByStudentId(studentId: number) {
  79 + return http.get<StudentContact[]>(`/infra/student/student-contact/list-by-student-id?studentId=${studentId}`)
  80 +}
  81 +
  82 +// ==================== 子表:学生班主任 ====================
  83 +
  84 +/** 获取学生班主任 */
  85 +export function getStudentTeacherByStudentId(studentId: number) {
  86 + return http.get<StudentTeacher | null>(`/infra/student/student-teacher/get-by-student-id?studentId=${studentId}`)
  87 +}
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/detail/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + title="学生详情"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <view class="pb-160rpx">
  11 + <!-- 主表详情 -->
  12 + <wd-cell-group border title="学生信息">
  13 + <wd-cell title="编号" :value="formData?.id ?? '-'" />
  14 + <wd-cell title="名字" :value="formData?.name ?? '-'" />
  15 + <wd-cell title="简介" :value="formData?.description ?? '-'" />
  16 + <wd-cell title="出生日期" :value="formatDateTime(formData?.birthday) || '-'" />
  17 + <wd-cell title="性别">
  18 + <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="formData?.sex" />
  19 + </wd-cell>
  20 + <wd-cell title="是否有效">
  21 + <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.enabled" />
  22 + </wd-cell>
  23 + <wd-cell title="头像" :value="formData?.avatar ?? '-'" />
  24 + <wd-cell title="附件" :value="formData?.video ?? '-'" />
  25 + <wd-cell title="备注" :value="formData?.memo ?? '-'" />
  26 + <wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
  27 + </wd-cell-group>
  28 +
  29 + <!-- 子表:学生联系人 -->
  30 + <view class="mt-20rpx px-24rpx py-16rpx text-28rpx text-[#999]">
  31 + 学生联系人({{ studentContacts.length }})
  32 + </view>
  33 + <view class="px-24rpx">
  34 + <view v-if="!studentContacts.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
  35 + 暂无学生联系人
  36 + </view>
  37 + <view
  38 + v-for="item in studentContacts"
  39 + :key="item.id"
  40 + class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
  41 + >
  42 + <view class="mb-8rpx text-28rpx text-[#666]">
  43 + 学生编号:{{ item.studentId ?? '-' }}
  44 + </view>
  45 + <view class="mb-8rpx text-28rpx text-[#666]">
  46 + 名字:{{ item.name ?? '-' }}
  47 + </view>
  48 + <view class="mb-8rpx text-28rpx text-[#666]">
  49 + 简介:{{ item.description ?? '-' }}
  50 + </view>
  51 + <view class="mb-8rpx text-28rpx text-[#666]">
  52 + 出生日期:{{ item.birthday ?? '-' }}
  53 + </view>
  54 + <view class="mb-8rpx text-28rpx text-[#666]">
  55 + 性别:{{ item.sex ?? '-' }}
  56 + </view>
  57 + <view class="mb-8rpx text-28rpx text-[#666]">
  58 + 是否有效:{{ item.enabled ?? '-' }}
  59 + </view>
  60 + <view class="mb-8rpx text-28rpx text-[#666]">
  61 + 头像:{{ item.avatar ?? '-' }}
  62 + </view>
  63 + <view class="mb-8rpx text-28rpx text-[#666]">
  64 + 附件:{{ item.video ?? '-' }}
  65 + </view>
  66 + <view class="mb-8rpx text-28rpx text-[#666]">
  67 + 备注:{{ item.memo ?? '-' }}
  68 + </view>
  69 + <view class="mb-8rpx text-28rpx text-[#666]">
  70 + 创建时间:{{ item.createTime ?? '-' }}
  71 + </view>
  72 + </view>
  73 + </view>
  74 + <!-- 子表:学生班主任 -->
  75 + <view class="mt-20rpx px-24rpx py-16rpx text-28rpx text-[#999]">
  76 + 学生班主任
  77 + </view>
  78 + <view class="px-24rpx">
  79 + <view class="rounded-12rpx bg-white p-24rpx shadow-sm">
  80 + <template v-if="studentTeacher">
  81 + <view class="mb-8rpx text-28rpx text-[#666]">
  82 + 学生编号:{{ studentTeacher.studentId ?? '-' }}
  83 + </view>
  84 + <view class="mb-8rpx text-28rpx text-[#666]">
  85 + 名字:{{ studentTeacher.name ?? '-' }}
  86 + </view>
  87 + <view class="mb-8rpx text-28rpx text-[#666]">
  88 + 简介:{{ studentTeacher.description ?? '-' }}
  89 + </view>
  90 + <view class="mb-8rpx text-28rpx text-[#666]">
  91 + 出生日期:{{ studentTeacher.birthday ?? '-' }}
  92 + </view>
  93 + <view class="mb-8rpx text-28rpx text-[#666]">
  94 + 性别:{{ studentTeacher.sex ?? '-' }}
  95 + </view>
  96 + <view class="mb-8rpx text-28rpx text-[#666]">
  97 + 是否有效:{{ studentTeacher.enabled ?? '-' }}
  98 + </view>
  99 + <view class="mb-8rpx text-28rpx text-[#666]">
  100 + 头像:{{ studentTeacher.avatar ?? '-' }}
  101 + </view>
  102 + <view class="mb-8rpx text-28rpx text-[#666]">
  103 + 附件:{{ studentTeacher.video ?? '-' }}
  104 + </view>
  105 + <view class="mb-8rpx text-28rpx text-[#666]">
  106 + 备注:{{ studentTeacher.memo ?? '-' }}
  107 + </view>
  108 + <view class="mb-8rpx text-28rpx text-[#666]">
  109 + 创建时间:{{ studentTeacher.createTime ?? '-' }}
  110 + </view>
  111 + </template>
  112 + <view v-else class="text-center text-26rpx text-[#999]">
  113 + 暂无学生班主任
  114 + </view>
  115 + </view>
  116 + </view>
  117 + </view>
  118 +
  119 + <!-- 底部操作按钮 -->
  120 + <view class="yd-detail-footer">
  121 + <view class="yd-detail-footer-actions">
  122 + <wd-button
  123 + v-if="hasAccessByCodes(['infra:student:update'])"
  124 + class="flex-1" type="warning" @click="handleEdit"
  125 + >
  126 + 编辑
  127 + </wd-button>
  128 + <wd-button
  129 + v-if="hasAccessByCodes(['infra:student:delete'])"
  130 + class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
  131 + >
  132 + 删除
  133 + </wd-button>
  134 + </view>
  135 + </view>
  136 + </view>
  137 +</template>
  138 +
  139 +<script lang="ts" setup>
  140 +import type {
  141 + Student,
  142 + StudentContact,
  143 + StudentTeacher,
  144 +} from '@/api/infra/demo'
  145 +import { onShow } from '@dcloudio/uni-app'
  146 +import { ref } from 'vue'
  147 +import { useDialog } from '@wot-ui/ui/components/wd-dialog'
  148 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  149 +import {
  150 + deleteStudent,
  151 + getStudent,
  152 + getStudentContactListByStudentId,
  153 + getStudentTeacherByStudentId,
  154 +} from '@/api/infra/demo'
  155 +import { useAccess } from '@/hooks/useAccess'
  156 +import { delay, navigateBackPlus } from '@/utils'
  157 +import { DICT_TYPE } from '@/utils/constants'
  158 +import { formatDateTime } from '@/utils/date'
  159 +
  160 +const props = defineProps<{
  161 + id?: number
  162 +}>()
  163 +
  164 +definePage({
  165 + style: {
  166 + navigationBarTitleText: '',
  167 + navigationStyle: 'custom',
  168 + },
  169 +})
  170 +
  171 +const { hasAccessByCodes } = useAccess()
  172 +const dialog = useDialog()
  173 +const toast = useToast()
  174 +const formData = ref<Student>() // 主表数据
  175 +const studentContacts = ref<StudentContact[]>([]) // 学生联系人数据
  176 +const studentTeacher = ref<StudentTeacher | null>(null) // 学生班主任数据
  177 +const deleting = ref(false) // 删除状态
  178 +
  179 +/** 返回上一页 */
  180 +function handleBack() {
  181 + navigateBackPlus('/pages-infra/demo/index')
  182 +}
  183 +
  184 +/** 加载主表和子表详情 */
  185 +async function getDetail() {
  186 + if (!props.id || deleting.value) {
  187 + return
  188 + }
  189 + const [data, studentContactData, studentTeacherData] = await Promise.all([
  190 + getStudent(props.id),
  191 + getStudentContactListByStudentId(props.id),
  192 + getStudentTeacherByStudentId(props.id),
  193 + ])
  194 + formData.value = data
  195 + studentContacts.value = studentContactData || []
  196 + studentTeacher.value = studentTeacherData || null
  197 +}
  198 +
  199 +/** 编辑学生 */
  200 +function handleEdit() {
  201 + uni.navigateTo({ url: `/pages-infra/demo/form/index?id=${props.id}` })
  202 +}
  203 +
  204 +/** 删除学生 */
  205 +async function handleDelete() {
  206 + if (!props.id) {
  207 + return
  208 + }
  209 + try {
  210 + await dialog.confirm({ title: '提示', msg: '确定要删除该学生吗?' })
  211 + } catch {
  212 + return
  213 + }
  214 + deleting.value = true
  215 + try {
  216 + await deleteStudent(props.id)
  217 + toast.success('删除成功')
  218 + uni.$emit('infra:demo:reload')
  219 + delay(handleBack)
  220 + } finally {
  221 + deleting.value = false
  222 + }
  223 +}
  224 +
  225 +/** 进入页面加载(含编辑返回后刷新) */
  226 +onShow(() => {
  227 + getDetail()
  228 +})
  229 +</script>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/form/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + :title="getTitle"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <!-- 表单区域 -->
  11 + <view>
  12 + <wd-form ref="formRef" :model="formData" :schema="formSchema">
  13 + <wd-cell-group border>
  14 + <wd-form-item title="名字" title-width="180rpx" prop="name">
  15 + <wd-input
  16 + v-model="formData.name"
  17 + clearable
  18 + placeholder="请输入名字"
  19 + />
  20 + </wd-form-item>
  21 + <wd-form-item title="简介" title-width="180rpx" prop="description">
  22 + <wd-textarea
  23 + v-model="formData.description"
  24 + clearable
  25 + placeholder="请输入简介"
  26 + :maxlength="200"
  27 + show-word-limit
  28 + />
  29 + </wd-form-item>
  30 + <wd-form-item
  31 + title="出生日期"
  32 + title-width="180rpx"
  33 + prop="birthday"
  34 + is-link
  35 + :value="formData.birthday ? formatDateTime(formData.birthday) : ''"
  36 + placeholder="请选择出生日期"
  37 + @click="birthdayVisible = true"
  38 + />
  39 + <wd-datetime-picker
  40 + v-model:visible="birthdayVisible"
  41 + :model-value="formData.birthday || Date.now()"
  42 + type="datetime"
  43 + @update:model-value="value => formData.birthday = value"
  44 + />
  45 + <yd-form-picker
  46 + v-model="formData.sex"
  47 + label="性别"
  48 + label-width="180rpx"
  49 + prop="sex"
  50 + :dict-type="DICT_TYPE.SYSTEM_USER_SEX"
  51 + placeholder="请选择性别"
  52 + />
  53 + <wd-form-item title="是否有效" title-width="180rpx" prop="enabled" center>
  54 + <wd-radio-group v-model="formData.enabled" type="button">
  55 + <wd-radio
  56 + v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  57 + :key="dict.value"
  58 + :value="dict.value"
  59 + >
  60 + {{ dict.label }}
  61 + </wd-radio>
  62 + </wd-radio-group>
  63 + </wd-form-item>
  64 + <wd-form-item title="头像" title-width="180rpx" prop="avatar">
  65 + <yd-upload-img v-model="formData.avatar" directory="infra/demo" />
  66 + </wd-form-item>
  67 + <wd-form-item title="附件" title-width="180rpx" prop="video">
  68 + <yd-upload-file v-model="formData.video" directory="infra/demo" />
  69 + </wd-form-item>
  70 + <wd-form-item title="备注" title-width="180rpx" prop="memo">
  71 + <wd-input
  72 + v-model="formData.memo"
  73 + clearable
  74 + placeholder="请输入备注"
  75 + />
  76 + </wd-form-item>
  77 + </wd-cell-group>
  78 + </wd-form>
  79 +
  80 + <!-- 子表:学生联系人 -->
  81 + <view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
  82 + <text class="text-28rpx text-[#333] font-semibold">学生联系人</text>
  83 + <wd-button size="small" type="primary" variant="plain" @click="addStudentContact">
  84 + 添加
  85 + </wd-button>
  86 + </view>
  87 + <view class="px-24rpx">
  88 + <view v-if="!studentContacts.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
  89 + 暂无学生联系人
  90 + </view>
  91 + <view
  92 + v-for="(item, index) in studentContacts"
  93 + :key="index"
  94 + class="mb-20rpx rounded-12rpx bg-white p-24rpx shadow-sm"
  95 + >
  96 + <view class="mb-16rpx flex justify-end">
  97 + <wd-button size="small" type="danger" variant="plain" @click="removeStudentContact(index)">
  98 + 删除
  99 + </wd-button>
  100 + </view>
  101 + <wd-form-item title="名字" title-width="180rpx">
  102 + <wd-input v-model="item.name" clearable placeholder="请输入名字" />
  103 + </wd-form-item>
  104 + <wd-form-item title="简介" title-width="180rpx">
  105 + <wd-textarea v-model="item.description" clearable placeholder="请输入简介" />
  106 + </wd-form-item>
  107 + <wd-form-item
  108 + title="出生日期"
  109 + title-width="180rpx"
  110 + is-link
  111 + :value="item.birthday ? formatDateTime(item.birthday) : ''"
  112 + placeholder="请选择出生日期"
  113 + @click="subDateVisible[`studentContacts.${index}.birthday`] = true"
  114 + />
  115 + <wd-datetime-picker
  116 + v-model:visible="subDateVisible[`studentContacts.${index}.birthday`]"
  117 + :model-value="item.birthday || Date.now()"
  118 + type="datetime"
  119 + @update:model-value="value => item.birthday = value"
  120 + />
  121 + <yd-form-picker
  122 + v-model="item.sex"
  123 + label="性别"
  124 + label-width="180rpx"
  125 + :dict-type="DICT_TYPE.SYSTEM_USER_SEX"
  126 + placeholder="请选择性别"
  127 + />
  128 + <wd-form-item title="是否有效" title-width="180rpx" center>
  129 + <wd-radio-group v-model="item.enabled" type="button">
  130 + <wd-radio
  131 + v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  132 + :key="dict.value"
  133 + :value="dict.value"
  134 + >
  135 + {{ dict.label }}
  136 + </wd-radio>
  137 + </wd-radio-group>
  138 + </wd-form-item>
  139 + <wd-form-item title="头像" title-width="180rpx">
  140 + <yd-upload-img v-model="item.avatar" directory="infra/demo" />
  141 + </wd-form-item>
  142 + <wd-form-item title="附件" title-width="180rpx">
  143 + <yd-upload-file v-model="item.video" directory="infra/demo" />
  144 + </wd-form-item>
  145 + <wd-form-item title="备注" title-width="180rpx">
  146 + <wd-input v-model="item.memo" clearable placeholder="请输入备注" />
  147 + </wd-form-item>
  148 + </view>
  149 + </view>
  150 +
  151 + <!-- 子表:学生班主任 -->
  152 + <view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
  153 + <text class="text-28rpx text-[#333] font-semibold">学生班主任</text>
  154 + <wd-button
  155 + v-if="!studentTeacher"
  156 + size="small"
  157 + type="primary"
  158 + variant="plain"
  159 + @click="addStudentTeacher()"
  160 + >
  161 + 添加
  162 + </wd-button>
  163 + </view>
  164 + <view class="px-24rpx">
  165 + <view v-if="!studentTeacher" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
  166 + 暂无学生班主任
  167 + </view>
  168 + <view v-else class="rounded-12rpx bg-white p-24rpx shadow-sm">
  169 + <wd-form-item title="名字" title-width="180rpx">
  170 + <wd-input v-model="studentTeacher.name" clearable placeholder="请输入名字" />
  171 + </wd-form-item>
  172 + <wd-form-item title="简介" title-width="180rpx">
  173 + <wd-textarea v-model="studentTeacher.description" clearable placeholder="请输入简介" />
  174 + </wd-form-item>
  175 + <wd-form-item
  176 + title="出生日期"
  177 + title-width="180rpx"
  178 + is-link
  179 + :value="studentTeacher.birthday ? formatDateTime(studentTeacher.birthday) : ''"
  180 + placeholder="请选择出生日期"
  181 + @click="subDateVisible['studentTeacher.birthday'] = true"
  182 + />
  183 + <wd-datetime-picker
  184 + v-model:visible="subDateVisible['studentTeacher.birthday']"
  185 + :model-value="studentTeacher.birthday || Date.now()"
  186 + type="datetime"
  187 + @update:model-value="value => studentTeacher.birthday = value"
  188 + />
  189 + <yd-form-picker
  190 + v-model="studentTeacher.sex"
  191 + label="性别"
  192 + label-width="180rpx"
  193 + :dict-type="DICT_TYPE.SYSTEM_USER_SEX"
  194 + placeholder="请选择性别"
  195 + />
  196 + <wd-form-item title="是否有效" title-width="180rpx" center>
  197 + <wd-radio-group v-model="studentTeacher.enabled" type="button">
  198 + <wd-radio
  199 + v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  200 + :key="dict.value"
  201 + :value="dict.value"
  202 + >
  203 + {{ dict.label }}
  204 + </wd-radio>
  205 + </wd-radio-group>
  206 + </wd-form-item>
  207 + <wd-form-item title="头像" title-width="180rpx">
  208 + <yd-upload-img v-model="studentTeacher.avatar" directory="infra/demo" />
  209 + </wd-form-item>
  210 + <wd-form-item title="附件" title-width="180rpx">
  211 + <yd-upload-file v-model="studentTeacher.video" directory="infra/demo" />
  212 + </wd-form-item>
  213 + <wd-form-item title="备注" title-width="180rpx">
  214 + <wd-input v-model="studentTeacher.memo" clearable placeholder="请输入备注" />
  215 + </wd-form-item>
  216 + </view>
  217 + </view>
  218 + </view>
  219 +
  220 + <!-- 底部保存按钮 -->
  221 + <view class="yd-detail-footer">
  222 + <wd-button
  223 + type="primary"
  224 + block
  225 + :loading="formLoading"
  226 + @click="handleSubmit"
  227 + >
  228 + 保存
  229 + </wd-button>
  230 + </view>
  231 + </view>
  232 +</template>
  233 +
  234 +<script lang="ts" setup>
  235 +import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
  236 +import type {
  237 + Student,
  238 + StudentContact,
  239 + StudentTeacher,
  240 +} from '@/api/infra/demo'
  241 +import { computed, onMounted, ref } from 'vue'
  242 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  243 +import {
  244 + createStudent,
  245 + getStudent,
  246 + getStudentContactListByStudentId,
  247 + getStudentTeacherByStudentId,
  248 + updateStudent,
  249 +} from '@/api/infra/demo'
  250 +import { getBoolDictOptions } from '@/hooks/useDict'
  251 +import { delay, navigateBackPlus } from '@/utils'
  252 +import { DICT_TYPE } from '@/utils/constants'
  253 +import { formatDateTime } from '@/utils/date'
  254 +import { createFormSchema } from '@/utils/wot'
  255 +
  256 +const props = defineProps<{
  257 + id?: number
  258 +}>()
  259 +
  260 +definePage({
  261 + style: {
  262 + navigationBarTitleText: '',
  263 + navigationStyle: 'custom',
  264 + },
  265 +})
  266 +
  267 +const toast = useToast()
  268 +const getTitle = computed(() => props.id ? '编辑学生' : '新增学生')
  269 +const formLoading = ref(false) // 表单提交状态
  270 +const formData = ref<Student>({
  271 + id: undefined,
  272 + name: '',
  273 + description: '',
  274 + birthday: 0,
  275 + sex: 0,
  276 + enabled: false,
  277 + avatar: '',
  278 + video: '',
  279 + memo: '',
  280 +}) // 表单数据
  281 +const studentContacts = ref<StudentContact[]>([]) // 学生联系人数据
  282 +const studentTeacher = ref<StudentTeacher | null>(null) // 学生班主任数据
  283 +const birthdayVisible = ref(false) // 出生日期选择器状态
  284 +const subDateVisible = ref<Record<string, boolean>>({}) // 子表日期选择器状态
  285 +const formSchema = createFormSchema({
  286 + name: [{ required: true, message: '名字不能为空' }],
  287 + description: [{ required: true, message: '简介不能为空' }],
  288 + birthday: [{ required: true, message: '出生日期不能为空' }],
  289 + sex: [{ required: true, message: '性别不能为空' }],
  290 + enabled: [{ required: true, message: '是否有效不能为空' }],
  291 + avatar: [{ required: true, message: '头像不能为空' }],
  292 + video: [{ required: true, message: '附件不能为空' }],
  293 + memo: [{ required: true, message: '备注不能为空' }],
  294 +}) // 表单校验规则
  295 +const formRef = ref<FormInstance>() // 表单组件引用
  296 +
  297 +/** 添加学生联系人 */
  298 +function addStudentContact() {
  299 + studentContacts.value.push({
  300 + name: '',
  301 + description: '',
  302 + birthday: 0,
  303 + sex: 0,
  304 + enabled: false,
  305 + avatar: '',
  306 + video: undefined,
  307 + memo: '',
  308 + })
  309 +}
  310 +
  311 +/** 删除学生联系人 */
  312 +function removeStudentContact(index: number) {
  313 + studentContacts.value.splice(index, 1)
  314 +}
  315 +
  316 +/** 添加学生班主任 */
  317 +function addStudentTeacher() {
  318 + studentTeacher.value = {
  319 + name: '',
  320 + description: '',
  321 + birthday: 0,
  322 + sex: 0,
  323 + enabled: false,
  324 + avatar: '',
  325 + video: undefined,
  326 + memo: '',
  327 + }
  328 +}
  329 +
  330 +/** 校验主子表数据 */
  331 +function isEmptySubValue(value: unknown) {
  332 + return value === undefined || value === null || value === ''
  333 +}
  334 +
  335 +function validateSubTables() {
  336 + for (let index = 0; index < studentContacts.value.length; index++) {
  337 + const item = studentContacts.value[index]!
  338 + if (isEmptySubValue(item.name)) {
  339 + toast.warning(`学生联系人第${index + 1}项的名字不能为空`)
  340 + return false
  341 + }
  342 + if (isEmptySubValue(item.description)) {
  343 + toast.warning(`学生联系人第${index + 1}项的简介不能为空`)
  344 + return false
  345 + }
  346 + if (isEmptySubValue(item.birthday)) {
  347 + toast.warning(`学生联系人第${index + 1}项的出生日期不能为空`)
  348 + return false
  349 + }
  350 + if (isEmptySubValue(item.sex)) {
  351 + toast.warning(`学生联系人第${index + 1}项的性别不能为空`)
  352 + return false
  353 + }
  354 + if (isEmptySubValue(item.enabled)) {
  355 + toast.warning(`学生联系人第${index + 1}项的是否有效不能为空`)
  356 + return false
  357 + }
  358 + if (isEmptySubValue(item.avatar)) {
  359 + toast.warning(`学生联系人第${index + 1}项的头像不能为空`)
  360 + return false
  361 + }
  362 + if (isEmptySubValue(item.memo)) {
  363 + toast.warning(`学生联系人第${index + 1}项的备注不能为空`)
  364 + return false
  365 + }
  366 + }
  367 + if (studentTeacher.value) {
  368 + if (isEmptySubValue(studentTeacher.value.name)) {
  369 + toast.warning('学生班主任的名字不能为空')
  370 + return false
  371 + }
  372 + if (isEmptySubValue(studentTeacher.value.description)) {
  373 + toast.warning('学生班主任的简介不能为空')
  374 + return false
  375 + }
  376 + if (isEmptySubValue(studentTeacher.value.birthday)) {
  377 + toast.warning('学生班主任的出生日期不能为空')
  378 + return false
  379 + }
  380 + if (isEmptySubValue(studentTeacher.value.sex)) {
  381 + toast.warning('学生班主任的性别不能为空')
  382 + return false
  383 + }
  384 + if (isEmptySubValue(studentTeacher.value.enabled)) {
  385 + toast.warning('学生班主任的是否有效不能为空')
  386 + return false
  387 + }
  388 + if (isEmptySubValue(studentTeacher.value.avatar)) {
  389 + toast.warning('学生班主任的头像不能为空')
  390 + return false
  391 + }
  392 + if (isEmptySubValue(studentTeacher.value.memo)) {
  393 + toast.warning('学生班主任的备注不能为空')
  394 + return false
  395 + }
  396 + }
  397 + return true
  398 +}
  399 +
  400 +/** 返回上一页 */
  401 +function handleBack() {
  402 + navigateBackPlus('/pages-infra/demo/index')
  403 +}
  404 +
  405 +/** 加载学生详情 */
  406 +async function getDetail() {
  407 + if (!props.id) {
  408 + return
  409 + }
  410 + const [data, studentContactData, studentTeacherData] = await Promise.all([
  411 + getStudent(props.id),
  412 + getStudentContactListByStudentId(props.id),
  413 + getStudentTeacherByStudentId(props.id),
  414 + ])
  415 + formData.value = data
  416 + studentContacts.value = studentContactData || []
  417 + studentTeacher.value = studentTeacherData || null
  418 +}
  419 +
  420 +/** 提交表单 */
  421 +async function handleSubmit() {
  422 + const { valid } = await formRef.value.validate()
  423 + if (!valid) {
  424 + return
  425 + }
  426 + if (!validateSubTables()) {
  427 + return
  428 + }
  429 +
  430 + formLoading.value = true
  431 + try {
  432 + const data: Student = {
  433 + ...formData.value,
  434 + studentContacts: studentContacts.value,
  435 + studentTeacher: studentTeacher.value,
  436 + }
  437 + if (props.id) {
  438 + await updateStudent(data)
  439 + toast.success('修改成功')
  440 + } else {
  441 + await createStudent(data)
  442 + toast.success('新增成功')
  443 + }
  444 + uni.$emit('infra:demo:reload')
  445 + delay(handleBack)
  446 + } finally {
  447 + formLoading.value = false
  448 + }
  449 +}
  450 +
  451 +/** 初始化 */
  452 +onMounted(async () => {
  453 + getDetail()
  454 +})
  455 +</script>
  456 +
  457 +<style lang="scss" scoped>
  458 +</style>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container yd-page-container-paging">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + title="学生管理"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <!-- 搜索组件 -->
  11 + <SearchForm @search="handleQuery" @reset="handleReset" />
  12 +
  13 + <!-- 学生列表 -->
  14 + <z-paging
  15 + ref="pagingRef"
  16 + v-model="list"
  17 + :fixed="false"
  18 + class="min-h-0 flex-1"
  19 + :default-page-size="10"
  20 + :refresher-enabled="true"
  21 + :inside-more="true"
  22 + :loading-more-default-as-loading="true"
  23 + empty-view-text="暂无学生数据"
  24 + @query="queryList"
  25 + >
  26 + <view class="p-24rpx">
  27 + <view
  28 + v-for="item in list"
  29 + :key="item.id"
  30 + class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
  31 + @click="handleDetail(item)"
  32 + >
  33 + <view class="p-24rpx">
  34 + <view class="mb-16rpx flex items-center justify-between">
  35 + <view class="text-32rpx text-[#333] font-semibold">
  36 + {{ item.name }}
  37 + </view>
  38 + <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="item.sex" />
  39 + </view>
  40 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  41 + <text class="mr-8rpx text-[#999]">简介:</text>
  42 + <text class="line-clamp-1">{{ item.description }}</text>
  43 + </view>
  44 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  45 + <text class="mr-8rpx text-[#999]">出生日期:</text>
  46 + <text class="line-clamp-1">{{ formatDateTime(item.birthday) || '-' }}</text>
  47 + </view>
  48 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  49 + <text class="mr-8rpx text-[#999]">是否有效:</text>
  50 + <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="item.enabled" />
  51 + </view>
  52 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  53 + <text class="mr-8rpx text-[#999]">头像:</text>
  54 + <text class="line-clamp-1">{{ item.avatar }}</text>
  55 + </view>
  56 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  57 + <text class="mr-8rpx text-[#999]">附件:</text>
  58 + <text class="line-clamp-1">{{ item.video }}</text>
  59 + </view>
  60 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  61 + <text class="mr-8rpx text-[#999]">备注:</text>
  62 + <text class="line-clamp-1">{{ item.memo }}</text>
  63 + </view>
  64 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  65 + <text class="mr-8rpx text-[#999]">创建时间:</text>
  66 + <text class="line-clamp-1">{{ formatDateTime(item.createTime) || '-' }}</text>
  67 + </view>
  68 + </view>
  69 + </view>
  70 + </view>
  71 + </z-paging>
  72 +
  73 + <!-- 新增按钮 -->
  74 + <wd-fab
  75 + v-if="hasAccessByCodes(['infra:student:create'])"
  76 + position="right-bottom"
  77 + type="primary"
  78 + :expandable="false"
  79 + @click="handleAdd"
  80 + />
  81 + </view>
  82 +</template>
  83 +
  84 +<script lang="ts" setup>
  85 +import type { Student } from '@/api/infra/demo'
  86 +import { onUnload } from '@dcloudio/uni-app'
  87 +import { onMounted, ref } from 'vue'
  88 +import { getStudentPage } from '@/api/infra/demo'
  89 +import { useAccess } from '@/hooks/useAccess'
  90 +import { navigateBackPlus } from '@/utils'
  91 +import { DICT_TYPE } from '@/utils/constants'
  92 +import { formatDateTime } from '@/utils/date'
  93 +import SearchForm from './components/search-form.vue'
  94 +
  95 +definePage({
  96 + style: {
  97 + navigationBarTitleText: '',
  98 + navigationStyle: 'custom',
  99 + },
  100 +})
  101 +
  102 +const { hasAccessByCodes } = useAccess()
  103 +const list = ref<Student[]>([]) // 列表数据
  104 +const pagingRef = ref<any>() // 分页组件引用
  105 +const queryParams = ref<Record<string, any>>({}) // 查询参数
  106 +
  107 +/** 返回上一页 */
  108 +function handleBack() {
  109 + navigateBackPlus()
  110 +}
  111 +
  112 +/** 查询学生列表 */
  113 +async function queryList(pageNo: number, pageSize: number) {
  114 + try {
  115 + const params = {
  116 + ...queryParams.value,
  117 + pageNo,
  118 + pageSize,
  119 + }
  120 + const data = await getStudentPage(params)
  121 + pagingRef.value?.completeByTotal(data.list, data.total)
  122 + } catch {
  123 + pagingRef.value?.complete(false)
  124 + }
  125 +}
  126 +
  127 +/** 搜索按钮操作 */
  128 +function handleQuery(data?: Record<string, any>) {
  129 + queryParams.value = { ...data }
  130 + reload()
  131 +}
  132 +
  133 +/** 重置按钮操作 */
  134 +function handleReset() {
  135 + handleQuery()
  136 +}
  137 +
  138 +/** 重新加载 */
  139 +function reload() {
  140 + pagingRef.value?.reload()
  141 +}
  142 +
  143 +/** 新增学生 */
  144 +function handleAdd() {
  145 + uni.navigateTo({
  146 + url: '/pages-infra/demo/form/index',
  147 + })
  148 +}
  149 +
  150 +/** 查看详情 */
  151 +function handleDetail(item: Student) {
  152 + uni.navigateTo({
  153 + url: `/pages-infra/demo/detail/index?id=${item.id}`,
  154 + })
  155 +}
  156 +
  157 +/** 初始化 */
  158 +onMounted(() => {
  159 + uni.$on('infra:demo:reload', reload)
  160 +})
  161 +
  162 +/** 卸载 */
  163 +onUnload(() => {
  164 + uni.$off('infra:demo:reload', reload)
  165 +})
  166 +</script>
  167 +
  168 +<style lang="scss" scoped>
  169 +</style>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/vue/search-form 0 → 100644
  1 +<template>
  2 + <!-- 搜索框入口 -->
  3 + <view @click="visible = true">
  4 + <wd-search :placeholder="placeholder" hide-cancel disabled />
  5 + </view>
  6 +
  7 + <!-- 搜索弹窗 -->
  8 + <wd-popup v-model="visible" position="top" @close="visible = false">
  9 + <view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
  10 + <view class="yd-search-form-item">
  11 + <view class="yd-search-form-label">
  12 + 名字
  13 + </view>
  14 + <wd-input
  15 + v-model="formData.name"
  16 + placeholder="请输入名字"
  17 + clearable
  18 + />
  19 + </view>
  20 + <view class="yd-search-form-item">
  21 + <view class="yd-search-form-label">
  22 + 出生日期
  23 + </view>
  24 + <wd-input
  25 + v-model="formData.birthday"
  26 + placeholder="请输入出生日期"
  27 + clearable
  28 + />
  29 + </view>
  30 + <yd-search-picker
  31 + v-model="formData.sex"
  32 + label="性别"
  33 + :columns="getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
  34 + all-option
  35 + />
  36 + <yd-search-picker
  37 + v-model="formData.enabled"
  38 + label="是否有效"
  39 + :columns="getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  40 + all-option
  41 + />
  42 + <yd-search-date-range v-model="formData.createTime" label="创建时间" />
  43 + <view class="yd-search-form-actions">
  44 + <wd-button class="flex-1" plain @click="handleReset">
  45 + 重置
  46 + </wd-button>
  47 + <wd-button class="flex-1" type="primary" @click="handleSearch">
  48 + 搜索
  49 + </wd-button>
  50 + </view>
  51 + </view>
  52 + </wd-popup>
  53 +</template>
  54 +
  55 +<script lang="ts" setup>
  56 +import { computed, reactive, ref } from 'vue'
  57 +import { getNavbarHeight } from '@/utils'
  58 +import { formatDate, formatDateRange } from '@/utils/date'
  59 +import { getDictLabel, getIntDictOptions, getBoolDictOptions } from '@/hooks/useDict'
  60 +import { DICT_TYPE } from '@/utils/constants'
  61 +
  62 +const emit = defineEmits<{
  63 + search: [data: Record<string, any>]
  64 + reset: []
  65 +}>()
  66 +
  67 +const visible = ref(false) // 搜索弹窗显示状态
  68 +const formData = reactive({
  69 + name: undefined as string | undefined,
  70 + birthday: undefined as string | undefined,
  71 + sex: undefined as number | undefined,
  72 + enabled: undefined as boolean | undefined,
  73 + createTime: [undefined, undefined] as [number | undefined, number | undefined],
  74 +}) // 搜索表单数据
  75 +
  76 +/** 搜索条件 placeholder 拼接 */
  77 +const placeholder = computed(() => {
  78 + const conditions: string[] = []
  79 + if (formData.name) {
  80 + conditions.push(`名字:${formData.name}`)
  81 + }
  82 + if (formData.birthday) {
  83 + conditions.push(`出生日期:${formData.birthday}`)
  84 + }
  85 + if (formData.sex !== undefined) {
  86 + conditions.push(`性别:${getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, formData.sex)}`)
  87 + }
  88 + if (formData.enabled !== undefined) {
  89 + conditions.push(`是否有效:${getDictLabel(DICT_TYPE.INFRA_BOOLEAN_STRING, formData.enabled)}`)
  90 + }
  91 + if (formData.createTime?.[0] && formData.createTime?.[1]) {
  92 + conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
  93 + }
  94 + return conditions.length > 0 ? conditions.join(' | ') : '搜索学生'
  95 +})
  96 +
  97 +/** 搜索按钮操作 */
  98 +function handleSearch() {
  99 + visible.value = false
  100 + emit('search', {
  101 + ...formData,
  102 + createTime: formatDateRange(formData.createTime),
  103 + })
  104 +}
  105 +
  106 +/** 重置按钮操作 */
  107 +function handleReset() {
  108 + formData.name = undefined
  109 + formData.birthday = undefined
  110 + formData.sex = undefined
  111 + formData.enabled = undefined
  112 + formData.createTime = [undefined, undefined]
  113 + visible.value = false
  114 + emit('reset')
  115 +}
  116 +</script>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_inner/xml/InfraStudentMapper 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3 +<mapper namespace="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper">
  4 +
  5 + <!--
  6 + 一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
  7 + 无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
  8 + 代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
  9 + 文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
  10 + -->
  11 +
  12 +</mapper>
0 13 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/assert.json 0 → 100644
  1 +[ {
  2 + "contentPath" : "java/InfraStudentPageReqVO",
  3 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
  4 +}, {
  5 + "contentPath" : "java/InfraStudentRespVO",
  6 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
  7 +}, {
  8 + "contentPath" : "java/InfraStudentSaveReqVO",
  9 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
  10 +}, {
  11 + "contentPath" : "java/InfraStudentController",
  12 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
  13 +}, {
  14 + "contentPath" : "java/InfraStudentDO",
  15 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
  16 +}, {
  17 + "contentPath" : "java/InfraStudentContactDO",
  18 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
  19 +}, {
  20 + "contentPath" : "java/InfraStudentTeacherDO",
  21 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
  22 +}, {
  23 + "contentPath" : "java/InfraStudentMapper",
  24 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
  25 +}, {
  26 + "contentPath" : "java/InfraStudentContactMapper",
  27 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
  28 +}, {
  29 + "contentPath" : "java/InfraStudentTeacherMapper",
  30 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
  31 +}, {
  32 + "contentPath" : "xml/InfraStudentMapper",
  33 + "filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraStudentMapper.xml"
  34 +}, {
  35 + "contentPath" : "java/InfraStudentServiceImpl",
  36 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
  37 +}, {
  38 + "contentPath" : "java/InfraStudentService",
  39 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
  40 +}, {
  41 + "contentPath" : "java/InfraStudentServiceImplTest",
  42 + "filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
  43 +}, {
  44 + "contentPath" : "java/ErrorCodeConstants_手动操作",
  45 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
  46 +}, {
  47 + "contentPath" : "sql/sql",
  48 + "filePath" : "sql/sql.sql"
  49 +}, {
  50 + "contentPath" : "sql/h2",
  51 + "filePath" : "sql/h2.sql"
  52 +}, {
  53 + "contentPath" : "ts/index",
  54 + "filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
  55 +}, {
  56 + "contentPath" : "vue/index",
  57 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
  58 +}, {
  59 + "contentPath" : "vue/search-form",
  60 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
  61 +}, {
  62 + "contentPath" : "vue/form/index",
  63 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
  64 +}, {
  65 + "contentPath" : "vue/detail/index",
  66 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"
  67 +} ]
0 68 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/ErrorCodeConstants_手动操作 0 → 100644
  1 +// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
  2 +// ========== 学生 TODO 补充编号 ==========
  3 +ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
0 4 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactDO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import java.time.LocalDateTime;
  6 +import java.time.LocalDateTime;
  7 +import com.baomidou.mybatisplus.annotation.*;
  8 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  9 +
  10 +/**
  11 + * 学生联系人 DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("infra_student_contact")
  16 +@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class InfraStudentContactDO extends BaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 学生编号
  32 + */
  33 + private Long studentId;
  34 + /**
  35 + * 名字
  36 + */
  37 + private String name;
  38 + /**
  39 + * 简介
  40 + */
  41 + private String description;
  42 + /**
  43 + * 出生日期
  44 + */
  45 + private LocalDateTime birthday;
  46 + /**
  47 + * 性别
  48 + *
  49 + * 枚举 {@link TODO system_user_sex 对应的类}
  50 + */
  51 + private Integer sex;
  52 + /**
  53 + * 是否有效
  54 + *
  55 + * 枚举 {@link TODO infra_boolean_string 对应的类}
  56 + */
  57 + private Boolean enabled;
  58 + /**
  59 + * 头像
  60 + */
  61 + private String avatar;
  62 + /**
  63 + * 附件
  64 + */
  65 + private String video;
  66 + /**
  67 + * 备注
  68 + */
  69 + private String memo;
  70 +
  71 +}
0 72 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentContactMapper 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.mysql.demo;
  2 +
  3 +import java.util.*;
  4 +
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  8 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  9 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  10 +import org.apache.ibatis.annotations.Mapper;
  11 +
  12 +/**
  13 + * 学生联系人 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
  19 +
  20 + default List<InfraStudentContactDO> selectListByStudentId(Long studentId) {
  21 + return selectList(InfraStudentContactDO::getStudentId, studentId);
  22 + }
  23 +
  24 + default int deleteByStudentId(Long studentId) {
  25 + return delete(InfraStudentContactDO::getStudentId, studentId);
  26 + }
  27 +
  28 + default int deleteByStudentIds(List<Long> studentIds) {
  29 + return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
  30 + }
  31 +
  32 +}
0 33 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentController 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo;
  2 +
  3 +import org.springframework.web.bind.annotation.*;
  4 +import jakarta.annotation.Resource;
  5 +import org.springframework.validation.annotation.Validated;
  6 +import org.springframework.security.access.prepost.PreAuthorize;
  7 +import io.swagger.v3.oas.annotations.tags.Tag;
  8 +import io.swagger.v3.oas.annotations.Parameter;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +
  11 +import jakarta.validation.constraints.*;
  12 +import jakarta.validation.*;
  13 +import jakarta.servlet.http.*;
  14 +import java.util.*;
  15 +import java.io.IOException;
  16 +
  17 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  18 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  19 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  20 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  21 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  22 +
  23 +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
  24 +
  25 +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
  26 +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
  27 +
  28 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  29 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  30 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  31 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  32 +import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
  33 +
  34 +@Tag(name = "管理后台 - 学生")
  35 +@RestController
  36 +@RequestMapping("/infra/student")
  37 +@Validated
  38 +public class InfraStudentController {
  39 +
  40 + @Resource
  41 + private InfraStudentService studentService;
  42 +
  43 + @PostMapping("/create")
  44 + @Operation(summary = "创建学生")
  45 + @PreAuthorize("@ss.hasPermission('infra:student:create')")
  46 + public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
  47 + return success(studentService.createStudent(createReqVO));
  48 + }
  49 +
  50 + @PutMapping("/update")
  51 + @Operation(summary = "更新学生")
  52 + @PreAuthorize("@ss.hasPermission('infra:student:update')")
  53 + public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
  54 + studentService.updateStudent(updateReqVO);
  55 + return success(true);
  56 + }
  57 +
  58 + @DeleteMapping("/delete")
  59 + @Operation(summary = "删除学生")
  60 + @Parameter(name = "id", description = "编号", required = true)
  61 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  62 + public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
  63 + studentService.deleteStudent(id);
  64 + return success(true);
  65 + }
  66 +
  67 + @DeleteMapping("/delete-list")
  68 + @Parameter(name = "ids", description = "编号", required = true)
  69 + @Operation(summary = "批量删除学生")
  70 + @PreAuthorize("@ss.hasPermission('infra:student:delete')")
  71 + public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
  72 + studentService.deleteStudentListByIds(ids);
  73 + return success(true);
  74 + }
  75 +
  76 + @GetMapping("/get")
  77 + @Operation(summary = "获得学生")
  78 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  79 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  80 + public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
  81 + InfraStudentDO student = studentService.getStudent(id);
  82 + return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
  83 + }
  84 +
  85 + @GetMapping("/page")
  86 + @Operation(summary = "获得学生分页")
  87 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  88 + public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
  89 + PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
  90 + return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
  91 + }
  92 +
  93 + @GetMapping("/export-excel")
  94 + @Operation(summary = "导出学生 Excel")
  95 + @PreAuthorize("@ss.hasPermission('infra:student:export')")
  96 + @ApiAccessLog(operateType = EXPORT)
  97 + public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
  98 + HttpServletResponse response) throws IOException {
  99 + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
  100 + List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
  101 + // 导出 Excel
  102 + ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
  103 + BeanUtils.toBean(list, InfraStudentRespVO.class));
  104 + }
  105 +
  106 + // ==================== 子表(学生联系人) ====================
  107 +
  108 + @GetMapping("/student-contact/list-by-student-id")
  109 + @Operation(summary = "获得学生联系人列表")
  110 + @Parameter(name = "studentId", description = "学生编号")
  111 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  112 + public CommonResult<List<InfraStudentContactDO>> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) {
  113 + return success(studentService.getStudentContactListByStudentId(studentId));
  114 + }
  115 +
  116 + // ==================== 子表(学生班主任) ====================
  117 +
  118 + @GetMapping("/student-teacher/get-by-student-id")
  119 + @Operation(summary = "获得学生班主任")
  120 + @Parameter(name = "studentId", description = "学生编号")
  121 + @PreAuthorize("@ss.hasPermission('infra:student:query')")
  122 + public CommonResult<InfraStudentTeacherDO> getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) {
  123 + return success(studentService.getStudentTeacherByStudentId(studentId));
  124 + }
  125 +
  126 +}
0 127 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentDO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import java.time.LocalDateTime;
  6 +import java.time.LocalDateTime;
  7 +import com.baomidou.mybatisplus.annotation.*;
  8 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  9 +
  10 +/**
  11 + * 学生 DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("infra_student")
  16 +@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class InfraStudentDO extends BaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 名字
  32 + */
  33 + private String name;
  34 + /**
  35 + * 简介
  36 + */
  37 + private String description;
  38 + /**
  39 + * 出生日期
  40 + */
  41 + private LocalDateTime birthday;
  42 + /**
  43 + * 性别
  44 + *
  45 + * 枚举 {@link TODO system_user_sex 对应的类}
  46 + */
  47 + private Integer sex;
  48 + /**
  49 + * 是否有效
  50 + *
  51 + * 枚举 {@link TODO infra_boolean_string 对应的类}
  52 + */
  53 + private Boolean enabled;
  54 + /**
  55 + * 头像
  56 + */
  57 + private String avatar;
  58 + /**
  59 + * 附件
  60 + */
  61 + private String video;
  62 + /**
  63 + * 备注
  64 + */
  65 + private String memo;
  66 +
  67 +
  68 +}
0 69 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentMapper 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.mysql.demo;
  2 +
  3 +import java.util.*;
  4 +
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  7 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  8 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  9 +import org.apache.ibatis.annotations.Mapper;
  10 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  11 +
  12 +/**
  13 + * 学生 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
  19 +
  20 + default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
  21 + return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
  22 + .likeIfPresent(InfraStudentDO::getName, reqVO.getName())
  23 + .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
  24 + .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
  25 + .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
  26 + .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
  27 + .orderByDesc(InfraStudentDO::getId));
  28 + }
  29 +
  30 +}
0 31 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentPageReqVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +import java.time.LocalDateTime;
  9 +
  10 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  11 +
  12 +@Schema(description = "管理后台 - 学生分页 Request VO")
  13 +@Data
  14 +public class InfraStudentPageReqVO extends PageParam {
  15 +
  16 + @Schema(description = "名字", example = "芋头")
  17 + private String name;
  18 +
  19 + @Schema(description = "出生日期")
  20 + private LocalDateTime birthday;
  21 +
  22 + @Schema(description = "性别", example = "1")
  23 + private Integer sex;
  24 +
  25 + @Schema(description = "是否有效", example = "true")
  26 + private Boolean enabled;
  27 +
  28 + @Schema(description = "创建时间")
  29 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  30 + private LocalDateTime[] createTime;
  31 +
  32 +}
0 33 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentRespVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +import java.util.*;
  6 +import org.springframework.format.annotation.DateTimeFormat;
  7 +import java.time.LocalDateTime;
  8 +import cn.idev.excel.annotation.*;
  9 +import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
  10 +import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
  11 +
  12 +@Schema(description = "管理后台 - 学生 Response VO")
  13 +@Data
  14 +@ExcelIgnoreUnannotated
  15 +public class InfraStudentRespVO {
  16 +
  17 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  18 + @ExcelProperty("编号")
  19 + private Long id;
  20 +
  21 + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
  22 + @ExcelProperty("名字")
  23 + private String name;
  24 +
  25 + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
  26 + @ExcelProperty("简介")
  27 + private String description;
  28 +
  29 + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
  30 + @ExcelProperty("出生日期")
  31 + private LocalDateTime birthday;
  32 +
  33 + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  34 + @ExcelProperty(value = "性别", converter = DictConvert.class)
  35 + @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
  36 + private Integer sex;
  37 +
  38 + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  39 + @ExcelProperty(value = "是否有效", converter = DictConvert.class)
  40 + @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
  41 + private Boolean enabled;
  42 +
  43 + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
  44 + @ExcelProperty("头像")
  45 + private String avatar;
  46 +
  47 + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
  48 + @ExcelProperty("附件")
  49 + private String video;
  50 +
  51 + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
  52 + @ExcelProperty("备注")
  53 + private String memo;
  54 +
  55 + @Schema(description = "创建时间")
  56 + @ExcelProperty("创建时间")
  57 + private LocalDateTime createTime;
  58 +
  59 +}
0 60 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentSaveReqVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +import java.util.*;
  6 +import jakarta.validation.constraints.*;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +import java.time.LocalDateTime;
  9 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  10 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  11 +
  12 +@Schema(description = "管理后台 - 学生新增/修改 Request VO")
  13 +@Data
  14 +public class InfraStudentSaveReqVO {
  15 +
  16 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  17 + private Long id;
  18 +
  19 + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
  20 + @NotEmpty(message = "名字不能为空")
  21 + private String name;
  22 +
  23 + @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
  24 + @NotEmpty(message = "简介不能为空")
  25 + private String description;
  26 +
  27 + @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
  28 + @NotNull(message = "出生日期不能为空")
  29 + private LocalDateTime birthday;
  30 +
  31 + @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  32 + @NotNull(message = "性别不能为空")
  33 + private Integer sex;
  34 +
  35 + @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  36 + @NotNull(message = "是否有效不能为空")
  37 + private Boolean enabled;
  38 +
  39 + @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
  40 + @NotEmpty(message = "头像不能为空")
  41 + private String avatar;
  42 +
  43 + @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
  44 + @NotEmpty(message = "附件不能为空")
  45 + private String video;
  46 +
  47 + @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
  48 + @NotEmpty(message = "备注不能为空")
  49 + private String memo;
  50 +
  51 + @Schema(description = "学生联系人列表")
  52 + private List<InfraStudentContactDO> studentContacts;
  53 +
  54 + @Schema(description = "学生班主任")
  55 + private InfraStudentTeacherDO studentTeacher;
  56 +
  57 +}
0 58 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentService 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import java.util.*;
  4 +import jakarta.validation.*;
  5 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  6 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  7 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  8 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  9 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  10 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  11 +
  12 +/**
  13 + * 学生 Service 接口
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public interface InfraStudentService {
  18 +
  19 + /**
  20 + * 创建学生
  21 + *
  22 + * @param createReqVO 创建信息
  23 + * @return 编号
  24 + */
  25 + Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
  26 +
  27 + /**
  28 + * 更新学生
  29 + *
  30 + * @param updateReqVO 更新信息
  31 + */
  32 + void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
  33 +
  34 + /**
  35 + * 删除学生
  36 + *
  37 + * @param id 编号
  38 + */
  39 + void deleteStudent(Long id);
  40 +
  41 + /**
  42 + * 批量删除学生
  43 + *
  44 + * @param ids 编号
  45 + */
  46 + void deleteStudentListByIds(List<Long> ids);
  47 +
  48 + /**
  49 + * 获得学生
  50 + *
  51 + * @param id 编号
  52 + * @return 学生
  53 + */
  54 + InfraStudentDO getStudent(Long id);
  55 +
  56 + /**
  57 + * 获得学生分页
  58 + *
  59 + * @param pageReqVO 分页查询
  60 + * @return 学生分页
  61 + */
  62 + PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
  63 +
  64 + // ==================== 子表(学生联系人) ====================
  65 +
  66 + /**
  67 + * 获得学生联系人列表
  68 + *
  69 + * @param studentId 学生编号
  70 + * @return 学生联系人列表
  71 + */
  72 + List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId);
  73 +
  74 + // ==================== 子表(学生班主任) ====================
  75 +
  76 + /**
  77 + * 获得学生班主任
  78 + *
  79 + * @param studentId 学生编号
  80 + * @return 学生班主任
  81 + */
  82 + InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId);
  83 +
  84 +}
0 85 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImpl 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjectUtil;
  5 +import org.springframework.stereotype.Service;
  6 +import jakarta.annotation.Resource;
  7 +import org.springframework.validation.annotation.Validated;
  8 +import org.springframework.transaction.annotation.Transactional;
  9 +
  10 +import java.util.*;
  11 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  12 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  13 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
  14 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  15 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  16 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  17 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  18 +
  19 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
  20 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
  21 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
  22 +
  23 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  24 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  25 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
  26 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
  27 +
  28 +/**
  29 + * 学生 Service 实现类
  30 + *
  31 + * @author 芋道源码
  32 + */
  33 +@Service
  34 +@Validated
  35 +public class InfraStudentServiceImpl implements InfraStudentService {
  36 +
  37 + @Resource
  38 + private InfraStudentMapper studentMapper;
  39 + @Resource
  40 + private InfraStudentContactMapper studentContactMapper;
  41 + @Resource
  42 + private InfraStudentTeacherMapper studentTeacherMapper;
  43 +
  44 + @Override
  45 + @Transactional(rollbackFor = Exception.class)
  46 + public Long createStudent(InfraStudentSaveReqVO createReqVO) {
  47 + // 插入
  48 + InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
  49 + studentMapper.insert(student);
  50 +
  51 +
  52 + // 插入子表
  53 + createStudentContactList(student.getId(), createReqVO.getStudentContacts());
  54 + createStudentTeacher(student.getId(), createReqVO.getStudentTeacher());
  55 + // 返回
  56 + return student.getId();
  57 + }
  58 +
  59 + @Override
  60 + @Transactional(rollbackFor = Exception.class)
  61 + public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
  62 + // 校验存在
  63 + validateStudentExists(updateReqVO.getId());
  64 + // 更新
  65 + InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
  66 + studentMapper.updateById(updateObj);
  67 +
  68 + // 更新子表
  69 + updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts());
  70 + updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher());
  71 + }
  72 +
  73 + @Override
  74 + @Transactional(rollbackFor = Exception.class)
  75 + public void deleteStudent(Long id) {
  76 + // 校验存在
  77 + validateStudentExists(id);
  78 + // 删除
  79 + studentMapper.deleteById(id);
  80 +
  81 + // 删除子表
  82 + deleteStudentContactByStudentId(id);
  83 + deleteStudentTeacherByStudentId(id);
  84 + }
  85 +
  86 + @Override
  87 + @Transactional(rollbackFor = Exception.class)
  88 + public void deleteStudentListByIds(List<Long> ids) {
  89 + // 删除
  90 + studentMapper.deleteByIds(ids);
  91 +
  92 + // 删除子表
  93 + deleteStudentContactByStudentIds(ids);
  94 + deleteStudentTeacherByStudentIds(ids);
  95 + }
  96 +
  97 +
  98 + private void validateStudentExists(Long id) {
  99 + if (studentMapper.selectById(id) == null) {
  100 + throw exception(STUDENT_NOT_EXISTS);
  101 + }
  102 + }
  103 +
  104 + @Override
  105 + public InfraStudentDO getStudent(Long id) {
  106 + return studentMapper.selectById(id);
  107 + }
  108 +
  109 + @Override
  110 + public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
  111 + return studentMapper.selectPage(pageReqVO);
  112 + }
  113 +
  114 + // ==================== 子表(学生联系人) ====================
  115 +
  116 + @Override
  117 + public List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId) {
  118 + return studentContactMapper.selectListByStudentId(studentId);
  119 + }
  120 +
  121 + private void createStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
  122 + if (CollUtil.isEmpty(list)) {
  123 + return;
  124 + }
  125 + list.forEach(o -> o.setStudentId(studentId).clean());
  126 + studentContactMapper.insertBatch(list);
  127 + }
  128 +
  129 + private void updateStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
  130 + list.forEach(o -> o.setStudentId(studentId).clean());
  131 + List<InfraStudentContactDO> oldList = studentContactMapper.selectListByStudentId(studentId);
  132 + List<List<InfraStudentContactDO>> diffList = diffList(oldList, list, (oldVal, newVal) -> {
  133 + boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId());
  134 + if (same) {
  135 + newVal.setId(oldVal.getId()).clean(); // 解决更新情况下:updateTime 不更新
  136 + }
  137 + return same;
  138 + });
  139 +
  140 + // 第二步,批量添加、修改、删除
  141 + if (CollUtil.isNotEmpty(diffList.get(0))) {
  142 + studentContactMapper.insertBatch(diffList.get(0));
  143 + }
  144 + if (CollUtil.isNotEmpty(diffList.get(1))) {
  145 + studentContactMapper.updateBatch(diffList.get(1));
  146 + }
  147 + if (CollUtil.isNotEmpty(diffList.get(2))) {
  148 + studentContactMapper.deleteByIds(convertList(diffList.get(2), InfraStudentContactDO::getId));
  149 + }
  150 + }
  151 +
  152 + private void deleteStudentContactByStudentId(Long studentId) {
  153 + studentContactMapper.deleteByStudentId(studentId);
  154 + }
  155 +
  156 + private void deleteStudentContactByStudentIds(List<Long> studentIds) {
  157 + studentContactMapper.deleteByStudentIds(studentIds);
  158 + }
  159 +
  160 + // ==================== 子表(学生班主任) ====================
  161 +
  162 + @Override
  163 + public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) {
  164 + return studentTeacherMapper.selectByStudentId(studentId);
  165 + }
  166 +
  167 + private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
  168 + if (studentTeacher == null) {
  169 + return;
  170 + }
  171 + studentTeacher.setStudentId(studentId);
  172 + studentTeacherMapper.insert(studentTeacher);
  173 + }
  174 +
  175 + private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
  176 + if (studentTeacher == null) {
  177 + return;
  178 + }
  179 + studentTeacher.setStudentId(studentId).clean();// 解决更新情况下:updateTime 不更新
  180 + studentTeacherMapper.insertOrUpdate(studentTeacher);
  181 + }
  182 +
  183 + private void deleteStudentTeacherByStudentId(Long studentId) {
  184 + studentTeacherMapper.deleteByStudentId(studentId);
  185 + }
  186 +
  187 + private void deleteStudentTeacherByStudentIds(List<Long> studentIds) {
  188 + studentTeacherMapper.deleteByStudentIds(studentIds);
  189 + }
  190 +
  191 +}
0 192 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentServiceImplTest 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import org.junit.jupiter.api.Disabled;
  4 +import org.junit.jupiter.api.Test;
  5 +
  6 +import jakarta.annotation.Resource;
  7 +
  8 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  9 +
  10 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  11 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
  12 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
  13 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  14 +
  15 +import jakarta.annotation.Resource;
  16 +import org.springframework.context.annotation.Import;
  17 +import java.util.*;
  18 +import java.time.LocalDateTime;
  19 +
  20 +import static cn.hutool.core.util.RandomUtil.*;
  21 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
  22 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
  23 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
  24 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
  25 +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
  26 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
  27 +import static org.junit.jupiter.api.Assertions.*;
  28 +import static org.mockito.Mockito.*;
  29 +
  30 +/**
  31 + * {@link InfraStudentServiceImpl} 的单元测试类
  32 + *
  33 + * @author 芋道源码
  34 + */
  35 +@Import(InfraStudentServiceImpl.class)
  36 +public class InfraStudentServiceImplTest extends BaseDbUnitTest {
  37 +
  38 + @Resource
  39 + private InfraStudentServiceImpl studentService;
  40 +
  41 + @Resource
  42 + private InfraStudentMapper studentMapper;
  43 +
  44 + @Test
  45 + public void testCreateStudent_success() {
  46 + // 准备参数
  47 + InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
  48 +
  49 + // 调用
  50 + Long studentId = studentService.createStudent(createReqVO);
  51 + // 断言
  52 + assertNotNull(studentId);
  53 + // 校验记录的属性是否正确
  54 + InfraStudentDO student = studentMapper.selectById(studentId);
  55 + assertPojoEquals(createReqVO, student, "id");
  56 + }
  57 +
  58 + @Test
  59 + public void testUpdateStudent_success() {
  60 + // mock 数据
  61 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
  62 + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
  63 + // 准备参数
  64 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
  65 + o.setId(dbStudent.getId()); // 设置更新的 ID
  66 + });
  67 +
  68 + // 调用
  69 + studentService.updateStudent(updateReqVO);
  70 + // 校验是否更新正确
  71 + InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
  72 + assertPojoEquals(updateReqVO, student);
  73 + }
  74 +
  75 + @Test
  76 + public void testUpdateStudent_notExists() {
  77 + // 准备参数
  78 + InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
  79 +
  80 + // 调用, 并断言异常
  81 + assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
  82 + }
  83 +
  84 + @Test
  85 + public void testDeleteStudent_success() {
  86 + // mock 数据
  87 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
  88 + studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
  89 + // 准备参数
  90 + Long id = dbStudent.getId();
  91 +
  92 + // 调用
  93 + studentService.deleteStudent(id);
  94 + // 校验数据不存在了
  95 + assertNull(studentMapper.selectById(id));
  96 + }
  97 +
  98 + @Test
  99 + public void testDeleteStudent_notExists() {
  100 + // 准备参数
  101 + Long id = randomLongId();
  102 +
  103 + // 调用, 并断言异常
  104 + assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
  105 + }
  106 +
  107 + @Test
  108 + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
  109 + public void testGetStudentPage() {
  110 + // mock 数据
  111 + InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
  112 + o.setName(null);
  113 + o.setBirthday(null);
  114 + o.setSex(null);
  115 + o.setEnabled(null);
  116 + o.setCreateTime(null);
  117 + });
  118 + studentMapper.insert(dbStudent);
  119 + // 测试 name 不匹配
  120 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
  121 + // 测试 birthday 不匹配
  122 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
  123 + // 测试 sex 不匹配
  124 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
  125 + // 测试 enabled 不匹配
  126 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
  127 + // 测试 createTime 不匹配
  128 + studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
  129 + // 准备参数
  130 + InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
  131 + reqVO.setName(null);
  132 + reqVO.setBirthday(null);
  133 + reqVO.setSex(null);
  134 + reqVO.setEnabled(null);
  135 + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
  136 +
  137 + // 调用
  138 + PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
  139 + // 断言
  140 + assertEquals(1, pageResult.getTotal());
  141 + assertEquals(1, pageResult.getList().size());
  142 + assertPojoEquals(dbStudent, pageResult.getList().get(0));
  143 + }
  144 +
  145 +}
0 146 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherDO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import java.time.LocalDateTime;
  6 +import java.time.LocalDateTime;
  7 +import com.baomidou.mybatisplus.annotation.*;
  8 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  9 +
  10 +/**
  11 + * 学生班主任 DO
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@TableName("infra_student_teacher")
  16 +@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  17 +@Data
  18 +@EqualsAndHashCode(callSuper = true)
  19 +@ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
  23 +public class InfraStudentTeacherDO extends BaseDO {
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 学生编号
  32 + */
  33 + private Long studentId;
  34 + /**
  35 + * 名字
  36 + */
  37 + private String name;
  38 + /**
  39 + * 简介
  40 + */
  41 + private String description;
  42 + /**
  43 + * 出生日期
  44 + */
  45 + private LocalDateTime birthday;
  46 + /**
  47 + * 性别
  48 + *
  49 + * 枚举 {@link TODO system_user_sex 对应的类}
  50 + */
  51 + private Integer sex;
  52 + /**
  53 + * 是否有效
  54 + *
  55 + * 枚举 {@link TODO infra_boolean_string 对应的类}
  56 + */
  57 + private Boolean enabled;
  58 + /**
  59 + * 头像
  60 + */
  61 + private String avatar;
  62 + /**
  63 + * 附件
  64 + */
  65 + private String video;
  66 + /**
  67 + * 备注
  68 + */
  69 + private String memo;
  70 +
  71 +}
0 72 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/java/InfraStudentTeacherMapper 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.mysql.demo;
  2 +
  3 +import java.util.*;
  4 +
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  8 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  9 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
  10 +import org.apache.ibatis.annotations.Mapper;
  11 +
  12 +/**
  13 + * 学生班主任 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
  19 +
  20 + default InfraStudentTeacherDO selectByStudentId(Long studentId) {
  21 + return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
  22 + }
  23 +
  24 + default int deleteByStudentId(Long studentId) {
  25 + return delete(InfraStudentTeacherDO::getStudentId, studentId);
  26 + }
  27 +
  28 + default int deleteByStudentIds(List<Long> studentIds) {
  29 + return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds);
  30 + }
  31 +
  32 +}
0 33 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/h2 0 → 100644
  1 +-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
  2 +CREATE TABLE IF NOT EXISTS "infra_student" (
  3 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  4 + "name" varchar NOT NULL,
  5 + "description" varchar NOT NULL,
  6 + "birthday" varchar NOT NULL,
  7 + "sex" int NOT NULL,
  8 + "enabled" bit NOT NULL,
  9 + "avatar" varchar NOT NULL,
  10 + "video" varchar NOT NULL,
  11 + "memo" varchar NOT NULL,
  12 + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  13 + PRIMARY KEY ("id")
  14 +) COMMENT '学生表';
  15 +
  16 +-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
  17 +DELETE FROM "infra_student";
0 18 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/sql/sql 0 → 100644
  1 +-- 菜单 SQL
  2 +INSERT INTO system_menu(
  3 + name, permission, type, sort, parent_id,
  4 + path, icon, component, status, component_name
  5 +)
  6 +VALUES (
  7 + '学生管理', '', 2, 0, 888,
  8 + 'student', '', 'infra/demo/index', 0, 'InfraStudent'
  9 +);
  10 +
  11 +-- 按钮父菜单ID
  12 +SELECT @parentId := LAST_INSERT_ID();
  13 +
  14 +-- 按钮 SQL
  15 +INSERT INTO system_menu(
  16 + name, permission, type, sort, parent_id,
  17 + path, icon, component, status
  18 +)
  19 +VALUES (
  20 + '学生查询', 'infra:student:query', 3, 1, @parentId,
  21 + '', '', '', 0
  22 +);
  23 +INSERT INTO system_menu(
  24 + name, permission, type, sort, parent_id,
  25 + path, icon, component, status
  26 +)
  27 +VALUES (
  28 + '学生创建', 'infra:student:create', 3, 2, @parentId,
  29 + '', '', '', 0
  30 +);
  31 +INSERT INTO system_menu(
  32 + name, permission, type, sort, parent_id,
  33 + path, icon, component, status
  34 +)
  35 +VALUES (
  36 + '学生更新', 'infra:student:update', 3, 3, @parentId,
  37 + '', '', '', 0
  38 +);
  39 +INSERT INTO system_menu(
  40 + name, permission, type, sort, parent_id,
  41 + path, icon, component, status
  42 +)
  43 +VALUES (
  44 + '学生删除', 'infra:student:delete', 3, 4, @parentId,
  45 + '', '', '', 0
  46 +);
  47 +INSERT INTO system_menu(
  48 + name, permission, type, sort, parent_id,
  49 + path, icon, component, status
  50 +)
  51 +VALUES (
  52 + '学生导出', 'infra:student:export', 3, 5, @parentId,
  53 + '', '', '', 0
  54 +);
0 55 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/ts/index 0 → 100644
  1 +import type { PageParam, PageResult } from '@/http/types'
  2 +import { http } from '@/http/http'
  3 +
  4 +/** 学生联系人信息 */
  5 +export interface StudentContact {
  6 + id?: number
  7 + studentId?: number
  8 + name: string
  9 + description: string
  10 + birthday: number
  11 + sex: number
  12 + enabled: boolean
  13 + avatar: string
  14 + video?: string
  15 + memo: string
  16 + createTime?: number
  17 +}
  18 +
  19 +/** 学生班主任信息 */
  20 +export interface StudentTeacher {
  21 + id?: number
  22 + studentId?: number
  23 + name: string
  24 + description: string
  25 + birthday: number
  26 + sex: number
  27 + enabled: boolean
  28 + avatar: string
  29 + video?: string
  30 + memo: string
  31 + createTime?: number
  32 +}
  33 +
  34 +/** 学生信息 */
  35 +export interface Student {
  36 + id?: number
  37 + name: string
  38 + description: string
  39 + birthday: number
  40 + sex: number
  41 + enabled: boolean
  42 + avatar: string
  43 + video: string
  44 + memo: string
  45 + createTime?: number
  46 + studentContacts?: StudentContact[]
  47 + studentTeacher?: StudentTeacher | null
  48 +}
  49 +
  50 +/** 获取学生分页列表 */
  51 +export function getStudentPage(params: PageParam) {
  52 + return http.get<PageResult<Student>>('/infra/student/page', params)
  53 +}
  54 +
  55 +/** 获取学生详情 */
  56 +export function getStudent(id: number) {
  57 + return http.get<Student>(`/infra/student/get?id=${id}`)
  58 +}
  59 +
  60 +/** 创建学生 */
  61 +export function createStudent(data: Student) {
  62 + return http.post<number>('/infra/student/create', data)
  63 +}
  64 +
  65 +/** 更新学生 */
  66 +export function updateStudent(data: Student) {
  67 + return http.put<boolean>('/infra/student/update', data)
  68 +}
  69 +
  70 +/** 删除学生 */
  71 +export function deleteStudent(id: number) {
  72 + return http.delete<boolean>(`/infra/student/delete?id=${id}`)
  73 +}
  74 +
  75 +// ==================== 子表:学生联系人 ====================
  76 +
  77 +/** 获取学生联系人列表 */
  78 +export function getStudentContactListByStudentId(studentId: number) {
  79 + return http.get<StudentContact[]>(`/infra/student/student-contact/list-by-student-id?studentId=${studentId}`)
  80 +}
  81 +
  82 +// ==================== 子表:学生班主任 ====================
  83 +
  84 +/** 获取学生班主任 */
  85 +export function getStudentTeacherByStudentId(studentId: number) {
  86 + return http.get<StudentTeacher | null>(`/infra/student/student-teacher/get-by-student-id?studentId=${studentId}`)
  87 +}
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/detail/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + title="学生详情"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <view class="pb-160rpx">
  11 + <!-- 主表详情 -->
  12 + <wd-cell-group border title="学生信息">
  13 + <wd-cell title="编号" :value="formData?.id ?? '-'" />
  14 + <wd-cell title="名字" :value="formData?.name ?? '-'" />
  15 + <wd-cell title="简介" :value="formData?.description ?? '-'" />
  16 + <wd-cell title="出生日期" :value="formatDateTime(formData?.birthday) || '-'" />
  17 + <wd-cell title="性别">
  18 + <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="formData?.sex" />
  19 + </wd-cell>
  20 + <wd-cell title="是否有效">
  21 + <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.enabled" />
  22 + </wd-cell>
  23 + <wd-cell title="头像" :value="formData?.avatar ?? '-'" />
  24 + <wd-cell title="附件" :value="formData?.video ?? '-'" />
  25 + <wd-cell title="备注" :value="formData?.memo ?? '-'" />
  26 + <wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
  27 + </wd-cell-group>
  28 +
  29 + <!-- 子表:学生联系人 -->
  30 + <view class="mt-20rpx px-24rpx py-16rpx text-28rpx text-[#999]">
  31 + 学生联系人({{ studentContacts.length }})
  32 + </view>
  33 + <view class="px-24rpx">
  34 + <view v-if="!studentContacts.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
  35 + 暂无学生联系人
  36 + </view>
  37 + <view
  38 + v-for="item in studentContacts"
  39 + :key="item.id"
  40 + class="mb-16rpx rounded-12rpx bg-white p-24rpx shadow-sm"
  41 + >
  42 + <view class="mb-8rpx text-28rpx text-[#666]">
  43 + 学生编号:{{ item.studentId ?? '-' }}
  44 + </view>
  45 + <view class="mb-8rpx text-28rpx text-[#666]">
  46 + 名字:{{ item.name ?? '-' }}
  47 + </view>
  48 + <view class="mb-8rpx text-28rpx text-[#666]">
  49 + 简介:{{ item.description ?? '-' }}
  50 + </view>
  51 + <view class="mb-8rpx text-28rpx text-[#666]">
  52 + 出生日期:{{ item.birthday ?? '-' }}
  53 + </view>
  54 + <view class="mb-8rpx text-28rpx text-[#666]">
  55 + 性别:{{ item.sex ?? '-' }}
  56 + </view>
  57 + <view class="mb-8rpx text-28rpx text-[#666]">
  58 + 是否有效:{{ item.enabled ?? '-' }}
  59 + </view>
  60 + <view class="mb-8rpx text-28rpx text-[#666]">
  61 + 头像:{{ item.avatar ?? '-' }}
  62 + </view>
  63 + <view class="mb-8rpx text-28rpx text-[#666]">
  64 + 附件:{{ item.video ?? '-' }}
  65 + </view>
  66 + <view class="mb-8rpx text-28rpx text-[#666]">
  67 + 备注:{{ item.memo ?? '-' }}
  68 + </view>
  69 + <view class="mb-8rpx text-28rpx text-[#666]">
  70 + 创建时间:{{ item.createTime ?? '-' }}
  71 + </view>
  72 + </view>
  73 + </view>
  74 + <!-- 子表:学生班主任 -->
  75 + <view class="mt-20rpx px-24rpx py-16rpx text-28rpx text-[#999]">
  76 + 学生班主任
  77 + </view>
  78 + <view class="px-24rpx">
  79 + <view class="rounded-12rpx bg-white p-24rpx shadow-sm">
  80 + <template v-if="studentTeacher">
  81 + <view class="mb-8rpx text-28rpx text-[#666]">
  82 + 学生编号:{{ studentTeacher.studentId ?? '-' }}
  83 + </view>
  84 + <view class="mb-8rpx text-28rpx text-[#666]">
  85 + 名字:{{ studentTeacher.name ?? '-' }}
  86 + </view>
  87 + <view class="mb-8rpx text-28rpx text-[#666]">
  88 + 简介:{{ studentTeacher.description ?? '-' }}
  89 + </view>
  90 + <view class="mb-8rpx text-28rpx text-[#666]">
  91 + 出生日期:{{ studentTeacher.birthday ?? '-' }}
  92 + </view>
  93 + <view class="mb-8rpx text-28rpx text-[#666]">
  94 + 性别:{{ studentTeacher.sex ?? '-' }}
  95 + </view>
  96 + <view class="mb-8rpx text-28rpx text-[#666]">
  97 + 是否有效:{{ studentTeacher.enabled ?? '-' }}
  98 + </view>
  99 + <view class="mb-8rpx text-28rpx text-[#666]">
  100 + 头像:{{ studentTeacher.avatar ?? '-' }}
  101 + </view>
  102 + <view class="mb-8rpx text-28rpx text-[#666]">
  103 + 附件:{{ studentTeacher.video ?? '-' }}
  104 + </view>
  105 + <view class="mb-8rpx text-28rpx text-[#666]">
  106 + 备注:{{ studentTeacher.memo ?? '-' }}
  107 + </view>
  108 + <view class="mb-8rpx text-28rpx text-[#666]">
  109 + 创建时间:{{ studentTeacher.createTime ?? '-' }}
  110 + </view>
  111 + </template>
  112 + <view v-else class="text-center text-26rpx text-[#999]">
  113 + 暂无学生班主任
  114 + </view>
  115 + </view>
  116 + </view>
  117 + </view>
  118 +
  119 + <!-- 底部操作按钮 -->
  120 + <view class="yd-detail-footer">
  121 + <view class="yd-detail-footer-actions">
  122 + <wd-button
  123 + v-if="hasAccessByCodes(['infra:student:update'])"
  124 + class="flex-1" type="warning" @click="handleEdit"
  125 + >
  126 + 编辑
  127 + </wd-button>
  128 + <wd-button
  129 + v-if="hasAccessByCodes(['infra:student:delete'])"
  130 + class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
  131 + >
  132 + 删除
  133 + </wd-button>
  134 + </view>
  135 + </view>
  136 + </view>
  137 +</template>
  138 +
  139 +<script lang="ts" setup>
  140 +import type {
  141 + Student,
  142 + StudentContact,
  143 + StudentTeacher,
  144 +} from '@/api/infra/demo'
  145 +import { onShow } from '@dcloudio/uni-app'
  146 +import { ref } from 'vue'
  147 +import { useDialog } from '@wot-ui/ui/components/wd-dialog'
  148 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  149 +import {
  150 + deleteStudent,
  151 + getStudent,
  152 + getStudentContactListByStudentId,
  153 + getStudentTeacherByStudentId,
  154 +} from '@/api/infra/demo'
  155 +import { useAccess } from '@/hooks/useAccess'
  156 +import { delay, navigateBackPlus } from '@/utils'
  157 +import { DICT_TYPE } from '@/utils/constants'
  158 +import { formatDateTime } from '@/utils/date'
  159 +
  160 +const props = defineProps<{
  161 + id?: number
  162 +}>()
  163 +
  164 +definePage({
  165 + style: {
  166 + navigationBarTitleText: '',
  167 + navigationStyle: 'custom',
  168 + },
  169 +})
  170 +
  171 +const { hasAccessByCodes } = useAccess()
  172 +const dialog = useDialog()
  173 +const toast = useToast()
  174 +const formData = ref<Student>() // 主表数据
  175 +const studentContacts = ref<StudentContact[]>([]) // 学生联系人数据
  176 +const studentTeacher = ref<StudentTeacher | null>(null) // 学生班主任数据
  177 +const deleting = ref(false) // 删除状态
  178 +
  179 +/** 返回上一页 */
  180 +function handleBack() {
  181 + navigateBackPlus('/pages-infra/demo/index')
  182 +}
  183 +
  184 +/** 加载主表和子表详情 */
  185 +async function getDetail() {
  186 + if (!props.id || deleting.value) {
  187 + return
  188 + }
  189 + const [data, studentContactData, studentTeacherData] = await Promise.all([
  190 + getStudent(props.id),
  191 + getStudentContactListByStudentId(props.id),
  192 + getStudentTeacherByStudentId(props.id),
  193 + ])
  194 + formData.value = data
  195 + studentContacts.value = studentContactData || []
  196 + studentTeacher.value = studentTeacherData || null
  197 +}
  198 +
  199 +/** 编辑学生 */
  200 +function handleEdit() {
  201 + uni.navigateTo({ url: `/pages-infra/demo/form/index?id=${props.id}` })
  202 +}
  203 +
  204 +/** 删除学生 */
  205 +async function handleDelete() {
  206 + if (!props.id) {
  207 + return
  208 + }
  209 + try {
  210 + await dialog.confirm({ title: '提示', msg: '确定要删除该学生吗?' })
  211 + } catch {
  212 + return
  213 + }
  214 + deleting.value = true
  215 + try {
  216 + await deleteStudent(props.id)
  217 + toast.success('删除成功')
  218 + uni.$emit('infra:demo:reload')
  219 + delay(handleBack)
  220 + } finally {
  221 + deleting.value = false
  222 + }
  223 +}
  224 +
  225 +/** 进入页面加载(含编辑返回后刷新) */
  226 +onShow(() => {
  227 + getDetail()
  228 +})
  229 +</script>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/form/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + :title="getTitle"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <!-- 表单区域 -->
  11 + <view>
  12 + <wd-form ref="formRef" :model="formData" :schema="formSchema">
  13 + <wd-cell-group border>
  14 + <wd-form-item title="名字" title-width="180rpx" prop="name">
  15 + <wd-input
  16 + v-model="formData.name"
  17 + clearable
  18 + placeholder="请输入名字"
  19 + />
  20 + </wd-form-item>
  21 + <wd-form-item title="简介" title-width="180rpx" prop="description">
  22 + <wd-textarea
  23 + v-model="formData.description"
  24 + clearable
  25 + placeholder="请输入简介"
  26 + :maxlength="200"
  27 + show-word-limit
  28 + />
  29 + </wd-form-item>
  30 + <wd-form-item
  31 + title="出生日期"
  32 + title-width="180rpx"
  33 + prop="birthday"
  34 + is-link
  35 + :value="formData.birthday ? formatDateTime(formData.birthday) : ''"
  36 + placeholder="请选择出生日期"
  37 + @click="birthdayVisible = true"
  38 + />
  39 + <wd-datetime-picker
  40 + v-model:visible="birthdayVisible"
  41 + :model-value="formData.birthday || Date.now()"
  42 + type="datetime"
  43 + @update:model-value="value => formData.birthday = value"
  44 + />
  45 + <yd-form-picker
  46 + v-model="formData.sex"
  47 + label="性别"
  48 + label-width="180rpx"
  49 + prop="sex"
  50 + :dict-type="DICT_TYPE.SYSTEM_USER_SEX"
  51 + placeholder="请选择性别"
  52 + />
  53 + <wd-form-item title="是否有效" title-width="180rpx" prop="enabled" center>
  54 + <wd-radio-group v-model="formData.enabled" type="button">
  55 + <wd-radio
  56 + v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  57 + :key="dict.value"
  58 + :value="dict.value"
  59 + >
  60 + {{ dict.label }}
  61 + </wd-radio>
  62 + </wd-radio-group>
  63 + </wd-form-item>
  64 + <wd-form-item title="头像" title-width="180rpx" prop="avatar">
  65 + <yd-upload-img v-model="formData.avatar" directory="infra/demo" />
  66 + </wd-form-item>
  67 + <wd-form-item title="附件" title-width="180rpx" prop="video">
  68 + <yd-upload-file v-model="formData.video" directory="infra/demo" />
  69 + </wd-form-item>
  70 + <wd-form-item title="备注" title-width="180rpx" prop="memo">
  71 + <wd-input
  72 + v-model="formData.memo"
  73 + clearable
  74 + placeholder="请输入备注"
  75 + />
  76 + </wd-form-item>
  77 + </wd-cell-group>
  78 + </wd-form>
  79 +
  80 + <!-- 子表:学生联系人 -->
  81 + <view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
  82 + <text class="text-28rpx text-[#333] font-semibold">学生联系人</text>
  83 + <wd-button size="small" type="primary" variant="plain" @click="addStudentContact">
  84 + 添加
  85 + </wd-button>
  86 + </view>
  87 + <view class="px-24rpx">
  88 + <view v-if="!studentContacts.length" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
  89 + 暂无学生联系人
  90 + </view>
  91 + <view
  92 + v-for="(item, index) in studentContacts"
  93 + :key="index"
  94 + class="mb-20rpx rounded-12rpx bg-white p-24rpx shadow-sm"
  95 + >
  96 + <view class="mb-16rpx flex justify-end">
  97 + <wd-button size="small" type="danger" variant="plain" @click="removeStudentContact(index)">
  98 + 删除
  99 + </wd-button>
  100 + </view>
  101 + <wd-form-item title="名字" title-width="180rpx">
  102 + <wd-input v-model="item.name" clearable placeholder="请输入名字" />
  103 + </wd-form-item>
  104 + <wd-form-item title="简介" title-width="180rpx">
  105 + <wd-textarea v-model="item.description" clearable placeholder="请输入简介" />
  106 + </wd-form-item>
  107 + <wd-form-item
  108 + title="出生日期"
  109 + title-width="180rpx"
  110 + is-link
  111 + :value="item.birthday ? formatDateTime(item.birthday) : ''"
  112 + placeholder="请选择出生日期"
  113 + @click="subDateVisible[`studentContacts.${index}.birthday`] = true"
  114 + />
  115 + <wd-datetime-picker
  116 + v-model:visible="subDateVisible[`studentContacts.${index}.birthday`]"
  117 + :model-value="item.birthday || Date.now()"
  118 + type="datetime"
  119 + @update:model-value="value => item.birthday = value"
  120 + />
  121 + <yd-form-picker
  122 + v-model="item.sex"
  123 + label="性别"
  124 + label-width="180rpx"
  125 + :dict-type="DICT_TYPE.SYSTEM_USER_SEX"
  126 + placeholder="请选择性别"
  127 + />
  128 + <wd-form-item title="是否有效" title-width="180rpx" center>
  129 + <wd-radio-group v-model="item.enabled" type="button">
  130 + <wd-radio
  131 + v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  132 + :key="dict.value"
  133 + :value="dict.value"
  134 + >
  135 + {{ dict.label }}
  136 + </wd-radio>
  137 + </wd-radio-group>
  138 + </wd-form-item>
  139 + <wd-form-item title="头像" title-width="180rpx">
  140 + <yd-upload-img v-model="item.avatar" directory="infra/demo" />
  141 + </wd-form-item>
  142 + <wd-form-item title="附件" title-width="180rpx">
  143 + <yd-upload-file v-model="item.video" directory="infra/demo" />
  144 + </wd-form-item>
  145 + <wd-form-item title="备注" title-width="180rpx">
  146 + <wd-input v-model="item.memo" clearable placeholder="请输入备注" />
  147 + </wd-form-item>
  148 + </view>
  149 + </view>
  150 +
  151 + <!-- 子表:学生班主任 -->
  152 + <view class="mt-20rpx flex items-center justify-between px-24rpx py-16rpx">
  153 + <text class="text-28rpx text-[#333] font-semibold">学生班主任</text>
  154 + <wd-button
  155 + v-if="!studentTeacher"
  156 + size="small"
  157 + type="primary"
  158 + variant="plain"
  159 + @click="addStudentTeacher()"
  160 + >
  161 + 添加
  162 + </wd-button>
  163 + </view>
  164 + <view class="px-24rpx">
  165 + <view v-if="!studentTeacher" class="rounded-12rpx bg-white py-40rpx text-center text-26rpx text-[#999]">
  166 + 暂无学生班主任
  167 + </view>
  168 + <view v-else class="rounded-12rpx bg-white p-24rpx shadow-sm">
  169 + <wd-form-item title="名字" title-width="180rpx">
  170 + <wd-input v-model="studentTeacher.name" clearable placeholder="请输入名字" />
  171 + </wd-form-item>
  172 + <wd-form-item title="简介" title-width="180rpx">
  173 + <wd-textarea v-model="studentTeacher.description" clearable placeholder="请输入简介" />
  174 + </wd-form-item>
  175 + <wd-form-item
  176 + title="出生日期"
  177 + title-width="180rpx"
  178 + is-link
  179 + :value="studentTeacher.birthday ? formatDateTime(studentTeacher.birthday) : ''"
  180 + placeholder="请选择出生日期"
  181 + @click="subDateVisible['studentTeacher.birthday'] = true"
  182 + />
  183 + <wd-datetime-picker
  184 + v-model:visible="subDateVisible['studentTeacher.birthday']"
  185 + :model-value="studentTeacher.birthday || Date.now()"
  186 + type="datetime"
  187 + @update:model-value="value => studentTeacher.birthday = value"
  188 + />
  189 + <yd-form-picker
  190 + v-model="studentTeacher.sex"
  191 + label="性别"
  192 + label-width="180rpx"
  193 + :dict-type="DICT_TYPE.SYSTEM_USER_SEX"
  194 + placeholder="请选择性别"
  195 + />
  196 + <wd-form-item title="是否有效" title-width="180rpx" center>
  197 + <wd-radio-group v-model="studentTeacher.enabled" type="button">
  198 + <wd-radio
  199 + v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  200 + :key="dict.value"
  201 + :value="dict.value"
  202 + >
  203 + {{ dict.label }}
  204 + </wd-radio>
  205 + </wd-radio-group>
  206 + </wd-form-item>
  207 + <wd-form-item title="头像" title-width="180rpx">
  208 + <yd-upload-img v-model="studentTeacher.avatar" directory="infra/demo" />
  209 + </wd-form-item>
  210 + <wd-form-item title="附件" title-width="180rpx">
  211 + <yd-upload-file v-model="studentTeacher.video" directory="infra/demo" />
  212 + </wd-form-item>
  213 + <wd-form-item title="备注" title-width="180rpx">
  214 + <wd-input v-model="studentTeacher.memo" clearable placeholder="请输入备注" />
  215 + </wd-form-item>
  216 + </view>
  217 + </view>
  218 + </view>
  219 +
  220 + <!-- 底部保存按钮 -->
  221 + <view class="yd-detail-footer">
  222 + <wd-button
  223 + type="primary"
  224 + block
  225 + :loading="formLoading"
  226 + @click="handleSubmit"
  227 + >
  228 + 保存
  229 + </wd-button>
  230 + </view>
  231 + </view>
  232 +</template>
  233 +
  234 +<script lang="ts" setup>
  235 +import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
  236 +import type {
  237 + Student,
  238 + StudentContact,
  239 + StudentTeacher,
  240 +} from '@/api/infra/demo'
  241 +import { computed, onMounted, ref } from 'vue'
  242 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  243 +import {
  244 + createStudent,
  245 + getStudent,
  246 + getStudentContactListByStudentId,
  247 + getStudentTeacherByStudentId,
  248 + updateStudent,
  249 +} from '@/api/infra/demo'
  250 +import { getBoolDictOptions } from '@/hooks/useDict'
  251 +import { delay, navigateBackPlus } from '@/utils'
  252 +import { DICT_TYPE } from '@/utils/constants'
  253 +import { formatDateTime } from '@/utils/date'
  254 +import { createFormSchema } from '@/utils/wot'
  255 +
  256 +const props = defineProps<{
  257 + id?: number
  258 +}>()
  259 +
  260 +definePage({
  261 + style: {
  262 + navigationBarTitleText: '',
  263 + navigationStyle: 'custom',
  264 + },
  265 +})
  266 +
  267 +const toast = useToast()
  268 +const getTitle = computed(() => props.id ? '编辑学生' : '新增学生')
  269 +const formLoading = ref(false) // 表单提交状态
  270 +const formData = ref<Student>({
  271 + id: undefined,
  272 + name: '',
  273 + description: '',
  274 + birthday: 0,
  275 + sex: 0,
  276 + enabled: false,
  277 + avatar: '',
  278 + video: '',
  279 + memo: '',
  280 +}) // 表单数据
  281 +const studentContacts = ref<StudentContact[]>([]) // 学生联系人数据
  282 +const studentTeacher = ref<StudentTeacher | null>(null) // 学生班主任数据
  283 +const birthdayVisible = ref(false) // 出生日期选择器状态
  284 +const subDateVisible = ref<Record<string, boolean>>({}) // 子表日期选择器状态
  285 +const formSchema = createFormSchema({
  286 + name: [{ required: true, message: '名字不能为空' }],
  287 + description: [{ required: true, message: '简介不能为空' }],
  288 + birthday: [{ required: true, message: '出生日期不能为空' }],
  289 + sex: [{ required: true, message: '性别不能为空' }],
  290 + enabled: [{ required: true, message: '是否有效不能为空' }],
  291 + avatar: [{ required: true, message: '头像不能为空' }],
  292 + video: [{ required: true, message: '附件不能为空' }],
  293 + memo: [{ required: true, message: '备注不能为空' }],
  294 +}) // 表单校验规则
  295 +const formRef = ref<FormInstance>() // 表单组件引用
  296 +
  297 +/** 添加学生联系人 */
  298 +function addStudentContact() {
  299 + studentContacts.value.push({
  300 + name: '',
  301 + description: '',
  302 + birthday: 0,
  303 + sex: 0,
  304 + enabled: false,
  305 + avatar: '',
  306 + video: undefined,
  307 + memo: '',
  308 + })
  309 +}
  310 +
  311 +/** 删除学生联系人 */
  312 +function removeStudentContact(index: number) {
  313 + studentContacts.value.splice(index, 1)
  314 +}
  315 +
  316 +/** 添加学生班主任 */
  317 +function addStudentTeacher() {
  318 + studentTeacher.value = {
  319 + name: '',
  320 + description: '',
  321 + birthday: 0,
  322 + sex: 0,
  323 + enabled: false,
  324 + avatar: '',
  325 + video: undefined,
  326 + memo: '',
  327 + }
  328 +}
  329 +
  330 +/** 校验主子表数据 */
  331 +function isEmptySubValue(value: unknown) {
  332 + return value === undefined || value === null || value === ''
  333 +}
  334 +
  335 +function validateSubTables() {
  336 + for (let index = 0; index < studentContacts.value.length; index++) {
  337 + const item = studentContacts.value[index]!
  338 + if (isEmptySubValue(item.name)) {
  339 + toast.warning(`学生联系人第${index + 1}项的名字不能为空`)
  340 + return false
  341 + }
  342 + if (isEmptySubValue(item.description)) {
  343 + toast.warning(`学生联系人第${index + 1}项的简介不能为空`)
  344 + return false
  345 + }
  346 + if (isEmptySubValue(item.birthday)) {
  347 + toast.warning(`学生联系人第${index + 1}项的出生日期不能为空`)
  348 + return false
  349 + }
  350 + if (isEmptySubValue(item.sex)) {
  351 + toast.warning(`学生联系人第${index + 1}项的性别不能为空`)
  352 + return false
  353 + }
  354 + if (isEmptySubValue(item.enabled)) {
  355 + toast.warning(`学生联系人第${index + 1}项的是否有效不能为空`)
  356 + return false
  357 + }
  358 + if (isEmptySubValue(item.avatar)) {
  359 + toast.warning(`学生联系人第${index + 1}项的头像不能为空`)
  360 + return false
  361 + }
  362 + if (isEmptySubValue(item.memo)) {
  363 + toast.warning(`学生联系人第${index + 1}项的备注不能为空`)
  364 + return false
  365 + }
  366 + }
  367 + if (studentTeacher.value) {
  368 + if (isEmptySubValue(studentTeacher.value.name)) {
  369 + toast.warning('学生班主任的名字不能为空')
  370 + return false
  371 + }
  372 + if (isEmptySubValue(studentTeacher.value.description)) {
  373 + toast.warning('学生班主任的简介不能为空')
  374 + return false
  375 + }
  376 + if (isEmptySubValue(studentTeacher.value.birthday)) {
  377 + toast.warning('学生班主任的出生日期不能为空')
  378 + return false
  379 + }
  380 + if (isEmptySubValue(studentTeacher.value.sex)) {
  381 + toast.warning('学生班主任的性别不能为空')
  382 + return false
  383 + }
  384 + if (isEmptySubValue(studentTeacher.value.enabled)) {
  385 + toast.warning('学生班主任的是否有效不能为空')
  386 + return false
  387 + }
  388 + if (isEmptySubValue(studentTeacher.value.avatar)) {
  389 + toast.warning('学生班主任的头像不能为空')
  390 + return false
  391 + }
  392 + if (isEmptySubValue(studentTeacher.value.memo)) {
  393 + toast.warning('学生班主任的备注不能为空')
  394 + return false
  395 + }
  396 + }
  397 + return true
  398 +}
  399 +
  400 +/** 返回上一页 */
  401 +function handleBack() {
  402 + navigateBackPlus('/pages-infra/demo/index')
  403 +}
  404 +
  405 +/** 加载学生详情 */
  406 +async function getDetail() {
  407 + if (!props.id) {
  408 + return
  409 + }
  410 + const [data, studentContactData, studentTeacherData] = await Promise.all([
  411 + getStudent(props.id),
  412 + getStudentContactListByStudentId(props.id),
  413 + getStudentTeacherByStudentId(props.id),
  414 + ])
  415 + formData.value = data
  416 + studentContacts.value = studentContactData || []
  417 + studentTeacher.value = studentTeacherData || null
  418 +}
  419 +
  420 +/** 提交表单 */
  421 +async function handleSubmit() {
  422 + const { valid } = await formRef.value.validate()
  423 + if (!valid) {
  424 + return
  425 + }
  426 + if (!validateSubTables()) {
  427 + return
  428 + }
  429 +
  430 + formLoading.value = true
  431 + try {
  432 + const data: Student = {
  433 + ...formData.value,
  434 + studentContacts: studentContacts.value,
  435 + studentTeacher: studentTeacher.value,
  436 + }
  437 + if (props.id) {
  438 + await updateStudent(data)
  439 + toast.success('修改成功')
  440 + } else {
  441 + await createStudent(data)
  442 + toast.success('新增成功')
  443 + }
  444 + uni.$emit('infra:demo:reload')
  445 + delay(handleBack)
  446 + } finally {
  447 + formLoading.value = false
  448 + }
  449 +}
  450 +
  451 +/** 初始化 */
  452 +onMounted(async () => {
  453 + getDetail()
  454 +})
  455 +</script>
  456 +
  457 +<style lang="scss" scoped>
  458 +</style>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container yd-page-container-paging">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + title="学生管理"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <!-- 搜索组件 -->
  11 + <SearchForm @search="handleQuery" @reset="handleReset" />
  12 +
  13 + <!-- 学生列表 -->
  14 + <z-paging
  15 + ref="pagingRef"
  16 + v-model="list"
  17 + :fixed="false"
  18 + class="min-h-0 flex-1"
  19 + :default-page-size="10"
  20 + :refresher-enabled="true"
  21 + :inside-more="true"
  22 + :loading-more-default-as-loading="true"
  23 + empty-view-text="暂无学生数据"
  24 + @query="queryList"
  25 + >
  26 + <view class="p-24rpx">
  27 + <view
  28 + v-for="item in list"
  29 + :key="item.id"
  30 + class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
  31 + @click="handleDetail(item)"
  32 + >
  33 + <view class="p-24rpx">
  34 + <view class="mb-16rpx flex items-center justify-between">
  35 + <view class="text-32rpx text-[#333] font-semibold">
  36 + {{ item.name }}
  37 + </view>
  38 + <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="item.sex" />
  39 + </view>
  40 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  41 + <text class="mr-8rpx text-[#999]">简介:</text>
  42 + <text class="line-clamp-1">{{ item.description }}</text>
  43 + </view>
  44 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  45 + <text class="mr-8rpx text-[#999]">出生日期:</text>
  46 + <text class="line-clamp-1">{{ formatDateTime(item.birthday) || '-' }}</text>
  47 + </view>
  48 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  49 + <text class="mr-8rpx text-[#999]">是否有效:</text>
  50 + <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="item.enabled" />
  51 + </view>
  52 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  53 + <text class="mr-8rpx text-[#999]">头像:</text>
  54 + <text class="line-clamp-1">{{ item.avatar }}</text>
  55 + </view>
  56 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  57 + <text class="mr-8rpx text-[#999]">附件:</text>
  58 + <text class="line-clamp-1">{{ item.video }}</text>
  59 + </view>
  60 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  61 + <text class="mr-8rpx text-[#999]">备注:</text>
  62 + <text class="line-clamp-1">{{ item.memo }}</text>
  63 + </view>
  64 + <view class="mb-12rpx flex items-center text-28rpx text-[#666]">
  65 + <text class="mr-8rpx text-[#999]">创建时间:</text>
  66 + <text class="line-clamp-1">{{ formatDateTime(item.createTime) || '-' }}</text>
  67 + </view>
  68 + </view>
  69 + </view>
  70 + </view>
  71 + </z-paging>
  72 +
  73 + <!-- 新增按钮 -->
  74 + <wd-fab
  75 + v-if="hasAccessByCodes(['infra:student:create'])"
  76 + position="right-bottom"
  77 + type="primary"
  78 + :expandable="false"
  79 + @click="handleAdd"
  80 + />
  81 + </view>
  82 +</template>
  83 +
  84 +<script lang="ts" setup>
  85 +import type { Student } from '@/api/infra/demo'
  86 +import { onUnload } from '@dcloudio/uni-app'
  87 +import { onMounted, ref } from 'vue'
  88 +import { getStudentPage } from '@/api/infra/demo'
  89 +import { useAccess } from '@/hooks/useAccess'
  90 +import { navigateBackPlus } from '@/utils'
  91 +import { DICT_TYPE } from '@/utils/constants'
  92 +import { formatDateTime } from '@/utils/date'
  93 +import SearchForm from './components/search-form.vue'
  94 +
  95 +definePage({
  96 + style: {
  97 + navigationBarTitleText: '',
  98 + navigationStyle: 'custom',
  99 + },
  100 +})
  101 +
  102 +const { hasAccessByCodes } = useAccess()
  103 +const list = ref<Student[]>([]) // 列表数据
  104 +const pagingRef = ref<any>() // 分页组件引用
  105 +const queryParams = ref<Record<string, any>>({}) // 查询参数
  106 +
  107 +/** 返回上一页 */
  108 +function handleBack() {
  109 + navigateBackPlus()
  110 +}
  111 +
  112 +/** 查询学生列表 */
  113 +async function queryList(pageNo: number, pageSize: number) {
  114 + try {
  115 + const params = {
  116 + ...queryParams.value,
  117 + pageNo,
  118 + pageSize,
  119 + }
  120 + const data = await getStudentPage(params)
  121 + pagingRef.value?.completeByTotal(data.list, data.total)
  122 + } catch {
  123 + pagingRef.value?.complete(false)
  124 + }
  125 +}
  126 +
  127 +/** 搜索按钮操作 */
  128 +function handleQuery(data?: Record<string, any>) {
  129 + queryParams.value = { ...data }
  130 + reload()
  131 +}
  132 +
  133 +/** 重置按钮操作 */
  134 +function handleReset() {
  135 + handleQuery()
  136 +}
  137 +
  138 +/** 重新加载 */
  139 +function reload() {
  140 + pagingRef.value?.reload()
  141 +}
  142 +
  143 +/** 新增学生 */
  144 +function handleAdd() {
  145 + uni.navigateTo({
  146 + url: '/pages-infra/demo/form/index',
  147 + })
  148 +}
  149 +
  150 +/** 查看详情 */
  151 +function handleDetail(item: Student) {
  152 + uni.navigateTo({
  153 + url: `/pages-infra/demo/detail/index?id=${item.id}`,
  154 + })
  155 +}
  156 +
  157 +/** 初始化 */
  158 +onMounted(() => {
  159 + uni.$on('infra:demo:reload', reload)
  160 +})
  161 +
  162 +/** 卸载 */
  163 +onUnload(() => {
  164 + uni.$off('infra:demo:reload', reload)
  165 +})
  166 +</script>
  167 +
  168 +<style lang="scss" scoped>
  169 +</style>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/vue/search-form 0 → 100644
  1 +<template>
  2 + <!-- 搜索框入口 -->
  3 + <view @click="visible = true">
  4 + <wd-search :placeholder="placeholder" hide-cancel disabled />
  5 + </view>
  6 +
  7 + <!-- 搜索弹窗 -->
  8 + <wd-popup v-model="visible" position="top" @close="visible = false">
  9 + <view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
  10 + <view class="yd-search-form-item">
  11 + <view class="yd-search-form-label">
  12 + 名字
  13 + </view>
  14 + <wd-input
  15 + v-model="formData.name"
  16 + placeholder="请输入名字"
  17 + clearable
  18 + />
  19 + </view>
  20 + <view class="yd-search-form-item">
  21 + <view class="yd-search-form-label">
  22 + 出生日期
  23 + </view>
  24 + <wd-input
  25 + v-model="formData.birthday"
  26 + placeholder="请输入出生日期"
  27 + clearable
  28 + />
  29 + </view>
  30 + <yd-search-picker
  31 + v-model="formData.sex"
  32 + label="性别"
  33 + :columns="getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
  34 + all-option
  35 + />
  36 + <yd-search-picker
  37 + v-model="formData.enabled"
  38 + label="是否有效"
  39 + :columns="getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
  40 + all-option
  41 + />
  42 + <yd-search-date-range v-model="formData.createTime" label="创建时间" />
  43 + <view class="yd-search-form-actions">
  44 + <wd-button class="flex-1" plain @click="handleReset">
  45 + 重置
  46 + </wd-button>
  47 + <wd-button class="flex-1" type="primary" @click="handleSearch">
  48 + 搜索
  49 + </wd-button>
  50 + </view>
  51 + </view>
  52 + </wd-popup>
  53 +</template>
  54 +
  55 +<script lang="ts" setup>
  56 +import { computed, reactive, ref } from 'vue'
  57 +import { getNavbarHeight } from '@/utils'
  58 +import { formatDate, formatDateRange } from '@/utils/date'
  59 +import { getDictLabel, getIntDictOptions, getBoolDictOptions } from '@/hooks/useDict'
  60 +import { DICT_TYPE } from '@/utils/constants'
  61 +
  62 +const emit = defineEmits<{
  63 + search: [data: Record<string, any>]
  64 + reset: []
  65 +}>()
  66 +
  67 +const visible = ref(false) // 搜索弹窗显示状态
  68 +const formData = reactive({
  69 + name: undefined as string | undefined,
  70 + birthday: undefined as string | undefined,
  71 + sex: undefined as number | undefined,
  72 + enabled: undefined as boolean | undefined,
  73 + createTime: [undefined, undefined] as [number | undefined, number | undefined],
  74 +}) // 搜索表单数据
  75 +
  76 +/** 搜索条件 placeholder 拼接 */
  77 +const placeholder = computed(() => {
  78 + const conditions: string[] = []
  79 + if (formData.name) {
  80 + conditions.push(`名字:${formData.name}`)
  81 + }
  82 + if (formData.birthday) {
  83 + conditions.push(`出生日期:${formData.birthday}`)
  84 + }
  85 + if (formData.sex !== undefined) {
  86 + conditions.push(`性别:${getDictLabel(DICT_TYPE.SYSTEM_USER_SEX, formData.sex)}`)
  87 + }
  88 + if (formData.enabled !== undefined) {
  89 + conditions.push(`是否有效:${getDictLabel(DICT_TYPE.INFRA_BOOLEAN_STRING, formData.enabled)}`)
  90 + }
  91 + if (formData.createTime?.[0] && formData.createTime?.[1]) {
  92 + conditions.push(`创建时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
  93 + }
  94 + return conditions.length > 0 ? conditions.join(' | ') : '搜索学生'
  95 +})
  96 +
  97 +/** 搜索按钮操作 */
  98 +function handleSearch() {
  99 + visible.value = false
  100 + emit('search', {
  101 + ...formData,
  102 + createTime: formatDateRange(formData.createTime),
  103 + })
  104 +}
  105 +
  106 +/** 重置按钮操作 */
  107 +function handleReset() {
  108 + formData.name = undefined
  109 + formData.birthday = undefined
  110 + formData.sex = undefined
  111 + formData.enabled = undefined
  112 + formData.createTime = [undefined, undefined]
  113 + visible.value = false
  114 + emit('reset')
  115 +}
  116 +</script>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_master_normal/xml/InfraStudentMapper 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3 +<mapper namespace="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper">
  4 +
  5 + <!--
  6 + 一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
  7 + 无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
  8 + 代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
  9 + 文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
  10 + -->
  11 +
  12 +</mapper>
0 13 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/ts/index
... ... @@ -6,13 +6,13 @@ export interface Student {
6 6 id?: number
7 7 name: string
8 8 description: string
9   - birthday: Date
  9 + birthday: number
10 10 sex: number
11 11 enabled: boolean
12 12 avatar: string
13 13 video: string
14 14 memo: string
15   - createTime?: Date
  15 + createTime?: number
16 16 }
17 17  
18 18 /** 获取学生分页列表 */
... ... @@ -38,4 +38,4 @@ export function updateStudent(data: Student) {
38 38 /** 删除学生 */
39 39 export function deleteStudent(id: number) {
40 40 return http.delete<boolean>(`/infra/student/delete?id=${id}`)
41   -}
42 41 \ No newline at end of file
  42 +}
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/detail/index
... ... @@ -38,7 +38,7 @@
38 38 </wd-button>
39 39 <wd-button
40 40 v-if="hasAccessByCodes(['infra:student:delete'])"
41   - class="flex-1" type="error" :loading="deleting" @click="handleDelete"
  41 + class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
42 42 >
43 43 删除
44 44 </wd-button>
... ... @@ -51,6 +51,7 @@
51 51 import type { Student } from '@/api/infra/demo'
52 52 import { onUnload } from '@dcloudio/uni-app'
53 53 import { onMounted, ref } from 'vue'
  54 +import { useDialog } from '@wot-ui/ui/components/wd-dialog'
54 55 import { useToast } from '@wot-ui/ui/components/wd-toast'
55 56 import { deleteStudent, getStudent } from '@/api/infra/demo'
56 57 import { useAccess } from '@/hooks/useAccess'
... ... @@ -59,7 +60,7 @@ import { DICT_TYPE } from &#39;@/utils/constants&#39;
59 60 import { formatDateTime } from '@/utils/date'
60 61  
61 62 const props = defineProps<{
62   - id?: number | any
  63 + id?: number
63 64 }>()
64 65  
65 66 definePage({
... ... @@ -70,6 +71,7 @@ definePage({
70 71 })
71 72  
72 73 const { hasAccessByCodes } = useAccess()
  74 +const dialog = useDialog()
73 75 const toast = useToast()
74 76 const formData = ref<Student>() // 详情数据
75 77 const deleting = ref(false) // 删除状态
... ... @@ -100,28 +102,27 @@ function handleEdit() {
100 102 }
101 103  
102 104 /** 删除学生 */
103   -function handleDelete() {
  105 +async function handleDelete() {
104 106 if (!props.id) {
105 107 return
106 108 }
107   - uni.showModal({
108   - title: '提示',
109   - content: '确定要删除该学生吗?',
110   - success: async (res) => {
111   - if (!res.confirm) {
112   - return
113   - }
114   - deleting.value = true
115   - try {
116   - await deleteStudent(props.id)
117   - toast.success('删除成功')
118   - uni.$emit('infra:demo:reload')
119   - delay(handleBack)
120   - } finally {
121   - deleting.value = false
122   - }
123   - },
124   - })
  109 + try {
  110 + await dialog.confirm({
  111 + title: '提示',
  112 + msg: '确定要删除该学生吗?',
  113 + })
  114 + } catch {
  115 + return
  116 + }
  117 + deleting.value = true
  118 + try {
  119 + await deleteStudent(props.id)
  120 + toast.success('删除成功')
  121 + uni.$emit('infra:demo:reload')
  122 + delay(handleBack)
  123 + } finally {
  124 + deleting.value = false
  125 + }
125 126 }
126 127  
127 128 /** 初始化 */
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_one/vue/form/index
... ... @@ -11,14 +11,14 @@
11 11 <view>
12 12 <wd-form ref="formRef" :model="formData" :schema="formSchema">
13 13 <wd-cell-group border>
14   - <wd-form-item title="名字" title-width="180rpx" prop="name">
  14 + <wd-form-item title="名字" title-width="180rpx" prop="name">
15 15 <wd-input
16 16 v-model="formData.name"
17 17 clearable
18 18 placeholder="请输入名字"
19 19 />
20 20 </wd-form-item>
21   - <wd-form-item title="简介" title-width="180rpx" prop="description">
  21 + <wd-form-item title="简介" title-width="180rpx" prop="description">
22 22 <wd-textarea
23 23 v-model="formData.description"
24 24 clearable
... ... @@ -27,21 +27,22 @@
27 27 show-word-limit
28 28 />
29 29 </wd-form-item>
30   - <wd-form-item
  30 + <wd-form-item
31 31 title="出生日期"
32 32 title-width="180rpx"
33 33 prop="birthday"
34 34 is-link
35   - :value="formatDateTime(formData.birthday)"
  35 + :value="formData.birthday ? formatDateTime(formData.birthday) : ''"
36 36 placeholder="请选择出生日期"
37 37 @click="birthdayVisible = true"
38 38 />
39 39 <wd-datetime-picker
40   - v-model="formData.birthday"
41 40 v-model:visible="birthdayVisible"
  41 + :model-value="formData.birthday || Date.now()"
42 42 type="datetime"
  43 + @update:model-value="value => formData.birthday = value"
43 44 />
44   - <yd-form-picker
  45 + <yd-form-picker
45 46 v-model="formData.sex"
46 47 label="性别"
47 48 label-width="180rpx"
... ... @@ -49,7 +50,7 @@
49 50 :dict-type="DICT_TYPE.SYSTEM_USER_SEX"
50 51 placeholder="请选择性别"
51 52 />
52   - <wd-form-item title="是否有效" title-width="180rpx" prop="enabled" center>
  53 + <wd-form-item title="是否有效" title-width="180rpx" prop="enabled" center>
53 54 <wd-radio-group v-model="formData.enabled" type="button">
54 55 <wd-radio
55 56 v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
... ... @@ -60,13 +61,13 @@
60 61 </wd-radio>
61 62 </wd-radio-group>
62 63 </wd-form-item>
63   - <wd-form-item title="头像" title-width="180rpx" prop="avatar">
  64 + <wd-form-item title="头像" title-width="180rpx" prop="avatar">
64 65 <yd-upload-img v-model="formData.avatar" directory="infra/demo" />
65 66 </wd-form-item>
66   - <wd-form-item title="附件" title-width="180rpx" prop="video">
  67 + <wd-form-item title="附件" title-width="180rpx" prop="video">
67 68 <yd-upload-file v-model="formData.video" directory="infra/demo" />
68 69 </wd-form-item>
69   - <wd-form-item title="备注" title-width="180rpx" prop="memo">
  70 + <wd-form-item title="备注" title-width="180rpx" prop="memo">
70 71 <wd-input
71 72 v-model="formData.memo"
72 73 clearable
... ... @@ -93,7 +94,9 @@
93 94  
94 95 <script lang="ts" setup>
95 96 import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
96   -import type { Student } from '@/api/infra/demo'
  97 +import type {
  98 + Student,
  99 +} from '@/api/infra/demo'
97 100 import { computed, onMounted, ref } from 'vue'
98 101 import { useToast } from '@wot-ui/ui/components/wd-toast'
99 102 import { createStudent, getStudent, updateStudent } from '@/api/infra/demo'
... ... @@ -104,7 +107,7 @@ import { formatDateTime } from &#39;@/utils/date&#39;
104 107 import { createFormSchema } from '@/utils/wot'
105 108  
106 109 const props = defineProps<{
107   - id?: number | any
  110 + id?: number
108 111 }>()
109 112  
110 113 definePage({
... ... @@ -121,7 +124,7 @@ const formData = ref&lt;Student&gt;({
121 124 id: undefined,
122 125 name: '',
123 126 description: '',
124   - birthday: undefined,
  127 + birthday: 0,
125 128 sex: 0,
126 129 enabled: false,
127 130 avatar: '',
... ... @@ -163,11 +166,12 @@ async function handleSubmit() {
163 166  
164 167 formLoading.value = true
165 168 try {
  169 + const data = formData.value
166 170 if (props.id) {
167   - await updateStudent(formData.value)
  171 + await updateStudent(data)
168 172 toast.success('修改成功')
169 173 } else {
170   - await createStudent(formData.value)
  174 + await createStudent(data)
171 175 toast.success('新增成功')
172 176 }
173 177 uni.$emit('infra:demo:reload')
... ... @@ -178,7 +182,7 @@ async function handleSubmit() {
178 182 }
179 183  
180 184 /** 初始化 */
181   -onMounted(() => {
  185 +onMounted(async () => {
182 186 getDetail()
183 187 })
184 188 </script>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/assert.json 0 → 100644
  1 +[ {
  2 + "contentPath" : "java/InfraCategoryListReqVO",
  3 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraCategoryListReqVO.java"
  4 +}, {
  5 + "contentPath" : "java/InfraCategoryRespVO",
  6 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraCategoryRespVO.java"
  7 +}, {
  8 + "contentPath" : "java/InfraCategorySaveReqVO",
  9 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraCategorySaveReqVO.java"
  10 +}, {
  11 + "contentPath" : "java/InfraCategoryController",
  12 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraCategoryController.java"
  13 +}, {
  14 + "contentPath" : "java/InfraCategoryDO",
  15 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraCategoryDO.java"
  16 +}, {
  17 + "contentPath" : "java/InfraCategoryMapper",
  18 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraCategoryMapper.java"
  19 +}, {
  20 + "contentPath" : "xml/InfraCategoryMapper",
  21 + "filePath" : "yudao-module-infra/src/main/resources/mapper/demo/InfraCategoryMapper.xml"
  22 +}, {
  23 + "contentPath" : "java/InfraCategoryServiceImpl",
  24 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraCategoryServiceImpl.java"
  25 +}, {
  26 + "contentPath" : "java/InfraCategoryService",
  27 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraCategoryService.java"
  28 +}, {
  29 + "contentPath" : "java/InfraCategoryServiceImplTest",
  30 + "filePath" : "yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraCategoryServiceImplTest.java"
  31 +}, {
  32 + "contentPath" : "java/ErrorCodeConstants_手动操作",
  33 + "filePath" : "yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
  34 +}, {
  35 + "contentPath" : "sql/sql",
  36 + "filePath" : "sql/sql.sql"
  37 +}, {
  38 + "contentPath" : "sql/h2",
  39 + "filePath" : "sql/h2.sql"
  40 +}, {
  41 + "contentPath" : "ts/index",
  42 + "filePath" : "yudao-ui-admin-uniapp/src/api/infra/demo/index.ts"
  43 +}, {
  44 + "contentPath" : "vue/search-form",
  45 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/search-form.vue"
  46 +}, {
  47 + "contentPath" : "vue/form/index",
  48 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/form/index.vue"
  49 +}, {
  50 + "contentPath" : "vue/detail/index",
  51 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/detail/index.vue"
  52 +}, {
  53 + "contentPath" : "vue/index",
  54 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/index.vue"
  55 +}, {
  56 + "contentPath" : "vue/breadcrumb",
  57 + "filePath" : "yudao-ui-admin-uniapp/src/pages-infra/demo/components/breadcrumb.vue"
  58 +} ]
0 59 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/ErrorCodeConstants_手动操作 0 → 100644
  1 +// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
  2 +// ========== 分类 TODO 补充编号 ==========
  3 +ErrorCode CATEGORY_NOT_EXISTS = new ErrorCode(TODO 补充编号, "分类不存在");
  4 +ErrorCode CATEGORY_EXITS_CHILDREN = new ErrorCode(TODO 补充编号, "存在存在子分类,无法删除");
  5 +ErrorCode CATEGORY_PARENT_NOT_EXITS = new ErrorCode(TODO 补充编号,"父级分类不存在");
  6 +ErrorCode CATEGORY_PARENT_ERROR = new ErrorCode(TODO 补充编号, "不能设置自己为父分类");
  7 +ErrorCode CATEGORY_NAME_DUPLICATE = new ErrorCode(TODO 补充编号, "已经存在该名字的分类");
  8 +ErrorCode CATEGORY_PARENT_IS_CHILD = new ErrorCode(TODO 补充编号, "不能设置自己的子InfraCategory为父InfraCategory");
0 9 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryController 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo;
  2 +
  3 +import org.springframework.web.bind.annotation.*;
  4 +import jakarta.annotation.Resource;
  5 +import org.springframework.validation.annotation.Validated;
  6 +import org.springframework.security.access.prepost.PreAuthorize;
  7 +import io.swagger.v3.oas.annotations.tags.Tag;
  8 +import io.swagger.v3.oas.annotations.Parameter;
  9 +import io.swagger.v3.oas.annotations.Operation;
  10 +
  11 +import jakarta.validation.constraints.*;
  12 +import jakarta.validation.*;
  13 +import jakarta.servlet.http.*;
  14 +import java.util.*;
  15 +import java.io.IOException;
  16 +
  17 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  18 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  19 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  20 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  21 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  22 +
  23 +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
  24 +
  25 +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
  26 +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
  27 +
  28 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  29 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO;
  30 +import cn.iocoder.yudao.module.infra.service.demo.InfraCategoryService;
  31 +
  32 +@Tag(name = "管理后台 - 分类")
  33 +@RestController
  34 +@RequestMapping("/infra/category")
  35 +@Validated
  36 +public class InfraCategoryController {
  37 +
  38 + @Resource
  39 + private InfraCategoryService categoryService;
  40 +
  41 + @PostMapping("/create")
  42 + @Operation(summary = "创建分类")
  43 + @PreAuthorize("@ss.hasPermission('infra:category:create')")
  44 + public CommonResult<Long> createCategory(@Valid @RequestBody InfraCategorySaveReqVO createReqVO) {
  45 + return success(categoryService.createCategory(createReqVO));
  46 + }
  47 +
  48 + @PutMapping("/update")
  49 + @Operation(summary = "更新分类")
  50 + @PreAuthorize("@ss.hasPermission('infra:category:update')")
  51 + public CommonResult<Boolean> updateCategory(@Valid @RequestBody InfraCategorySaveReqVO updateReqVO) {
  52 + categoryService.updateCategory(updateReqVO);
  53 + return success(true);
  54 + }
  55 +
  56 + @DeleteMapping("/delete")
  57 + @Operation(summary = "删除分类")
  58 + @Parameter(name = "id", description = "编号", required = true)
  59 + @PreAuthorize("@ss.hasPermission('infra:category:delete')")
  60 + public CommonResult<Boolean> deleteCategory(@RequestParam("id") Long id) {
  61 + categoryService.deleteCategory(id);
  62 + return success(true);
  63 + }
  64 +
  65 +
  66 + @GetMapping("/get")
  67 + @Operation(summary = "获得分类")
  68 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  69 + @PreAuthorize("@ss.hasPermission('infra:category:query')")
  70 + public CommonResult<InfraCategoryRespVO> getCategory(@RequestParam("id") Long id) {
  71 + InfraCategoryDO category = categoryService.getCategory(id);
  72 + return success(BeanUtils.toBean(category, InfraCategoryRespVO.class));
  73 + }
  74 +
  75 + @GetMapping("/list")
  76 + @Operation(summary = "获得分类列表")
  77 + @PreAuthorize("@ss.hasPermission('infra:category:query')")
  78 + public CommonResult<List<InfraCategoryRespVO>> getCategoryList(@Valid InfraCategoryListReqVO listReqVO) {
  79 + List<InfraCategoryDO> list = categoryService.getCategoryList(listReqVO);
  80 + return success(BeanUtils.toBean(list, InfraCategoryRespVO.class));
  81 + }
  82 +
  83 + @GetMapping("/export-excel")
  84 + @Operation(summary = "导出分类 Excel")
  85 + @PreAuthorize("@ss.hasPermission('infra:category:export')")
  86 + @ApiAccessLog(operateType = EXPORT)
  87 + public void exportCategoryExcel(@Valid InfraCategoryListReqVO listReqVO,
  88 + HttpServletResponse response) throws IOException {
  89 + List<InfraCategoryDO> list = categoryService.getCategoryList(listReqVO);
  90 + // 导出 Excel
  91 + ExcelUtils.write(response, "分类.xls", "数据", InfraCategoryRespVO.class,
  92 + BeanUtils.toBean(list, InfraCategoryRespVO.class));
  93 + }
  94 +
  95 +}
0 96 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryDO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import com.baomidou.mybatisplus.annotation.*;
  6 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  7 +
  8 +/**
  9 + * 分类 DO
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@TableName("infra_category")
  14 +@KeySequence("infra_category_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
  15 +@Data
  16 +@EqualsAndHashCode(callSuper = true)
  17 +@ToString(callSuper = true)
  18 +@Builder
  19 +@NoArgsConstructor
  20 +@AllArgsConstructor
  21 +public class InfraCategoryDO extends BaseDO {
  22 +
  23 + public static final Long PARENT_ID_ROOT = 0L;
  24 +
  25 + /**
  26 + * 编号
  27 + */
  28 + @TableId
  29 + private Long id;
  30 + /**
  31 + * 名字
  32 + */
  33 + private String name;
  34 + /**
  35 + * 父编号
  36 + */
  37 + private Long parentId;
  38 +
  39 +
  40 +}
0 41 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryListReqVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import lombok.*;
  4 +import java.util.*;
  5 +import io.swagger.v3.oas.annotations.media.Schema;
  6 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  7 +
  8 +@Schema(description = "管理后台 - 分类列表 Request VO")
  9 +@Data
  10 +public class InfraCategoryListReqVO {
  11 +
  12 + @Schema(description = "名字", example = "芋头")
  13 + private String name;
  14 +
  15 +}
0 16 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryMapper 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.dal.mysql.demo;
  2 +
  3 +import java.util.*;
  4 +
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  7 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  8 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO;
  9 +import org.apache.ibatis.annotations.Mapper;
  10 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  11 +
  12 +/**
  13 + * 分类 Mapper
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Mapper
  18 +public interface InfraCategoryMapper extends BaseMapperX<InfraCategoryDO> {
  19 +
  20 + default List<InfraCategoryDO> selectList(InfraCategoryListReqVO reqVO) {
  21 + return selectList(new LambdaQueryWrapperX<InfraCategoryDO>()
  22 + .likeIfPresent(InfraCategoryDO::getName, reqVO.getName())
  23 + .orderByDesc(InfraCategoryDO::getId));
  24 + }
  25 +
  26 + default InfraCategoryDO selectByParentIdAndName(Long parentId, String name) {
  27 + return selectOne(InfraCategoryDO::getParentId, parentId, InfraCategoryDO::getName, name);
  28 + }
  29 +
  30 + default Long selectCountByParentId(Long parentId) {
  31 + return selectCount(InfraCategoryDO::getParentId, parentId);
  32 + }
  33 +
  34 +}
0 35 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryRespVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +import java.util.*;
  6 +import cn.idev.excel.annotation.*;
  7 +
  8 +@Schema(description = "管理后台 - 分类 Response VO")
  9 +@Data
  10 +@ExcelIgnoreUnannotated
  11 +public class InfraCategoryRespVO {
  12 +
  13 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  14 + @ExcelProperty("编号")
  15 + private Long id;
  16 +
  17 + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
  18 + @ExcelProperty("名字")
  19 + private String name;
  20 +
  21 + @Schema(description = "父编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
  22 + @ExcelProperty("父编号")
  23 + private Long parentId;
  24 +
  25 +}
0 26 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategorySaveReqVO 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.*;
  5 +import java.util.*;
  6 +import jakarta.validation.constraints.*;
  7 +
  8 +@Schema(description = "管理后台 - 分类新增/修改 Request VO")
  9 +@Data
  10 +public class InfraCategorySaveReqVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private Long id;
  14 +
  15 + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
  16 + @NotEmpty(message = "名字不能为空")
  17 + private String name;
  18 +
  19 + @Schema(description = "父编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
  20 + @NotNull(message = "父编号不能为空")
  21 + private Long parentId;
  22 +
  23 +}
0 24 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryService 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import java.util.*;
  4 +import jakarta.validation.*;
  5 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  6 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO;
  7 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  8 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  9 +
  10 +/**
  11 + * 分类 Service 接口
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +public interface InfraCategoryService {
  16 +
  17 + /**
  18 + * 创建分类
  19 + *
  20 + * @param createReqVO 创建信息
  21 + * @return 编号
  22 + */
  23 + Long createCategory(@Valid InfraCategorySaveReqVO createReqVO);
  24 +
  25 + /**
  26 + * 更新分类
  27 + *
  28 + * @param updateReqVO 更新信息
  29 + */
  30 + void updateCategory(@Valid InfraCategorySaveReqVO updateReqVO);
  31 +
  32 + /**
  33 + * 删除分类
  34 + *
  35 + * @param id 编号
  36 + */
  37 + void deleteCategory(Long id);
  38 +
  39 +
  40 + /**
  41 + * 获得分类
  42 + *
  43 + * @param id 编号
  44 + * @return 分类
  45 + */
  46 + InfraCategoryDO getCategory(Long id);
  47 +
  48 + /**
  49 + * 获得分类列表
  50 + *
  51 + * @param listReqVO 查询条件
  52 + * @return 分类列表
  53 + */
  54 + List<InfraCategoryDO> getCategoryList(InfraCategoryListReqVO listReqVO);
  55 +
  56 +}
0 57 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImpl 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import org.springframework.stereotype.Service;
  5 +import jakarta.annotation.Resource;
  6 +import org.springframework.validation.annotation.Validated;
  7 +import org.springframework.transaction.annotation.Transactional;
  8 +
  9 +import java.util.*;
  10 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  11 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO;
  12 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  13 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  14 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  15 +
  16 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraCategoryMapper;
  17 +
  18 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  19 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  20 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
  21 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
  22 +
  23 +/**
  24 + * 分类 Service 实现类
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +@Service
  29 +@Validated
  30 +public class InfraCategoryServiceImpl implements InfraCategoryService {
  31 +
  32 + @Resource
  33 + private InfraCategoryMapper categoryMapper;
  34 +
  35 + @Override
  36 + public Long createCategory(InfraCategorySaveReqVO createReqVO) {
  37 + // 校验父编号的有效性
  38 + validateParentCategory(null, createReqVO.getParentId());
  39 + // 校验名字的唯一性
  40 + validateCategoryNameUnique(null, createReqVO.getParentId(), createReqVO.getName());
  41 +
  42 + // 插入
  43 + InfraCategoryDO category = BeanUtils.toBean(createReqVO, InfraCategoryDO.class);
  44 + categoryMapper.insert(category);
  45 +
  46 + // 返回
  47 + return category.getId();
  48 + }
  49 +
  50 + @Override
  51 + public void updateCategory(InfraCategorySaveReqVO updateReqVO) {
  52 + // 校验存在
  53 + validateCategoryExists(updateReqVO.getId());
  54 + // 校验父编号的有效性
  55 + validateParentCategory(updateReqVO.getId(), updateReqVO.getParentId());
  56 + // 校验名字的唯一性
  57 + validateCategoryNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName());
  58 +
  59 + // 更新
  60 + InfraCategoryDO updateObj = BeanUtils.toBean(updateReqVO, InfraCategoryDO.class);
  61 + categoryMapper.updateById(updateObj);
  62 + }
  63 +
  64 + @Override
  65 + public void deleteCategory(Long id) {
  66 + // 校验存在
  67 + validateCategoryExists(id);
  68 + // 校验是否有子分类
  69 + if (categoryMapper.selectCountByParentId(id) > 0) {
  70 + throw exception(CATEGORY_EXITS_CHILDREN);
  71 + }
  72 + // 删除
  73 + categoryMapper.deleteById(id);
  74 + }
  75 +
  76 +
  77 + private void validateCategoryExists(Long id) {
  78 + if (categoryMapper.selectById(id) == null) {
  79 + throw exception(CATEGORY_NOT_EXISTS);
  80 + }
  81 + }
  82 +
  83 + private void validateParentCategory(Long id, Long parentId) {
  84 + if (parentId == null || CategoryDO.PARENT_ID_ROOT.equals(parentId)) {
  85 + return;
  86 + }
  87 + // 1. 不能设置自己为父分类
  88 + if (Objects.equals(id, parentId)) {
  89 + throw exception(CATEGORY_PARENT_ERROR);
  90 + }
  91 + // 2. 父分类不存在
  92 + CategoryDO parentCategory = categoryMapper.selectById(parentId);
  93 + if (parentCategory == null) {
  94 + throw exception(CATEGORY_PARENT_NOT_EXITS);
  95 + }
  96 + // 3. 递归校验父分类,如果父分类是自己的子分类,则报错,避免形成环路
  97 + if (id == null) { // id 为空,说明新增,不需要考虑环路
  98 + return;
  99 + }
  100 + for (int i = 0; i < Short.MAX_VALUE; i++) {
  101 + // 3.1 校验环路
  102 + parentId = parentCategory.getParentId();
  103 + if (Objects.equals(id, parentId)) {
  104 + throw exception(CATEGORY_PARENT_IS_CHILD);
  105 + }
  106 + // 3.2 继续递归下一级父分类
  107 + if (parentId == null || CategoryDO.PARENT_ID_ROOT.equals(parentId)) {
  108 + break;
  109 + }
  110 + parentCategory = categoryMapper.selectById(parentId);
  111 + if (parentCategory == null) {
  112 + break;
  113 + }
  114 + }
  115 + }
  116 +
  117 + private void validateCategoryNameUnique(Long id, Long parentId, String name) {
  118 + CategoryDO category = categoryMapper.selectByParentIdAndName(parentId, name);
  119 + if (category == null) {
  120 + return;
  121 + }
  122 + // 如果 id 为空,说明不用比较是否为相同 id 的分类
  123 + if (id == null) {
  124 + throw exception(CATEGORY_NAME_DUPLICATE);
  125 + }
  126 + if (!Objects.equals(category.getId(), id)) {
  127 + throw exception(CATEGORY_NAME_DUPLICATE);
  128 + }
  129 + }
  130 +
  131 + @Override
  132 + public InfraCategoryDO getCategory(Long id) {
  133 + return categoryMapper.selectById(id);
  134 + }
  135 +
  136 + @Override
  137 + public List<InfraCategoryDO> getCategoryList(InfraCategoryListReqVO listReqVO) {
  138 + return categoryMapper.selectList(listReqVO);
  139 + }
  140 +
  141 +}
0 142 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/java/InfraCategoryServiceImplTest 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.service.demo;
  2 +
  3 +import org.junit.jupiter.api.Disabled;
  4 +import org.junit.jupiter.api.Test;
  5 +
  6 +import jakarta.annotation.Resource;
  7 +
  8 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  9 +
  10 +import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
  11 +import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraCategoryDO;
  12 +import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraCategoryMapper;
  13 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  14 +
  15 +import jakarta.annotation.Resource;
  16 +import org.springframework.context.annotation.Import;
  17 +import java.util.*;
  18 +import java.time.LocalDateTime;
  19 +
  20 +import static cn.hutool.core.util.RandomUtil.*;
  21 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
  22 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
  23 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
  24 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
  25 +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
  26 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
  27 +import static org.junit.jupiter.api.Assertions.*;
  28 +import static org.mockito.Mockito.*;
  29 +
  30 +/**
  31 + * {@link InfraCategoryServiceImpl} 的单元测试类
  32 + *
  33 + * @author 芋道源码
  34 + */
  35 +@Import(InfraCategoryServiceImpl.class)
  36 +public class InfraCategoryServiceImplTest extends BaseDbUnitTest {
  37 +
  38 + @Resource
  39 + private InfraCategoryServiceImpl categoryService;
  40 +
  41 + @Resource
  42 + private InfraCategoryMapper categoryMapper;
  43 +
  44 + @Test
  45 + public void testCreateCategory_success() {
  46 + // 准备参数
  47 + InfraCategorySaveReqVO createReqVO = randomPojo(InfraCategorySaveReqVO.class).setId(null);
  48 +
  49 + // 调用
  50 + Long categoryId = categoryService.createCategory(createReqVO);
  51 + // 断言
  52 + assertNotNull(categoryId);
  53 + // 校验记录的属性是否正确
  54 + InfraCategoryDO category = categoryMapper.selectById(categoryId);
  55 + assertPojoEquals(createReqVO, category, "id");
  56 + }
  57 +
  58 + @Test
  59 + public void testUpdateCategory_success() {
  60 + // mock 数据
  61 + InfraCategoryDO dbCategory = randomPojo(InfraCategoryDO.class);
  62 + categoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据
  63 + // 准备参数
  64 + InfraCategorySaveReqVO updateReqVO = randomPojo(InfraCategorySaveReqVO.class, o -> {
  65 + o.setId(dbCategory.getId()); // 设置更新的 ID
  66 + });
  67 +
  68 + // 调用
  69 + categoryService.updateCategory(updateReqVO);
  70 + // 校验是否更新正确
  71 + InfraCategoryDO category = categoryMapper.selectById(updateReqVO.getId()); // 获取最新的
  72 + assertPojoEquals(updateReqVO, category);
  73 + }
  74 +
  75 + @Test
  76 + public void testUpdateCategory_notExists() {
  77 + // 准备参数
  78 + InfraCategorySaveReqVO updateReqVO = randomPojo(InfraCategorySaveReqVO.class);
  79 +
  80 + // 调用, 并断言异常
  81 + assertServiceException(() -> categoryService.updateCategory(updateReqVO), CATEGORY_NOT_EXISTS);
  82 + }
  83 +
  84 + @Test
  85 + public void testDeleteCategory_success() {
  86 + // mock 数据
  87 + InfraCategoryDO dbCategory = randomPojo(InfraCategoryDO.class);
  88 + categoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据
  89 + // 准备参数
  90 + Long id = dbCategory.getId();
  91 +
  92 + // 调用
  93 + categoryService.deleteCategory(id);
  94 + // 校验数据不存在了
  95 + assertNull(categoryMapper.selectById(id));
  96 + }
  97 +
  98 + @Test
  99 + public void testDeleteCategory_notExists() {
  100 + // 准备参数
  101 + Long id = randomLongId();
  102 +
  103 + // 调用, 并断言异常
  104 + assertServiceException(() -> categoryService.deleteCategory(id), CATEGORY_NOT_EXISTS);
  105 + }
  106 +
  107 + @Test
  108 + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
  109 + public void testGetCategoryList() {
  110 + // mock 数据
  111 + InfraCategoryDO dbCategory = randomPojo(InfraCategoryDO.class, o -> { // 等会查询到
  112 + o.setName(null);
  113 + });
  114 + categoryMapper.insert(dbCategory);
  115 + // 测试 name 不匹配
  116 + categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setName(null)));
  117 + // 准备参数
  118 + InfraCategoryListReqVO reqVO = new InfraCategoryListReqVO();
  119 + reqVO.setName(null);
  120 +
  121 + // 调用
  122 + List<InfraCategoryDO> list = categoryService.getCategoryList(reqVO);
  123 + // 断言
  124 + assertEquals(1, list.size());
  125 + assertPojoEquals(dbCategory, list.get(0));
  126 + }
  127 +
  128 +}
0 129 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/h2 0 → 100644
  1 +-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
  2 +CREATE TABLE IF NOT EXISTS "infra_category" (
  3 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  4 + "name" varchar NOT NULL,
  5 + "description" bigint NOT NULL,
  6 + PRIMARY KEY ("id")
  7 +) COMMENT '分类表';
  8 +
  9 +-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
  10 +DELETE FROM "infra_category";
0 11 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/sql/sql 0 → 100644
  1 +-- 菜单 SQL
  2 +INSERT INTO system_menu(
  3 + name, permission, type, sort, parent_id,
  4 + path, icon, component, status, component_name
  5 +)
  6 +VALUES (
  7 + '分类管理', '', 2, 0, 888,
  8 + 'category', '', 'infra/demo/index', 0, 'InfraCategory'
  9 +);
  10 +
  11 +-- 按钮父菜单ID
  12 +SELECT @parentId := LAST_INSERT_ID();
  13 +
  14 +-- 按钮 SQL
  15 +INSERT INTO system_menu(
  16 + name, permission, type, sort, parent_id,
  17 + path, icon, component, status
  18 +)
  19 +VALUES (
  20 + '分类查询', 'infra:category:query', 3, 1, @parentId,
  21 + '', '', '', 0
  22 +);
  23 +INSERT INTO system_menu(
  24 + name, permission, type, sort, parent_id,
  25 + path, icon, component, status
  26 +)
  27 +VALUES (
  28 + '分类创建', 'infra:category:create', 3, 2, @parentId,
  29 + '', '', '', 0
  30 +);
  31 +INSERT INTO system_menu(
  32 + name, permission, type, sort, parent_id,
  33 + path, icon, component, status
  34 +)
  35 +VALUES (
  36 + '分类更新', 'infra:category:update', 3, 3, @parentId,
  37 + '', '', '', 0
  38 +);
  39 +INSERT INTO system_menu(
  40 + name, permission, type, sort, parent_id,
  41 + path, icon, component, status
  42 +)
  43 +VALUES (
  44 + '分类删除', 'infra:category:delete', 3, 4, @parentId,
  45 + '', '', '', 0
  46 +);
  47 +INSERT INTO system_menu(
  48 + name, permission, type, sort, parent_id,
  49 + path, icon, component, status
  50 +)
  51 +VALUES (
  52 + '分类导出', 'infra:category:export', 3, 5, @parentId,
  53 + '', '', '', 0
  54 +);
0 55 \ No newline at end of file
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/ts/index 0 → 100644
  1 +import { http } from '@/http/http'
  2 +
  3 +/** 分类信息 */
  4 +export interface Category {
  5 + id?: number
  6 + name: string
  7 + parentId: number
  8 + children?: Category[]
  9 +}
  10 +
  11 +/** 获取分类列表 */
  12 +export function getCategoryList(params?: Record<string, any>) {
  13 + return http.get<Category[]>('/infra/category/list', params)
  14 +}
  15 +
  16 +/** 获取分类详情 */
  17 +export function getCategory(id: number) {
  18 + return http.get<Category>(`/infra/category/get?id=${id}`)
  19 +}
  20 +
  21 +/** 创建分类 */
  22 +export function createCategory(data: Category) {
  23 + return http.post<number>('/infra/category/create', data)
  24 +}
  25 +
  26 +/** 更新分类 */
  27 +export function updateCategory(data: Category) {
  28 + return http.put<boolean>('/infra/category/update', data)
  29 +}
  30 +
  31 +/** 删除分类 */
  32 +export function deleteCategory(id: number) {
  33 + return http.delete<boolean>(`/infra/category/delete?id=${id}`)
  34 +}
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/breadcrumb 0 → 100644
  1 +<template>
  2 + <view class="bg-white px-24rpx py-16rpx">
  3 + <scroll-view scroll-x class="whitespace-nowrap">
  4 + <view class="inline-flex items-center">
  5 + <view
  6 + class="flex items-center text-28rpx"
  7 + :class="items.length > 0 ? 'text-[#1890ff]' : 'text-[#333]'"
  8 + @click="handleClick(-1)"
  9 + >
  10 + <text>全部分类</text>
  11 + </view>
  12 + <template v-for="(item, index) in items" :key="item.id">
  13 + <wd-icon name="arrow-right" size="12px" color="#999" custom-class="mx-8rpx" />
  14 + <view
  15 + class="flex items-center text-28rpx"
  16 + :class="index < items.length - 1 ? 'text-[#1890ff]' : 'text-[#333]'"
  17 + @click="handleClick(index)"
  18 + >
  19 + <text>{{ item.name }}</text>
  20 + </view>
  21 + </template>
  22 + </view>
  23 + </scroll-view>
  24 + </view>
  25 +</template>
  26 +
  27 +<script lang="ts" setup>
  28 +import { ref, watch } from 'vue'
  29 +
  30 +interface BreadcrumbItem {
  31 + id: number
  32 + name: string
  33 +}
  34 +
  35 +const props = defineProps<{
  36 + modelValue: number
  37 +}>()
  38 +
  39 +const emit = defineEmits<{
  40 + 'update:modelValue': [value: number]
  41 +}>()
  42 +
  43 +const items = ref<BreadcrumbItem[]>([]) // 面包屑数据
  44 +
  45 +/** 监听外部值变化 */
  46 +watch(() => props.modelValue, (value) => {
  47 + if (value === 0) {
  48 + items.value = []
  49 + }
  50 +})
  51 +
  52 +/** 点击面包屑 */
  53 +function handleClick(index: number) {
  54 + if (index === -1) {
  55 + reset()
  56 + return
  57 + }
  58 + if (index < items.value.length - 1) {
  59 + items.value = items.value.slice(0, index + 1)
  60 + emit('update:modelValue', items.value[index].id)
  61 + }
  62 +}
  63 +
  64 +/** 进入子层级 */
  65 +function enter(item: BreadcrumbItem) {
  66 + items.value.push(item)
  67 + emit('update:modelValue', item.id)
  68 +}
  69 +
  70 +/** 返回上一层级 */
  71 +function back(): boolean {
  72 + if (items.value.length === 0) {
  73 + return false
  74 + }
  75 + items.value.pop()
  76 + emit('update:modelValue', items.value.at(-1)?.id ?? 0)
  77 + return true
  78 +}
  79 +
  80 +/** 重置面包屑 */
  81 +function reset() {
  82 + items.value = []
  83 + emit('update:modelValue', 0)
  84 +}
  85 +
  86 +defineExpose({ back, enter, reset })
  87 +</script>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/detail/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + title="分类详情"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <!-- 详情内容 -->
  11 + <view>
  12 + <wd-cell-group border>
  13 + <wd-cell title="编号" :value="formData?.id ?? '-'" />
  14 + <wd-cell title="名字" :value="formData?.name ?? '-'" />
  15 + <wd-cell title="父编号" :value="formData?.parentId ?? '-'" />
  16 + </wd-cell-group>
  17 + </view>
  18 +
  19 + <!-- 底部操作按钮 -->
  20 + <view class="yd-detail-footer">
  21 + <view class="yd-detail-footer-actions">
  22 + <wd-button
  23 + v-if="hasAccessByCodes(['infra:category:update'])"
  24 + class="flex-1" type="warning" @click="handleEdit"
  25 + >
  26 + 编辑
  27 + </wd-button>
  28 + <wd-button
  29 + v-if="hasAccessByCodes(['infra:category:delete'])"
  30 + class="flex-1" type="danger" :loading="deleting" @click="handleDelete"
  31 + >
  32 + 删除
  33 + </wd-button>
  34 + </view>
  35 + </view>
  36 + </view>
  37 +</template>
  38 +
  39 +<script lang="ts" setup>
  40 +import type { Category } from '@/api/infra/demo'
  41 +import { onUnload } from '@dcloudio/uni-app'
  42 +import { onMounted, ref } from 'vue'
  43 +import { useDialog } from '@wot-ui/ui/components/wd-dialog'
  44 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  45 +import { deleteCategory, getCategory } from '@/api/infra/demo'
  46 +import { useAccess } from '@/hooks/useAccess'
  47 +import { delay, navigateBackPlus } from '@/utils'
  48 +
  49 +const props = defineProps<{
  50 + id?: number
  51 +}>()
  52 +
  53 +definePage({
  54 + style: {
  55 + navigationBarTitleText: '',
  56 + navigationStyle: 'custom',
  57 + },
  58 +})
  59 +
  60 +const { hasAccessByCodes } = useAccess()
  61 +const dialog = useDialog()
  62 +const toast = useToast()
  63 +const formData = ref<Category>() // 详情数据
  64 +const deleting = ref(false) // 删除状态
  65 +
  66 +/** 返回上一页 */
  67 +function handleBack() {
  68 + navigateBackPlus('/pages-infra/demo/index')
  69 +}
  70 +
  71 +/** 加载分类详情 */
  72 +async function getDetail() {
  73 + if (!props.id || deleting.value) {
  74 + return
  75 + }
  76 + try {
  77 + toast.loading('加载中...')
  78 + formData.value = await getCategory(props.id)
  79 + } finally {
  80 + toast.close()
  81 + }
  82 +}
  83 +
  84 +/** 编辑分类 */
  85 +function handleEdit() {
  86 + uni.navigateTo({
  87 + url: `/pages-infra/demo/form/index?id=${props.id}`,
  88 + })
  89 +}
  90 +
  91 +/** 删除分类 */
  92 +async function handleDelete() {
  93 + if (!props.id) {
  94 + return
  95 + }
  96 + try {
  97 + await dialog.confirm({
  98 + title: '提示',
  99 + msg: '确定要删除该分类吗?',
  100 + })
  101 + } catch {
  102 + return
  103 + }
  104 + deleting.value = true
  105 + try {
  106 + await deleteCategory(props.id)
  107 + toast.success('删除成功')
  108 + uni.$emit('infra:demo:reload')
  109 + delay(handleBack)
  110 + } finally {
  111 + deleting.value = false
  112 + }
  113 +}
  114 +
  115 +/** 初始化 */
  116 +onMounted(() => {
  117 + uni.$on('infra:demo:reload', getDetail)
  118 + getDetail()
  119 +})
  120 +
  121 +/** 卸载 */
  122 +onUnload(() => {
  123 + uni.$off('infra:demo:reload', getDetail)
  124 +})
  125 +</script>
  126 +
  127 +<style lang="scss" scoped>
  128 +</style>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/form/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + :title="getTitle"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <!-- 表单区域 -->
  11 + <view>
  12 + <wd-form ref="formRef" :model="formData" :schema="formSchema">
  13 + <wd-cell-group border>
  14 + <wd-form-item title="名字" title-width="180rpx" prop="name">
  15 + <wd-input
  16 + v-model="formData.name"
  17 + clearable
  18 + placeholder="请输入名字"
  19 + />
  20 + </wd-form-item>
  21 + <yd-tree-select
  22 + v-model="formData.parentId"
  23 + :data="treeData"
  24 + label="父编号"
  25 + prop="parentId"
  26 + :props="{ label: 'name', value: 'id' }"
  27 + placeholder="请选择父编号"
  28 + />
  29 + </wd-cell-group>
  30 + </wd-form>
  31 + </view>
  32 +
  33 + <!-- 底部保存按钮 -->
  34 + <view class="yd-detail-footer">
  35 + <wd-button
  36 + type="primary"
  37 + block
  38 + :loading="formLoading"
  39 + @click="handleSubmit"
  40 + >
  41 + 保存
  42 + </wd-button>
  43 + </view>
  44 + </view>
  45 +</template>
  46 +
  47 +<script lang="ts" setup>
  48 +import type { FormInstance } from '@wot-ui/ui/components/wd-form/types'
  49 +import type {
  50 + Category,
  51 +} from '@/api/infra/demo'
  52 +import { computed, onMounted, ref } from 'vue'
  53 +import { useToast } from '@wot-ui/ui/components/wd-toast'
  54 +import { createCategory, getCategory, getCategoryList, updateCategory } from '@/api/infra/demo'
  55 +import { delay, navigateBackPlus } from '@/utils'
  56 +import { createFormSchema } from '@/utils/wot'
  57 +import { handleTree } from '@/utils/tree'
  58 +
  59 +const props = defineProps<{
  60 + id?: number
  61 + parentId?: number
  62 +}>()
  63 +
  64 +definePage({
  65 + style: {
  66 + navigationBarTitleText: '',
  67 + navigationStyle: 'custom',
  68 + },
  69 +})
  70 +
  71 +const toast = useToast()
  72 +const getTitle = computed(() => props.id ? '编辑分类' : '新增分类')
  73 +const formLoading = ref(false) // 表单提交状态
  74 +const formData = ref<Category>({
  75 + id: undefined,
  76 + name: '',
  77 + parentId: props.parentId ?? 0,
  78 +}) // 表单数据
  79 +const formSchema = createFormSchema({
  80 + name: [{ required: true, message: '名字不能为空' }],
  81 + parentId: [{ required: true, message: '父编号不能为空' }],
  82 +}) // 表单校验规则
  83 +const formRef = ref<FormInstance>() // 表单组件引用
  84 +const treeData = ref<Category[]>([]) // 父级树形选项
  85 +
  86 +/** 返回上一页 */
  87 +function handleBack() {
  88 + navigateBackPlus('/pages-infra/demo/index')
  89 +}
  90 +
  91 +/** 加载分类详情 */
  92 +async function getDetail() {
  93 + if (!props.id) {
  94 + return
  95 + }
  96 + formData.value = await getCategory(props.id)
  97 +}
  98 +
  99 +/** 提交表单 */
  100 +async function handleSubmit() {
  101 + const { valid } = await formRef.value.validate()
  102 + if (!valid) {
  103 + return
  104 + }
  105 +
  106 + formLoading.value = true
  107 + try {
  108 + const data = formData.value
  109 + if (props.id) {
  110 + await updateCategory(data)
  111 + toast.success('修改成功')
  112 + } else {
  113 + await createCategory(data)
  114 + toast.success('新增成功')
  115 + }
  116 + uni.$emit('infra:demo:reload')
  117 + delay(handleBack)
  118 + } finally {
  119 + formLoading.value = false
  120 + }
  121 +}
  122 +
  123 +/** 初始化 */
  124 +onMounted(async () => {
  125 + const data = await getCategoryList()
  126 + treeData.value = [{
  127 + id: 0,
  128 + name: '顶级分类',
  129 + parentId: 0,
  130 + children: handleTree(data, 'id', 'parentId'),
  131 + } as Category]
  132 + getDetail()
  133 +})
  134 +</script>
  135 +
  136 +<style lang="scss" scoped>
  137 +</style>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/index 0 → 100644
  1 +<template>
  2 + <view class="yd-page-container">
  3 + <!-- 顶部导航栏 -->
  4 + <wd-navbar
  5 + title="分类管理"
  6 + left-arrow placeholder safe-area-inset-top fixed
  7 + @click-left="handleBack"
  8 + />
  9 +
  10 + <!-- 搜索组件 -->
  11 + <SearchForm @search="handleQuery" @reset="handleReset" />
  12 +
  13 + <!-- 面包屑导航 -->
  14 + <Breadcrumb ref="breadcrumbRef" v-model="currentParentId" />
  15 +
  16 + <!-- 分类列表 -->
  17 + <view class="p-24rpx">
  18 + <view
  19 + v-for="item in currentList"
  20 + :key="item.id"
  21 + class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
  22 + >
  23 + <view class="p-24rpx" @click="handleDetail(item)">
  24 + <view class="flex items-center justify-between">
  25 + <view class="flex min-w-0 items-center">
  26 + <view class="mr-16rpx h-48rpx w-48rpx shrink-0 flex items-center justify-center rounded-8rpx bg-[#1890ff]">
  27 + <wd-icon name="folder" size="20px" color="#fff" />
  28 + </view>
  29 + <view class="truncate text-32rpx text-[#333] font-semibold">
  30 + {{ item.name }}
  31 + </view>
  32 + </view>
  33 + <view
  34 + v-if="item.children?.length"
  35 + class="ml-16rpx shrink-0 flex items-center"
  36 + @click.stop="handleEnterChildren(item)"
  37 + >
  38 + <text class="text-24rpx text-[#1890ff]">子级 ({{ item.children.length }})</text>
  39 + <wd-icon name="arrow-right" size="12px" color="#1890ff" />
  40 + </view>
  41 + </view>
  42 + </view>
  43 + </view>
  44 +
  45 + <!-- 空状态 -->
  46 + <view v-if="!loading && currentList.length === 0" class="py-100rpx text-center">
  47 + <wd-empty icon="content" tip="暂无分类数据" />
  48 + </view>
  49 + </view>
  50 +
  51 + <!-- 新增按钮 -->
  52 + <wd-fab
  53 + v-if="hasAccessByCodes(['infra:category:create'])"
  54 + position="right-bottom"
  55 + type="primary"
  56 + :expandable="false"
  57 + @click="handleAdd"
  58 + />
  59 + </view>
  60 +</template>
  61 +
  62 +<script lang="ts" setup>
  63 +import type { Category } from '@/api/infra/demo'
  64 +import { onUnload } from '@dcloudio/uni-app'
  65 +import { computed, onMounted, ref } from 'vue'
  66 +import { getCategoryList } from '@/api/infra/demo'
  67 +import { useAccess } from '@/hooks/useAccess'
  68 +import { navigateBackPlus } from '@/utils'
  69 +import { findChildren, handleTree } from '@/utils/tree'
  70 +import Breadcrumb from './components/breadcrumb.vue'
  71 +import SearchForm from './components/search-form.vue'
  72 +
  73 +definePage({
  74 + style: {
  75 + navigationBarTitleText: '',
  76 + navigationStyle: 'custom',
  77 + },
  78 +})
  79 +
  80 +const { hasAccessByCodes } = useAccess()
  81 +const loading = ref(false) // 列表加载状态
  82 +const list = ref<Category[]>([]) // 完整树形列表
  83 +const currentParentId = ref<number>(0) // 当前层级的父节点编号
  84 +const currentList = computed(() => currentParentId.value === 0
  85 + ? list.value
  86 + : findChildren(list.value, currentParentId.value, 'id')) // 当前层级列表
  87 +const breadcrumbRef = ref<InstanceType<typeof Breadcrumb>>() // 面包屑组件引用
  88 +const queryParams = ref<Record<string, any>>({}) // 查询参数
  89 +
  90 +/** 返回上一页或上一层级 */
  91 +function handleBack() {
  92 + if (!breadcrumbRef.value?.back()) {
  93 + navigateBackPlus()
  94 + }
  95 +}
  96 +
  97 +/** 进入子层级 */
  98 +function handleEnterChildren(item: Category) {
  99 + breadcrumbRef.value?.enter({ id: item.id!, name: item.name })
  100 +}
  101 +
  102 +/** 查询分类列表 */
  103 +async function getList() {
  104 + loading.value = true
  105 + try {
  106 + list.value = handleTree(await getCategoryList(queryParams.value), 'id', 'parentId')
  107 + } finally {
  108 + loading.value = false
  109 + }
  110 +}
  111 +
  112 +/** 搜索按钮操作 */
  113 +function handleQuery(data?: Record<string, any>) {
  114 + queryParams.value = { ...data }
  115 + currentParentId.value = 0
  116 + breadcrumbRef.value?.reset()
  117 + getList()
  118 +}
  119 +
  120 +/** 重置按钮操作 */
  121 +function handleReset() {
  122 + handleQuery()
  123 +}
  124 +
  125 +/** 新增分类 */
  126 +function handleAdd() {
  127 + uni.navigateTo({ url: `/pages-infra/demo/form/index?parentId=${currentParentId.value}` })
  128 +}
  129 +
  130 +/** 查看详情 */
  131 +function handleDetail(item: Category) {
  132 + uni.navigateTo({ url: `/pages-infra/demo/detail/index?id=${item.id}` })
  133 +}
  134 +
  135 +/** 初始化 */
  136 +onMounted(() => {
  137 + getList()
  138 + uni.$on('infra:demo:reload', getList)
  139 +})
  140 +
  141 +/** 卸载 */
  142 +onUnload(() => {
  143 + uni.$off('infra:demo:reload', getList)
  144 +})
  145 +</script>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/vue/search-form 0 → 100644
  1 +<template>
  2 + <!-- 搜索框入口 -->
  3 + <view @click="visible = true">
  4 + <wd-search :placeholder="placeholder" hide-cancel disabled />
  5 + </view>
  6 +
  7 + <!-- 搜索弹窗 -->
  8 + <wd-popup v-model="visible" position="top" @close="visible = false">
  9 + <view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
  10 + <view class="yd-search-form-item">
  11 + <view class="yd-search-form-label">
  12 + 名字
  13 + </view>
  14 + <wd-input
  15 + v-model="formData.name"
  16 + placeholder="请输入名字"
  17 + clearable
  18 + />
  19 + </view>
  20 + <view class="yd-search-form-actions">
  21 + <wd-button class="flex-1" plain @click="handleReset">
  22 + 重置
  23 + </wd-button>
  24 + <wd-button class="flex-1" type="primary" @click="handleSearch">
  25 + 搜索
  26 + </wd-button>
  27 + </view>
  28 + </view>
  29 + </wd-popup>
  30 +</template>
  31 +
  32 +<script lang="ts" setup>
  33 +import { computed, reactive, ref } from 'vue'
  34 +import { getNavbarHeight } from '@/utils'
  35 +
  36 +const emit = defineEmits<{
  37 + search: [data: Record<string, any>]
  38 + reset: []
  39 +}>()
  40 +
  41 +const visible = ref(false) // 搜索弹窗显示状态
  42 +const formData = reactive({
  43 + name: undefined as string | undefined,
  44 +}) // 搜索表单数据
  45 +
  46 +/** 搜索条件 placeholder 拼接 */
  47 +const placeholder = computed(() => {
  48 + const conditions: string[] = []
  49 + if (formData.name) {
  50 + conditions.push(`名字:${formData.name}`)
  51 + }
  52 + return conditions.length > 0 ? conditions.join(' | ') : '搜索分类'
  53 +})
  54 +
  55 +/** 搜索按钮操作 */
  56 +function handleSearch() {
  57 + visible.value = false
  58 + emit('search', {
  59 + ...formData,
  60 + })
  61 +}
  62 +
  63 +/** 重置按钮操作 */
  64 +function handleReset() {
  65 + formData.name = undefined
  66 + visible.value = false
  67 + emit('reset')
  68 +}
  69 +</script>
... ...
yudao-module-infra/src/test/resources/codegen/vue3_admin_uniapp_tree/xml/InfraCategoryMapper 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3 +<mapper namespace="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraCategoryMapper">
  4 +
  5 + <!--
  6 + 一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
  7 + 无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
  8 + 代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
  9 + 文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
  10 + -->
  11 +
  12 +</mapper>
0 13 \ No newline at end of file
... ...