diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/vo/SysAccountImportErrorDetailVo.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/vo/SysAccountImportErrorDetailVo.java new file mode 100644 index 0000000..3021de7 --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/vo/SysAccountImportErrorDetailVo.java @@ -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; + } +} diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/vo/SysAccountImportErrorRowVo.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/vo/SysAccountImportErrorRowVo.java index b7c2686..b970525 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/vo/SysAccountImportErrorRowVo.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/entity/vo/SysAccountImportErrorRowVo.java @@ -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 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 getDetails() { + return details; + } + + /** + * 设置失败明细。 + * + * @param details 失败明细 + */ + public void setDetails(List details) { + this.details = details; } } diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java index 599d592..e5be820 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java @@ -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 deptMap = buildDeptCodeMap(); - Map roleMap = buildRoleKeyMap(); - Map positionMap = buildPositionCodeMap(); + ImportNameLookup deptLookup = buildImportNameLookup( + sysDeptMapper.selectListByQuery(QueryWrapper.create()), + SysDept::getDeptName + ); + ImportNameLookup roleLookup = buildImportNameLookup( + sysRoleMapper.selectListByQuery(QueryWrapper.create()), + SysRole::getRoleName + ); + ImportNameLookup 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()); + ExcelWriter excelWriter = EasyExcel.write(outputStream).build(); + try { + WriteSheet templateSheet = EasyExcel.writerSheet("模板") + .head(buildImportHeadList()) + .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 deptMap, - Map roleMap, - Map positionMap) { - String deptCode = trimToNull(row.getDeptCode()); + ImportNameLookup deptLookup, + ImportNameLookup roleLookup, + ImportNameLookup positionLookup) { + List 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 roleIds = resolveRoleIds(row.getRoleKeys(), roleMap); - List positionIds = resolvePositionIds(row.getPositionCodes(), positionMap); + SysDept dept = resolveSingleResource(deptName, deptLookup, IMPORT_FIELD_DEPT_NAME, details); + Integer status = parseStatus(row.getStatus(), details); + List roleIds = resolveResourceIds( + row.getRoleNames(), + roleLookup, + SysRole::getId, + IMPORT_FIELD_ROLE_NAME, + details + ); + List 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 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 details) { String status = trimToNull(rawStatus); if (status == null) { return EnumDataStatus.AVAILABLE.getCode(); @@ -346,39 +417,38 @@ public class SysAccountServiceImpl extends ServiceImpl resolveRoleIds(String roleKeysText, Map roleMap) { - List roleKeys = splitCodes(roleKeysText); - if (roleKeys.isEmpty()) { + /** + * 解析名称列表并映射为主键集合。 + * + * @param rawNames 原始名称文本 + * @param lookup 名称查找表 + * @param fieldName 字段名称 + * @param details 错误明细收集器 + * @param 资源类型 + * @return 匹配到的主键集合 + */ + private List resolveResourceIds( + String rawNames, + ImportNameLookup lookup, + Function idExtractor, + String fieldName, + List details) { + List names = splitCodes(rawNames); + if (names.isEmpty()) { return Collections.emptyList(); } - List roleIds = new ArrayList<>(roleKeys.size()); - for (String roleKey : roleKeys) { - SysRole role = roleMap.get(roleKey); - if (role == null) { - throw new BusinessException("角色编码不存在: " + roleKey); + List 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 resolvePositionIds(String positionCodesText, Map positionMap) { - List positionCodes = splitCodes(positionCodesText); - if (positionCodes.isEmpty()) { - return Collections.emptyList(); - } - List 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 splitCodes(String rawCodes) { @@ -398,40 +468,32 @@ public class SysAccountServiceImpl extends ServiceImpl buildDeptCodeMap() { - List deptList = sysDeptMapper.selectListByQuery(QueryWrapper.create()); - Map deptMap = new HashMap<>(); - for (SysDept dept : deptList) { - String deptCode = trimToNull(dept.getDeptCode()); - if (deptCode != null) { - deptMap.putIfAbsent(deptCode, dept); + /** + * 构建导入名称查找表,并标记重名数据。 + * + * @param resources 资源集合 + * @param nameExtractor 名称提取器 + * @param 资源类型 + * @return 名称查找表 + */ + private ImportNameLookup buildImportNameLookup(List resources, Function nameExtractor) { + Map uniqueMap = new HashMap<>(); + Set 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 buildRoleKeyMap() { - List roleList = sysRoleMapper.selectListByQuery(QueryWrapper.create()); - Map roleMap = new HashMap<>(); - for (SysRole role : roleList) { - String roleKey = trimToNull(role.getRoleKey()); - if (roleKey != null) { - roleMap.putIfAbsent(roleKey, role); - } - } - return roleMap; - } - - private Map buildPositionCodeMap() { - List positionList = sysPositionMapper.selectListByQuery(QueryWrapper.create()); - Map 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 parseImportRows(MultipartFile file) { @@ -462,12 +524,21 @@ public class SysAccountServiceImpl extends ServiceImpl 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> buildImportHeadList() { List> 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> buildImportGuideHeadList() { + List> headList = new ArrayList<>(2); + headList.add(Collections.singletonList(IMPORT_GUIDE_HEAD_ITEM)); + headList.add(Collections.singletonList(IMPORT_GUIDE_HEAD_CONTENT)); + return headList; + } + + private List> buildImportGuideRows() { + List> 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 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 resolveSingleResource( + String rawName, + ImportNameLookup lookup, + String fieldName, + List 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 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 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 { + private final Map uniqueMap; + private final Set duplicateNames; + + private ImportNameLookup(Map uniqueMap, Set duplicateNames) { + this.uniqueMap = uniqueMap; + this.duplicateNames = duplicateNames; + } + + public Map getUniqueMap() { + return uniqueMap; + } + + public Set getDuplicateNames() { + return duplicateNames; + } + } + + private static class ImportRowValidationException extends RuntimeException { + private final List details; + + private ImportRowValidationException(List details) { + super("导入数据校验失败"); + this.details = details; + } + + public List getDetails() { + return details; + } + } } diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/sysAccount.json b/easyflow-ui-admin/app/src/locales/langs/en-US/sysAccount.json index 884e462..1a02944 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/sysAccount.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/sysAccount.json @@ -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." } diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/sysAccount.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/sysAccount.json index 1ed4a9d..b2ce5e5 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/sysAccount.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/sysAccount.json @@ -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": "角色和岗位支持多个名称,使用英文逗号或中文逗号分隔。" } diff --git a/easyflow-ui-admin/app/src/views/system/sysAccount/SysAccountImportModal.vue b/easyflow-ui-admin/app/src/views/system/sysAccount/SysAccountImportModal.vue index 1223e9d..5210083 100644 --- a/easyflow-ui-admin/app/src/views/system/sysAccount/SysAccountImportModal.vue +++ b/easyflow-ui-admin/app/src/views/system/sysAccount/SysAccountImportModal.vue @@ -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([]); const currentFile = ref(null); const submitLoading = ref(false); const downloadLoading = ref(false); -const importResult = ref(null); +const importResult = ref(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,8 +137,20 @@ async function handleImport() { }); if (res.errorCode === 0) { importResult.value = res.data; - ElMessage.success($t('sysAccount.importFinished')); - emit('reload'); + 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() { +
+
{{ $t('sysAccount.importGuideTitle') }}
+
{{ $t('sysAccount.importGuideNameRule') }}
+
{{ $t('sysAccount.importGuideRequired') }}
+
+ {{ $t('sysAccount.importGuideMultiValue') }} +
+
+
@@ -183,6 +239,22 @@ async function handleImport() { {{ $t('sysAccount.importResultTitle') }}
+
+ + {{ + $t('sysAccount.importPartialSuccess', { + successCount: importResult?.successCount || 0, + errorCount: importResult?.errorCount || 0, + }) + }} + + + {{ $t('sysAccount.importAllFailed') }} + + + {{ $t('sysAccount.importFinished') }} + +
@@ -211,7 +283,7 @@ async function handleImport() { @@ -221,23 +293,36 @@ async function handleImport() { width="96" /> + + + + min-width="320" + > + +
@@ -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; diff --git a/easyflow-ui-admin/app/src/views/system/sysAccount/SysAccountModal.vue b/easyflow-ui-admin/app/src/views/system/sysAccount/SysAccountModal.vue index b119185..032538c 100644 --- a/easyflow-ui-admin/app/src/views/system/sysAccount/SysAccountModal.vue +++ b/easyflow-ui-admin/app/src/views/system/sysAccount/SysAccountModal.vue @@ -1,7 +1,7 @@