优化后台界面与无操作退出体验
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user