Implement PWA state management for improved iOS background persistence

Co-authored-by: jxxghp <jxxghp@163.com>
This commit is contained in:
Cursor Agent
2025-07-06 06:44:06 +00:00
parent a16dd497c4
commit 0e440955c8
7 changed files with 1417 additions and 2 deletions

View File

@@ -0,0 +1,185 @@
<template>
<VCard class="ma-4" title="PWA状态管理演示">
<VCardText>
<VAlert
v-if="isPWAMode"
type="success"
class="mb-4"
>
<VIcon icon="mdi-check-circle" class="me-2" />
检测到PWA模式状态管理功能已启用
</VAlert>
<VAlert
v-else
type="info"
class="mb-4"
>
<VIcon icon="mdi-information" class="me-2" />
当前在浏览器模式请添加到桌面后体验状态管理功能
</VAlert>
<VRow>
<VCol cols="12" md="6">
<VCard variant="outlined">
<VCardTitle>状态信息</VCardTitle>
<VCardText>
<VList density="compact">
<VListItem>
<VListItemTitle>PWA模式</VListItemTitle>
<VListItemSubtitle>{{ isPWAMode ? '是' : '否' }}</VListItemSubtitle>
</VListItem>
<VListItem>
<VListItemTitle>状态管理器可用</VListItemTitle>
<VListItemSubtitle>{{ isStateManagerAvailable() ? '是' : '否' }}</VListItemSubtitle>
</VListItem>
<VListItem>
<VListItemTitle>状态恢复次数</VListItemTitle>
<VListItemSubtitle>{{ stateRestoreCount }}</VListItemSubtitle>
</VListItem>
<VListItem v-if="isStateRestored">
<VListItemTitle>最后恢复时间</VListItemTitle>
<VListItemSubtitle>{{ lastRestoredState?.timestamp ? new Date(lastRestoredState.timestamp).toLocaleString() : '无' }}</VListItemSubtitle>
</VListItem>
</VList>
</VCardText>
</VCard>
</VCol>
<VCol cols="12" md="6">
<VCard variant="outlined">
<VCardTitle>操作面板</VCardTitle>
<VCardText class="d-flex flex-column ga-3">
<VBtn
@click="saveCurrentState"
:disabled="!isStateManagerAvailable()"
color="primary"
prepend-icon="mdi-content-save"
>
手动保存状态
</VBtn>
<VBtn
@click="checkStateRestore"
:disabled="!isStateManagerAvailable()"
color="secondary"
prepend-icon="mdi-restore"
>
检查状态恢复
</VBtn>
<VBtn
@click="clearStoredState"
color="warning"
prepend-icon="mdi-delete"
>
清除存储状态
</VBtn>
<VBtn
@click="resetStateRestored"
v-if="isStateRestored"
color="info"
prepend-icon="mdi-refresh"
>
重置恢复标志
</VBtn>
</VCardText>
</VCard>
</VCol>
</VRow>
<!-- 测试表单 -->
<VCard variant="outlined" class="mt-4">
<VCardTitle>测试表单用于验证状态恢复</VCardTitle>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="testForm.name"
label="姓名"
name="test-name"
persistent-hint
hint="切换应用后再回来,这个值应该被恢复"
/>
</VCol>
<VCol cols="12" md="6">
<VTextField
v-model="testForm.email"
label="邮箱"
name="test-email"
type="email"
/>
</VCol>
<VCol cols="12">
<VTextarea
v-model="testForm.message"
label="消息"
name="test-message"
rows="3"
/>
</VCol>
</VRow>
</VForm>
</VCardText>
</VCard>
<!-- 状态恢复提示 -->
<VAlert
v-if="isStateRestored"
type="success"
class="mt-4"
closable
@click:close="resetStateRestored"
>
<VIcon icon="mdi-check-circle" class="me-2" />
状态已成功恢复滚动位置和表单数据应该已经恢复到之前的状态
</VAlert>
</VCardText>
</VCard>
</template>
<script setup lang="ts">
import { usePWAState, useGlobalPWAState } from '@/composables/usePWAState'
// 使用PWA状态管理
const {
isPWAMode,
isStateRestored,
stateRestoreCount,
lastRestoredState,
saveCurrentState,
checkStateRestore,
resetStateRestored,
isStateManagerAvailable
} = usePWAState()
// 使用全局PWA状态管理
const { clearStoredState } = useGlobalPWAState()
// 测试表单数据
const testForm = ref({
name: '',
email: '',
message: ''
})
// 监听状态恢复事件,恢复表单数据
watch(isStateRestored, (restored) => {
if (restored && lastRestoredState.value?.appData?.formState) {
console.log('检测到状态恢复,尝试恢复表单数据')
// 这里可以添加更复杂的表单数据恢复逻辑
}
})
onMounted(() => {
console.log('PWA状态演示组件已挂载')
})
</script>
<style scoped>
.v-card {
transition: all 0.3s ease;
}
</style>

View File

@@ -0,0 +1,171 @@
/**
* PWA状态管理的Vue组合式API
*/
import type { PWAState } from '@/utils/pwaStateManager'
export function usePWAState() {
const isStateRestored = ref(false)
const stateRestoreCount = ref(0)
const lastRestoredState = ref<PWAState | null>(null)
// 检查是否在PWA模式下运行
const isPWAMode = ref(false)
// 检查PWA模式
const checkPWAMode = () => {
isPWAMode.value = window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as any).standalone ||
document.referrer.includes('android-app://')
}
// 保存当前状态
const saveCurrentState = async () => {
if (window.pwaStateController) {
await window.pwaStateController.saveCurrentState()
console.log('手动保存PWA状态')
}
}
// 手动触发状态恢复检查
const checkStateRestore = async () => {
if (window.pwaStateController) {
// 这里可以添加手动检查状态恢复的逻辑
console.log('检查状态恢复')
}
}
// 监听状态恢复事件
const handleStateRestored = (event: CustomEvent<{ state: PWAState }>) => {
isStateRestored.value = true
stateRestoreCount.value++
lastRestoredState.value = event.detail.state
console.log('Vue组件收到状态恢复通知:', event.detail.state)
}
// 重置状态恢复标志
const resetStateRestored = () => {
isStateRestored.value = false
lastRestoredState.value = null
}
// 获取状态管理器实例
const getStateController = () => {
return window.pwaStateController
}
// 检查状态管理器是否可用
const isStateManagerAvailable = () => {
return !!window.pwaStateController
}
onMounted(() => {
checkPWAMode()
// 监听状态恢复事件
window.addEventListener('pwaStateRestored', handleStateRestored)
// 监听PWA模式变化
const mediaQuery = window.matchMedia('(display-mode: standalone)')
const handleDisplayModeChange = (e: MediaQueryListEvent) => {
isPWAMode.value = e.matches
}
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleDisplayModeChange)
} else {
// 兼容旧版本
mediaQuery.addListener(handleDisplayModeChange)
}
onUnmounted(() => {
if (mediaQuery.removeEventListener) {
mediaQuery.removeEventListener('change', handleDisplayModeChange)
} else {
// 兼容旧版本
mediaQuery.removeListener(handleDisplayModeChange)
}
})
})
onUnmounted(() => {
window.removeEventListener('pwaStateRestored', handleStateRestored)
})
return {
// 响应式状态
isPWAMode,
isStateRestored,
stateRestoreCount,
lastRestoredState,
// 方法
saveCurrentState,
checkStateRestore,
resetStateRestored,
getStateController,
isStateManagerAvailable,
checkPWAMode
}
}
/**
* 全局PWA状态管理器
*/
export function useGlobalPWAState() {
// 检查是否在PWA环境中
const isPWAEnvironment = () => {
return window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as any).standalone ||
document.referrer.includes('android-app://')
}
// 初始化状态管理器(如果尚未初始化)
const initStateManager = async () => {
if (!window.pwaStateController && isPWAEnvironment()) {
const { PWAStateController } = await import('@/utils/pwaStateManager')
window.pwaStateController = new PWAStateController()
console.log('延迟初始化PWA状态管理器')
}
}
// 保存应用状态
const saveAppState = async (customData?: any) => {
await initStateManager()
if (window.pwaStateController) {
// 如果有自定义数据,可以通过这种方式传递
if (customData) {
// 临时存储自定义数据
;(window as any).tempCustomState = customData
}
await window.pwaStateController.saveCurrentState()
// 清除临时数据
if (customData) {
delete (window as any).tempCustomState
}
}
}
// 获取存储的状态
const getStoredState = () => {
return localStorage.getItem('mp-pwa-app-state')
}
// 清除存储的状态
const clearStoredState = () => {
localStorage.removeItem('mp-pwa-app-state')
sessionStorage.removeItem('mp-pwa-session-state')
}
return {
isPWAEnvironment,
initStateManager,
saveAppState,
getStoredState,
clearStoredState
}
}

View File

@@ -43,6 +43,9 @@ import HeaderTab from './layouts/components/HeaderTab.vue'
// 7. 样式文件 - 合并为单一导入
import '@/styles/main.scss'
// 8. PWA状态管理
import { PWAStateController } from '@/utils/pwaStateManager'
// 创建Vue实例
const app = createApp(App)
@@ -89,3 +92,44 @@ app
.use(ConfirmDialog)
.use(i18n)
.mount('#app')
// 5. 初始化PWA状态管理器
let pwaStateController: PWAStateController | null = null
// 等待DOM准备就绪后初始化状态管理
document.addEventListener('DOMContentLoaded', () => {
// 检查是否在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) => {
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模式跳过状态管理器初始化')
}
})
// 导出状态管理器供其他模块使用
export { pwaStateController }

View File

@@ -13,6 +13,10 @@ const options = {
// 存储未读消息数量的键名
const UNREAD_COUNT_KEY = 'mp_unread_count'
// 状态管理相关的缓存名称和端点
const STATE_CACHE_NAME = 'mp-pwa-state-cache'
const STATE_ENDPOINT = '/api/pwa-state'
// 从IndexedDB获取未读消息数量
async function getStoredUnreadCount(): Promise<number> {
try {
@@ -33,6 +37,52 @@ async function setStoredUnreadCount(count: number): Promise<void> {
}
}
// 保存PWA状态到缓存
async function saveStateToCache(request: Request): Promise<Response> {
try {
const state = await request.json()
const cache = await caches.open(STATE_CACHE_NAME)
await cache.put(STATE_ENDPOINT, new Response(JSON.stringify({
...state,
timestamp: Date.now()
})))
return new Response(JSON.stringify({ success: true }))
} catch (error) {
console.error('Failed to save state to cache:', error)
return new Response(JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error) }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}
}
// 从缓存获取PWA状态
async function getStateFromCache(): Promise<Response> {
try {
const cache = await caches.open(STATE_CACHE_NAME)
const response = await cache.match(STATE_ENDPOINT)
if (response) {
const state = await response.json()
return new Response(JSON.stringify(state), {
headers: { 'Content-Type': 'application/json' }
})
}
return new Response(JSON.stringify({}), {
headers: { 'Content-Type': 'application/json' }
})
} catch (error) {
console.error('Failed to get state from cache:', error)
return new Response(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
})
}
}
// 简单的IndexedDB包装器
async function openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
@@ -123,6 +173,18 @@ self.addEventListener('activate', event => {
// 处理API请求当离线时发送消息到客户端
self.addEventListener('fetch', event => {
const url = new URL(event.request.url)
// 处理PWA状态管理请求
if (url.pathname === STATE_ENDPOINT) {
if (event.request.method === 'POST') {
event.respondWith(saveStateToCache(event.request))
} else if (event.request.method === 'GET') {
event.respondWith(getStateFromCache())
}
return
}
if (event.request.url.includes('/api/v1/') && event.request.method === 'GET') {
event.respondWith(
(async () => {
@@ -226,7 +288,7 @@ self.addEventListener('message', function (event) {
})
.catch(error => {
console.error('Failed to clear badge:', error)
event.ports[0]?.postMessage({ success: false, error: error.message })
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) })
})
} else if (event.data && event.data.type === 'UPDATE_BADGE') {
// 更新徽章数量
@@ -238,7 +300,7 @@ self.addEventListener('message', function (event) {
})
.catch(error => {
console.error('Failed to update badge:', error)
event.ports[0]?.postMessage({ success: false, error: error.message })
event.ports[0]?.postMessage({ success: false, error: error instanceof Error ? error.message : String(error) })
})
} else if (event.data && event.data.type === 'GET_UNREAD_COUNT') {
// 获取未读消息数量
@@ -250,5 +312,32 @@ self.addEventListener('message', function (event) {
console.error('Failed to get unread count:', error)
event.ports[0]?.postMessage({ count: 0 })
})
} else if (event.data && event.data.type === 'SAVE_PWA_STATE') {
// 保存PWA状态
const state = event.data.state || {}
saveStateToCache(new Request(STATE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state)
}))
.then(response => response.json())
.then(result => {
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') {
// 获取PWA状态
getStateFromCache()
.then(response => response.json())
.then(state => {
event.ports[0]?.postMessage({ state })
})
.catch(error => {
console.error('Failed to get PWA state:', error)
event.ports[0]?.postMessage({ state: {} })
})
}
})

26
src/types/pwa.d.ts vendored Normal file
View File

@@ -0,0 +1,26 @@
/**
* PWA相关的类型声明
*/
// 扩展Window接口
declare global {
interface Window {
pwaStateController?: import('@/utils/pwaStateManager').PWAStateController
orientation?: number
}
interface Navigator {
standalone?: boolean
setAppBadge?: (count: number) => Promise<void>
clearAppBadge?: () => Promise<void>
}
// 自定义事件类型
interface WindowEventMap {
'pwaStateRestored': CustomEvent<{
state: import('@/utils/pwaStateManager').PWAState
}>
}
}
export {}

View File

@@ -0,0 +1,630 @@
/**
* PWA状态管理器
* 用于在iOS设备上防止后台被杀时丢失状态提供状态恢复功能
*/
// 应用状态接口
export interface PWAState {
url: string
scrollPosition: number
orientation: number
timestamp: number
appData?: any
formData?: Record<string, any>
userSelections?: {
selectedItems: string[]
activeTab?: string
}
}
// 当前上下文接口
export interface PWAContext {
url: string
orientation: number
timestamp: number
}
/**
* 基础状态管理器使用localStorage和sessionStorage
*/
export class PWAStateManager {
private storageKey = 'mp-pwa-app-state'
private sessionKey = 'mp-pwa-session-state'
// 保存应用状态
saveState(state: PWAState): void {
try {
// 主要状态存储到localStorage
localStorage.setItem(this.storageKey, JSON.stringify({
...state,
timestamp: Date.now()
}))
// 临时状态存储到sessionStorage
sessionStorage.setItem(this.sessionKey, JSON.stringify({
scrollPosition: state.scrollPosition,
activeTab: state.appData?.activeTab,
formData: state.formData
}))
} catch (error) {
console.error('状态保存失败:', error)
}
}
// 恢复应用状态
restoreState(): PWAState | null {
try {
const savedState = localStorage.getItem(this.storageKey)
const sessionState = sessionStorage.getItem(this.sessionKey)
if (savedState) {
const state = JSON.parse(savedState)
const sessionData = sessionState ? JSON.parse(sessionState) : {}
return {
...state,
...sessionData,
isRestored: true
}
}
} catch (error) {
console.error('状态恢复失败:', error)
}
return null
}
// 清除过期状态
clearExpiredState(maxAge = 24 * 60 * 60 * 1000): void { // 24小时
try {
const savedState = localStorage.getItem(this.storageKey)
if (savedState) {
const state = JSON.parse(savedState)
if (Date.now() - state.timestamp > maxAge) {
localStorage.removeItem(this.storageKey)
sessionStorage.removeItem(this.sessionKey)
}
}
} catch (error) {
console.error('清除过期状态失败:', error)
}
}
}
/**
* IndexedDB状态管理器
*/
export class PWAIndexedDBManager {
private dbName = 'MPPWAStateDB'
private dbVersion = 1
private storeName = 'appState'
private async initDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve(request.result)
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, { keyPath: 'id' })
}
}
})
}
async saveState(state: PWAState): Promise<void> {
try {
const db = await this.initDB()
const transaction = db.transaction([this.storeName], 'readwrite')
const store = transaction.objectStore(this.storeName)
await store.put({
id: 'appState',
data: state,
timestamp: Date.now()
})
} catch (error) {
console.error('IndexedDB保存失败:', error)
}
}
async restoreState(): Promise<PWAState | null> {
try {
const db = await this.initDB()
const transaction = db.transaction([this.storeName], 'readonly')
const store = transaction.objectStore(this.storeName)
return new Promise((resolve, reject) => {
const request = store.get('appState')
request.onsuccess = () => {
const result = request.result
resolve(result ? result.data : null)
}
request.onerror = () => reject(request.error)
})
} catch (error) {
console.error('IndexedDB恢复失败:', error)
return null
}
}
}
/**
* Service Worker状态同步
*/
export class ServiceWorkerStateSync {
private stateEndpoint = '/api/pwa-state'
async saveState(state: PWAState): Promise<boolean> {
try {
const response = await fetch(this.stateEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(state)
})
const result = await response.json()
return result.success
} catch (error) {
console.error('Service Worker状态保存失败:', error)
return false
}
}
async loadState(): Promise<PWAState | null> {
try {
const response = await fetch(this.stateEndpoint)
const state = await response.json()
return Object.keys(state).length > 0 ? state : null
} catch (error) {
console.error('Service Worker状态加载失败:', error)
return null
}
}
// 使用MessageChannel与Service Worker通信
async saveStateViaMessage(state: PWAState): Promise<boolean> {
return new Promise((resolve) => {
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
resolve(event.data.success)
}
navigator.serviceWorker.controller.postMessage({
type: 'SAVE_PWA_STATE',
state
}, [channel.port2])
} else {
resolve(false)
}
})
}
async loadStateViaMessage(): Promise<PWAState | null> {
return new Promise((resolve) => {
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
resolve(event.data.state || null)
}
navigator.serviceWorker.controller.postMessage({
type: 'GET_PWA_STATE'
}, [channel.port2])
} else {
resolve(null)
}
})
}
}
/**
* 状态恢复决策器
*/
export class StateRestoreDecision {
private maxStateAge = 30 * 60 * 1000 // 30分钟
shouldRestoreState(savedState: PWAState | null, currentContext: PWAContext): boolean {
if (!savedState) return false
// 检查状态年龄
if (this.isStateExpired(savedState)) {
return false
}
// 检查URL匹配
if (!this.isUrlCompatible(savedState.url, currentContext.url)) {
return false
}
// 检查设备方向
if (this.isOrientationChanged(savedState, currentContext)) {
return false
}
return true
}
private isStateExpired(savedState: PWAState): boolean {
return Date.now() - savedState.timestamp > this.maxStateAge
}
private isUrlCompatible(savedUrl: string, currentUrl: string): boolean {
if (!savedUrl || !currentUrl) return false
try {
const savedPath = new URL(savedUrl).pathname
const currentPath = new URL(currentUrl).pathname
return savedPath === currentPath
} catch {
return false
}
}
private isOrientationChanged(savedState: PWAState, currentContext: PWAContext): boolean {
return savedState.orientation !== currentContext.orientation
}
}
/**
* 页面可见性状态管理器
*/
export class VisibilityStateManager {
private stateManager: PWAStateManager
private blurTimer: number | null = null
constructor(stateManager: PWAStateManager) {
this.stateManager = stateManager
this.setupVisibilityListener()
}
private setupVisibilityListener(): void {
// 监听页面可见性变化
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.handlePageHidden()
} else {
this.handlePageVisible()
}
})
// 监听页面卸载
window.addEventListener('beforeunload', () => {
this.handlePageUnload()
})
// 监听页面焦点变化
window.addEventListener('blur', () => {
this.handlePageBlur()
})
window.addEventListener('focus', () => {
this.handlePageFocus()
})
}
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('页面显示,已恢复状态')
}
}
private handlePageUnload(): void {
const currentState = this.getCurrentAppState()
this.stateManager.saveState(currentState)
}
private handlePageBlur(): void {
if (this.blurTimer) clearTimeout(this.blurTimer)
this.blurTimer = window.setTimeout(() => {
const currentState = this.getCurrentAppState()
this.stateManager.saveState(currentState)
}, 1000)
}
private handlePageFocus(): void {
if (this.blurTimer) {
clearTimeout(this.blurTimer)
this.blurTimer = null
}
}
private getCurrentAppState(): PWAState {
return {
url: window.location.href,
scrollPosition: window.scrollY,
orientation: window.orientation || 0,
timestamp: Date.now(),
appData: this.getAppSpecificState()
}
}
private restoreAppState(state: PWAState): void {
if (state.scrollPosition) {
window.scrollTo(0, state.scrollPosition)
}
if (state.appData) {
this.restoreAppSpecificState(state.appData)
}
}
private getAppSpecificState(): any {
// 获取应用特定状态
return {
formData: this.getFormData(),
userSelections: this.getUserSelections()
}
}
private restoreAppSpecificState(appData: any): void {
if (appData.formData) {
this.restoreFormData(appData.formData)
}
if (appData.userSelections) {
this.restoreUserSelections(appData.userSelections)
}
}
private getFormData(): Record<string, any> {
const forms = document.querySelectorAll('form')
const formData: Record<string, any> = {}
forms.forEach((form, index) => {
const data = new FormData(form)
formData[`form-${index}`] = Object.fromEntries(data)
})
return formData
}
private restoreFormData(formData: Record<string, any>): void {
Object.entries(formData).forEach(([formId, data]) => {
const formIndex = parseInt(formId.split('-')[1])
const form = document.querySelectorAll('form')[formIndex]
if (form) {
Object.entries(data).forEach(([name, value]) => {
const input = form.querySelector(`[name="${name}"]`) as HTMLInputElement
if (input) {
input.value = value as string
}
})
}
})
}
private getUserSelections(): any {
return {
selectedItems: Array.from(document.querySelectorAll('.selected')).map(el => el.id),
activeTab: document.querySelector('.tab.active')?.id
}
}
private restoreUserSelections(selections: any): void {
if (selections.selectedItems) {
selections.selectedItems.forEach((id: string) => {
const element = document.getElementById(id)
if (element) {
element.classList.add('selected')
}
})
}
if (selections.activeTab) {
const tab = document.getElementById(selections.activeTab)
if (tab) {
tab.classList.add('active')
}
}
}
}
/**
* 完整的PWA状态管理器
*/
export class PWAStateController {
private stateManager: PWAStateManager
private indexedDBManager: PWAIndexedDBManager
private swStateSync: ServiceWorkerStateSync
private visibilityManager: VisibilityStateManager
private restoreDecision: StateRestoreDecision
constructor() {
this.stateManager = new PWAStateManager()
this.indexedDBManager = new PWAIndexedDBManager()
this.swStateSync = new ServiceWorkerStateSync()
this.visibilityManager = new VisibilityStateManager(this.stateManager)
this.restoreDecision = new StateRestoreDecision()
this.init()
}
private async init(): Promise<void> {
// 清理过期状态
this.stateManager.clearExpiredState()
// 检查是否需要恢复状态
await this.checkAndRestoreState()
// 设置定期保存
this.setupPeriodicSave()
}
private async checkAndRestoreState(): Promise<void> {
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()
]
for (const source of sources) {
try {
const savedState = await source()
if (this.restoreDecision.shouldRestoreState(savedState, currentContext)) {
await this.restoreState(savedState!)
return
}
} catch (error) {
console.error('状态恢复失败:', error)
}
}
}
async saveCurrentState(): Promise<void> {
const state: PWAState = {
url: window.location.href,
scrollPosition: window.scrollY,
orientation: window.orientation || 0,
timestamp: Date.now(),
appData: this.getAppSpecificState()
}
// 多重保存策略
await Promise.allSettled([
this.stateManager.saveState(state),
this.indexedDBManager.saveState(state),
this.swStateSync.saveState(state),
this.swStateSync.saveStateViaMessage(state)
])
}
private async restoreState(state: PWAState): Promise<void> {
// 恢复滚动位置
if (state.scrollPosition) {
window.scrollTo(0, state.scrollPosition)
}
// 恢复应用特定状态
if (state.appData) {
this.restoreAppSpecificState(state.appData)
}
// 触发状态恢复事件
this.dispatchStateRestoreEvent(state)
}
private setupPeriodicSave(): void {
// 每30秒保存一次状态
setInterval(() => {
if (!document.hidden) {
this.saveCurrentState()
}
}, 30000)
}
private getAppSpecificState(): any {
// 可以在这里添加MoviePilot特定的状态
return {
// 路由状态
routerState: this.getRouterState(),
// 用户界面状态
uiState: this.getUIState(),
// 表单状态
formState: this.getFormState()
}
}
private getRouterState(): any {
// 获取Vue Router状态
return {
currentRoute: window.location.pathname,
query: window.location.search,
hash: window.location.hash
}
}
private getUIState(): any {
// 获取UI状态
return {
sidebarOpen: document.querySelector('.v-navigation-drawer--active') !== null,
darkMode: document.documentElement.classList.contains('dark') ||
document.documentElement.getAttribute('data-theme') === 'dark'
}
}
private getFormState(): any {
// 获取表单状态
const forms = document.querySelectorAll('form')
const formData: Record<string, any> = {}
forms.forEach((form, index) => {
const inputs = form.querySelectorAll('input, select, textarea')
const data: Record<string, any> = {}
inputs.forEach((input) => {
const element = input as HTMLInputElement
if (element.name) {
data[element.name] = element.value
}
})
if (Object.keys(data).length > 0) {
formData[`form-${index}`] = data
}
})
return formData
}
private restoreAppSpecificState(appData: any): void {
if (appData.uiState) {
this.restoreUIState(appData.uiState)
}
if (appData.formState) {
this.restoreFormState(appData.formState)
}
}
private restoreUIState(uiState: any): void {
// 恢复UI状态
if (uiState.darkMode !== undefined) {
// 这里可以根据实际的主题切换逻辑来恢复
console.log('恢复主题状态:', uiState.darkMode)
}
}
private restoreFormState(formState: any): void {
// 恢复表单状态
Object.entries(formState).forEach(([formId, data]) => {
const formIndex = parseInt(formId.split('-')[1])
const form = document.querySelectorAll('form')[formIndex]
if (form) {
Object.entries(data as Record<string, any>).forEach(([name, value]) => {
const input = form.querySelector(`[name="${name}"]`) as HTMLInputElement
if (input) {
input.value = value as string
// 触发change事件以便Vue能够响应
input.dispatchEvent(new Event('input', { bubbles: true }))
}
})
}
})
}
private dispatchStateRestoreEvent(state: PWAState): void {
const event = new CustomEvent('pwaStateRestored', {
detail: { state }
})
window.dispatchEvent(event)
}
}