修复冻结用户即时下线

This commit is contained in:
yml2213
2026-08-30 21:19:47 +08:00
parent 21fbc69c7a
commit 327a03672a
4 changed files with 124 additions and 2 deletions
+2
View File
@@ -5,10 +5,12 @@ import { computed } from 'vue'
import { RouterView, useRoute } from 'vue-router'
import InAppBrowserPrompt from './components/InAppBrowserPrompt.vue'
import { useSessionMonitor } from './features/auth/composables/useSessionMonitor'
import AdminLayout from './layouts/AdminLayout.vue'
import AppLayout from './layouts/AppLayout.vue'
const route = useRoute()
useSessionMonitor()
const layout = computed(() => {
if (route.path === '/m' || route.path.startsWith('/m/')) {
return 'blank'
+2 -2
View File
@@ -80,8 +80,8 @@ export async function resetPassword(phone: string, code: string, newPassword: st
await apiClient.post('/auth/password/reset', { phone, code, new_password: newPassword })
}
export async function fetchMe() {
const { data } = await apiClient.get<ApiResponse<AuthUser>>('/me')
export async function fetchMe(options?: { silent?: boolean }) {
const { data } = await apiClient.get<ApiResponse<AuthUser>>('/me', options)
return data.data
}
@@ -0,0 +1,117 @@
import { onBeforeUnmount, onMounted, watch } from 'vue'
import { fetchMe } from '@/features/auth/api/auth'
import { getAccessToken, type AuthScope } from '@/shared/utils/authStorage'
import { useSessionStore } from '@/stores/session'
const scope: AuthScope = 'user'
const reconnectDelay = 3000
const verificationInterval = 5000
// 保持一个轻量级会话通道,使后台冻结能在没有页面请求时立即让用户退出。
export function useSessionMonitor() {
const session = useSessionStore()
let source: EventSource | null = null
let reconnectTimer: number | null = null
let verificationTimer: number | null = null
let checking = false
let stopped = false
function closeSource() {
source?.close()
source = null
}
function scheduleReconnect() {
if (stopped || reconnectTimer !== null || !session.isLoggedIn) return
reconnectTimer = window.setTimeout(() => {
reconnectTimer = null
connect()
}, reconnectDelay)
}
async function verifySession() {
if (stopped || checking || !session.isLoggedIn) return
checking = true
try {
await fetchMe({ silent: true })
} catch {
// 认证失败由 axios 拦截器清理令牌并跳转;网络错误等待下一次校验。
} finally {
checking = false
}
}
function connect() {
if (stopped || source || !session.isLoggedIn) return
const token = getAccessToken(scope)
if (!token) return
source = new EventSource(`/api/auth/events?token=${encodeURIComponent(token)}`, {
withCredentials: true,
})
source.addEventListener('connected', () => undefined)
source.onerror = () => {
closeSource()
void verifySession()
scheduleReconnect()
}
}
function stop() {
closeSource()
if (reconnectTimer !== null) {
window.clearTimeout(reconnectTimer)
reconnectTimer = null
}
if (verificationTimer !== null) {
window.clearInterval(verificationTimer)
verificationTimer = null
}
}
function start() {
stop()
if (!session.isLoggedIn) return
connect()
void verifySession()
verificationTimer = window.setInterval(verifySession, verificationInterval)
}
function handleAuthStorageChanged(event: Event) {
const detail = (event as CustomEvent<{ scope?: AuthScope }>).detail
if (detail?.scope !== scope || stopped) return
if (session.isLoggedIn) {
start()
} else {
stop()
}
}
function handleVisibilityChange() {
if (document.visibilityState === 'visible') void verifySession()
}
onMounted(() => {
stopped = false
start()
window.addEventListener('auth-storage-changed', handleAuthStorageChanged)
document.addEventListener('visibilitychange', handleVisibilityChange)
})
onBeforeUnmount(() => {
stopped = true
stop()
window.removeEventListener('auth-storage-changed', handleAuthStorageChanged)
document.removeEventListener('visibilitychange', handleVisibilityChange)
})
watch(
() => session.isLoggedIn,
loggedIn => {
if (stopped) return
if (loggedIn) start()
else stop()
}
)
}