2025-12-10 20:52:44 +08:00
|
|
|
<template>
|
|
|
|
|
<RouterView />
|
|
|
|
|
<ToastContainer />
|
|
|
|
|
<ConfirmContainer />
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-03-17 16:34:09 +08:00
|
|
|
import { onMounted, onErrorCaptured, onUnmounted } from 'vue'
|
2025-12-10 20:52:44 +08:00
|
|
|
import { useAuthStore } from '@/stores/auth'
|
|
|
|
|
import ToastContainer from '@/components/ToastContainer.vue'
|
|
|
|
|
import ConfirmContainer from '@/components/ConfirmContainer.vue'
|
2026-09-04 03:45:52 +08:00
|
|
|
import {
|
|
|
|
|
AUTH_SESSION_SIGNAL_KEY,
|
|
|
|
|
AUTH_STATE_CHANGE_EVENT,
|
|
|
|
|
parseAuthSessionSignal,
|
|
|
|
|
type AuthStateChangeDetail,
|
|
|
|
|
} from '@/api/client'
|
2025-12-10 20:52:44 +08:00
|
|
|
import { NETWORK_CONFIG, AUTH_CONFIG } from '@/config/constants'
|
2026-03-17 16:34:09 +08:00
|
|
|
import router from '@/router'
|
2025-12-10 20:52:44 +08:00
|
|
|
import { log } from '@/utils/logger'
|
|
|
|
|
|
|
|
|
|
const authStore = useAuthStore()
|
|
|
|
|
|
|
|
|
|
// 全局错误处理器 - 只处理特定错误,避免完全吞掉所有错误
|
|
|
|
|
onErrorCaptured((error: Error) => {
|
|
|
|
|
log.error('Error captured in component', error)
|
|
|
|
|
// 对于非关键错误,不阻止传播
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 统一的模块加载错误处理
|
|
|
|
|
let moduleLoadFailureCount = 0
|
|
|
|
|
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
// 处理未捕获的 Promise 拒绝
|
|
|
|
|
window.addEventListener('unhandledrejection', (event) => {
|
|
|
|
|
const error = event.reason
|
|
|
|
|
|
|
|
|
|
// 只处理模块加载失败的情况
|
|
|
|
|
if (error?.message?.includes('Failed to fetch dynamically imported module')) {
|
|
|
|
|
event.preventDefault() // 阻止控制台显示这个特定错误
|
|
|
|
|
|
|
|
|
|
if (moduleLoadFailureCount < NETWORK_CONFIG.MODULE_LOAD_RETRY_LIMIT) {
|
|
|
|
|
moduleLoadFailureCount++
|
|
|
|
|
log.info(`模块加载失败,尝试刷新页面 (${moduleLoadFailureCount}/${NETWORK_CONFIG.MODULE_LOAD_RETRY_LIMIT})`)
|
|
|
|
|
window.location.reload()
|
|
|
|
|
} else {
|
|
|
|
|
// 超过最大重试次数,显示友好提示
|
|
|
|
|
alert('页面加载失败,请手动刷新浏览器。如问题持续,请清除浏览器缓存后重试。')
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 其他 Promise 错误记录日志
|
|
|
|
|
log.error('Unhandled promise rejection', event.reason)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 处理全局错误
|
|
|
|
|
window.addEventListener('error', (event) => {
|
|
|
|
|
// 过滤掉常见的无害警告
|
|
|
|
|
const harmlessWarnings = [
|
|
|
|
|
'ResizeObserver loop completed with undelivered notifications',
|
|
|
|
|
'ResizeObserver loop limit exceeded'
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const isHarmless = harmlessWarnings.some(warning =>
|
|
|
|
|
event.message?.includes(warning)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (isHarmless) {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 记录其他错误
|
|
|
|
|
if (event.error) {
|
|
|
|
|
log.error('Global error', event.error)
|
|
|
|
|
} else {
|
|
|
|
|
log.warn('Global error event', {
|
|
|
|
|
message: event.message,
|
|
|
|
|
filename: event.filename,
|
|
|
|
|
lineno: event.lineno,
|
|
|
|
|
colno: event.colno
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 03:45:52 +08:00
|
|
|
async function syncExternalAuthState(authenticated: boolean): Promise<void> {
|
|
|
|
|
if (!authenticated) {
|
|
|
|
|
if (authStore.token || authStore.user) {
|
2026-03-17 16:34:09 +08:00
|
|
|
authStore.applyExternalLogout()
|
|
|
|
|
await router.replace('/')
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 03:45:52 +08:00
|
|
|
if (!await authStore.applyExternalLogin()) {
|
2026-03-17 16:34:09 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 18:12:46 +08:00
|
|
|
if (router.currentRoute.value.path.startsWith('/admin') && !authStore.canAccessAdmin) {
|
2026-03-17 16:34:09 +08:00
|
|
|
await router.replace('/dashboard')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleAuthStorageChange(event: StorageEvent): void {
|
2026-09-04 03:45:52 +08:00
|
|
|
if (event.key !== AUTH_SESSION_SIGNAL_KEY) {
|
2026-03-17 16:34:09 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 03:45:52 +08:00
|
|
|
const signal = parseAuthSessionSignal(event.newValue)
|
|
|
|
|
if (!signal) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
syncExternalAuthState(signal.authenticated).catch((err) => log.error('syncExternalAuthState failed', err))
|
2026-03-17 16:34:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleLocalAuthStateChange(event: Event): void {
|
2026-09-04 03:45:52 +08:00
|
|
|
const authEvent = event as CustomEvent<AuthStateChangeDetail>
|
|
|
|
|
if (!authEvent.detail) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (!authEvent.detail.authenticated) {
|
|
|
|
|
syncExternalAuthState(false).catch((err) => log.error('syncExternalAuthState failed', err))
|
|
|
|
|
} else {
|
|
|
|
|
authStore.syncToken()
|
|
|
|
|
}
|
2026-03-17 16:34:09 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
onMounted(async () => {
|
2026-03-17 16:34:09 +08:00
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
window.addEventListener('storage', handleAuthStorageChange)
|
|
|
|
|
window.addEventListener(AUTH_STATE_CHANGE_EVENT, handleLocalAuthStateChange as (event: Event) => void)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
// 延迟检查认证状态,让页面先加载
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
try {
|
|
|
|
|
await authStore.checkAuth()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// 即使checkAuth失败,也不要做任何会导致退出的操作
|
|
|
|
|
log.warn('Auth check failed, but keeping session', error)
|
|
|
|
|
}
|
|
|
|
|
}, AUTH_CONFIG.TOKEN_REFRESH_INTERVAL)
|
|
|
|
|
})
|
2026-03-17 16:34:09 +08:00
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
window.removeEventListener('storage', handleAuthStorageChange)
|
|
|
|
|
window.removeEventListener(
|
|
|
|
|
AUTH_STATE_CHANGE_EVENT,
|
|
|
|
|
handleLocalAuthStateChange as (event: Event) => void,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-12-10 20:52:44 +08:00
|
|
|
</script>
|