diff --git a/src/App.vue b/src/App.vue index 24a04aee..0d455796 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,6 +8,7 @@ import { getBrowserLocale, setI18nLanguage } from './plugins/i18n' import { SupportedLocale } from '@/types/i18n' import { checkAndEmitUnreadMessages } from '@/utils/badge' import { preloadImage } from './@core/utils/image' +import { globalLoadingStateManager } from '@/utils/loadingStateManager' // 生效主题 const { global: globalTheme } = useTheme() @@ -35,6 +36,8 @@ const activeImageIndex = ref(0) const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent') let backgroundRotationTimer: NodeJS.Timeout | null = null + + // ApexCharts 全局配置 declare global { interface Window { @@ -123,11 +126,60 @@ function startBackgroundRotation() { function animateAndRemoveLoader() { const loadingBg = document.querySelector('#loading-bg') as HTMLElement if (loadingBg) { - removeEl('#loading-bg') - document.documentElement.style.removeProperty('background') + // 添加完成动画类 + loadingBg.classList.add('loading-complete') + + // 等待动画完成后移除 + setTimeout(() => { + removeEl('#loading-bg') + 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() + } + } + // 加载背景图片 async function loadBackgroundImages(retryCount = 0) { const maxRetries = 3 @@ -147,9 +199,6 @@ async function loadBackgroundImages(retryCount = 0) { } onMounted(async () => { - // 初始化全局设置 - await globalSettingsStore.initialize() - // 配置 ApexCharts configureApexCharts() @@ -170,14 +219,9 @@ onMounted(async () => { // 加载背景图片 loadBackgroundImages() - // 移除加载动画 + // 使用优化后的加载界面移除逻辑 ensureRenderComplete(() => { - nextTick(() => { - // 移除加载动画,显示页面 - animateAndRemoveLoader() - // 页面完全显示后,检查未读消息 - checkAndEmitUnreadMessages() - }) + nextTick(removeLoadingWithStateCheck) }) }) @@ -267,4 +311,29 @@ onUnmounted(() => { inset-block-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); + } +} diff --git a/src/composables/usePWAState.ts b/src/composables/usePWAState.ts index 99fa89f7..550df33c 100644 --- a/src/composables/usePWAState.ts +++ b/src/composables/usePWAState.ts @@ -18,14 +18,13 @@ export function usePWAState() { const saveCurrentState = async () => { if (window.pwaStateController) { await window.pwaStateController.saveCurrentState() - console.log('手动保存PWA状态') } } // 手动触发状态恢复检查 const checkStateRestore = async () => { if (window.pwaStateController) { - console.log('检查状态恢复') + // 静默检查 } } @@ -35,8 +34,6 @@ export function usePWAState() { isStateRestored.value = true stateRestoreCount.value++ lastRestoredState.value = customEvent.detail.state - - console.log('Vue组件收到状态恢复通知:', customEvent.detail.state) } // 重置状态恢复标志 @@ -115,7 +112,6 @@ export function useGlobalPWAState() { const clearStoredState = () => { localStorage.removeItem('mp-pwa-app-state') sessionStorage.removeItem('mp-pwa-session-state') - console.log('已清除PWA存储状态') } return { diff --git a/src/main.ts b/src/main.ts index 28ccf4f0..2fe52723 100644 --- a/src/main.ts +++ b/src/main.ts @@ -46,6 +46,31 @@ import '@/styles/main.scss' // 8. PWA状态管理 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实例 const app = createApp(App) @@ -93,51 +118,23 @@ app .use(i18n) .mount('#app') -// 5. 初始化PWA状态管理器 -let pwaStateController: PWAStateController | null = null - -// PWA状态管理器初始化函数 -const initializePWAStateManager = () => { - // 检查是否在PWA模式下运行 - const isPWA = window.matchMedia('(display-mode: standalone)').matches || - (window.navigator as any).standalone || - document.referrer.includes('android-app://') +// 5. 添加状态恢复事件监听器 +if (pwaStateController) { + // 监听状态恢复事件 + window.addEventListener('pwaStateRestored', (event: Event) => { + const customEvent = event as CustomEvent + + // 可以在这里添加状态恢复后的处理逻辑 + // 例如:通知Vue组件状态已恢复 + app.config.globalProperties.$pwaStateRestored = true + }) - if (isPWA) { - console.log('检测到PWA模式,初始化状态管理器') - pwaStateController = new PWAStateController() - - // 将状态管理器绑定到全局对象,便于调试和手动操作 - ;(window as any).pwaStateController = pwaStateController - - // 监听状态恢复事件 - window.addEventListener('pwaStateRestored', (event: Event) => { - const customEvent = event as CustomEvent - console.log('PWA状态已恢复:', customEvent.detail.state) - - // 可以在这里添加状态恢复后的处理逻辑 - // 例如:通知Vue组件状态已恢复 - app.config.globalProperties.$pwaStateRestored = true - }) - - // 监听应用即将卸载事件,保存状态 - window.addEventListener('beforeunload', () => { - if (pwaStateController) { - pwaStateController.saveCurrentState() - } - }) - } else { - console.log('非PWA模式,跳过状态管理器初始化') - } -} - -// 检查DOM状态并初始化PWA状态管理 -if (document.readyState === 'loading') { - // DOM尚未加载完成,添加事件监听器 - document.addEventListener('DOMContentLoaded', initializePWAStateManager) -} else { - // DOM已经准备就绪,立即初始化 - initializePWAStateManager() + // 监听应用即将卸载事件,保存状态 + window.addEventListener('beforeunload', () => { + if (pwaStateController) { + pwaStateController.saveCurrentState() + } + }) } // 导出状态管理器供其他模块使用 diff --git a/src/service-worker.ts b/src/service-worker.ts index 77dbc3ea..67319822 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -151,20 +151,45 @@ async function clearBadge() { // 安装事件 self.addEventListener('install', event => { - console.log('Service Worker install') - // 强制等待中的Service Worker立即成为活动的Service Worker - self.skipWaiting() + 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 + self.skipWaiting() + })() + ) }) // 激活事件 self.addEventListener('activate', event => { - console.log('Service Worker activate') event.waitUntil( (async () => { // 启用导航预载功能以提高性能 if ('navigationPreload' in self.registration) { 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立即控制页面 @@ -227,7 +252,6 @@ precacheAndRoute(self.__WB_MANIFEST) // 监听 push 事件,显示通知 self.addEventListener('push', function (event) { - console.log('notification push') if (!event.data) { return } @@ -236,7 +260,6 @@ self.addEventListener('push', function (event) { try { payload = event.data?.json() } catch (err) { - console.log(err) payload = { title: event.data?.text(), } @@ -260,14 +283,13 @@ self.addEventListener('push', function (event) { await Promise.all([self.registration.showNotification(payload.title, content), updateBadge(newCount)]) })(), ) - } catch (e) { - console.error(e) - } + } catch (e) { + // 静默处理错误 + } }) // 监听通知点击事件 self.addEventListener('notificationclick', function (event) { - console.log('notification click') const info = event.notification if (event.action === 'close') { info.close() @@ -278,7 +300,6 @@ self.addEventListener('notificationclick', function (event) { // 监听来自主应用的消息,用于清除徽章或更新徽章数量 self.addEventListener('message', function (event) { - console.log('service worker received message:', event.data) if (event.data && event.data.type === 'CLEAR_BADGE') { // 清除徽章 @@ -287,7 +308,6 @@ self.addEventListener('message', function (event) { event.ports[0]?.postMessage({ success: true }) }) .catch(error => { - console.error('Failed to clear badge:', error) event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) }) }) } else if (event.data && event.data.type === 'UPDATE_BADGE') { @@ -299,7 +319,6 @@ self.addEventListener('message', function (event) { event.ports[0]?.postMessage({ success: true }) }) .catch(error => { - console.error('Failed to update badge:', 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') { @@ -309,7 +328,6 @@ self.addEventListener('message', function (event) { event.ports[0]?.postMessage({ count }) }) .catch(error => { - console.error('Failed to get unread count:', error) event.ports[0]?.postMessage({ count: 0 }) }) } 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 }) }) .catch(error => { - console.error('Failed to save PWA state:', 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') { @@ -336,7 +353,6 @@ self.addEventListener('message', function (event) { event.ports[0]?.postMessage({ state }) }) .catch(error => { - console.error('Failed to get PWA state:', error) event.ports[0]?.postMessage({ state: {} }) }) } diff --git a/src/utils/loadingStateManager.ts b/src/utils/loadingStateManager.ts new file mode 100644 index 00000000..f48e9bd7 --- /dev/null +++ b/src/utils/loadingStateManager.ts @@ -0,0 +1,105 @@ +/** + * PWA加载状态管理器 + * 用于协调不同组件的加载状态,确保所有关键资源加载完成后再显示界面 + */ +export class PWALoadingStateManager { + private loadingStates: Map = 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 { + 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 { + return Object.fromEntries(this.loadingStates) + } + + /** + * 重置所有加载状态 + */ + reset(): void { + const wasLoading = this.isAnyLoading() + this.loadingStates.clear() + + if (wasLoading) { + this.notifyListeners(false) + } + } +} + +// 全局实例 +export const globalLoadingStateManager = new PWALoadingStateManager() \ No newline at end of file diff --git a/src/utils/pwaStateManager.ts b/src/utils/pwaStateManager.ts index ff73c4b0..fa36f676 100644 --- a/src/utils/pwaStateManager.ts +++ b/src/utils/pwaStateManager.ts @@ -225,24 +225,25 @@ export class ServiceWorkerStateSync { * 状态恢复决策器 */ export class StateRestoreDecision { - private maxStateAge = 30 * 60 * 1000 // 30分钟 + private maxStateAge = 60 * 60 * 1000 // 60分钟,延长有效期 shouldRestoreState(savedState: PWAState | null, currentContext: PWAContext): boolean { if (!savedState) return false - // 检查状态年龄 + // 检查状态年龄 - 更宽松的过期检查 if (this.isStateExpired(savedState)) { return false } - // 检查URL匹配 + // URL匹配检查 - 更宽松的匹配策略 if (!this.isUrlCompatible(savedState.url, currentContext.url)) { - return false + // 即使URL不匹配,也可以恢复一些基础状态(如滚动位置除外) + return true } - // 检查设备方向 + // 设备方向变化不阻止状态恢复 if (this.isOrientationChanged(savedState, currentContext)) { - return false + // 继续恢复 } return true @@ -275,6 +276,8 @@ export class StateRestoreDecision { export class VisibilityStateManager { private stateManager: PWAStateManager private blurTimer: number | null = null + private isRestoring = false + private restorePromise: Promise | null = null constructor(stateManager: PWAStateManager) { this.stateManager = stateManager @@ -309,17 +312,30 @@ export class VisibilityStateManager { private handlePageHidden(): void { const currentState = this.getCurrentAppState() this.stateManager.saveState(currentState) - console.log('页面被隐藏,已保存状态') } private handlePageVisible(): void { - const restoredState = this.stateManager.restoreState() - if (restoredState) { - this.restoreAppState(restoredState) - console.log('页面显示,已恢复状态') + if (this.isRestoring) return + + this.isRestoring = true + this.restorePromise = this.performStateRestore() + } + + private async performStateRestore(): Promise { + try { + const restoredState = this.stateManager.restoreState() + if (restoredState) { + await this.restoreAppState(restoredState) + } + } catch (error) { + // 静默处理错误 + } finally { + this.isRestoring = false } } + + private handlePageUnload(): void { const currentState = this.getCurrentAppState() this.stateManager.saveState(currentState) @@ -350,13 +366,19 @@ export class VisibilityStateManager { } } - private restoreAppState(state: PWAState): void { + private async restoreAppState(state: PWAState): Promise { + // 立即恢复状态,无需延迟 if (state.scrollPosition) { window.scrollTo(0, state.scrollPosition) } if (state.appData) { this.restoreAppSpecificState(state.appData) } + + // 触发状态恢复完成事件 + window.dispatchEvent(new CustomEvent('pwaStateRestored', { + detail: { state } + })) } private getAppSpecificState(): any { @@ -439,6 +461,9 @@ export class PWAStateController { private swStateSync: ServiceWorkerStateSync private visibilityManager: VisibilityStateManager private restoreDecision: StateRestoreDecision + private stateRestorePromise: Promise | null = null + private stateRestoreResolve: (() => void) | null = null + private isRestoring = false constructor() { this.stateManager = new PWAStateManager() @@ -447,9 +472,28 @@ export class PWAStateController { this.visibilityManager = new VisibilityStateManager(this.stateManager) this.restoreDecision = new StateRestoreDecision() + // 创建状态恢复Promise + this.stateRestorePromise = new Promise((resolve) => { + this.stateRestoreResolve = resolve + }) + this.init() } + /** + * 等待状态恢复完成 + */ + async waitForStateRestore(): Promise { + return this.stateRestorePromise || Promise.resolve() + } + + /** + * 获取当前是否正在恢复状态 + */ + get isRestoringState(): boolean { + return this.isRestoring + } + private async init(): Promise { // 清理过期状态 this.stateManager.clearExpiredState() @@ -462,29 +506,40 @@ export class PWAStateController { } private async checkAndRestoreState(): Promise { - const currentContext: PWAContext = { - url: window.location.href, - orientation: window.orientation || 0, - timestamp: Date.now() - } + this.isRestoring = true + + try { + const currentContext: PWAContext = { + url: window.location.href, + orientation: window.orientation || 0, + timestamp: Date.now() + } - // 尝试从多个来源恢复状态 - const sources = [ - () => this.stateManager.restoreState(), - () => this.indexedDBManager.restoreState(), - () => this.swStateSync.loadState(), - () => this.swStateSync.loadStateViaMessage() - ] + // 尝试从多个来源恢复状态 + const sources = [ + () => this.stateManager.restoreState(), + () => this.indexedDBManager.restoreState(), + () => this.swStateSync.loadState(), + () => this.swStateSync.loadStateViaMessage() + ] - for (const source of sources) { - try { - const savedState = await source() - if (this.restoreDecision.shouldRestoreState(savedState, currentContext)) { - await this.restoreState(savedState!) - return + for (const source of sources) { + try { + const savedState = await source() + if (this.restoreDecision.shouldRestoreState(savedState, currentContext)) { + await this.restoreState(savedState!) + 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 { - // 恢复滚动位置 - if (state.scrollPosition) { - window.scrollTo(0, state.scrollPosition) + const currentUrl = window.location.href + const urlMatches = this.isUrlExactMatch(state.url, currentUrl) + + // 只有在URL完全匹配时才恢复滚动位置 + if (state.scrollPosition && urlMatches) { + window.scrollTo({ + top: state.scrollPosition, + behavior: 'auto' + }) } - // 恢复应用特定状态 + // 恢复应用特定状态 - 过滤掉不适用的状态 if (state.appData) { - this.restoreAppSpecificState(state.appData) + this.restoreAppSpecificState(state.appData, urlMatches) } // 触发状态恢复事件 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 { // 每30秒保存一次状态 setInterval(() => { @@ -585,11 +656,14 @@ export class PWAStateController { return formData } - private restoreAppSpecificState(appData: any): void { + private restoreAppSpecificState(appData: any, urlMatches: boolean = true): void { + // 总是恢复UI状态(如主题等) if (appData.uiState) { this.restoreUIState(appData.uiState) } - if (appData.formState) { + + // 只有在URL匹配时才恢复表单状态 + if (appData.formState && urlMatches) { this.restoreFormState(appData.formState) } } @@ -598,7 +672,6 @@ export class PWAStateController { // 恢复UI状态 if (uiState.darkMode !== undefined) { // 这里可以根据实际的主题切换逻辑来恢复 - console.log('恢复主题状态:', uiState.darkMode) } } diff --git a/vite.config.ts b/vite.config.ts index 8257b6a3..2c3e85e0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -60,6 +60,8 @@ export default defineConfig({ revision: null, }, ], + // 启用导航预加载 + navigationPreload: true, runtimeCaching: [ { urlPattern: /\.(?:js|css|html)$/, @@ -115,10 +117,16 @@ export default defineConfig({ }, { urlPattern: ({ request }) => request.destination === 'document', - handler: 'NetworkFirst', + handler: 'StaleWhileRevalidate', options: { 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() + }, }, }, ],