feat(ui): 支持外部邮箱启用解绑与账号管理删除功能,发布V2.12.0部署包
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-10-17 22:38:40 +08:00
parent 583b9e88b7
commit d2fe1f8304
6 changed files with 100 additions and 8 deletions

View File

@ -139,6 +139,11 @@ export async function packageRelease(selectedTargets = targets, { workspace = ro
const legacyArchive = path.join(releaseRoot, legacyArchiveName)
try { await copyFile(archive, legacyArchive) } catch (e) {}
}
// 同步复制到 out 根目录,满足直接从 out 目录引用的部署习惯
try {
await copyFile(archive, path.join(outputRoot, archiveName))
await copyFile(archive, path.join(outputRoot, legacyArchiveName))
} catch (e) {}
await rename(stagedReceipt, receipt)
await rename(staging, directory)
releases.push({ target, directory, archive, receipt })

View File

@ -1401,4 +1401,19 @@
max-width: none !important;
overflow: visible !important;
}
}
}
.external-item-actions {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-shrink: 0;
}
.external-item-actions :deep(.el-button) {
display: inline-flex;
align-items: center;
justify-content: center;
margin: 0;
white-space: nowrap;
}

View File

@ -337,7 +337,28 @@ export function usePage() {
notify('外部安全邮箱绑定成功', 'success')
}
const disableExternalEmail = async item => {
const rebindExternalEmail = (item) => {
openExternalEmailDialog()
}
const deleteExternalEmail = async item => {
try {
await ElMessageBox.confirm(`确定彻底删除/解绑安全邮箱 ${item.email} 吗?解绑后将移除该邮箱记录。`, '删除外部邮箱', {
type: 'warning',
confirmButtonText: '确定删除',
cancelButtonText: '取消'
})
const res = await http.post('/api/v1/security/external-email/delete', { id: item.id })
if (res.errorNo !== 0) throw new Error(res.errorMsg || '删除外部邮箱失败')
await loadSecurityData()
await globalStatus.refresh()
notify('外部安全邮箱已删除', 'success')
} catch (error) {
if (error !== 'cancel' && error !== 'close') notify(error?.message || '删除外部邮箱失败')
}
}
const disableExternalEmail = async item => {
try {
await ElMessageBox.confirm(`确定停用 ${item.email} 吗?停用后不能用于登录验证和恢复。`, '停用外部邮箱', { type: 'warning' })
const res = await http.post('/api/v1/security/external-email/disable', { id: item.id })
@ -451,6 +472,6 @@ export function usePage() {
externalDialogVisible, ruleForm, rules, submit, registerCredential, renameCredential, revokeCredential,
saveSecurityQuestion, generateRecoveryCodes, revealRecoveryCodes, saveRecoveryCodes, revokeRecoveryCodes, openExternalEmailDialog,
handleExternalEmailBound,
disableExternalEmail, formatTime,
rebindExternalEmail, deleteExternalEmail, disableExternalEmail, formatTime,
}
}

View File

@ -136,7 +136,11 @@
<div v-if="externalEmails.length" v-loading="externalListLoading" class="external-list">
<div v-for="item in externalEmails" :key="item.id" class="external-item">
<div><strong>{{ item.email }}</strong><span>{{ item.type === 'primary' ? '主要邮箱' : '备用邮箱' }}</span><el-tag size="small" :type="item.status === 'verified' ? 'success' : 'warning'">{{ item.status === 'verified' ? '已验证' : item.status === 'pending' ? '待验证' : '已停用' }}</el-tag></div>
<el-button v-if="item.status !== 'disabled'" type="danger" plain size="small" @click="disableExternalEmail(item)">停用</el-button>
<div class="external-item-actions">
<el-button v-if="item.status === 'disabled'" type="primary" plain size="small" @click="rebindExternalEmail(item)">启用</el-button>
<el-button v-else type="warning" plain size="small" @click="disableExternalEmail(item)">停用</el-button>
<el-button type="danger" plain size="small" @click="deleteExternalEmail(item)">删除</el-button>
</div>
</div>
</div>
<el-empty v-else v-loading="externalListLoading" description="暂无外部安全邮箱" />

View File

@ -4,7 +4,7 @@ import lang from '../../../i18n/i18n';
import {http} from "@/utils/axios";
import {ElNotification} from "element-plus";
import {ElNotification, ElMessageBox} from "element-plus";
import {Plus} from "@element-plus/icons-vue";
@ -283,7 +283,41 @@ export function usePage() {
reflushList()
return {
const canDeleteUser = user => {
return canEditUser(user) && !isSameUser(user, globalStatus.userInfos)
}
const handleDeleteUser = async user => {
if (!canDeleteUser(user)) {
ElNotification({ type: 'error', message: '无法删除该用户' })
return
}
try {
await ElMessageBox.confirm(
`确定彻底删除用户 "${user.Account}" 吗?此操作将永久清理该用户的所有关联配置、安全密钥与绑定数据,且不可逆!`,
'删除用户警告',
{
type: 'warning',
confirmButtonText: '确定删除',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--danger'
}
)
const res = await http.post('/api/v1/user/delete', { id: user.ID, account: user.Account })
if (res.errorNo !== 0) {
throw new Error(res.errorMsg || '删除用户失败')
}
ElNotification({ type: 'success', message: `用户 ${user.Account} 已删除` })
await reflushList()
await globalStatus.refresh()
} catch (error) {
if (error !== 'cancel' && error !== 'close') {
ElNotification({ type: 'error', message: error?.message || '删除用户失败' })
}
}
}
return {
reactive,
ref,
lang,
@ -320,6 +354,8 @@ export function usePage() {
handleEdit,
canEditUser,
createUser,
canDeleteUser,
handleDeleteUser,
submit,
roleMeta,
recoveryCodeMeta,
@ -331,4 +367,4 @@ export function usePage() {
export {
Plus,
roleMeta
}
}

View File

@ -84,7 +84,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="right" :width="canManageMembership ? 126 : 68">
<el-table-column label="操作" align="right" :width="canManageMembership ? 186 : 126">
<template #default="scope">
<el-button
size="small"
@ -97,6 +97,17 @@
>
编辑
</el-button>
<el-button
size="small"
type="danger"
text
bg
@click="handleDeleteUser(scope.row)"
:disabled="!canDeleteUser(scope.row)"
class="action-btn"
>
删除
</el-button>
<el-button v-if="canManageMembership" size="small" text :disabled="groupSaving" @click="editMembership(scope.row)">分组</el-button>
</template>
</el-table-column>