feat: 支持展示并复制重置后的密码

- 账号重置接口返回本次使用的默认强密码

- 管理端新增紧凑结果弹窗与复制反馈

- 补充服务测试和中英文文案
This commit is contained in:
2026-07-30 14:17:44 +08:00
parent 766554bf63
commit 5f11219226
8 changed files with 253 additions and 8 deletions

View File

@@ -29,6 +29,11 @@
"resetPassword": "Reset Password",
"resetPasswordConfirm": "Reset this account password to the system default strong password? The user will be required to change it on next login.",
"resetPasswordSuccess": "Password has been reset to the system default strong password and must be changed on next login",
"resetPasswordResultTitle": "Password Reset Successful",
"resetPasswordResultDescription": "Copy the password and share it securely. The user must change it at the next sign-in.",
"resetPasswordResultLabel": "New Password",
"resetPasswordResultMissing": "The password was reset, but the new password was not returned. Contact an administrator.",
"copyPassword": "Copy password",
"batchSelectedCount": "{count} selected",
"batchToolbarHint": "Batch actions are available for selected accounts",
"batchActionSelectRequired": "Please select at least one account",

View File

@@ -30,6 +30,11 @@
"resetPassword": "重置密码",
"resetPasswordConfirm": "确认将该用户密码重置为系统默认强密码吗?重置后用户下次登录必须先修改密码。",
"resetPasswordSuccess": "密码已重置为系统默认强密码,用户下次登录需修改密码",
"resetPasswordResultTitle": "密码重置成功",
"resetPasswordResultDescription": "请复制密码并安全地告知用户,用户下次登录时需修改密码。",
"resetPasswordResultLabel": "新密码",
"resetPasswordResultMissing": "密码已重置,但接口未返回新密码,请联系管理员",
"copyPassword": "复制密码",
"batchSelectedCount": "已选择 {count} 项",
"batchToolbarHint": "可对选中账号执行批量操作",
"batchActionSelectRequired": "请先选择要操作的账号",

View File

@@ -32,6 +32,7 @@ import { useDictStore } from '#/store';
import SysAccountImportModal from './SysAccountImportModal.vue';
import SysAccountModal from './SysAccountModal.vue';
import SysAccountPasswordResultModal from './SysAccountPasswordResultModal.vue';
onMounted(() => {
initDict();
@@ -40,6 +41,7 @@ onMounted(() => {
const pageDataRef = ref();
const tableRef = ref();
const importDialog = ref();
const passwordResultDialog = ref();
const saveDialog = ref();
const selectedRows = ref<any[]>([]);
const batchActionLoading = ref(false);
@@ -124,6 +126,7 @@ function remove(row: any) {
}).catch(() => {});
}
function resetPassword(row: any) {
let plainPassword = '';
ElMessageBox.confirm(
$t('sysAccount.resetPasswordConfirm'),
$t('message.noticeTitle'),
@@ -139,7 +142,7 @@ function resetPassword(row: any) {
.then((res) => {
instance.confirmButtonLoading = false;
if (res.errorCode === 0) {
ElMessage.success($t('sysAccount.resetPasswordSuccess'));
plainPassword = typeof res.data === 'string' ? res.data : '';
clearSelection();
done();
}
@@ -152,7 +155,15 @@ function resetPassword(row: any) {
}
},
},
).catch(() => {});
)
.then(() => {
if (!plainPassword) {
ElMessage.error($t('sysAccount.resetPasswordResultMissing'));
return;
}
passwordResultDialog.value?.openDialog(plainPassword);
})
.catch(() => {});
}
function handleSelectionChange(rows: any[]) {
selectedRows.value = rows;
@@ -260,6 +271,7 @@ function isAdmin(data: any) {
<div class="flex h-full flex-col gap-6 p-6">
<SysAccountImportModal ref="importDialog" @reload="reset" />
<SysAccountModal ref="saveDialog" @reload="reset" />
<SysAccountPasswordResultModal ref="passwordResultDialog" />
<ListPageShell>
<template #filters>
<HeaderSearch

View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { EasyFlowPanelModal } from '@easyflow/common-ui';
import { CopyDocument } from '@element-plus/icons-vue';
import { ElButton, ElInput } from 'element-plus';
import { $t } from '#/locales';
import { copyTextWithFeedback } from '#/utils/clipboard-feedback';
const dialogVisible = ref(false);
const password = ref('');
watch(dialogVisible, (visible) => {
if (!visible) {
password.value = '';
}
});
function openDialog(value: string) {
password.value = value;
dialogVisible.value = true;
}
function closeDialog() {
dialogVisible.value = false;
}
function selectPassword(event: FocusEvent) {
if (event.target instanceof HTMLInputElement) {
event.target.select();
}
}
async function copyPassword() {
await copyTextWithFeedback(
password.value,
$t('message.copySuccess'),
$t('message.copyFail'),
);
}
defineExpose({
openDialog,
});
</script>
<template>
<EasyFlowPanelModal
v-model:open="dialogVisible"
centered
width="482px"
:confirm-text="$t('message.ok')"
:description="$t('sysAccount.resetPasswordResultDescription')"
:show-cancel-button="false"
:title="$t('sysAccount.resetPasswordResultTitle')"
@confirm="closeDialog"
>
<div class="password-result">
<label class="password-result__label" for="reset-password-result">
{{ $t('sysAccount.resetPasswordResultLabel') }}
</label>
<ElInput
id="reset-password-result"
class="password-result__input"
:model-value="password"
readonly
size="large"
@focus="selectPassword"
>
<template #suffix>
<ElButton
class="password-result__copy"
:aria-label="$t('sysAccount.copyPassword')"
:icon="CopyDocument"
link
:title="$t('sysAccount.copyPassword')"
@click="copyPassword"
/>
</template>
</ElInput>
</div>
</EasyFlowPanelModal>
</template>
<style scoped>
.password-result {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.password-result__label {
font-size: 13px;
font-weight: 600;
line-height: 20px;
color: hsl(var(--text-strong));
}
:deep(.password-result__input .el-input__wrapper) {
padding-inline: var(--space-4) var(--space-2);
background: hsl(var(--surface-subtle));
border-radius: var(--radius-control);
box-shadow: 0 0 0 1px hsl(var(--line-subtle)) inset;
transition:
background-color var(--motion-duration-fast) var(--motion-ease-standard),
box-shadow var(--motion-duration-fast) var(--motion-ease-standard);
}
:deep(.password-result__input .el-input__wrapper:hover) {
box-shadow: 0 0 0 1px hsl(var(--border)) inset;
}
:deep(.password-result__input .el-input__wrapper.is-focus) {
background: hsl(var(--surface-panel));
box-shadow: 0 0 0 2px hsl(var(--primary) / 18%) inset;
}
:deep(.password-result__input .el-input__inner) {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.04em;
color: hsl(var(--text-strong));
}
:deep(.password-result__copy) {
width: 32px;
height: 32px;
padding: 0;
font-size: 16px;
color: hsl(var(--text-muted));
border-radius: var(--radius-control);
}
:deep(.password-result__copy:not(.is-disabled):hover),
:deep(.password-result__copy:not(.is-disabled):focus-visible) {
color: hsl(var(--primary));
background: hsl(var(--primary) / 8%);
}
</style>