feat: 优化用户导入失败反馈与模板说明
- 支持按字段返回用户导入失败明细 - 补充导入模板填写说明与名称匹配规则 - 优化管理端导入失败弹窗与错误展示
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package tech.easyflow.system.entity.vo;
|
||||
|
||||
/**
|
||||
* 用户导入失败明细。
|
||||
*/
|
||||
public class SysAccountImportErrorDetailVo {
|
||||
|
||||
private String fieldName;
|
||||
|
||||
private String fieldValue;
|
||||
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 获取问题字段名。
|
||||
*
|
||||
* @return 字段名
|
||||
*/
|
||||
public String getFieldName() {
|
||||
return fieldName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置问题字段名。
|
||||
*
|
||||
* @param fieldName 字段名
|
||||
*/
|
||||
public void setFieldName(String fieldName) {
|
||||
this.fieldName = fieldName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户填写值。
|
||||
*
|
||||
* @return 用户填写值
|
||||
*/
|
||||
public String getFieldValue() {
|
||||
return fieldValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户填写值。
|
||||
*
|
||||
* @param fieldValue 用户填写值
|
||||
*/
|
||||
public void setFieldValue(String fieldValue) {
|
||||
this.fieldValue = fieldValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取失败原因。
|
||||
*
|
||||
* @return 失败原因
|
||||
*/
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置失败原因。
|
||||
*
|
||||
* @param reason 失败原因
|
||||
*/
|
||||
public void setReason(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package tech.easyflow.system.entity.vo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户导入失败行。
|
||||
*/
|
||||
@@ -7,41 +10,141 @@ public class SysAccountImportErrorRowVo {
|
||||
|
||||
private Integer rowNumber;
|
||||
|
||||
private String deptCode;
|
||||
private String deptName;
|
||||
|
||||
private String loginName;
|
||||
|
||||
private String reason;
|
||||
private String nickname;
|
||||
|
||||
private String roleNames;
|
||||
|
||||
private String positionNames;
|
||||
|
||||
private List<SysAccountImportErrorDetailVo> details = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 获取行号。
|
||||
*
|
||||
* @return 行号
|
||||
*/
|
||||
public Integer getRowNumber() {
|
||||
return rowNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置行号。
|
||||
*
|
||||
* @param rowNumber 行号
|
||||
*/
|
||||
public void setRowNumber(Integer rowNumber) {
|
||||
this.rowNumber = rowNumber;
|
||||
}
|
||||
|
||||
public String getDeptCode() {
|
||||
return deptCode;
|
||||
/**
|
||||
* 获取部门名称。
|
||||
*
|
||||
* @return 部门名称
|
||||
*/
|
||||
public String getDeptName() {
|
||||
return deptName;
|
||||
}
|
||||
|
||||
public void setDeptCode(String deptCode) {
|
||||
this.deptCode = deptCode;
|
||||
/**
|
||||
* 设置部门名称。
|
||||
*
|
||||
* @param deptName 部门名称
|
||||
*/
|
||||
public void setDeptName(String deptName) {
|
||||
this.deptName = deptName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录账号。
|
||||
*
|
||||
* @return 登录账号
|
||||
*/
|
||||
public String getLoginName() {
|
||||
return loginName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置登录账号。
|
||||
*
|
||||
* @param loginName 登录账号
|
||||
*/
|
||||
public void setLoginName(String loginName) {
|
||||
this.loginName = loginName;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
/**
|
||||
* 获取昵称。
|
||||
*
|
||||
* @return 昵称
|
||||
*/
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public void setReason(String reason) {
|
||||
this.reason = reason;
|
||||
/**
|
||||
* 设置昵称。
|
||||
*
|
||||
* @param nickname 昵称
|
||||
*/
|
||||
public void setNickname(String nickname) {
|
||||
this.nickname = nickname;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色名称集合文本。
|
||||
*
|
||||
* @return 角色名称集合文本
|
||||
*/
|
||||
public String getRoleNames() {
|
||||
return roleNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置角色名称集合文本。
|
||||
*
|
||||
* @param roleNames 角色名称集合文本
|
||||
*/
|
||||
public void setRoleNames(String roleNames) {
|
||||
this.roleNames = roleNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取岗位名称集合文本。
|
||||
*
|
||||
* @return 岗位名称集合文本
|
||||
*/
|
||||
public String getPositionNames() {
|
||||
return positionNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置岗位名称集合文本。
|
||||
*
|
||||
* @param positionNames 岗位名称集合文本
|
||||
*/
|
||||
public void setPositionNames(String positionNames) {
|
||||
this.positionNames = positionNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取失败明细。
|
||||
*
|
||||
* @return 失败明细
|
||||
*/
|
||||
public List<SysAccountImportErrorDetailVo> getDetails() {
|
||||
return details;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置失败明细。
|
||||
*
|
||||
* @param details 失败明细
|
||||
*/
|
||||
public void setDetails(List<SysAccountImportErrorDetailVo> details) {
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package tech.easyflow.system.service.impl;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import cn.idev.excel.EasyExcel;
|
||||
import cn.idev.excel.ExcelWriter;
|
||||
import cn.idev.excel.FastExcel;
|
||||
import cn.idev.excel.context.AnalysisContext;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.read.listener.ReadListener;
|
||||
import cn.idev.excel.write.metadata.WriteSheet;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -28,6 +30,7 @@ import tech.easyflow.system.entity.SysPosition;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.entity.vo.SysAccountBatchActionErrorItemVo;
|
||||
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo;
|
||||
import tech.easyflow.system.entity.vo.SysAccountImportErrorDetailVo;
|
||||
import tech.easyflow.system.entity.vo.SysAccountImportErrorRowVo;
|
||||
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
||||
import tech.easyflow.system.mapper.SysAccountMapper;
|
||||
@@ -53,6 +56,7 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 用户表 服务层实现。
|
||||
@@ -69,15 +73,27 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
private static final String DEFAULT_RESET_PASSWORD = "123456";
|
||||
private static final long MAX_IMPORT_FILE_SIZE_BYTES = 10L * 1024 * 1024;
|
||||
private static final int MAX_IMPORT_ROWS = 5000;
|
||||
private static final String IMPORT_HEAD_DEPT_CODE = "部门编码";
|
||||
private static final String IMPORT_HEAD_LOGIN_NAME = "登录账号";
|
||||
private static final String IMPORT_HEAD_NICKNAME = "昵称";
|
||||
private static final String IMPORT_HEAD_DEPT_NAME = "部门名称*";
|
||||
private static final String IMPORT_HEAD_LOGIN_NAME = "登录账号*";
|
||||
private static final String IMPORT_HEAD_NICKNAME = "昵称*";
|
||||
private static final String IMPORT_HEAD_MOBILE = "手机号";
|
||||
private static final String IMPORT_HEAD_EMAIL = "邮箱";
|
||||
private static final String IMPORT_HEAD_STATUS = "状态";
|
||||
private static final String IMPORT_HEAD_ROLE_KEYS = "角色编码";
|
||||
private static final String IMPORT_HEAD_POSITION_CODES = "岗位编码";
|
||||
private static final String IMPORT_HEAD_ROLE_NAMES = "角色名称";
|
||||
private static final String IMPORT_HEAD_POSITION_NAMES = "岗位名称";
|
||||
private static final String IMPORT_HEAD_REMARK = "备注";
|
||||
private static final String IMPORT_GUIDE_HEAD_ITEM = "说明项";
|
||||
private static final String IMPORT_GUIDE_HEAD_CONTENT = "内容";
|
||||
private static final String IMPORT_FIELD_DEPT_NAME = "部门名称";
|
||||
private static final String IMPORT_FIELD_LOGIN_NAME = "登录账号";
|
||||
private static final String IMPORT_FIELD_NICKNAME = "昵称";
|
||||
private static final String IMPORT_FIELD_MOBILE = "手机号";
|
||||
private static final String IMPORT_FIELD_EMAIL = "邮箱";
|
||||
private static final String IMPORT_FIELD_STATUS = "状态";
|
||||
private static final String IMPORT_FIELD_ROLE_NAME = "角色名称";
|
||||
private static final String IMPORT_FIELD_POSITION_NAME = "岗位名称";
|
||||
private static final String IMPORT_FIELD_SYSTEM = "系统";
|
||||
private static final String IMPORT_ERROR_DUPLICATED_RESOURCE = "存在重名,请先在管理端处理";
|
||||
|
||||
@Resource
|
||||
private SysAccountRoleMapper sysAccountRoleMapper;
|
||||
@@ -244,16 +260,25 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<String, SysDept> deptMap = buildDeptCodeMap();
|
||||
Map<String, SysRole> roleMap = buildRoleKeyMap();
|
||||
Map<String, SysPosition> positionMap = buildPositionCodeMap();
|
||||
ImportNameLookup<SysDept> deptLookup = buildImportNameLookup(
|
||||
sysDeptMapper.selectListByQuery(QueryWrapper.create()),
|
||||
SysDept::getDeptName
|
||||
);
|
||||
ImportNameLookup<SysRole> roleLookup = buildImportNameLookup(
|
||||
sysRoleMapper.selectListByQuery(QueryWrapper.create()),
|
||||
SysRole::getRoleName
|
||||
);
|
||||
ImportNameLookup<SysPosition> positionLookup = buildImportNameLookup(
|
||||
sysPositionMapper.selectListByQuery(QueryWrapper.create()),
|
||||
SysPosition::getPositionName
|
||||
);
|
||||
for (SysAccountImportRow row : rows) {
|
||||
try {
|
||||
executeInRowTransaction(() -> importSingleRow(row, loginAccount, deptMap, roleMap, positionMap));
|
||||
executeInRowTransaction(() -> importSingleRow(row, loginAccount, deptLookup, roleLookup, positionLookup));
|
||||
result.setSuccessCount(result.getSuccessCount() + 1);
|
||||
} catch (Exception e) {
|
||||
result.setErrorCount(result.getErrorCount() + 1);
|
||||
appendImportError(result, row, extractImportErrorMessage(e));
|
||||
appendImportError(result, row, e);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -261,10 +286,20 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
|
||||
@Override
|
||||
public void writeImportTemplate(OutputStream outputStream) {
|
||||
EasyExcel.write(outputStream)
|
||||
ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
|
||||
try {
|
||||
WriteSheet templateSheet = EasyExcel.writerSheet("模板")
|
||||
.head(buildImportHeadList())
|
||||
.sheet("模板")
|
||||
.doWrite(new ArrayList<>());
|
||||
.build();
|
||||
excelWriter.write(new ArrayList<>(), templateSheet);
|
||||
|
||||
WriteSheet guideSheet = EasyExcel.writerSheet("填写说明")
|
||||
.head(buildImportGuideHeadList())
|
||||
.build();
|
||||
excelWriter.write(buildImportGuideRows(), guideSheet);
|
||||
} finally {
|
||||
excelWriter.finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void executeInRowTransaction(Runnable action) {
|
||||
@@ -281,29 +316,54 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
|
||||
private void importSingleRow(SysAccountImportRow row,
|
||||
LoginAccount loginAccount,
|
||||
Map<String, SysDept> deptMap,
|
||||
Map<String, SysRole> roleMap,
|
||||
Map<String, SysPosition> positionMap) {
|
||||
String deptCode = trimToNull(row.getDeptCode());
|
||||
ImportNameLookup<SysDept> deptLookup,
|
||||
ImportNameLookup<SysRole> roleLookup,
|
||||
ImportNameLookup<SysPosition> positionLookup) {
|
||||
List<SysAccountImportErrorDetailVo> details = new ArrayList<>();
|
||||
String deptName = trimToNull(row.getDeptName());
|
||||
String loginName = trimToNull(row.getLoginName());
|
||||
String nickname = trimToNull(row.getNickname());
|
||||
if (deptCode == null) {
|
||||
throw new BusinessException("部门编码不能为空");
|
||||
String mobile = trimToNull(row.getMobile());
|
||||
String email = trimToNull(row.getEmail());
|
||||
|
||||
if (deptName == null) {
|
||||
addImportDetail(details, IMPORT_FIELD_DEPT_NAME, row.getDeptName(), "部门名称不能为空");
|
||||
}
|
||||
if (loginName == null) {
|
||||
throw new BusinessException("登录账号不能为空");
|
||||
addImportDetail(details, IMPORT_FIELD_LOGIN_NAME, row.getLoginName(), "登录账号不能为空");
|
||||
} else if (isLoginNameExists(loginName)) {
|
||||
addImportDetail(details, IMPORT_FIELD_LOGIN_NAME, row.getLoginName(), "登录账号已存在");
|
||||
}
|
||||
if (nickname == null) {
|
||||
throw new BusinessException("昵称不能为空");
|
||||
addImportDetail(details, IMPORT_FIELD_NICKNAME, row.getNickname(), "昵称不能为空");
|
||||
}
|
||||
SysDept dept = deptMap.get(deptCode);
|
||||
if (dept == null) {
|
||||
throw new BusinessException("部门编码不存在: " + deptCode);
|
||||
if (mobile != null && !StringUtil.isMobileNumber(mobile)) {
|
||||
addImportDetail(details, IMPORT_FIELD_MOBILE, row.getMobile(), "手机号格式不正确");
|
||||
}
|
||||
if (email != null && !StringUtil.isEmail(email)) {
|
||||
addImportDetail(details, IMPORT_FIELD_EMAIL, row.getEmail(), "邮箱格式不正确");
|
||||
}
|
||||
ensureLoginNameNotExists(loginName);
|
||||
|
||||
List<BigInteger> roleIds = resolveRoleIds(row.getRoleKeys(), roleMap);
|
||||
List<BigInteger> positionIds = resolvePositionIds(row.getPositionCodes(), positionMap);
|
||||
SysDept dept = resolveSingleResource(deptName, deptLookup, IMPORT_FIELD_DEPT_NAME, details);
|
||||
Integer status = parseStatus(row.getStatus(), details);
|
||||
List<BigInteger> roleIds = resolveResourceIds(
|
||||
row.getRoleNames(),
|
||||
roleLookup,
|
||||
SysRole::getId,
|
||||
IMPORT_FIELD_ROLE_NAME,
|
||||
details
|
||||
);
|
||||
List<BigInteger> positionIds = resolveResourceIds(
|
||||
row.getPositionNames(),
|
||||
positionLookup,
|
||||
SysPosition::getId,
|
||||
IMPORT_FIELD_POSITION_NAME,
|
||||
details
|
||||
);
|
||||
|
||||
if (!details.isEmpty()) {
|
||||
throw new ImportRowValidationException(details);
|
||||
}
|
||||
|
||||
SysAccount entity = new SysAccount();
|
||||
entity.setDeptId(dept.getId());
|
||||
@@ -313,9 +373,9 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
entity.setPasswordResetRequired(true);
|
||||
entity.setAccountType(EnumAccountType.NORMAL.getCode());
|
||||
entity.setNickname(nickname);
|
||||
entity.setMobile(trimToNull(row.getMobile()));
|
||||
entity.setEmail(trimToNull(row.getEmail()));
|
||||
entity.setStatus(parseStatus(row.getStatus()));
|
||||
entity.setMobile(mobile);
|
||||
entity.setEmail(email);
|
||||
entity.setStatus(status);
|
||||
entity.setRemark(trimToNull(row.getRemark()));
|
||||
entity.setCreated(new Date());
|
||||
entity.setCreatedBy(loginAccount.getId());
|
||||
@@ -327,15 +387,26 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
syncRelations(entity);
|
||||
}
|
||||
|
||||
private void ensureLoginNameNotExists(String loginName) {
|
||||
/**
|
||||
* 校验登录账号是否已存在。
|
||||
*
|
||||
* @param loginName 登录账号
|
||||
* @return 是否已存在
|
||||
*/
|
||||
private boolean isLoginNameExists(String loginName) {
|
||||
QueryWrapper wrapper = QueryWrapper.create();
|
||||
wrapper.eq(SysAccount::getLoginName, loginName);
|
||||
if (count(wrapper) > 0) {
|
||||
throw new BusinessException("登录账号已存在: " + loginName);
|
||||
}
|
||||
return count(wrapper) > 0;
|
||||
}
|
||||
|
||||
private Integer parseStatus(String rawStatus) {
|
||||
/**
|
||||
* 解析导入状态列。
|
||||
*
|
||||
* @param rawStatus 原始状态文本
|
||||
* @param details 错误明细收集器
|
||||
* @return 解析后的状态编码
|
||||
*/
|
||||
private Integer parseStatus(String rawStatus, List<SysAccountImportErrorDetailVo> details) {
|
||||
String status = trimToNull(rawStatus);
|
||||
if (status == null) {
|
||||
return EnumDataStatus.AVAILABLE.getCode();
|
||||
@@ -346,39 +417,38 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
if ("0".equals(status) || "未启用".equals(status) || "停用".equals(status) || "禁用".equals(status)) {
|
||||
return EnumDataStatus.UNAVAILABLE.getCode();
|
||||
}
|
||||
throw new BusinessException("状态不合法,仅支持 1/0 或 已启用/未启用");
|
||||
addImportDetail(details, IMPORT_FIELD_STATUS, rawStatus, "状态不合法,仅支持 1/0/已启用/启用/未启用/停用/禁用");
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<BigInteger> resolveRoleIds(String roleKeysText, Map<String, SysRole> roleMap) {
|
||||
List<String> roleKeys = splitCodes(roleKeysText);
|
||||
if (roleKeys.isEmpty()) {
|
||||
/**
|
||||
* 解析名称列表并映射为主键集合。
|
||||
*
|
||||
* @param rawNames 原始名称文本
|
||||
* @param lookup 名称查找表
|
||||
* @param fieldName 字段名称
|
||||
* @param details 错误明细收集器
|
||||
* @param <T> 资源类型
|
||||
* @return 匹配到的主键集合
|
||||
*/
|
||||
private <T> List<BigInteger> resolveResourceIds(
|
||||
String rawNames,
|
||||
ImportNameLookup<T> lookup,
|
||||
Function<T, BigInteger> idExtractor,
|
||||
String fieldName,
|
||||
List<SysAccountImportErrorDetailVo> details) {
|
||||
List<String> names = splitCodes(rawNames);
|
||||
if (names.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<BigInteger> roleIds = new ArrayList<>(roleKeys.size());
|
||||
for (String roleKey : roleKeys) {
|
||||
SysRole role = roleMap.get(roleKey);
|
||||
if (role == null) {
|
||||
throw new BusinessException("角色编码不存在: " + roleKey);
|
||||
List<BigInteger> ids = new ArrayList<>(names.size());
|
||||
for (String name : names) {
|
||||
T resource = resolveSingleResource(name, lookup, fieldName, details);
|
||||
if (resource != null) {
|
||||
ids.add(idExtractor.apply(resource));
|
||||
}
|
||||
roleIds.add(role.getId());
|
||||
}
|
||||
return roleIds;
|
||||
}
|
||||
|
||||
private List<BigInteger> resolvePositionIds(String positionCodesText, Map<String, SysPosition> positionMap) {
|
||||
List<String> positionCodes = splitCodes(positionCodesText);
|
||||
if (positionCodes.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<BigInteger> positionIds = new ArrayList<>(positionCodes.size());
|
||||
for (String positionCode : positionCodes) {
|
||||
SysPosition position = positionMap.get(positionCode);
|
||||
if (position == null) {
|
||||
throw new BusinessException("岗位编码不存在: " + positionCode);
|
||||
}
|
||||
positionIds.add(position.getId());
|
||||
}
|
||||
return positionIds;
|
||||
return ids;
|
||||
}
|
||||
|
||||
private List<String> splitCodes(String rawCodes) {
|
||||
@@ -398,40 +468,32 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, SysDept> buildDeptCodeMap() {
|
||||
List<SysDept> deptList = sysDeptMapper.selectListByQuery(QueryWrapper.create());
|
||||
Map<String, SysDept> deptMap = new HashMap<>();
|
||||
for (SysDept dept : deptList) {
|
||||
String deptCode = trimToNull(dept.getDeptCode());
|
||||
if (deptCode != null) {
|
||||
deptMap.putIfAbsent(deptCode, dept);
|
||||
/**
|
||||
* 构建导入名称查找表,并标记重名数据。
|
||||
*
|
||||
* @param resources 资源集合
|
||||
* @param nameExtractor 名称提取器
|
||||
* @param <T> 资源类型
|
||||
* @return 名称查找表
|
||||
*/
|
||||
private <T> ImportNameLookup<T> buildImportNameLookup(List<T> resources, Function<T, String> nameExtractor) {
|
||||
Map<String, T> uniqueMap = new HashMap<>();
|
||||
Set<String> duplicateNames = new LinkedHashSet<>();
|
||||
for (T resource : resources) {
|
||||
String name = trimToNull(nameExtractor.apply(resource));
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
if (uniqueMap.containsKey(name)) {
|
||||
duplicateNames.add(name);
|
||||
uniqueMap.remove(name);
|
||||
continue;
|
||||
}
|
||||
if (!duplicateNames.contains(name)) {
|
||||
uniqueMap.put(name, resource);
|
||||
}
|
||||
}
|
||||
return deptMap;
|
||||
}
|
||||
|
||||
private Map<String, SysRole> buildRoleKeyMap() {
|
||||
List<SysRole> roleList = sysRoleMapper.selectListByQuery(QueryWrapper.create());
|
||||
Map<String, SysRole> roleMap = new HashMap<>();
|
||||
for (SysRole role : roleList) {
|
||||
String roleKey = trimToNull(role.getRoleKey());
|
||||
if (roleKey != null) {
|
||||
roleMap.putIfAbsent(roleKey, role);
|
||||
}
|
||||
}
|
||||
return roleMap;
|
||||
}
|
||||
|
||||
private Map<String, SysPosition> buildPositionCodeMap() {
|
||||
List<SysPosition> positionList = sysPositionMapper.selectListByQuery(QueryWrapper.create());
|
||||
Map<String, SysPosition> positionMap = new HashMap<>();
|
||||
for (SysPosition position : positionList) {
|
||||
String positionCode = trimToNull(position.getPositionCode());
|
||||
if (positionCode != null) {
|
||||
positionMap.putIfAbsent(positionCode, position);
|
||||
}
|
||||
}
|
||||
return positionMap;
|
||||
return new ImportNameLookup<>(uniqueMap, duplicateNames);
|
||||
}
|
||||
|
||||
private List<SysAccountImportRow> parseImportRows(MultipartFile file) {
|
||||
@@ -462,12 +524,21 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
}
|
||||
}
|
||||
|
||||
private void appendImportError(SysAccountImportResultVo result, SysAccountImportRow row, String reason) {
|
||||
private void appendImportError(SysAccountImportResultVo result, SysAccountImportRow row, Exception exception) {
|
||||
SysAccountImportErrorRowVo errorRow = new SysAccountImportErrorRowVo();
|
||||
errorRow.setRowNumber(row.getRowNumber());
|
||||
errorRow.setDeptCode(row.getDeptCode());
|
||||
errorRow.setDeptName(row.getDeptName());
|
||||
errorRow.setLoginName(row.getLoginName());
|
||||
errorRow.setReason(reason);
|
||||
errorRow.setNickname(row.getNickname());
|
||||
errorRow.setRoleNames(row.getRoleNames());
|
||||
errorRow.setPositionNames(row.getPositionNames());
|
||||
if (exception instanceof ImportRowValidationException validationException) {
|
||||
errorRow.setDetails(validationException.getDetails());
|
||||
} else {
|
||||
List<SysAccountImportErrorDetailVo> details = new ArrayList<>(1);
|
||||
addImportDetail(details, IMPORT_FIELD_SYSTEM, null, extractImportErrorMessage(exception));
|
||||
errorRow.setDetails(details);
|
||||
}
|
||||
result.getErrorRows().add(errorRow);
|
||||
}
|
||||
|
||||
@@ -537,18 +608,69 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
|
||||
private List<List<String>> buildImportHeadList() {
|
||||
List<List<String>> headList = new ArrayList<>(9);
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_DEPT_CODE));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_DEPT_NAME));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_LOGIN_NAME));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_NICKNAME));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_MOBILE));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_EMAIL));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_STATUS));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_ROLE_KEYS));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_POSITION_CODES));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_ROLE_NAMES));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_POSITION_NAMES));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_REMARK));
|
||||
return headList;
|
||||
}
|
||||
|
||||
private List<List<String>> buildImportGuideHeadList() {
|
||||
List<List<String>> headList = new ArrayList<>(2);
|
||||
headList.add(Collections.singletonList(IMPORT_GUIDE_HEAD_ITEM));
|
||||
headList.add(Collections.singletonList(IMPORT_GUIDE_HEAD_CONTENT));
|
||||
return headList;
|
||||
}
|
||||
|
||||
private List<List<String>> buildImportGuideRows() {
|
||||
List<List<String>> rows = new ArrayList<>();
|
||||
rows.add(List.of("填写规则", "请按名称填写部门、角色、岗位。"));
|
||||
rows.add(List.of("必填字段", "部门名称*、登录账号*、昵称*"));
|
||||
rows.add(List.of("可选字段", "手机号、邮箱、状态、角色名称、岗位名称、备注"));
|
||||
rows.add(List.of("状态可选值", "可留空,或填写 1/0/已启用/启用/未启用/停用/禁用"));
|
||||
rows.add(List.of("多值分隔", "角色名称、岗位名称支持使用英文逗号,或中文逗号,分隔多个名称"));
|
||||
rows.add(List.of("导入后初始密码", "导入成功的账号默认密码为 123456,首次登录需要修改密码"));
|
||||
rows.add(List.of("示例行", "市场部 | zhangsan | 张三 | 13800138000 | zhangsan@example.com | 已启用 | 普通员工,审批专员 | 产品经理 | 示例导入"));
|
||||
return rows;
|
||||
}
|
||||
|
||||
private void addImportDetail(List<SysAccountImportErrorDetailVo> details,
|
||||
String fieldName,
|
||||
String fieldValue,
|
||||
String reason) {
|
||||
SysAccountImportErrorDetailVo detail = new SysAccountImportErrorDetailVo();
|
||||
detail.setFieldName(fieldName);
|
||||
detail.setFieldValue(trimToNull(fieldValue));
|
||||
detail.setReason(reason);
|
||||
details.add(detail);
|
||||
}
|
||||
|
||||
private <T> T resolveSingleResource(
|
||||
String rawName,
|
||||
ImportNameLookup<T> lookup,
|
||||
String fieldName,
|
||||
List<SysAccountImportErrorDetailVo> details) {
|
||||
String name = trimToNull(rawName);
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
if (lookup.getDuplicateNames().contains(name)) {
|
||||
addImportDetail(details, fieldName, rawName, fieldName + IMPORT_ERROR_DUPLICATED_RESOURCE);
|
||||
return null;
|
||||
}
|
||||
T resource = lookup.getUniqueMap().get(name);
|
||||
if (resource == null) {
|
||||
addImportDetail(details, fieldName, rawName, fieldName + "不存在");
|
||||
return null;
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (!StringUtil.hasText(value)) {
|
||||
return null;
|
||||
@@ -558,14 +680,14 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
|
||||
private static class SysAccountImportRow {
|
||||
private Integer rowNumber;
|
||||
private String deptCode;
|
||||
private String deptName;
|
||||
private String loginName;
|
||||
private String nickname;
|
||||
private String mobile;
|
||||
private String email;
|
||||
private String status;
|
||||
private String roleKeys;
|
||||
private String positionCodes;
|
||||
private String roleNames;
|
||||
private String positionNames;
|
||||
private String remark;
|
||||
|
||||
public Integer getRowNumber() {
|
||||
@@ -576,12 +698,12 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
this.rowNumber = rowNumber;
|
||||
}
|
||||
|
||||
public String getDeptCode() {
|
||||
return deptCode;
|
||||
public String getDeptName() {
|
||||
return deptName;
|
||||
}
|
||||
|
||||
public void setDeptCode(String deptCode) {
|
||||
this.deptCode = deptCode;
|
||||
public void setDeptName(String deptName) {
|
||||
this.deptName = deptName;
|
||||
}
|
||||
|
||||
public String getLoginName() {
|
||||
@@ -624,20 +746,20 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getRoleKeys() {
|
||||
return roleKeys;
|
||||
public String getRoleNames() {
|
||||
return roleNames;
|
||||
}
|
||||
|
||||
public void setRoleKeys(String roleKeys) {
|
||||
this.roleKeys = roleKeys;
|
||||
public void setRoleNames(String roleNames) {
|
||||
this.roleNames = roleNames;
|
||||
}
|
||||
|
||||
public String getPositionCodes() {
|
||||
return positionCodes;
|
||||
public String getPositionNames() {
|
||||
return positionNames;
|
||||
}
|
||||
|
||||
public void setPositionCodes(String positionCodes) {
|
||||
this.positionCodes = positionCodes;
|
||||
public void setPositionNames(String positionNames) {
|
||||
this.positionNames = positionNames;
|
||||
}
|
||||
|
||||
public String getRemark() {
|
||||
@@ -658,23 +780,23 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
@Override
|
||||
public void invoke(LinkedHashMap<Integer, Object> data, AnalysisContext context) {
|
||||
sheetRowNo++;
|
||||
String deptCode = getCellValue(data, IMPORT_HEAD_DEPT_CODE);
|
||||
String deptName = getCellValue(data, IMPORT_HEAD_DEPT_NAME);
|
||||
String loginName = getCellValue(data, IMPORT_HEAD_LOGIN_NAME);
|
||||
String nickname = getCellValue(data, IMPORT_HEAD_NICKNAME);
|
||||
String mobile = getCellValue(data, IMPORT_HEAD_MOBILE);
|
||||
String email = getCellValue(data, IMPORT_HEAD_EMAIL);
|
||||
String status = getCellValue(data, IMPORT_HEAD_STATUS);
|
||||
String roleKeys = getCellValue(data, IMPORT_HEAD_ROLE_KEYS);
|
||||
String positionCodes = getCellValue(data, IMPORT_HEAD_POSITION_CODES);
|
||||
String roleNames = getCellValue(data, IMPORT_HEAD_ROLE_NAMES);
|
||||
String positionNames = getCellValue(data, IMPORT_HEAD_POSITION_NAMES);
|
||||
String remark = getCellValue(data, IMPORT_HEAD_REMARK);
|
||||
if (!StringUtil.hasText(deptCode)
|
||||
if (!StringUtil.hasText(deptName)
|
||||
&& !StringUtil.hasText(loginName)
|
||||
&& !StringUtil.hasText(nickname)
|
||||
&& !StringUtil.hasText(mobile)
|
||||
&& !StringUtil.hasText(email)
|
||||
&& !StringUtil.hasText(status)
|
||||
&& !StringUtil.hasText(roleKeys)
|
||||
&& !StringUtil.hasText(positionCodes)
|
||||
&& !StringUtil.hasText(roleNames)
|
||||
&& !StringUtil.hasText(positionNames)
|
||||
&& !StringUtil.hasText(remark)) {
|
||||
return;
|
||||
}
|
||||
@@ -683,14 +805,14 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
}
|
||||
SysAccountImportRow row = new SysAccountImportRow();
|
||||
row.setRowNumber(sheetRowNo + 1);
|
||||
row.setDeptCode(deptCode);
|
||||
row.setDeptName(deptName);
|
||||
row.setLoginName(loginName);
|
||||
row.setNickname(nickname);
|
||||
row.setMobile(mobile);
|
||||
row.setEmail(email);
|
||||
row.setStatus(status);
|
||||
row.setRoleKeys(roleKeys);
|
||||
row.setPositionCodes(positionCodes);
|
||||
row.setRoleNames(roleNames);
|
||||
row.setPositionNames(positionNames);
|
||||
row.setRemark(remark);
|
||||
rows.add(row);
|
||||
}
|
||||
@@ -705,15 +827,9 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
}
|
||||
}
|
||||
List<String> requiredHeads = List.of(
|
||||
IMPORT_HEAD_DEPT_CODE,
|
||||
IMPORT_HEAD_DEPT_NAME,
|
||||
IMPORT_HEAD_LOGIN_NAME,
|
||||
IMPORT_HEAD_NICKNAME,
|
||||
IMPORT_HEAD_MOBILE,
|
||||
IMPORT_HEAD_EMAIL,
|
||||
IMPORT_HEAD_STATUS,
|
||||
IMPORT_HEAD_ROLE_KEYS,
|
||||
IMPORT_HEAD_POSITION_CODES,
|
||||
IMPORT_HEAD_REMARK
|
||||
IMPORT_HEAD_NICKNAME
|
||||
);
|
||||
for (String requiredHead : requiredHeads) {
|
||||
if (!headIndex.containsKey(requiredHead)) {
|
||||
@@ -740,4 +856,35 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
return value == null ? null : String.valueOf(value).trim();
|
||||
}
|
||||
}
|
||||
|
||||
private static class ImportNameLookup<T> {
|
||||
private final Map<String, T> uniqueMap;
|
||||
private final Set<String> duplicateNames;
|
||||
|
||||
private ImportNameLookup(Map<String, T> uniqueMap, Set<String> duplicateNames) {
|
||||
this.uniqueMap = uniqueMap;
|
||||
this.duplicateNames = duplicateNames;
|
||||
}
|
||||
|
||||
public Map<String, T> getUniqueMap() {
|
||||
return uniqueMap;
|
||||
}
|
||||
|
||||
public Set<String> getDuplicateNames() {
|
||||
return duplicateNames;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ImportRowValidationException extends RuntimeException {
|
||||
private final List<SysAccountImportErrorDetailVo> details;
|
||||
|
||||
private ImportRowValidationException(List<SysAccountImportErrorDetailVo> details) {
|
||||
super("导入数据校验失败");
|
||||
this.details = details;
|
||||
}
|
||||
|
||||
public List<SysAccountImportErrorDetailVo> getDetails() {
|
||||
return details;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,15 +45,23 @@
|
||||
"batchResetPasswordAllFailed": "Batch password reset failed",
|
||||
"importTitle": "Import Users",
|
||||
"importUploadTitle": "Drag the Excel file here, or click to select a file",
|
||||
"importUploadDesc": "Only .xlsx / .xls files are supported. Import only creates users and duplicate accounts will fail.",
|
||||
"importUploadDesc": "Only .xlsx / .xls files are supported. Fill department, role, and position by name. Headers marked with * are required.",
|
||||
"importSelectFileRequired": "Please select a file to import",
|
||||
"downloadTemplate": "Download Template",
|
||||
"importFinished": "User import completed",
|
||||
"importPartialSuccess": "Import completed. {successCount} succeeded and {errorCount} failed. See the details below.",
|
||||
"importAllFailed": "Import failed. See the details below.",
|
||||
"importResultTitle": "Import Result",
|
||||
"importTotalCount": "Total",
|
||||
"importSuccessCount": "Success",
|
||||
"importErrorCount": "Failed",
|
||||
"importRowNumber": "Row",
|
||||
"importDeptCode": "Dept Code",
|
||||
"importReason": "Reason"
|
||||
"importFieldName": "Field",
|
||||
"importFieldValue": "Value",
|
||||
"importReason": "Reason",
|
||||
"importGuideTitle": "Instructions",
|
||||
"importGuideNameRule": "Enter department, role, and position by name.",
|
||||
"importGuideRequired": "Only department name, login name, and nickname are required. Position, mobile, and email are optional.",
|
||||
"importGuideMultiValue": "Role and position accept multiple names separated by commas."
|
||||
}
|
||||
|
||||
@@ -46,15 +46,23 @@
|
||||
"batchResetPasswordAllFailed": "批量重置密码失败",
|
||||
"importTitle": "导入用户",
|
||||
"importUploadTitle": "拖拽 Excel 文件到此处,或点击选择文件",
|
||||
"importUploadDesc": "仅支持 .xlsx / .xls,导入只新增用户,重复账号会报错",
|
||||
"importUploadDesc": "仅支持 .xlsx / .xls。请按名称填写部门、角色、岗位,模板中的 * 为必填项。",
|
||||
"importSelectFileRequired": "请先选择要导入的文件",
|
||||
"downloadTemplate": "下载导入模板",
|
||||
"importFinished": "用户导入完成",
|
||||
"importPartialSuccess": "导入完成,成功 {successCount} 条,失败 {errorCount} 条,请查看下方明细",
|
||||
"importAllFailed": "导入失败,请查看下方明细",
|
||||
"importResultTitle": "导入结果",
|
||||
"importTotalCount": "总条数",
|
||||
"importSuccessCount": "成功数",
|
||||
"importErrorCount": "失败数",
|
||||
"importRowNumber": "行号",
|
||||
"importDeptCode": "部门编码",
|
||||
"importReason": "失败原因"
|
||||
"importFieldName": "问题字段",
|
||||
"importFieldValue": "填写内容",
|
||||
"importReason": "失败原因",
|
||||
"importGuideTitle": "填写说明",
|
||||
"importGuideNameRule": "部门、角色、岗位都填写名称。",
|
||||
"importGuideRequired": "仅部门名称、登录账号、昵称为必填;岗位、手机号、邮箱为非必填。",
|
||||
"importGuideMultiValue": "角色和岗位支持多个名称,使用英文逗号或中文逗号分隔。"
|
||||
}
|
||||
|
||||
@@ -26,6 +26,29 @@ import {
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
interface ImportErrorDetail {
|
||||
fieldName?: string;
|
||||
fieldValue?: null | string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
interface ImportErrorRow {
|
||||
rowNumber?: number;
|
||||
deptName?: string;
|
||||
loginName?: string;
|
||||
nickname?: string;
|
||||
roleNames?: string;
|
||||
positionNames?: string;
|
||||
details?: ImportErrorDetail[];
|
||||
}
|
||||
|
||||
interface ImportResult {
|
||||
totalCount?: number;
|
||||
successCount?: number;
|
||||
errorCount?: number;
|
||||
errorRows?: ImportErrorRow[];
|
||||
}
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
|
||||
defineExpose({
|
||||
@@ -37,10 +60,22 @@ const fileList = ref<any[]>([]);
|
||||
const currentFile = ref<File | null>(null);
|
||||
const submitLoading = ref(false);
|
||||
const downloadLoading = ref(false);
|
||||
const importResult = ref<any>(null);
|
||||
const importResult = ref<ImportResult | null>(null);
|
||||
|
||||
const hasErrors = computed(() => (importResult.value?.errorCount || 0) > 0);
|
||||
const hasSuccess = computed(() => (importResult.value?.successCount || 0) > 0);
|
||||
const selectedFileName = computed(() => currentFile.value?.name || '');
|
||||
const errorDetailRows = computed(() => {
|
||||
return (importResult.value?.errorRows || []).flatMap((row) =>
|
||||
(row.details || []).map((detail) => ({
|
||||
rowNumber: row.rowNumber,
|
||||
loginName: row.loginName,
|
||||
fieldName: detail.fieldName,
|
||||
fieldValue: detail.fieldValue,
|
||||
reason: detail.reason,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
function openDialog() {
|
||||
dialogVisible.value = true;
|
||||
@@ -102,9 +137,21 @@ async function handleImport() {
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
importResult.value = res.data;
|
||||
const successCount = res.data?.successCount || 0;
|
||||
const errorCount = res.data?.errorCount || 0;
|
||||
if (errorCount === 0) {
|
||||
ElMessage.success($t('sysAccount.importFinished'));
|
||||
} else if (successCount === 0) {
|
||||
ElMessage.error($t('sysAccount.importAllFailed'));
|
||||
} else {
|
||||
ElMessage.warning(
|
||||
$t('sysAccount.importPartialSuccess', { successCount, errorCount }),
|
||||
);
|
||||
}
|
||||
if (successCount > 0) {
|
||||
emit('reload');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
@@ -149,6 +196,15 @@ async function handleImport() {
|
||||
</div>
|
||||
</ElUpload>
|
||||
|
||||
<div class="import-guide-card">
|
||||
<div class="guide-title">{{ $t('sysAccount.importGuideTitle') }}</div>
|
||||
<div class="guide-text">{{ $t('sysAccount.importGuideNameRule') }}</div>
|
||||
<div class="guide-text">{{ $t('sysAccount.importGuideRequired') }}</div>
|
||||
<div class="guide-text">
|
||||
{{ $t('sysAccount.importGuideMultiValue') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedFileName" class="selected-file-card">
|
||||
<div class="selected-file-main">
|
||||
<ElIcon class="selected-file-icon"><Document /></ElIcon>
|
||||
@@ -183,6 +239,22 @@ async function handleImport() {
|
||||
{{ $t('sysAccount.importResultTitle') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="result-summary">
|
||||
<span v-if="hasErrors && hasSuccess">
|
||||
{{
|
||||
$t('sysAccount.importPartialSuccess', {
|
||||
successCount: importResult?.successCount || 0,
|
||||
errorCount: importResult?.errorCount || 0,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-else-if="hasErrors">
|
||||
{{ $t('sysAccount.importAllFailed') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('sysAccount.importFinished') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-stats">
|
||||
<div class="stat-item">
|
||||
@@ -211,7 +283,7 @@ async function handleImport() {
|
||||
|
||||
<ElTable
|
||||
v-if="hasErrors"
|
||||
:data="importResult.errorRows || []"
|
||||
:data="errorDetailRows"
|
||||
size="small"
|
||||
class="result-error-table"
|
||||
>
|
||||
@@ -221,23 +293,36 @@ async function handleImport() {
|
||||
width="96"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="deptCode"
|
||||
:label="$t('sysAccount.importDeptCode')"
|
||||
prop="loginName"
|
||||
:label="$t('sysAccount.loginName')"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="loginName"
|
||||
:label="$t('sysAccount.loginName')"
|
||||
min-width="160"
|
||||
prop="fieldName"
|
||||
:label="$t('sysAccount.importFieldName')"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="fieldValue"
|
||||
:label="$t('sysAccount.importFieldValue')"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.fieldValue || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="reason"
|
||||
:label="$t('sysAccount.importReason')"
|
||||
min-width="260"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
min-width="320"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="error-reason-cell">{{ row.reason }}</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
</div>
|
||||
@@ -292,6 +377,26 @@ async function handleImport() {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.import-guide-card {
|
||||
padding: 14px 16px;
|
||||
background: hsl(var(--surface-subtle) / 94%);
|
||||
border: 1px solid hsl(var(--border) / 72%);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.guide-text {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.selected-file-main {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -329,6 +434,13 @@ async function handleImport() {
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.result-summary {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.result-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -362,6 +474,12 @@ async function handleImport() {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.error-reason-cell {
|
||||
word-break: normal;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal, EasyFlowInputPassword } from '@easyflow/common-ui';
|
||||
|
||||
@@ -29,6 +29,7 @@ function createDefaultEntity() {
|
||||
deptId: '',
|
||||
loginName: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
accountType: '',
|
||||
nickname: '',
|
||||
mobile: '',
|
||||
@@ -54,6 +55,21 @@ const validateStrongPassword = (_rule: any, value: string, callback: any) => {
|
||||
}
|
||||
callback();
|
||||
};
|
||||
const validateConfirmPassword = (_rule: any, value: string, callback: any) => {
|
||||
if (!isAdd.value) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
callback(new Error($t('sysAccount.repeatPwd')));
|
||||
return;
|
||||
}
|
||||
if (value !== entity.value.password) {
|
||||
callback(new Error($t('sysAccount.notSamePwd')));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
};
|
||||
const btnLoading = ref(false);
|
||||
const rules = ref({
|
||||
deptId: [
|
||||
@@ -68,6 +84,9 @@ const rules = ref({
|
||||
password: [
|
||||
{ required: true, validator: validateStrongPassword, trigger: 'blur' },
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, validator: validateConfirmPassword, trigger: 'blur' },
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
@@ -91,10 +110,11 @@ function save() {
|
||||
saveForm.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
btnLoading.value = true;
|
||||
const { confirmPassword: _confirmPassword, ...payload } = entity.value;
|
||||
api
|
||||
.post(
|
||||
isAdd.value ? 'api/v1/sysAccount/save' : 'api/v1/sysAccount/update',
|
||||
entity.value,
|
||||
payload,
|
||||
)
|
||||
.then((res) => {
|
||||
btnLoading.value = false;
|
||||
@@ -116,6 +136,16 @@ function closeDialog() {
|
||||
entity.value = createDefaultEntity();
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => entity.value.password,
|
||||
() => {
|
||||
if (!dialogVisible.value || !isAdd.value || !entity.value.confirmPassword) {
|
||||
return;
|
||||
}
|
||||
saveForm.value?.validateField('confirmPassword').catch(() => {});
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -159,6 +189,16 @@ function closeDialog() {
|
||||
</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="isAdd"
|
||||
prop="confirmPassword"
|
||||
:label="$t('sysAccount.confirmPwd')"
|
||||
>
|
||||
<EasyFlowInputPassword
|
||||
v-model="entity.confirmPassword"
|
||||
:placeholder="$t('sysAccount.repeatPwd')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="nickname" :label="$t('sysAccount.nickname')">
|
||||
<ElInput v-model.trim="entity.nickname" />
|
||||
</ElFormItem>
|
||||
|
||||
Reference in New Issue
Block a user