198 lines
5.8 KiB
Vue
198 lines
5.8 KiB
Vue
<template>
|
|
<ChunkErrorBoundary>
|
|
<ElConfigProvider size="default" :locale="locales[language]" :z-index="3000">
|
|
<RouterView></RouterView>
|
|
</ElConfigProvider>
|
|
</ChunkErrorBoundary>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useUserStore } from './store/modules/user'
|
|
import zh from 'element-plus/es/locale/lang/zh-cn'
|
|
import en from 'element-plus/es/locale/lang/en'
|
|
import { systemUpgrade } from '@/utils'
|
|
import { AuthService } from '@/api/modules'
|
|
import { ApiStatus } from './utils/http/status'
|
|
import { setThemeTransitionClass } from '@/utils'
|
|
import { checkStorageCompatibility } from '@/utils'
|
|
import ChunkErrorBoundary from '@/components/core/others/ChunkErrorBoundary.vue'
|
|
import { ElMessageBox } from 'element-plus'
|
|
|
|
const userStore = useUserStore()
|
|
const { language } = storeToRefs(userStore)
|
|
|
|
const locales = {
|
|
zh: zh,
|
|
en: en
|
|
}
|
|
|
|
const VERSION_CHECK_INTERVAL = 5 * 60 * 1000
|
|
const VERSION_PROMPT_STORAGE_KEY = 'admin-xm-iot:confirmed-version-update'
|
|
let versionCheckTimer: number | undefined
|
|
let currentEntrySignature = ''
|
|
let upgradePromptVisible = false
|
|
let removeVisibilityChangeListener: (() => void) | undefined
|
|
|
|
onBeforeMount(() => {
|
|
setThemeTransitionClass(true)
|
|
})
|
|
|
|
onMounted(() => {
|
|
// 检查存储兼容性
|
|
checkStorageCompatibility()
|
|
// 提升暗黑主题下页面刷新视觉体验
|
|
setThemeTransitionClass(false)
|
|
// 系统升级
|
|
systemUpgrade()
|
|
// 检查前端版本更新
|
|
startVersionUpdateCheck()
|
|
// 获取用户信息
|
|
getUserInfo()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (versionCheckTimer) {
|
|
window.clearInterval(versionCheckTimer)
|
|
}
|
|
removeVisibilityChangeListener?.()
|
|
})
|
|
|
|
// 获取用户信息
|
|
const getUserInfo = async () => {
|
|
if (userStore.isLogin && userStore.accessToken) {
|
|
try {
|
|
const res = await AuthService.getUserInfo()
|
|
if (res.code === ApiStatus.success && res.data) {
|
|
// API 返回的是 { user, permissions },我们需要保存 user 和 permissions
|
|
userStore.setUserInfo(res.data.user)
|
|
userStore.setPermissions(res.data.permissions || [])
|
|
}
|
|
} catch (error) {
|
|
console.error('获取用户信息失败:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
const getEntrySignature = (html: string) => {
|
|
const matches = html.match(/(?:src|href)="[^"]*\/assets\/[^""]+\.(?:js|css)"/g)
|
|
return matches?.sort().join('|') || ''
|
|
}
|
|
|
|
const getLoadedEntrySignature = () => {
|
|
return Array.from(
|
|
document.querySelectorAll<HTMLScriptElement | HTMLLinkElement>('script[src],link[href]')
|
|
)
|
|
.map((element) => ('src' in element ? element.src : element.href))
|
|
.filter((url) => /\/assets\/.*\.(js|css)(\?|$)/.test(url))
|
|
.sort()
|
|
.join('|')
|
|
}
|
|
|
|
const getStringHash = (value: string) => {
|
|
let hash = 0
|
|
|
|
for (let i = 0; i < value.length; i++) {
|
|
hash = (hash << 5) - hash + value.charCodeAt(i)
|
|
hash |= 0
|
|
}
|
|
|
|
return Math.abs(hash).toString(36)
|
|
}
|
|
|
|
const getBrowserFingerprint = () => {
|
|
return getStringHash(
|
|
[
|
|
navigator.userAgent,
|
|
navigator.language,
|
|
navigator.platform,
|
|
Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
`${screen.width}x${screen.height}x${screen.colorDepth}`
|
|
].join('|')
|
|
)
|
|
}
|
|
|
|
const getConfirmedVersionKey = (signature: string) => {
|
|
return `${getBrowserFingerprint()}:${getStringHash(signature)}`
|
|
}
|
|
|
|
const getConfirmedVersionUpdate = () => {
|
|
try {
|
|
return localStorage.getItem(VERSION_PROMPT_STORAGE_KEY)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
const setConfirmedVersionUpdate = (signature: string) => {
|
|
try {
|
|
localStorage.setItem(VERSION_PROMPT_STORAGE_KEY, getConfirmedVersionKey(signature))
|
|
} catch {
|
|
// localStorage may be unavailable in private mode.
|
|
}
|
|
}
|
|
|
|
const reloadWithCacheBuster = () => {
|
|
const url = new URL(window.location.href)
|
|
url.searchParams.set('_t', Date.now().toString())
|
|
window.location.replace(url.toString())
|
|
}
|
|
|
|
const checkVersionUpdate = async () => {
|
|
if (upgradePromptVisible || document.hidden) return
|
|
|
|
try {
|
|
const response = await fetch(import.meta.env.BASE_URL, {
|
|
cache: 'no-store'
|
|
})
|
|
const html = await response.text()
|
|
const latestSignature = getEntrySignature(html)
|
|
|
|
if (!latestSignature) return
|
|
|
|
if (!currentEntrySignature) {
|
|
currentEntrySignature = latestSignature
|
|
return
|
|
}
|
|
|
|
if (currentEntrySignature !== latestSignature) {
|
|
if (getConfirmedVersionUpdate() === getConfirmedVersionKey(latestSignature)) {
|
|
currentEntrySignature = latestSignature
|
|
return
|
|
}
|
|
|
|
upgradePromptVisible = true
|
|
await ElMessageBox.alert('系统已发布新版本,请刷新页面后继续使用。', '发现新版本', {
|
|
confirmButtonText: '立即更新',
|
|
type: 'warning',
|
|
closeOnClickModal: false,
|
|
closeOnPressEscape: false
|
|
})
|
|
setConfirmedVersionUpdate(latestSignature)
|
|
currentEntrySignature = latestSignature
|
|
reloadWithCacheBuster()
|
|
}
|
|
} catch (error) {
|
|
console.warn('检查版本更新失败:', error)
|
|
} finally {
|
|
upgradePromptVisible = false
|
|
}
|
|
}
|
|
|
|
const startVersionUpdateCheck = () => {
|
|
if (import.meta.env.DEV) return
|
|
|
|
currentEntrySignature = getLoadedEntrySignature()
|
|
checkVersionUpdate()
|
|
versionCheckTimer = window.setInterval(checkVersionUpdate, VERSION_CHECK_INTERVAL)
|
|
const handleVisibilityChange = () => {
|
|
if (!document.hidden) {
|
|
checkVersionUpdate()
|
|
}
|
|
}
|
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
|
removeVisibilityChangeListener = () => {
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
}
|
|
}
|
|
</script>
|