优化后台界面与无操作退出体验

This commit is contained in:
Cheng Zhou
2026-06-10 15:20:06 +08:00
parent ea3f19e241
commit 84e55af8fe
39 changed files with 793 additions and 608 deletions
@@ -0,0 +1,199 @@
<template>
<transition name="session-timeout-fade">
<div v-if="visible" class="session-timeout-prompt" role="status" aria-live="polite">
<div class="timeout-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="9"></circle>
<path d="M12 7v5l3 2"></path>
</svg>
</div>
<div class="timeout-copy">
<strong>即将自动退出</strong>
<span>{{ countdownText }} 后将退出登录以保护项目数据</span>
</div>
<div class="timeout-actions">
<el-button size="small" @click="logoutNow">立即退出</el-button>
<el-button size="small" type="primary" @click="continueSession">继续使用</el-button>
</div>
<div class="timeout-progress" aria-hidden="true">
<span :style="{ width: progressWidth }"></span>
</div>
</div>
</transition>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { useAuthStore } from "../store/auth";
import { useSessionStore } from "../store/session";
import {
forceLogout,
LOGOUT_REASON_MANUAL,
markUserActive,
TIMEOUT_WARNING_SECONDS,
} from "../session/sessionManager";
const auth = useAuthStore();
const session = useSessionStore();
const nowTs = ref(Date.now());
let timer: number | null = null;
const visible = computed(() => Boolean(auth.token && session.timeoutWarningVisible));
const countdownText = computed(() => {
const remainSeconds = Math.max(0, Math.ceil((session.timeoutAt - nowTs.value) / 1000));
return `${remainSeconds}`;
});
const progressWidth = computed(() => {
const remainSeconds = Math.max(0, Math.ceil((session.timeoutAt - nowTs.value) / 1000));
const ratio = Math.min(1, remainSeconds / TIMEOUT_WARNING_SECONDS);
return `${Math.round(ratio * 100)}%`;
});
const stopTimer = () => {
if (!timer) return;
window.clearInterval(timer);
timer = null;
};
const startTimer = () => {
stopTimer();
nowTs.value = Date.now();
timer = window.setInterval(() => {
nowTs.value = Date.now();
}, 1000);
};
const continueSession = () => {
markUserActive();
};
const logoutNow = () => {
forceLogout(LOGOUT_REASON_MANUAL);
};
watch(
visible,
(value) => {
if (value) {
startTimer();
return;
}
stopTimer();
},
{ immediate: true },
);
onBeforeUnmount(() => {
stopTimer();
});
</script>
<style scoped>
.session-timeout-prompt {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 2200;
width: min(420px, calc(100vw - 32px));
display: grid;
grid-template-columns: 38px minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
padding: 14px 16px;
border-radius: 14px;
border: 1px solid rgba(197, 139, 42, 0.24);
background: rgba(255, 252, 246, 0.96);
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.14), inset 0 1px 0 rgba(255, 255, 255, 0.72);
backdrop-filter: blur(16px);
color: #3b2b12;
}
.timeout-progress {
grid-column: 1 / -1;
height: 3px;
overflow: hidden;
border-radius: 999px;
background: rgba(197, 139, 42, 0.14);
}
.timeout-progress span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #d59a35, #f2b84f);
box-shadow: 0 0 12px rgba(242, 184, 79, 0.36);
transition: width 220ms ease;
}
.timeout-icon {
width: 38px;
height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 11px;
color: var(--ctms-warning);
background: #fff3dc;
box-shadow: inset 0 0 0 1px rgba(197, 139, 42, 0.16);
}
.timeout-icon svg {
width: 20px;
height: 20px;
}
.timeout-copy {
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.timeout-copy strong {
font-size: 13px;
line-height: 18px;
font-weight: 800;
color: #182235;
}
.timeout-copy span {
font-size: 12px;
line-height: 18px;
font-weight: 600;
color: #7a5a23;
}
.timeout-actions {
display: inline-flex;
gap: 8px;
align-items: center;
}
.session-timeout-fade-enter-active,
.session-timeout-fade-leave-active {
transition: opacity 160ms ease, transform 160ms ease;
}
.session-timeout-fade-enter-from,
.session-timeout-fade-leave-to {
opacity: 0;
transform: translateY(8px);
}
@media (max-width: 640px) {
.session-timeout-prompt {
left: 16px;
right: 16px;
bottom: 16px;
width: auto;
grid-template-columns: 34px minmax(0, 1fr);
}
.timeout-actions {
grid-column: 1 / -1;
justify-content: flex-end;
}
}
</style>