import router from '@/router' import { Message } from '@arco-design/web-vue' import { useRbacUserStore } from '@/store/modules/rbac-user' import { resolvePostLogoutLogin } from '@/utils/login-portal' import { watch } from 'vue' // 用户最后一次交互起,1小时后进入“过期/跳登录” const KEEP_ALIVE_IDLE_MS = 60 * 60 * 1000 // 提前 1 分钟续期(尽量减少 accessToken 刚好到期导致的 401) const RENEW_BEFORE_MS = 60 * 1000 // mousemove 等高频事件做节流,避免频繁重置计时器 const ACTION_THROTTLE_MS = 1000 let started = false let lastActionAt = 0 let refreshTimer: ReturnType | null = null let logoutTimer: ReturnType | null = null function clearTimers() { if (refreshTimer) window.clearTimeout(refreshTimer) if (logoutTimer) window.clearTimeout(logoutTimer) refreshTimer = null logoutTimer = null } function scheduleLogoutAt(ts: number) { if (!ts || ts <= 0) return const delay = ts - Date.now() if (delay <= 0) { void doLogout() return } logoutTimer = window.setTimeout(() => { void doLogout() }, delay) } async function doLogout() { clearTimers() const userStore = useRbacUserStore() const loginType = userStore.resolveLoginTypeForRedirect() const currentPath = router.currentRoute.value.fullPath if (userStore.isLoggedIn) { userStore.disableKeepAlive() userStore.logout() } Message.error('登录已过期,请重新登录') const target = resolvePostLogoutLogin({ loginType, path: currentPath, }) if (target.path) { router.replace({ path: target.path, query: target.query, }) return } router.replace({ name: target.name, query: target.query, }) } function scheduleProactiveRefreshAt(expectedKeepAliveUntil: number) { if (!expectedKeepAliveUntil || expectedKeepAliveUntil <= 0) return const refreshAt = expectedKeepAliveUntil - RENEW_BEFORE_MS const delay = refreshAt - Date.now() if (delay <= 0) { // 到点了就立即尝试续期 void (async () => { await tryRefresh(expectedKeepAliveUntil) })() return } refreshTimer = window.setTimeout(() => { void tryRefresh(expectedKeepAliveUntil) }, delay) } async function tryRefresh(expectedKeepAliveUntil: number) { const userStore = useRbacUserStore() // 如果期间用户又做了交互,倒计时已被重置,则旧定时器作废 if (userStore.getKeepAliveUntil() !== expectedKeepAliveUntil) return if (!userStore.isKeepAliveActive()) return const success = await userStore.refreshAccessToken() if (!success) { // 刷新失败时立即兜底登出,避免仅清空 token 但不跳转 void doLogout() } } function onUserActivity() { const now = Date.now() if (now - lastActionAt < ACTION_THROTTLE_MS) return lastActionAt = now const userStore = useRbacUserStore() if (!userStore.isLoggedIn) return // 登录页不做保活 if (router.currentRoute.value.name === 'login' || router.currentRoute.value.name === 'adminLogin') return // 开启并重置保活倒计时 userStore.enableKeepAlive() clearTimers() const newKeepAliveUntil = userStore.getKeepAliveUntil() || (now + KEEP_ALIVE_IDLE_MS) scheduleLogoutAt(newKeepAliveUntil) scheduleProactiveRefreshAt(newKeepAliveUntil) } function attachListeners() { // 仅使用 window 监听,避免 window+document 重复监听导致的双倍事件开销 const capture = true const passiveCapture: AddEventListenerOptions = { capture: true, passive: true } // Window window.addEventListener('click', onUserActivity, capture) window.addEventListener('mousemove', onUserActivity, capture) window.addEventListener('keydown', onUserActivity, capture) window.addEventListener('scroll', onUserActivity, capture) window.addEventListener('wheel', onUserActivity, passiveCapture) window.addEventListener('touchstart', onUserActivity, capture) window.addEventListener('touchmove', onUserActivity, passiveCapture) window.addEventListener('pointerdown', onUserActivity, capture) window.addEventListener('pointermove', onUserActivity, capture) window.addEventListener('focusin', onUserActivity, capture) window.addEventListener('input', onUserActivity, capture) } export function initAuthKeepAlive() { if (started) return started = true const userStore = useRbacUserStore() clearTimers() // 未发生交互前:若已从 storage 恢复保活窗,则按 keepAliveUntil 调度;否则仅 access 到期登出 attachListeners() // 路由切换也视为一次用户活动(兜底:至少点菜单/跳转会续期) // 使用 setTimeout(0) 延后执行,避免阻塞路由切换完成;若近期已有交互(如点击菜单)则跳过 router.afterEach(() => { if (Date.now() - lastActionAt < 500) return setTimeout(() => onUserActivity(), 0) }) watch( () => userStore.isLoggedIn, (isLoggedIn) => { clearTimers() if (!isLoggedIn) return if (userStore.isKeepAliveActive()) { const until = userStore.getKeepAliveUntil() scheduleLogoutAt(until) scheduleProactiveRefreshAt(until) return } // 登录后的“无操作兜底”:未交互前不续期,直接在 accessToken 真实过期时跳登录 const accessTokenRealExpiresAt = userStore.getAccessTokenRealExpiresAt() scheduleLogoutAt(accessTokenRealExpiresAt) }, { immediate: true }, ) }