【同步】BOOT 和 CLOUD 的功能(infra)
parent
8c7087ca2a
commit
6b91b4169d
|
|
@ -55,4 +55,10 @@ public class CodegenProperties {
|
|||
@NotNull(message = "是否生成单元测试不能为空")
|
||||
private Boolean unitTestEnable;
|
||||
|
||||
/**
|
||||
* 是否生成 Excel 导入接口
|
||||
*/
|
||||
@NotNull(message = "是否生成 Excel 导入接口不能为空")
|
||||
private Boolean importEnable;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||
AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret()));
|
||||
URI endpoint = URI.create(buildEndpoint());
|
||||
URI presignerEndpoint = URI.create(buildPresignerEndpoint());
|
||||
S3Configuration serviceConfiguration = S3Configuration.builder() // Path-style 访问
|
||||
.pathStyleAccessEnabled(Boolean.TRUE.equals(config.getEnablePathStyleAccess()))
|
||||
.chunkedEncodingEnabled(false) // 禁用分块编码,参见 https://t.zsxq.com/kBy57
|
||||
|
|
@ -66,7 +67,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||
presigner = S3Presigner.builder()
|
||||
.credentialsProvider(credentialsProvider)
|
||||
.region(region)
|
||||
.endpointOverride(endpoint)
|
||||
.endpointOverride(presignerEndpoint)
|
||||
.serviceConfiguration(serviceConfiguration)
|
||||
.build();
|
||||
}
|
||||
|
|
@ -161,6 +162,23 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||
return StrUtil.format("https://{}", config.getEndpoint());
|
||||
}
|
||||
|
||||
/**
|
||||
* presigner 节点地址
|
||||
*
|
||||
* @return 节点地址
|
||||
*/
|
||||
private String buildPresignerEndpoint() {
|
||||
// 补全 domain
|
||||
if (StrUtil.isEmpty(config.getDomain())) {
|
||||
config.setDomain(buildDomain());
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(config.getEnablePathStyleAccess())) {
|
||||
return StrUtil.removeSuffix(config.getDomain(), StrUtil.format("/{}", config.getBucket()));
|
||||
}
|
||||
return StrUtil.replace(config.getDomain(), StrUtil.format("://{}.", config.getBucket()), "://");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 AWS 区域
|
||||
* 优先级:配置的 region > 从 endpoint 解析的 region > 默认值 us-east-1
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnListConditionEnu
|
|||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableField;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
|
@ -117,8 +118,8 @@ public class CodegenBuilder {
|
|||
table.setBusinessName(toCamelCase(subAfter(tableName, '_', false)).toLowerCase());
|
||||
// 驼峰 + 首字母大写;第一步,第一个 _ 前缀的后面,作为 class 名字;第二步,驼峰命名
|
||||
table.setClassName(upperFirst(toCamelCase(subAfter(tableName, '_', false))));
|
||||
// 去除结尾的表,作为类描述
|
||||
table.setClassComment(StrUtil.removeSuffixIgnoreCase(table.getTableComment(), "表"));
|
||||
// 去除结尾的表,作为类描述;注释中的英文引号替换为中文引号,避免破坏生成代码中的字符串字面量
|
||||
table.setClassComment(StrUtil.removeSuffixIgnoreCase(sanitizeComment(table.getTableComment()), "表"));
|
||||
table.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
}
|
||||
|
||||
|
|
@ -128,6 +129,7 @@ public class CodegenBuilder {
|
|||
for (CodegenColumnDO column : columns) {
|
||||
column.setTableId(tableId);
|
||||
column.setOrdinalPosition(index++);
|
||||
column.setColumnComment(sanitizeComment(column.getColumnComment()));
|
||||
// 特殊处理:Byte => Integer
|
||||
if (Byte.class.getSimpleName().equals(column.getJavaType())) {
|
||||
column.setJavaType(Integer.class.getSimpleName());
|
||||
|
|
@ -217,4 +219,18 @@ public class CodegenBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将注释中的英文引号替换为中文引号,避免破坏生成代码中的字符串字面量
|
||||
*
|
||||
* @param comment 原始注释
|
||||
* @return 清理后的注释
|
||||
*/
|
||||
@VisibleForTesting
|
||||
String sanitizeComment(String comment) {
|
||||
if (StrUtil.isEmpty(comment)) {
|
||||
return comment;
|
||||
}
|
||||
return comment.replace('"', '“').replace('\'', '‘');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ public class CodegenEngine {
|
|||
.put(javaTemplatePath("controller/vo/listReqVO"), javaModuleImplVOFilePath("ListReqVO"))
|
||||
.put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO"))
|
||||
.put(javaTemplatePath("controller/vo/saveReqVO"), javaModuleImplVOFilePath("SaveReqVO"))
|
||||
.put(javaTemplatePath("controller/vo/importExcelVO"), javaModuleImplVOFilePath("ImportExcelVO"))
|
||||
.put(javaTemplatePath("controller/vo/importRespVO"), javaModuleImplVOFilePath("ImportRespVO"))
|
||||
.put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath())
|
||||
.put(javaTemplatePath("dal/do"),
|
||||
javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO"))
|
||||
|
|
@ -126,6 +128,8 @@ public class CodegenEngine {
|
|||
vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/form.vue"),
|
||||
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/import.vue"),
|
||||
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}ImportForm.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -164,6 +168,8 @@ public class CodegenEngine {
|
|||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/form.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/import.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("api/api.ts"),
|
||||
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -181,6 +187,8 @@ public class CodegenEngine {
|
|||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/import.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"),
|
||||
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -200,6 +208,8 @@ public class CodegenEngine {
|
|||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/form.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/import.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("api/api.ts"),
|
||||
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -217,6 +227,8 @@ public class CodegenEngine {
|
|||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/form.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/import.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("api/api.ts"),
|
||||
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -284,6 +296,7 @@ public class CodegenEngine {
|
|||
globalBindingMap.put("jakartaPackage", jakartaEnable ? "jakarta" : "javax");
|
||||
globalBindingMap.put("voType", codegenProperties.getVoType());
|
||||
globalBindingMap.put("deleteBatchEnable", codegenProperties.getDeleteBatchEnable());
|
||||
globalBindingMap.put("importEnable", codegenProperties.getImportEnable());
|
||||
// 全局 Java Bean
|
||||
globalBindingMap.put("CommonResultClassName", CommonResult.class.getName());
|
||||
globalBindingMap.put("PageResultClassName", PageResult.class.getName());
|
||||
|
|
@ -343,6 +356,11 @@ public class CodegenEngine {
|
|||
if (!CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
|
||||
return;
|
||||
}
|
||||
} else if (isImportTemplate(vmPath)) {
|
||||
// 关闭 import 时,跳过 ImportExcelVO / ImportRespVO 的生成
|
||||
if (!Boolean.TRUE.equals(codegenProperties.getImportEnable())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 2.3 默认生成
|
||||
generateCode(result, vmPath, filePath, bindingMap);
|
||||
|
|
@ -676,4 +694,9 @@ public class CodegenEngine {
|
|||
return path.contains("listReqVO");
|
||||
}
|
||||
|
||||
private static boolean isImportTemplate(String path) {
|
||||
return path.contains("importExcelVO") || path.contains("importRespVO")
|
||||
|| path.contains("views/import.vue");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.infra.service.file;
|
|||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
|
|
@ -93,7 +94,7 @@ public class FileConfigServiceImpl implements FileConfigService {
|
|||
fileConfigMapper.updateById(updateObj);
|
||||
|
||||
// 清空缓存
|
||||
clearCache(config.getId(), null);
|
||||
clearCache(config.getId(), config.getMaster());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -132,7 +133,7 @@ public class FileConfigServiceImpl implements FileConfigService {
|
|||
fileConfigMapper.deleteById(id);
|
||||
|
||||
// 清空缓存
|
||||
clearCache(id, null);
|
||||
clearCache(id, config.getMaster());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -149,7 +150,7 @@ public class FileConfigServiceImpl implements FileConfigService {
|
|||
fileConfigMapper.deleteByIds(ids);
|
||||
|
||||
// 清空缓存
|
||||
ids.forEach(id -> clearCache(id, null));
|
||||
ids.forEach(id -> clearCache(id, false));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -191,7 +192,7 @@ public class FileConfigServiceImpl implements FileConfigService {
|
|||
validateFileConfigExists(id);
|
||||
// 上传文件
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
return getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg");
|
||||
return getFileClient(id).upload(content, "public" + StrUtil.SLASH + IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.service.file;
|
|||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
|
|
@ -41,12 +42,19 @@ public class FileServiceImpl implements FileService {
|
|||
*/
|
||||
static boolean PATH_PREFIX_DATE_ENABLE = true;
|
||||
/**
|
||||
* 上传文件的后缀,是否包含时间戳
|
||||
* 上传文件的后缀,是否启用
|
||||
*
|
||||
* 目的:保证文件的唯一性,避免覆盖
|
||||
* 算法:当前时间戳(毫秒)+ 5 位随机数;目的是保证文件的唯一性,避免覆盖
|
||||
* 定制:可按需调整成 UUID、或者其他方式
|
||||
*/
|
||||
static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = false;
|
||||
/**
|
||||
* 后缀是否作为上级目录
|
||||
*
|
||||
* true:{@code yyyyMMdd/<后缀>/原文件名.ext};保留原文件名
|
||||
* false:{@code yyyyMMdd/原文件名_<后缀>.ext};后缀拼到文件名
|
||||
*/
|
||||
static boolean PATH_SUFFIX_AS_DIRECTORY = true;
|
||||
|
||||
@Resource
|
||||
private FileConfigService fileConfigService;
|
||||
|
|
@ -101,16 +109,21 @@ public class FileServiceImpl implements FileService {
|
|||
}
|
||||
String suffix = null;
|
||||
if (PATH_SUFFIX_TIMESTAMP_ENABLE) {
|
||||
suffix = String.valueOf(System.currentTimeMillis());
|
||||
// 5 位随机数,避免同一毫秒内的重复
|
||||
suffix = String.valueOf(System.currentTimeMillis()) + RandomUtil.randomInt(10000, 100000);
|
||||
}
|
||||
|
||||
// 2.1 先拼接 suffix 后缀
|
||||
if (StrUtil.isNotEmpty(suffix)) {
|
||||
String ext = FileUtil.extName(name);
|
||||
if (StrUtil.isNotEmpty(ext)) {
|
||||
name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
|
||||
if (PATH_SUFFIX_AS_DIRECTORY) {
|
||||
name = suffix + StrUtil.SLASH + name;
|
||||
} else {
|
||||
name = name + StrUtil.C_UNDERLINE + suffix;
|
||||
String ext = FileUtil.extName(name);
|
||||
if (StrUtil.isNotEmpty(ext)) {
|
||||
name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
|
||||
} else {
|
||||
name = name + StrUtil.C_UNDERLINE + suffix;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2.2 再拼接 prefix 前缀
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ yudao:
|
|||
vo-type: 10 # VO 的类型,参见 CodegenVOTypeEnum 枚举类
|
||||
delete-batch-enable: true # 是否生成批量删除接口
|
||||
unit-test-enable: false # 是否生成单元测试
|
||||
import-enable: false # 是否生成 Excel 导入接口
|
||||
tenant: # 多租户相关配置项
|
||||
enable: true
|
||||
ignore-urls:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName};
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
#if ($importEnable)
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
#end
|
||||
import ${jakartaPackage}.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
#if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end
|
||||
|
|
@ -159,6 +162,29 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
|
|||
BeanUtils.toBean(list, ${table.className}RespVO.class));
|
||||
}
|
||||
#end
|
||||
#if ($importEnable)
|
||||
|
||||
@GetMapping("/get-import-template")
|
||||
@Operation(summary = "获得导入${table.classComment}模板")
|
||||
#if ($sceneEnum.scene == 1)
|
||||
@PreAuthorize("@ss.hasPermission('${permissionPrefix}:import')")
|
||||
#end
|
||||
public void importTemplate(HttpServletResponse response) throws IOException {
|
||||
ExcelUtils.write(response, "${table.classComment}导入模板.xls", "数据",
|
||||
${sceneEnum.prefixClass}${table.className}ImportExcelVO.class, Collections.emptyList());
|
||||
}
|
||||
|
||||
@PostMapping("/import")
|
||||
@Operation(summary = "导入${table.classComment}")
|
||||
@Parameter(name = "file", description = "Excel 文件", required = true)
|
||||
#if ($sceneEnum.scene == 1)
|
||||
@PreAuthorize("@ss.hasPermission('${permissionPrefix}:import')")
|
||||
#end
|
||||
public CommonResult<${sceneEnum.prefixClass}${table.className}ImportRespVO> importExcel(@RequestParam("file") MultipartFile file) throws Exception {
|
||||
List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> list = ExcelUtils.read(file, ${sceneEnum.prefixClass}${table.className}ImportExcelVO.class);
|
||||
return success(${classNameVar}Service.import${simpleClassName}List(list));
|
||||
}
|
||||
#end
|
||||
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
|
|
@ -268,4 +294,4 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
|
|||
|
||||
#end
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
#foreach ($column in $columns)
|
||||
#if (${column.createOperation} && "$!column.dictType" != "")
|
||||
import ${DictFormatClassName};
|
||||
import ${DictConvertClassName};
|
||||
#break
|
||||
#end
|
||||
#end
|
||||
|
||||
/**
|
||||
* ${table.classComment} Excel 导入 VO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ${sceneEnum.prefixClass}${table.className}ImportExcelVO {
|
||||
|
||||
## 逐个处理字段
|
||||
#foreach ($column in $columns)
|
||||
#if (${column.createOperation})
|
||||
#if ("$!column.dictType" != "")
|
||||
@ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class)
|
||||
@DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||
#else
|
||||
@ExcelProperty("${column.columnComment}")
|
||||
#end
|
||||
private ${column.javaType} ${column.javaField};
|
||||
|
||||
#end
|
||||
#end
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "${sceneEnum.name} - ${table.classComment}导入 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
public class ${sceneEnum.prefixClass}${table.className}ImportRespVO {
|
||||
|
||||
@Schema(description = "创建成功的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
private Integer successCount;
|
||||
|
||||
@Schema(description = "导入失败的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer failureCount;
|
||||
|
||||
@Schema(description = "导入失败的数据集合,key 为行号,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Map<Integer, String> failureRows;
|
||||
|
||||
}
|
||||
|
|
@ -25,6 +25,16 @@ public interface ${table.className}Service {
|
|||
* @return 编号
|
||||
*/
|
||||
${primaryColumn.javaType} create${simpleClassName}(@Valid ${saveReqVOClass} ${saveReqVOVar});
|
||||
#if ($importEnable)
|
||||
|
||||
/**
|
||||
* 导入${table.classComment}
|
||||
*
|
||||
* @param importList 导入信息
|
||||
* @return 导入结果
|
||||
*/
|
||||
${sceneEnum.prefixClass}${table.className}ImportRespVO import${simpleClassName}List(List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> importList);
|
||||
#end
|
||||
|
||||
/**
|
||||
* 更新${table.classComment}
|
||||
|
|
@ -162,4 +172,4 @@ public interface ${table.className}Service {
|
|||
|
||||
#end
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import org.springframework.validation.annotation.Validated;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
#if ($importEnable)
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
#end
|
||||
import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*;
|
||||
import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO;
|
||||
## 特殊:主子表专属逻辑
|
||||
|
|
@ -91,6 +94,32 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
|
|||
// 返回
|
||||
return ${classNameVar}.getId();
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
|
||||
public ${sceneEnum.prefixClass}${table.className}ImportRespVO import${simpleClassName}List(List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> importList) {
|
||||
if (CollUtil.isEmpty(importList)) {
|
||||
return ${sceneEnum.prefixClass}${table.className}ImportRespVO.builder()
|
||||
.successCount(0).failureCount(0).failureRows(new LinkedHashMap<>()).build();
|
||||
}
|
||||
// 遍历,逐个创建
|
||||
${sceneEnum.prefixClass}${table.className}ImportRespVO respVO = ${sceneEnum.prefixClass}${table.className}ImportRespVO.builder()
|
||||
.successCount(0).failureCount(0).failureRows(new LinkedHashMap<>()).build();
|
||||
AtomicInteger index = new AtomicInteger(1);
|
||||
importList.forEach(importItem -> {
|
||||
int currentIndex = index.getAndIncrement();
|
||||
try {
|
||||
create${simpleClassName}(BeanUtils.toBean(importItem, ${saveReqVOClass}.class));
|
||||
respVO.setSuccessCount(respVO.getSuccessCount() + 1);
|
||||
} catch (Exception ex) {
|
||||
respVO.getFailureRows().put(currentIndex, ex.getMessage());
|
||||
}
|
||||
});
|
||||
respVO.setFailureCount(respVO.getFailureRows().size());
|
||||
return respVO;
|
||||
}
|
||||
#end
|
||||
|
||||
@Override
|
||||
## 特殊:主子表专属逻辑(非 ERP 模式)
|
||||
|
|
@ -359,6 +388,9 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
|
|||
#else
|
||||
#if ( $subTable.subJoinMany)
|
||||
private void create${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
list.forEach(o -> o.set${SubJoinColumnName}(${subJoinColumn.javaField}).clean());
|
||||
${subClassNameVars.get($index)}Mapper.insertBatch(list);
|
||||
}
|
||||
|
|
@ -416,4 +448,4 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
|
|||
#end
|
||||
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
## 通用变量定义
|
||||
#if ($importEnable)
|
||||
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出', '导入'])
|
||||
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export', 'import'])
|
||||
#else
|
||||
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出'])
|
||||
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export'])
|
||||
#end
|
||||
##
|
||||
## 宏定义:生成按钮 SQL(通用部分)
|
||||
#macro(insertButtonSql $parentIdVar)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,26 @@ export function export${simpleClassName}Excel(params) {
|
|||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
// 下载${table.classComment}导入模板
|
||||
export function import${simpleClassName}Template() {
|
||||
return request({
|
||||
url: '${baseURL}/get-import-template',
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 导入${table.classComment}
|
||||
export function import${simpleClassName}(data) {
|
||||
return request({
|
||||
url: '${baseURL}/import',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
#end
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
#set ($index = $foreach.count - 1)
|
||||
|
|
@ -157,4 +177,4 @@ export function export${simpleClassName}Excel(params) {
|
|||
})
|
||||
}
|
||||
#end
|
||||
#end
|
||||
#end
|
||||
|
|
|
|||
|
|
@ -49,6 +49,16 @@
|
|||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
|
||||
v-hasPermi="['${permissionPrefix}:create']">新增</el-button>
|
||||
</el-col>
|
||||
#if ($importEnable)
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="el-icon-upload2" size="mini" @click="handleImport"
|
||||
:loading="importLoading" v-hasPermi="['${permissionPrefix}:import']">导入</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="el-icon-document" size="mini" @click="handleImportTemplate"
|
||||
v-hasPermi="['${permissionPrefix}:import']">导入模板</el-button>
|
||||
</el-col>
|
||||
#end
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
|
||||
v-hasPermi="['${permissionPrefix}:export']">导出</el-button>
|
||||
|
|
@ -78,6 +88,9 @@
|
|||
#end
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
#if ($importEnable)
|
||||
<input ref="importFileRef" type="file" style="display: none" accept=".xls,.xlsx" @change="handleImportFileChange" />
|
||||
#end
|
||||
|
||||
## 特殊:主子表专属逻辑
|
||||
#if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
|
||||
|
|
@ -244,6 +257,10 @@ export default {
|
|||
loading: true,
|
||||
// 导出遮罩层
|
||||
exportLoading: false,
|
||||
#if ($importEnable)
|
||||
// 导入遮罩层
|
||||
importLoading: false,
|
||||
#end
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
## 特殊:树表专属逻辑(树不需要分页接口)
|
||||
|
|
@ -322,6 +339,44 @@ export default {
|
|||
openForm(id) {
|
||||
this.#[[$]]#refs["formRef"].open(id);
|
||||
},
|
||||
#if ($importEnable)
|
||||
/** 导入按钮操作 */
|
||||
handleImport() {
|
||||
this.$refs.importFileRef && this.$refs.importFileRef.click();
|
||||
},
|
||||
/** 导入模板下载 */
|
||||
async handleImportTemplate() {
|
||||
const data = await ${simpleClassName}Api.import${simpleClassName}Template();
|
||||
this.#[[$]]#download.excel(data, '${table.classComment}导入模板.xls');
|
||||
},
|
||||
/** 导入文件变更 */
|
||||
async handleImportFileChange(event) {
|
||||
const target = event.target;
|
||||
const file = target.files && target.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
this.importLoading = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await ${simpleClassName}Api.import${simpleClassName}(formData);
|
||||
const data = res.data || res;
|
||||
let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + ';';
|
||||
if (data.failureRows) {
|
||||
Object.keys(data.failureRows).forEach((rowNo) => {
|
||||
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >';
|
||||
});
|
||||
}
|
||||
await this.$alert(text, '${table.classComment}导入结果', { dangerouslyUseHTMLString: true });
|
||||
await this.getList();
|
||||
} catch {
|
||||
} finally {
|
||||
target.value = '';
|
||||
this.importLoading = false;
|
||||
}
|
||||
},
|
||||
#end
|
||||
/** 删除按钮操作 */
|
||||
async handleDelete(row) {
|
||||
const ${primaryColumn.javaField} = row.${primaryColumn.javaField};
|
||||
|
|
|
|||
|
|
@ -98,6 +98,18 @@ export const ${simpleClassName}Api = {
|
|||
export${simpleClassName}: async (params) => {
|
||||
return await request.download({ url: `${baseURL}/export-excel`, params })
|
||||
},
|
||||
#if ($importEnable)
|
||||
|
||||
// 下载${table.classComment}导入模板
|
||||
import${simpleClassName}Template: async () => {
|
||||
return await request.download({ url: `${baseURL}/get-import-template` })
|
||||
},
|
||||
|
||||
// 导入${table.classComment}
|
||||
import${simpleClassName}: async (data: FormData) => {
|
||||
return await request.upload({ url: `${baseURL}/import`, data })
|
||||
},
|
||||
#end
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
#set ($index = $foreach.count - 1)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<Dialog v-model="dialogVisible" title="${table.classComment}导入" width="400">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
v-model:file-list="fileList"
|
||||
:auto-upload="false"
|
||||
:disabled="formLoading"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
accept=".xlsx, .xls"
|
||||
action="none"
|
||||
drag
|
||||
>
|
||||
<Icon icon="ep:upload" />
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip text-center">
|
||||
<span>仅允许导入 xls、xlsx 格式文件。</span>
|
||||
<el-link
|
||||
:underline="false"
|
||||
style="font-size: 12px; vertical-align: baseline"
|
||||
type="primary"
|
||||
@click="handleDownloadTemplate"
|
||||
>
|
||||
下载模板
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<template #footer>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { UploadUserFile } from 'element-plus'
|
||||
import download from '@/utils/download'
|
||||
import { ${simpleClassName}Api } from '@/api/${table.moduleName}/${table.businessName}'
|
||||
|
||||
/** ${table.classComment} 导入 */
|
||||
defineOptions({ name: '${simpleClassName}ImportForm' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const formLoading = ref(false) // 表单的加载中:上传、下载模板
|
||||
const uploadRef = ref()
|
||||
const fileList = ref<UploadUserFile[]>([])
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async () => {
|
||||
dialogVisible.value = true
|
||||
await resetForm()
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交导入 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
if (fileList.value.length === 0) {
|
||||
message.error('请上传文件')
|
||||
return
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', fileList.value[0].raw as Blob)
|
||||
const res = await ${simpleClassName}Api.import${simpleClassName}(formData)
|
||||
const data = res.data
|
||||
let text = '导入成功数量:' + data.successCount + ';导入失败数量:' + data.failureCount + ';'
|
||||
if (data.failureCount > 0) {
|
||||
for (const rowNo in data.failureRows) {
|
||||
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >'
|
||||
}
|
||||
}
|
||||
message.alert(text)
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
await resetForm()
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载导入模板 */
|
||||
const handleDownloadTemplate = async () => {
|
||||
const data = await ${simpleClassName}Api.import${simpleClassName}Template()
|
||||
download.excel(data, '${table.classComment}导入模板.xls')
|
||||
}
|
||||
|
||||
/** 文件超限 */
|
||||
const handleExceed = (): void => {
|
||||
message.error('最多只能上传一个文件!')
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = async () => {
|
||||
fileList.value = []
|
||||
await nextTick()
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
</script>
|
||||
|
|
@ -92,6 +92,16 @@
|
|||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
#if ($importEnable)
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
@click="handleImport"
|
||||
v-hasPermi="['${permissionPrefix}:import']"
|
||||
>
|
||||
<Icon icon="ep:upload" class="mr-5px" /> 导入
|
||||
</el-button>
|
||||
#end
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
|
|
@ -237,6 +247,11 @@
|
|||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
#if ($importEnable)
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<${simpleClassName}ImportForm ref="importRef" @success="getList" />
|
||||
#end
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<${simpleClassName}Form ref="formRef" @success="getList" />
|
||||
|
|
@ -263,6 +278,9 @@
|
|||
import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { isEmpty } from '@/utils/is'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
#if ($importEnable)
|
||||
import ${simpleClassName}ImportForm from './${simpleClassName}ImportForm.vue'
|
||||
#end
|
||||
## 特殊:树表专属逻辑
|
||||
#if ( $table.templateType == 2 )
|
||||
import { handleTree } from '@/utils/tree'
|
||||
|
|
@ -308,6 +326,9 @@ const queryParams = reactive({
|
|||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
#if ($importEnable)
|
||||
const importRef = ref() // ${table.classComment} 导入组件的 Ref
|
||||
#end
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
|
|
@ -344,6 +365,13 @@ const formRef = ref()
|
|||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
/** 导入按钮操作 */
|
||||
const handleImport = () => {
|
||||
importRef.value.open()
|
||||
}
|
||||
#end
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
|
|
@ -421,4 +449,4 @@ const toggleExpandAll = async () => {
|
|||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -30,3 +30,15 @@ export function delete${simpleClassName}(id: number) {
|
|||
export function export${simpleClassName}(params) {
|
||||
return defHttp.download({ url: '${baseURL}/export-excel', params }, '${table.classComment}.xls')
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
// 下载${table.classComment}导入模板
|
||||
export function import${simpleClassName}Template() {
|
||||
return defHttp.download({ url: '${baseURL}/get-import-template' }, '${table.classComment}导入模板.xls')
|
||||
}
|
||||
|
||||
// 导入${table.classComment}
|
||||
export function import${simpleClassName}(data: FormData) {
|
||||
return defHttp.post({ url: '${baseURL}/import', data })
|
||||
}
|
||||
#end
|
||||
|
|
|
|||
|
|
@ -1,18 +1,31 @@
|
|||
<script lang="ts" setup>
|
||||
import ${simpleClassName}Modal from './${simpleClassName}Modal.vue'
|
||||
import { columns, searchFormSchema } from './${classNameVar}.data'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { useModal } from '@/components/Modal'
|
||||
import { IconEnum } from '@/enums/appEnum'
|
||||
import { BasicTable, TableAction, useTable } from '@/components/Table'
|
||||
import { delete${simpleClassName}, export${simpleClassName}, get${simpleClassName}Page } from '@/api/${table.moduleName}/${table.businessName}'
|
||||
import {
|
||||
delete${simpleClassName},
|
||||
export${simpleClassName},
|
||||
get${simpleClassName}Page,
|
||||
#if ($importEnable)
|
||||
import${simpleClassName},
|
||||
import${simpleClassName}Template,
|
||||
#end
|
||||
} from '@/api/${table.moduleName}/${table.businessName}'
|
||||
|
||||
defineOptions({ name: '${table.className}' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const { createConfirm, createMessage } = useMessage()
|
||||
const [registerModal, { openModal }] = useModal()
|
||||
#if ($importEnable)
|
||||
const importFileRef = ref<HTMLInputElement>()
|
||||
const importLoading = ref(false)
|
||||
#end
|
||||
|
||||
const [registerTable, { getForm, reload }] = useTable({
|
||||
title: '${table.classComment}列表',
|
||||
|
|
@ -48,6 +61,44 @@ async function handleExport() {
|
|||
},
|
||||
})
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
function handleImport() {
|
||||
importFileRef.value?.click()
|
||||
}
|
||||
|
||||
async function handleImportTemplateDownload() {
|
||||
await import${simpleClassName}Template()
|
||||
createMessage.success('模板下载已开始')
|
||||
}
|
||||
|
||||
async function handleImportFileChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
importLoading.value = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const response: any = await import${simpleClassName}(formData)
|
||||
const data = response?.data ?? response
|
||||
let text =
|
||||
'导入成功数量:' + (data?.successCount || 0) + ';导入失败数量:' + (data?.failureCount || 0) + ';'
|
||||
if (data?.failureRows) {
|
||||
Object.keys(data.failureRows).forEach((rowNo) => {
|
||||
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >'
|
||||
})
|
||||
}
|
||||
createMessage.success(text)
|
||||
await reload()
|
||||
} finally {
|
||||
target.value = ''
|
||||
importLoading.value = false
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
async function handleDelete(record: Recordable) {
|
||||
await delete${simpleClassName}(record.id)
|
||||
|
|
@ -62,6 +113,14 @@ async function handleDelete(record: Recordable) {
|
|||
<a-button type="primary" v-auth="['${permissionPrefix}:create']" :preIcon="IconEnum.ADD" @click="handleCreate">
|
||||
{{ t('action.create') }}
|
||||
</a-button>
|
||||
#if ($importEnable)
|
||||
<a-button v-auth="['${permissionPrefix}:import']" :loading="importLoading" @click="handleImport">
|
||||
导入
|
||||
</a-button>
|
||||
<a-button v-auth="['${permissionPrefix}:import']" @click="handleImportTemplateDownload">
|
||||
导入模板
|
||||
</a-button>
|
||||
#end
|
||||
<a-button v-auth="['${permissionPrefix}:export']" :preIcon="IconEnum.EXPORT" @click="handleExport">
|
||||
{{ t('action.export') }}
|
||||
</a-button>
|
||||
|
|
@ -87,6 +146,9 @@ async function handleDelete(record: Recordable) {
|
|||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
#if ($importEnable)
|
||||
<input ref="importFileRef" type="file" accept=".xls,.xlsx" class="hidden" @change="handleImportFileChange" />
|
||||
#end
|
||||
<${simpleClassName}Modal @register="registerModal" @success="reload()" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -100,6 +100,18 @@ export function delete${simpleClassName}List(ids: number[]) {
|
|||
export function export${simpleClassName}(params: any) {
|
||||
return requestClient.download('${baseURL}/export-excel', { params });
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
/** 下载${table.classComment}导入模板 */
|
||||
export function import${simpleClassName}Template() {
|
||||
return requestClient.download('${baseURL}/get-import-template');
|
||||
}
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
export function import${simpleClassName}(data: FormData) {
|
||||
return requestClient.post('${baseURL}/import', data);
|
||||
}
|
||||
#end
|
||||
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
<script lang="ts" setup>
|
||||
import type { FileType } from 'ant-design-vue/es/upload/interface';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { Button, message, Upload } from 'ant-design-vue';
|
||||
|
||||
import { import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}';
|
||||
|
||||
defineOptions({ name: '${simpleClassName}Import' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const fileRef = ref<File | null>(null);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
if (!fileRef.value) {
|
||||
message.error('请上传文件');
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileRef.value);
|
||||
const response: any = await import${simpleClassName}(formData);
|
||||
const data = response?.data ?? response ?? {};
|
||||
let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + ';';
|
||||
if (data.failureRows) {
|
||||
Object.keys(data.failureRows).forEach((rowNo) => {
|
||||
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >';
|
||||
});
|
||||
}
|
||||
message.info(text);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 上传前:拦截 antd Upload 的自动上传,文件存到 ref */
|
||||
function beforeUpload(file: FileType) {
|
||||
fileRef.value = file as unknown as File;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 下载导入模板 */
|
||||
async function handleDownload() {
|
||||
const data = await import${simpleClassName}Template();
|
||||
downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="导入${table.classComment}" class="w-1/3">
|
||||
<div class="mx-4">
|
||||
<Upload :max-count="1" accept=".xls,.xlsx" :before-upload="beforeUpload">
|
||||
<Button type="primary"> 选择 Excel 文件 </Button>
|
||||
</Upload>
|
||||
</div>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<Button @click="handleDownload"> 下载导入模板 </Button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -31,6 +31,9 @@ import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClas
|
|||
#else## 标准表接口
|
||||
import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
|
||||
#end
|
||||
#if ($importEnable)
|
||||
import ${simpleClassName}Import from './modules/import-form.vue';
|
||||
#end
|
||||
|
||||
#if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况
|
||||
/** 子表的列表 */
|
||||
|
|
@ -119,6 +122,17 @@ const [FormModal, formModalApi] = useVbenModal({
|
|||
connectedComponent: ${simpleClassName}Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
#if ($importEnable)
|
||||
const [ImportFormModal, importFormModalApi] = useVbenModal({
|
||||
connectedComponent: ${simpleClassName}Import,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
function handleImport() {
|
||||
importFormModalApi.open();
|
||||
}
|
||||
#end
|
||||
|
||||
/** 创建${table.classComment} */
|
||||
function handleCreate() {
|
||||
|
|
@ -190,6 +204,7 @@ try {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#if (${table.templateType} == 2)
|
||||
/** 切换树形展开/收缩状态 */
|
||||
const isExpanded = ref(true);
|
||||
|
|
@ -209,6 +224,9 @@ onMounted(() => {
|
|||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="getList" />
|
||||
#if ($importEnable)
|
||||
<ImportFormModal @success="getList" />
|
||||
#end
|
||||
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
|
|
@ -314,6 +332,16 @@ onMounted(() => {
|
|||
>
|
||||
{{ $t('ui.actionTitle.create', ['${table.classComment}']) }}
|
||||
</Button>
|
||||
#if ($importEnable)
|
||||
<Button
|
||||
class="ml-2"
|
||||
type="primary"
|
||||
@click="handleImport"
|
||||
v-access:code="['${permissionPrefix}:import']"
|
||||
>
|
||||
导入
|
||||
</Button>
|
||||
#end
|
||||
<Button
|
||||
:icon="h(Download)"
|
||||
type="primary"
|
||||
|
|
|
|||
|
|
@ -134,6 +134,21 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
];
|
||||
}
|
||||
|
||||
#if ($importEnable)
|
||||
/** 导入的表单 */
|
||||
export function useImportFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'file',
|
||||
label: '${table.classComment}数据',
|
||||
component: 'Upload',
|
||||
rules: 'required',
|
||||
help: '仅允许导入 xls、xlsx 格式文件',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
#end
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts" setup>
|
||||
import type { FileType } from 'ant-design-vue/es/upload/interface';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { Button, message, Upload } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useImportFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useImportFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await import${simpleClassName}(data.file);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 上传前 */
|
||||
function beforeUpload(file: FileType) {
|
||||
formApi.setFieldValue('file', file);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 下载模板 */
|
||||
async function handleDownload() {
|
||||
const data = await import${simpleClassName}Template();
|
||||
downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="导入${table.classComment}" class="w-1/3">
|
||||
<Form class="mx-4">
|
||||
<template #file>
|
||||
<div class="w-full">
|
||||
<Upload
|
||||
:max-count="1"
|
||||
accept=".xls,.xlsx"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<Button type="primary"> 选择 Excel 文件 </Button>
|
||||
</Upload>
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<Button @click="handleDownload"> 下载导入模板 </Button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -26,6 +26,9 @@ import {
|
|||
get${simpleClassName}Page,
|
||||
} from '#/api/${table.moduleName}/${table.businessName}';
|
||||
#end
|
||||
#if ($importEnable)
|
||||
import ${simpleClassName}Import from './modules/import-form.vue';
|
||||
#end
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
|
@ -50,6 +53,17 @@ const [FormModal, formModalApi] = useVbenModal({
|
|||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
#if ($importEnable)
|
||||
const [ImportFormModal, importFormModalApi] = useVbenModal({
|
||||
connectedComponent: ${simpleClassName}Import,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
function handleImport() {
|
||||
importFormModalApi.open();
|
||||
}
|
||||
#end
|
||||
#if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
|
||||
|
||||
/** 切换树形展开/收缩状态 */
|
||||
|
|
@ -135,6 +149,7 @@ async function handleExport() {
|
|||
downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data });
|
||||
}
|
||||
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
|
|
@ -210,6 +225,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
#if ($importEnable)
|
||||
<ImportFormModal @success="handleRefresh" />
|
||||
#end
|
||||
#if ($table.templateType == 11) ## erp情况
|
||||
<div>
|
||||
#end
|
||||
|
|
@ -247,6 +265,14 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
onClick: handleExpand,
|
||||
},
|
||||
#end
|
||||
#if ($importEnable)
|
||||
{
|
||||
label: '导入',
|
||||
type: 'primary',
|
||||
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:import'],
|
||||
onClick: handleImport,
|
||||
},
|
||||
#end
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
|
|
@ -318,4 +344,4 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
</div>
|
||||
#end
|
||||
</Page>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -100,6 +100,18 @@ export function delete${simpleClassName}List(ids: number[]) {
|
|||
export function export${simpleClassName}(params: any) {
|
||||
return requestClient.download('${baseURL}/export-excel', { params });
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
/** 下载${table.classComment}导入模板 */
|
||||
export function import${simpleClassName}Template() {
|
||||
return requestClient.download('${baseURL}/get-import-template');
|
||||
}
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
export function import${simpleClassName}(data: FormData) {
|
||||
return requestClient.post('${baseURL}/import', data);
|
||||
}
|
||||
#end
|
||||
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { ElButton, ElMessage, ElUpload } from 'element-plus';
|
||||
|
||||
import { import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}';
|
||||
|
||||
defineOptions({ name: '${simpleClassName}Import' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const fileRef = ref<File | null>(null);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
if (!fileRef.value) {
|
||||
ElMessage.error('请上传文件');
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileRef.value);
|
||||
const response: any = await import${simpleClassName}(formData);
|
||||
const data = response?.data ?? response ?? {};
|
||||
let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + ';';
|
||||
if (data.failureRows) {
|
||||
Object.keys(data.failureRows).forEach((rowNo) => {
|
||||
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >';
|
||||
});
|
||||
}
|
||||
ElMessage.info(text);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 文件改变 */
|
||||
function handleChange(file: any) {
|
||||
if (file.raw) {
|
||||
fileRef.value = file.raw;
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载导入模板 */
|
||||
async function handleDownload() {
|
||||
const data = await import${simpleClassName}Template();
|
||||
downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="导入${table.classComment}" class="w-1/3">
|
||||
<div class="mx-4">
|
||||
<ElUpload
|
||||
:limit="1"
|
||||
accept=".xls,.xlsx"
|
||||
:on-change="handleChange"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
|
||||
</ElUpload>
|
||||
</div>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<ElButton @click="handleDownload"> 下载导入模板 </ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -31,7 +31,9 @@ import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClas
|
|||
import { isEmpty } from '@vben/utils';
|
||||
import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
|
||||
#end
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
#if ($importEnable)
|
||||
import ${simpleClassName}Import from './modules/import-form.vue';
|
||||
#end
|
||||
|
||||
#if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况
|
||||
/** 子表的列表 */
|
||||
|
|
@ -120,6 +122,17 @@ const [FormModal, formModalApi] = useVbenModal({
|
|||
connectedComponent: ${simpleClassName}Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
#if ($importEnable)
|
||||
const [ImportFormModal, importFormModalApi] = useVbenModal({
|
||||
connectedComponent: ${simpleClassName}Import,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
function handleImport() {
|
||||
importFormModalApi.open();
|
||||
}
|
||||
#end
|
||||
|
||||
/** 创建${table.classComment} */
|
||||
function handleCreate() {
|
||||
|
|
@ -189,6 +202,7 @@ try {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#if (${table.templateType} == 2)
|
||||
/** 切换树形展开/收缩状态 */
|
||||
const isExpanded = ref(true);
|
||||
|
|
@ -208,6 +222,9 @@ onMounted(() => {
|
|||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="getList" />
|
||||
#if ($importEnable)
|
||||
<ImportFormModal @success="getList" />
|
||||
#end
|
||||
|
||||
<ContentWrap v-if="!hiddenSearchBar">
|
||||
<!-- 搜索工作栏 -->
|
||||
|
|
@ -316,6 +333,16 @@ onMounted(() => {
|
|||
>
|
||||
{{ $t('ui.actionTitle.create', ['${table.classComment}']) }}
|
||||
</el-button>
|
||||
#if ($importEnable)
|
||||
<el-button
|
||||
class="ml-2"
|
||||
type="primary"
|
||||
@click="handleImport"
|
||||
v-access:code="['${permissionPrefix}:import']"
|
||||
>
|
||||
导入
|
||||
</el-button>
|
||||
#end
|
||||
<el-button
|
||||
:icon="h(Download)"
|
||||
type="primary"
|
||||
|
|
|
|||
|
|
@ -113,6 +113,18 @@ export function delete${simpleClassName}List(ids: number[]) {
|
|||
export function export${simpleClassName}(params: any) {
|
||||
return requestClient.download('${baseURL}/export-excel', { params });
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
/** 下载${table.classComment}导入模板 */
|
||||
export function import${simpleClassName}Template() {
|
||||
return requestClient.download('${baseURL}/get-import-template');
|
||||
}
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
export function import${simpleClassName}(file: File) {
|
||||
return requestClient.upload('${baseURL}/import', { file });
|
||||
}
|
||||
#end
|
||||
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
|
|
|
|||
|
|
@ -137,6 +137,21 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
];
|
||||
}
|
||||
|
||||
#if ($importEnable)
|
||||
/** 导入的表单 */
|
||||
export function useImportFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'file',
|
||||
label: '${table.classComment}数据',
|
||||
component: 'Upload',
|
||||
rules: 'required',
|
||||
help: '仅允许导入 xls、xlsx 格式文件',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
#end
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { ElButton, ElMessage, ElUpload } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useImportFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useImportFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await import${simpleClassName}(data.file);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 文件改变时 */
|
||||
function handleChange(file: any) {
|
||||
if (file.raw) {
|
||||
formApi.setFieldValue('file', file.raw);
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载模板 */
|
||||
async function handleDownload() {
|
||||
const data = await import${simpleClassName}Template();
|
||||
downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="导入${table.classComment}" class="w-1/3">
|
||||
<Form class="mx-4">
|
||||
<template #file>
|
||||
<div class="w-full">
|
||||
<ElUpload
|
||||
:limit="1"
|
||||
accept=".xls,.xlsx"
|
||||
:on-change="handleChange"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
|
||||
</ElUpload>
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<ElButton @click="handleDownload"> 下载导入模板 </ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -26,6 +26,9 @@ import {
|
|||
get${simpleClassName}Page,
|
||||
} from '#/api/${table.moduleName}/${table.businessName}';
|
||||
#end
|
||||
#if ($importEnable)
|
||||
import ${simpleClassName}Import from './modules/import-form.vue';
|
||||
#end
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
|
@ -50,6 +53,17 @@ const [FormModal, formModalApi] = useVbenModal({
|
|||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
#if ($importEnable)
|
||||
const [ImportFormModal, importFormModalApi] = useVbenModal({
|
||||
connectedComponent: ${simpleClassName}Import,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
function handleImport() {
|
||||
importFormModalApi.open();
|
||||
}
|
||||
#end
|
||||
#if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
|
||||
|
||||
/** 切换树形展开/收缩状态 */
|
||||
|
|
@ -133,6 +147,7 @@ async function handleExport() {
|
|||
downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data });
|
||||
}
|
||||
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
|
|
@ -208,6 +223,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
#if ($importEnable)
|
||||
<ImportFormModal @success="handleRefresh" />
|
||||
#end
|
||||
#if ($table.templateType == 11) ## erp情况
|
||||
<div>
|
||||
#end
|
||||
|
|
@ -245,6 +263,14 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
onClick: handleExpand,
|
||||
},
|
||||
#end
|
||||
#if ($importEnable)
|
||||
{
|
||||
label: '导入',
|
||||
type: 'primary',
|
||||
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:import'],
|
||||
onClick: handleImport,
|
||||
},
|
||||
#end
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
|
|
@ -317,4 +343,4 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
</div>
|
||||
#end
|
||||
</Page>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -84,4 +84,19 @@ public class CodegenBuilderTest extends BaseMockitoUnitTest {
|
|||
assertEquals("input", column.getHtmlType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSanitizeComment() {
|
||||
// 1. null / 空字符串:原样返回
|
||||
assertNull(codegenBuilder.sanitizeComment(null));
|
||||
assertEquals("", codegenBuilder.sanitizeComment(""));
|
||||
// 2. 无英文引号:原样返回
|
||||
assertEquals("无引号注释", codegenBuilder.sanitizeComment("无引号注释"));
|
||||
// 3. 含英文双引号:替换为中文左双引号
|
||||
assertEquals("含“双“引号", codegenBuilder.sanitizeComment("含\"双\"引号"));
|
||||
// 4. 含英文单引号:替换为中文左单引号
|
||||
assertEquals("含‘单‘引号", codegenBuilder.sanitizeComment("含'单'引号"));
|
||||
// 5. 双 / 单引号混合
|
||||
assertEquals("“混‘搭“‘", codegenBuilder.sanitizeComment("\"混'搭\"'"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
|||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenVOTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.mockito.InjectMocks;
|
||||
|
|
@ -19,8 +20,10 @@ import java.io.ByteArrayInputStream;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
|
@ -41,16 +44,18 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
|
|||
|
||||
@Spy
|
||||
protected CodegenProperties codegenProperties = new CodegenProperties()
|
||||
.setBasePackage("cn.iocoder.yudao");
|
||||
.setBasePackage("cn.iocoder.yudao")
|
||||
.setVoType(CodegenVOTypeEnum.VO.getType())
|
||||
.setDeleteBatchEnable(true)
|
||||
.setUnitTestEnable(true)
|
||||
.setImportEnable(false);
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
codegenEngine.setJakartaEnable(true); // 强制使用 jakarta,保证单测可以基于 jakarta 断言
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 单测强制使用
|
||||
// 获取测试文件 resources 路径
|
||||
// 获取测试文件 resources 路径,writeResult 调试用
|
||||
String absolutePath = FileUtil.getAbsolutePath("application-unit-test.yaml");
|
||||
// 系统不一样生成的文件也有差异,那就各自生成各自的
|
||||
resourcesPath = absolutePath.split("/target")[0] + "/src/test/resources/codegen/";
|
||||
}
|
||||
|
||||
|
|
@ -82,17 +87,32 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
|
|||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成断言数据的开关,命令行加 {@code -Dcodegen.regenerate=true} 启用
|
||||
*/
|
||||
private static final boolean REGENERATE = Boolean.parseBoolean(System.getProperty("codegen.regenerate", "false"));
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
protected static void assertResult(Map<String, String> result, String path) {
|
||||
protected void assertResult(Map<String, String> result, String path) {
|
||||
if (REGENERATE) {
|
||||
writeResult(result, resourcesPath + path);
|
||||
return;
|
||||
}
|
||||
String assertContent = ResourceUtil.readUtf8Str("codegen/" + path + "/assert.json");
|
||||
List<HashMap> asserts = JsonUtils.parseArray(assertContent, HashMap.class);
|
||||
assertEquals(asserts.size(), result.size());
|
||||
// 校验每个文件
|
||||
Set<String> expectedFiles = asserts.stream()
|
||||
.map(m -> (String) m.get("filePath"))
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
assertEquals(expectedFiles, result.keySet(), "生成文件集合不匹配");
|
||||
// 校验每个文件;归一化 \r\n 为 \n,让断言不依赖文件落盘的换行风格
|
||||
asserts.forEach(assertMap -> {
|
||||
String contentPath = (String) assertMap.get("contentPath");
|
||||
String filePath = (String) assertMap.get("filePath");
|
||||
String content = ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath);
|
||||
assertEquals(content, result.get(filePath), filePath + ":不匹配");
|
||||
String expected = ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath)
|
||||
.replace("\r\n", "\n");
|
||||
String actual = result.get(filePath);
|
||||
assertEquals(expected, actual == null ? null : actual.replace("\r\n", "\n"),
|
||||
filePath + ":不匹配");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + AntD + General 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5AntdGeneralTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType();
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_general_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_general_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_general_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_normal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_antd_general_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_erp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_antd_general_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_inner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_antd_general_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + AntD + Schema 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5AntdSchemaTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType();
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_schema_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_schema_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_schema_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_normal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_antd_schema_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_erp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_antd_schema_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_inner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_antd_schema_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + Element Plus + General 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5EleGeneralTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType();
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_general_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_general_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_general_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_normal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_ele_general_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_erp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_ele_general_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_inner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_ele_general_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + Element Plus + Schema 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5EleSchemaTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType();
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_schema_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_schema_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_schema_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_normal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_ele_schema_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_erp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_ele_schema_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_inner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_ele_schema_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
|||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
|
@ -17,7 +16,6 @@ import java.util.Map;
|
|||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Disabled
|
||||
public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
|
||||
|
||||
@Test
|
||||
|
|
@ -36,6 +34,23 @@ public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
|
|||
assertResult(result, "/vue2_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue2_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vue2_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue2_tree() {
|
||||
// 准备参数
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
|||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
|
@ -17,7 +16,6 @@ import java.util.Map;
|
|||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Disabled
|
||||
public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
|
||||
|
||||
@Test
|
||||
|
|
@ -36,6 +34,23 @@ public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
|
|||
assertResult(result, "/vue3_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue3_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vue3_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue3_tree() {
|
||||
// 准备参数
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
public void setUp() {
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = true;
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -93,7 +94,7 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String url = randomString();
|
||||
AtomicReference<String> pathRef = new AtomicReference<>();
|
||||
when(client.upload(same(content), argThat(path -> {
|
||||
assertTrue(path.matches(directory + "/\\d{8}/" + name + "_\\d+.jpg"));
|
||||
assertTrue(path.matches(directory + "/\\d{8}/\\d+/" + name + ".jpg"));
|
||||
pathRef.set(path);
|
||||
return true;
|
||||
}), eq(type))).thenReturn(url);
|
||||
|
|
@ -125,7 +126,7 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String url = randomString();
|
||||
AtomicReference<String> pathRef = new AtomicReference<>();
|
||||
when(client.upload(same(content), argThat(path -> {
|
||||
assertTrue(path.matches("\\d{8}/6318848e882d8a7e7e82789d87608f684ee52d41966bfc8cad3ce15aad2b970e_\\d+\\.jpg"));
|
||||
assertTrue(path.matches("\\d{8}/\\d+/6318848e882d8a7e7e82789d87608f684ee52d41966bfc8cad3ce15aad2b970e\\.jpg"));
|
||||
pathRef.set(path);
|
||||
return true;
|
||||
}), eq(type))).thenReturn(url);
|
||||
|
|
@ -200,10 +201,10 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/test_timestamp.jpg
|
||||
// 格式为:avatar/yyyyMMdd/{时间戳+随机数}/test.jpg
|
||||
assertTrue(path.startsWith(directory + "/"));
|
||||
// 包含日期格式:8 位数字,如 20240517
|
||||
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+\\.jpg"));
|
||||
assertTrue(path.matches(directory + "/\\d{8}/\\d+/test\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -236,9 +237,9 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/test_timestamp.jpg
|
||||
// 格式为:avatar/{时间戳+随机数}/test.jpg
|
||||
assertTrue(path.startsWith(directory + "/"));
|
||||
assertTrue(path.matches(directory + "/test_\\d+\\.jpg"));
|
||||
assertTrue(path.matches(directory + "/\\d+/test\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -269,9 +270,9 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/test_timestamp
|
||||
// 格式为:avatar/yyyyMMdd/{时间戳+随机数}/test
|
||||
assertTrue(path.startsWith(directory + "/"));
|
||||
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+"));
|
||||
assertTrue(path.matches(directory + "/\\d{8}/\\d+/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -286,8 +287,59 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:yyyyMMdd/test_timestamp.jpg
|
||||
assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg"));
|
||||
// 格式为:yyyyMMdd/{时间戳+随机数}/test.jpg
|
||||
assertTrue(path.matches("\\d{8}/\\d+/test\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_SuffixAsName_AllEnabled() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = false;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/test_{时间戳+随机数}.jpg
|
||||
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_SuffixAsName_PrefixDisabled() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = false;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/test_{时间戳+随机数}.jpg
|
||||
assertTrue(path.matches(directory + "/test_\\d+\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_SuffixAsName_NoExtension() {
|
||||
// 准备参数
|
||||
String name = "test";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = false;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/test_{时间戳+随机数}
|
||||
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -302,8 +354,8 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:yyyyMMdd/test_timestamp.jpg
|
||||
assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg"));
|
||||
// 格式为:yyyyMMdd/{时间戳+随机数}/test.jpg
|
||||
assertTrue(path.matches("\\d{8}/\\d+/test\\.jpg"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
[ {
|
||||
"contentPath" : "java/InfraStudentPageReqVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentRespVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentSaveReqVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentController",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
|
||||
}, {
|
||||
"contentPath" : "xml/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/resources/mapper/demo/InfraStudentMapper.xml"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImpl",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentService",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImplTest",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
|
||||
}, {
|
||||
"contentPath" : "java/ErrorCodeConstants_手动操作",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
|
||||
}, {
|
||||
"contentPath" : "sql/sql",
|
||||
"filePath" : "sql/sql.sql"
|
||||
}, {
|
||||
"contentPath" : "sql/h2",
|
||||
"filePath" : "sql/h2.sql"
|
||||
}, {
|
||||
"contentPath" : "vue/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/index.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/form.vue"
|
||||
}, {
|
||||
"contentPath" : "ts/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/api/infra/demo/index.ts"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-list",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-list.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-list",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-list.vue"
|
||||
} ]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
ErrorCode STUDENT_CONTACT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生联系人不存在");
|
||||
ErrorCode STUDENT_TEACHER_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任不存在");
|
||||
ErrorCode STUDENT_TEACHER_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任已存在");
|
||||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
ErrorCode STUDENT_CONTACT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生联系人不存在");
|
||||
ErrorCode STUDENT_TEACHER_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任不存在");
|
||||
ErrorCode STUDENT_TEACHER_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任已存在");
|
||||
|
|
@ -27,4 +27,8 @@ public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentConta
|
|||
return delete(InfraStudentContactDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
package cn.iocoder.yudao.module.infra.controller.admin.demo;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import javax.validation.constraints.*;
|
||||
import javax.validation.*;
|
||||
import javax.servlet.http.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import jakarta.validation.*;
|
||||
import jakarta.servlet.http.*;
|
||||
import java.util.*;
|
||||
import java.io.IOException;
|
||||
|
||||
|
|
@ -22,8 +22,8 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
|||
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
|
||||
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
|
||||
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
|
|
@ -64,6 +64,15 @@ public class InfraStudentController {
|
|||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得学生")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
|
|
@ -84,7 +93,7 @@ public class InfraStudentController {
|
|||
@GetMapping("/export-excel")
|
||||
@Operation(summary = "导出学生 Excel")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:export')")
|
||||
@OperateLog(type = EXPORT)
|
||||
@ApiAccessLog(operateType = EXPORT)
|
||||
public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
|
|
@ -129,6 +138,15 @@ public class InfraStudentController {
|
|||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/student-contact/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生联系人")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentContactList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentContactListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/student-contact/get")
|
||||
@Operation(summary = "获得学生联系人")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
|
|
@ -172,6 +190,15 @@ public class InfraStudentController {
|
|||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/student-teacher/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生班主任")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentTeacherList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentTeacherListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/student-teacher/get")
|
||||
@Operation(summary = "获得学生班主任")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
|
|
@ -64,4 +64,5 @@ public class InfraStudentDO extends BaseDO {
|
|||
*/
|
||||
private String memo;
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -11,8 +11,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
|
|||
|
||||
@Schema(description = "管理后台 - 学生分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class InfraStudentPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "名字", example = "芋头")
|
||||
|
|
@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.util.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import cn.idev.excel.annotation.*;
|
||||
|
|
@ -57,4 +56,4 @@ public class InfraStudentRespVO {
|
|||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,7 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import javax.validation.constraints.*;
|
||||
import java.util.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import java.util.*;
|
||||
import javax.validation.*;
|
||||
import jakarta.validation.*;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
|
|
@ -38,6 +38,13 @@ public interface InfraStudentService {
|
|||
*/
|
||||
void deleteStudent(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除学生
|
||||
*
|
||||
* @param ids 编号
|
||||
*/
|
||||
void deleteStudentListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得学生
|
||||
*
|
||||
|
|
@ -87,6 +94,13 @@ public interface InfraStudentService {
|
|||
*/
|
||||
void deleteStudentContact(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除学生联系人
|
||||
*
|
||||
* @param ids 编号
|
||||
*/
|
||||
void deleteStudentContactListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得学生联系人
|
||||
*
|
||||
|
|
@ -128,6 +142,13 @@ public interface InfraStudentService {
|
|||
*/
|
||||
void deleteStudentTeacher(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除学生班主任
|
||||
*
|
||||
* @param ids 编号
|
||||
*/
|
||||
void deleteStudentTeacherListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得学生班主任
|
||||
*
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
|
|
@ -19,6 +20,8 @@ import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
|
|||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
|
|
@ -42,6 +45,7 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
// 插入
|
||||
InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
|
||||
studentMapper.insert(student);
|
||||
|
||||
// 返回
|
||||
return student.getId();
|
||||
}
|
||||
|
|
@ -68,6 +72,18 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
deleteStudentTeacherByStudentId(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteStudentListByIds(List<Long> ids) {
|
||||
// 删除
|
||||
studentMapper.deleteByIds(ids);
|
||||
|
||||
// 删除子表
|
||||
deleteStudentContactByStudentIds(ids);
|
||||
deleteStudentTeacherByStudentIds(ids);
|
||||
}
|
||||
|
||||
|
||||
private void validateStudentExists(Long id) {
|
||||
if (studentMapper.selectById(id) == null) {
|
||||
throw exception(STUDENT_NOT_EXISTS);
|
||||
|
|
@ -93,6 +109,7 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
|
||||
@Override
|
||||
public Long createStudentContact(InfraStudentContactDO studentContact) {
|
||||
studentContact.clean(); // 清理掉创建、更新时间等相关属性值
|
||||
studentContactMapper.insert(studentContact);
|
||||
return studentContact.getId();
|
||||
}
|
||||
|
|
@ -102,17 +119,22 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
// 校验存在
|
||||
validateStudentContactExists(studentContact.getId());
|
||||
// 更新
|
||||
studentContact.clean(); // 解决更新情况下:updateTime 不更新
|
||||
studentContactMapper.updateById(studentContact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteStudentContact(Long id) {
|
||||
// 校验存在
|
||||
validateStudentContactExists(id);
|
||||
// 删除
|
||||
studentContactMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteStudentContactListByIds(List<Long> ids) {
|
||||
// 删除
|
||||
studentContactMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfraStudentContactDO getStudentContact(Long id) {
|
||||
return studentContactMapper.selectById(id);
|
||||
|
|
@ -128,6 +150,10 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
studentContactMapper.deleteByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void deleteStudentContactByStudentIds(List<Long> studentIds) {
|
||||
studentContactMapper.deleteByStudentIds(studentIds);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
@Override
|
||||
|
|
@ -142,6 +168,7 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
throw exception(STUDENT_TEACHER_EXISTS);
|
||||
}
|
||||
// 插入
|
||||
studentTeacher.clean(); // 清理掉创建、更新时间等相关属性值
|
||||
studentTeacherMapper.insert(studentTeacher);
|
||||
return studentTeacher.getId();
|
||||
}
|
||||
|
|
@ -151,17 +178,22 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
// 校验存在
|
||||
validateStudentTeacherExists(studentTeacher.getId());
|
||||
// 更新
|
||||
studentTeacher.clean(); // 解决更新情况下:updateTime 不更新
|
||||
studentTeacherMapper.updateById(studentTeacher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteStudentTeacher(Long id) {
|
||||
// 校验存在
|
||||
validateStudentTeacherExists(id);
|
||||
// 删除
|
||||
studentTeacherMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteStudentTeacherListByIds(List<Long> ids) {
|
||||
// 删除
|
||||
studentTeacherMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfraStudentTeacherDO getStudentTeacher(Long id) {
|
||||
return studentTeacherMapper.selectById(id);
|
||||
|
|
@ -177,4 +209,8 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
studentTeacherMapper.deleteByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void deleteStudentTeacherByStudentIds(List<Long> studentIds) {
|
||||
studentTeacherMapper.deleteByStudentIds(studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.service.demo;
|
|||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
|||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
|
@ -22,9 +22,16 @@ public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeach
|
|||
.eq(InfraStudentTeacherDO::getStudentId, studentId)
|
||||
.orderByDesc(InfraStudentTeacherDO::getId));
|
||||
}
|
||||
default InfraStudentTeacherDO selectByStudentId(Long studentId) {
|
||||
return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentId(Long studentId) {
|
||||
return delete(InfraStudentTeacherDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,55 +1,54 @@
|
|||
-- 菜单 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, component_name
|
||||
)
|
||||
VALUES (
|
||||
'学生管理', '', 2, 0, 888,
|
||||
'student', '', 'infra/demo/index', 0, 'InfraStudent'
|
||||
);
|
||||
|
||||
-- 按钮父菜单ID
|
||||
-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
|
||||
SELECT @parentId := LAST_INSERT_ID();
|
||||
|
||||
-- 按钮 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生查询', 'infra:student:query', 3, 1, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生创建', 'infra:student:create', 3, 2, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生更新', 'infra:student:update', 3, 3, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生删除', 'infra:student:delete', 3, 4, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生导出', 'infra:student:export', 3, 5, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
-- 菜单 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, component_name
|
||||
)
|
||||
VALUES (
|
||||
'学生管理', '', 2, 0, 888,
|
||||
'student', '', 'infra/demo/index', 0, 'InfraStudent'
|
||||
);
|
||||
|
||||
-- 按钮父菜单ID
|
||||
SELECT @parentId := LAST_INSERT_ID();
|
||||
|
||||
-- 按钮 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生查询', 'infra:student:query', 3, 1, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生创建', 'infra:student:create', 3, 2, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生更新', 'infra:student:update', 3, 3, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生删除', 'infra:student:delete', 3, 4, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生导出', 'infra:student:export', 3, 5, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace StudentApi {
|
||||
/** 学生联系人信息 */
|
||||
export interface StudentContact {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
|
||||
/** 学生班主任信息 */
|
||||
export interface StudentTeacher {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
|
||||
/** 学生信息 */
|
||||
export interface Student {
|
||||
id: number; // 编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video?: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询学生分页 */
|
||||
export function getStudentPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<StudentApi.Student>>('/infra/student/page', { params });
|
||||
}
|
||||
|
||||
/** 查询学生详情 */
|
||||
export function getStudent(id: number) {
|
||||
return requestClient.get<StudentApi.Student>(`/infra/student/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增学生 */
|
||||
export function createStudent(data: StudentApi.Student) {
|
||||
return requestClient.post('/infra/student/create', data);
|
||||
}
|
||||
|
||||
/** 修改学生 */
|
||||
export function updateStudent(data: StudentApi.Student) {
|
||||
return requestClient.put('/infra/student/update', data);
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
export function deleteStudent(id: number) {
|
||||
return requestClient.delete(`/infra/student/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
export function deleteStudentList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/student/delete-list?ids=${ids.join(',')}`)
|
||||
}
|
||||
|
||||
/** 导出学生 */
|
||||
export function exportStudent(params: any) {
|
||||
return requestClient.download('/infra/student/export-excel', { params });
|
||||
}
|
||||
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
/** 获得学生联系人分页 */
|
||||
export function getStudentContactPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<StudentApi.StudentContact>>(`/infra/student/student-contact/page`, { params });
|
||||
}
|
||||
/** 新增学生联系人 */
|
||||
export function createStudentContact(data: StudentApi.StudentContact) {
|
||||
return requestClient.post(`/infra/student/student-contact/create`, data);
|
||||
}
|
||||
|
||||
/** 修改学生联系人 */
|
||||
export function updateStudentContact(data: StudentApi.StudentContact) {
|
||||
return requestClient.put(`/infra/student/student-contact/update`, data);
|
||||
}
|
||||
|
||||
/** 删除学生联系人 */
|
||||
export function deleteStudentContact(id: number) {
|
||||
return requestClient.delete(`/infra/student/student-contact/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除学生联系人 */
|
||||
export function deleteStudentContactList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/student/student-contact/delete-list?ids=${ids.join(',')}`)
|
||||
}
|
||||
|
||||
/** 获得学生联系人 */
|
||||
export function getStudentContact(id: number) {
|
||||
return requestClient.get<StudentApi.StudentContact>(`/infra/student/student-contact/get?id=${id}`);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
/** 获得学生班主任分页 */
|
||||
export function getStudentTeacherPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<StudentApi.StudentTeacher>>(`/infra/student/student-teacher/page`, { params });
|
||||
}
|
||||
/** 新增学生班主任 */
|
||||
export function createStudentTeacher(data: StudentApi.StudentTeacher) {
|
||||
return requestClient.post(`/infra/student/student-teacher/create`, data);
|
||||
}
|
||||
|
||||
/** 修改学生班主任 */
|
||||
export function updateStudentTeacher(data: StudentApi.StudentTeacher) {
|
||||
return requestClient.put(`/infra/student/student-teacher/update`, data);
|
||||
}
|
||||
|
||||
/** 删除学生班主任 */
|
||||
export function deleteStudentTeacher(id: number) {
|
||||
return requestClient.delete(`/infra/student/student-teacher/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除学生班主任 */
|
||||
export function deleteStudentTeacherList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/student/student-teacher/delete-list?ids=${ids.join(',')}`)
|
||||
}
|
||||
|
||||
/** 获得学生班主任 */
|
||||
export function getStudentTeacher(id: number) {
|
||||
return requestClient.get<StudentApi.StudentTeacher>(`/infra/student/student-teacher/get?id=${id}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { getStudent, createStudent, updateStudent } from '#/api/infra/demo';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.Student>>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
video: [{ required: true, message: '附件不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生'])
|
||||
: $t('ui.actionTitle.create', ['学生']);
|
||||
});
|
||||
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm() {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
await formRef.value?.validate();
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = formData.value as StudentApi.Student;
|
||||
try {
|
||||
await (formData.value?.id ? updateStudent(data) : createStudent(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data = modalApi.getData<StudentApi.Student>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getStudent(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
formData.value = data;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { ref, h, reactive, onMounted, nextTick } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
|
||||
import { cloneDeep, downloadFileFromBlobPart, formatDateTime, isEmpty } from '@vben/utils';
|
||||
import { Button, Card, message, Tabs, Pagination, Form, RangePicker, DatePicker, Select, Input } from 'ant-design-vue';
|
||||
import StudentForm from './modules/form.vue';
|
||||
import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
|
||||
import StudentContactList from './modules/student-contact-list.vue'
|
||||
import StudentTeacherList from './modules/student-teacher-list.vue'
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/demo';
|
||||
|
||||
/** 子表的列表 */
|
||||
const subTabsName = ref('studentContact')
|
||||
const selectStudent = ref<StudentApi.Student>();
|
||||
async function onCellClick({ row }: { row: StudentApi.Student }) {
|
||||
selectStudent.value = row
|
||||
}
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.Student[]>([]) // 列表的数据
|
||||
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
birthday: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
createTime: undefined,
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = cloneDeep(queryParams) as any;
|
||||
if (params.birthday && Array.isArray(params.birthday)) {
|
||||
params.birthday = (params.birthday as string[]).join(',');
|
||||
}
|
||||
if (params.createTime && Array.isArray(params.createTime)) {
|
||||
params.createTime = (params.createTime as string[]).join(',');
|
||||
}
|
||||
const data = await getStudentPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: StudentForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑学生 */
|
||||
function handleEdit(row: StudentApi.Student) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
|
||||
/** 删除学生 */
|
||||
async function handleDelete(row: StudentApi.Student) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudent(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
async function handleDeleteBatch() {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: StudentApi.Student[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
try {
|
||||
exportLoading.value = true;
|
||||
const data = await exportStudent(queryParams);
|
||||
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
|
||||
}finally {
|
||||
exportLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 初始化 */
|
||||
const { hiddenSearchBar, tableToolbarRef, tableRef } = useTableToolbar();
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="getList" />
|
||||
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="请输入名字"
|
||||
allowClear
|
||||
@pressEnter="handleQuery"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="queryParams.birthday"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="选择出生日期"
|
||||
allowClear
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select
|
||||
v-model:value="queryParams.sex"
|
||||
placeholder="请选择性别"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<Select
|
||||
v-model:value="queryParams.enabled"
|
||||
placeholder="请选择是否有效"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="创建时间" name="createTime">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.createTime"
|
||||
v-bind="getRangePickerDefaultProps()"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||
搜索
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<!-- 列表 -->
|
||||
<Card title="学生">
|
||||
<template #extra>
|
||||
<VbenVxeTableToolbar
|
||||
ref="tableToolbarRef"
|
||||
v-model:hidden-search="hiddenSearchBar"
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
:icon="h(Plus)"
|
||||
type="primary"
|
||||
@click="handleCreate"
|
||||
v-access:code="['infra:student:create']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Download)"
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
:loading="exportLoading"
|
||||
@click="handleExport"
|
||||
v-access:code="['infra:student:export']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Trash2)"
|
||||
type="primary"
|
||||
danger
|
||||
class="ml-2"
|
||||
:disabled="isEmpty(checkedIds)"
|
||||
@click="handleDeleteBatch"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</VbenVxeTableToolbar>
|
||||
</template>
|
||||
<VxeTable
|
||||
ref="tableRef"
|
||||
:data="list"
|
||||
@cell-click="onCellClick"
|
||||
:row-config="{
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
isCurrent: true,
|
||||
}"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
@checkboxAll="handleRowCheckboxChange"
|
||||
@checkboxChange="handleRowCheckboxChange"
|
||||
>
|
||||
<VxeColumn type="checkbox" width="40" />
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{row}">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleEdit(row)"
|
||||
v-access:code="['infra:student:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
class="ml-2"
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<!-- 子表的表单 -->
|
||||
<Tabs v-model:active-key="subTabsName">
|
||||
<Tabs.TabPane key="studentContact" tab="学生联系人" force-render>
|
||||
<StudentContactList :student-id="selectStudent?.id" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="studentTeacher" tab="学生班主任" force-render>
|
||||
<StudentTeacherList :student-id="selectStudent?.id" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { getStudentContact, createStudentContact, updateStudentContact } from '#/api/infra/demo';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生联系人'])
|
||||
: $t('ui.actionTitle.create', ['学生联系人']);
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.StudentContact>>({
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
await formRef.value?.validate();
|
||||
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = formData.value as StudentApi.StudentContact;
|
||||
try {
|
||||
await (formData.value?.id ? updateStudentContact(data) : createStudentContact(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
let data = modalApi.getData<StudentApi.StudentContact>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getStudentContact(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
},
|
||||
});
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm(){
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
|
||||
import { reactive, ref, h, nextTick, watch, onMounted } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
|
||||
import StudentContactForm from './student-contact-form.vue'
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Button, Card, Tabs, Pagination, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, RangePicker, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
import { Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { deleteStudentContact, deleteStudentContactList, getStudentContactPage } from '#/api/infra/demo';
|
||||
import { isEmpty, cloneDeep } from '@vben/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: StudentContactForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生联系人 */
|
||||
function handleCreate() {
|
||||
if (!props.studentId){
|
||||
message.warning("请先选择一个学生!")
|
||||
return
|
||||
}
|
||||
formModalApi.setData({studentId: props.studentId}).open();
|
||||
}
|
||||
|
||||
/** 编辑学生联系人 */
|
||||
function handleEdit(row: StudentApi.StudentContact) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除学生联系人 */
|
||||
async function handleDelete(row: StudentApi.StudentContact) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentContact(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生联系人 */
|
||||
async function handleDeleteBatch() {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentContactList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: StudentApi.StudentContact[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.StudentContact[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
birthday: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
createTime: undefined,
|
||||
})
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!props.studentId){
|
||||
return []
|
||||
}
|
||||
const params = cloneDeep(queryParams) as any;
|
||||
if (params.birthday && Array.isArray(params.birthday)) {
|
||||
params.birthday = (params.birthday as string[]).join(',');
|
||||
}
|
||||
if (params.createTime && Array.isArray(params.createTime)) {
|
||||
params.createTime = (params.createTime as string[]).join(',');
|
||||
}
|
||||
params.studentId = props.studentId;
|
||||
const data = await getStudentContactPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await getList()
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
const { hiddenSearchBar, tableToolbarRef, tableRef } = useTableToolbar();
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormModal @success="getList" />
|
||||
<div class="h-[600px]">
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="请输入名字"
|
||||
allowClear
|
||||
@pressEnter="handleQuery"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="queryParams.birthday"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="选择出生日期"
|
||||
allowClear
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select
|
||||
v-model:value="queryParams.sex"
|
||||
placeholder="请选择性别"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<Select
|
||||
v-model:value="queryParams.enabled"
|
||||
placeholder="请选择是否有效"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="创建时间" name="createTime">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.createTime"
|
||||
v-bind="getRangePickerDefaultProps()"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||
搜索
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<!-- 列表 -->
|
||||
<Card title="学生">
|
||||
<template #extra>
|
||||
<VbenVxeTableToolbar
|
||||
ref="tableToolbarRef"
|
||||
v-model:hidden-search="hiddenSearchBar"
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
:icon="h(Plus)"
|
||||
type="primary"
|
||||
@click="handleCreate"
|
||||
v-access:code="['infra:student:create']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Trash2)"
|
||||
type="primary"
|
||||
danger
|
||||
class="ml-2"
|
||||
:disabled="isEmpty(checkedIds)"
|
||||
@click="handleDeleteBatch"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</VbenVxeTableToolbar>
|
||||
</template>
|
||||
<VxeTable
|
||||
ref="tableRef"
|
||||
:data="list"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
@checkboxAll="handleRowCheckboxChange"
|
||||
@checkboxChange="handleRowCheckboxChange"
|
||||
>
|
||||
<VxeColumn type="checkbox" width="40" />
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{row}">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleEdit(row)"
|
||||
v-access:code="['infra:student:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
class="ml-2"
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { getStudentTeacher, createStudentTeacher, updateStudentTeacher } from '#/api/infra/demo';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生班主任'])
|
||||
: $t('ui.actionTitle.create', ['学生班主任']);
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.StudentTeacher>>({
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
await formRef.value?.validate();
|
||||
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = formData.value as StudentApi.StudentTeacher;
|
||||
try {
|
||||
await (formData.value?.id ? updateStudentTeacher(data) : createStudentTeacher(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
let data = modalApi.getData<StudentApi.StudentTeacher>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getStudentTeacher(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
},
|
||||
});
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm(){
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
|
||||
import { reactive, ref, h, nextTick, watch, onMounted } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
|
||||
import StudentTeacherForm from './student-teacher-form.vue'
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Button, Card, Tabs, Pagination, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, RangePicker, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
import { Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { deleteStudentTeacher, deleteStudentTeacherList, getStudentTeacherPage } from '#/api/infra/demo';
|
||||
import { isEmpty, cloneDeep } from '@vben/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: StudentTeacherForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生班主任 */
|
||||
function handleCreate() {
|
||||
if (!props.studentId){
|
||||
message.warning("请先选择一个学生!")
|
||||
return
|
||||
}
|
||||
formModalApi.setData({studentId: props.studentId}).open();
|
||||
}
|
||||
|
||||
/** 编辑学生班主任 */
|
||||
function handleEdit(row: StudentApi.StudentTeacher) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除学生班主任 */
|
||||
async function handleDelete(row: StudentApi.StudentTeacher) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentTeacher(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生班主任 */
|
||||
async function handleDeleteBatch() {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentTeacherList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: StudentApi.StudentTeacher[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.StudentTeacher[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
birthday: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
createTime: undefined,
|
||||
})
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!props.studentId){
|
||||
return []
|
||||
}
|
||||
const params = cloneDeep(queryParams) as any;
|
||||
if (params.birthday && Array.isArray(params.birthday)) {
|
||||
params.birthday = (params.birthday as string[]).join(',');
|
||||
}
|
||||
if (params.createTime && Array.isArray(params.createTime)) {
|
||||
params.createTime = (params.createTime as string[]).join(',');
|
||||
}
|
||||
params.studentId = props.studentId;
|
||||
const data = await getStudentTeacherPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await getList()
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
const { hiddenSearchBar, tableToolbarRef, tableRef } = useTableToolbar();
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormModal @success="getList" />
|
||||
<div class="h-[600px]">
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="请输入名字"
|
||||
allowClear
|
||||
@pressEnter="handleQuery"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="queryParams.birthday"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="选择出生日期"
|
||||
allowClear
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select
|
||||
v-model:value="queryParams.sex"
|
||||
placeholder="请选择性别"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<Select
|
||||
v-model:value="queryParams.enabled"
|
||||
placeholder="请选择是否有效"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="创建时间" name="createTime">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.createTime"
|
||||
v-bind="getRangePickerDefaultProps()"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||
搜索
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<!-- 列表 -->
|
||||
<Card title="学生">
|
||||
<template #extra>
|
||||
<VbenVxeTableToolbar
|
||||
ref="tableToolbarRef"
|
||||
v-model:hidden-search="hiddenSearchBar"
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
:icon="h(Plus)"
|
||||
type="primary"
|
||||
@click="handleCreate"
|
||||
v-access:code="['infra:student:create']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Trash2)"
|
||||
type="primary"
|
||||
danger
|
||||
class="ml-2"
|
||||
:disabled="isEmpty(checkedIds)"
|
||||
@click="handleDeleteBatch"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</VbenVxeTableToolbar>
|
||||
</template>
|
||||
<VxeTable
|
||||
ref="tableRef"
|
||||
:data="list"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
@checkboxAll="handleRowCheckboxChange"
|
||||
@checkboxChange="handleRowCheckboxChange"
|
||||
>
|
||||
<VxeColumn type="checkbox" width="40" />
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{row}">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleEdit(row)"
|
||||
v-access:code="['infra:student:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
class="ml-2"
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
[ {
|
||||
"contentPath" : "java/InfraStudentPageReqVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentRespVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentSaveReqVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentController",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
|
||||
}, {
|
||||
"contentPath" : "xml/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/resources/mapper/demo/InfraStudentMapper.xml"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImpl",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentService",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImplTest",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
|
||||
}, {
|
||||
"contentPath" : "java/ErrorCodeConstants_手动操作",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
|
||||
}, {
|
||||
"contentPath" : "sql/sql",
|
||||
"filePath" : "sql/sql.sql"
|
||||
}, {
|
||||
"contentPath" : "sql/h2",
|
||||
"filePath" : "sql/h2.sql"
|
||||
}, {
|
||||
"contentPath" : "vue/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/index.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/form.vue"
|
||||
}, {
|
||||
"contentPath" : "ts/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/api/infra/demo/index.ts"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-list",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-list.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-list",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-list.vue"
|
||||
} ]
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
|
|
@ -25,4 +25,8 @@ public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentConta
|
|||
return delete(InfraStudentContactDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
package cn.iocoder.yudao.module.infra.controller.admin.demo;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import javax.validation.constraints.*;
|
||||
import javax.validation.*;
|
||||
import javax.servlet.http.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import jakarta.validation.*;
|
||||
import jakarta.servlet.http.*;
|
||||
import java.util.*;
|
||||
import java.io.IOException;
|
||||
|
||||
|
|
@ -22,8 +22,8 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
|||
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
|
||||
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
|
||||
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
|
|
@ -64,6 +64,15 @@ public class InfraStudentController {
|
|||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得学生")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
|
|
@ -84,7 +93,7 @@ public class InfraStudentController {
|
|||
@GetMapping("/export-excel")
|
||||
@Operation(summary = "导出学生 Excel")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:export')")
|
||||
@OperateLog(type = EXPORT)
|
||||
@ApiAccessLog(operateType = EXPORT)
|
||||
public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
|
|
@ -64,4 +64,5 @@ public class InfraStudentDO extends BaseDO {
|
|||
*/
|
||||
private String memo;
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -11,8 +11,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
|
|||
|
||||
@Schema(description = "管理后台 - 学生分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class InfraStudentPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "名字", example = "芋头")
|
||||
|
|
@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.util.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import cn.idev.excel.annotation.*;
|
||||
|
|
@ -57,4 +56,4 @@ public class InfraStudentRespVO {
|
|||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,7 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import javax.validation.constraints.*;
|
||||
import java.util.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import java.util.*;
|
||||
import javax.validation.*;
|
||||
import jakarta.validation.*;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
|
|
@ -38,6 +38,13 @@ public interface InfraStudentService {
|
|||
*/
|
||||
void deleteStudent(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除学生
|
||||
*
|
||||
* @param ids 编号
|
||||
*/
|
||||
void deleteStudentListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得学生
|
||||
*
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
|
|
@ -19,6 +20,8 @@ import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
|
|||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
|
|
@ -44,6 +47,7 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
|
||||
studentMapper.insert(student);
|
||||
|
||||
|
||||
// 插入子表
|
||||
createStudentContactList(student.getId(), createReqVO.getStudentContacts());
|
||||
createStudentTeacher(student.getId(), createReqVO.getStudentTeacher());
|
||||
|
|
@ -78,6 +82,18 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
deleteStudentTeacherByStudentId(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteStudentListByIds(List<Long> ids) {
|
||||
// 删除
|
||||
studentMapper.deleteByIds(ids);
|
||||
|
||||
// 删除子表
|
||||
deleteStudentContactByStudentIds(ids);
|
||||
deleteStudentTeacherByStudentIds(ids);
|
||||
}
|
||||
|
||||
|
||||
private void validateStudentExists(Long id) {
|
||||
if (studentMapper.selectById(id) == null) {
|
||||
throw exception(STUDENT_NOT_EXISTS);
|
||||
|
|
@ -102,20 +118,44 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
}
|
||||
|
||||
private void createStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
|
||||
list.forEach(o -> o.setStudentId(studentId));
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
list.forEach(o -> o.setStudentId(studentId).clean());
|
||||
studentContactMapper.insertBatch(list);
|
||||
}
|
||||
|
||||
private void updateStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
|
||||
deleteStudentContactByStudentId(studentId);
|
||||
list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新
|
||||
createStudentContactList(studentId, list);
|
||||
list.forEach(o -> o.setStudentId(studentId).clean());
|
||||
List<InfraStudentContactDO> oldList = studentContactMapper.selectListByStudentId(studentId);
|
||||
List<List<InfraStudentContactDO>> diffList = diffList(oldList, list, (oldVal, newVal) -> {
|
||||
boolean same = ObjectUtil.equal(oldVal.getId(), newVal.getId());
|
||||
if (same) {
|
||||
newVal.setId(oldVal.getId()).clean(); // 解决更新情况下:updateTime 不更新
|
||||
}
|
||||
return same;
|
||||
});
|
||||
|
||||
// 第二步,批量添加、修改、删除
|
||||
if (CollUtil.isNotEmpty(diffList.get(0))) {
|
||||
studentContactMapper.insertBatch(diffList.get(0));
|
||||
}
|
||||
if (CollUtil.isNotEmpty(diffList.get(1))) {
|
||||
studentContactMapper.updateBatch(diffList.get(1));
|
||||
}
|
||||
if (CollUtil.isNotEmpty(diffList.get(2))) {
|
||||
studentContactMapper.deleteByIds(convertList(diffList.get(2), InfraStudentContactDO::getId));
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteStudentContactByStudentId(Long studentId) {
|
||||
studentContactMapper.deleteByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void deleteStudentContactByStudentIds(List<Long> studentIds) {
|
||||
studentContactMapper.deleteByStudentIds(studentIds);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
@Override
|
||||
|
|
@ -135,8 +175,7 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
if (studentTeacher == null) {
|
||||
return;
|
||||
}
|
||||
studentTeacher.setStudentId(studentId);
|
||||
studentTeacher.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新
|
||||
studentTeacher.setStudentId(studentId).clean();// 解决更新情况下:updateTime 不更新
|
||||
studentTeacherMapper.insertOrUpdate(studentTeacher);
|
||||
}
|
||||
|
||||
|
|
@ -144,4 +183,8 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
studentTeacherMapper.deleteByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void deleteStudentTeacherByStudentIds(List<Long> studentIds) {
|
||||
studentTeacherMapper.deleteByStudentIds(studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.service.demo;
|
|||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
|||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
|
@ -25,4 +25,8 @@ public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeach
|
|||
return delete(InfraStudentTeacherDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,55 +1,54 @@
|
|||
-- 菜单 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, component_name
|
||||
)
|
||||
VALUES (
|
||||
'学生管理', '', 2, 0, 888,
|
||||
'student', '', 'infra/demo/index', 0, 'InfraStudent'
|
||||
);
|
||||
|
||||
-- 按钮父菜单ID
|
||||
-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
|
||||
SELECT @parentId := LAST_INSERT_ID();
|
||||
|
||||
-- 按钮 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生查询', 'infra:student:query', 3, 1, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生创建', 'infra:student:create', 3, 2, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生更新', 'infra:student:update', 3, 3, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生删除', 'infra:student:delete', 3, 4, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生导出', 'infra:student:export', 3, 5, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
-- 菜单 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, component_name
|
||||
)
|
||||
VALUES (
|
||||
'学生管理', '', 2, 0, 888,
|
||||
'student', '', 'infra/demo/index', 0, 'InfraStudent'
|
||||
);
|
||||
|
||||
-- 按钮父菜单ID
|
||||
SELECT @parentId := LAST_INSERT_ID();
|
||||
|
||||
-- 按钮 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生查询', 'infra:student:query', 3, 1, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生创建', 'infra:student:create', 3, 2, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生更新', 'infra:student:update', 3, 3, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生删除', 'infra:student:delete', 3, 4, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生导出', 'infra:student:export', 3, 5, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace StudentApi {
|
||||
/** 学生联系人信息 */
|
||||
export interface StudentContact {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
|
||||
/** 学生班主任信息 */
|
||||
export interface StudentTeacher {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
|
||||
/** 学生信息 */
|
||||
export interface Student {
|
||||
id: number; // 编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video?: string; // 附件
|
||||
memo?: string; // 备注
|
||||
studentcontacts?: StudentContact[]
|
||||
studentteacher?: StudentTeacher
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询学生分页 */
|
||||
export function getStudentPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<StudentApi.Student>>('/infra/student/page', { params });
|
||||
}
|
||||
|
||||
/** 查询学生详情 */
|
||||
export function getStudent(id: number) {
|
||||
return requestClient.get<StudentApi.Student>(`/infra/student/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增学生 */
|
||||
export function createStudent(data: StudentApi.Student) {
|
||||
return requestClient.post('/infra/student/create', data);
|
||||
}
|
||||
|
||||
/** 修改学生 */
|
||||
export function updateStudent(data: StudentApi.Student) {
|
||||
return requestClient.put('/infra/student/update', data);
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
export function deleteStudent(id: number) {
|
||||
return requestClient.delete(`/infra/student/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
export function deleteStudentList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/student/delete-list?ids=${ids.join(',')}`)
|
||||
}
|
||||
|
||||
/** 导出学生 */
|
||||
export function exportStudent(params: any) {
|
||||
return requestClient.download('/infra/student/export-excel', { params });
|
||||
}
|
||||
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
/** 获得学生联系人列表 */
|
||||
export function getStudentContactListByStudentId(studentId: number) {
|
||||
return requestClient.get<StudentApi.StudentContact[]>(`/infra/student/student-contact/list-by-student-id?studentId=${studentId}`);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
/** 获得学生班主任 */
|
||||
export function getStudentTeacherByStudentId(studentId: number) {
|
||||
return requestClient.get<StudentApi.StudentTeacher>(`/infra/student/student-teacher/get-by-student-id?studentId=${studentId}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
import StudentContactForm from './student-contact-form.vue'
|
||||
import StudentTeacherForm from './student-teacher-form.vue'
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { getStudent, createStudent, updateStudent } from '#/api/infra/demo';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.Student>>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
video: [{ required: true, message: '附件不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生'])
|
||||
: $t('ui.actionTitle.create', ['学生']);
|
||||
});
|
||||
|
||||
/** 子表的表单 */
|
||||
const subTabsName = ref('studentContact')
|
||||
const studentContactFormRef = ref<InstanceType<typeof StudentContactForm>>()
|
||||
const studentTeacherFormRef = ref<InstanceType<typeof StudentTeacherForm>>()
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm() {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
await formRef.value?.validate();
|
||||
// 校验子表单
|
||||
try {
|
||||
await studentTeacherFormRef.value?.validate()
|
||||
} catch (e) {
|
||||
subTabsName.value = 'studentTeacher'
|
||||
return
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = formData.value as StudentApi.Student;
|
||||
// 拼接子表的数据
|
||||
data.studentContacts = studentContactFormRef.value?.getData();
|
||||
data.studentTeacher = studentTeacherFormRef.value?.getValues();
|
||||
try {
|
||||
await (formData.value?.id ? updateStudent(data) : createStudent(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data = modalApi.getData<StudentApi.Student>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getStudent(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
formData.value = data;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<!-- 子表的表单 -->
|
||||
<Tabs v-model:active-key="subTabsName">
|
||||
<Tabs.TabPane key="studentContact" tab="学生联系人" force-render>
|
||||
<StudentContactForm ref="studentContactFormRef" :student-id="formData?.id" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="studentTeacher" tab="学生班主任" force-render>
|
||||
<StudentTeacherForm ref="studentTeacherFormRef" :student-id="formData?.id" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,351 @@
|
|||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { ref, h, reactive, onMounted, nextTick } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
|
||||
import { cloneDeep, downloadFileFromBlobPart, formatDateTime, isEmpty } from '@vben/utils';
|
||||
import { Button, Card, message, Tabs, Pagination, Form, RangePicker, DatePicker, Select, Input } from 'ant-design-vue';
|
||||
import StudentForm from './modules/form.vue';
|
||||
import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
|
||||
import StudentContactList from './modules/student-contact-list.vue'
|
||||
import StudentTeacherList from './modules/student-teacher-list.vue'
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/demo';
|
||||
|
||||
/** 子表的列表 */
|
||||
const subTabsName = ref('studentContact')
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.Student[]>([]) // 列表的数据
|
||||
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
birthday: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
createTime: undefined,
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = cloneDeep(queryParams) as any;
|
||||
if (params.birthday && Array.isArray(params.birthday)) {
|
||||
params.birthday = (params.birthday as string[]).join(',');
|
||||
}
|
||||
if (params.createTime && Array.isArray(params.createTime)) {
|
||||
params.createTime = (params.createTime as string[]).join(',');
|
||||
}
|
||||
const data = await getStudentPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: StudentForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑学生 */
|
||||
function handleEdit(row: StudentApi.Student) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
|
||||
/** 删除学生 */
|
||||
async function handleDelete(row: StudentApi.Student) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudent(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
async function handleDeleteBatch() {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: StudentApi.Student[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
try {
|
||||
exportLoading.value = true;
|
||||
const data = await exportStudent(queryParams);
|
||||
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
|
||||
}finally {
|
||||
exportLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 初始化 */
|
||||
const { hiddenSearchBar, tableToolbarRef, tableRef } = useTableToolbar();
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="getList" />
|
||||
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="请输入名字"
|
||||
allowClear
|
||||
@pressEnter="handleQuery"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="queryParams.birthday"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="选择出生日期"
|
||||
allowClear
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select
|
||||
v-model:value="queryParams.sex"
|
||||
placeholder="请选择性别"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<Select
|
||||
v-model:value="queryParams.enabled"
|
||||
placeholder="请选择是否有效"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="创建时间" name="createTime">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.createTime"
|
||||
v-bind="getRangePickerDefaultProps()"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||
搜索
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<!-- 列表 -->
|
||||
<Card title="学生">
|
||||
<template #extra>
|
||||
<VbenVxeTableToolbar
|
||||
ref="tableToolbarRef"
|
||||
v-model:hidden-search="hiddenSearchBar"
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
:icon="h(Plus)"
|
||||
type="primary"
|
||||
@click="handleCreate"
|
||||
v-access:code="['infra:student:create']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Download)"
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
:loading="exportLoading"
|
||||
@click="handleExport"
|
||||
v-access:code="['infra:student:export']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Trash2)"
|
||||
type="primary"
|
||||
danger
|
||||
class="ml-2"
|
||||
:disabled="isEmpty(checkedIds)"
|
||||
@click="handleDeleteBatch"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</VbenVxeTableToolbar>
|
||||
</template>
|
||||
<VxeTable
|
||||
ref="tableRef"
|
||||
:data="list"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
@checkboxAll="handleRowCheckboxChange"
|
||||
@checkboxChange="handleRowCheckboxChange"
|
||||
>
|
||||
<VxeColumn type="checkbox" width="40" />
|
||||
<!-- 子表的列表 -->
|
||||
<VxeColumn type="expand" width="60">
|
||||
<template #content="{ row }">
|
||||
<!-- 子表的表单 -->
|
||||
<Tabs v-model:active-key="subTabsName" class="mx-8">
|
||||
<Tabs.TabPane key="studentContact" tab="学生联系人" force-render>
|
||||
<StudentContactList :student-id="row?.id" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="studentTeacher" tab="学生班主任" force-render>
|
||||
<StudentTeacherList :student-id="row?.id" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{row}">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleEdit(row)"
|
||||
v-access:code="['infra:student:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
class="ml-2"
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref, h, onMounted, watch, nextTick } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { message, Tabs, Form, Input, Textarea, Button, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker } from 'ant-design-vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
import { Plus } from "@vben/icons";
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { getStudentContactListByStudentId } from '#/api/infra/demo';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
const list = ref<StudentApi.StudentContact[]>([]) // 列表的数据
|
||||
const tableRef = ref<VxeTableInstance>();
|
||||
|
||||
/** 添加学生联系人 */
|
||||
async function handleAdd() {
|
||||
await tableRef.value?.insertAt({} as StudentApi.StudentContact, -1);
|
||||
}
|
||||
|
||||
/** 删除学生联系人 */
|
||||
async function handleDelete(row: StudentApi.StudentContact) {
|
||||
await tableRef.value?.remove(row);
|
||||
}
|
||||
|
||||
/** 提供获取表格数据的方法供父组件调用 */
|
||||
defineExpose({
|
||||
getData: (): StudentApi.StudentContact[] => {
|
||||
const data = list.value as StudentApi.StudentContact[];
|
||||
const removeRecords = tableRef.value?.getRemoveRecords() as StudentApi.StudentContact[];
|
||||
const insertRecords = tableRef.value?.getInsertRecords() as StudentApi.StudentContact[];
|
||||
return [
|
||||
...data.filter(
|
||||
(row) => !removeRecords.some((removed) => removed.id === row.id),
|
||||
),
|
||||
...insertRecords.map((row: any) => ({ ...row, id: undefined })),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
list.value = await getStudentContactListByStudentId(props.studentId!);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VxeTable ref="tableRef" :data="list" show-overflow class="mx-4">
|
||||
<VxeColumn field="name" title="名字" align="center">
|
||||
<template #default="{ row }">
|
||||
<Input v-model:value="row.name" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="description" title="简介" align="center">
|
||||
<template #default="{ row }">
|
||||
<Textarea v-model:value="row.description" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{ row }">
|
||||
<DatePicker
|
||||
v-model:value="row.birthday"
|
||||
:showTime="true"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
valueFormat='x'
|
||||
/>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{ row }">
|
||||
<Select v-model:value="row.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{ row }">
|
||||
<RadioGroup v-model:value="row.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center">
|
||||
<template #default="{ row }">
|
||||
<ImageUpload v-model:value="row.avatar" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="video" title="附件" align="center">
|
||||
<template #default="{ row }">
|
||||
<FileUpload v-model:value="row.video" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="memo" title="备注" align="center">
|
||||
<template #default="{ row }">
|
||||
<Textarea v-model:value="row.memo" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{ row }">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<div class="flex justify-center mt-4">
|
||||
<Button :icon="h(Plus)" type="primary" ghost @click="handleAdd" v-access:code="['infra:student:create']">
|
||||
{{ $t('ui.actionTitle.create', ['学生联系人']) }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
|
||||
import { reactive, ref, h, nextTick, watch, onMounted } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
|
||||
import { getStudentContactListByStudentId } from '#/api/infra/demo';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.StudentContact[]>([]) // 列表的数据
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!props.studentId){
|
||||
return []
|
||||
}
|
||||
list.value = await getStudentContactListByStudentId(props.studentId!);
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await getList()
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card title="学生联系人列表">
|
||||
<VxeTable
|
||||
:data="list"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
>
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref, h, onMounted, watch, nextTick } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { message, Tabs, Form, Input, Textarea, Button, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker } from 'ant-design-vue';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { getStudentTeacherByStudentId } from '#/api/infra/demo';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.StudentTeacher>>({
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
/** 暴露出表单校验方法和表单值获取方法 */
|
||||
defineExpose({
|
||||
validate: async () => await formRef.value?.validate(),
|
||||
getValues: ()=> formData.value,
|
||||
});
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
formData.value = await getStudentTeacherByStudentId(props.studentId!);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
ref="formRef"
|
||||
class="mx-4"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
|
||||
import { reactive, ref, h, nextTick, watch, onMounted } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
|
||||
import { getStudentTeacherByStudentId } from '#/api/infra/demo';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.StudentTeacher[]>([]) // 列表的数据
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!props.studentId){
|
||||
return []
|
||||
}
|
||||
list.value = [await getStudentTeacherByStudentId(props.studentId!)];
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await getList()
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card title="学生班主任列表">
|
||||
<VxeTable
|
||||
:data="list"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
>
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
</Card>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
[ {
|
||||
"contentPath" : "java/InfraStudentPageReqVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentRespVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentSaveReqVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentController",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
|
||||
}, {
|
||||
"contentPath" : "xml/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/resources/mapper/demo/InfraStudentMapper.xml"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImpl",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentService",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImplTest",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
|
||||
}, {
|
||||
"contentPath" : "java/ErrorCodeConstants_手动操作",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
|
||||
}, {
|
||||
"contentPath" : "sql/sql",
|
||||
"filePath" : "sql/sql.sql"
|
||||
}, {
|
||||
"contentPath" : "sql/h2",
|
||||
"filePath" : "sql/h2.sql"
|
||||
}, {
|
||||
"contentPath" : "vue/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/index.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/form.vue"
|
||||
}, {
|
||||
"contentPath" : "ts/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/api/infra/demo/index.ts"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-form.vue"
|
||||
} ]
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
|
|
@ -25,4 +25,8 @@ public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentConta
|
|||
return delete(InfraStudentContactDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
package cn.iocoder.yudao.module.infra.controller.admin.demo;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import javax.validation.constraints.*;
|
||||
import javax.validation.*;
|
||||
import javax.servlet.http.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import jakarta.validation.*;
|
||||
import jakarta.servlet.http.*;
|
||||
import java.util.*;
|
||||
import java.io.IOException;
|
||||
|
||||
|
|
@ -22,8 +22,8 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
|||
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
|
||||
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
|
||||
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
|
|
@ -64,6 +64,15 @@ public class InfraStudentController {
|
|||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得学生")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
|
|
@ -84,7 +93,7 @@ public class InfraStudentController {
|
|||
@GetMapping("/export-excel")
|
||||
@Operation(summary = "导出学生 Excel")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:export')")
|
||||
@OperateLog(type = EXPORT)
|
||||
@ApiAccessLog(operateType = EXPORT)
|
||||
public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
|
|
@ -64,4 +64,5 @@ public class InfraStudentDO extends BaseDO {
|
|||
*/
|
||||
private String memo;
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -11,8 +11,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
|
|||
|
||||
@Schema(description = "管理后台 - 学生分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class InfraStudentPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "名字", example = "芋头")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue