refactor(account): 账户弹窗与退出确认改用页面通用弹窗外壳
This commit is contained in:
@@ -1,137 +1,300 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { onBeforeUnmount, watch } from 'vue'
|
||||||
|
|
||||||
import { useAccountPanel } from '@/composables/useAccountPanel'
|
import { useAccountPanel } from '@/composables/useAccountPanel'
|
||||||
|
|
||||||
const { panel, profile, loading, saving, error, contacts, password, strength, open, close, save } =
|
const { panel, profile, loading, saving, error, contacts, password, strength, open, close, save } =
|
||||||
useAccountPanel()
|
useAccountPanel()
|
||||||
defineExpose({ open })
|
defineExpose({ open })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗挂在 body 上,卡片本身不持有焦点,`@keydown` 收不到按键,
|
||||||
|
* 所以 Esc 关闭(原先由 el-dialog 的 `close-on-press-escape` 提供)要挂在 document 上。
|
||||||
|
* `close()` 内部已挡掉保存中,这里不必再判一次。
|
||||||
|
*/
|
||||||
|
function handleEscape(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') close()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(panel, (value) => {
|
||||||
|
if (value) document.addEventListener('keydown', handleEscape)
|
||||||
|
else document.removeEventListener('keydown', handleEscape)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => document.removeEventListener('keydown', handleEscape))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<!-- 外壳走页面通用弹窗样式(与作废确认、文书预览等同一套 .dialog-* / .action-button),
|
||||||
:model-value="panel !== null"
|
不再用 Element Plus 默认的 el-dialog / el-button —— 那套默认蓝与全局品牌色不一致。 -->
|
||||||
:title="panel === 'profile' ? '个人信息' : '修改密码'"
|
<Teleport to="body">
|
||||||
:width="panel === 'profile' ? '560px' : '460px'"
|
<div v-if="panel !== null" class="account-mask" role="presentation">
|
||||||
class="account-dialog"
|
<section
|
||||||
append-to-body
|
class="account-card"
|
||||||
:close-on-click-modal="false"
|
:class="panel === 'profile' ? 'account-card--profile' : 'account-card--password'"
|
||||||
:close-on-press-escape="!saving"
|
role="dialog"
|
||||||
:show-close="!saving"
|
aria-modal="true"
|
||||||
@update:model-value="close"
|
aria-labelledby="account-dialog-title"
|
||||||
>
|
|
||||||
<form id="account-form" @submit.prevent="save">
|
|
||||||
<template v-if="panel === 'profile'">
|
|
||||||
<p class="hint">基本信息由信息科统一维护,联系方式可自助修改</p>
|
|
||||||
<p v-if="loading" role="status">正在加载个人信息…</p>
|
|
||||||
<div v-else-if="profile" class="profile-grid">
|
|
||||||
<label class="full"
|
|
||||||
>ID(员工编号)<input :value="profile.employeeId ?? '—'" disabled
|
|
||||||
/></label>
|
|
||||||
<label>工号<input :value="profile.employeeNo || '—'" disabled /></label>
|
|
||||||
<label>姓名<input :value="profile.displayName" disabled /></label>
|
|
||||||
<label
|
|
||||||
>状态<input
|
|
||||||
:value="{ ENABLED: '启用', DISABLED: '停用', LOCKED: '锁定' }[profile.status]"
|
|
||||||
disabled
|
|
||||||
/></label>
|
|
||||||
<label
|
|
||||||
>院区 / 科室<input
|
|
||||||
:value="[profile.campusName || '—', profile.departmentName || '—'].join(' / ')"
|
|
||||||
disabled
|
|
||||||
/></label>
|
|
||||||
<label class="full"
|
|
||||||
>角色 / 岗位<input
|
|
||||||
:value="
|
|
||||||
[
|
|
||||||
profile.roles.map((role) => role.name).join('、'),
|
|
||||||
profile.position,
|
|
||||||
profile.professionalTitle,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' / ') || '—'
|
|
||||||
"
|
|
||||||
disabled
|
|
||||||
/></label>
|
|
||||||
<label
|
|
||||||
>手机号<input
|
|
||||||
v-model="contacts.phone"
|
|
||||||
type="tel"
|
|
||||||
maxlength="32"
|
|
||||||
:disabled="saving"
|
|
||||||
autocomplete="tel"
|
|
||||||
/></label>
|
|
||||||
<label
|
|
||||||
>邮箱<input
|
|
||||||
v-model="contacts.email"
|
|
||||||
type="text"
|
|
||||||
inputmode="email"
|
|
||||||
maxlength="255"
|
|
||||||
:disabled="saving"
|
|
||||||
autocomplete="email"
|
|
||||||
/></label>
|
|
||||||
<p class="hint full">联系方式已脱敏;保持原值不修改,输入完整内容可替换,清空可删除。</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div v-else class="password-fields">
|
|
||||||
<p class="hint">12–128 位,须同时包含大小写字母、数字和特殊字符。修改后需重新登录。</p>
|
|
||||||
<label
|
|
||||||
>当前密码<input
|
|
||||||
v-model="password.current"
|
|
||||||
type="password"
|
|
||||||
autocomplete="current-password"
|
|
||||||
maxlength="128"
|
|
||||||
:disabled="saving"
|
|
||||||
/></label>
|
|
||||||
<label
|
|
||||||
>新密码<input
|
|
||||||
v-model="password.next"
|
|
||||||
type="password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
maxlength="128"
|
|
||||||
:disabled="saving"
|
|
||||||
/></label>
|
|
||||||
<p class="hint" aria-live="polite">强度:{{ strength }}</p>
|
|
||||||
<label
|
|
||||||
>确认新密码<input
|
|
||||||
v-model="password.confirm"
|
|
||||||
type="password"
|
|
||||||
autocomplete="new-password"
|
|
||||||
maxlength="128"
|
|
||||||
:disabled="saving"
|
|
||||||
/></label>
|
|
||||||
</div>
|
|
||||||
<p v-if="error" class="error" role="alert">{{ error }}</p>
|
|
||||||
</form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button :disabled="saving" @click="close">取消</el-button>
|
|
||||||
<el-button v-if="panel === 'profile' && !profile && !loading" @click="open('profile')"
|
|
||||||
>重新加载</el-button
|
|
||||||
>
|
>
|
||||||
<el-button
|
<header class="account-header">
|
||||||
type="primary"
|
<strong id="account-dialog-title">
|
||||||
native-type="submit"
|
{{ panel === 'profile' ? '个人信息' : '修改密码' }}
|
||||||
form="account-form"
|
</strong>
|
||||||
:loading="saving"
|
<button
|
||||||
:disabled="loading || (panel === 'profile' && !profile)"
|
v-if="!saving"
|
||||||
>{{ panel === 'profile' ? '保存' : '确认修改' }}</el-button
|
type="button"
|
||||||
>
|
class="account-close"
|
||||||
</template>
|
aria-label="关闭"
|
||||||
</el-dialog>
|
@click="close"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form id="account-form" class="account-body" @submit.prevent="save">
|
||||||
|
<template v-if="panel === 'profile'">
|
||||||
|
<p class="hint">基本信息由信息科统一维护,联系方式可自助修改</p>
|
||||||
|
<p v-if="loading" role="status">正在加载个人信息…</p>
|
||||||
|
<div v-else-if="profile" class="profile-grid">
|
||||||
|
<label class="full"
|
||||||
|
>ID(员工编号)<input :value="profile.employeeId ?? '—'" disabled
|
||||||
|
/></label>
|
||||||
|
<label>工号<input :value="profile.employeeNo || '—'" disabled /></label>
|
||||||
|
<label>姓名<input :value="profile.displayName" disabled /></label>
|
||||||
|
<label
|
||||||
|
>状态<input
|
||||||
|
:value="{ ENABLED: '启用', DISABLED: '停用', LOCKED: '锁定' }[profile.status]"
|
||||||
|
disabled
|
||||||
|
/></label>
|
||||||
|
<label
|
||||||
|
>院区 / 科室<input
|
||||||
|
:value="[profile.campusName || '—', profile.departmentName || '—'].join(' / ')"
|
||||||
|
disabled
|
||||||
|
/></label>
|
||||||
|
<label class="full"
|
||||||
|
>角色 / 岗位<input
|
||||||
|
:value="
|
||||||
|
[
|
||||||
|
profile.roles.map((role) => role.name).join('、'),
|
||||||
|
profile.position,
|
||||||
|
profile.professionalTitle,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' / ') || '—'
|
||||||
|
"
|
||||||
|
disabled
|
||||||
|
/></label>
|
||||||
|
<label
|
||||||
|
>手机号<input
|
||||||
|
v-model="contacts.phone"
|
||||||
|
type="tel"
|
||||||
|
maxlength="32"
|
||||||
|
:disabled="saving"
|
||||||
|
autocomplete="tel"
|
||||||
|
/></label>
|
||||||
|
<label
|
||||||
|
>邮箱<input
|
||||||
|
v-model="contacts.email"
|
||||||
|
type="text"
|
||||||
|
inputmode="email"
|
||||||
|
maxlength="255"
|
||||||
|
:disabled="saving"
|
||||||
|
autocomplete="email"
|
||||||
|
/></label>
|
||||||
|
<p class="hint full">
|
||||||
|
联系方式已脱敏;保持原值不修改,输入完整内容可替换,清空可删除。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="password-fields">
|
||||||
|
<p class="hint">12–128 位,须同时包含大小写字母、数字和特殊字符。修改后需重新登录。</p>
|
||||||
|
<label
|
||||||
|
>当前密码<input
|
||||||
|
v-model="password.current"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
maxlength="128"
|
||||||
|
:disabled="saving"
|
||||||
|
/></label>
|
||||||
|
<label
|
||||||
|
>新密码<input
|
||||||
|
v-model="password.next"
|
||||||
|
type="password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
maxlength="128"
|
||||||
|
:disabled="saving"
|
||||||
|
/></label>
|
||||||
|
<p class="hint" aria-live="polite">强度:{{ strength }}</p>
|
||||||
|
<label
|
||||||
|
>确认新密码<input
|
||||||
|
v-model="password.confirm"
|
||||||
|
type="password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
maxlength="128"
|
||||||
|
:disabled="saving"
|
||||||
|
/></label>
|
||||||
|
</div>
|
||||||
|
<p v-if="error" class="error" role="alert">{{ error }}</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<footer class="account-footer">
|
||||||
|
<span class="account-spacer" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="action-button action-button--ghost"
|
||||||
|
:disabled="saving"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="panel === 'profile' && !profile && !loading"
|
||||||
|
type="button"
|
||||||
|
class="action-button action-button--ghost"
|
||||||
|
@click="open('profile')"
|
||||||
|
>
|
||||||
|
重新加载
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
form="account-form"
|
||||||
|
class="action-button"
|
||||||
|
:disabled="saving || loading || (panel === 'profile' && !profile)"
|
||||||
|
>
|
||||||
|
{{ saving ? '处理中…' : panel === 'profile' ? '保存' : '确认修改' }}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.account-mask {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 50;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: rgb(9 40 52 / 45%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-card {
|
||||||
|
display: flex;
|
||||||
|
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: account-pop 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-card--profile {
|
||||||
|
width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-card--password {
|
||||||
|
width: 460px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-header {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 18px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-header strong {
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-close {
|
||||||
|
padding: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--mut);
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-close:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-body {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 12px 18px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-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:hover:not(:disabled) {
|
||||||
|
background: var(--brand-d);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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:disabled {
|
||||||
|
color: #9aabb3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: #eef3f5;
|
||||||
|
}
|
||||||
|
|
||||||
.profile-grid {
|
.profile-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full {
|
.full {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--mut);
|
color: var(--mut);
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -140,32 +303,48 @@ input {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
input:disabled {
|
input:disabled {
|
||||||
background: #f3f7f9;
|
background: #f3f7f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus {
|
input:focus {
|
||||||
outline: 2px solid var(--brand);
|
outline: 2px solid var(--brand);
|
||||||
outline-offset: 1px;
|
outline-offset: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
margin: 0 0 14px;
|
margin: 0 0 14px;
|
||||||
color: var(--mut);
|
color: var(--mut);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-fields {
|
.password-fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-fields .hint {
|
.password-fields .hint {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: var(--err);
|
color: var(--err);
|
||||||
}
|
}
|
||||||
:global(.account-dialog) {
|
|
||||||
max-width: calc(100vw - 32px);
|
@keyframes account-pop {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.96) translateY(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
@media (max-width: 520px) {
|
||||||
.profile-grid {
|
.profile-grid {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import AccountPanel from './AccountPanel.vue'
|
import AccountPanel from './AccountPanel.vue'
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import { ElMessageBox } from 'element-plus'
|
|
||||||
|
|
||||||
import { useLoginStore } from '@/stores/login'
|
import { useLoginStore } from '@/stores/login'
|
||||||
|
|
||||||
const loginStore = useLoginStore()
|
const loginStore = useLoginStore()
|
||||||
const accountPanel = ref<InstanceType<typeof AccountPanel>>()
|
const accountPanel = ref<InstanceType<typeof AccountPanel>>()
|
||||||
const accountMenuOpen = ref(false)
|
const accountMenuOpen = ref(false)
|
||||||
|
const logoutConfirmOpen = ref(false)
|
||||||
const loggingOut = ref(false)
|
const loggingOut = ref(false)
|
||||||
const identity = computed(() => {
|
const identity = computed(() => {
|
||||||
const labels = [
|
const labels = [
|
||||||
@@ -46,6 +46,18 @@ const visibleMenuGroups = computed(() =>
|
|||||||
.filter((group) => group.items.length),
|
.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)
|
* 本地身份数据(token / tokenExpiresAt / userInfo,其中 userInfo 里含 permissions)
|
||||||
* 由 `loginStore.logout()` → `clearSession()` → `clearAuthStorage()` 删除。
|
* 由 `loginStore.logout()` → `clearSession()` → `clearAuthStorage()` 删除。
|
||||||
*/
|
*/
|
||||||
async function handleLogout() {
|
async function confirmLogout() {
|
||||||
if (loggingOut.value) return
|
if (loggingOut.value) return
|
||||||
accountMenuOpen.value = false
|
|
||||||
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm('退出后需要重新登录才能继续使用,确定退出当前账号吗?', '退出登录', {
|
|
||||||
confirmButtonText: '退出登录',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
confirmButtonClass: 'logout-confirm-button',
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
// 用户点了取消,保持在当前页面
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loggingOut.value = true
|
loggingOut.value = true
|
||||||
try {
|
try {
|
||||||
@@ -84,6 +83,18 @@ async function handleLogout() {
|
|||||||
// 用 replace 而不是 assign:退出后按浏览器后退不该再回到已登录页面
|
// 用 replace 而不是 assign:退出后按浏览器后退不该再回到已登录页面
|
||||||
window.location.replace('/login')
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -160,18 +171,59 @@ async function handleLogout() {
|
|||||||
<span>工号 {{ loginStore.user?.employeeNo || '—' }}</span>
|
<span>工号 {{ loginStore.user?.employeeNo || '—' }}</span>
|
||||||
<span>{{ loginStore.user?.campusName || '—' }} · {{ loginStore.department }}</span>
|
<span>{{ loginStore.user?.campusName || '—' }} · {{ loginStore.department }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" @click="openAccount('profile')">
|
<button type="button" @click="openAccount('profile')">个人信息</button>
|
||||||
<span aria-hidden="true">👤</span>个人信息
|
<button type="button" @click="openAccount('password')">修改密码</button>
|
||||||
</button>
|
<button class="account-logout" type="button" :disabled="loggingOut" @click="requestLogout">
|
||||||
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
<AccountPanel ref="accountPanel" />
|
<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>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -360,7 +412,6 @@ async function handleLogout() {
|
|||||||
.account-actions button {
|
.account-actions button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 9px 12px;
|
padding: 9px 12px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -378,4 +429,127 @@ async function handleLogout() {
|
|||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
color: #d0534f;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -72,12 +72,3 @@ a {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 「退出登录」二次确认的确认按钮:终止会话不可逆,按危险操作着色。
|
|
||||||
MessageBox 挂在 body 上,组件里的 scoped 选择器命中不了,只能放全局。 */
|
|
||||||
.el-message-box .logout-confirm-button,
|
|
||||||
.el-message-box .logout-confirm-button:hover,
|
|
||||||
.el-message-box .logout-confirm-button:focus {
|
|
||||||
background-color: var(--err);
|
|
||||||
border-color: var(--err);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user