【同步】BOOT 和 CLOUD 的功能(infra)

pull/248/MERGE
YunaiV 2026-05-03 22:59:43 +08:00
parent 8c7087ca2a
commit 6b91b4169d
861 changed files with 55465 additions and 3521 deletions

View File

@ -55,4 +55,10 @@ public class CodegenProperties {
@NotNull(message = "是否生成单元测试不能为空")
private Boolean unitTestEnable;
/**
* Excel
*/
@NotNull(message = "是否生成 Excel 导入接口不能为空")
private Boolean importEnable;
}

View File

@ -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

View File

@ -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('\'', '');
}
}

View File

@ -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");
}
}

View File

@ -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

View File

@ -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 前缀

View File

@ -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:

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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;
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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

View File

@ -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};

View File

@ -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)

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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)

View File

@ -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>

View File

@ -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"

View File

@ -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 [

View File

@ -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>

View File

@ -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>

View File

@ -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)

View File

@ -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>

View File

@ -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"

View File

@ -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)

View File

@ -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 [

View File

@ -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>

View File

@ -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>

View File

@ -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("\"混'搭\"'"));
}
}

View File

@ -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 + ":不匹配");
});
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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() {
// 准备参数

View File

@ -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() {
// 准备参数

View File

@ -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"));
}
}

View File

@ -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"
} ]

View File

@ -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 补充编号, "学生班主任已存在");

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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 = "芋头")

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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);
/**
* 获得学生班主任
*

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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
);

View File

@ -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}`);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"
} ]

View File

@ -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 补充编号, "学生不存在");

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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 = "芋头")

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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);
/**
* 获得学生
*

View File

@ -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)); // 解决更新情况下1id 冲突2updateTime 不更新
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);
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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
);

View File

@ -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}`);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"
} ]

View File

@ -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 补充编号, "学生不存在");

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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