feat: 优化用户导入失败反馈与模板说明

- 支持按字段返回用户导入失败明细

- 补充导入模板填写说明与名称匹配规则

- 优化管理端导入失败弹窗与错误展示
This commit is contained in:
2026-04-09 17:25:50 +08:00
parent 4e565aef99
commit cfbeaf11fe
7 changed files with 652 additions and 161 deletions

View File

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

View File

@@ -1,5 +1,8 @@
package tech.easyflow.system.entity.vo; 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 Integer rowNumber;
private String deptCode; private String deptName;
private String loginName; 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() { public Integer getRowNumber() {
return rowNumber; return rowNumber;
} }
/**
* 设置行号。
*
* @param rowNumber 行号
*/
public void setRowNumber(Integer rowNumber) { public void setRowNumber(Integer rowNumber) {
this.rowNumber = 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() { public String getLoginName() {
return loginName; return loginName;
} }
/**
* 设置登录账号。
*
* @param loginName 登录账号
*/
public void setLoginName(String loginName) { public void setLoginName(String loginName) {
this.loginName = 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;
} }
} }

View File

@@ -3,10 +3,12 @@ package tech.easyflow.system.service.impl;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.crypto.digest.BCrypt; import cn.hutool.crypto.digest.BCrypt;
import cn.idev.excel.EasyExcel; import cn.idev.excel.EasyExcel;
import cn.idev.excel.ExcelWriter;
import cn.idev.excel.FastExcel; import cn.idev.excel.FastExcel;
import cn.idev.excel.context.AnalysisContext; import cn.idev.excel.context.AnalysisContext;
import cn.idev.excel.metadata.data.ReadCellData; import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.read.listener.ReadListener; import cn.idev.excel.read.listener.ReadListener;
import cn.idev.excel.write.metadata.WriteSheet;
import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl; import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; 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.SysRole;
import tech.easyflow.system.entity.vo.SysAccountBatchActionErrorItemVo; import tech.easyflow.system.entity.vo.SysAccountBatchActionErrorItemVo;
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo; 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.SysAccountImportErrorRowVo;
import tech.easyflow.system.entity.vo.SysAccountImportResultVo; import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
import tech.easyflow.system.mapper.SysAccountMapper; import tech.easyflow.system.mapper.SysAccountMapper;
@@ -53,6 +56,7 @@ import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; 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 String DEFAULT_RESET_PASSWORD = "123456";
private static final long MAX_IMPORT_FILE_SIZE_BYTES = 10L * 1024 * 1024; private static final long MAX_IMPORT_FILE_SIZE_BYTES = 10L * 1024 * 1024;
private static final int MAX_IMPORT_ROWS = 5000; private static final int MAX_IMPORT_ROWS = 5000;
private static final String IMPORT_HEAD_DEPT_CODE = "部门编码"; private static final String IMPORT_HEAD_DEPT_NAME = "部门名称*";
private static final String IMPORT_HEAD_LOGIN_NAME = "登录账号"; private static final String IMPORT_HEAD_LOGIN_NAME = "登录账号*";
private static final String IMPORT_HEAD_NICKNAME = "昵称"; private static final String IMPORT_HEAD_NICKNAME = "昵称*";
private static final String IMPORT_HEAD_MOBILE = "手机号"; private static final String IMPORT_HEAD_MOBILE = "手机号";
private static final String IMPORT_HEAD_EMAIL = "邮箱"; private static final String IMPORT_HEAD_EMAIL = "邮箱";
private static final String IMPORT_HEAD_STATUS = "状态"; private static final String IMPORT_HEAD_STATUS = "状态";
private static final String IMPORT_HEAD_ROLE_KEYS = "角色编码"; private static final String IMPORT_HEAD_ROLE_NAMES = "角色名称";
private static final String IMPORT_HEAD_POSITION_CODES = "岗位编码"; private static final String IMPORT_HEAD_POSITION_NAMES = "岗位名称";
private static final String IMPORT_HEAD_REMARK = "备注"; 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 @Resource
private SysAccountRoleMapper sysAccountRoleMapper; private SysAccountRoleMapper sysAccountRoleMapper;
@@ -244,16 +260,25 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
return result; return result;
} }
Map<String, SysDept> deptMap = buildDeptCodeMap(); ImportNameLookup<SysDept> deptLookup = buildImportNameLookup(
Map<String, SysRole> roleMap = buildRoleKeyMap(); sysDeptMapper.selectListByQuery(QueryWrapper.create()),
Map<String, SysPosition> positionMap = buildPositionCodeMap(); 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) { for (SysAccountImportRow row : rows) {
try { try {
executeInRowTransaction(() -> importSingleRow(row, loginAccount, deptMap, roleMap, positionMap)); executeInRowTransaction(() -> importSingleRow(row, loginAccount, deptLookup, roleLookup, positionLookup));
result.setSuccessCount(result.getSuccessCount() + 1); result.setSuccessCount(result.getSuccessCount() + 1);
} catch (Exception e) { } catch (Exception e) {
result.setErrorCount(result.getErrorCount() + 1); result.setErrorCount(result.getErrorCount() + 1);
appendImportError(result, row, extractImportErrorMessage(e)); appendImportError(result, row, e);
} }
} }
return result; return result;
@@ -261,10 +286,20 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
@Override @Override
public void writeImportTemplate(OutputStream outputStream) { public void writeImportTemplate(OutputStream outputStream) {
EasyExcel.write(outputStream) ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
try {
WriteSheet templateSheet = EasyExcel.writerSheet("模板")
.head(buildImportHeadList()) .head(buildImportHeadList())
.sheet("模板") .build();
.doWrite(new ArrayList<>()); excelWriter.write(new ArrayList<>(), templateSheet);
WriteSheet guideSheet = EasyExcel.writerSheet("填写说明")
.head(buildImportGuideHeadList())
.build();
excelWriter.write(buildImportGuideRows(), guideSheet);
} finally {
excelWriter.finish();
}
} }
private void executeInRowTransaction(Runnable action) { private void executeInRowTransaction(Runnable action) {
@@ -281,29 +316,54 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
private void importSingleRow(SysAccountImportRow row, private void importSingleRow(SysAccountImportRow row,
LoginAccount loginAccount, LoginAccount loginAccount,
Map<String, SysDept> deptMap, ImportNameLookup<SysDept> deptLookup,
Map<String, SysRole> roleMap, ImportNameLookup<SysRole> roleLookup,
Map<String, SysPosition> positionMap) { ImportNameLookup<SysPosition> positionLookup) {
String deptCode = trimToNull(row.getDeptCode()); List<SysAccountImportErrorDetailVo> details = new ArrayList<>();
String deptName = trimToNull(row.getDeptName());
String loginName = trimToNull(row.getLoginName()); String loginName = trimToNull(row.getLoginName());
String nickname = trimToNull(row.getNickname()); String nickname = trimToNull(row.getNickname());
if (deptCode == null) { String mobile = trimToNull(row.getMobile());
throw new BusinessException("部门编码不能为空"); String email = trimToNull(row.getEmail());
if (deptName == null) {
addImportDetail(details, IMPORT_FIELD_DEPT_NAME, row.getDeptName(), "部门名称不能为空");
} }
if (loginName == null) { 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) { if (nickname == null) {
throw new BusinessException("昵称不能为空"); addImportDetail(details, IMPORT_FIELD_NICKNAME, row.getNickname(), "昵称不能为空");
} }
SysDept dept = deptMap.get(deptCode); if (mobile != null && !StringUtil.isMobileNumber(mobile)) {
if (dept == null) { addImportDetail(details, IMPORT_FIELD_MOBILE, row.getMobile(), "手机号格式不正确");
throw new BusinessException("部门编码不存在: " + deptCode); }
if (email != null && !StringUtil.isEmail(email)) {
addImportDetail(details, IMPORT_FIELD_EMAIL, row.getEmail(), "邮箱格式不正确");
} }
ensureLoginNameNotExists(loginName);
List<BigInteger> roleIds = resolveRoleIds(row.getRoleKeys(), roleMap); SysDept dept = resolveSingleResource(deptName, deptLookup, IMPORT_FIELD_DEPT_NAME, details);
List<BigInteger> positionIds = resolvePositionIds(row.getPositionCodes(), positionMap); 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(); SysAccount entity = new SysAccount();
entity.setDeptId(dept.getId()); entity.setDeptId(dept.getId());
@@ -313,9 +373,9 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
entity.setPasswordResetRequired(true); entity.setPasswordResetRequired(true);
entity.setAccountType(EnumAccountType.NORMAL.getCode()); entity.setAccountType(EnumAccountType.NORMAL.getCode());
entity.setNickname(nickname); entity.setNickname(nickname);
entity.setMobile(trimToNull(row.getMobile())); entity.setMobile(mobile);
entity.setEmail(trimToNull(row.getEmail())); entity.setEmail(email);
entity.setStatus(parseStatus(row.getStatus())); entity.setStatus(status);
entity.setRemark(trimToNull(row.getRemark())); entity.setRemark(trimToNull(row.getRemark()));
entity.setCreated(new Date()); entity.setCreated(new Date());
entity.setCreatedBy(loginAccount.getId()); entity.setCreatedBy(loginAccount.getId());
@@ -327,15 +387,26 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
syncRelations(entity); syncRelations(entity);
} }
private void ensureLoginNameNotExists(String loginName) { /**
* 校验登录账号是否已存在。
*
* @param loginName 登录账号
* @return 是否已存在
*/
private boolean isLoginNameExists(String loginName) {
QueryWrapper wrapper = QueryWrapper.create(); QueryWrapper wrapper = QueryWrapper.create();
wrapper.eq(SysAccount::getLoginName, loginName); wrapper.eq(SysAccount::getLoginName, loginName);
if (count(wrapper) > 0) { return count(wrapper) > 0;
throw new BusinessException("登录账号已存在: " + loginName);
}
} }
private Integer parseStatus(String rawStatus) { /**
* 解析导入状态列。
*
* @param rawStatus 原始状态文本
* @param details 错误明细收集器
* @return 解析后的状态编码
*/
private Integer parseStatus(String rawStatus, List<SysAccountImportErrorDetailVo> details) {
String status = trimToNull(rawStatus); String status = trimToNull(rawStatus);
if (status == null) { if (status == null) {
return EnumDataStatus.AVAILABLE.getCode(); 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)) { if ("0".equals(status) || "未启用".equals(status) || "停用".equals(status) || "禁用".equals(status)) {
return EnumDataStatus.UNAVAILABLE.getCode(); 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(); return Collections.emptyList();
} }
List<BigInteger> roleIds = new ArrayList<>(roleKeys.size()); List<BigInteger> ids = new ArrayList<>(names.size());
for (String roleKey : roleKeys) { for (String name : names) {
SysRole role = roleMap.get(roleKey); T resource = resolveSingleResource(name, lookup, fieldName, details);
if (role == null) { if (resource != null) {
throw new BusinessException("角色编码不存在: " + roleKey); ids.add(idExtractor.apply(resource));
} }
roleIds.add(role.getId());
} }
return roleIds; return ids;
}
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;
} }
private List<String> splitCodes(String rawCodes) { private List<String> splitCodes(String rawCodes) {
@@ -398,40 +468,32 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
return result; return result;
} }
private Map<String, SysDept> buildDeptCodeMap() { /**
List<SysDept> deptList = sysDeptMapper.selectListByQuery(QueryWrapper.create()); * 构建导入名称查找表,并标记重名数据。
Map<String, SysDept> deptMap = new HashMap<>(); *
for (SysDept dept : deptList) { * @param resources 资源集合
String deptCode = trimToNull(dept.getDeptCode()); * @param nameExtractor 名称提取器
if (deptCode != null) { * @param <T> 资源类型
deptMap.putIfAbsent(deptCode, dept); * @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; return new ImportNameLookup<>(uniqueMap, duplicateNames);
}
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;
} }
private List<SysAccountImportRow> parseImportRows(MultipartFile file) { 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(); SysAccountImportErrorRowVo errorRow = new SysAccountImportErrorRowVo();
errorRow.setRowNumber(row.getRowNumber()); errorRow.setRowNumber(row.getRowNumber());
errorRow.setDeptCode(row.getDeptCode()); errorRow.setDeptName(row.getDeptName());
errorRow.setLoginName(row.getLoginName()); 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); result.getErrorRows().add(errorRow);
} }
@@ -537,18 +608,69 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
private List<List<String>> buildImportHeadList() { private List<List<String>> buildImportHeadList() {
List<List<String>> headList = new ArrayList<>(9); 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_LOGIN_NAME));
headList.add(Collections.singletonList(IMPORT_HEAD_NICKNAME)); headList.add(Collections.singletonList(IMPORT_HEAD_NICKNAME));
headList.add(Collections.singletonList(IMPORT_HEAD_MOBILE)); headList.add(Collections.singletonList(IMPORT_HEAD_MOBILE));
headList.add(Collections.singletonList(IMPORT_HEAD_EMAIL)); headList.add(Collections.singletonList(IMPORT_HEAD_EMAIL));
headList.add(Collections.singletonList(IMPORT_HEAD_STATUS)); headList.add(Collections.singletonList(IMPORT_HEAD_STATUS));
headList.add(Collections.singletonList(IMPORT_HEAD_ROLE_KEYS)); headList.add(Collections.singletonList(IMPORT_HEAD_ROLE_NAMES));
headList.add(Collections.singletonList(IMPORT_HEAD_POSITION_CODES)); headList.add(Collections.singletonList(IMPORT_HEAD_POSITION_NAMES));
headList.add(Collections.singletonList(IMPORT_HEAD_REMARK)); headList.add(Collections.singletonList(IMPORT_HEAD_REMARK));
return headList; 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) { private String trimToNull(String value) {
if (!StringUtil.hasText(value)) { if (!StringUtil.hasText(value)) {
return null; return null;
@@ -558,14 +680,14 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
private static class SysAccountImportRow { private static class SysAccountImportRow {
private Integer rowNumber; private Integer rowNumber;
private String deptCode; private String deptName;
private String loginName; private String loginName;
private String nickname; private String nickname;
private String mobile; private String mobile;
private String email; private String email;
private String status; private String status;
private String roleKeys; private String roleNames;
private String positionCodes; private String positionNames;
private String remark; private String remark;
public Integer getRowNumber() { public Integer getRowNumber() {
@@ -576,12 +698,12 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
this.rowNumber = rowNumber; this.rowNumber = rowNumber;
} }
public String getDeptCode() { public String getDeptName() {
return deptCode; return deptName;
} }
public void setDeptCode(String deptCode) { public void setDeptName(String deptName) {
this.deptCode = deptCode; this.deptName = deptName;
} }
public String getLoginName() { public String getLoginName() {
@@ -624,20 +746,20 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
this.status = status; this.status = status;
} }
public String getRoleKeys() { public String getRoleNames() {
return roleKeys; return roleNames;
} }
public void setRoleKeys(String roleKeys) { public void setRoleNames(String roleNames) {
this.roleKeys = roleKeys; this.roleNames = roleNames;
} }
public String getPositionCodes() { public String getPositionNames() {
return positionCodes; return positionNames;
} }
public void setPositionCodes(String positionCodes) { public void setPositionNames(String positionNames) {
this.positionCodes = positionCodes; this.positionNames = positionNames;
} }
public String getRemark() { public String getRemark() {
@@ -658,23 +780,23 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
@Override @Override
public void invoke(LinkedHashMap<Integer, Object> data, AnalysisContext context) { public void invoke(LinkedHashMap<Integer, Object> data, AnalysisContext context) {
sheetRowNo++; 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 loginName = getCellValue(data, IMPORT_HEAD_LOGIN_NAME);
String nickname = getCellValue(data, IMPORT_HEAD_NICKNAME); String nickname = getCellValue(data, IMPORT_HEAD_NICKNAME);
String mobile = getCellValue(data, IMPORT_HEAD_MOBILE); String mobile = getCellValue(data, IMPORT_HEAD_MOBILE);
String email = getCellValue(data, IMPORT_HEAD_EMAIL); String email = getCellValue(data, IMPORT_HEAD_EMAIL);
String status = getCellValue(data, IMPORT_HEAD_STATUS); String status = getCellValue(data, IMPORT_HEAD_STATUS);
String roleKeys = getCellValue(data, IMPORT_HEAD_ROLE_KEYS); String roleNames = getCellValue(data, IMPORT_HEAD_ROLE_NAMES);
String positionCodes = getCellValue(data, IMPORT_HEAD_POSITION_CODES); String positionNames = getCellValue(data, IMPORT_HEAD_POSITION_NAMES);
String remark = getCellValue(data, IMPORT_HEAD_REMARK); String remark = getCellValue(data, IMPORT_HEAD_REMARK);
if (!StringUtil.hasText(deptCode) if (!StringUtil.hasText(deptName)
&& !StringUtil.hasText(loginName) && !StringUtil.hasText(loginName)
&& !StringUtil.hasText(nickname) && !StringUtil.hasText(nickname)
&& !StringUtil.hasText(mobile) && !StringUtil.hasText(mobile)
&& !StringUtil.hasText(email) && !StringUtil.hasText(email)
&& !StringUtil.hasText(status) && !StringUtil.hasText(status)
&& !StringUtil.hasText(roleKeys) && !StringUtil.hasText(roleNames)
&& !StringUtil.hasText(positionCodes) && !StringUtil.hasText(positionNames)
&& !StringUtil.hasText(remark)) { && !StringUtil.hasText(remark)) {
return; return;
} }
@@ -683,14 +805,14 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
} }
SysAccountImportRow row = new SysAccountImportRow(); SysAccountImportRow row = new SysAccountImportRow();
row.setRowNumber(sheetRowNo + 1); row.setRowNumber(sheetRowNo + 1);
row.setDeptCode(deptCode); row.setDeptName(deptName);
row.setLoginName(loginName); row.setLoginName(loginName);
row.setNickname(nickname); row.setNickname(nickname);
row.setMobile(mobile); row.setMobile(mobile);
row.setEmail(email); row.setEmail(email);
row.setStatus(status); row.setStatus(status);
row.setRoleKeys(roleKeys); row.setRoleNames(roleNames);
row.setPositionCodes(positionCodes); row.setPositionNames(positionNames);
row.setRemark(remark); row.setRemark(remark);
rows.add(row); rows.add(row);
} }
@@ -705,15 +827,9 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
} }
} }
List<String> requiredHeads = List.of( List<String> requiredHeads = List.of(
IMPORT_HEAD_DEPT_CODE, IMPORT_HEAD_DEPT_NAME,
IMPORT_HEAD_LOGIN_NAME, IMPORT_HEAD_LOGIN_NAME,
IMPORT_HEAD_NICKNAME, IMPORT_HEAD_NICKNAME
IMPORT_HEAD_MOBILE,
IMPORT_HEAD_EMAIL,
IMPORT_HEAD_STATUS,
IMPORT_HEAD_ROLE_KEYS,
IMPORT_HEAD_POSITION_CODES,
IMPORT_HEAD_REMARK
); );
for (String requiredHead : requiredHeads) { for (String requiredHead : requiredHeads) {
if (!headIndex.containsKey(requiredHead)) { if (!headIndex.containsKey(requiredHead)) {
@@ -740,4 +856,35 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
return value == null ? null : String.valueOf(value).trim(); 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;
}
}
} }

View File

@@ -45,15 +45,23 @@
"batchResetPasswordAllFailed": "Batch password reset failed", "batchResetPasswordAllFailed": "Batch password reset failed",
"importTitle": "Import Users", "importTitle": "Import Users",
"importUploadTitle": "Drag the Excel file here, or click to select a file", "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", "importSelectFileRequired": "Please select a file to import",
"downloadTemplate": "Download Template", "downloadTemplate": "Download Template",
"importFinished": "User import completed", "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", "importResultTitle": "Import Result",
"importTotalCount": "Total", "importTotalCount": "Total",
"importSuccessCount": "Success", "importSuccessCount": "Success",
"importErrorCount": "Failed", "importErrorCount": "Failed",
"importRowNumber": "Row", "importRowNumber": "Row",
"importDeptCode": "Dept Code", "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."
} }

View File

@@ -46,15 +46,23 @@
"batchResetPasswordAllFailed": "批量重置密码失败", "batchResetPasswordAllFailed": "批量重置密码失败",
"importTitle": "导入用户", "importTitle": "导入用户",
"importUploadTitle": "拖拽 Excel 文件到此处,或点击选择文件", "importUploadTitle": "拖拽 Excel 文件到此处,或点击选择文件",
"importUploadDesc": "仅支持 .xlsx / .xls,导入只新增用户,重复账号会报错", "importUploadDesc": "仅支持 .xlsx / .xls。请按名称填写部门、角色、岗位,模板中的 * 为必填项。",
"importSelectFileRequired": "请先选择要导入的文件", "importSelectFileRequired": "请先选择要导入的文件",
"downloadTemplate": "下载导入模板", "downloadTemplate": "下载导入模板",
"importFinished": "用户导入完成", "importFinished": "用户导入完成",
"importPartialSuccess": "导入完成,成功 {successCount} 条,失败 {errorCount} 条,请查看下方明细",
"importAllFailed": "导入失败,请查看下方明细",
"importResultTitle": "导入结果", "importResultTitle": "导入结果",
"importTotalCount": "总条数", "importTotalCount": "总条数",
"importSuccessCount": "成功数", "importSuccessCount": "成功数",
"importErrorCount": "失败数", "importErrorCount": "失败数",
"importRowNumber": "行号", "importRowNumber": "行号",
"importDeptCode": "部门编码", "importDeptCode": "部门编码",
"importReason": "失败原因" "importFieldName": "问题字段",
"importFieldValue": "填写内容",
"importReason": "失败原因",
"importGuideTitle": "填写说明",
"importGuideNameRule": "部门、角色、岗位都填写名称。",
"importGuideRequired": "仅部门名称、登录账号、昵称为必填;岗位、手机号、邮箱为非必填。",
"importGuideMultiValue": "角色和岗位支持多个名称,使用英文逗号或中文逗号分隔。"
} }

View File

@@ -26,6 +26,29 @@ import {
import { api } from '#/api/request'; import { api } from '#/api/request';
import { $t } from '#/locales'; 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']); const emit = defineEmits(['reload']);
defineExpose({ defineExpose({
@@ -37,10 +60,22 @@ const fileList = ref<any[]>([]);
const currentFile = ref<File | null>(null); const currentFile = ref<File | null>(null);
const submitLoading = ref(false); const submitLoading = ref(false);
const downloadLoading = 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 hasErrors = computed(() => (importResult.value?.errorCount || 0) > 0);
const hasSuccess = computed(() => (importResult.value?.successCount || 0) > 0);
const selectedFileName = computed(() => currentFile.value?.name || ''); 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() { function openDialog() {
dialogVisible.value = true; dialogVisible.value = true;
@@ -102,9 +137,21 @@ async function handleImport() {
}); });
if (res.errorCode === 0) { if (res.errorCode === 0) {
importResult.value = res.data; importResult.value = res.data;
const successCount = res.data?.successCount || 0;
const errorCount = res.data?.errorCount || 0;
if (errorCount === 0) {
ElMessage.success($t('sysAccount.importFinished')); 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'); emit('reload');
} }
}
} finally { } finally {
submitLoading.value = false; submitLoading.value = false;
} }
@@ -149,6 +196,15 @@ async function handleImport() {
</div> </div>
</ElUpload> </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 v-if="selectedFileName" class="selected-file-card">
<div class="selected-file-main"> <div class="selected-file-main">
<ElIcon class="selected-file-icon"><Document /></ElIcon> <ElIcon class="selected-file-icon"><Document /></ElIcon>
@@ -183,6 +239,22 @@ async function handleImport() {
{{ $t('sysAccount.importResultTitle') }} {{ $t('sysAccount.importResultTitle') }}
</span> </span>
</div> </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>
<div class="result-stats"> <div class="result-stats">
<div class="stat-item"> <div class="stat-item">
@@ -211,7 +283,7 @@ async function handleImport() {
<ElTable <ElTable
v-if="hasErrors" v-if="hasErrors"
:data="importResult.errorRows || []" :data="errorDetailRows"
size="small" size="small"
class="result-error-table" class="result-error-table"
> >
@@ -221,23 +293,36 @@ async function handleImport() {
width="96" width="96"
/> />
<ElTableColumn <ElTableColumn
prop="deptCode" prop="loginName"
:label="$t('sysAccount.importDeptCode')" :label="$t('sysAccount.loginName')"
min-width="140" min-width="140"
show-overflow-tooltip show-overflow-tooltip
/> />
<ElTableColumn <ElTableColumn
prop="loginName" prop="fieldName"
:label="$t('sysAccount.loginName')" :label="$t('sysAccount.importFieldName')"
min-width="160" min-width="140"
show-overflow-tooltip show-overflow-tooltip
/> />
<ElTableColumn
prop="fieldValue"
:label="$t('sysAccount.importFieldValue')"
min-width="180"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.fieldValue || '-' }}
</template>
</ElTableColumn>
<ElTableColumn <ElTableColumn
prop="reason" prop="reason"
:label="$t('sysAccount.importReason')" :label="$t('sysAccount.importReason')"
min-width="260" min-width="320"
show-overflow-tooltip >
/> <template #default="{ row }">
<div class="error-reason-cell">{{ row.reason }}</div>
</template>
</ElTableColumn>
</ElTable> </ElTable>
</div> </div>
</div> </div>
@@ -292,6 +377,26 @@ async function handleImport() {
border-radius: 16px; 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 { .selected-file-main {
display: flex; display: flex;
gap: 10px; gap: 10px;
@@ -329,6 +434,13 @@ async function handleImport() {
color: var(--el-text-color-primary); 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 { .result-stats {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -362,6 +474,12 @@ async function handleImport() {
color: var(--el-color-danger); color: var(--el-color-danger);
} }
.error-reason-cell {
word-break: normal;
overflow-wrap: anywhere;
white-space: normal;
}
.dialog-footer { .dialog-footer {
display: flex; display: flex;
gap: 12px; gap: 12px;

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance } from 'element-plus'; import type { FormInstance } from 'element-plus';
import { onMounted, ref } from 'vue'; import { onMounted, ref, watch } from 'vue';
import { EasyFlowFormModal, EasyFlowInputPassword } from '@easyflow/common-ui'; import { EasyFlowFormModal, EasyFlowInputPassword } from '@easyflow/common-ui';
@@ -29,6 +29,7 @@ function createDefaultEntity() {
deptId: '', deptId: '',
loginName: '', loginName: '',
password: '', password: '',
confirmPassword: '',
accountType: '', accountType: '',
nickname: '', nickname: '',
mobile: '', mobile: '',
@@ -54,6 +55,21 @@ const validateStrongPassword = (_rule: any, value: string, callback: any) => {
} }
callback(); 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 btnLoading = ref(false);
const rules = ref({ const rules = ref({
deptId: [ deptId: [
@@ -68,6 +84,9 @@ const rules = ref({
password: [ password: [
{ required: true, validator: validateStrongPassword, trigger: 'blur' }, { required: true, validator: validateStrongPassword, trigger: 'blur' },
], ],
confirmPassword: [
{ required: true, validator: validateConfirmPassword, trigger: 'blur' },
],
status: [ status: [
{ required: true, message: $t('message.required'), trigger: 'blur' }, { required: true, message: $t('message.required'), trigger: 'blur' },
], ],
@@ -91,10 +110,11 @@ function save() {
saveForm.value?.validate((valid) => { saveForm.value?.validate((valid) => {
if (valid) { if (valid) {
btnLoading.value = true; btnLoading.value = true;
const { confirmPassword: _confirmPassword, ...payload } = entity.value;
api api
.post( .post(
isAdd.value ? 'api/v1/sysAccount/save' : 'api/v1/sysAccount/update', isAdd.value ? 'api/v1/sysAccount/save' : 'api/v1/sysAccount/update',
entity.value, payload,
) )
.then((res) => { .then((res) => {
btnLoading.value = false; btnLoading.value = false;
@@ -116,6 +136,16 @@ function closeDialog() {
entity.value = createDefaultEntity(); entity.value = createDefaultEntity();
dialogVisible.value = false; dialogVisible.value = false;
} }
watch(
() => entity.value.password,
() => {
if (!dialogVisible.value || !isAdd.value || !entity.value.confirmPassword) {
return;
}
saveForm.value?.validateField('confirmPassword').catch(() => {});
},
);
</script> </script>
<template> <template>
@@ -159,6 +189,16 @@ function closeDialog() {
</div> </div>
</div> </div>
</ElFormItem> </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')"> <ElFormItem prop="nickname" :label="$t('sysAccount.nickname')">
<ElInput v-model.trim="entity.nickname" /> <ElInput v-model.trim="entity.nickname" />
</ElFormItem> </ElFormItem>