mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-08 17:26:41 +08:00
+79
-10
@@ -8,6 +8,7 @@ import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
|
|||||||
import { SupportedLocale } from '@/types/i18n'
|
import { SupportedLocale } from '@/types/i18n'
|
||||||
import { checkAndEmitUnreadMessages } from '@/utils/badge'
|
import { checkAndEmitUnreadMessages } from '@/utils/badge'
|
||||||
import { preloadImage } from './@core/utils/image'
|
import { preloadImage } from './@core/utils/image'
|
||||||
|
import { globalLoadingStateManager } from '@/utils/loadingStateManager'
|
||||||
|
|
||||||
// 生效主题
|
// 生效主题
|
||||||
const { global: globalTheme } = useTheme()
|
const { global: globalTheme } = useTheme()
|
||||||
@@ -35,6 +36,8 @@ const activeImageIndex = ref(0)
|
|||||||
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
||||||
let backgroundRotationTimer: NodeJS.Timeout | null = null
|
let backgroundRotationTimer: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ApexCharts 全局配置
|
// ApexCharts 全局配置
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -123,8 +126,57 @@ function startBackgroundRotation() {
|
|||||||
function animateAndRemoveLoader() {
|
function animateAndRemoveLoader() {
|
||||||
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
|
const loadingBg = document.querySelector('#loading-bg') as HTMLElement
|
||||||
if (loadingBg) {
|
if (loadingBg) {
|
||||||
|
// 添加完成动画类
|
||||||
|
loadingBg.classList.add('loading-complete')
|
||||||
|
|
||||||
|
// 等待动画完成后移除
|
||||||
|
setTimeout(() => {
|
||||||
removeEl('#loading-bg')
|
removeEl('#loading-bg')
|
||||||
document.documentElement.style.removeProperty('background')
|
document.documentElement.style.removeProperty('background')
|
||||||
|
}, 800)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查PWA状态并移除加载界面
|
||||||
|
async function removeLoadingWithStateCheck() {
|
||||||
|
try {
|
||||||
|
// 设置各个组件的加载状态
|
||||||
|
globalLoadingStateManager.setLoadingState('pwa-state', true)
|
||||||
|
globalLoadingStateManager.setLoadingState('global-settings', true)
|
||||||
|
globalLoadingStateManager.setLoadingState('background-images', true)
|
||||||
|
|
||||||
|
// 静默检查PWA状态恢复
|
||||||
|
const pwaController = (window as any).pwaStateController
|
||||||
|
if (pwaController) {
|
||||||
|
await pwaController.waitForStateRestore()
|
||||||
|
}
|
||||||
|
globalLoadingStateManager.setLoadingState('pwa-state', false)
|
||||||
|
|
||||||
|
// 并行加载关键资源
|
||||||
|
await Promise.all([
|
||||||
|
globalSettingsStore.initialize().then(() => {
|
||||||
|
globalLoadingStateManager.setLoadingState('global-settings', false)
|
||||||
|
}),
|
||||||
|
new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
globalLoadingStateManager.setLoadingState('background-images', false)
|
||||||
|
resolve(void 0)
|
||||||
|
}, 50)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
// 等待所有加载完成
|
||||||
|
await globalLoadingStateManager.waitForAllComplete()
|
||||||
|
|
||||||
|
// 移除加载界面
|
||||||
|
animateAndRemoveLoader()
|
||||||
|
|
||||||
|
// 检查未读消息
|
||||||
|
checkAndEmitUnreadMessages()
|
||||||
|
} catch (error) {
|
||||||
|
// 即使出错也要移除加载界面
|
||||||
|
globalLoadingStateManager.reset()
|
||||||
|
animateAndRemoveLoader()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,9 +199,6 @@ async function loadBackgroundImages(retryCount = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 初始化全局设置
|
|
||||||
await globalSettingsStore.initialize()
|
|
||||||
|
|
||||||
// 配置 ApexCharts
|
// 配置 ApexCharts
|
||||||
configureApexCharts()
|
configureApexCharts()
|
||||||
|
|
||||||
@@ -170,14 +219,9 @@ onMounted(async () => {
|
|||||||
// 加载背景图片
|
// 加载背景图片
|
||||||
loadBackgroundImages()
|
loadBackgroundImages()
|
||||||
|
|
||||||
// 移除加载动画
|
// 使用优化后的加载界面移除逻辑
|
||||||
ensureRenderComplete(() => {
|
ensureRenderComplete(() => {
|
||||||
nextTick(() => {
|
nextTick(removeLoadingWithStateCheck)
|
||||||
// 移除加载动画,显示页面
|
|
||||||
animateAndRemoveLoader()
|
|
||||||
// 页面完全显示后,检查未读消息
|
|
||||||
checkAndEmitUnreadMessages()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -267,4 +311,29 @@ onUnmounted(() => {
|
|||||||
inset-block-start: 0;
|
inset-block-start: 0;
|
||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 优化加载完成动画 */
|
||||||
|
.loading-complete {
|
||||||
|
animation: fadeOutScale 0.8s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOutScale {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
filter: blur(0px);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(1.05);
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.1);
|
||||||
|
filter: blur(5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -18,14 +18,13 @@ export function usePWAState() {
|
|||||||
const saveCurrentState = async () => {
|
const saveCurrentState = async () => {
|
||||||
if (window.pwaStateController) {
|
if (window.pwaStateController) {
|
||||||
await window.pwaStateController.saveCurrentState()
|
await window.pwaStateController.saveCurrentState()
|
||||||
console.log('手动保存PWA状态')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手动触发状态恢复检查
|
// 手动触发状态恢复检查
|
||||||
const checkStateRestore = async () => {
|
const checkStateRestore = async () => {
|
||||||
if (window.pwaStateController) {
|
if (window.pwaStateController) {
|
||||||
console.log('检查状态恢复')
|
// 静默检查
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +34,6 @@ export function usePWAState() {
|
|||||||
isStateRestored.value = true
|
isStateRestored.value = true
|
||||||
stateRestoreCount.value++
|
stateRestoreCount.value++
|
||||||
lastRestoredState.value = customEvent.detail.state
|
lastRestoredState.value = customEvent.detail.state
|
||||||
|
|
||||||
console.log('Vue组件收到状态恢复通知:', customEvent.detail.state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置状态恢复标志
|
// 重置状态恢复标志
|
||||||
@@ -115,7 +112,6 @@ export function useGlobalPWAState() {
|
|||||||
const clearStoredState = () => {
|
const clearStoredState = () => {
|
||||||
localStorage.removeItem('mp-pwa-app-state')
|
localStorage.removeItem('mp-pwa-app-state')
|
||||||
sessionStorage.removeItem('mp-pwa-session-state')
|
sessionStorage.removeItem('mp-pwa-session-state')
|
||||||
console.log('已清除PWA存储状态')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+27
-30
@@ -46,6 +46,31 @@ import '@/styles/main.scss'
|
|||||||
// 8. PWA状态管理
|
// 8. PWA状态管理
|
||||||
import { PWAStateController } from '@/utils/pwaStateManager'
|
import { PWAStateController } from '@/utils/pwaStateManager'
|
||||||
|
|
||||||
|
// PWA状态管理器初始化函数
|
||||||
|
const initializePWABeforeMount = async () => {
|
||||||
|
// 检查是否在PWA模式下运行
|
||||||
|
const isPWA = window.matchMedia('(display-mode: standalone)').matches ||
|
||||||
|
(window.navigator as any).standalone ||
|
||||||
|
document.referrer.includes('android-app://')
|
||||||
|
|
||||||
|
if (isPWA) {
|
||||||
|
const pwaStateController = new PWAStateController()
|
||||||
|
|
||||||
|
// 等待状态恢复完成
|
||||||
|
await pwaStateController.waitForStateRestore()
|
||||||
|
|
||||||
|
// 将状态管理器绑定到全局对象
|
||||||
|
;(window as any).pwaStateController = pwaStateController
|
||||||
|
|
||||||
|
return pwaStateController
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在创建Vue应用前初始化PWA状态管理器
|
||||||
|
const pwaStateController = await initializePWABeforeMount()
|
||||||
|
|
||||||
// 创建Vue实例
|
// 创建Vue实例
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
@@ -93,27 +118,11 @@ app
|
|||||||
.use(i18n)
|
.use(i18n)
|
||||||
.mount('#app')
|
.mount('#app')
|
||||||
|
|
||||||
// 5. 初始化PWA状态管理器
|
// 5. 添加状态恢复事件监听器
|
||||||
let pwaStateController: PWAStateController | null = null
|
if (pwaStateController) {
|
||||||
|
|
||||||
// PWA状态管理器初始化函数
|
|
||||||
const initializePWAStateManager = () => {
|
|
||||||
// 检查是否在PWA模式下运行
|
|
||||||
const isPWA = window.matchMedia('(display-mode: standalone)').matches ||
|
|
||||||
(window.navigator as any).standalone ||
|
|
||||||
document.referrer.includes('android-app://')
|
|
||||||
|
|
||||||
if (isPWA) {
|
|
||||||
console.log('检测到PWA模式,初始化状态管理器')
|
|
||||||
pwaStateController = new PWAStateController()
|
|
||||||
|
|
||||||
// 将状态管理器绑定到全局对象,便于调试和手动操作
|
|
||||||
;(window as any).pwaStateController = pwaStateController
|
|
||||||
|
|
||||||
// 监听状态恢复事件
|
// 监听状态恢复事件
|
||||||
window.addEventListener('pwaStateRestored', (event: Event) => {
|
window.addEventListener('pwaStateRestored', (event: Event) => {
|
||||||
const customEvent = event as CustomEvent
|
const customEvent = event as CustomEvent
|
||||||
console.log('PWA状态已恢复:', customEvent.detail.state)
|
|
||||||
|
|
||||||
// 可以在这里添加状态恢复后的处理逻辑
|
// 可以在这里添加状态恢复后的处理逻辑
|
||||||
// 例如:通知Vue组件状态已恢复
|
// 例如:通知Vue组件状态已恢复
|
||||||
@@ -126,18 +135,6 @@ const initializePWAStateManager = () => {
|
|||||||
pwaStateController.saveCurrentState()
|
pwaStateController.saveCurrentState()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
console.log('非PWA模式,跳过状态管理器初始化')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查DOM状态并初始化PWA状态管理
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
// DOM尚未加载完成,添加事件监听器
|
|
||||||
document.addEventListener('DOMContentLoaded', initializePWAStateManager)
|
|
||||||
} else {
|
|
||||||
// DOM已经准备就绪,立即初始化
|
|
||||||
initializePWAStateManager()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出状态管理器供其他模块使用
|
// 导出状态管理器供其他模块使用
|
||||||
|
|||||||
+28
-12
@@ -151,20 +151,45 @@ async function clearBadge() {
|
|||||||
|
|
||||||
// 安装事件
|
// 安装事件
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
console.log('Service Worker install')
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
// 预缓存关键状态数据
|
||||||
|
try {
|
||||||
|
const cache = await caches.open(STATE_CACHE_NAME)
|
||||||
|
const existingState = await cache.match(STATE_ENDPOINT)
|
||||||
|
|
||||||
|
if (existingState) {
|
||||||
|
// 预热状态数据
|
||||||
|
const state = await existingState.json()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 静默处理错误
|
||||||
|
}
|
||||||
|
|
||||||
// 强制等待中的Service Worker立即成为活动的Service Worker
|
// 强制等待中的Service Worker立即成为活动的Service Worker
|
||||||
self.skipWaiting()
|
self.skipWaiting()
|
||||||
|
})()
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 激活事件
|
// 激活事件
|
||||||
self.addEventListener('activate', event => {
|
self.addEventListener('activate', event => {
|
||||||
console.log('Service Worker activate')
|
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
(async () => {
|
(async () => {
|
||||||
// 启用导航预载功能以提高性能
|
// 启用导航预载功能以提高性能
|
||||||
if ('navigationPreload' in self.registration) {
|
if ('navigationPreload' in self.registration) {
|
||||||
await self.registration.navigationPreload.enable()
|
await self.registration.navigationPreload.enable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理旧版本的缓存
|
||||||
|
const cacheNames = await caches.keys()
|
||||||
|
await Promise.all(
|
||||||
|
cacheNames.map(cacheName => {
|
||||||
|
if (cacheName.includes('old-') || cacheName.includes('deprecated-')) {
|
||||||
|
return caches.delete(cacheName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
})(),
|
})(),
|
||||||
)
|
)
|
||||||
// 告诉活动的Service Worker立即控制页面
|
// 告诉活动的Service Worker立即控制页面
|
||||||
@@ -227,7 +252,6 @@ precacheAndRoute(self.__WB_MANIFEST)
|
|||||||
|
|
||||||
// 监听 push 事件,显示通知
|
// 监听 push 事件,显示通知
|
||||||
self.addEventListener('push', function (event) {
|
self.addEventListener('push', function (event) {
|
||||||
console.log('notification push')
|
|
||||||
if (!event.data) {
|
if (!event.data) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -236,7 +260,6 @@ self.addEventListener('push', function (event) {
|
|||||||
try {
|
try {
|
||||||
payload = event.data?.json()
|
payload = event.data?.json()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
|
||||||
payload = {
|
payload = {
|
||||||
title: event.data?.text(),
|
title: event.data?.text(),
|
||||||
}
|
}
|
||||||
@@ -261,13 +284,12 @@ self.addEventListener('push', function (event) {
|
|||||||
})(),
|
})(),
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
// 静默处理错误
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听通知点击事件
|
// 监听通知点击事件
|
||||||
self.addEventListener('notificationclick', function (event) {
|
self.addEventListener('notificationclick', function (event) {
|
||||||
console.log('notification click')
|
|
||||||
const info = event.notification
|
const info = event.notification
|
||||||
if (event.action === 'close') {
|
if (event.action === 'close') {
|
||||||
info.close()
|
info.close()
|
||||||
@@ -278,7 +300,6 @@ self.addEventListener('notificationclick', function (event) {
|
|||||||
|
|
||||||
// 监听来自主应用的消息,用于清除徽章或更新徽章数量
|
// 监听来自主应用的消息,用于清除徽章或更新徽章数量
|
||||||
self.addEventListener('message', function (event) {
|
self.addEventListener('message', function (event) {
|
||||||
console.log('service worker received message:', event.data)
|
|
||||||
|
|
||||||
if (event.data && event.data.type === 'CLEAR_BADGE') {
|
if (event.data && event.data.type === 'CLEAR_BADGE') {
|
||||||
// 清除徽章
|
// 清除徽章
|
||||||
@@ -287,7 +308,6 @@ self.addEventListener('message', function (event) {
|
|||||||
event.ports[0]?.postMessage({ success: true })
|
event.ports[0]?.postMessage({ success: true })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Failed to clear badge:', error)
|
|
||||||
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) })
|
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) })
|
||||||
})
|
})
|
||||||
} else if (event.data && event.data.type === 'UPDATE_BADGE') {
|
} else if (event.data && event.data.type === 'UPDATE_BADGE') {
|
||||||
@@ -299,7 +319,6 @@ self.addEventListener('message', function (event) {
|
|||||||
event.ports[0]?.postMessage({ success: true })
|
event.ports[0]?.postMessage({ success: true })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Failed to update badge:', error)
|
|
||||||
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) })
|
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) })
|
||||||
})
|
})
|
||||||
} else if (event.data && event.data.type === 'GET_UNREAD_COUNT') {
|
} else if (event.data && event.data.type === 'GET_UNREAD_COUNT') {
|
||||||
@@ -309,7 +328,6 @@ self.addEventListener('message', function (event) {
|
|||||||
event.ports[0]?.postMessage({ count })
|
event.ports[0]?.postMessage({ count })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Failed to get unread count:', error)
|
|
||||||
event.ports[0]?.postMessage({ count: 0 })
|
event.ports[0]?.postMessage({ count: 0 })
|
||||||
})
|
})
|
||||||
} else if (event.data && event.data.type === 'SAVE_PWA_STATE') {
|
} else if (event.data && event.data.type === 'SAVE_PWA_STATE') {
|
||||||
@@ -325,7 +343,6 @@ self.addEventListener('message', function (event) {
|
|||||||
event.ports[0]?.postMessage({ success: result.success })
|
event.ports[0]?.postMessage({ success: result.success })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Failed to save PWA state:', error)
|
|
||||||
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) })
|
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) })
|
||||||
})
|
})
|
||||||
} else if (event.data && event.data.type === 'GET_PWA_STATE') {
|
} else if (event.data && event.data.type === 'GET_PWA_STATE') {
|
||||||
@@ -336,7 +353,6 @@ self.addEventListener('message', function (event) {
|
|||||||
event.ports[0]?.postMessage({ state })
|
event.ports[0]?.postMessage({ state })
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Failed to get PWA state:', error)
|
|
||||||
event.ports[0]?.postMessage({ state: {} })
|
event.ports[0]?.postMessage({ state: {} })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* PWA加载状态管理器
|
||||||
|
* 用于协调不同组件的加载状态,确保所有关键资源加载完成后再显示界面
|
||||||
|
*/
|
||||||
|
export class PWALoadingStateManager {
|
||||||
|
private loadingStates: Map<string, boolean> = new Map()
|
||||||
|
private listeners: Set<(isLoading: boolean) => void> = new Set()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置加载状态
|
||||||
|
* @param key 状态键名
|
||||||
|
* @param loading 是否正在加载
|
||||||
|
*/
|
||||||
|
setLoadingState(key: string, loading: boolean): void {
|
||||||
|
const wasLoading = this.isAnyLoading()
|
||||||
|
this.loadingStates.set(key, loading)
|
||||||
|
const isLoading = this.isAnyLoading()
|
||||||
|
|
||||||
|
// 如果总体加载状态发生变化,通知监听器
|
||||||
|
if (wasLoading !== isLoading) {
|
||||||
|
this.notifyListeners(isLoading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有任何组件正在加载
|
||||||
|
*/
|
||||||
|
isAnyLoading(): boolean {
|
||||||
|
return Array.from(this.loadingStates.values()).some(loading => loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待所有加载完成
|
||||||
|
*/
|
||||||
|
waitForAllComplete(): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!this.isAnyLoading()) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkComplete = () => {
|
||||||
|
if (!this.isAnyLoading()) {
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
// 检查间隔
|
||||||
|
setTimeout(checkComplete, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkComplete()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加状态变化监听器
|
||||||
|
* @param listener 监听器函数
|
||||||
|
*/
|
||||||
|
addListener(listener: (isLoading: boolean) => void): void {
|
||||||
|
this.listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除状态变化监听器
|
||||||
|
* @param listener 监听器函数
|
||||||
|
*/
|
||||||
|
removeListener(listener: (isLoading: boolean) => void): void {
|
||||||
|
this.listeners.delete(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知所有监听器
|
||||||
|
* @param isLoading 是否正在加载
|
||||||
|
*/
|
||||||
|
private notifyListeners(isLoading: boolean): void {
|
||||||
|
this.listeners.forEach(listener => {
|
||||||
|
try {
|
||||||
|
listener(isLoading)
|
||||||
|
} catch (error) {
|
||||||
|
// 静默处理错误
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前加载状态详情
|
||||||
|
*/
|
||||||
|
getLoadingStates(): Record<string, boolean> {
|
||||||
|
return Object.fromEntries(this.loadingStates)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置所有加载状态
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
const wasLoading = this.isAnyLoading()
|
||||||
|
this.loadingStates.clear()
|
||||||
|
|
||||||
|
if (wasLoading) {
|
||||||
|
this.notifyListeners(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局实例
|
||||||
|
export const globalLoadingStateManager = new PWALoadingStateManager()
|
||||||
@@ -225,24 +225,25 @@ export class ServiceWorkerStateSync {
|
|||||||
* 状态恢复决策器
|
* 状态恢复决策器
|
||||||
*/
|
*/
|
||||||
export class StateRestoreDecision {
|
export class StateRestoreDecision {
|
||||||
private maxStateAge = 30 * 60 * 1000 // 30分钟
|
private maxStateAge = 60 * 60 * 1000 // 60分钟,延长有效期
|
||||||
|
|
||||||
shouldRestoreState(savedState: PWAState | null, currentContext: PWAContext): boolean {
|
shouldRestoreState(savedState: PWAState | null, currentContext: PWAContext): boolean {
|
||||||
if (!savedState) return false
|
if (!savedState) return false
|
||||||
|
|
||||||
// 检查状态年龄
|
// 检查状态年龄 - 更宽松的过期检查
|
||||||
if (this.isStateExpired(savedState)) {
|
if (this.isStateExpired(savedState)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查URL匹配
|
// URL匹配检查 - 更宽松的匹配策略
|
||||||
if (!this.isUrlCompatible(savedState.url, currentContext.url)) {
|
if (!this.isUrlCompatible(savedState.url, currentContext.url)) {
|
||||||
return false
|
// 即使URL不匹配,也可以恢复一些基础状态(如滚动位置除外)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查设备方向
|
// 设备方向变化不阻止状态恢复
|
||||||
if (this.isOrientationChanged(savedState, currentContext)) {
|
if (this.isOrientationChanged(savedState, currentContext)) {
|
||||||
return false
|
// 继续恢复
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -275,6 +276,8 @@ export class StateRestoreDecision {
|
|||||||
export class VisibilityStateManager {
|
export class VisibilityStateManager {
|
||||||
private stateManager: PWAStateManager
|
private stateManager: PWAStateManager
|
||||||
private blurTimer: number | null = null
|
private blurTimer: number | null = null
|
||||||
|
private isRestoring = false
|
||||||
|
private restorePromise: Promise<void> | null = null
|
||||||
|
|
||||||
constructor(stateManager: PWAStateManager) {
|
constructor(stateManager: PWAStateManager) {
|
||||||
this.stateManager = stateManager
|
this.stateManager = stateManager
|
||||||
@@ -309,17 +312,30 @@ export class VisibilityStateManager {
|
|||||||
private handlePageHidden(): void {
|
private handlePageHidden(): void {
|
||||||
const currentState = this.getCurrentAppState()
|
const currentState = this.getCurrentAppState()
|
||||||
this.stateManager.saveState(currentState)
|
this.stateManager.saveState(currentState)
|
||||||
console.log('页面被隐藏,已保存状态')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handlePageVisible(): void {
|
private handlePageVisible(): void {
|
||||||
|
if (this.isRestoring) return
|
||||||
|
|
||||||
|
this.isRestoring = true
|
||||||
|
this.restorePromise = this.performStateRestore()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async performStateRestore(): Promise<void> {
|
||||||
|
try {
|
||||||
const restoredState = this.stateManager.restoreState()
|
const restoredState = this.stateManager.restoreState()
|
||||||
if (restoredState) {
|
if (restoredState) {
|
||||||
this.restoreAppState(restoredState)
|
await this.restoreAppState(restoredState)
|
||||||
console.log('页面显示,已恢复状态')
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 静默处理错误
|
||||||
|
} finally {
|
||||||
|
this.isRestoring = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private handlePageUnload(): void {
|
private handlePageUnload(): void {
|
||||||
const currentState = this.getCurrentAppState()
|
const currentState = this.getCurrentAppState()
|
||||||
this.stateManager.saveState(currentState)
|
this.stateManager.saveState(currentState)
|
||||||
@@ -350,13 +366,19 @@ export class VisibilityStateManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private restoreAppState(state: PWAState): void {
|
private async restoreAppState(state: PWAState): Promise<void> {
|
||||||
|
// 立即恢复状态,无需延迟
|
||||||
if (state.scrollPosition) {
|
if (state.scrollPosition) {
|
||||||
window.scrollTo(0, state.scrollPosition)
|
window.scrollTo(0, state.scrollPosition)
|
||||||
}
|
}
|
||||||
if (state.appData) {
|
if (state.appData) {
|
||||||
this.restoreAppSpecificState(state.appData)
|
this.restoreAppSpecificState(state.appData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 触发状态恢复完成事件
|
||||||
|
window.dispatchEvent(new CustomEvent('pwaStateRestored', {
|
||||||
|
detail: { state }
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAppSpecificState(): any {
|
private getAppSpecificState(): any {
|
||||||
@@ -439,6 +461,9 @@ export class PWAStateController {
|
|||||||
private swStateSync: ServiceWorkerStateSync
|
private swStateSync: ServiceWorkerStateSync
|
||||||
private visibilityManager: VisibilityStateManager
|
private visibilityManager: VisibilityStateManager
|
||||||
private restoreDecision: StateRestoreDecision
|
private restoreDecision: StateRestoreDecision
|
||||||
|
private stateRestorePromise: Promise<void> | null = null
|
||||||
|
private stateRestoreResolve: (() => void) | null = null
|
||||||
|
private isRestoring = false
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.stateManager = new PWAStateManager()
|
this.stateManager = new PWAStateManager()
|
||||||
@@ -447,9 +472,28 @@ export class PWAStateController {
|
|||||||
this.visibilityManager = new VisibilityStateManager(this.stateManager)
|
this.visibilityManager = new VisibilityStateManager(this.stateManager)
|
||||||
this.restoreDecision = new StateRestoreDecision()
|
this.restoreDecision = new StateRestoreDecision()
|
||||||
|
|
||||||
|
// 创建状态恢复Promise
|
||||||
|
this.stateRestorePromise = new Promise((resolve) => {
|
||||||
|
this.stateRestoreResolve = resolve
|
||||||
|
})
|
||||||
|
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待状态恢复完成
|
||||||
|
*/
|
||||||
|
async waitForStateRestore(): Promise<void> {
|
||||||
|
return this.stateRestorePromise || Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前是否正在恢复状态
|
||||||
|
*/
|
||||||
|
get isRestoringState(): boolean {
|
||||||
|
return this.isRestoring
|
||||||
|
}
|
||||||
|
|
||||||
private async init(): Promise<void> {
|
private async init(): Promise<void> {
|
||||||
// 清理过期状态
|
// 清理过期状态
|
||||||
this.stateManager.clearExpiredState()
|
this.stateManager.clearExpiredState()
|
||||||
@@ -462,6 +506,9 @@ export class PWAStateController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async checkAndRestoreState(): Promise<void> {
|
private async checkAndRestoreState(): Promise<void> {
|
||||||
|
this.isRestoring = true
|
||||||
|
|
||||||
|
try {
|
||||||
const currentContext: PWAContext = {
|
const currentContext: PWAContext = {
|
||||||
url: window.location.href,
|
url: window.location.href,
|
||||||
orientation: window.orientation || 0,
|
orientation: window.orientation || 0,
|
||||||
@@ -484,7 +531,15 @@ export class PWAStateController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('状态恢复失败:', error)
|
// 静默处理错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isRestoring = false
|
||||||
|
// 状态恢复完成(无论成功还是失败)
|
||||||
|
if (this.stateRestoreResolve) {
|
||||||
|
this.stateRestoreResolve()
|
||||||
|
this.stateRestoreResolve = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,20 +563,36 @@ export class PWAStateController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async restoreState(state: PWAState): Promise<void> {
|
private async restoreState(state: PWAState): Promise<void> {
|
||||||
// 恢复滚动位置
|
const currentUrl = window.location.href
|
||||||
if (state.scrollPosition) {
|
const urlMatches = this.isUrlExactMatch(state.url, currentUrl)
|
||||||
window.scrollTo(0, state.scrollPosition)
|
|
||||||
|
// 只有在URL完全匹配时才恢复滚动位置
|
||||||
|
if (state.scrollPosition && urlMatches) {
|
||||||
|
window.scrollTo({
|
||||||
|
top: state.scrollPosition,
|
||||||
|
behavior: 'auto'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复应用特定状态
|
// 恢复应用特定状态 - 过滤掉不适用的状态
|
||||||
if (state.appData) {
|
if (state.appData) {
|
||||||
this.restoreAppSpecificState(state.appData)
|
this.restoreAppSpecificState(state.appData, urlMatches)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发状态恢复事件
|
// 触发状态恢复事件
|
||||||
this.dispatchStateRestoreEvent(state)
|
this.dispatchStateRestoreEvent(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isUrlExactMatch(savedUrl: string, currentUrl: string): boolean {
|
||||||
|
try {
|
||||||
|
const saved = new URL(savedUrl)
|
||||||
|
const current = new URL(currentUrl)
|
||||||
|
return saved.pathname === current.pathname
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private setupPeriodicSave(): void {
|
private setupPeriodicSave(): void {
|
||||||
// 每30秒保存一次状态
|
// 每30秒保存一次状态
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
@@ -585,11 +656,14 @@ export class PWAStateController {
|
|||||||
return formData
|
return formData
|
||||||
}
|
}
|
||||||
|
|
||||||
private restoreAppSpecificState(appData: any): void {
|
private restoreAppSpecificState(appData: any, urlMatches: boolean = true): void {
|
||||||
|
// 总是恢复UI状态(如主题等)
|
||||||
if (appData.uiState) {
|
if (appData.uiState) {
|
||||||
this.restoreUIState(appData.uiState)
|
this.restoreUIState(appData.uiState)
|
||||||
}
|
}
|
||||||
if (appData.formState) {
|
|
||||||
|
// 只有在URL匹配时才恢复表单状态
|
||||||
|
if (appData.formState && urlMatches) {
|
||||||
this.restoreFormState(appData.formState)
|
this.restoreFormState(appData.formState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -598,7 +672,6 @@ export class PWAStateController {
|
|||||||
// 恢复UI状态
|
// 恢复UI状态
|
||||||
if (uiState.darkMode !== undefined) {
|
if (uiState.darkMode !== undefined) {
|
||||||
// 这里可以根据实际的主题切换逻辑来恢复
|
// 这里可以根据实际的主题切换逻辑来恢复
|
||||||
console.log('恢复主题状态:', uiState.darkMode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -60,6 +60,8 @@ export default defineConfig({
|
|||||||
revision: null,
|
revision: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
// 启用导航预加载
|
||||||
|
navigationPreload: true,
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern: /\.(?:js|css|html)$/,
|
urlPattern: /\.(?:js|css|html)$/,
|
||||||
@@ -115,10 +117,16 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
urlPattern: ({ request }) => request.destination === 'document',
|
urlPattern: ({ request }) => request.destination === 'document',
|
||||||
handler: 'NetworkFirst',
|
handler: 'StaleWhileRevalidate',
|
||||||
options: {
|
options: {
|
||||||
cacheName: 'pages-cache',
|
cacheName: 'pages-cache',
|
||||||
networkTimeoutSeconds: 10,
|
cacheKeyWillBeUsed: async ({ request }) => {
|
||||||
|
// 忽略状态参数,提高缓存命中率
|
||||||
|
const url = new URL(request.url)
|
||||||
|
url.searchParams.delete('restored')
|
||||||
|
url.searchParams.delete('t')
|
||||||
|
return url.toString()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user