refactor(account): 账户弹窗与退出确认改用页面通用弹窗外壳

This commit is contained in:
yelan
2026-10-07 10:24:40 +08:00
parent 2a296d470a
commit ee7e90ab01
3 changed files with 488 additions and 144 deletions
+199 -25
View File
@@ -1,14 +1,14 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import AccountPanel from './AccountPanel.vue'
import { RouterLink } from 'vue-router'
import { ElMessageBox } from 'element-plus'
import { useLoginStore } from '@/stores/login'
const loginStore = useLoginStore()
const accountPanel = ref<InstanceType<typeof AccountPanel>>()
const accountMenuOpen = ref(false)
const logoutConfirmOpen = ref(false)
const loggingOut = ref(false)
const identity = computed(() => {
const labels = [
@@ -46,6 +46,18 @@ const visibleMenuGroups = computed(() =>
.filter((group) => group.items.length),
)
/** 打开二次确认(确认弹窗用页面通用样式,不再走 ElMessageBox 的 Element 默认外观) */
function requestLogout() {
if (loggingOut.value) return
accountMenuOpen.value = false
logoutConfirmOpen.value = true
}
function cancelLogout() {
if (loggingOut.value) return
logoutConfirmOpen.value = false
}
/**
* 退出登录:二次确认 → 注销服务端会话 → 清掉本地身份数据 → **整页**回登录页。
*
@@ -58,21 +70,8 @@ const visibleMenuGroups = computed(() =>
* 本地身份数据(token / tokenExpiresAt / userInfo,其中 userInfo 里含 permissions)
* 由 `loginStore.logout()` → `clearSession()` → `clearAuthStorage()` 删除。
*/
async function handleLogout() {
async function confirmLogout() {
if (loggingOut.value) return
accountMenuOpen.value = false
try {
await ElMessageBox.confirm('退出后需要重新登录才能继续使用,确定退出当前账号吗?', '退出登录', {
confirmButtonText: '退出登录',
cancelButtonText: '取消',
type: 'warning',
confirmButtonClass: 'logout-confirm-button',
})
} catch {
// 用户点了取消,保持在当前页面
return
}
loggingOut.value = true
try {
@@ -84,6 +83,18 @@ async function handleLogout() {
// 用 replace 而不是 assign:退出后按浏览器后退不该再回到已登录页面
window.location.replace('/login')
}
/** 确认弹窗挂在 body 上,Esc 关闭要挂 document 才收得到(与 AccountPanel 同一处理) */
function handleEscape(event: KeyboardEvent) {
if (event.key === 'Escape') cancelLogout()
}
watch(logoutConfirmOpen, (value) => {
if (value) document.addEventListener('keydown', handleEscape)
else document.removeEventListener('keydown', handleEscape)
})
onBeforeUnmount(() => document.removeEventListener('keydown', handleEscape))
</script>
<template>
@@ -160,18 +171,59 @@ async function handleLogout() {
<span>工号 {{ loginStore.user?.employeeNo || '—' }}</span>
<span>{{ loginStore.user?.campusName || '—' }} · {{ loginStore.department }}</span>
</div>
<button type="button" @click="openAccount('profile')">
<span aria-hidden="true">👤</span>个人信息
</button>
<button type="button" @click="openAccount('password')">
<span aria-hidden="true">🔑</span>修改密码
</button>
<button class="account-logout" type="button" :disabled="loggingOut" @click="handleLogout">
<span aria-hidden="true">⏻</span>{{ loggingOut ? '退出中…' : '退出登录' }}
<button type="button" @click="openAccount('profile')">个人信息</button>
<button type="button" @click="openAccount('password')">修改密码</button>
<button class="account-logout" type="button" :disabled="loggingOut" @click="requestLogout">
退出登录
</button>
</div>
</el-popover>
<AccountPanel ref="accountPanel" />
<!-- 退出二次确认:与账户弹窗同一套页面通用外壳与按钮(不再用 ElMessageBox 默认外观) -->
<Teleport to="body">
<div v-if="logoutConfirmOpen" class="logout-mask" role="presentation">
<section
class="logout-card"
role="alertdialog"
aria-modal="true"
aria-labelledby="logout-dialog-title"
>
<header class="logout-header">
<strong id="logout-dialog-title">退出登录</strong>
<button
v-if="!loggingOut"
type="button"
class="logout-close"
aria-label="关闭"
@click="cancelLogout"
>
×
</button>
</header>
<div class="logout-body">退出后需要重新登录才能继续使用,确定退出当前账号吗?</div>
<footer class="logout-footer">
<span class="logout-spacer" />
<button
type="button"
class="action-button action-button--ghost"
:disabled="loggingOut"
@click="cancelLogout"
>
取消
</button>
<button
type="button"
class="action-button action-button--danger"
:disabled="loggingOut"
@click="confirmLogout"
>
{{ loggingOut ? '退出中…' : '退出登录' }}
</button>
</footer>
</section>
</div>
</Teleport>
</aside>
</template>
@@ -360,7 +412,6 @@ async function handleLogout() {
.account-actions button {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 9px 12px;
background: transparent;
@@ -378,4 +429,127 @@ async function handleLogout() {
border-top: 1px solid var(--line);
color: #d0534f;
}
/* 退出二次确认弹窗:外壳与按钮沿用页面通用弹窗样式(与 AccountPanel 同一套) */
.logout-mask {
position: fixed;
z-index: 50;
inset: 0;
display: flex;
padding: 16px;
overflow-y: auto;
background: rgb(9 40 52 / 45%);
}
.logout-card {
display: flex;
width: 440px;
max-width: 96vw;
max-height: calc(100vh - 32px);
flex-direction: column;
margin: auto;
overflow: hidden;
background: #fff;
border-radius: 14px;
box-shadow: var(--sh-modal);
animation: logout-pop 0.18s ease;
}
.logout-header {
display: flex;
flex-shrink: 0;
align-items: center;
padding: 12px 18px;
border-bottom: 1px solid var(--line);
}
.logout-header strong {
color: var(--ink);
font-size: 15px;
}
.logout-close {
padding: 0;
margin-left: auto;
color: var(--mut);
font-size: 20px;
line-height: 1;
background: transparent;
}
.logout-close:hover {
color: var(--ink);
}
.logout-body {
min-height: 0;
padding: 16px 18px;
overflow-y: auto;
color: var(--ink);
font-size: 13px;
line-height: 1.7;
}
.logout-footer {
display: flex;
flex-shrink: 0;
gap: 10px;
align-items: center;
padding: 10px 18px;
background: #f8fbfc;
border-top: 1px solid var(--line);
border-radius: 0 0 14px 14px;
}
.logout-spacer {
flex: 1;
}
.action-button {
flex-shrink: 0;
padding: 8px 16px;
color: #fff;
font-size: 13px;
font-weight: 700;
background: var(--brand);
border-radius: 7px;
}
.action-button--ghost {
color: var(--brand);
background: #fff;
border: 1px solid var(--brand);
}
.action-button--ghost:hover:not(:disabled) {
background: var(--brand-l);
}
.action-button--danger {
color: var(--err);
background: #fff;
border: 1px solid var(--err);
}
.action-button--danger:hover:not(:disabled) {
background: var(--err-l);
}
.action-button:disabled {
color: #9aabb3;
cursor: not-allowed;
background: #eef3f5;
}
@keyframes logout-pop {
from {
opacity: 0;
transform: scale(0.96) translateY(8px);
}
to {
opacity: 1;
transform: none;
}
}
</style>