Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts: # yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/remote/IotDeviceApiImpl.javapull/250/head
commit
116b6766b3
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -62,6 +62,10 @@ def load_and_clean(sql_file: str) -> str:
|
|||
content = open(sql_file, encoding="utf-8").read()
|
||||
for replace_pair in REPLACE_PAIR_LIST:
|
||||
content = content.replace(*replace_pair)
|
||||
# 移除所有 CHARACTER SET / COLLATE 变体 (utf8mb3、utf8 等)
|
||||
content = re.sub(r" CHARACTER SET \w+ COLLATE \w+", "", content)
|
||||
content = re.sub(r" CHARACTER SET \w+", "", content)
|
||||
content = re.sub(r" COLLATE \w+", "", content)
|
||||
# 移除索引字段的前缀长度定义,例如: `name`(32) -> `name`
|
||||
# 移除索引定义上的 USING BTREE COMMENT 部分
|
||||
# 相关 issue:https://t.zsxq.com/96IFc 、https://t.zsxq.com/rC3A3
|
||||
|
|
@ -77,7 +81,11 @@ class Convertor(ABC):
|
|||
self.src = src
|
||||
self.db_type = db_type
|
||||
self.content = load_and_clean(self.src)
|
||||
self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", self.content)
|
||||
# original_content 保留原始 COMMENT 信息,用于注释提取
|
||||
self.original_content = open(src, encoding="utf-8").read()
|
||||
# 剥离列级 COMMENT 以避免 COMMENT 值内的分号截断 CREATE TABLE 正则
|
||||
content_no_comment = re.sub(r" COMMENT '(?:[^'\\]|\\.)*'", "", self.content)
|
||||
self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", content_no_comment)
|
||||
|
||||
@abstractmethod
|
||||
def translate_type(self, type: str, size: Optional[Union[int, Tuple[int]]]) -> str:
|
||||
|
|
@ -182,7 +190,8 @@ class Convertor(ABC):
|
|||
head = head.strip().replace("`", "").lower()
|
||||
tail = tail.strip().replace(r"\"", '"')
|
||||
# tail = tail.replace("b'0'", "'0'").replace("b'1'", "'1'")
|
||||
yield f"INSERT INTO {table_name.lower()} {head} VALUES {tail}"
|
||||
col_part = f" {head}" if head else ""
|
||||
yield f"INSERT INTO {table_name.lower()}{col_part} VALUES {tail}"
|
||||
|
||||
@staticmethod
|
||||
def index(ddl: Dict) -> Generator:
|
||||
|
|
@ -227,7 +236,8 @@ class Convertor(ABC):
|
|||
yield field, comment_string
|
||||
|
||||
def table_comment(self, table_sql: str) -> str:
|
||||
match = re.search(r"COMMENT \='([^']+)';", table_sql)
|
||||
# 兼容 COMMENT='xxx' / COMMENT = 'xxx' 等等号两侧空格变体;允许 COMMENT 内含转义单引号
|
||||
match = re.search(r"COMMENT\s*=\s*'((?:[^'\\]|\\.)*)'", table_sql)
|
||||
return match.group(1) if match else None
|
||||
|
||||
def print(self):
|
||||
|
|
@ -251,7 +261,9 @@ class Convertor(ABC):
|
|||
|
||||
error_scripts = []
|
||||
for table_sql in self.table_script_list:
|
||||
ddl = DDLParser(table_sql.replace("`", "")).run()
|
||||
# 剥离 COMMENT 子句避免 DDLParser 无法处理中文等特殊字符
|
||||
table_sql_for_parse = re.sub(r"\s+COMMENT\s+'[^']*'", "", table_sql)
|
||||
ddl = DDLParser(table_sql_for_parse.replace("`", "")).run()
|
||||
|
||||
# 如果parse失败, 需要跟进
|
||||
if len(ddl) == 0:
|
||||
|
|
@ -266,17 +278,23 @@ class Convertor(ABC):
|
|||
continue
|
||||
|
||||
# 解析注释
|
||||
# 从原始 SQL 提取注释(支持中文等 DDLParser 不能处理的内容)
|
||||
# 按表名定位完整建表段(以「换行 + )」起始的 ENGINE 行作为终止),避免被列 COMMENT 内的分号截断
|
||||
orig_match = re.search(
|
||||
rf"CREATE TABLE\s+`{re.escape(table_name)}`[\s\S]*?\n\)[^;]*;",
|
||||
self.original_content,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
orig_table_sql = orig_match.group() if orig_match else table_sql
|
||||
comments_dict = dict(Convertor.filed_comments(orig_table_sql))
|
||||
for column in table_ddl["columns"]:
|
||||
column["comment"] = bytes(column["comment"], "utf-8").decode(
|
||||
r"unicode_escape"
|
||||
)[1:-1]
|
||||
table_ddl["comment"] = bytes(table_ddl["comment"], "utf-8").decode(
|
||||
r"unicode_escape"
|
||||
)[1:-1]
|
||||
column["comment"] = comments_dict.get(column["name"], "")
|
||||
table_ddl["comment"] = self.table_comment(orig_table_sql) or ""
|
||||
|
||||
# 为每个表生成个6个基本部分
|
||||
create = self.gen_create(table_ddl)
|
||||
pk = self.gen_pk(table_name)
|
||||
has_id = any(col["name"].lower() == "id" for col in table_ddl["columns"])
|
||||
pk = self.gen_pk(table_name) if has_id else ""
|
||||
uk = self.gen_uk(table_ddl)
|
||||
index = self.gen_index(table_ddl)
|
||||
comment = self.gen_comment(table_ddl)
|
||||
|
|
@ -320,25 +338,31 @@ class PostgreSQLConvertor(Convertor):
|
|||
|
||||
if type == "varchar":
|
||||
return f"varchar({size})"
|
||||
if type in ("int", "int unsigned"):
|
||||
if type in ("int", "int unsigned", "int unsigned zerofill"):
|
||||
return "int4"
|
||||
if type in ("bigint", "bigint unsigned"):
|
||||
return "int8"
|
||||
if type == "datetime":
|
||||
if type in ("tinyint", "smallint", "tinyint unsigned"):
|
||||
return "int2"
|
||||
if type in ("datetime", "timestamp null"):
|
||||
return "timestamp"
|
||||
if type == "date":
|
||||
return "date"
|
||||
if type == "json":
|
||||
return "jsonb"
|
||||
if type == "double":
|
||||
return "double precision"
|
||||
if type == "timestamp":
|
||||
return f"timestamp({size})"
|
||||
return f"timestamp({size})" if size else "timestamp"
|
||||
if type == "bit":
|
||||
return "bool"
|
||||
if type in ("tinyint", "smallint"):
|
||||
return "int2"
|
||||
if type in ("text", "longtext"):
|
||||
return "text"
|
||||
if type in ("blob", "mediumblob"):
|
||||
if type in ("blob", "mediumblob", "longblob"):
|
||||
return "bytea"
|
||||
if type == "decimal":
|
||||
return (
|
||||
f"numeric({','.join(str(s) for s in size)})" if len(size) else "numeric"
|
||||
f"numeric({','.join(str(s) for s in size)})" if size and len(size) else "numeric"
|
||||
)
|
||||
|
||||
def gen_create(self, ddl: Dict) -> str:
|
||||
|
|
@ -351,6 +375,10 @@ class PostgreSQLConvertor(Convertor):
|
|||
|
||||
type = col["type"].lower()
|
||||
full_type = self.translate_type(type, col["size"])
|
||||
if full_type is None:
|
||||
raise NotImplementedError(
|
||||
f"未支持的类型: '{col['type']}' (列: {name}, 表: {ddl['table_name']})"
|
||||
)
|
||||
nullable = "NULL" if col["nullable"] else "NOT NULL"
|
||||
default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
|
||||
return f"{name} {full_type} {nullable} {default}"
|
||||
|
|
@ -407,6 +435,8 @@ CREATE TABLE {table_name} (
|
|||
"""生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence"""
|
||||
|
||||
inserts = list(Convertor.inserts(table_name, self.content))
|
||||
# 转换 MySQL 字符串转义为 PostgreSQL 格式:\\ -> \,\' -> ''
|
||||
inserts = [re.sub(r"\\\\|\\'", lambda m: "\\" if m.group() == "\\\\" else "''", s) for s in inserts]
|
||||
## 生成 insert 脚本
|
||||
script = ""
|
||||
last_id = 0
|
||||
|
|
|
|||
|
|
@ -173,6 +173,24 @@ public class JsonUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON 字符串成指定类型的对象,如果解析失败,则返回 null
|
||||
*
|
||||
* @param text 字符串
|
||||
* @param clazz 类型
|
||||
* @return 指定类型的对象
|
||||
*/
|
||||
public static <T> T parseObjectQuietly(String text, Class<T> clazz) {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(text, clazz);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> List<T> parseArray(String text, Class<T> clazz) {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return new ArrayList<>();
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,7 +5,12 @@ import cn.iocoder.yudao.framework.ip.core.Area;
|
|||
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* {@link AreaUtils} 的单元测试
|
||||
|
|
@ -31,6 +36,46 @@ public class AreaUtilsTest {
|
|||
assertEquals(AreaUtils.format(110105), "北京市 北京市 朝阳区");
|
||||
assertEquals(AreaUtils.format(1), "中国");
|
||||
assertEquals(AreaUtils.format(2), "蒙古");
|
||||
// 中国台湾省:省/市/区三级
|
||||
assertEquals(AreaUtils.format(710101), "台湾省 台北市 中正区");
|
||||
// 自定义分隔符
|
||||
assertEquals(AreaUtils.format(110105, "/"), "北京市/北京市/朝阳区");
|
||||
// 不存在的编号
|
||||
assertNull(AreaUtils.format(-1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseArea() {
|
||||
// 调用:通过路径解析得到地区
|
||||
Area area = AreaUtils.parseArea("北京市/北京市/朝阳区");
|
||||
// 断言
|
||||
assertNotNull(area);
|
||||
assertEquals(area.getId(), 110105);
|
||||
// 路径不存在时返回 null
|
||||
assertNull(AreaUtils.parseArea("不存在/路径"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetParentIdByType() {
|
||||
// 调用:朝阳区向上找省
|
||||
Integer provinceId = AreaUtils.getParentIdByType(110105, AreaTypeEnum.PROVINCE);
|
||||
// 断言
|
||||
assertEquals(provinceId, 110000);
|
||||
// 自身就是目标类型
|
||||
assertEquals(AreaUtils.getParentIdByType(110000, AreaTypeEnum.PROVINCE), 110000);
|
||||
// 不存在的编号返回 null
|
||||
assertNull(AreaUtils.getParentIdByType(-1, AreaTypeEnum.PROVINCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetByType() {
|
||||
// 调用:获取所有省份
|
||||
List<Area> provinces = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area);
|
||||
// 断言:包含北京、台湾、香港、澳门
|
||||
assertTrue(provinces.stream().anyMatch(area -> "北京市".equals(area.getName())));
|
||||
assertTrue(provinces.stream().anyMatch(area -> "台湾省".equals(area.getName())));
|
||||
assertTrue(provinces.stream().anyMatch(area -> "香港特别行政区".equals(area.getName())));
|
||||
assertTrue(provinces.stream().anyMatch(area -> "澳门特别行政区".equals(area.getName())));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package cn.iocoder.yudao.module.bpm.enums.definition;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* BPM 条件表达式操作符枚举
|
||||
*
|
||||
* @author Lesan
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public enum BpmConditionOpCodeEnum {
|
||||
|
||||
EQ("==", "等于", " var:getOrDefault(%s, null) == %s "),
|
||||
NE("!=", "不等于", " var:getOrDefault(%s, null) != %s "),
|
||||
GT(">", "大于", " var:getOrDefault(%s, null) > %s "),
|
||||
GE(">=", "大于等于", " var:getOrDefault(%s, null) >= %s "),
|
||||
LT("<", "小于", " var:getOrDefault(%s, null) < %s "),
|
||||
LE("<=", "小于等于", " var:getOrDefault(%s, null) <= %s "),
|
||||
|
||||
CONTAINS("contain", "包含", " var:contains(%s, %s) "),
|
||||
NOT_CONTAINS("!contain", "不包含", " !var:contains(%s, %s) ");
|
||||
|
||||
private final String code;
|
||||
private final String des;
|
||||
private final String symbol;
|
||||
|
||||
public static BpmConditionOpCodeEnum fromCode(String code) {
|
||||
for (BpmConditionOpCodeEnum op : BpmConditionOpCodeEnum.values()) {
|
||||
if (op.code.equalsIgnoreCase(code)) {
|
||||
return op;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未知操作符: " + code);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form;
|
|||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 流程表单字段 VO
|
||||
*/
|
||||
|
|
@ -20,5 +22,9 @@ public class BpmFormFieldVO {
|
|||
* 字段标题
|
||||
*/
|
||||
private String title;
|
||||
/**
|
||||
* 子表单字段(处理布局组件)
|
||||
*/
|
||||
private List<BpmFormFieldVO> children;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,15 +66,15 @@ public class BpmProcessInstanceCopyController {
|
|||
Map<String, HistoricProcessInstance> processInstanceMap = processInstanceService.getHistoricProcessInstanceMap(
|
||||
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId));
|
||||
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(),
|
||||
copy -> Stream.of(copy.getStartUserId(), Long.parseLong(copy.getCreator()))));
|
||||
copy -> Stream.of(copy.getStartUserId(), copy.getUserId())));
|
||||
Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap(
|
||||
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessDefinitionId));
|
||||
return success(convertPage(pageResult, copy -> {
|
||||
BpmProcessInstanceCopyRespVO copyVO = BeanUtils.toBean(copy, BpmProcessInstanceCopyRespVO.class);
|
||||
MapUtils.findAndThen(userMap, Long.valueOf(copy.getCreator()),
|
||||
user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
|
||||
MapUtils.findAndThen(userMap, copy.getStartUserId(),
|
||||
MapUtils.findAndThen(userMap, copy.getUserId(),
|
||||
user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
|
||||
MapUtils.findAndThen(userMap, copy.getStartUserId(),
|
||||
user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
|
||||
MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(),
|
||||
processInstance -> {
|
||||
copyVO.setSummary(FlowableUtils.getSummary(
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public class BpmOALeaveDO extends BaseDO {
|
|||
/**
|
||||
* 请假类型
|
||||
*/
|
||||
private String type;
|
||||
private Integer type;
|
||||
/**
|
||||
* 原因
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import org.springframework.stereotype.Component;
|
|||
/**
|
||||
* 根据流程变量 variable 的类型,转换参数的值
|
||||
*
|
||||
* 目前用于 ConditionNodeConvert 的 buildConditionExpression 方法中
|
||||
*
|
||||
* @deprecated 已无调用方
|
||||
* @author jason
|
||||
*/
|
||||
@Deprecated // TODO @芋艿:兼容老版本,预计 27 年删除;
|
||||
@Component
|
||||
public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVariableExpressionFunction {
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
|
|||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
|
|
@ -247,9 +248,7 @@ public class FlowableUtils {
|
|||
Map<String, BpmFormFieldVO> formFieldsMap = new HashMap<>();
|
||||
processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
|
||||
BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class);
|
||||
if (formField != null) {
|
||||
formFieldsMap.put(formField.getField(), formField);
|
||||
}
|
||||
parseFormField(formField, formFieldsMap);
|
||||
});
|
||||
|
||||
// 情况一:当自定义了摘要
|
||||
|
|
@ -273,6 +272,26 @@ public class FlowableUtils {
|
|||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归解析表单字段
|
||||
*/
|
||||
private static void parseFormField(BpmFormFieldVO formField, Map<String, BpmFormFieldVO> formFieldsMap) {
|
||||
if (formField == null) {
|
||||
return;
|
||||
}
|
||||
// 如果存在 children -> 说明是布局组件
|
||||
if (formField.getChildren() != null && !formField.getChildren().isEmpty()) {
|
||||
for (BpmFormFieldVO child : formField.getChildren()) {
|
||||
parseFormField(child, formFieldsMap);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 真实字段才加入 map
|
||||
if (StrUtil.isNotBlank(formField.getField())) {
|
||||
formFieldsMap.put(formField.getField(), formField);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Task 相关的工具方法 ==========
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -707,10 +707,9 @@ public class SimpleModelUtils {
|
|||
List<String> list = convertList(item.getRules(), (rule) -> {
|
||||
String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide()
|
||||
: "\"" + rule.getRightSide() + "\""; // 如果非数值类型加引号
|
||||
return String.format(" vars:getOrDefault(%s, null) %s var:convertByType(%s,%s) ",
|
||||
return String.format(BpmConditionOpCodeEnum.fromCode(rule.getOpCode()).getSymbol(),
|
||||
rule.getLeftSide(), // 左侧:读取变量
|
||||
rule.getOpCode(), // 中间:操作符,比较
|
||||
rule.getLeftSide(), rightSide); // 右侧:转换变量,VariableConvertByTypeExpressionFunction
|
||||
rightSide); // 右侧:取值变量
|
||||
});
|
||||
// 构造条件组的表达式
|
||||
Boolean and = item.getAnd();
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
package cn.iocoder.yudao.module.bpm.service.definition.dto;
|
||||
|
||||
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* BPM 流程 MetaInfo Response DTO
|
||||
* 主要用于 { Model#setMetaInfo(String)} 的存储
|
||||
*
|
||||
* 最终,它的字段和 {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO} 是一致的
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class BpmModelMetaInfoRespDTO {
|
||||
|
||||
/**
|
||||
* 流程图标
|
||||
*/
|
||||
private String icon;
|
||||
/**
|
||||
* 流程描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 表单类型
|
||||
*/
|
||||
private Integer formType;
|
||||
/**
|
||||
* 表单编号
|
||||
* 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时
|
||||
*/
|
||||
private Long formId;
|
||||
/**
|
||||
* 自定义表单的提交路径,使用 Vue 的路由地址
|
||||
* 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时
|
||||
*/
|
||||
private String formCustomCreatePath;
|
||||
/**
|
||||
* 自定义表单的查看路径,使用 Vue 的路由地址
|
||||
* 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时
|
||||
*/
|
||||
private String formCustomViewPath;
|
||||
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -55,4 +55,10 @@ public class CodegenProperties {
|
|||
@NotNull(message = "是否生成单元测试不能为空")
|
||||
private Boolean unitTestEnable;
|
||||
|
||||
/**
|
||||
* 是否生成 Excel 导入接口
|
||||
*/
|
||||
@NotNull(message = "是否生成 Excel 导入接口不能为空")
|
||||
private Boolean importEnable;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||
AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret()));
|
||||
URI endpoint = URI.create(buildEndpoint());
|
||||
URI presignerEndpoint = URI.create(buildPresignerEndpoint());
|
||||
S3Configuration serviceConfiguration = S3Configuration.builder() // Path-style 访问
|
||||
.pathStyleAccessEnabled(Boolean.TRUE.equals(config.getEnablePathStyleAccess()))
|
||||
.chunkedEncodingEnabled(false) // 禁用分块编码,参见 https://t.zsxq.com/kBy57
|
||||
|
|
@ -66,7 +67,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||
presigner = S3Presigner.builder()
|
||||
.credentialsProvider(credentialsProvider)
|
||||
.region(region)
|
||||
.endpointOverride(endpoint)
|
||||
.endpointOverride(presignerEndpoint)
|
||||
.serviceConfiguration(serviceConfiguration)
|
||||
.build();
|
||||
}
|
||||
|
|
@ -161,6 +162,23 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||
return StrUtil.format("https://{}", config.getEndpoint());
|
||||
}
|
||||
|
||||
/**
|
||||
* presigner 节点地址
|
||||
*
|
||||
* @return 节点地址
|
||||
*/
|
||||
private String buildPresignerEndpoint() {
|
||||
// 补全 domain
|
||||
if (StrUtil.isEmpty(config.getDomain())) {
|
||||
config.setDomain(buildDomain());
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(config.getEnablePathStyleAccess())) {
|
||||
return StrUtil.removeSuffix(config.getDomain(), StrUtil.format("/{}", config.getBucket()));
|
||||
}
|
||||
return StrUtil.replace(config.getDomain(), StrUtil.format("://{}.", config.getBucket()), "://");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 AWS 区域
|
||||
* 优先级:配置的 region > 从 endpoint 解析的 region > 默认值 us-east-1
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import org.springframework.context.annotation.Configuration;
|
|||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
|
@ -40,6 +41,9 @@ public class AdminServerConfiguration {
|
|||
@Value("${spring.boot.admin.client.password:admin}")
|
||||
private String password;
|
||||
|
||||
@Value("${spring.boot.admin.frame-ancestors:'self'}")
|
||||
private String frameAncestors;
|
||||
|
||||
/**
|
||||
* Spring Boot Admin 专用的 InMemoryUserDetailsManager
|
||||
* 使用内存存储,与系统用户隔离
|
||||
|
|
@ -100,6 +104,16 @@ public class AdminServerConfiguration {
|
|||
adminSeverContextPath + "/instances", // Admin Client 注册端点忽略 CSRF
|
||||
adminSeverContextPath + "/actuator/**" // Actuator 端点忽略 CSRF
|
||||
)
|
||||
)
|
||||
.headers(headers -> headers
|
||||
// 特殊:Spring Boot Admin 前端基于 Vue,需 unsafe-inline / unsafe-eval 支持内联脚本与表达式
|
||||
.contentSecurityPolicy(csp -> csp.policyDirectives(
|
||||
"default-src 'self'; "
|
||||
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
|
||||
+ "style-src 'self' 'unsafe-inline'; "
|
||||
+ "frame-ancestors " + frameAncestors))
|
||||
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin) // 显式设置 X-Frame-Options 为 SAMEORIGIN
|
||||
.cacheControl(HeadersConfigurer.CacheControlConfig::disable) // 禁用缓存,避免旧配置生效
|
||||
);
|
||||
return httpSecurity.build();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnListConditionEnu
|
|||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableField;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
|
@ -117,8 +118,8 @@ public class CodegenBuilder {
|
|||
table.setBusinessName(toCamelCase(subAfter(tableName, '_', false)).toLowerCase());
|
||||
// 驼峰 + 首字母大写;第一步,第一个 _ 前缀的后面,作为 class 名字;第二步,驼峰命名
|
||||
table.setClassName(upperFirst(toCamelCase(subAfter(tableName, '_', false))));
|
||||
// 去除结尾的表,作为类描述
|
||||
table.setClassComment(StrUtil.removeSuffixIgnoreCase(table.getTableComment(), "表"));
|
||||
// 去除结尾的表,作为类描述;注释中的英文引号替换为中文引号,避免破坏生成代码中的字符串字面量
|
||||
table.setClassComment(StrUtil.removeSuffixIgnoreCase(sanitizeComment(table.getTableComment()), "表"));
|
||||
table.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
}
|
||||
|
||||
|
|
@ -128,6 +129,7 @@ public class CodegenBuilder {
|
|||
for (CodegenColumnDO column : columns) {
|
||||
column.setTableId(tableId);
|
||||
column.setOrdinalPosition(index++);
|
||||
column.setColumnComment(sanitizeComment(column.getColumnComment()));
|
||||
// 特殊处理:Byte => Integer
|
||||
if (Byte.class.getSimpleName().equals(column.getJavaType())) {
|
||||
column.setJavaType(Integer.class.getSimpleName());
|
||||
|
|
@ -217,4 +219,18 @@ public class CodegenBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将注释中的英文引号替换为中文引号,避免破坏生成代码中的字符串字面量
|
||||
*
|
||||
* @param comment 原始注释
|
||||
* @return 清理后的注释
|
||||
*/
|
||||
@VisibleForTesting
|
||||
String sanitizeComment(String comment) {
|
||||
if (StrUtil.isEmpty(comment)) {
|
||||
return comment;
|
||||
}
|
||||
return comment.replace('"', '“').replace('\'', '‘');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ public class CodegenEngine {
|
|||
.put(javaTemplatePath("controller/vo/listReqVO"), javaModuleImplVOFilePath("ListReqVO"))
|
||||
.put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO"))
|
||||
.put(javaTemplatePath("controller/vo/saveReqVO"), javaModuleImplVOFilePath("SaveReqVO"))
|
||||
.put(javaTemplatePath("controller/vo/importExcelVO"), javaModuleImplVOFilePath("ImportExcelVO"))
|
||||
.put(javaTemplatePath("controller/vo/importRespVO"), javaModuleImplVOFilePath("ImportRespVO"))
|
||||
.put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath())
|
||||
.put(javaTemplatePath("dal/do"),
|
||||
javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO"))
|
||||
|
|
@ -126,6 +128,8 @@ public class CodegenEngine {
|
|||
vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/form.vue"),
|
||||
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/import.vue"),
|
||||
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}ImportForm.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -164,6 +168,8 @@ public class CodegenEngine {
|
|||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/form.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/import.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("api/api.ts"),
|
||||
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -181,6 +187,8 @@ public class CodegenEngine {
|
|||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/import.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"),
|
||||
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -200,6 +208,8 @@ public class CodegenEngine {
|
|||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/form.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/import.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("api/api.ts"),
|
||||
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -217,6 +227,8 @@ public class CodegenEngine {
|
|||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/form.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/import.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("api/api.ts"),
|
||||
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -284,6 +296,7 @@ public class CodegenEngine {
|
|||
globalBindingMap.put("jakartaPackage", jakartaEnable ? "jakarta" : "javax");
|
||||
globalBindingMap.put("voType", codegenProperties.getVoType());
|
||||
globalBindingMap.put("deleteBatchEnable", codegenProperties.getDeleteBatchEnable());
|
||||
globalBindingMap.put("importEnable", codegenProperties.getImportEnable());
|
||||
// 全局 Java Bean
|
||||
globalBindingMap.put("CommonResultClassName", CommonResult.class.getName());
|
||||
globalBindingMap.put("PageResultClassName", PageResult.class.getName());
|
||||
|
|
@ -343,6 +356,11 @@ public class CodegenEngine {
|
|||
if (!CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
|
||||
return;
|
||||
}
|
||||
} else if (isImportTemplate(vmPath)) {
|
||||
// 关闭 import 时,跳过 ImportExcelVO / ImportRespVO 的生成
|
||||
if (!Boolean.TRUE.equals(codegenProperties.getImportEnable())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 2.3 默认生成
|
||||
generateCode(result, vmPath, filePath, bindingMap);
|
||||
|
|
@ -676,4 +694,9 @@ public class CodegenEngine {
|
|||
return path.contains("listReqVO");
|
||||
}
|
||||
|
||||
private static boolean isImportTemplate(String path) {
|
||||
return path.contains("importExcelVO") || path.contains("importRespVO")
|
||||
|| path.contains("views/import.vue");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.infra.service.file;
|
|||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
|
|
@ -93,7 +94,7 @@ public class FileConfigServiceImpl implements FileConfigService {
|
|||
fileConfigMapper.updateById(updateObj);
|
||||
|
||||
// 清空缓存
|
||||
clearCache(config.getId(), null);
|
||||
clearCache(config.getId(), config.getMaster());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -132,7 +133,7 @@ public class FileConfigServiceImpl implements FileConfigService {
|
|||
fileConfigMapper.deleteById(id);
|
||||
|
||||
// 清空缓存
|
||||
clearCache(id, null);
|
||||
clearCache(id, config.getMaster());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -149,7 +150,7 @@ public class FileConfigServiceImpl implements FileConfigService {
|
|||
fileConfigMapper.deleteByIds(ids);
|
||||
|
||||
// 清空缓存
|
||||
ids.forEach(id -> clearCache(id, null));
|
||||
ids.forEach(id -> clearCache(id, false));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -191,7 +192,7 @@ public class FileConfigServiceImpl implements FileConfigService {
|
|||
validateFileConfigExists(id);
|
||||
// 上传文件
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
return getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg");
|
||||
return getFileClient(id).upload(content, "public" + StrUtil.SLASH + IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.service.file;
|
|||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
|
|
@ -41,12 +42,19 @@ public class FileServiceImpl implements FileService {
|
|||
*/
|
||||
static boolean PATH_PREFIX_DATE_ENABLE = true;
|
||||
/**
|
||||
* 上传文件的后缀,是否包含时间戳
|
||||
* 上传文件的后缀,是否启用
|
||||
*
|
||||
* 目的:保证文件的唯一性,避免覆盖
|
||||
* 算法:当前时间戳(毫秒)+ 5 位随机数;目的是保证文件的唯一性,避免覆盖
|
||||
* 定制:可按需调整成 UUID、或者其他方式
|
||||
*/
|
||||
static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = false;
|
||||
/**
|
||||
* 后缀是否作为上级目录
|
||||
*
|
||||
* true:{@code yyyyMMdd/<后缀>/原文件名.ext};保留原文件名
|
||||
* false:{@code yyyyMMdd/原文件名_<后缀>.ext};后缀拼到文件名
|
||||
*/
|
||||
static boolean PATH_SUFFIX_AS_DIRECTORY = true;
|
||||
|
||||
@Resource
|
||||
private FileConfigService fileConfigService;
|
||||
|
|
@ -101,16 +109,21 @@ public class FileServiceImpl implements FileService {
|
|||
}
|
||||
String suffix = null;
|
||||
if (PATH_SUFFIX_TIMESTAMP_ENABLE) {
|
||||
suffix = String.valueOf(System.currentTimeMillis());
|
||||
// 5 位随机数,避免同一毫秒内的重复
|
||||
suffix = String.valueOf(System.currentTimeMillis()) + RandomUtil.randomInt(10000, 100000);
|
||||
}
|
||||
|
||||
// 2.1 先拼接 suffix 后缀
|
||||
if (StrUtil.isNotEmpty(suffix)) {
|
||||
String ext = FileUtil.extName(name);
|
||||
if (StrUtil.isNotEmpty(ext)) {
|
||||
name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
|
||||
if (PATH_SUFFIX_AS_DIRECTORY) {
|
||||
name = suffix + StrUtil.SLASH + name;
|
||||
} else {
|
||||
name = name + StrUtil.C_UNDERLINE + suffix;
|
||||
String ext = FileUtil.extName(name);
|
||||
if (StrUtil.isNotEmpty(ext)) {
|
||||
name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
|
||||
} else {
|
||||
name = name + StrUtil.C_UNDERLINE + suffix;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2.2 再拼接 prefix 前缀
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
@ -137,6 +137,8 @@ spring:
|
|||
password: admin
|
||||
# Spring Boot Admin Server 服务端的相关配置
|
||||
context-path: /admin # 配置 Spring
|
||||
# 允许嵌入 iframe 的域名(支持通配符),实际部署时,可以改为 "'self' [你的公网域名]"
|
||||
frame-ancestors: "'self' localhost localhost:48082 127.0.0.1 127.0.0.1:48082"
|
||||
|
||||
# 日志文件配置
|
||||
logging:
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ yudao:
|
|||
vo-type: 10 # VO 的类型,参见 CodegenVOTypeEnum 枚举类
|
||||
delete-batch-enable: true # 是否生成批量删除接口
|
||||
unit-test-enable: false # 是否生成单元测试
|
||||
import-enable: false # 是否生成 Excel 导入接口
|
||||
tenant: # 多租户相关配置项
|
||||
enable: true
|
||||
ignore-urls:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName};
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
#if ($importEnable)
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
#end
|
||||
import ${jakartaPackage}.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
#if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end
|
||||
|
|
@ -159,6 +162,29 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
|
|||
BeanUtils.toBean(list, ${table.className}RespVO.class));
|
||||
}
|
||||
#end
|
||||
#if ($importEnable)
|
||||
|
||||
@GetMapping("/get-import-template")
|
||||
@Operation(summary = "获得导入${table.classComment}模板")
|
||||
#if ($sceneEnum.scene == 1)
|
||||
@PreAuthorize("@ss.hasPermission('${permissionPrefix}:import')")
|
||||
#end
|
||||
public void importTemplate(HttpServletResponse response) throws IOException {
|
||||
ExcelUtils.write(response, "${table.classComment}导入模板.xls", "数据",
|
||||
${sceneEnum.prefixClass}${table.className}ImportExcelVO.class, Collections.emptyList());
|
||||
}
|
||||
|
||||
@PostMapping("/import")
|
||||
@Operation(summary = "导入${table.classComment}")
|
||||
@Parameter(name = "file", description = "Excel 文件", required = true)
|
||||
#if ($sceneEnum.scene == 1)
|
||||
@PreAuthorize("@ss.hasPermission('${permissionPrefix}:import')")
|
||||
#end
|
||||
public CommonResult<${sceneEnum.prefixClass}${table.className}ImportRespVO> importExcel(@RequestParam("file") MultipartFile file) throws Exception {
|
||||
List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> list = ExcelUtils.read(file, ${sceneEnum.prefixClass}${table.className}ImportExcelVO.class);
|
||||
return success(${classNameVar}Service.import${simpleClassName}List(list));
|
||||
}
|
||||
#end
|
||||
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
|
|
@ -268,4 +294,4 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
|
|||
|
||||
#end
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
#foreach ($column in $columns)
|
||||
#if (${column.createOperation} && "$!column.dictType" != "")
|
||||
import ${DictFormatClassName};
|
||||
import ${DictConvertClassName};
|
||||
#break
|
||||
#end
|
||||
#end
|
||||
|
||||
/**
|
||||
* ${table.classComment} Excel 导入 VO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ${sceneEnum.prefixClass}${table.className}ImportExcelVO {
|
||||
|
||||
## 逐个处理字段
|
||||
#foreach ($column in $columns)
|
||||
#if (${column.createOperation})
|
||||
#if ("$!column.dictType" != "")
|
||||
@ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class)
|
||||
@DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||
#else
|
||||
@ExcelProperty("${column.columnComment}")
|
||||
#end
|
||||
private ${column.javaType} ${column.javaField};
|
||||
|
||||
#end
|
||||
#end
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "${sceneEnum.name} - ${table.classComment}导入 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
public class ${sceneEnum.prefixClass}${table.className}ImportRespVO {
|
||||
|
||||
@Schema(description = "创建成功的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
private Integer successCount;
|
||||
|
||||
@Schema(description = "导入失败的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer failureCount;
|
||||
|
||||
@Schema(description = "导入失败的数据集合,key 为行号,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Map<Integer, String> failureRows;
|
||||
|
||||
}
|
||||
|
|
@ -25,6 +25,16 @@ public interface ${table.className}Service {
|
|||
* @return 编号
|
||||
*/
|
||||
${primaryColumn.javaType} create${simpleClassName}(@Valid ${saveReqVOClass} ${saveReqVOVar});
|
||||
#if ($importEnable)
|
||||
|
||||
/**
|
||||
* 导入${table.classComment}
|
||||
*
|
||||
* @param importList 导入信息
|
||||
* @return 导入结果
|
||||
*/
|
||||
${sceneEnum.prefixClass}${table.className}ImportRespVO import${simpleClassName}List(List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> importList);
|
||||
#end
|
||||
|
||||
/**
|
||||
* 更新${table.classComment}
|
||||
|
|
@ -162,4 +172,4 @@ public interface ${table.className}Service {
|
|||
|
||||
#end
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import org.springframework.validation.annotation.Validated;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
#if ($importEnable)
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
#end
|
||||
import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*;
|
||||
import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO;
|
||||
## 特殊:主子表专属逻辑
|
||||
|
|
@ -91,6 +94,32 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
|
|||
// 返回
|
||||
return ${classNameVar}.getId();
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
|
||||
public ${sceneEnum.prefixClass}${table.className}ImportRespVO import${simpleClassName}List(List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> importList) {
|
||||
if (CollUtil.isEmpty(importList)) {
|
||||
return ${sceneEnum.prefixClass}${table.className}ImportRespVO.builder()
|
||||
.successCount(0).failureCount(0).failureRows(new LinkedHashMap<>()).build();
|
||||
}
|
||||
// 遍历,逐个创建
|
||||
${sceneEnum.prefixClass}${table.className}ImportRespVO respVO = ${sceneEnum.prefixClass}${table.className}ImportRespVO.builder()
|
||||
.successCount(0).failureCount(0).failureRows(new LinkedHashMap<>()).build();
|
||||
AtomicInteger index = new AtomicInteger(1);
|
||||
importList.forEach(importItem -> {
|
||||
int currentIndex = index.getAndIncrement();
|
||||
try {
|
||||
create${simpleClassName}(BeanUtils.toBean(importItem, ${saveReqVOClass}.class));
|
||||
respVO.setSuccessCount(respVO.getSuccessCount() + 1);
|
||||
} catch (Exception ex) {
|
||||
respVO.getFailureRows().put(currentIndex, ex.getMessage());
|
||||
}
|
||||
});
|
||||
respVO.setFailureCount(respVO.getFailureRows().size());
|
||||
return respVO;
|
||||
}
|
||||
#end
|
||||
|
||||
@Override
|
||||
## 特殊:主子表专属逻辑(非 ERP 模式)
|
||||
|
|
@ -359,6 +388,9 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
|
|||
#else
|
||||
#if ( $subTable.subJoinMany)
|
||||
private void create${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
list.forEach(o -> o.set${SubJoinColumnName}(${subJoinColumn.javaField}).clean());
|
||||
${subClassNameVars.get($index)}Mapper.insertBatch(list);
|
||||
}
|
||||
|
|
@ -416,4 +448,4 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
|
|||
#end
|
||||
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
## 通用变量定义
|
||||
#if ($importEnable)
|
||||
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出', '导入'])
|
||||
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export', 'import'])
|
||||
#else
|
||||
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出'])
|
||||
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export'])
|
||||
#end
|
||||
##
|
||||
## 宏定义:生成按钮 SQL(通用部分)
|
||||
#macro(insertButtonSql $parentIdVar)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,26 @@ export function export${simpleClassName}Excel(params) {
|
|||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
// 下载${table.classComment}导入模板
|
||||
export function import${simpleClassName}Template() {
|
||||
return request({
|
||||
url: '${baseURL}/get-import-template',
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 导入${table.classComment}
|
||||
export function import${simpleClassName}(data) {
|
||||
return request({
|
||||
url: '${baseURL}/import',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
#end
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
#set ($index = $foreach.count - 1)
|
||||
|
|
@ -157,4 +177,4 @@ export function export${simpleClassName}Excel(params) {
|
|||
})
|
||||
}
|
||||
#end
|
||||
#end
|
||||
#end
|
||||
|
|
|
|||
|
|
@ -49,6 +49,16 @@
|
|||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
|
||||
v-hasPermi="['${permissionPrefix}:create']">新增</el-button>
|
||||
</el-col>
|
||||
#if ($importEnable)
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="el-icon-upload2" size="mini" @click="handleImport"
|
||||
:loading="importLoading" v-hasPermi="['${permissionPrefix}:import']">导入</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="el-icon-document" size="mini" @click="handleImportTemplate"
|
||||
v-hasPermi="['${permissionPrefix}:import']">导入模板</el-button>
|
||||
</el-col>
|
||||
#end
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
|
||||
v-hasPermi="['${permissionPrefix}:export']">导出</el-button>
|
||||
|
|
@ -78,6 +88,9 @@
|
|||
#end
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
#if ($importEnable)
|
||||
<input ref="importFileRef" type="file" style="display: none" accept=".xls,.xlsx" @change="handleImportFileChange" />
|
||||
#end
|
||||
|
||||
## 特殊:主子表专属逻辑
|
||||
#if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
|
||||
|
|
@ -244,6 +257,10 @@ export default {
|
|||
loading: true,
|
||||
// 导出遮罩层
|
||||
exportLoading: false,
|
||||
#if ($importEnable)
|
||||
// 导入遮罩层
|
||||
importLoading: false,
|
||||
#end
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
## 特殊:树表专属逻辑(树不需要分页接口)
|
||||
|
|
@ -322,6 +339,44 @@ export default {
|
|||
openForm(id) {
|
||||
this.#[[$]]#refs["formRef"].open(id);
|
||||
},
|
||||
#if ($importEnable)
|
||||
/** 导入按钮操作 */
|
||||
handleImport() {
|
||||
this.$refs.importFileRef && this.$refs.importFileRef.click();
|
||||
},
|
||||
/** 导入模板下载 */
|
||||
async handleImportTemplate() {
|
||||
const data = await ${simpleClassName}Api.import${simpleClassName}Template();
|
||||
this.#[[$]]#download.excel(data, '${table.classComment}导入模板.xls');
|
||||
},
|
||||
/** 导入文件变更 */
|
||||
async handleImportFileChange(event) {
|
||||
const target = event.target;
|
||||
const file = target.files && target.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
this.importLoading = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await ${simpleClassName}Api.import${simpleClassName}(formData);
|
||||
const data = res.data || res;
|
||||
let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + ';';
|
||||
if (data.failureRows) {
|
||||
Object.keys(data.failureRows).forEach((rowNo) => {
|
||||
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >';
|
||||
});
|
||||
}
|
||||
await this.$alert(text, '${table.classComment}导入结果', { dangerouslyUseHTMLString: true });
|
||||
await this.getList();
|
||||
} catch {
|
||||
} finally {
|
||||
target.value = '';
|
||||
this.importLoading = false;
|
||||
}
|
||||
},
|
||||
#end
|
||||
/** 删除按钮操作 */
|
||||
async handleDelete(row) {
|
||||
const ${primaryColumn.javaField} = row.${primaryColumn.javaField};
|
||||
|
|
|
|||
|
|
@ -98,6 +98,18 @@ export const ${simpleClassName}Api = {
|
|||
export${simpleClassName}: async (params) => {
|
||||
return await request.download({ url: `${baseURL}/export-excel`, params })
|
||||
},
|
||||
#if ($importEnable)
|
||||
|
||||
// 下载${table.classComment}导入模板
|
||||
import${simpleClassName}Template: async () => {
|
||||
return await request.download({ url: `${baseURL}/get-import-template` })
|
||||
},
|
||||
|
||||
// 导入${table.classComment}
|
||||
import${simpleClassName}: async (data: FormData) => {
|
||||
return await request.upload({ url: `${baseURL}/import`, data })
|
||||
},
|
||||
#end
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
#set ($index = $foreach.count - 1)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<Dialog v-model="dialogVisible" title="${table.classComment}导入" width="400">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
v-model:file-list="fileList"
|
||||
:auto-upload="false"
|
||||
:disabled="formLoading"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
accept=".xlsx, .xls"
|
||||
action="none"
|
||||
drag
|
||||
>
|
||||
<Icon icon="ep:upload" />
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip text-center">
|
||||
<span>仅允许导入 xls、xlsx 格式文件。</span>
|
||||
<el-link
|
||||
:underline="false"
|
||||
style="font-size: 12px; vertical-align: baseline"
|
||||
type="primary"
|
||||
@click="handleDownloadTemplate"
|
||||
>
|
||||
下载模板
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<template #footer>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { UploadUserFile } from 'element-plus'
|
||||
import download from '@/utils/download'
|
||||
import { ${simpleClassName}Api } from '@/api/${table.moduleName}/${table.businessName}'
|
||||
|
||||
/** ${table.classComment} 导入 */
|
||||
defineOptions({ name: '${simpleClassName}ImportForm' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const formLoading = ref(false) // 表单的加载中:上传、下载模板
|
||||
const uploadRef = ref()
|
||||
const fileList = ref<UploadUserFile[]>([])
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async () => {
|
||||
dialogVisible.value = true
|
||||
await resetForm()
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交导入 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
if (fileList.value.length === 0) {
|
||||
message.error('请上传文件')
|
||||
return
|
||||
}
|
||||
formLoading.value = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', fileList.value[0].raw as Blob)
|
||||
const res = await ${simpleClassName}Api.import${simpleClassName}(formData)
|
||||
const data = res.data
|
||||
let text = '导入成功数量:' + data.successCount + ';导入失败数量:' + data.failureCount + ';'
|
||||
if (data.failureCount > 0) {
|
||||
for (const rowNo in data.failureRows) {
|
||||
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >'
|
||||
}
|
||||
}
|
||||
message.alert(text)
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
await resetForm()
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载导入模板 */
|
||||
const handleDownloadTemplate = async () => {
|
||||
const data = await ${simpleClassName}Api.import${simpleClassName}Template()
|
||||
download.excel(data, '${table.classComment}导入模板.xls')
|
||||
}
|
||||
|
||||
/** 文件超限 */
|
||||
const handleExceed = (): void => {
|
||||
message.error('最多只能上传一个文件!')
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = async () => {
|
||||
fileList.value = []
|
||||
await nextTick()
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
</script>
|
||||
|
|
@ -92,6 +92,16 @@
|
|||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
#if ($importEnable)
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
@click="handleImport"
|
||||
v-hasPermi="['${permissionPrefix}:import']"
|
||||
>
|
||||
<Icon icon="ep:upload" class="mr-5px" /> 导入
|
||||
</el-button>
|
||||
#end
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
|
|
@ -237,6 +247,11 @@
|
|||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
#if ($importEnable)
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<${simpleClassName}ImportForm ref="importRef" @success="getList" />
|
||||
#end
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<${simpleClassName}Form ref="formRef" @success="getList" />
|
||||
|
|
@ -263,6 +278,9 @@
|
|||
import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { isEmpty } from '@/utils/is'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
#if ($importEnable)
|
||||
import ${simpleClassName}ImportForm from './${simpleClassName}ImportForm.vue'
|
||||
#end
|
||||
## 特殊:树表专属逻辑
|
||||
#if ( $table.templateType == 2 )
|
||||
import { handleTree } from '@/utils/tree'
|
||||
|
|
@ -308,6 +326,9 @@ const queryParams = reactive({
|
|||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
#if ($importEnable)
|
||||
const importRef = ref() // ${table.classComment} 导入组件的 Ref
|
||||
#end
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
|
|
@ -344,6 +365,13 @@ const formRef = ref()
|
|||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
/** 导入按钮操作 */
|
||||
const handleImport = () => {
|
||||
importRef.value.open()
|
||||
}
|
||||
#end
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
|
|
@ -421,4 +449,4 @@ const toggleExpandAll = async () => {
|
|||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -30,3 +30,15 @@ export function delete${simpleClassName}(id: number) {
|
|||
export function export${simpleClassName}(params) {
|
||||
return defHttp.download({ url: '${baseURL}/export-excel', params }, '${table.classComment}.xls')
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
// 下载${table.classComment}导入模板
|
||||
export function import${simpleClassName}Template() {
|
||||
return defHttp.download({ url: '${baseURL}/get-import-template' }, '${table.classComment}导入模板.xls')
|
||||
}
|
||||
|
||||
// 导入${table.classComment}
|
||||
export function import${simpleClassName}(data: FormData) {
|
||||
return defHttp.post({ url: '${baseURL}/import', data })
|
||||
}
|
||||
#end
|
||||
|
|
|
|||
|
|
@ -1,18 +1,31 @@
|
|||
<script lang="ts" setup>
|
||||
import ${simpleClassName}Modal from './${simpleClassName}Modal.vue'
|
||||
import { columns, searchFormSchema } from './${classNameVar}.data'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { useModal } from '@/components/Modal'
|
||||
import { IconEnum } from '@/enums/appEnum'
|
||||
import { BasicTable, TableAction, useTable } from '@/components/Table'
|
||||
import { delete${simpleClassName}, export${simpleClassName}, get${simpleClassName}Page } from '@/api/${table.moduleName}/${table.businessName}'
|
||||
import {
|
||||
delete${simpleClassName},
|
||||
export${simpleClassName},
|
||||
get${simpleClassName}Page,
|
||||
#if ($importEnable)
|
||||
import${simpleClassName},
|
||||
import${simpleClassName}Template,
|
||||
#end
|
||||
} from '@/api/${table.moduleName}/${table.businessName}'
|
||||
|
||||
defineOptions({ name: '${table.className}' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const { createConfirm, createMessage } = useMessage()
|
||||
const [registerModal, { openModal }] = useModal()
|
||||
#if ($importEnable)
|
||||
const importFileRef = ref<HTMLInputElement>()
|
||||
const importLoading = ref(false)
|
||||
#end
|
||||
|
||||
const [registerTable, { getForm, reload }] = useTable({
|
||||
title: '${table.classComment}列表',
|
||||
|
|
@ -48,6 +61,44 @@ async function handleExport() {
|
|||
},
|
||||
})
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
function handleImport() {
|
||||
importFileRef.value?.click()
|
||||
}
|
||||
|
||||
async function handleImportTemplateDownload() {
|
||||
await import${simpleClassName}Template()
|
||||
createMessage.success('模板下载已开始')
|
||||
}
|
||||
|
||||
async function handleImportFileChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
importLoading.value = true
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const response: any = await import${simpleClassName}(formData)
|
||||
const data = response?.data ?? response
|
||||
let text =
|
||||
'导入成功数量:' + (data?.successCount || 0) + ';导入失败数量:' + (data?.failureCount || 0) + ';'
|
||||
if (data?.failureRows) {
|
||||
Object.keys(data.failureRows).forEach((rowNo) => {
|
||||
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >'
|
||||
})
|
||||
}
|
||||
createMessage.success(text)
|
||||
await reload()
|
||||
} finally {
|
||||
target.value = ''
|
||||
importLoading.value = false
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
async function handleDelete(record: Recordable) {
|
||||
await delete${simpleClassName}(record.id)
|
||||
|
|
@ -62,6 +113,14 @@ async function handleDelete(record: Recordable) {
|
|||
<a-button type="primary" v-auth="['${permissionPrefix}:create']" :preIcon="IconEnum.ADD" @click="handleCreate">
|
||||
{{ t('action.create') }}
|
||||
</a-button>
|
||||
#if ($importEnable)
|
||||
<a-button v-auth="['${permissionPrefix}:import']" :loading="importLoading" @click="handleImport">
|
||||
导入
|
||||
</a-button>
|
||||
<a-button v-auth="['${permissionPrefix}:import']" @click="handleImportTemplateDownload">
|
||||
导入模板
|
||||
</a-button>
|
||||
#end
|
||||
<a-button v-auth="['${permissionPrefix}:export']" :preIcon="IconEnum.EXPORT" @click="handleExport">
|
||||
{{ t('action.export') }}
|
||||
</a-button>
|
||||
|
|
@ -87,6 +146,9 @@ async function handleDelete(record: Recordable) {
|
|||
</template>
|
||||
</template>
|
||||
</BasicTable>
|
||||
#if ($importEnable)
|
||||
<input ref="importFileRef" type="file" accept=".xls,.xlsx" class="hidden" @change="handleImportFileChange" />
|
||||
#end
|
||||
<${simpleClassName}Modal @register="registerModal" @success="reload()" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -100,6 +100,18 @@ export function delete${simpleClassName}List(ids: number[]) {
|
|||
export function export${simpleClassName}(params: any) {
|
||||
return requestClient.download('${baseURL}/export-excel', { params });
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
/** 下载${table.classComment}导入模板 */
|
||||
export function import${simpleClassName}Template() {
|
||||
return requestClient.download('${baseURL}/get-import-template');
|
||||
}
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
export function import${simpleClassName}(data: FormData) {
|
||||
return requestClient.post('${baseURL}/import', data);
|
||||
}
|
||||
#end
|
||||
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
<script lang="ts" setup>
|
||||
import type { FileType } from 'ant-design-vue/es/upload/interface';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { Button, message, Upload } from 'ant-design-vue';
|
||||
|
||||
import { import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}';
|
||||
|
||||
defineOptions({ name: '${simpleClassName}Import' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const fileRef = ref<File | null>(null);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
if (!fileRef.value) {
|
||||
message.error('请上传文件');
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileRef.value);
|
||||
const response: any = await import${simpleClassName}(formData);
|
||||
const data = response?.data ?? response ?? {};
|
||||
let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + ';';
|
||||
if (data.failureRows) {
|
||||
Object.keys(data.failureRows).forEach((rowNo) => {
|
||||
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >';
|
||||
});
|
||||
}
|
||||
message.info(text);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 上传前:拦截 antd Upload 的自动上传,文件存到 ref */
|
||||
function beforeUpload(file: FileType) {
|
||||
fileRef.value = file as unknown as File;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 下载导入模板 */
|
||||
async function handleDownload() {
|
||||
const data = await import${simpleClassName}Template();
|
||||
downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="导入${table.classComment}" class="w-1/3">
|
||||
<div class="mx-4">
|
||||
<Upload :max-count="1" accept=".xls,.xlsx" :before-upload="beforeUpload">
|
||||
<Button type="primary"> 选择 Excel 文件 </Button>
|
||||
</Upload>
|
||||
</div>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<Button @click="handleDownload"> 下载导入模板 </Button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -31,6 +31,9 @@ import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClas
|
|||
#else## 标准表接口
|
||||
import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${table.businessName}';
|
||||
#end
|
||||
#if ($importEnable)
|
||||
import ${simpleClassName}Import from './modules/import-form.vue';
|
||||
#end
|
||||
|
||||
#if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况
|
||||
/** 子表的列表 */
|
||||
|
|
@ -119,6 +122,17 @@ const [FormModal, formModalApi] = useVbenModal({
|
|||
connectedComponent: ${simpleClassName}Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
#if ($importEnable)
|
||||
const [ImportFormModal, importFormModalApi] = useVbenModal({
|
||||
connectedComponent: ${simpleClassName}Import,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
function handleImport() {
|
||||
importFormModalApi.open();
|
||||
}
|
||||
#end
|
||||
|
||||
/** 创建${table.classComment} */
|
||||
function handleCreate() {
|
||||
|
|
@ -190,6 +204,7 @@ try {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#if (${table.templateType} == 2)
|
||||
/** 切换树形展开/收缩状态 */
|
||||
const isExpanded = ref(true);
|
||||
|
|
@ -209,6 +224,9 @@ onMounted(() => {
|
|||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="getList" />
|
||||
#if ($importEnable)
|
||||
<ImportFormModal @success="getList" />
|
||||
#end
|
||||
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
|
|
@ -314,6 +332,16 @@ onMounted(() => {
|
|||
>
|
||||
{{ $t('ui.actionTitle.create', ['${table.classComment}']) }}
|
||||
</Button>
|
||||
#if ($importEnable)
|
||||
<Button
|
||||
class="ml-2"
|
||||
type="primary"
|
||||
@click="handleImport"
|
||||
v-access:code="['${permissionPrefix}:import']"
|
||||
>
|
||||
导入
|
||||
</Button>
|
||||
#end
|
||||
<Button
|
||||
:icon="h(Download)"
|
||||
type="primary"
|
||||
|
|
|
|||
|
|
@ -134,6 +134,21 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
];
|
||||
}
|
||||
|
||||
#if ($importEnable)
|
||||
/** 导入的表单 */
|
||||
export function useImportFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'file',
|
||||
label: '${table.classComment}数据',
|
||||
component: 'Upload',
|
||||
rules: 'required',
|
||||
help: '仅允许导入 xls、xlsx 格式文件',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
#end
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts" setup>
|
||||
import type { FileType } from 'ant-design-vue/es/upload/interface';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { Button, message, Upload } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useImportFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useImportFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await import${simpleClassName}(data.file);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 上传前 */
|
||||
function beforeUpload(file: FileType) {
|
||||
formApi.setFieldValue('file', file);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 下载模板 */
|
||||
async function handleDownload() {
|
||||
const data = await import${simpleClassName}Template();
|
||||
downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="导入${table.classComment}" class="w-1/3">
|
||||
<Form class="mx-4">
|
||||
<template #file>
|
||||
<div class="w-full">
|
||||
<Upload
|
||||
:max-count="1"
|
||||
accept=".xls,.xlsx"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<Button type="primary"> 选择 Excel 文件 </Button>
|
||||
</Upload>
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<Button @click="handleDownload"> 下载导入模板 </Button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -26,6 +26,9 @@ import {
|
|||
get${simpleClassName}Page,
|
||||
} from '#/api/${table.moduleName}/${table.businessName}';
|
||||
#end
|
||||
#if ($importEnable)
|
||||
import ${simpleClassName}Import from './modules/import-form.vue';
|
||||
#end
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
|
@ -50,6 +53,17 @@ const [FormModal, formModalApi] = useVbenModal({
|
|||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
#if ($importEnable)
|
||||
const [ImportFormModal, importFormModalApi] = useVbenModal({
|
||||
connectedComponent: ${simpleClassName}Import,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
function handleImport() {
|
||||
importFormModalApi.open();
|
||||
}
|
||||
#end
|
||||
#if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
|
||||
|
||||
/** 切换树形展开/收缩状态 */
|
||||
|
|
@ -135,6 +149,7 @@ async function handleExport() {
|
|||
downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data });
|
||||
}
|
||||
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
|
|
@ -210,6 +225,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
#if ($importEnable)
|
||||
<ImportFormModal @success="handleRefresh" />
|
||||
#end
|
||||
#if ($table.templateType == 11) ## erp情况
|
||||
<div>
|
||||
#end
|
||||
|
|
@ -247,6 +265,14 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
onClick: handleExpand,
|
||||
},
|
||||
#end
|
||||
#if ($importEnable)
|
||||
{
|
||||
label: '导入',
|
||||
type: 'primary',
|
||||
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:import'],
|
||||
onClick: handleImport,
|
||||
},
|
||||
#end
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
|
|
@ -318,4 +344,4 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
</div>
|
||||
#end
|
||||
</Page>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -100,6 +100,18 @@ export function delete${simpleClassName}List(ids: number[]) {
|
|||
export function export${simpleClassName}(params: any) {
|
||||
return requestClient.download('${baseURL}/export-excel', { params });
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
/** 下载${table.classComment}导入模板 */
|
||||
export function import${simpleClassName}Template() {
|
||||
return requestClient.download('${baseURL}/get-import-template');
|
||||
}
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
export function import${simpleClassName}(data: FormData) {
|
||||
return requestClient.post('${baseURL}/import', data);
|
||||
}
|
||||
#end
|
||||
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { ElButton, ElMessage, ElUpload } from 'element-plus';
|
||||
|
||||
import { import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}';
|
||||
|
||||
defineOptions({ name: '${simpleClassName}Import' });
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const fileRef = ref<File | null>(null);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
if (!fileRef.value) {
|
||||
ElMessage.error('请上传文件');
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileRef.value);
|
||||
const response: any = await import${simpleClassName}(formData);
|
||||
const data = response?.data ?? response ?? {};
|
||||
let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + ';';
|
||||
if (data.failureRows) {
|
||||
Object.keys(data.failureRows).forEach((rowNo) => {
|
||||
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >';
|
||||
});
|
||||
}
|
||||
ElMessage.info(text);
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 文件改变 */
|
||||
function handleChange(file: any) {
|
||||
if (file.raw) {
|
||||
fileRef.value = file.raw;
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载导入模板 */
|
||||
async function handleDownload() {
|
||||
const data = await import${simpleClassName}Template();
|
||||
downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="导入${table.classComment}" class="w-1/3">
|
||||
<div class="mx-4">
|
||||
<ElUpload
|
||||
:limit="1"
|
||||
accept=".xls,.xlsx"
|
||||
:on-change="handleChange"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
|
||||
</ElUpload>
|
||||
</div>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<ElButton @click="handleDownload"> 下载导入模板 </ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -31,7 +31,9 @@ import { get${simpleClassName}List, delete${simpleClassName}, export${simpleClas
|
|||
import { isEmpty } from '@vben/utils';
|
||||
import { get${simpleClassName}Page, delete${simpleClassName},#if ($deleteBatchEnable) delete${simpleClassName}List,#end export${simpleClassName} } from '#/api/${table.moduleName}/${simpleClassName_strikeCase}';
|
||||
#end
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
#if ($importEnable)
|
||||
import ${simpleClassName}Import from './modules/import-form.vue';
|
||||
#end
|
||||
|
||||
#if ($table.templateType == 12 || $table.templateType == 11) ## 内嵌和erp情况
|
||||
/** 子表的列表 */
|
||||
|
|
@ -120,6 +122,17 @@ const [FormModal, formModalApi] = useVbenModal({
|
|||
connectedComponent: ${simpleClassName}Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
#if ($importEnable)
|
||||
const [ImportFormModal, importFormModalApi] = useVbenModal({
|
||||
connectedComponent: ${simpleClassName}Import,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
function handleImport() {
|
||||
importFormModalApi.open();
|
||||
}
|
||||
#end
|
||||
|
||||
/** 创建${table.classComment} */
|
||||
function handleCreate() {
|
||||
|
|
@ -189,6 +202,7 @@ try {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#if (${table.templateType} == 2)
|
||||
/** 切换树形展开/收缩状态 */
|
||||
const isExpanded = ref(true);
|
||||
|
|
@ -208,6 +222,9 @@ onMounted(() => {
|
|||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="getList" />
|
||||
#if ($importEnable)
|
||||
<ImportFormModal @success="getList" />
|
||||
#end
|
||||
|
||||
<ContentWrap v-if="!hiddenSearchBar">
|
||||
<!-- 搜索工作栏 -->
|
||||
|
|
@ -316,6 +333,16 @@ onMounted(() => {
|
|||
>
|
||||
{{ $t('ui.actionTitle.create', ['${table.classComment}']) }}
|
||||
</el-button>
|
||||
#if ($importEnable)
|
||||
<el-button
|
||||
class="ml-2"
|
||||
type="primary"
|
||||
@click="handleImport"
|
||||
v-access:code="['${permissionPrefix}:import']"
|
||||
>
|
||||
导入
|
||||
</el-button>
|
||||
#end
|
||||
<el-button
|
||||
:icon="h(Download)"
|
||||
type="primary"
|
||||
|
|
|
|||
|
|
@ -113,6 +113,18 @@ export function delete${simpleClassName}List(ids: number[]) {
|
|||
export function export${simpleClassName}(params: any) {
|
||||
return requestClient.download('${baseURL}/export-excel', { params });
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
/** 下载${table.classComment}导入模板 */
|
||||
export function import${simpleClassName}Template() {
|
||||
return requestClient.download('${baseURL}/get-import-template');
|
||||
}
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
export function import${simpleClassName}(file: File) {
|
||||
return requestClient.upload('${baseURL}/import', { file });
|
||||
}
|
||||
#end
|
||||
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
|
|
|
|||
|
|
@ -137,6 +137,21 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
];
|
||||
}
|
||||
|
||||
#if ($importEnable)
|
||||
/** 导入的表单 */
|
||||
export function useImportFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'file',
|
||||
label: '${table.classComment}数据',
|
||||
component: 'Upload',
|
||||
rules: 'required',
|
||||
help: '仅允许导入 xls、xlsx 格式文件',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
#end
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts" setup>
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||
|
||||
import { ElButton, ElMessage, ElUpload } from 'element-plus';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { import${simpleClassName}, import${simpleClassName}Template } from '#/api/${table.moduleName}/${table.businessName}';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useImportFormSchema } from '../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useImportFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = await formApi.getValues();
|
||||
try {
|
||||
await import${simpleClassName}(data.file);
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 文件改变时 */
|
||||
function handleChange(file: any) {
|
||||
if (file.raw) {
|
||||
formApi.setFieldValue('file', file.raw);
|
||||
}
|
||||
}
|
||||
|
||||
/** 下载模板 */
|
||||
async function handleDownload() {
|
||||
const data = await import${simpleClassName}Template();
|
||||
downloadFileFromBlobPart({ fileName: '${table.classComment}导入模板.xls', source: data });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="导入${table.classComment}" class="w-1/3">
|
||||
<Form class="mx-4">
|
||||
<template #file>
|
||||
<div class="w-full">
|
||||
<ElUpload
|
||||
:limit="1"
|
||||
accept=".xls,.xlsx"
|
||||
:on-change="handleChange"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<ElButton type="primary"> 选择 Excel 文件 </ElButton>
|
||||
</ElUpload>
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
<template #prepend-footer>
|
||||
<div class="flex flex-auto items-center">
|
||||
<ElButton @click="handleDownload"> 下载导入模板 </ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -26,6 +26,9 @@ import {
|
|||
get${simpleClassName}Page,
|
||||
} from '#/api/${table.moduleName}/${table.businessName}';
|
||||
#end
|
||||
#if ($importEnable)
|
||||
import ${simpleClassName}Import from './modules/import-form.vue';
|
||||
#end
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
|
@ -50,6 +53,17 @@ const [FormModal, formModalApi] = useVbenModal({
|
|||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
#if ($importEnable)
|
||||
const [ImportFormModal, importFormModalApi] = useVbenModal({
|
||||
connectedComponent: ${simpleClassName}Import,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 导入${table.classComment} */
|
||||
function handleImport() {
|
||||
importFormModalApi.open();
|
||||
}
|
||||
#end
|
||||
#if (${table.templateType} == 2)## 树表特有:控制表格展开收缩
|
||||
|
||||
/** 切换树形展开/收缩状态 */
|
||||
|
|
@ -133,6 +147,7 @@ async function handleExport() {
|
|||
downloadFileFromBlobPart({ fileName: '${table.classComment}.xls', source: data });
|
||||
}
|
||||
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({
|
||||
formOptions: {
|
||||
schema: useGridFormSchema(),
|
||||
|
|
@ -208,6 +223,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="handleRefresh" />
|
||||
#if ($importEnable)
|
||||
<ImportFormModal @success="handleRefresh" />
|
||||
#end
|
||||
#if ($table.templateType == 11) ## erp情况
|
||||
<div>
|
||||
#end
|
||||
|
|
@ -245,6 +263,14 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
onClick: handleExpand,
|
||||
},
|
||||
#end
|
||||
#if ($importEnable)
|
||||
{
|
||||
label: '导入',
|
||||
type: 'primary',
|
||||
auth: ['${table.moduleName}:${simpleClassName_strikeCase}:import'],
|
||||
onClick: handleImport,
|
||||
},
|
||||
#end
|
||||
{
|
||||
label: $t('ui.actionTitle.export'),
|
||||
type: 'primary',
|
||||
|
|
@ -317,4 +343,4 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
</div>
|
||||
#end
|
||||
</Page>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -84,4 +84,19 @@ public class CodegenBuilderTest extends BaseMockitoUnitTest {
|
|||
assertEquals("input", column.getHtmlType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSanitizeComment() {
|
||||
// 1. null / 空字符串:原样返回
|
||||
assertNull(codegenBuilder.sanitizeComment(null));
|
||||
assertEquals("", codegenBuilder.sanitizeComment(""));
|
||||
// 2. 无英文引号:原样返回
|
||||
assertEquals("无引号注释", codegenBuilder.sanitizeComment("无引号注释"));
|
||||
// 3. 含英文双引号:替换为中文左双引号
|
||||
assertEquals("含“双“引号", codegenBuilder.sanitizeComment("含\"双\"引号"));
|
||||
// 4. 含英文单引号:替换为中文左单引号
|
||||
assertEquals("含‘单‘引号", codegenBuilder.sanitizeComment("含'单'引号"));
|
||||
// 5. 双 / 单引号混合
|
||||
assertEquals("“混‘搭“‘", codegenBuilder.sanitizeComment("\"混'搭\"'"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
|||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenVOTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.mockito.InjectMocks;
|
||||
|
|
@ -19,8 +20,10 @@ import java.io.ByteArrayInputStream;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
|
@ -41,16 +44,18 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
|
|||
|
||||
@Spy
|
||||
protected CodegenProperties codegenProperties = new CodegenProperties()
|
||||
.setBasePackage("cn.iocoder.yudao");
|
||||
.setBasePackage("cn.iocoder.yudao")
|
||||
.setVoType(CodegenVOTypeEnum.VO.getType())
|
||||
.setDeleteBatchEnable(true)
|
||||
.setUnitTestEnable(true)
|
||||
.setImportEnable(false);
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
codegenEngine.setJakartaEnable(true); // 强制使用 jakarta,保证单测可以基于 jakarta 断言
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 单测强制使用
|
||||
// 获取测试文件 resources 路径
|
||||
// 获取测试文件 resources 路径,writeResult 调试用
|
||||
String absolutePath = FileUtil.getAbsolutePath("application-unit-test.yaml");
|
||||
// 系统不一样生成的文件也有差异,那就各自生成各自的
|
||||
resourcesPath = absolutePath.split("/target")[0] + "/src/test/resources/codegen/";
|
||||
}
|
||||
|
||||
|
|
@ -82,17 +87,32 @@ public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
|
|||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新生成断言数据的开关,命令行加 {@code -Dcodegen.regenerate=true} 启用
|
||||
*/
|
||||
private static final boolean REGENERATE = Boolean.parseBoolean(System.getProperty("codegen.regenerate", "false"));
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
protected static void assertResult(Map<String, String> result, String path) {
|
||||
protected void assertResult(Map<String, String> result, String path) {
|
||||
if (REGENERATE) {
|
||||
writeResult(result, resourcesPath + path);
|
||||
return;
|
||||
}
|
||||
String assertContent = ResourceUtil.readUtf8Str("codegen/" + path + "/assert.json");
|
||||
List<HashMap> asserts = JsonUtils.parseArray(assertContent, HashMap.class);
|
||||
assertEquals(asserts.size(), result.size());
|
||||
// 校验每个文件
|
||||
Set<String> expectedFiles = asserts.stream()
|
||||
.map(m -> (String) m.get("filePath"))
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
assertEquals(expectedFiles, result.keySet(), "生成文件集合不匹配");
|
||||
// 校验每个文件;归一化 \r\n 为 \n,让断言不依赖文件落盘的换行风格
|
||||
asserts.forEach(assertMap -> {
|
||||
String contentPath = (String) assertMap.get("contentPath");
|
||||
String filePath = (String) assertMap.get("filePath");
|
||||
String content = ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath);
|
||||
assertEquals(content, result.get(filePath), filePath + ":不匹配");
|
||||
String expected = ResourceUtil.readUtf8Str("codegen/" + path + "/" + contentPath)
|
||||
.replace("\r\n", "\n");
|
||||
String actual = result.get(filePath);
|
||||
assertEquals(expected, actual == null ? null : actual.replace("\r\n", "\n"),
|
||||
filePath + ":不匹配");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + AntD + General 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5AntdGeneralTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType();
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_general_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_general_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_general_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_normal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_antd_general_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_erp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_antd_general_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_inner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_antd_general_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + AntD + Schema 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5AntdSchemaTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType();
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_schema_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_schema_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_antd_schema_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_normal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_antd_schema_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_erp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_antd_schema_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_inner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_antd_schema_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + Element Plus + General 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5EleGeneralTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType();
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_general_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_general_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_general_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_normal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_ele_general_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_erp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_ele_general_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_inner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_ele_general_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package cn.iocoder.yudao.module.infra.service.codegen.inner;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link CodegenEngine} 的 Vue3 + Vben5 + Element Plus + Schema 单元测试
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class CodegenEngineVben5EleSchemaTest extends CodegenEngineAbstractTest {
|
||||
|
||||
private static final Integer FRONT_TYPE = CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType();
|
||||
|
||||
@Test
|
||||
public void testExecute_one() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_schema_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_schema_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_tree() {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("category")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("category");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vben5_ele_schema_tree");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_normal() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "/vben5_ele_schema_master_normal");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_erp() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_ERP, "/vben5_ele_schema_master_erp");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_master_inner() {
|
||||
testExecute_master(CodegenTemplateTypeEnum.MASTER_INNER, "/vben5_ele_schema_master_inner");
|
||||
}
|
||||
|
||||
private void testExecute_master(CodegenTemplateTypeEnum templateType, String path) {
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setTemplateType(templateType.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
// 准备参数(子表)
|
||||
CodegenTableDO contactTable = getTable("contact")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(100L).setSubJoinMany(true);
|
||||
List<CodegenColumnDO> contactColumns = getColumnList("contact");
|
||||
// 准备参数(班主任)
|
||||
CodegenTableDO teacherTable = getTable("teacher")
|
||||
.setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
|
||||
.setFrontType(FRONT_TYPE)
|
||||
.setSubJoinColumnId(200L).setSubJoinMany(false);
|
||||
List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns,
|
||||
Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
|
||||
// 断言
|
||||
assertResult(result, path);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
|||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
|
@ -17,7 +16,6 @@ import java.util.Map;
|
|||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Disabled
|
||||
public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
|
||||
|
||||
@Test
|
||||
|
|
@ -36,6 +34,23 @@ public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
|
|||
assertResult(result, "/vue2_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue2_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE2_ELEMENT_UI.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vue2_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue2_tree() {
|
||||
// 准备参数
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
|
|||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenFrontTypeEnum;
|
||||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
|
@ -17,7 +16,6 @@ import java.util.Map;
|
|||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Disabled
|
||||
public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
|
||||
|
||||
@Test
|
||||
|
|
@ -36,6 +34,23 @@ public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
|
|||
assertResult(result, "/vue3_one");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue3_one_importEnable() {
|
||||
// 开启 import 开关
|
||||
codegenProperties.setImportEnable(true);
|
||||
codegenEngine.initGlobalBindingMap();
|
||||
// 准备参数
|
||||
CodegenTableDO table = getTable("student")
|
||||
.setFrontType(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType())
|
||||
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
List<CodegenColumnDO> columns = getColumnList("student");
|
||||
|
||||
// 调用
|
||||
Map<String, String> result = codegenEngine.execute(DbType.MYSQL, table, columns, null, null);
|
||||
// 断言
|
||||
assertResult(result, "/vue3_one_importEnable");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExecute_vue3_tree() {
|
||||
// 准备参数
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
public void setUp() {
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = true;
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -93,7 +94,7 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String url = randomString();
|
||||
AtomicReference<String> pathRef = new AtomicReference<>();
|
||||
when(client.upload(same(content), argThat(path -> {
|
||||
assertTrue(path.matches(directory + "/\\d{8}/" + name + "_\\d+.jpg"));
|
||||
assertTrue(path.matches(directory + "/\\d{8}/\\d+/" + name + ".jpg"));
|
||||
pathRef.set(path);
|
||||
return true;
|
||||
}), eq(type))).thenReturn(url);
|
||||
|
|
@ -125,7 +126,7 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String url = randomString();
|
||||
AtomicReference<String> pathRef = new AtomicReference<>();
|
||||
when(client.upload(same(content), argThat(path -> {
|
||||
assertTrue(path.matches("\\d{8}/6318848e882d8a7e7e82789d87608f684ee52d41966bfc8cad3ce15aad2b970e_\\d+\\.jpg"));
|
||||
assertTrue(path.matches("\\d{8}/\\d+/6318848e882d8a7e7e82789d87608f684ee52d41966bfc8cad3ce15aad2b970e\\.jpg"));
|
||||
pathRef.set(path);
|
||||
return true;
|
||||
}), eq(type))).thenReturn(url);
|
||||
|
|
@ -200,10 +201,10 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/test_timestamp.jpg
|
||||
// 格式为:avatar/yyyyMMdd/{时间戳+随机数}/test.jpg
|
||||
assertTrue(path.startsWith(directory + "/"));
|
||||
// 包含日期格式:8 位数字,如 20240517
|
||||
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+\\.jpg"));
|
||||
assertTrue(path.matches(directory + "/\\d{8}/\\d+/test\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -236,9 +237,9 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/test_timestamp.jpg
|
||||
// 格式为:avatar/{时间戳+随机数}/test.jpg
|
||||
assertTrue(path.startsWith(directory + "/"));
|
||||
assertTrue(path.matches(directory + "/test_\\d+\\.jpg"));
|
||||
assertTrue(path.matches(directory + "/\\d+/test\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -269,9 +270,9 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/test_timestamp
|
||||
// 格式为:avatar/yyyyMMdd/{时间戳+随机数}/test
|
||||
assertTrue(path.startsWith(directory + "/"));
|
||||
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+"));
|
||||
assertTrue(path.matches(directory + "/\\d{8}/\\d+/test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -286,8 +287,59 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:yyyyMMdd/test_timestamp.jpg
|
||||
assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg"));
|
||||
// 格式为:yyyyMMdd/{时间戳+随机数}/test.jpg
|
||||
assertTrue(path.matches("\\d{8}/\\d+/test\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_SuffixAsName_AllEnabled() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = false;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/test_{时间戳+随机数}.jpg
|
||||
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_SuffixAsName_PrefixDisabled() {
|
||||
// 准备参数
|
||||
String name = "test.jpg";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = false;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/test_{时间戳+随机数}.jpg
|
||||
assertTrue(path.matches(directory + "/test_\\d+\\.jpg"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGenerateUploadPath_SuffixAsName_NoExtension() {
|
||||
// 准备参数
|
||||
String name = "test";
|
||||
String directory = "avatar";
|
||||
FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
FileServiceImpl.PATH_SUFFIX_AS_DIRECTORY = false;
|
||||
|
||||
// 调用
|
||||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:avatar/yyyyMMdd/test_{时间戳+随机数}
|
||||
assertTrue(path.matches(directory + "/\\d{8}/test_\\d+"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -302,8 +354,8 @@ public class FileServiceImplTest extends BaseDbUnitTest {
|
|||
String path = fileService.generateUploadPath(name, directory);
|
||||
|
||||
// 断言
|
||||
// 格式为:yyyyMMdd/test_timestamp.jpg
|
||||
assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg"));
|
||||
// 格式为:yyyyMMdd/{时间戳+随机数}/test.jpg
|
||||
assertTrue(path.matches("\\d{8}/\\d+/test\\.jpg"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
[ {
|
||||
"contentPath" : "java/InfraStudentPageReqVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentRespVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentSaveReqVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentController",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
|
||||
}, {
|
||||
"contentPath" : "xml/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/resources/mapper/demo/InfraStudentMapper.xml"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImpl",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentService",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImplTest",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
|
||||
}, {
|
||||
"contentPath" : "java/ErrorCodeConstants_手动操作",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
|
||||
}, {
|
||||
"contentPath" : "sql/sql",
|
||||
"filePath" : "sql/sql.sql"
|
||||
}, {
|
||||
"contentPath" : "sql/h2",
|
||||
"filePath" : "sql/h2.sql"
|
||||
}, {
|
||||
"contentPath" : "vue/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/index.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/form.vue"
|
||||
}, {
|
||||
"contentPath" : "ts/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/api/infra/demo/index.ts"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-list",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-list.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-list",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-list.vue"
|
||||
} ]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
ErrorCode STUDENT_CONTACT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生联系人不存在");
|
||||
ErrorCode STUDENT_TEACHER_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任不存在");
|
||||
ErrorCode STUDENT_TEACHER_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任已存在");
|
||||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
ErrorCode STUDENT_CONTACT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生联系人不存在");
|
||||
ErrorCode STUDENT_TEACHER_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任不存在");
|
||||
ErrorCode STUDENT_TEACHER_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任已存在");
|
||||
|
|
@ -27,4 +27,8 @@ public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentConta
|
|||
return delete(InfraStudentContactDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
package cn.iocoder.yudao.module.infra.controller.admin.demo;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import javax.validation.constraints.*;
|
||||
import javax.validation.*;
|
||||
import javax.servlet.http.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import jakarta.validation.*;
|
||||
import jakarta.servlet.http.*;
|
||||
import java.util.*;
|
||||
import java.io.IOException;
|
||||
|
||||
|
|
@ -22,8 +22,8 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
|||
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
|
||||
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
|
||||
import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
|
||||
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
|
||||
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
|
||||
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
|
|
@ -64,6 +64,15 @@ public class InfraStudentController {
|
|||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得学生")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
|
|
@ -84,7 +93,7 @@ public class InfraStudentController {
|
|||
@GetMapping("/export-excel")
|
||||
@Operation(summary = "导出学生 Excel")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:export')")
|
||||
@OperateLog(type = EXPORT)
|
||||
@ApiAccessLog(operateType = EXPORT)
|
||||
public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
|
||||
HttpServletResponse response) throws IOException {
|
||||
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
|
||||
|
|
@ -129,6 +138,15 @@ public class InfraStudentController {
|
|||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/student-contact/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生联系人")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentContactList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentContactListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/student-contact/get")
|
||||
@Operation(summary = "获得学生联系人")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
|
|
@ -172,6 +190,15 @@ public class InfraStudentController {
|
|||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/student-teacher/delete-list")
|
||||
@Parameter(name = "ids", description = "编号", required = true)
|
||||
@Operation(summary = "批量删除学生班主任")
|
||||
@PreAuthorize("@ss.hasPermission('infra:student:delete')")
|
||||
public CommonResult<Boolean> deleteStudentTeacherList(@RequestParam("ids") List<Long> ids) {
|
||||
studentService.deleteStudentTeacherListByIds(ids);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/student-teacher/get")
|
||||
@Operation(summary = "获得学生班主任")
|
||||
@Parameter(name = "id", description = "编号", required = true)
|
||||
|
|
@ -64,4 +64,5 @@ public class InfraStudentDO extends BaseDO {
|
|||
*/
|
||||
private String memo;
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -11,8 +11,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
|
|||
|
||||
@Schema(description = "管理后台 - 学生分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class InfraStudentPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "名字", example = "芋头")
|
||||
|
|
@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import java.util.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import cn.idev.excel.annotation.*;
|
||||
|
|
@ -57,4 +56,4 @@ public class InfraStudentRespVO {
|
|||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,7 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
import java.util.*;
|
||||
import javax.validation.constraints.*;
|
||||
import java.util.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import java.util.*;
|
||||
import javax.validation.*;
|
||||
import jakarta.validation.*;
|
||||
import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
||||
import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
|
||||
|
|
@ -38,6 +38,13 @@ public interface InfraStudentService {
|
|||
*/
|
||||
void deleteStudent(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除学生
|
||||
*
|
||||
* @param ids 编号
|
||||
*/
|
||||
void deleteStudentListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得学生
|
||||
*
|
||||
|
|
@ -87,6 +94,13 @@ public interface InfraStudentService {
|
|||
*/
|
||||
void deleteStudentContact(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除学生联系人
|
||||
*
|
||||
* @param ids 编号
|
||||
*/
|
||||
void deleteStudentContactListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得学生联系人
|
||||
*
|
||||
|
|
@ -128,6 +142,13 @@ public interface InfraStudentService {
|
|||
*/
|
||||
void deleteStudentTeacher(Long id);
|
||||
|
||||
/**
|
||||
* 批量删除学生班主任
|
||||
*
|
||||
* @param ids 编号
|
||||
*/
|
||||
void deleteStudentTeacherListByIds(List<Long> ids);
|
||||
|
||||
/**
|
||||
* 获得学生班主任
|
||||
*
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
package cn.iocoder.yudao.module.infra.service.demo;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
|
|
@ -19,6 +20,8 @@ import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
|
|||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList;
|
||||
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
|
|
@ -42,6 +45,7 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
// 插入
|
||||
InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
|
||||
studentMapper.insert(student);
|
||||
|
||||
// 返回
|
||||
return student.getId();
|
||||
}
|
||||
|
|
@ -68,6 +72,18 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
deleteStudentTeacherByStudentId(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteStudentListByIds(List<Long> ids) {
|
||||
// 删除
|
||||
studentMapper.deleteByIds(ids);
|
||||
|
||||
// 删除子表
|
||||
deleteStudentContactByStudentIds(ids);
|
||||
deleteStudentTeacherByStudentIds(ids);
|
||||
}
|
||||
|
||||
|
||||
private void validateStudentExists(Long id) {
|
||||
if (studentMapper.selectById(id) == null) {
|
||||
throw exception(STUDENT_NOT_EXISTS);
|
||||
|
|
@ -93,6 +109,7 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
|
||||
@Override
|
||||
public Long createStudentContact(InfraStudentContactDO studentContact) {
|
||||
studentContact.clean(); // 清理掉创建、更新时间等相关属性值
|
||||
studentContactMapper.insert(studentContact);
|
||||
return studentContact.getId();
|
||||
}
|
||||
|
|
@ -102,17 +119,22 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
// 校验存在
|
||||
validateStudentContactExists(studentContact.getId());
|
||||
// 更新
|
||||
studentContact.clean(); // 解决更新情况下:updateTime 不更新
|
||||
studentContactMapper.updateById(studentContact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteStudentContact(Long id) {
|
||||
// 校验存在
|
||||
validateStudentContactExists(id);
|
||||
// 删除
|
||||
studentContactMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteStudentContactListByIds(List<Long> ids) {
|
||||
// 删除
|
||||
studentContactMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfraStudentContactDO getStudentContact(Long id) {
|
||||
return studentContactMapper.selectById(id);
|
||||
|
|
@ -128,6 +150,10 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
studentContactMapper.deleteByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void deleteStudentContactByStudentIds(List<Long> studentIds) {
|
||||
studentContactMapper.deleteByStudentIds(studentIds);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
@Override
|
||||
|
|
@ -142,6 +168,7 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
throw exception(STUDENT_TEACHER_EXISTS);
|
||||
}
|
||||
// 插入
|
||||
studentTeacher.clean(); // 清理掉创建、更新时间等相关属性值
|
||||
studentTeacherMapper.insert(studentTeacher);
|
||||
return studentTeacher.getId();
|
||||
}
|
||||
|
|
@ -151,17 +178,22 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
// 校验存在
|
||||
validateStudentTeacherExists(studentTeacher.getId());
|
||||
// 更新
|
||||
studentTeacher.clean(); // 解决更新情况下:updateTime 不更新
|
||||
studentTeacherMapper.updateById(studentTeacher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteStudentTeacher(Long id) {
|
||||
// 校验存在
|
||||
validateStudentTeacherExists(id);
|
||||
// 删除
|
||||
studentTeacherMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteStudentTeacherListByIds(List<Long> ids) {
|
||||
// 删除
|
||||
studentTeacherMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InfraStudentTeacherDO getStudentTeacher(Long id) {
|
||||
return studentTeacherMapper.selectById(id);
|
||||
|
|
@ -177,4 +209,8 @@ public class InfraStudentServiceImpl implements InfraStudentService {
|
|||
studentTeacherMapper.deleteByStudentId(studentId);
|
||||
}
|
||||
|
||||
private void deleteStudentTeacherByStudentIds(List<Long> studentIds) {
|
||||
studentTeacherMapper.deleteByStudentIds(studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.infra.service.demo;
|
|||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
|
|||
import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
|
@ -22,9 +22,16 @@ public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeach
|
|||
.eq(InfraStudentTeacherDO::getStudentId, studentId)
|
||||
.orderByDesc(InfraStudentTeacherDO::getId));
|
||||
}
|
||||
default InfraStudentTeacherDO selectByStudentId(Long studentId) {
|
||||
return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentId(Long studentId) {
|
||||
return delete(InfraStudentTeacherDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentTeacherDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,55 +1,54 @@
|
|||
-- 菜单 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, component_name
|
||||
)
|
||||
VALUES (
|
||||
'学生管理', '', 2, 0, 888,
|
||||
'student', '', 'infra/demo/index', 0, 'InfraStudent'
|
||||
);
|
||||
|
||||
-- 按钮父菜单ID
|
||||
-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
|
||||
SELECT @parentId := LAST_INSERT_ID();
|
||||
|
||||
-- 按钮 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生查询', 'infra:student:query', 3, 1, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生创建', 'infra:student:create', 3, 2, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生更新', 'infra:student:update', 3, 3, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生删除', 'infra:student:delete', 3, 4, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生导出', 'infra:student:export', 3, 5, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
-- 菜单 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status, component_name
|
||||
)
|
||||
VALUES (
|
||||
'学生管理', '', 2, 0, 888,
|
||||
'student', '', 'infra/demo/index', 0, 'InfraStudent'
|
||||
);
|
||||
|
||||
-- 按钮父菜单ID
|
||||
SELECT @parentId := LAST_INSERT_ID();
|
||||
|
||||
-- 按钮 SQL
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生查询', 'infra:student:query', 3, 1, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生创建', 'infra:student:create', 3, 2, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生更新', 'infra:student:update', 3, 3, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生删除', 'infra:student:delete', 3, 4, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
INSERT INTO system_menu(
|
||||
name, permission, type, sort, parent_id,
|
||||
path, icon, component, status
|
||||
)
|
||||
VALUES (
|
||||
'学生导出', 'infra:student:export', 3, 5, @parentId,
|
||||
'', '', '', 0
|
||||
);
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace StudentApi {
|
||||
/** 学生联系人信息 */
|
||||
export interface StudentContact {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
|
||||
/** 学生班主任信息 */
|
||||
export interface StudentTeacher {
|
||||
id: number; // 编号
|
||||
studentId?: number; // 学生编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
|
||||
/** 学生信息 */
|
||||
export interface Student {
|
||||
id: number; // 编号
|
||||
name?: string; // 名字
|
||||
description?: string; // 简介
|
||||
birthday?: string | Dayjs; // 出生日期
|
||||
sex?: number; // 性别
|
||||
enabled?: boolean; // 是否有效
|
||||
avatar?: string; // 头像
|
||||
video?: string; // 附件
|
||||
memo?: string; // 备注
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询学生分页 */
|
||||
export function getStudentPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<StudentApi.Student>>('/infra/student/page', { params });
|
||||
}
|
||||
|
||||
/** 查询学生详情 */
|
||||
export function getStudent(id: number) {
|
||||
return requestClient.get<StudentApi.Student>(`/infra/student/get?id=${id}`);
|
||||
}
|
||||
|
||||
/** 新增学生 */
|
||||
export function createStudent(data: StudentApi.Student) {
|
||||
return requestClient.post('/infra/student/create', data);
|
||||
}
|
||||
|
||||
/** 修改学生 */
|
||||
export function updateStudent(data: StudentApi.Student) {
|
||||
return requestClient.put('/infra/student/update', data);
|
||||
}
|
||||
|
||||
/** 删除学生 */
|
||||
export function deleteStudent(id: number) {
|
||||
return requestClient.delete(`/infra/student/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
export function deleteStudentList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/student/delete-list?ids=${ids.join(',')}`)
|
||||
}
|
||||
|
||||
/** 导出学生 */
|
||||
export function exportStudent(params: any) {
|
||||
return requestClient.download('/infra/student/export-excel', { params });
|
||||
}
|
||||
|
||||
|
||||
// ==================== 子表(学生联系人) ====================
|
||||
|
||||
/** 获得学生联系人分页 */
|
||||
export function getStudentContactPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<StudentApi.StudentContact>>(`/infra/student/student-contact/page`, { params });
|
||||
}
|
||||
/** 新增学生联系人 */
|
||||
export function createStudentContact(data: StudentApi.StudentContact) {
|
||||
return requestClient.post(`/infra/student/student-contact/create`, data);
|
||||
}
|
||||
|
||||
/** 修改学生联系人 */
|
||||
export function updateStudentContact(data: StudentApi.StudentContact) {
|
||||
return requestClient.put(`/infra/student/student-contact/update`, data);
|
||||
}
|
||||
|
||||
/** 删除学生联系人 */
|
||||
export function deleteStudentContact(id: number) {
|
||||
return requestClient.delete(`/infra/student/student-contact/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除学生联系人 */
|
||||
export function deleteStudentContactList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/student/student-contact/delete-list?ids=${ids.join(',')}`)
|
||||
}
|
||||
|
||||
/** 获得学生联系人 */
|
||||
export function getStudentContact(id: number) {
|
||||
return requestClient.get<StudentApi.StudentContact>(`/infra/student/student-contact/get?id=${id}`);
|
||||
}
|
||||
|
||||
// ==================== 子表(学生班主任) ====================
|
||||
|
||||
/** 获得学生班主任分页 */
|
||||
export function getStudentTeacherPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<StudentApi.StudentTeacher>>(`/infra/student/student-teacher/page`, { params });
|
||||
}
|
||||
/** 新增学生班主任 */
|
||||
export function createStudentTeacher(data: StudentApi.StudentTeacher) {
|
||||
return requestClient.post(`/infra/student/student-teacher/create`, data);
|
||||
}
|
||||
|
||||
/** 修改学生班主任 */
|
||||
export function updateStudentTeacher(data: StudentApi.StudentTeacher) {
|
||||
return requestClient.put(`/infra/student/student-teacher/update`, data);
|
||||
}
|
||||
|
||||
/** 删除学生班主任 */
|
||||
export function deleteStudentTeacher(id: number) {
|
||||
return requestClient.delete(`/infra/student/student-teacher/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除学生班主任 */
|
||||
export function deleteStudentTeacherList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/student/student-teacher/delete-list?ids=${ids.join(',')}`)
|
||||
}
|
||||
|
||||
/** 获得学生班主任 */
|
||||
export function getStudentTeacher(id: number) {
|
||||
return requestClient.get<StudentApi.StudentTeacher>(`/infra/student/student-teacher/get?id=${id}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { getStudent, createStudent, updateStudent } from '#/api/infra/demo';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.Student>>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
video: [{ required: true, message: '附件不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生'])
|
||||
: $t('ui.actionTitle.create', ['学生']);
|
||||
});
|
||||
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm() {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
await formRef.value?.validate();
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = formData.value as StudentApi.Student;
|
||||
try {
|
||||
await (formData.value?.id ? updateStudent(data) : createStudent(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
let data = modalApi.getData<StudentApi.Student>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getStudent(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
formData.value = data;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { ref, h, reactive, onMounted, nextTick } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
|
||||
import { cloneDeep, downloadFileFromBlobPart, formatDateTime, isEmpty } from '@vben/utils';
|
||||
import { Button, Card, message, Tabs, Pagination, Form, RangePicker, DatePicker, Select, Input } from 'ant-design-vue';
|
||||
import StudentForm from './modules/form.vue';
|
||||
import { Download, Plus, RefreshCw, Search, Trash2 } from '@vben/icons';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
|
||||
import StudentContactList from './modules/student-contact-list.vue'
|
||||
import StudentTeacherList from './modules/student-teacher-list.vue'
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { getStudentPage, deleteStudent, deleteStudentList, exportStudent } from '#/api/infra/demo';
|
||||
|
||||
/** 子表的列表 */
|
||||
const subTabsName = ref('studentContact')
|
||||
const selectStudent = ref<StudentApi.Student>();
|
||||
async function onCellClick({ row }: { row: StudentApi.Student }) {
|
||||
selectStudent.value = row
|
||||
}
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.Student[]>([]) // 列表的数据
|
||||
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
birthday: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
createTime: undefined,
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = cloneDeep(queryParams) as any;
|
||||
if (params.birthday && Array.isArray(params.birthday)) {
|
||||
params.birthday = (params.birthday as string[]).join(',');
|
||||
}
|
||||
if (params.createTime && Array.isArray(params.createTime)) {
|
||||
params.createTime = (params.createTime as string[]).join(',');
|
||||
}
|
||||
const data = await getStudentPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: StudentForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生 */
|
||||
function handleCreate() {
|
||||
formModalApi.setData(null).open();
|
||||
}
|
||||
|
||||
/** 编辑学生 */
|
||||
function handleEdit(row: StudentApi.Student) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
|
||||
/** 删除学生 */
|
||||
async function handleDelete(row: StudentApi.Student) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudent(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生 */
|
||||
async function handleDeleteBatch() {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: StudentApi.Student[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
/** 导出表格 */
|
||||
async function handleExport() {
|
||||
try {
|
||||
exportLoading.value = true;
|
||||
const data = await exportStudent(queryParams);
|
||||
downloadFileFromBlobPart({ fileName: '学生.xls', source: data });
|
||||
}finally {
|
||||
exportLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 初始化 */
|
||||
const { hiddenSearchBar, tableToolbarRef, tableRef } = useTableToolbar();
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page auto-content-height>
|
||||
<FormModal @success="getList" />
|
||||
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="请输入名字"
|
||||
allowClear
|
||||
@pressEnter="handleQuery"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="queryParams.birthday"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="选择出生日期"
|
||||
allowClear
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select
|
||||
v-model:value="queryParams.sex"
|
||||
placeholder="请选择性别"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<Select
|
||||
v-model:value="queryParams.enabled"
|
||||
placeholder="请选择是否有效"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="创建时间" name="createTime">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.createTime"
|
||||
v-bind="getRangePickerDefaultProps()"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||
搜索
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<!-- 列表 -->
|
||||
<Card title="学生">
|
||||
<template #extra>
|
||||
<VbenVxeTableToolbar
|
||||
ref="tableToolbarRef"
|
||||
v-model:hidden-search="hiddenSearchBar"
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
:icon="h(Plus)"
|
||||
type="primary"
|
||||
@click="handleCreate"
|
||||
v-access:code="['infra:student:create']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Download)"
|
||||
type="primary"
|
||||
class="ml-2"
|
||||
:loading="exportLoading"
|
||||
@click="handleExport"
|
||||
v-access:code="['infra:student:export']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.export') }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Trash2)"
|
||||
type="primary"
|
||||
danger
|
||||
class="ml-2"
|
||||
:disabled="isEmpty(checkedIds)"
|
||||
@click="handleDeleteBatch"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</VbenVxeTableToolbar>
|
||||
</template>
|
||||
<VxeTable
|
||||
ref="tableRef"
|
||||
:data="list"
|
||||
@cell-click="onCellClick"
|
||||
:row-config="{
|
||||
keyField: 'id',
|
||||
isHover: true,
|
||||
isCurrent: true,
|
||||
}"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
@checkboxAll="handleRowCheckboxChange"
|
||||
@checkboxChange="handleRowCheckboxChange"
|
||||
>
|
||||
<VxeColumn type="checkbox" width="40" />
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{row}">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleEdit(row)"
|
||||
v-access:code="['infra:student:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
class="ml-2"
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<!-- 子表的表单 -->
|
||||
<Tabs v-model:active-key="subTabsName">
|
||||
<Tabs.TabPane key="studentContact" tab="学生联系人" force-render>
|
||||
<StudentContactList :student-id="selectStudent?.id" />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="studentTeacher" tab="学生班主任" force-render>
|
||||
<StudentTeacherList :student-id="selectStudent?.id" />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Page>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { getStudentContact, createStudentContact, updateStudentContact } from '#/api/infra/demo';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生联系人'])
|
||||
: $t('ui.actionTitle.create', ['学生联系人']);
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.StudentContact>>({
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
await formRef.value?.validate();
|
||||
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = formData.value as StudentApi.StudentContact;
|
||||
try {
|
||||
await (formData.value?.id ? updateStudentContact(data) : createStudentContact(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
let data = modalApi.getData<StudentApi.StudentContact>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getStudentContact(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
},
|
||||
});
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm(){
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
|
||||
import { reactive, ref, h, nextTick, watch, onMounted } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
|
||||
import StudentContactForm from './student-contact-form.vue'
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Button, Card, Tabs, Pagination, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, RangePicker, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
import { Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { deleteStudentContact, deleteStudentContactList, getStudentContactPage } from '#/api/infra/demo';
|
||||
import { isEmpty, cloneDeep } from '@vben/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: StudentContactForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生联系人 */
|
||||
function handleCreate() {
|
||||
if (!props.studentId){
|
||||
message.warning("请先选择一个学生!")
|
||||
return
|
||||
}
|
||||
formModalApi.setData({studentId: props.studentId}).open();
|
||||
}
|
||||
|
||||
/** 编辑学生联系人 */
|
||||
function handleEdit(row: StudentApi.StudentContact) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除学生联系人 */
|
||||
async function handleDelete(row: StudentApi.StudentContact) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentContact(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生联系人 */
|
||||
async function handleDeleteBatch() {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentContactList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: StudentApi.StudentContact[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.StudentContact[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
birthday: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
createTime: undefined,
|
||||
})
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!props.studentId){
|
||||
return []
|
||||
}
|
||||
const params = cloneDeep(queryParams) as any;
|
||||
if (params.birthday && Array.isArray(params.birthday)) {
|
||||
params.birthday = (params.birthday as string[]).join(',');
|
||||
}
|
||||
if (params.createTime && Array.isArray(params.createTime)) {
|
||||
params.createTime = (params.createTime as string[]).join(',');
|
||||
}
|
||||
params.studentId = props.studentId;
|
||||
const data = await getStudentContactPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await getList()
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
const { hiddenSearchBar, tableToolbarRef, tableRef } = useTableToolbar();
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormModal @success="getList" />
|
||||
<div class="h-[600px]">
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="请输入名字"
|
||||
allowClear
|
||||
@pressEnter="handleQuery"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="queryParams.birthday"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="选择出生日期"
|
||||
allowClear
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select
|
||||
v-model:value="queryParams.sex"
|
||||
placeholder="请选择性别"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<Select
|
||||
v-model:value="queryParams.enabled"
|
||||
placeholder="请选择是否有效"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="创建时间" name="createTime">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.createTime"
|
||||
v-bind="getRangePickerDefaultProps()"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||
搜索
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<!-- 列表 -->
|
||||
<Card title="学生">
|
||||
<template #extra>
|
||||
<VbenVxeTableToolbar
|
||||
ref="tableToolbarRef"
|
||||
v-model:hidden-search="hiddenSearchBar"
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
:icon="h(Plus)"
|
||||
type="primary"
|
||||
@click="handleCreate"
|
||||
v-access:code="['infra:student:create']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Trash2)"
|
||||
type="primary"
|
||||
danger
|
||||
class="ml-2"
|
||||
:disabled="isEmpty(checkedIds)"
|
||||
@click="handleDeleteBatch"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</VbenVxeTableToolbar>
|
||||
</template>
|
||||
<VxeTable
|
||||
ref="tableRef"
|
||||
:data="list"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
@checkboxAll="handleRowCheckboxChange"
|
||||
@checkboxChange="handleRowCheckboxChange"
|
||||
>
|
||||
<VxeColumn type="checkbox" width="40" />
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{row}">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleEdit(row)"
|
||||
v-access:code="['infra:student:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
class="ml-2"
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Rule } from 'ant-design-vue/es/form';
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Tabs, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { getStudentTeacher, createStudentTeacher, updateStudentTeacher } from '#/api/infra/demo';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['学生班主任'])
|
||||
: $t('ui.actionTitle.create', ['学生班主任']);
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const formData = ref<Partial<StudentApi.StudentTeacher>>({
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
});
|
||||
const rules: Record<string, Rule[]> = {
|
||||
studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
|
||||
description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
|
||||
birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
|
||||
enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
|
||||
avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
|
||||
memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
await formRef.value?.validate();
|
||||
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = formData.value as StudentApi.StudentTeacher;
|
||||
try {
|
||||
await (formData.value?.id ? updateStudentTeacher(data) : createStudentTeacher(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success({
|
||||
content: $t('ui.actionMessage.operationSuccess'),
|
||||
});
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetForm()
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
let data = modalApi.getData<StudentApi.StudentTeacher>();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.id) {
|
||||
modalApi.lock();
|
||||
try {
|
||||
data = await getStudentTeacher(data.id);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = data;
|
||||
},
|
||||
});
|
||||
|
||||
/** 重置表单 */
|
||||
function resetForm(){
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
avatar: undefined,
|
||||
video: undefined,
|
||||
memo: undefined,
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :title="getTitle">
|
||||
<Form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input v-model:value="formData.name" placeholder="请输入名字" />
|
||||
</Form.Item>
|
||||
<Form.Item label="简介" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入简介" />
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="formData.birthday"
|
||||
valueFormat="x"
|
||||
placeholder="选择出生日期"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select v-model:value="formData.sex" placeholder="请选择性别">
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<RadioGroup v-model:value="formData.enabled">
|
||||
<Radio
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</Form.Item>
|
||||
<Form.Item label="头像" name="avatar">
|
||||
<ImageUpload v-model:value="formData.avatar" />
|
||||
</Form.Item>
|
||||
<Form.Item label="附件" name="video">
|
||||
<FileUpload v-model:value="formData.video" />
|
||||
</Form.Item>
|
||||
<Form.Item label="备注" name="memo">
|
||||
<RichTextarea v-model="formData.memo" height="500px" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,335 @@
|
|||
<script lang="ts" setup>
|
||||
import type { StudentApi } from '#/api/infra/demo';
|
||||
import type { VxeTableInstance } from '#/adapter/vxe-table';
|
||||
|
||||
import { reactive, ref, h, nextTick, watch, onMounted } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { getRangePickerDefaultProps } from '#/utils/rangePickerProps';
|
||||
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { useTableToolbar, VbenVxeTableToolbar } from '@vben/plugins/vxe-table';
|
||||
import StudentTeacherForm from './student-teacher-form.vue'
|
||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||
import { ImageUpload, FileUpload } from "#/components/upload";
|
||||
import { message, Button, Card, Tabs, Pagination, Form, Input, Textarea, Select, RadioGroup, Radio, CheckboxGroup, Checkbox, RangePicker, DatePicker, TreeSelect } from 'ant-design-vue';
|
||||
import { Plus, Trash2 } from '@vben/icons';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { deleteStudentTeacher, deleteStudentTeacherList, getStudentTeacherPage } from '#/api/infra/demo';
|
||||
import { isEmpty, cloneDeep } from '@vben/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
studentId?: number // 学生编号(主表的关联字段)
|
||||
}>()
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: StudentTeacherForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
|
||||
/** 创建学生班主任 */
|
||||
function handleCreate() {
|
||||
if (!props.studentId){
|
||||
message.warning("请先选择一个学生!")
|
||||
return
|
||||
}
|
||||
formModalApi.setData({studentId: props.studentId}).open();
|
||||
}
|
||||
|
||||
/** 编辑学生班主任 */
|
||||
function handleEdit(row: StudentApi.StudentTeacher) {
|
||||
formModalApi.setData(row).open();
|
||||
}
|
||||
|
||||
/** 删除学生班主任 */
|
||||
async function handleDelete(row: StudentApi.StudentTeacher) {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting', [row.id]),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentTeacher(row.id!);
|
||||
message.success($t('ui.actionMessage.deleteSuccess', [row.id]));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量删除学生班主任 */
|
||||
async function handleDeleteBatch() {
|
||||
const hideLoading = message.loading({
|
||||
content: $t('ui.actionMessage.deleting'),
|
||||
duration: 0,
|
||||
});
|
||||
try {
|
||||
await deleteStudentTeacherList(checkedIds.value);
|
||||
checkedIds.value = [];
|
||||
message.success($t('ui.actionMessage.deleteSuccess'));
|
||||
await getList();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const checkedIds = ref<number[]>([])
|
||||
function handleRowCheckboxChange({
|
||||
records,
|
||||
}: {
|
||||
records: StudentApi.StudentTeacher[];
|
||||
}) {
|
||||
checkedIds.value = records.map((item) => item.id!);
|
||||
}
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<StudentApi.StudentTeacher[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
birthday: undefined,
|
||||
birthday: undefined,
|
||||
sex: undefined,
|
||||
enabled: undefined,
|
||||
createTime: undefined,
|
||||
})
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (!props.studentId){
|
||||
return []
|
||||
}
|
||||
const params = cloneDeep(queryParams) as any;
|
||||
if (params.birthday && Array.isArray(params.birthday)) {
|
||||
params.birthday = (params.birthday as string[]).join(',');
|
||||
}
|
||||
if (params.createTime && Array.isArray(params.createTime)) {
|
||||
params.createTime = (params.createTime as string[]).join(',');
|
||||
}
|
||||
params.studentId = props.studentId;
|
||||
const data = await getStudentTeacherPage(params)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听主表的关联字段的变化,加载对应的子表数据 */
|
||||
watch(
|
||||
() => props.studentId,
|
||||
async (val) => {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
await getList()
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 初始化 */
|
||||
const { hiddenSearchBar, tableToolbarRef, tableRef } = useTableToolbar();
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormModal @success="getList" />
|
||||
<div class="h-[600px]">
|
||||
<Card v-if="!hiddenSearchBar" class="mb-4">
|
||||
<!-- 搜索工作栏 -->
|
||||
<Form
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
layout="inline"
|
||||
>
|
||||
<Form.Item label="名字" name="name">
|
||||
<Input
|
||||
v-model:value="queryParams.name"
|
||||
placeholder="请输入名字"
|
||||
allowClear
|
||||
@pressEnter="handleQuery"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="出生日期" name="birthday">
|
||||
<DatePicker
|
||||
v-model:value="queryParams.birthday"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
placeholder="选择出生日期"
|
||||
allowClear
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="性别" name="sex">
|
||||
<Select
|
||||
v-model:value="queryParams.sex"
|
||||
placeholder="请选择性别"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="是否有效" name="enabled">
|
||||
<Select
|
||||
v-model:value="queryParams.enabled"
|
||||
placeholder="请选择是否有效"
|
||||
allowClear
|
||||
class="w-full"
|
||||
>
|
||||
<Select.Option
|
||||
v-for="dict in getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean')"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item label="创建时间" name="createTime">
|
||||
<RangePicker
|
||||
v-model:value="queryParams.createTime"
|
||||
v-bind="getRangePickerDefaultProps()"
|
||||
class="w-full"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button class="ml-2" @click="resetQuery"> 重置 </Button>
|
||||
<Button class="ml-2" @click="handleQuery" type="primary">
|
||||
搜索
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<!-- 列表 -->
|
||||
<Card title="学生">
|
||||
<template #extra>
|
||||
<VbenVxeTableToolbar
|
||||
ref="tableToolbarRef"
|
||||
v-model:hidden-search="hiddenSearchBar"
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
:icon="h(Plus)"
|
||||
type="primary"
|
||||
@click="handleCreate"
|
||||
v-access:code="['infra:student:create']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.create', ['学生']) }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="h(Trash2)"
|
||||
type="primary"
|
||||
danger
|
||||
class="ml-2"
|
||||
:disabled="isEmpty(checkedIds)"
|
||||
@click="handleDeleteBatch"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
批量删除
|
||||
</Button>
|
||||
</VbenVxeTableToolbar>
|
||||
</template>
|
||||
<VxeTable
|
||||
ref="tableRef"
|
||||
:data="list"
|
||||
show-overflow
|
||||
:loading="loading"
|
||||
@checkboxAll="handleRowCheckboxChange"
|
||||
@checkboxChange="handleRowCheckboxChange"
|
||||
>
|
||||
<VxeColumn type="checkbox" width="40" />
|
||||
<VxeColumn field="id" title="编号" align="center" />
|
||||
<VxeColumn field="studentId" title="学生编号" align="center" />
|
||||
<VxeColumn field="name" title="名字" align="center" />
|
||||
<VxeColumn field="description" title="简介" align="center" />
|
||||
<VxeColumn field="birthday" title="出生日期" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.birthday)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="sex" title="性别" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="enabled" title="是否有效" align="center">
|
||||
<template #default="{row}">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.enabled" />
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="avatar" title="头像" align="center" />
|
||||
<VxeColumn field="video" title="附件" align="center" />
|
||||
<VxeColumn field="memo" title="备注" align="center" />
|
||||
<VxeColumn field="createTime" title="创建时间" align="center">
|
||||
<template #default="{row}">
|
||||
{{formatDateTime(row.createTime)}}
|
||||
</template>
|
||||
</VxeColumn>
|
||||
<VxeColumn field="operation" title="操作" align="center">
|
||||
<template #default="{row}">
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleEdit(row)"
|
||||
v-access:code="['infra:student:update']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.edit') }}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="link"
|
||||
danger
|
||||
class="ml-2"
|
||||
@click="handleDelete(row)"
|
||||
v-access:code="['infra:student:delete']"
|
||||
>
|
||||
{{ $t('ui.actionTitle.delete') }}
|
||||
</Button>
|
||||
</template>
|
||||
</VxeColumn>
|
||||
</VxeTable>
|
||||
<!-- 分页 -->
|
||||
<div class="mt-2 flex justify-end">
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:current="queryParams.pageNo"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
show-size-changer
|
||||
@change="getList"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
[ {
|
||||
"contentPath" : "java/InfraStudentPageReqVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentRespVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentSaveReqVO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentController",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherDO",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentContactMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentTeacherMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
|
||||
}, {
|
||||
"contentPath" : "xml/InfraStudentMapper",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/resources/mapper/demo/InfraStudentMapper.xml"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImpl",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentService",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
|
||||
}, {
|
||||
"contentPath" : "java/InfraStudentServiceImplTest",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
|
||||
}, {
|
||||
"contentPath" : "java/ErrorCodeConstants_手动操作",
|
||||
"filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
|
||||
}, {
|
||||
"contentPath" : "sql/sql",
|
||||
"filePath" : "sql/sql.sql"
|
||||
}, {
|
||||
"contentPath" : "sql/h2",
|
||||
"filePath" : "sql/h2.sql"
|
||||
}, {
|
||||
"contentPath" : "vue/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/index.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/form.vue"
|
||||
}, {
|
||||
"contentPath" : "ts/index",
|
||||
"filePath" : "yudao-ui-admin-vben/src/api/infra/demo/index.ts"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-form",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-form.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-contact-list",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-contact-list.vue"
|
||||
}, {
|
||||
"contentPath" : "vue/student-teacher-list",
|
||||
"filePath" : "yudao-ui-admin-vben/src/views/infra/demo/modules/student-teacher-list.vue"
|
||||
} ]
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
// TODO 待办:请将下面的错误码复制到 yudao-module-infra 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
|
||||
// ========== 学生 TODO 补充编号 ==========
|
||||
ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
|
||||
|
|
@ -25,4 +25,8 @@ public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentConta
|
|||
return delete(InfraStudentContactDO::getStudentId, studentId);
|
||||
}
|
||||
|
||||
default int deleteByStudentIds(List<Long> studentIds) {
|
||||
return deleteBatch(InfraStudentContactDO::getStudentId, studentIds);
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue