mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-28 19:47:49 +08:00
重构PWA状态管理
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import type { ComputedRef, Ref } from 'vue'
|
import type { ComputedRef, Ref } from 'vue'
|
||||||
|
import { useTabStateRestore } from '@/composables/useStateRestore'
|
||||||
|
|
||||||
// 动态标签页相关类型
|
// 动态标签页相关类型
|
||||||
interface DynamicHeaderTabButton {
|
interface DynamicHeaderTabButton {
|
||||||
@@ -38,7 +39,17 @@ export function useDynamicHeaderTab() {
|
|||||||
items: DynamicHeaderTabItem[] | ComputedRef<DynamicHeaderTabItem[]> | Ref<DynamicHeaderTabItem[]>
|
items: DynamicHeaderTabItem[] | ComputedRef<DynamicHeaderTabItem[]> | Ref<DynamicHeaderTabItem[]>
|
||||||
modelValue: Ref<string>
|
modelValue: Ref<string>
|
||||||
appendButtons?: DynamicHeaderTabButton[]
|
appendButtons?: DynamicHeaderTabButton[]
|
||||||
|
enableStateRestore?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
|
// 集成PWA状态恢复功能
|
||||||
|
const enablePWARestore = config.enableStateRestore !== false // 默认启用
|
||||||
|
const pwaTabState = enablePWARestore ? useTabStateRestore(config.modelValue.value) : null
|
||||||
|
|
||||||
|
// 如果启用了PWA状态恢复,先尝试恢复状态
|
||||||
|
if (pwaTabState && pwaTabState.activeTab.value) {
|
||||||
|
config.modelValue.value = pwaTabState.activeTab.value
|
||||||
|
}
|
||||||
|
|
||||||
const tabConfig: DynamicHeaderTabConfig = {
|
const tabConfig: DynamicHeaderTabConfig = {
|
||||||
items: Array.isArray(config.items) ? config.items : config.items.value,
|
items: Array.isArray(config.items) ? config.items : config.items.value,
|
||||||
modelValue: config.modelValue.value,
|
modelValue: config.modelValue.value,
|
||||||
@@ -46,12 +57,34 @@ export function useDynamicHeaderTab() {
|
|||||||
routePath: route.path,
|
routePath: route.path,
|
||||||
onUpdateModelValue: (value: string) => {
|
onUpdateModelValue: (value: string) => {
|
||||||
config.modelValue.value = value
|
config.modelValue.value = value
|
||||||
|
// 同步到PWA状态
|
||||||
|
if (pwaTabState && value) {
|
||||||
|
pwaTabState.activeTab.value = value
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果启用了PWA状态恢复,监听PWA状态变化并同步到modelValue
|
||||||
|
if (pwaTabState) {
|
||||||
|
watch(pwaTabState.activeTab, newTab => {
|
||||||
|
if (newTab && newTab !== config.modelValue.value) {
|
||||||
|
config.modelValue.value = newTab
|
||||||
|
// 更新tabConfig并重新注册
|
||||||
|
tabConfig.modelValue = newTab
|
||||||
|
if (registerDynamicHeaderTab) {
|
||||||
|
registerDynamicHeaderTab(tabConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 监听modelValue变化并更新配置
|
// 监听modelValue变化并更新配置
|
||||||
watch(config.modelValue, newValue => {
|
watch(config.modelValue, newValue => {
|
||||||
tabConfig.modelValue = newValue
|
tabConfig.modelValue = newValue
|
||||||
|
// 同步到PWA状态
|
||||||
|
if (pwaTabState && newValue) {
|
||||||
|
pwaTabState.activeTab.value = newValue
|
||||||
|
}
|
||||||
// 重新注册以更新值
|
// 重新注册以更新值
|
||||||
if (registerDynamicHeaderTab) {
|
if (registerDynamicHeaderTab) {
|
||||||
registerDynamicHeaderTab(tabConfig)
|
registerDynamicHeaderTab(tabConfig)
|
||||||
|
|||||||
+1
-138
@@ -1,7 +1,6 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { checkPWAStatus, isPWADisplayMode } from '@/@core/utils/navigator'
|
import { checkPWAStatus, isPWADisplayMode } from '@/@core/utils/navigator'
|
||||||
import type { PWAState } from '@/utils/pwaStateManager'
|
|
||||||
|
|
||||||
// 全局PWA状态,确保只初始化一次
|
// 全局PWA状态,确保只初始化一次
|
||||||
const globalPwaStatus = ref<{
|
const globalPwaStatus = ref<{
|
||||||
@@ -72,142 +71,6 @@ export function usePWA() {
|
|||||||
appMode,
|
appMode,
|
||||||
pwaStatus,
|
pwaStatus,
|
||||||
loading: globalLoading,
|
loading: globalLoading,
|
||||||
// 保留手动初始化方法以防需要
|
|
||||||
initializePWA: initializePWAGlobally,
|
initializePWA: initializePWAGlobally,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PWA状态管理 composable
|
|
||||||
export function usePWAState() {
|
|
||||||
const isStateRestored = ref(false)
|
|
||||||
const stateRestoreCount = ref(0)
|
|
||||||
const lastRestoredState = ref<PWAState | null>(null)
|
|
||||||
const isPWAMode = ref(false)
|
|
||||||
|
|
||||||
// 检查PWA模式 - 使用统一的检测方式
|
|
||||||
const checkPWAMode = async () => {
|
|
||||||
// 确保全局PWA状态已初始化
|
|
||||||
if (globalPwaStatus.value === null) {
|
|
||||||
await initializePWAGlobally()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取PWA状态
|
|
||||||
const status = globalPwaStatus.value
|
|
||||||
if (status) {
|
|
||||||
isPWAMode.value = status.isPWAEnvironment
|
|
||||||
} else {
|
|
||||||
// 如果状态获取失败,使用同步检测作为后备
|
|
||||||
isPWAMode.value = isPWADisplayMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存当前状态
|
|
||||||
const saveCurrentState = async () => {
|
|
||||||
if (window.pwaStateController) {
|
|
||||||
await window.pwaStateController.saveCurrentState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 手动触发状态恢复检查
|
|
||||||
const checkStateRestore = async () => {
|
|
||||||
if (window.pwaStateController) {
|
|
||||||
// 静默检查
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听状态恢复事件
|
|
||||||
const handleStateRestored = (event: Event) => {
|
|
||||||
const customEvent = event as CustomEvent<{ state: PWAState }>
|
|
||||||
isStateRestored.value = true
|
|
||||||
stateRestoreCount.value++
|
|
||||||
lastRestoredState.value = customEvent.detail.state
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置状态恢复标志
|
|
||||||
const resetStateRestored = () => {
|
|
||||||
isStateRestored.value = false
|
|
||||||
lastRestoredState.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查状态管理器是否可用
|
|
||||||
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
|
|
||||||
// 同步更新全局PWA状态
|
|
||||||
if (globalPwaStatus.value) {
|
|
||||||
globalPwaStatus.value.isStandaloneMode = e.matches
|
|
||||||
globalPwaStatus.value.isPWAEnvironment = globalPwaStatus.value.hasPWAFeatures || e.matches
|
|
||||||
globalPwaStatus.value.isFullPWA = globalPwaStatus.value.hasPWAFeatures && 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,
|
|
||||||
isStateManagerAvailable,
|
|
||||||
checkPWAMode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局PWA状态管理器
|
|
||||||
export function useGlobalPWAState() {
|
|
||||||
// 检查是否在PWA环境中 - 使用统一的检测方式
|
|
||||||
const isPWAEnvironment = () => {
|
|
||||||
return globalPwaStatus.value?.isPWAEnvironment ?? isPWADisplayMode()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取存储的状态
|
|
||||||
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,
|
|
||||||
getStoredState,
|
|
||||||
clearStoredState,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* PWA状态恢复组合式API
|
||||||
|
* 提供2个专门的hooks:路由、标签页
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref, onMounted, onUnmounted, watch, inject } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import type { StateRestore } from '@/plugins/stateRestore'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 1. 动态标签页状态恢复
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态标签页状态恢复Hook
|
||||||
|
* 自动保存和恢复v-tabs的当前激活标签
|
||||||
|
*/
|
||||||
|
export function useTabStateRestore(defaultTab?: string) {
|
||||||
|
const route = useRoute()
|
||||||
|
const stateRestore = inject<StateRestore>('stateRestore')
|
||||||
|
|
||||||
|
const activeTab = ref<string>(defaultTab || '')
|
||||||
|
|
||||||
|
// 保存标签页状态
|
||||||
|
const saveTabState = (tab: string) => {
|
||||||
|
if (stateRestore && tab) {
|
||||||
|
stateRestore.tab.saveTabState(route.path, tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复标签页状态
|
||||||
|
const restoreTabState = () => {
|
||||||
|
if (stateRestore) {
|
||||||
|
const savedTab = stateRestore.tab.getTabState(route.path)
|
||||||
|
if (savedTab) {
|
||||||
|
activeTab.value = savedTab
|
||||||
|
console.log(`恢复标签页状态: ${route.path} -> ${savedTab}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听activeTab变化,自动保存
|
||||||
|
watch(activeTab, newTab => {
|
||||||
|
if (newTab) {
|
||||||
|
saveTabState(newTab)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件挂载时恢复状态
|
||||||
|
onMounted(() => {
|
||||||
|
// 先尝试恢复,如果没有保存的状态则使用默认值
|
||||||
|
if (!restoreTabState() && defaultTab) {
|
||||||
|
activeTab.value = defaultTab
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听全局恢复事件
|
||||||
|
const handleRestore = () => {
|
||||||
|
restoreTabState()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('pwa-state-restore', handleRestore)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('pwa-state-restore', handleRestore)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeTab,
|
||||||
|
saveTabState,
|
||||||
|
restoreTabState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 2. 路由状态恢复
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由状态恢复Hook
|
||||||
|
* 获取路由恢复信息,主要用于调试和监控
|
||||||
|
*/
|
||||||
|
export function useRouteStateRestore() {
|
||||||
|
const stateRestore = inject<StateRestore>('stateRestore')
|
||||||
|
|
||||||
|
const lastRestoredRoute = ref<any>(null)
|
||||||
|
|
||||||
|
// 获取上次保存的路由
|
||||||
|
const getLastSavedRoute = () => {
|
||||||
|
if (stateRestore) {
|
||||||
|
return stateRestore.route.restoreRoute()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动保存当前路由
|
||||||
|
const saveCurrentRoute = () => {
|
||||||
|
if (stateRestore) {
|
||||||
|
stateRestore.route.saveCurrentRoute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除路由状态
|
||||||
|
const clearRouteState = () => {
|
||||||
|
if (stateRestore) {
|
||||||
|
stateRestore.route.clearRoute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听全局恢复事件
|
||||||
|
const handleRestore = (event: Event) => {
|
||||||
|
const customEvent = event as CustomEvent
|
||||||
|
if (customEvent.detail?.route) {
|
||||||
|
lastRestoredRoute.value = customEvent.detail.route
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('pwa-state-restore', handleRestore)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('pwa-state-restore', handleRestore)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
lastRestoredRoute,
|
||||||
|
getLastSavedRoute,
|
||||||
|
saveCurrentRoute,
|
||||||
|
clearRouteState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 3. 全量状态恢复Hook
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全量状态恢复Hook
|
||||||
|
* 用于清理所有状态或获取统计信息
|
||||||
|
*/
|
||||||
|
export function useStateRestore() {
|
||||||
|
const stateRestore = inject<StateRestore>('stateRestore')
|
||||||
|
|
||||||
|
// 清除所有状态
|
||||||
|
const clearAllStates = () => {
|
||||||
|
if (stateRestore) {
|
||||||
|
stateRestore.clearAllStates()
|
||||||
|
console.log('已清除所有PWA状态')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态统计
|
||||||
|
const getStateStats = () => {
|
||||||
|
if (!stateRestore) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasRoute: !!stateRestore.route.restoreRoute(),
|
||||||
|
// 可以扩展更多统计信息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
clearAllStates,
|
||||||
|
getStateStats,
|
||||||
|
stateRestore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 4. 快捷Hook组合
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面级状态恢复Hook
|
||||||
|
* 组合路由和标签页状态恢复功能,适用于有标签页的页面
|
||||||
|
*/
|
||||||
|
export function usePageStateRestore(defaultTab?: string) {
|
||||||
|
const tabs = defaultTab ? useTabStateRestore(defaultTab) : null
|
||||||
|
const route = useRouteStateRestore()
|
||||||
|
const global = useStateRestore()
|
||||||
|
|
||||||
|
return {
|
||||||
|
tabs,
|
||||||
|
route,
|
||||||
|
global,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useTabStateRestore } from '@/composables/useStateRestore'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -8,23 +10,52 @@ const props = defineProps({
|
|||||||
type: Array as PropType<{ title: string; icon: string; tab: string }[]>,
|
type: Array as PropType<{ title: string; icon: string; tab: string }[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
// 新增:是否启用PWA状态恢复
|
||||||
|
enableStateRestore: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
const currentValue = ref(props.modelValue)
|
// 集成PWA状态恢复功能
|
||||||
|
const pwaTabState = props.enableStateRestore ? useTabStateRestore(props.modelValue) : null
|
||||||
|
|
||||||
|
// 使用PWA状态恢复的activeTab或本地状态
|
||||||
|
const currentValue = ref(pwaTabState?.activeTab.value || props.modelValue)
|
||||||
|
|
||||||
|
// 监听currentValue变化,同时更新PWA状态和父组件
|
||||||
watch(currentValue, newVal => {
|
watch(currentValue, newVal => {
|
||||||
emit('update:modelValue', newVal)
|
emit('update:modelValue', newVal)
|
||||||
|
// 如果启用了PWA状态恢复,同步更新PWA状态
|
||||||
|
if (pwaTabState && newVal) {
|
||||||
|
pwaTabState.activeTab.value = newVal
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听父组件的modelValue变化
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
value => {
|
value => {
|
||||||
currentValue.value = value
|
currentValue.value = value
|
||||||
|
// 同步到PWA状态
|
||||||
|
if (pwaTabState && value) {
|
||||||
|
pwaTabState.activeTab.value = value
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 如果启用了PWA状态恢复,监听PWA状态变化
|
||||||
|
if (pwaTabState) {
|
||||||
|
watch(pwaTabState.activeTab, newTab => {
|
||||||
|
if (newTab && newTab !== currentValue.value) {
|
||||||
|
currentValue.value = newTab
|
||||||
|
emit('update:modelValue', newTab)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Ref for the tabs container
|
// Ref for the tabs container
|
||||||
const tabsContainerRef = ref<HTMLElement | null>(null)
|
const tabsContainerRef = ref<HTMLElement | null>(null)
|
||||||
// State for showing the scroll indicator
|
// State for showing the scroll indicator
|
||||||
|
|||||||
+12
-72
@@ -43,39 +43,17 @@ import HeaderTab from './layouts/components/HeaderTab.vue'
|
|||||||
// 7. 样式文件 - 合并为单一导入
|
// 7. 样式文件 - 合并为单一导入
|
||||||
import '@/styles/main.scss'
|
import '@/styles/main.scss'
|
||||||
|
|
||||||
// 8. PWA状态管理和后台优化
|
// 8. 状态恢复插件
|
||||||
import { PWAStateController } from '@/utils/pwaStateManager'
|
import stateRestorePlugin from '@/plugins/stateRestore'
|
||||||
|
|
||||||
|
// 9. 后台优化工具
|
||||||
import { backgroundManager } from '@/utils/backgroundManager'
|
import { backgroundManager } from '@/utils/backgroundManager'
|
||||||
import { sseManagerSingleton } from '@/utils/sseManager'
|
import { sseManagerSingleton } from '@/utils/sseManager'
|
||||||
import { checkPWAStatus } from '@/@core/utils/navigator'
|
|
||||||
|
|
||||||
// PWA状态管理器初始化函数
|
|
||||||
const initializePWABeforeMount = async () => {
|
|
||||||
// 使用统一的PWA检测方法
|
|
||||||
const pwaStatus = await checkPWAStatus()
|
|
||||||
|
|
||||||
if (pwaStatus.isPWAEnvironment) {
|
|
||||||
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)
|
||||||
|
|
||||||
// 注册pinia
|
// 1. 注册pinia
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
|
|
||||||
// 异步加载远程组件(不阻塞启动)
|
// 异步加载远程组件(不阻塞启动)
|
||||||
@@ -83,13 +61,16 @@ loadRemoteComponents().catch(error => {
|
|||||||
console.error('Failed to load remote components', error)
|
console.error('Failed to load remote components', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 1. 注册 UI 框架
|
// 2. 注册 UI 框架
|
||||||
app.use(vuetify)
|
app.use(vuetify)
|
||||||
|
|
||||||
// 2. 注册路由
|
// 3. 注册路由
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
// 3. 注册全局组件
|
// 4. 注册状态恢复插件
|
||||||
|
app.use(stateRestorePlugin)
|
||||||
|
|
||||||
|
// 5. 注册全局组件
|
||||||
app
|
app
|
||||||
.component('VAceEditor', VAceEditor)
|
.component('VAceEditor', VAceEditor)
|
||||||
.component('VApexChart', VueApexCharts)
|
.component('VApexChart', VueApexCharts)
|
||||||
@@ -108,7 +89,7 @@ app
|
|||||||
.component('VHeaderTab', HeaderTab)
|
.component('VHeaderTab', HeaderTab)
|
||||||
.component('VPageContentTitle', PageContentTitle)
|
.component('VPageContentTitle', PageContentTitle)
|
||||||
|
|
||||||
// 4. 注册其他插件
|
// 6. 注册其他插件
|
||||||
app
|
app
|
||||||
.use(PerfectScrollbarPlugin)
|
.use(PerfectScrollbarPlugin)
|
||||||
.use(Toast, {
|
.use(Toast, {
|
||||||
@@ -119,49 +100,8 @@ app
|
|||||||
.use(i18n)
|
.use(i18n)
|
||||||
.mount('#app')
|
.mount('#app')
|
||||||
|
|
||||||
// 5. 添加状态恢复事件监听器
|
|
||||||
if (pwaStateController) {
|
|
||||||
// 监听状态恢复事件
|
|
||||||
window.addEventListener('pwaStateRestored', (event: Event) => {
|
|
||||||
// 可以在这里添加状态恢复后的处理逻辑
|
|
||||||
// 例如:通知Vue组件状态已恢复
|
|
||||||
app.config.globalProperties.$pwaStateRestored = true
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听应用即将卸载事件,保存状态
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
if (pwaStateController) {
|
|
||||||
pwaStateController.saveCurrentState()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听页面隐藏事件,保存状态
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.hidden && pwaStateController) {
|
|
||||||
pwaStateController.saveCurrentState()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 初始化后台优化工具
|
|
||||||
if (import.meta.env.MODE === 'development') {
|
|
||||||
;(window as any).backgroundManager = backgroundManager
|
|
||||||
;(window as any).sseManagerSingleton = sseManagerSingleton
|
|
||||||
;(window as any).debugBackground = () => {
|
|
||||||
console.table(backgroundManager.getTimersInfo())
|
|
||||||
console.log('Background Status:', backgroundManager.getStatus())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面卸载时清理后台管理器
|
// 页面卸载时清理后台管理器
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
backgroundManager.destroy()
|
backgroundManager.destroy()
|
||||||
sseManagerSingleton.closeAllManagers()
|
sseManagerSingleton.closeAllManagers()
|
||||||
|
|
||||||
if (pwaStateController) {
|
|
||||||
pwaStateController.destroy()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 导出状态管理器供其他模块使用
|
|
||||||
export { pwaStateController }
|
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* PWA状态恢复插件 - 极简版
|
||||||
|
* 只专注2个核心功能:路由、标签页
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { App } from 'vue'
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 1. 路由状态管理器
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class RouteStateManager {
|
||||||
|
private readonly STORAGE_KEY = 'pwa-current-route'
|
||||||
|
|
||||||
|
// 保存当前路由
|
||||||
|
saveCurrentRoute() {
|
||||||
|
const route = {
|
||||||
|
path: window.location.pathname,
|
||||||
|
search: window.location.search,
|
||||||
|
hash: window.location.hash,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(route))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复路由
|
||||||
|
restoreRoute() {
|
||||||
|
try {
|
||||||
|
const saved = sessionStorage.getItem(this.STORAGE_KEY)
|
||||||
|
if (!saved) return null
|
||||||
|
|
||||||
|
const route = JSON.parse(saved)
|
||||||
|
// 检查是否过期(1小时)
|
||||||
|
if (Date.now() - route.timestamp > 60 * 60 * 1000) {
|
||||||
|
this.clearRoute()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return route
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除路由状态
|
||||||
|
clearRoute() {
|
||||||
|
sessionStorage.removeItem(this.STORAGE_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化路由恢复
|
||||||
|
init() {
|
||||||
|
// 监听路由变化,自动保存
|
||||||
|
window.addEventListener('popstate', () => this.saveCurrentRoute())
|
||||||
|
|
||||||
|
// 页面隐藏时保存
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
this.saveCurrentRoute()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面卸载时保存
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
this.saveCurrentRoute()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 2. 动态标签页状态管理器
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class TabStateManager {
|
||||||
|
private readonly STORAGE_KEY = 'pwa-active-tabs'
|
||||||
|
|
||||||
|
// 保存标签页状态
|
||||||
|
saveTabState(routePath: string, activeTab: string) {
|
||||||
|
try {
|
||||||
|
const allTabs = this.getAllTabStates()
|
||||||
|
allTabs[routePath] = {
|
||||||
|
activeTab,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}
|
||||||
|
sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(allTabs))
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('保存标签页状态失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取标签页状态
|
||||||
|
getTabState(routePath: string): string | null {
|
||||||
|
try {
|
||||||
|
const allTabs = this.getAllTabStates()
|
||||||
|
const tabState = allTabs[routePath]
|
||||||
|
|
||||||
|
if (!tabState) return null
|
||||||
|
|
||||||
|
// 检查是否过期(1小时)
|
||||||
|
if (Date.now() - tabState.timestamp > 60 * 60 * 1000) {
|
||||||
|
delete allTabs[routePath]
|
||||||
|
sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(allTabs))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabState.activeTab
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有标签页状态
|
||||||
|
private getAllTabStates(): Record<string, any> {
|
||||||
|
try {
|
||||||
|
const saved = sessionStorage.getItem(this.STORAGE_KEY)
|
||||||
|
return saved ? JSON.parse(saved) : {}
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除标签页状态
|
||||||
|
clearTabState(routePath?: string) {
|
||||||
|
if (routePath) {
|
||||||
|
const allTabs = this.getAllTabStates()
|
||||||
|
delete allTabs[routePath]
|
||||||
|
sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(allTabs))
|
||||||
|
} else {
|
||||||
|
sessionStorage.removeItem(this.STORAGE_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 3. 主状态恢复管理器
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class StateRestore {
|
||||||
|
public route = new RouteStateManager()
|
||||||
|
public tab = new TabStateManager()
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
init() {
|
||||||
|
this.route.init()
|
||||||
|
this.setupAutoRestore()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置自动恢复
|
||||||
|
private setupAutoRestore() {
|
||||||
|
// 页面显示时检查是否需要恢复状态
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (!document.hidden) {
|
||||||
|
this.checkAndRestore()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面加载完成后恢复状态
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setTimeout(() => this.checkAndRestore(), 100)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setTimeout(() => this.checkAndRestore(), 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查并恢复状态
|
||||||
|
private checkAndRestore() {
|
||||||
|
// 1. 恢复路由(如果当前路径与保存的不同)
|
||||||
|
const savedRoute = this.route.restoreRoute()
|
||||||
|
if (savedRoute && savedRoute.path !== window.location.pathname) {
|
||||||
|
const fullPath = savedRoute.path + savedRoute.search + savedRoute.hash
|
||||||
|
console.log('恢复路由:', fullPath)
|
||||||
|
window.history.replaceState(null, '', fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 发送恢复事件,让组件自行处理标签页恢复
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('pwa-state-restore', {
|
||||||
|
detail: {
|
||||||
|
route: savedRoute,
|
||||||
|
tabs: this.tab,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除所有状态
|
||||||
|
clearAllStates() {
|
||||||
|
this.route.clearRoute()
|
||||||
|
this.tab.clearTabState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 4. Vue插件安装
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const stateRestore = new StateRestore()
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(app: App) {
|
||||||
|
// 注册全局属性
|
||||||
|
app.config.globalProperties.$stateRestore = stateRestore
|
||||||
|
|
||||||
|
// 提供注入
|
||||||
|
app.provide('stateRestore', stateRestore)
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
stateRestore.init()
|
||||||
|
|
||||||
|
console.log('PWA状态恢复插件已安装(路由 + 标签页)')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出管理器实例
|
||||||
|
export { stateRestore }
|
||||||
|
|
||||||
|
// 导出类型
|
||||||
|
export type { RouteStateManager, TabStateManager, StateRestore }
|
||||||
Vendored
-26
@@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 {}
|
|
||||||
@@ -1,738 +0,0 @@
|
|||||||
/**
|
|
||||||
* PWA状态管理器
|
|
||||||
* 用于在iOS设备上防止后台被杀时丢失状态,提供状态恢复功能
|
|
||||||
* 只在页面隐藏时收集状态,避免实时监听影响性能
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface ScrollPosition {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
element?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModalState {
|
|
||||||
id: string
|
|
||||||
isOpen: boolean
|
|
||||||
data?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FormFieldState {
|
|
||||||
selector: string
|
|
||||||
value: string | number | boolean
|
|
||||||
type: string
|
|
||||||
checked?: boolean
|
|
||||||
selectedIndex?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PWAState {
|
|
||||||
url: string
|
|
||||||
scrollPosition: number
|
|
||||||
scrollPositions: ScrollPosition[]
|
|
||||||
orientation: number
|
|
||||||
timestamp: number
|
|
||||||
appData?: any
|
|
||||||
formFields?: FormFieldState[]
|
|
||||||
modalStates?: ModalState[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PWAContext {
|
|
||||||
url: string
|
|
||||||
orientation: number
|
|
||||||
timestamp: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 状态收集器
|
|
||||||
* 只在需要时收集状态,不进行实时监听
|
|
||||||
*/
|
|
||||||
export class StateCollector {
|
|
||||||
static collectScrollPositions(): ScrollPosition[] {
|
|
||||||
const positions: ScrollPosition[] = []
|
|
||||||
|
|
||||||
positions.push({
|
|
||||||
x: window.scrollX,
|
|
||||||
y: window.scrollY,
|
|
||||||
element: 'window',
|
|
||||||
})
|
|
||||||
|
|
||||||
const scrollContainers = [
|
|
||||||
'.v-main__wrap',
|
|
||||||
'.v-card-text',
|
|
||||||
'.v-sheet',
|
|
||||||
'.perfect-scrollbar',
|
|
||||||
'[data-simplebar]',
|
|
||||||
'.overflow-auto',
|
|
||||||
'.overflow-y-auto',
|
|
||||||
]
|
|
||||||
|
|
||||||
scrollContainers.forEach(selector => {
|
|
||||||
const elements = document.querySelectorAll(selector)
|
|
||||||
elements.forEach(element => {
|
|
||||||
if (element.scrollTop > 0 || element.scrollLeft > 0) {
|
|
||||||
positions.push({
|
|
||||||
x: element.scrollLeft,
|
|
||||||
y: element.scrollTop,
|
|
||||||
element: this.generateElementSelector(element),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return positions
|
|
||||||
}
|
|
||||||
|
|
||||||
static collectModalStates(): ModalState[] {
|
|
||||||
const states: ModalState[] = []
|
|
||||||
|
|
||||||
const modalSelectors = [
|
|
||||||
'.v-dialog',
|
|
||||||
'.v-menu',
|
|
||||||
'.v-overlay',
|
|
||||||
'.v-tooltip',
|
|
||||||
'.v-snackbar',
|
|
||||||
'.modal',
|
|
||||||
'.popup',
|
|
||||||
'.drawer',
|
|
||||||
'.v-navigation-drawer',
|
|
||||||
'[role="dialog"]',
|
|
||||||
'[role="alertdialog"]',
|
|
||||||
]
|
|
||||||
|
|
||||||
modalSelectors.forEach(selector => {
|
|
||||||
const elements = document.querySelectorAll(selector)
|
|
||||||
elements.forEach(element => {
|
|
||||||
if (this.isModalOpen(element)) {
|
|
||||||
states.push({
|
|
||||||
id: this.getModalId(element),
|
|
||||||
isOpen: true,
|
|
||||||
data: this.extractModalData(element),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return states
|
|
||||||
}
|
|
||||||
|
|
||||||
static collectFormFields(): FormFieldState[] {
|
|
||||||
const fields: FormFieldState[] = []
|
|
||||||
|
|
||||||
const formElements = document.querySelectorAll('input, textarea, select')
|
|
||||||
formElements.forEach(element => {
|
|
||||||
const inputElement = element as HTMLInputElement
|
|
||||||
|
|
||||||
if (inputElement.type === 'password' || inputElement.type === 'hidden') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputElement.value || inputElement.checked) {
|
|
||||||
fields.push({
|
|
||||||
selector: this.getFieldSelector(inputElement),
|
|
||||||
value: inputElement.value,
|
|
||||||
type: inputElement.type,
|
|
||||||
checked: inputElement.checked,
|
|
||||||
selectedIndex:
|
|
||||||
inputElement.tagName === 'SELECT'
|
|
||||||
? (inputElement as unknown as HTMLSelectElement).selectedIndex
|
|
||||||
: undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return fields
|
|
||||||
}
|
|
||||||
|
|
||||||
static restoreScrollPositions(positions: ScrollPosition[]): void {
|
|
||||||
positions.forEach(pos => {
|
|
||||||
if (pos.element === 'window') {
|
|
||||||
window.scrollTo({ top: pos.y, left: pos.x, behavior: 'auto' })
|
|
||||||
} else {
|
|
||||||
const elements = document.querySelectorAll(pos.element!)
|
|
||||||
elements.forEach(element => {
|
|
||||||
element.scrollTo({ top: pos.y, left: pos.x, behavior: 'auto' })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
static restoreModalStates(states: ModalState[]): void {
|
|
||||||
states.forEach(state => {
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent('restoreModalState', {
|
|
||||||
detail: state,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
static restoreFormFields(fields: FormFieldState[]): void {
|
|
||||||
fields.forEach(field => {
|
|
||||||
const elements = document.querySelectorAll(field.selector)
|
|
||||||
elements.forEach(element => {
|
|
||||||
const inputElement = element as HTMLInputElement
|
|
||||||
|
|
||||||
if (field.type === 'checkbox' || field.type === 'radio') {
|
|
||||||
inputElement.checked = field.checked || false
|
|
||||||
} else if (field.type === 'select-one' || field.type === 'select-multiple') {
|
|
||||||
const selectElement = inputElement as unknown as HTMLSelectElement
|
|
||||||
if (field.selectedIndex !== undefined) {
|
|
||||||
selectElement.selectedIndex = field.selectedIndex
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
inputElement.value = field.value as string
|
|
||||||
}
|
|
||||||
|
|
||||||
inputElement.dispatchEvent(new Event('input', { bubbles: true }))
|
|
||||||
inputElement.dispatchEvent(new Event('change', { bubbles: true }))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isModalOpen(element: Element): boolean {
|
|
||||||
const computedStyle = window.getComputedStyle(element)
|
|
||||||
return (
|
|
||||||
computedStyle.display !== 'none' &&
|
|
||||||
computedStyle.visibility !== 'hidden' &&
|
|
||||||
computedStyle.opacity !== '0' &&
|
|
||||||
!element.hasAttribute('hidden') &&
|
|
||||||
element.getAttribute('aria-hidden') !== 'true'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static getModalId(element: Element): string {
|
|
||||||
return (
|
|
||||||
element.id ||
|
|
||||||
element.getAttribute('data-modal-id') ||
|
|
||||||
element.className.replace(/\s+/g, '-') ||
|
|
||||||
`modal-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static extractModalData(element: Element): any {
|
|
||||||
const data: any = {}
|
|
||||||
|
|
||||||
const inputs = element.querySelectorAll('input, select, textarea')
|
|
||||||
if (inputs.length > 0) {
|
|
||||||
data.formData = {}
|
|
||||||
inputs.forEach(input => {
|
|
||||||
const inputElement = input as HTMLInputElement
|
|
||||||
if (inputElement.name && inputElement.value) {
|
|
||||||
data.formData[inputElement.name] = inputElement.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollableElements = element.querySelectorAll('[class*="overflow"], .v-card-text')
|
|
||||||
if (scrollableElements.length > 0) {
|
|
||||||
data.scrollPositions = Array.from(scrollableElements).map(el => ({
|
|
||||||
selector: this.generateElementSelector(el),
|
|
||||||
x: el.scrollLeft,
|
|
||||||
y: el.scrollTop,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
private static generateElementSelector(element: Element): string {
|
|
||||||
if (element.id) {
|
|
||||||
return `#${element.id}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = []
|
|
||||||
let current = element
|
|
||||||
|
|
||||||
while (current && current !== document.body) {
|
|
||||||
let selector = current.tagName.toLowerCase()
|
|
||||||
|
|
||||||
if (current.id) {
|
|
||||||
selector += `#${current.id}`
|
|
||||||
path.unshift(selector)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current.className) {
|
|
||||||
const classes = current.className.split(/\s+/).filter(c => c && !c.includes('v-'))
|
|
||||||
if (classes.length > 0) {
|
|
||||||
selector += `.${classes[0]}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use nth-child instead of nth-of-type, but only when necessary
|
|
||||||
const parent = current.parentElement
|
|
||||||
if (parent) {
|
|
||||||
const siblings = Array.from(parent.children).filter(
|
|
||||||
child => child.tagName === current.tagName && child.className === current.className,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (siblings.length > 1) {
|
|
||||||
const index = siblings.indexOf(current) + 1
|
|
||||||
selector += `:nth-child(${index})`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
path.unshift(selector)
|
|
||||||
current = current.parentElement as Element
|
|
||||||
|
|
||||||
if (path.length >= 4) break
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.join(' > ')
|
|
||||||
}
|
|
||||||
|
|
||||||
private static getFieldSelector(element: HTMLInputElement): string {
|
|
||||||
if (element.id) return `#${element.id}`
|
|
||||||
if (element.name) return `[name="${element.name}"]`
|
|
||||||
|
|
||||||
const path = []
|
|
||||||
let current = element as Element
|
|
||||||
|
|
||||||
while (current && current !== document.body) {
|
|
||||||
let selector = current.tagName.toLowerCase()
|
|
||||||
|
|
||||||
if (current.id) {
|
|
||||||
selector += `#${current.id}`
|
|
||||||
path.unshift(selector)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current.className) {
|
|
||||||
const classes = current.className.split(/\s+/).filter(c => c)
|
|
||||||
if (classes.length > 0) {
|
|
||||||
selector += `.${classes[0]}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
path.unshift(selector)
|
|
||||||
current = current.parentNode as Element
|
|
||||||
|
|
||||||
if (path.length >= 3) break
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.join(' > ')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基础状态管理器(使用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.appData?.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 {
|
|
||||||
// 通过Service Worker的fetch拦截器保存状态
|
|
||||||
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 = 60 * 60 * 1000 // 60分钟,延长有效期
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
// 即使URL不匹配,也可以恢复一些基础状态(如滚动位置除外)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设备方向变化不阻止状态恢复
|
|
||||||
if (this.isOrientationChanged(savedState, currentContext)) {
|
|
||||||
// 继续恢复
|
|
||||||
}
|
|
||||||
|
|
||||||
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 PWAStateController {
|
|
||||||
private stateManager: PWAStateManager
|
|
||||||
private indexedDBManager: PWAIndexedDBManager
|
|
||||||
private swStateSync: ServiceWorkerStateSync
|
|
||||||
private restoreDecision: StateRestoreDecision
|
|
||||||
private stateRestorePromise: Promise<void> | null = null
|
|
||||||
private stateRestoreResolve: (() => void) | null = null
|
|
||||||
private isRestoring = false
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.stateManager = new PWAStateManager()
|
|
||||||
this.indexedDBManager = new PWAIndexedDBManager()
|
|
||||||
this.swStateSync = new ServiceWorkerStateSync()
|
|
||||||
this.restoreDecision = new StateRestoreDecision()
|
|
||||||
|
|
||||||
this.stateRestorePromise = new Promise(resolve => {
|
|
||||||
this.stateRestoreResolve = resolve
|
|
||||||
})
|
|
||||||
|
|
||||||
this.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
async waitForStateRestore(): Promise<void> {
|
|
||||||
return this.stateRestorePromise || Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async init(): Promise<void> {
|
|
||||||
this.stateManager.clearExpiredState()
|
|
||||||
await this.checkAndRestoreState()
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkAndRestoreState(): Promise<void> {
|
|
||||||
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(),
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const source of sources) {
|
|
||||||
try {
|
|
||||||
const savedState = await source()
|
|
||||||
if (this.restoreDecision.shouldRestoreState(savedState, currentContext)) {
|
|
||||||
await this.restoreState(savedState!)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 静默处理错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.isRestoring = false
|
|
||||||
// 状态恢复完成(无论成功还是失败)
|
|
||||||
if (this.stateRestoreResolve) {
|
|
||||||
this.stateRestoreResolve()
|
|
||||||
this.stateRestoreResolve = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveCurrentState(): Promise<void> {
|
|
||||||
const scrollPositions = StateCollector.collectScrollPositions()
|
|
||||||
const modalStates = StateCollector.collectModalStates()
|
|
||||||
const formFields = StateCollector.collectFormFields()
|
|
||||||
|
|
||||||
const state: PWAState = {
|
|
||||||
url: window.location.href,
|
|
||||||
scrollPosition: window.scrollY,
|
|
||||||
scrollPositions:
|
|
||||||
scrollPositions.length > 0 ? scrollPositions : [{ x: window.scrollX, y: window.scrollY, element: 'window' }],
|
|
||||||
orientation: window.orientation || 0,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
appData: this.getAppSpecificState(),
|
|
||||||
modalStates: modalStates.length > 0 ? modalStates : undefined,
|
|
||||||
formFields: formFields.length > 0 ? formFields : undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
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> {
|
|
||||||
const currentUrl = window.location.href
|
|
||||||
const urlMatches = this.isUrlExactMatch(state.url, currentUrl)
|
|
||||||
|
|
||||||
if (state.scrollPositions && urlMatches) {
|
|
||||||
StateCollector.restoreScrollPositions(state.scrollPositions)
|
|
||||||
} else if (state.scrollPosition && urlMatches) {
|
|
||||||
window.scrollTo({
|
|
||||||
top: state.scrollPosition,
|
|
||||||
behavior: 'auto',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.modalStates) {
|
|
||||||
StateCollector.restoreModalStates(state.modalStates)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.formFields && urlMatches) {
|
|
||||||
StateCollector.restoreFormFields(state.formFields)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.appData) {
|
|
||||||
this.restoreAppSpecificState(state.appData)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 getAppSpecificState(): any {
|
|
||||||
return {
|
|
||||||
routerState: {
|
|
||||||
currentRoute: window.location.pathname,
|
|
||||||
query: window.location.search,
|
|
||||||
hash: window.location.hash,
|
|
||||||
},
|
|
||||||
uiState: {
|
|
||||||
sidebarOpen: document.querySelector('.v-navigation-drawer--active') !== null,
|
|
||||||
darkMode: document.documentElement.getAttribute('data-theme') === 'dark',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private restoreAppSpecificState(appData: any): void {
|
|
||||||
// 基础状态恢复,可根据需要扩展
|
|
||||||
}
|
|
||||||
|
|
||||||
private dispatchStateRestoreEvent(state: PWAState): void {
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent('pwaStateRestored', {
|
|
||||||
detail: { state },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(): void {
|
|
||||||
// 无需清理资源
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user