Optimize PWA state restoration and loading experience

Co-authored-by: jxxghp <jxxghp@163.com>
This commit is contained in:
Cursor Agent
2025-07-06 08:30:12 +00:00
parent 8e282fb216
commit c9867bc453
7 changed files with 649 additions and 86 deletions

View File

@@ -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,9 @@ const activeImageIndex = ref(0)
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
let backgroundRotationTimer: NodeJS.Timeout | null = null
// PWA状态恢复相关
const isRestoring = ref(false)
// ApexCharts 全局配置
declare global {
interface Window {
@@ -123,8 +127,65 @@ 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 {
console.log('开始检查加载状态...')
// 设置各个组件的加载状态
globalLoadingStateManager.setLoadingState('pwa-state', true)
globalLoadingStateManager.setLoadingState('global-settings', true)
globalLoadingStateManager.setLoadingState('background-images', true)
// 检查PWA状态是否已恢复
const pwaController = (window as any).pwaStateController
if (pwaController) {
isRestoring.value = true
await pwaController.waitForStateRestore()
console.log('PWA状态恢复完成')
}
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)
}, 200)
})
])
// 等待所有加载完成
await globalLoadingStateManager.waitForAllComplete()
console.log('所有资源加载完成,准备移除加载界面')
// 移除加载界面
animateAndRemoveLoader()
// 检查未读消息
checkAndEmitUnreadMessages()
} catch (error) {
console.error('移除加载界面时发生错误:', error)
// 即使出错也要移除加载界面
globalLoadingStateManager.reset()
animateAndRemoveLoader()
}
}
@@ -147,9 +208,11 @@ async function loadBackgroundImages(retryCount = 0) {
}
onMounted(async () => {
// 初始化全局设置
await globalSettingsStore.initialize()
// 监听PWA状态恢复事件
window.addEventListener('pwaStateRestored', () => {
isRestoring.value = false
})
// 配置 ApexCharts
configureApexCharts()
@@ -170,14 +233,9 @@ onMounted(async () => {
// 加载背景图片
loadBackgroundImages()
// 移除加载动画
// 使用优化后的加载界面移除逻辑
ensureRenderComplete(() => {
nextTick(() => {
// 移除加载动画,显示页面
animateAndRemoveLoader()
// 页面完全显示后,检查未读消息
checkAndEmitUnreadMessages()
})
nextTick(removeLoadingWithStateCheck)
})
})
@@ -205,7 +263,7 @@ onUnmounted(() => {
<div v-if="isLogin && isTransparentTheme" class="global-blur-layer"></div>
</div>
<!-- 页面内容 -->
<VApp :class="{ 'transparent-app': isTransparentTheme }">
<VApp :class="{ 'transparent-app': isTransparentTheme, 'pwa-restoring': isRestoring }">
<RouterView />
</VApp>
</div>
@@ -267,4 +325,37 @@ onUnmounted(() => {
inset-block-start: 0;
inset-inline-start: 0;
}
/* PWA过渡效果 */
.transparent-app {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.pwa-restoring {
opacity: 0.8;
transform: scale(0.98);
}
/* 优化加载完成动画 */
.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>

View File

@@ -46,6 +46,32 @@ 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) {
console.log('检测到PWA模式预初始化状态管理器')
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 +119,24 @@ 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
console.log('PWA状态已恢复:', customEvent.detail.state)
// 可以在这里添加状态恢复后的处理逻辑
// 例如通知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()
}
})
}
// 导出状态管理器供其他模块使用

View File

@@ -152,8 +152,26 @@ 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()
console.log('预缓存状态数据:', state)
}
} catch (error) {
console.error('预缓存状态数据失败:', error)
}
// 强制等待中的Service Worker立即成为活动的Service Worker
self.skipWaiting()
})()
)
})
// 激活事件
@@ -164,7 +182,19 @@ self.addEventListener('activate', event => {
// 启用导航预载功能以提高性能
if ('navigationPreload' in self.registration) {
await self.registration.navigationPreload.enable()
console.log('导航预加载已启用')
}
// 清理旧版本的缓存
const cacheNames = await caches.keys()
await Promise.all(
cacheNames.map(cacheName => {
if (cacheName.includes('old-') || cacheName.includes('deprecated-')) {
console.log('清理旧缓存:', cacheName)
return caches.delete(cacheName)
}
})
)
})(),
)
// 告诉活动的Service Worker立即控制页面

View File

@@ -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) {
console.error('加载状态监听器错误:', 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()

View File

@@ -275,6 +275,8 @@ export class StateRestoreDecision {
export class VisibilityStateManager {
private stateManager: PWAStateManager
private blurTimer: number | null = null
private isRestoring = false
private restorePromise: Promise<void> | null = null
constructor(stateManager: PWAStateManager) {
this.stateManager = stateManager
@@ -313,10 +315,106 @@ export class VisibilityStateManager {
}
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<void> {
try {
// 显示轻量级恢复指示器
this.showRestoreIndicator()
const restoredState = this.stateManager.restoreState()
if (restoredState) {
await this.restoreAppState(restoredState)
console.log('页面显示,已恢复状态')
}
} catch (error) {
console.error('状态恢复失败:', error)
} finally {
this.isRestoring = false
this.hideRestoreIndicator()
}
}
private showRestoreIndicator(): void {
// 检查是否已经存在指示器
if (document.getElementById('pwa-restore-indicator')) return
const indicator = document.createElement('div')
indicator.id = 'pwa-restore-indicator'
indicator.innerHTML = `
<div class="restore-indicator">
<div class="restore-spinner"></div>
<div class="restore-text">正在恢复状态...</div>
</div>
`
document.body.appendChild(indicator)
// 添加样式
const style = document.createElement('style')
style.textContent = `
#pwa-restore-indicator {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease;
}
#pwa-restore-indicator.show {
opacity: 1;
}
.restore-indicator {
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px;
border-radius: 10px;
display: flex;
align-items: center;
gap: 10px;
backdrop-filter: blur(10px);
}
.restore-spinner {
width: 20px;
height: 20px;
border: 2px solid transparent;
border-top: 2px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`
document.head.appendChild(style)
// 触发显示动画
setTimeout(() => {
indicator.classList.add('show')
}, 10)
}
private hideRestoreIndicator(): void {
const indicator = document.getElementById('pwa-restore-indicator')
if (indicator) {
indicator.classList.remove('show')
setTimeout(() => {
indicator.remove()
}, 300)
}
}
@@ -350,13 +448,21 @@ export class VisibilityStateManager {
}
}
private restoreAppState(state: PWAState): void {
private async restoreAppState(state: PWAState): Promise<void> {
// 添加小延迟以确保页面完全加载
await new Promise(resolve => setTimeout(resolve, 100))
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 +545,9 @@ export class PWAStateController {
private swStateSync: ServiceWorkerStateSync
private visibilityManager: VisibilityStateManager
private restoreDecision: StateRestoreDecision
private stateRestorePromise: Promise<void> | null = null
private stateRestoreResolve: (() => void) | null = null
private isRestoring = false
constructor() {
this.stateManager = new PWAStateManager()
@@ -447,9 +556,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<void> {
return this.stateRestorePromise || Promise.resolve()
}
/**
* 获取当前是否正在恢复状态
*/
get isRestoringState(): boolean {
return this.isRestoring
}
private async init(): Promise<void> {
// 清理过期状态
this.stateManager.clearExpiredState()
@@ -462,29 +590,41 @@ export class PWAStateController {
}
private async checkAndRestoreState(): Promise<void> {
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!)
console.log('PWA状态恢复成功')
return
}
} catch (error) {
console.error('状态恢复失败:', error)
}
} catch (error) {
console.error('状态恢复失败:', error)
}
} finally {
this.isRestoring = false
// 状态恢复完成(无论成功还是失败)
if (this.stateRestoreResolve) {
this.stateRestoreResolve()
this.stateRestoreResolve = null
}
}
}
@@ -508,18 +648,28 @@ export class PWAStateController {
}
private async restoreState(state: PWAState): Promise<void> {
console.log('开始恢复PWA状态:', {
url: state.url,
scrollPosition: state.scrollPosition,
timestamp: new Date(state.timestamp).toISOString(),
hasAppData: !!state.appData
})
// 恢复滚动位置
if (state.scrollPosition) {
console.log('恢复滚动位置:', state.scrollPosition)
window.scrollTo(0, state.scrollPosition)
}
// 恢复应用特定状态
if (state.appData) {
console.log('恢复应用特定状态:', state.appData)
this.restoreAppSpecificState(state.appData)
}
// 触发状态恢复事件
this.dispatchStateRestoreEvent(state)
console.log('PWA状态恢复完成')
}
private setupPeriodicSave(): void {