feat: 支持系统账号批量操作
- 新增账号批量删除和批量重置密码接口及结果返回 - 用户列表增加批量操作工具栏与结果提示 - 账号删除切换为逻辑删除语义
This commit is contained in:
@@ -22,6 +22,7 @@ import tech.easyflow.common.web.controller.BaseCurdController;
|
|||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
import tech.easyflow.log.annotation.LogRecord;
|
import tech.easyflow.log.annotation.LogRecord;
|
||||||
import tech.easyflow.system.entity.SysAccount;
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
|
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo;
|
||||||
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
||||||
import tech.easyflow.system.service.SysAccountService;
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
import tech.easyflow.system.util.SysPasswordPolicy;
|
import tech.easyflow.system.util.SysPasswordPolicy;
|
||||||
@@ -180,6 +181,28 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
|||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/removeBatchWithResult")
|
||||||
|
@SaCheckPermission("/api/v1/sysAccount/remove")
|
||||||
|
@LogRecord("批量删除用户")
|
||||||
|
public Result<SysAccountBatchActionResultVo> removeBatchWithResult(
|
||||||
|
@JsonBody(value = "ids", required = true) List<BigInteger> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
return Result.fail("ids不能为空", null);
|
||||||
|
}
|
||||||
|
return Result.ok(service.removeBatchWithResult(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/resetPasswordBatch")
|
||||||
|
@SaCheckPermission("/api/v1/sysAccount/save")
|
||||||
|
@LogRecord("批量重置用户密码")
|
||||||
|
public Result<SysAccountBatchActionResultVo> resetPasswordBatch(
|
||||||
|
@JsonBody(value = "ids", required = true) List<BigInteger> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
return Result.fail("ids不能为空", null);
|
||||||
|
}
|
||||||
|
return Result.ok(service.resetPasswordBatch(ids, SaTokenUtil.getLoginAccount().getId()));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/importExcel")
|
@PostMapping("/importExcel")
|
||||||
@SaCheckPermission("/api/v1/sysAccount/save")
|
@SaCheckPermission("/api/v1/sysAccount/save")
|
||||||
public Result<SysAccountImportResultVo> importExcel(MultipartFile file) {
|
public Result<SysAccountImportResultVo> importExcel(MultipartFile file) {
|
||||||
|
|||||||
@@ -115,6 +115,12 @@ public class SysAccountBase extends DateEntity implements Serializable {
|
|||||||
@Column(comment = "备注")
|
@Column(comment = "备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除标识
|
||||||
|
*/
|
||||||
|
@Column(value = "is_deleted", isLogicDelete = true, comment = "删除标识")
|
||||||
|
private BigInteger isDeleted;
|
||||||
|
|
||||||
public BigInteger getId() {
|
public BigInteger getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -251,4 +257,12 @@ public class SysAccountBase extends DateEntity implements Serializable {
|
|||||||
this.remark = remark;
|
this.remark = remark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigInteger getIsDeleted() {
|
||||||
|
return isDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsDeleted(BigInteger isDeleted) {
|
||||||
|
this.isDeleted = isDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package tech.easyflow.system.entity.vo;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户批量操作失败项。
|
||||||
|
*/
|
||||||
|
public class SysAccountBatchActionErrorItemVo {
|
||||||
|
|
||||||
|
private BigInteger id;
|
||||||
|
|
||||||
|
private String loginName;
|
||||||
|
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
public BigInteger getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(BigInteger id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLoginName() {
|
||||||
|
return loginName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoginName(String loginName) {
|
||||||
|
this.loginName = loginName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReason(String reason) {
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package tech.easyflow.system.entity.vo;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户批量操作结果。
|
||||||
|
*/
|
||||||
|
public class SysAccountBatchActionResultVo {
|
||||||
|
|
||||||
|
private int successCount = 0;
|
||||||
|
|
||||||
|
private int errorCount = 0;
|
||||||
|
|
||||||
|
private int totalCount = 0;
|
||||||
|
|
||||||
|
private List<SysAccountBatchActionErrorItemVo> errorItems = new ArrayList<>();
|
||||||
|
|
||||||
|
public int getSuccessCount() {
|
||||||
|
return successCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSuccessCount(int successCount) {
|
||||||
|
this.successCount = successCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getErrorCount() {
|
||||||
|
return errorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setErrorCount(int errorCount) {
|
||||||
|
this.errorCount = errorCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTotalCount() {
|
||||||
|
return totalCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalCount(int totalCount) {
|
||||||
|
this.totalCount = totalCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SysAccountBatchActionErrorItemVo> getErrorItems() {
|
||||||
|
return errorItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setErrorItems(List<SysAccountBatchActionErrorItemVo> errorItems) {
|
||||||
|
this.errorItems = errorItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,12 @@ import com.mybatisflex.core.service.IService;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.system.entity.SysAccount;
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
|
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo;
|
||||||
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
||||||
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户表 服务层。
|
* 用户表 服务层。
|
||||||
@@ -23,6 +25,10 @@ public interface SysAccountService extends IService<SysAccount> {
|
|||||||
|
|
||||||
void resetPassword(BigInteger accountId, BigInteger operatorId);
|
void resetPassword(BigInteger accountId, BigInteger operatorId);
|
||||||
|
|
||||||
|
SysAccountBatchActionResultVo removeBatchWithResult(Collection<BigInteger> ids);
|
||||||
|
|
||||||
|
SysAccountBatchActionResultVo resetPasswordBatch(Collection<BigInteger> ids, BigInteger operatorId);
|
||||||
|
|
||||||
SysAccountImportResultVo importAccounts(MultipartFile file, LoginAccount loginAccount);
|
SysAccountImportResultVo importAccounts(MultipartFile file, LoginAccount loginAccount);
|
||||||
|
|
||||||
void writeImportTemplate(OutputStream outputStream);
|
void writeImportTemplate(OutputStream outputStream);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import tech.easyflow.system.entity.SysAccountRole;
|
|||||||
import tech.easyflow.system.entity.SysDept;
|
import tech.easyflow.system.entity.SysDept;
|
||||||
import tech.easyflow.system.entity.SysPosition;
|
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.SysAccountBatchActionResultVo;
|
||||||
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;
|
||||||
@@ -42,6 +44,7 @@ import java.io.OutputStream;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -164,13 +167,7 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
if (record == null) {
|
if (record == null) {
|
||||||
throw new BusinessException("用户不存在");
|
throw new BusinessException("用户不存在");
|
||||||
}
|
}
|
||||||
Integer accountType = record.getAccountType();
|
validateResetPasswordAllowed(record);
|
||||||
if (EnumAccountType.SUPER_ADMIN.getCode().equals(accountType)) {
|
|
||||||
throw new BusinessException("不能重置超级管理员密码");
|
|
||||||
}
|
|
||||||
if (EnumAccountType.TENANT_ADMIN.getCode().equals(accountType)) {
|
|
||||||
throw new BusinessException("不能重置租户管理员密码");
|
|
||||||
}
|
|
||||||
SysAccount update = new SysAccount();
|
SysAccount update = new SysAccount();
|
||||||
update.setId(accountId);
|
update.setId(accountId);
|
||||||
update.setPassword(BCrypt.hashpw(DEFAULT_RESET_PASSWORD));
|
update.setPassword(BCrypt.hashpw(DEFAULT_RESET_PASSWORD));
|
||||||
@@ -181,6 +178,62 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
StpUtil.kickout(accountId);
|
StpUtil.kickout(accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SysAccountBatchActionResultVo removeBatchWithResult(Collection<BigInteger> ids) {
|
||||||
|
List<BigInteger> accountIds = normalizeAccountIds(ids);
|
||||||
|
SysAccountBatchActionResultVo result = initBatchActionResult(accountIds);
|
||||||
|
if (accountIds.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (BigInteger accountId : accountIds) {
|
||||||
|
SysAccount record = getById(accountId);
|
||||||
|
if (record == null) {
|
||||||
|
appendBatchError(result, accountId, null, "用户不存在");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
validateRemoveAllowed(record);
|
||||||
|
executeInRowTransaction(() -> {
|
||||||
|
boolean success = removeById(accountId);
|
||||||
|
if (!success) {
|
||||||
|
throw new BusinessException("删除失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
result.setSuccessCount(result.getSuccessCount() + 1);
|
||||||
|
} catch (Exception e) {
|
||||||
|
appendBatchError(result, accountId, record.getLoginName(), extractErrorMessage(e, "删除失败"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.setErrorCount(result.getErrorItems().size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SysAccountBatchActionResultVo resetPasswordBatch(Collection<BigInteger> ids, BigInteger operatorId) {
|
||||||
|
List<BigInteger> accountIds = normalizeAccountIds(ids);
|
||||||
|
SysAccountBatchActionResultVo result = initBatchActionResult(accountIds);
|
||||||
|
if (accountIds.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (BigInteger accountId : accountIds) {
|
||||||
|
SysAccount record = getById(accountId);
|
||||||
|
if (record == null) {
|
||||||
|
appendBatchError(result, accountId, null, "用户不存在");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
executeInRowTransaction(() -> resetPassword(accountId, operatorId));
|
||||||
|
result.setSuccessCount(result.getSuccessCount() + 1);
|
||||||
|
} catch (Exception e) {
|
||||||
|
appendBatchError(result, accountId, record.getLoginName(), extractErrorMessage(e, "重置密码失败"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.setErrorCount(result.getErrorItems().size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysAccountImportResultVo importAccounts(MultipartFile file, LoginAccount loginAccount) {
|
public SysAccountImportResultVo importAccounts(MultipartFile file, LoginAccount loginAccount) {
|
||||||
validateImportFile(file);
|
validateImportFile(file);
|
||||||
@@ -418,9 +471,21 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
result.getErrorRows().add(errorRow);
|
result.getErrorRows().add(errorRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void appendBatchError(SysAccountBatchActionResultVo result, BigInteger id, String loginName, String reason) {
|
||||||
|
SysAccountBatchActionErrorItemVo errorItem = new SysAccountBatchActionErrorItemVo();
|
||||||
|
errorItem.setId(id);
|
||||||
|
errorItem.setLoginName(loginName);
|
||||||
|
errorItem.setReason(reason);
|
||||||
|
result.getErrorItems().add(errorItem);
|
||||||
|
}
|
||||||
|
|
||||||
private String extractImportErrorMessage(Exception e) {
|
private String extractImportErrorMessage(Exception e) {
|
||||||
|
return extractErrorMessage(e, "导入失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractErrorMessage(Exception e, String defaultMessage) {
|
||||||
if (e == null) {
|
if (e == null) {
|
||||||
return "导入失败";
|
return defaultMessage;
|
||||||
}
|
}
|
||||||
if (e.getCause() != null && StringUtil.hasText(e.getCause().getMessage())) {
|
if (e.getCause() != null && StringUtil.hasText(e.getCause().getMessage())) {
|
||||||
return e.getCause().getMessage();
|
return e.getCause().getMessage();
|
||||||
@@ -428,7 +493,46 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
if (StringUtil.hasText(e.getMessage())) {
|
if (StringUtil.hasText(e.getMessage())) {
|
||||||
return e.getMessage();
|
return e.getMessage();
|
||||||
}
|
}
|
||||||
return "导入失败";
|
return defaultMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateResetPasswordAllowed(SysAccount record) {
|
||||||
|
Integer accountType = record.getAccountType();
|
||||||
|
if (EnumAccountType.SUPER_ADMIN.getCode().equals(accountType)) {
|
||||||
|
throw new BusinessException("不能重置超级管理员密码");
|
||||||
|
}
|
||||||
|
if (EnumAccountType.TENANT_ADMIN.getCode().equals(accountType)) {
|
||||||
|
throw new BusinessException("不能重置租户管理员密码");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRemoveAllowed(SysAccount record) {
|
||||||
|
Integer accountType = record.getAccountType();
|
||||||
|
if (EnumAccountType.SUPER_ADMIN.getCode().equals(accountType)) {
|
||||||
|
throw new BusinessException("不能删除超级管理员");
|
||||||
|
}
|
||||||
|
if (EnumAccountType.TENANT_ADMIN.getCode().equals(accountType)) {
|
||||||
|
throw new BusinessException("不能删除租户管理员");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BigInteger> normalizeAccountIds(Collection<BigInteger> ids) {
|
||||||
|
if (ids == null || ids.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
LinkedHashSet<BigInteger> uniqueIds = new LinkedHashSet<>();
|
||||||
|
for (BigInteger id : ids) {
|
||||||
|
if (id != null) {
|
||||||
|
uniqueIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(uniqueIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysAccountBatchActionResultVo initBatchActionResult(List<BigInteger> accountIds) {
|
||||||
|
SysAccountBatchActionResultVo result = new SysAccountBatchActionResultVo();
|
||||||
|
result.setTotalCount(accountIds.size());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<List<String>> buildImportHeadList() {
|
private List<List<String>> buildImportHeadList() {
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ public class MybatisConfig implements MyBatisFlexCustomizer {
|
|||||||
//开启审计功能
|
//开启审计功能
|
||||||
AuditManager.setAuditEnable(true);
|
AuditManager.setAuditEnable(true);
|
||||||
|
|
||||||
|
// 统一使用标准 0/1 逻辑删除语义。
|
||||||
|
flexGlobalConfig.setNormalValueOfLogicDelete(0);
|
||||||
|
flexGlobalConfig.setDeletedValueOfLogicDelete(1);
|
||||||
|
|
||||||
//取消控制台的 Banner 打印
|
//取消控制台的 Banner 打印
|
||||||
flexGlobalConfig.setPrintBanner(false);
|
flexGlobalConfig.setPrintBanner(false);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE `tb_sys_account`
|
||||||
|
ADD COLUMN `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除标识:0未删除,1已删除' AFTER `remark`;
|
||||||
|
|
||||||
|
ALTER TABLE `tb_sys_account`
|
||||||
|
DROP INDEX `uni_login_name`,
|
||||||
|
ADD INDEX `idx_login_name` (`login_name`) USING BTREE;
|
||||||
@@ -115,6 +115,9 @@ const handleDropdownClick = (button) => {
|
|||||||
<ElButton auto-insert-space @click="handleReset">
|
<ElButton auto-insert-space @click="handleReset">
|
||||||
{{ $t('button.reset') }}
|
{{ $t('button.reset') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
<div v-if="$slots.middle" class="search-middle">
|
||||||
|
<slot name="middle"></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -193,6 +196,12 @@ const handleDropdownClick = (button) => {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-middle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
width: min(360px, 100%);
|
width: min(360px, 100%);
|
||||||
}
|
}
|
||||||
@@ -267,6 +276,10 @@ const handleDropdownClick = (button) => {
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-middle {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,20 @@
|
|||||||
"resetPassword": "Reset Password",
|
"resetPassword": "Reset Password",
|
||||||
"resetPasswordConfirm": "Reset this account password to 123456? The user will be required to change it on next login.",
|
"resetPasswordConfirm": "Reset this account password to 123456? The user will be required to change it on next login.",
|
||||||
"resetPasswordSuccess": "Password has been reset to 123456 and must be changed on next login",
|
"resetPasswordSuccess": "Password has been reset to 123456 and must be changed on next login",
|
||||||
|
"batchSelectedCount": "{count} selected",
|
||||||
|
"batchToolbarHint": "Batch actions are available for selected accounts",
|
||||||
|
"batchActionSelectRequired": "Please select at least one account",
|
||||||
|
"batchActionFailed": "Operation failed. Please try again later.",
|
||||||
|
"batchDelete": "Batch Delete",
|
||||||
|
"batchDeleteConfirm": "Delete the selected {count} accounts? Protected administrator accounts will be skipped and returned as failures.",
|
||||||
|
"batchDeleteSuccess": "Batch delete completed. {count} accounts were removed.",
|
||||||
|
"batchDeletePartialSuccess": "Batch delete completed. {successCount} succeeded and {errorCount} failed.",
|
||||||
|
"batchDeleteAllFailed": "Batch delete failed",
|
||||||
|
"batchResetPassword": "Batch Reset Password",
|
||||||
|
"batchResetPasswordConfirm": "Reset the selected {count} accounts to 123456? Users must change it on next login, and protected administrator accounts will be skipped.",
|
||||||
|
"batchResetPasswordSuccess": "{count} account passwords have been reset",
|
||||||
|
"batchResetPasswordPartialSuccess": "Batch password reset completed. {successCount} succeeded and {errorCount} 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. Import only creates users and duplicate accounts will fail.",
|
||||||
|
|||||||
@@ -30,6 +30,20 @@
|
|||||||
"resetPassword": "重置密码",
|
"resetPassword": "重置密码",
|
||||||
"resetPasswordConfirm": "确认将该用户密码重置为 123456 吗?重置后用户下次登录必须先修改密码。",
|
"resetPasswordConfirm": "确认将该用户密码重置为 123456 吗?重置后用户下次登录必须先修改密码。",
|
||||||
"resetPasswordSuccess": "密码已重置为 123456,用户下次登录需修改密码",
|
"resetPasswordSuccess": "密码已重置为 123456,用户下次登录需修改密码",
|
||||||
|
"batchSelectedCount": "已选择 {count} 项",
|
||||||
|
"batchToolbarHint": "可对选中账号执行批量操作",
|
||||||
|
"batchActionSelectRequired": "请先选择要操作的账号",
|
||||||
|
"batchActionFailed": "操作失败,请稍后重试",
|
||||||
|
"batchDelete": "批量删除",
|
||||||
|
"batchDeleteConfirm": "确认批量删除已选中的 {count} 个账号吗?其中管理员账号将跳过并返回失败结果。",
|
||||||
|
"batchDeleteSuccess": "批量删除完成,共成功删除 {count} 个账号",
|
||||||
|
"batchDeletePartialSuccess": "批量删除完成,成功 {successCount} 个,失败 {errorCount} 个",
|
||||||
|
"batchDeleteAllFailed": "批量删除失败",
|
||||||
|
"batchResetPassword": "批量重置密码",
|
||||||
|
"batchResetPasswordConfirm": "确认将已选中的 {count} 个账号密码重置为 123456 吗?重置后用户下次登录必须先修改密码,管理员账号将跳过并返回失败结果。",
|
||||||
|
"batchResetPasswordSuccess": "已完成 {count} 个账号密码重置",
|
||||||
|
"batchResetPasswordPartialSuccess": "批量重置密码完成,成功 {successCount} 个,失败 {errorCount} 个",
|
||||||
|
"batchResetPasswordAllFailed": "批量重置密码失败",
|
||||||
"importTitle": "导入用户",
|
"importTitle": "导入用户",
|
||||||
"importUploadTitle": "拖拽 Excel 文件到此处,或点击选择文件",
|
"importUploadTitle": "拖拽 Excel 文件到此处,或点击选择文件",
|
||||||
"importUploadDesc": "仅支持 .xlsx / .xls,导入只新增用户,重复账号会报错",
|
"importUploadDesc": "仅支持 .xlsx / .xls,导入只新增用户,重复账号会报错",
|
||||||
|
|||||||
@@ -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 { markRaw, onMounted, ref } from 'vue';
|
import { computed, markRaw, nextTick, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Delete,
|
Delete,
|
||||||
@@ -38,9 +38,13 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const pageDataRef = ref();
|
const pageDataRef = ref();
|
||||||
|
const tableRef = ref();
|
||||||
const importDialog = ref();
|
const importDialog = ref();
|
||||||
const saveDialog = ref();
|
const saveDialog = ref();
|
||||||
|
const selectedRows = ref<any[]>([]);
|
||||||
|
const batchActionLoading = ref(false);
|
||||||
const dictStore = useDictStore();
|
const dictStore = useDictStore();
|
||||||
|
const selectedCount = computed(() => selectedRows.value.length);
|
||||||
const headerButtons = [
|
const headerButtons = [
|
||||||
{
|
{
|
||||||
key: 'create',
|
key: 'create',
|
||||||
@@ -65,8 +69,18 @@ function initDict() {
|
|||||||
const handleSearch = (params: string) => {
|
const handleSearch = (params: string) => {
|
||||||
pageDataRef.value.setQuery({ loginName: params, isQueryOr: true });
|
pageDataRef.value.setQuery({ loginName: params, isQueryOr: true });
|
||||||
};
|
};
|
||||||
|
function clearSelection() {
|
||||||
|
selectedRows.value = [];
|
||||||
|
tableRef.value?.clearSelection?.();
|
||||||
|
}
|
||||||
|
async function reloadPage() {
|
||||||
|
await pageDataRef.value?.reload?.();
|
||||||
|
await nextTick();
|
||||||
|
clearSelection();
|
||||||
|
}
|
||||||
function reset(formEl?: FormInstance) {
|
function reset(formEl?: FormInstance) {
|
||||||
formEl?.resetFields();
|
formEl?.resetFields();
|
||||||
|
clearSelection();
|
||||||
pageDataRef.value.setQuery({});
|
pageDataRef.value.setQuery({});
|
||||||
}
|
}
|
||||||
function showDialog(row: any) {
|
function showDialog(row: any) {
|
||||||
@@ -96,7 +110,7 @@ function remove(row: any) {
|
|||||||
instance.confirmButtonLoading = false;
|
instance.confirmButtonLoading = false;
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success(res.message);
|
ElMessage.success(res.message);
|
||||||
reset();
|
void reloadPage();
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -126,6 +140,7 @@ function resetPassword(row: any) {
|
|||||||
instance.confirmButtonLoading = false;
|
instance.confirmButtonLoading = false;
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success($t('sysAccount.resetPasswordSuccess'));
|
ElMessage.success($t('sysAccount.resetPasswordSuccess'));
|
||||||
|
clearSelection();
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -139,6 +154,103 @@ function resetPassword(row: any) {
|
|||||||
},
|
},
|
||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
}
|
}
|
||||||
|
function handleSelectionChange(rows: any[]) {
|
||||||
|
selectedRows.value = rows;
|
||||||
|
}
|
||||||
|
function getSelectedIds() {
|
||||||
|
return selectedRows.value
|
||||||
|
.map((row) => row?.id)
|
||||||
|
.filter((id) => id !== null && id !== undefined);
|
||||||
|
}
|
||||||
|
function getFirstBatchErrorReason(result: any) {
|
||||||
|
return result?.errorItems?.[0]?.reason;
|
||||||
|
}
|
||||||
|
function notifyBatchActionResult(action: 'delete' | 'reset', result: any) {
|
||||||
|
const successCount = result?.successCount || 0;
|
||||||
|
const errorCount = result?.errorCount || 0;
|
||||||
|
const firstReason = getFirstBatchErrorReason(result);
|
||||||
|
if (errorCount === 0) {
|
||||||
|
ElMessage.success(
|
||||||
|
action === 'delete'
|
||||||
|
? $t('sysAccount.batchDeleteSuccess', { count: successCount })
|
||||||
|
: $t('sysAccount.batchResetPasswordSuccess', { count: successCount }),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (successCount === 0) {
|
||||||
|
ElMessage.error(
|
||||||
|
firstReason ||
|
||||||
|
(action === 'delete'
|
||||||
|
? $t('sysAccount.batchDeleteAllFailed')
|
||||||
|
: $t('sysAccount.batchResetPasswordAllFailed')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ElMessage.warning(
|
||||||
|
action === 'delete'
|
||||||
|
? $t('sysAccount.batchDeletePartialSuccess', {
|
||||||
|
successCount,
|
||||||
|
errorCount,
|
||||||
|
})
|
||||||
|
: $t('sysAccount.batchResetPasswordPartialSuccess', {
|
||||||
|
successCount,
|
||||||
|
errorCount,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async function submitBatchAction(
|
||||||
|
url: string,
|
||||||
|
action: 'delete' | 'reset',
|
||||||
|
confirmMessage: string,
|
||||||
|
) {
|
||||||
|
const ids = getSelectedIds();
|
||||||
|
if (ids.length === 0) {
|
||||||
|
ElMessage.warning($t('sysAccount.batchActionSelectRequired'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(confirmMessage, $t('message.noticeTitle'), {
|
||||||
|
confirmButtonText: $t('message.ok'),
|
||||||
|
cancelButtonText: $t('message.cancel'),
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
batchActionLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await api.post(url, { ids });
|
||||||
|
if (res.errorCode !== 0) {
|
||||||
|
ElMessage.error(res.message || $t('sysAccount.batchActionFailed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notifyBatchActionResult(action, res.data || {});
|
||||||
|
if (action === 'delete' && (res.data?.successCount || 0) > 0) {
|
||||||
|
await reloadPage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearSelection();
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error?.message || $t('sysAccount.batchActionFailed'));
|
||||||
|
} finally {
|
||||||
|
batchActionLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function batchDelete() {
|
||||||
|
void submitBatchAction(
|
||||||
|
'/api/v1/sysAccount/removeBatchWithResult',
|
||||||
|
'delete',
|
||||||
|
$t('sysAccount.batchDeleteConfirm', { count: selectedCount.value }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function batchResetPassword() {
|
||||||
|
void submitBatchAction(
|
||||||
|
'/api/v1/sysAccount/resetPasswordBatch',
|
||||||
|
'reset',
|
||||||
|
$t('sysAccount.batchResetPasswordConfirm', { count: selectedCount.value }),
|
||||||
|
);
|
||||||
|
}
|
||||||
function isAdmin(data: any) {
|
function isAdmin(data: any) {
|
||||||
return data?.accountType === 1 || data?.accountType === 99;
|
return data?.accountType === 1 || data?.accountType === 99;
|
||||||
}
|
}
|
||||||
@@ -154,7 +266,37 @@ function isAdmin(data: any) {
|
|||||||
:buttons="headerButtons"
|
:buttons="headerButtons"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="handleHeaderButtonClick"
|
@button-click="handleHeaderButtonClick"
|
||||||
/>
|
>
|
||||||
|
<template #middle>
|
||||||
|
<div v-if="selectedCount > 0" class="sys-account-batch-inline">
|
||||||
|
<span class="sys-account-batch-inline__count">
|
||||||
|
{{ $t('sysAccount.batchSelectedCount', { count: selectedCount }) }}
|
||||||
|
</span>
|
||||||
|
<div class="sys-account-batch-inline__actions">
|
||||||
|
<div v-access:code="'/api/v1/sysAccount/save'">
|
||||||
|
<ElButton
|
||||||
|
class="sys-account-batch-inline__button"
|
||||||
|
:icon="Lock"
|
||||||
|
:loading="batchActionLoading"
|
||||||
|
@click="batchResetPassword"
|
||||||
|
>
|
||||||
|
{{ $t('sysAccount.batchResetPassword') }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
<div v-access:code="'/api/v1/sysAccount/remove'">
|
||||||
|
<ElButton
|
||||||
|
class="sys-account-batch-inline__button is-danger"
|
||||||
|
:icon="Delete"
|
||||||
|
:loading="batchActionLoading"
|
||||||
|
@click="batchDelete"
|
||||||
|
>
|
||||||
|
{{ $t('sysAccount.batchDelete') }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</HeaderSearch>
|
||||||
</template>
|
</template>
|
||||||
<PageData
|
<PageData
|
||||||
ref="pageDataRef"
|
ref="pageDataRef"
|
||||||
@@ -162,7 +304,13 @@ function isAdmin(data: any) {
|
|||||||
:page-size="10"
|
:page-size="10"
|
||||||
>
|
>
|
||||||
<template #default="{ pageList }">
|
<template #default="{ pageList }">
|
||||||
<ElTable :data="pageList" border>
|
<ElTable
|
||||||
|
ref="tableRef"
|
||||||
|
:data="pageList"
|
||||||
|
border
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<ElTableColumn type="selection" width="48" />
|
||||||
<ElTableColumn
|
<ElTableColumn
|
||||||
prop="avatar"
|
prop="avatar"
|
||||||
align="center"
|
align="center"
|
||||||
@@ -276,4 +424,68 @@ function isAdmin(data: any) {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.sys-account-batch-inline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sys-account-batch-inline__count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sys-account-batch-inline__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sys-account-batch-inline__actions :deep(.el-button) {
|
||||||
|
height: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
padding-inline: 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.sys-account-batch-inline__button) {
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.sys-account-batch-inline__button:not(.is-disabled):hover),
|
||||||
|
:deep(.sys-account-batch-inline__button:not(.is-disabled):focus-visible) {
|
||||||
|
color: hsl(var(--primary));
|
||||||
|
background: hsl(var(--primary) / 0.08);
|
||||||
|
border-color: hsl(var(--primary) / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.sys-account-batch-inline__button.is-danger) {
|
||||||
|
color: hsl(var(--destructive));
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.sys-account-batch-inline__button.is-danger:not(.is-disabled):hover),
|
||||||
|
:deep(.sys-account-batch-inline__button.is-danger:not(.is-disabled):focus-visible) {
|
||||||
|
color: hsl(var(--destructive));
|
||||||
|
background: hsl(var(--destructive) / 0.08);
|
||||||
|
border-color: hsl(var(--destructive) / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.sys-account-batch-inline {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sys-account-batch-inline__actions {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user