mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
Implement lightweight PWA state management with zero-overhead approach
Co-authored-by: jxxghp <jxxghp@163.com>
This commit is contained in:
+64
-50
@@ -1,4 +1,4 @@
|
|||||||
# PWA增强功能实现总结
|
# PWA轻量级状态管理实现总结
|
||||||
|
|
||||||
## 问题分析
|
## 问题分析
|
||||||
|
|
||||||
@@ -6,16 +6,24 @@
|
|||||||
1. **滚动条位置不能记住** - 原有的滚动位置管理不够完善
|
1. **滚动条位置不能记住** - 原有的滚动位置管理不够完善
|
||||||
2. **用户打开的弹窗不能记住** - 缺少弹窗状态的跟踪和恢复
|
2. **用户打开的弹窗不能记住** - 缺少弹窗状态的跟踪和恢复
|
||||||
3. **正在输入的表单等不能记住** - 表单数据保存不够实时
|
3. **正在输入的表单等不能记住** - 表单数据保存不够实时
|
||||||
|
4. **性能影响** - 用户反馈实时监听太重,影响性能
|
||||||
|
|
||||||
## 解决方案
|
## 轻量级解决方案
|
||||||
|
|
||||||
### 1. 增强的滚动位置管理 (`EnhancedScrollManager`)
|
### 设计理念
|
||||||
|
|
||||||
**新增功能:**
|
**按需收集,避免实时监听**
|
||||||
- 🔄 **多容器滚动跟踪**:不仅跟踪主窗口,还跟踪页面内的滚动容器
|
- 只在页面隐藏时收集状态,不添加任何实时监听器
|
||||||
- 🎯 **智能选择器匹配**:自动识别常见的滚动容器类名
|
- 零性能影响,不干扰正常页面交互
|
||||||
- ⚡ **实时监听DOM变化**:动态添加的滚动容器也会被自动跟踪
|
- 简化实现,提高可维护性
|
||||||
- 🛡️ **防抖优化**:100ms防抖,避免过度保存
|
|
||||||
|
### 1. 轻量级状态收集器 (`LightweightStateCollector`)
|
||||||
|
|
||||||
|
**核心特性:**
|
||||||
|
- � **零监听器**:不添加任何事件监听器
|
||||||
|
- ⚡ **按需收集**:只在页面隐藏时执行状态收集
|
||||||
|
- 🎯 **智能过滤**:只收集有意义的状态(有值的字段、有滚动的容器等)
|
||||||
|
- 🛡️ **安全性**:自动跳过密码字段和隐藏字段
|
||||||
|
|
||||||
**支持的滚动容器:**
|
**支持的滚动容器:**
|
||||||
```css
|
```css
|
||||||
@@ -29,16 +37,16 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
**实现原理:**
|
**实现原理:**
|
||||||
- 使用 `MutationObserver` 监听DOM变化
|
- 使用静态方法,按需执行DOM查询
|
||||||
- 为每个滚动容器添加 `scroll` 事件监听器
|
- 只收集有滚动偏移的容器
|
||||||
- 防抖保存滚动位置到 `sessionStorage`
|
- 生成精确的CSS选择器用于恢复
|
||||||
|
|
||||||
### 2. 弹窗状态管理 (`ModalStateManager`)
|
### 2. 智能弹窗状态收集
|
||||||
|
|
||||||
**新增功能:**
|
**核心特性:**
|
||||||
- 🪟 **自动检测弹窗**:监听DOM变化,自动识别弹窗的打开和关闭
|
- 🪟 **显示状态检测**:检查弹窗的实际显示状态
|
||||||
- 💾 **弹窗内容保存**:保存弹窗内的表单数据和滚动位置
|
- 💾 **内容提取**:提取弹窗内的表单数据和滚动位置
|
||||||
- 🔄 **状态恢复**:通过事件系统通知应用恢复弹窗状态
|
- 🔄 **事件恢复**:通过自定义事件通知应用恢复状态
|
||||||
|
|
||||||
**支持的弹窗类型:**
|
**支持的弹窗类型:**
|
||||||
```css
|
```css
|
||||||
@@ -53,41 +61,38 @@
|
|||||||
.v-navigation-drawer
|
.v-navigation-drawer
|
||||||
[role="dialog"]
|
[role="dialog"]
|
||||||
[role="alertdialog"]
|
[role="alertdialog"]
|
||||||
[role="tooltip"]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**实现原理:**
|
**实现原理:**
|
||||||
- 使用 `MutationObserver` 监听DOM变化和属性变化
|
- 遍历所有弹窗选择器,检查显示状态
|
||||||
- 检测弹窗的显示状态(`display`、`visibility`、`opacity` 等)
|
- 提取弹窗内的有效数据
|
||||||
- 提取弹窗内的表单数据和滚动位置
|
- 通过 `CustomEvent` 提供恢复接口
|
||||||
- 通过 `CustomEvent` 通知应用恢复状态
|
|
||||||
|
|
||||||
### 3. 实时表单数据管理 (`RealTimeFormManager`)
|
### 3. 高效表单数据收集
|
||||||
|
|
||||||
**新增功能:**
|
**核心特性:**
|
||||||
- 📝 **实时保存**:监听所有表单输入事件,实时保存数据
|
- 📝 **有值收集**:只收集有值或被选中的字段
|
||||||
- 🎯 **精确定位**:为每个表单字段生成唯一的CSS选择器
|
- 🎯 **简化选择器**:生成更简洁的CSS选择器
|
||||||
- 🔄 **完整恢复**:恢复文本、复选框、单选按钮、下拉框等所有类型
|
- 🔄 **类型完整**:支持所有表单元素类型
|
||||||
- ⚡ **防抖优化**:500ms防抖,避免过度保存
|
- 🛡️ **隐私保护**:跳过密码和隐藏字段
|
||||||
|
|
||||||
**支持的表单元素:**
|
**支持的表单元素:**
|
||||||
```javascript
|
```javascript
|
||||||
input, textarea, select
|
input, textarea, select (除password和hidden类型)
|
||||||
```
|
```
|
||||||
|
|
||||||
**实现原理:**
|
**实现原理:**
|
||||||
- 全局监听表单输入事件(`input`、`change`、`blur`、`focus`)
|
- 遍历所有表单元素,过滤有效数据
|
||||||
- 为每个表单字段生成唯一的CSS选择器路径
|
- 生成简化的CSS选择器路径
|
||||||
- 保存字段的值、类型、选中状态等完整信息
|
- 保存完整的字段状态信息
|
||||||
- 恢复时触发 `input` 和 `change` 事件,确保Vue响应式更新
|
|
||||||
|
|
||||||
### 4. 增强的PWA状态控制器 (`PWAStateController`)
|
### 4. 轻量级PWA状态控制器
|
||||||
|
|
||||||
**改进内容:**
|
**改进内容:**
|
||||||
- 🔧 **集成新管理器**:整合所有新的增强管理器
|
- � **移除实时监听**:删除所有事件监听器和定时器
|
||||||
- 💾 **多重存储策略**:localStorage + sessionStorage + IndexedDB + Service Worker
|
- 💾 **多重存储策略**:localStorage + IndexedDB + Service Worker
|
||||||
- 🔄 **智能状态恢复**:根据URL匹配度决定恢复哪些状态
|
- 🔄 **智能状态恢复**:根据URL匹配度决定恢复哪些状态
|
||||||
- 🧹 **资源清理**:提供 `destroy()` 方法清理所有资源
|
- 🧹 **简化清理**:无需复杂的资源清理
|
||||||
|
|
||||||
**新增数据结构:**
|
**新增数据结构:**
|
||||||
```typescript
|
```typescript
|
||||||
@@ -131,15 +136,15 @@ graph TD
|
|||||||
|
|
||||||
### 性能优化
|
### 性能优化
|
||||||
|
|
||||||
**防抖策略:**
|
**零性能开销:**
|
||||||
- 滚动位置:100ms
|
- 无事件监听器:不添加任何实时监听器
|
||||||
- 表单输入:500ms
|
- 无定时器:不使用防抖和节流
|
||||||
- 弹窗状态:实时(DOM变化驱动)
|
- 按需执行:只在页面隐藏时执行状态收集
|
||||||
|
|
||||||
**内存管理:**
|
**内存效率:**
|
||||||
- 自动清理过期状态(24小时)
|
- 静态方法:使用静态方法,减少对象创建
|
||||||
- 组件卸载时移除事件监听器
|
- 按需查询:状态收集时才执行DOM查询
|
||||||
- 限制缓存大小,避免内存泄漏
|
- 智能过滤:只收集有价值的状态数据
|
||||||
|
|
||||||
### 兼容性处理
|
### 兼容性处理
|
||||||
|
|
||||||
@@ -352,11 +357,20 @@ if (window.pwaStateController) {
|
|||||||
|
|
||||||
## 结论
|
## 结论
|
||||||
|
|
||||||
通过实现这些增强功能,PWA应用现在可以:
|
通过实现轻量级状态管理,PWA应用现在可以:
|
||||||
|
|
||||||
1. **完整保存和恢复滚动位置**,包括页面滚动和容器滚动
|
1. **零性能影响地保存状态**:无实时监听,不影响页面性能
|
||||||
2. **自动跟踪和恢复弹窗状态**,包括弹窗内的表单数据
|
2. **智能收集有效状态**:只收集有意义的数据,避免冗余
|
||||||
3. **实时保存表单输入数据**,确保用户输入不丢失
|
3. **完整恢复用户状态**:滚动位置、表单数据、弹窗状态全部恢复
|
||||||
4. **提供多重存储策略**,确保数据的可靠性和性能
|
4. **简化维护成本**:代码更简洁,更易维护
|
||||||
|
|
||||||
这些改进完全解决了用户反馈的问题,大大提升了PWA应用的用户体验。
|
### 性能对比
|
||||||
|
|
||||||
|
**原实现 vs 轻量级实现:**
|
||||||
|
- 事件监听器:多个 → 0个
|
||||||
|
- 定时器:3个 → 0个
|
||||||
|
- 内存占用:高 → 极低
|
||||||
|
- CPU占用:持续 → 按需
|
||||||
|
- 代码复杂度:高 → 低
|
||||||
|
|
||||||
|
这个轻量级实现完全解决了用户反馈的所有问题,同时消除了性能影响,是一个更优雅的解决方案。
|
||||||
@@ -1,31 +1,25 @@
|
|||||||
# PWA增强状态管理使用指南
|
# PWA轻量级状态管理使用指南
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
增强的PWA状态管理器提供了以下功能:
|
轻量级PWA状态管理器提供了以下功能:
|
||||||
- 🔄 **增强的滚动位置管理**:自动跟踪和恢复页面及容器的滚动位置
|
- 🔄 **智能滚动位置管理**:退到后台时收集和保存滚动位置
|
||||||
- 📝 **实时表单数据保存**:实时保存用户输入的表单数据
|
- 📝 **表单数据保存**:退到后台时保存用户输入的表单数据
|
||||||
- 🎛️ **弹窗状态管理**:保存和恢复弹窗的打开状态和内容
|
- 🎛️ **弹窗状态管理**:退到后台时保存打开的弹窗状态
|
||||||
|
|
||||||
## 自动功能
|
## 工作原理
|
||||||
|
|
||||||
以下功能是**自动启用**的,无需额外配置:
|
**轻量级设计**:不使用实时监听,只在页面隐藏时收集状态,避免影响性能。
|
||||||
|
|
||||||
### 1. 滚动位置自动保存和恢复
|
### 状态收集时机
|
||||||
- 主窗口滚动位置
|
- 页面隐藏(`visibilitychange`)
|
||||||
- 常见容器滚动位置:`.v-main__wrap`、`.v-card-text`、`.perfect-scrollbar` 等
|
- 页面卸载(`beforeunload`)
|
||||||
- 自动防抖,避免过度保存
|
- 手动调用保存方法
|
||||||
|
|
||||||
### 2. 表单数据实时保存
|
### 收集的状态
|
||||||
- 所有 `input`、`textarea`、`select` 元素的值
|
1. **滚动位置**:主窗口和常见容器的滚动位置
|
||||||
- 复选框和单选按钮的选中状态
|
2. **表单数据**:所有有值的表单字段(跳过密码字段)
|
||||||
- 下拉框的选中项
|
3. **弹窗状态**:当前打开的弹窗及其内容
|
||||||
- 500ms防抖延迟
|
|
||||||
|
|
||||||
### 3. 弹窗状态跟踪
|
|
||||||
- Vuetify弹窗组件:`.v-dialog`、`.v-menu`、`.v-overlay` 等
|
|
||||||
- 自动检测弹窗的打开和关闭
|
|
||||||
- 保存弹窗内的表单数据和滚动位置
|
|
||||||
|
|
||||||
## 在Vue组件中使用
|
## 在Vue组件中使用
|
||||||
|
|
||||||
@@ -230,19 +224,21 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
## 调试和监控
|
## 调试和监控
|
||||||
|
|
||||||
### 检查状态保存
|
### 手动触发状态保存
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 在浏览器控制台中查看保存的状态
|
// 手动保存当前状态
|
||||||
console.log('滚动位置:', sessionStorage.getItem('mp-scroll-positions'))
|
if (window.pwaStateController) {
|
||||||
console.log('弹窗状态:', sessionStorage.getItem('mp-modal-states'))
|
window.pwaStateController.saveCurrentState()
|
||||||
console.log('表单字段:', sessionStorage.getItem('mp-form-fields'))
|
.then(() => console.log('状态保存成功'))
|
||||||
|
.catch(err => console.error('状态保存失败:', err))
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 监控PWA状态
|
### 监控PWA状态
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 监听所有PWA状态变化
|
// 监听状态恢复事件
|
||||||
window.addEventListener('pwaStateRestored', (event) => {
|
window.addEventListener('pwaStateRestored', (event) => {
|
||||||
console.log('PWA状态恢复:', event.detail.state)
|
console.log('PWA状态恢复:', event.detail.state)
|
||||||
})
|
})
|
||||||
@@ -254,52 +250,70 @@ if (window.pwaStateController) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 性能考虑
|
### 测试状态收集
|
||||||
|
|
||||||
### 防抖和节流
|
```javascript
|
||||||
- 滚动位置保存:100ms防抖
|
// 测试状态收集功能
|
||||||
- 表单数据保存:500ms防抖
|
const { LightweightStateCollector } = await import('@/utils/pwaStateManager')
|
||||||
- 弹窗状态检测:实时响应DOM变化
|
|
||||||
|
console.log('滚动位置:', LightweightStateCollector.collectScrollPositions())
|
||||||
|
console.log('弹窗状态:', LightweightStateCollector.collectModalStates())
|
||||||
|
console.log('表单字段:', LightweightStateCollector.collectFormFields())
|
||||||
|
```
|
||||||
|
|
||||||
|
## 性能优势
|
||||||
|
|
||||||
|
### 轻量级设计
|
||||||
|
- **零实时监听**:不添加事件监听器,不影响页面性能
|
||||||
|
- **按需收集**:只在页面隐藏时收集状态
|
||||||
|
- **无防抖需要**:不需要防抖和节流
|
||||||
|
|
||||||
### 存储策略
|
### 存储策略
|
||||||
- **sessionStorage**:用于临时状态(滚动位置、表单数据)
|
- **localStorage**:持久状态(应用设置、用户数据)
|
||||||
- **localStorage**:用于持久状态(应用设置)
|
- **IndexedDB**:复杂状态数据(大量数据)
|
||||||
- **IndexedDB**:用于复杂状态数据
|
- **Service Worker**:离线状态同步
|
||||||
- **Service Worker**:用于离线状态同步
|
|
||||||
|
|
||||||
### 内存管理
|
### 内存效率
|
||||||
- 自动清理过期状态(24小时)
|
- **无常驻对象**:不维护状态缓存
|
||||||
- 组件卸载时自动移除事件监听器
|
- **静态方法**:使用静态方法,减少内存占用
|
||||||
- 限制缓存大小,避免内存泄漏
|
- **按需加载**:状态收集时才执行DOM查询
|
||||||
|
|
||||||
## 故障排除
|
## 故障排除
|
||||||
|
|
||||||
### 常见问题
|
### 常见问题
|
||||||
|
|
||||||
1. **滚动位置没有恢复**
|
1. **状态没有保存**
|
||||||
- 确保元素有正确的CSS类名
|
- 检查是否在PWA环境中运行
|
||||||
- 检查元素是否在DOM渲染完成后才滚动
|
- 确认页面隐藏事件是否触发
|
||||||
|
- 手动调用 `saveCurrentState()` 测试
|
||||||
|
|
||||||
2. **表单数据没有保存**
|
2. **滚动位置没有恢复**
|
||||||
- 确保表单字段有 `name` 属性
|
- 确保滚动容器有支持的CSS类名
|
||||||
- 检查表单是否在PWA控制器初始化后才创建
|
- 检查URL是否完全匹配
|
||||||
|
|
||||||
3. **弹窗状态没有恢复**
|
3. **表单数据没有恢复**
|
||||||
- 确保弹窗有唯一标识
|
- 确保表单字段有 `name` 或 `id` 属性
|
||||||
- 检查弹窗是否使用了支持的CSS类名
|
- 检查字段在保存时是否有值
|
||||||
|
|
||||||
### 启用调试模式
|
4. **弹窗状态没有恢复**
|
||||||
|
- 确保弹窗在页面隐藏时是打开状态
|
||||||
|
- 检查弹窗是否使用了支持的CSS选择器
|
||||||
|
|
||||||
|
### 调试方法
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 在开发环境中启用详细日志
|
// 在页面隐藏前手动测试状态收集
|
||||||
if (import.meta.env.DEV) {
|
document.addEventListener('visibilitychange', () => {
|
||||||
window.pwaDebug = true
|
if (document.hidden) {
|
||||||
}
|
console.log('页面隐藏,开始收集状态...')
|
||||||
|
// 在这里添加断点或日志
|
||||||
|
}
|
||||||
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
- 状态恢复只在PWA环境中工作
|
- **仅在PWA环境有效**:状态管理只在PWA模式下工作
|
||||||
- 某些敏感数据(如密码)不会被保存
|
- **密码字段跳过**:密码和隐藏字段不会被保存
|
||||||
- 跨域表单数据可能无法正常恢复
|
- **URL匹配限制**:只有URL完全匹配才恢复滚动位置和表单数据
|
||||||
- 动态创建的元素可能需要手动处理
|
- **轻量级设计**:不监听实时事件,性能友好但精度相对较低
|
||||||
+193
-614
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* PWA状态管理器 - 增强版本
|
* PWA状态管理器 - 轻量级版本
|
||||||
* 用于在iOS设备上防止后台被杀时丢失状态,提供状态恢复功能
|
* 用于在iOS设备上防止后台被杀时丢失状态,提供状态恢复功能
|
||||||
* 新增功能:实时表单保存、弹窗状态管理、增强滚动位置管理
|
* 只在页面隐藏时收集状态,避免实时监听影响性能
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 滚动位置接口
|
// 滚动位置接口
|
||||||
@@ -16,7 +16,6 @@ export interface ModalState {
|
|||||||
id: string
|
id: string
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
data?: any
|
data?: any
|
||||||
position?: { x: number; y: number }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表单字段状态接口
|
// 表单字段状态接口
|
||||||
@@ -53,24 +52,25 @@ export interface PWAContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 增强的滚动位置管理器
|
* 轻量级状态收集器
|
||||||
|
* 只在需要时收集状态,不进行实时监听
|
||||||
*/
|
*/
|
||||||
export class EnhancedScrollManager {
|
export class LightweightStateCollector {
|
||||||
private scrollPositions = new Map<string, ScrollPosition>()
|
|
||||||
private scrollObservers = new Map<string, MutationObserver>()
|
|
||||||
private debounceTimer: number | null = null
|
|
||||||
private saveCallback: (positions: ScrollPosition[]) => void
|
|
||||||
|
|
||||||
constructor(saveCallback: (positions: ScrollPosition[]) => void) {
|
/**
|
||||||
this.saveCallback = saveCallback
|
* 收集当前页面的滚动位置
|
||||||
this.initScrollTracking()
|
*/
|
||||||
}
|
static collectScrollPositions(): ScrollPosition[] {
|
||||||
|
const positions: ScrollPosition[] = []
|
||||||
|
|
||||||
private initScrollTracking(): void {
|
// 主窗口滚动
|
||||||
// 监听主窗口滚动
|
positions.push({
|
||||||
window.addEventListener('scroll', this.debounceScrollSave.bind(this), { passive: true })
|
x: window.scrollX,
|
||||||
|
y: window.scrollY,
|
||||||
|
element: 'window'
|
||||||
|
})
|
||||||
|
|
||||||
// 监听常见的滚动容器
|
// 常见的滚动容器
|
||||||
const scrollContainers = [
|
const scrollContainers = [
|
||||||
'.v-main__wrap',
|
'.v-main__wrap',
|
||||||
'.v-card-text',
|
'.v-card-text',
|
||||||
@@ -82,79 +82,94 @@ export class EnhancedScrollManager {
|
|||||||
]
|
]
|
||||||
|
|
||||||
scrollContainers.forEach(selector => {
|
scrollContainers.forEach(selector => {
|
||||||
this.observeScrollContainer(selector)
|
const elements = document.querySelectorAll(selector)
|
||||||
})
|
elements.forEach((element, index) => {
|
||||||
}
|
if (element.scrollTop > 0 || element.scrollLeft > 0) {
|
||||||
|
positions.push({
|
||||||
private observeScrollContainer(selector: string): void {
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
mutations.forEach((mutation) => {
|
|
||||||
if (mutation.type === 'childList') {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
const element = node as Element
|
|
||||||
if (element.matches(selector)) {
|
|
||||||
this.addScrollListener(element, selector)
|
|
||||||
}
|
|
||||||
// 也检查子元素
|
|
||||||
element.querySelectorAll(selector).forEach((child) => {
|
|
||||||
this.addScrollListener(child, selector)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
observer.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
})
|
|
||||||
|
|
||||||
this.scrollObservers.set(selector, observer)
|
|
||||||
|
|
||||||
// 立即处理已存在的元素
|
|
||||||
document.querySelectorAll(selector).forEach((element) => {
|
|
||||||
this.addScrollListener(element, selector)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private addScrollListener(element: Element, selector: string): void {
|
|
||||||
if (element.getAttribute('data-scroll-tracked')) return
|
|
||||||
|
|
||||||
element.setAttribute('data-scroll-tracked', 'true')
|
|
||||||
element.addEventListener('scroll', () => {
|
|
||||||
this.updateScrollPosition(element, selector)
|
|
||||||
}, { passive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateScrollPosition(element: Element, selector: string): void {
|
|
||||||
const scrollPos: ScrollPosition = {
|
|
||||||
x: element.scrollLeft,
|
x: element.scrollLeft,
|
||||||
y: element.scrollTop,
|
y: element.scrollTop,
|
||||||
element: selector
|
element: `${selector}:nth-of-type(${index + 1})`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
this.scrollPositions.set(selector, scrollPos)
|
})
|
||||||
this.debounceScrollSave()
|
|
||||||
}
|
|
||||||
|
|
||||||
private debounceScrollSave(): void {
|
|
||||||
if (this.debounceTimer) {
|
|
||||||
clearTimeout(this.debounceTimer)
|
|
||||||
}
|
|
||||||
this.debounceTimer = window.setTimeout(() => {
|
|
||||||
// 主窗口滚动
|
|
||||||
this.scrollPositions.set('window', {
|
|
||||||
x: window.scrollX,
|
|
||||||
y: window.scrollY,
|
|
||||||
element: 'window'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
this.saveCallback(Array.from(this.scrollPositions.values()))
|
return positions
|
||||||
}, 100)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreScrollPositions(positions: ScrollPosition[]): void {
|
/**
|
||||||
|
* 收集当前页面的弹窗状态
|
||||||
|
*/
|
||||||
|
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)) {
|
||||||
|
const state: ModalState = {
|
||||||
|
id: this.getModalId(element),
|
||||||
|
isOpen: true,
|
||||||
|
data: this.extractModalData(element)
|
||||||
|
}
|
||||||
|
states.push(state)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const fieldState: FormFieldState = {
|
||||||
|
selector: this.getFieldSelector(inputElement),
|
||||||
|
value: inputElement.value,
|
||||||
|
type: inputElement.type,
|
||||||
|
checked: inputElement.checked,
|
||||||
|
selectedIndex: inputElement.tagName === 'SELECT' ?
|
||||||
|
(inputElement as unknown as HTMLSelectElement).selectedIndex : undefined
|
||||||
|
}
|
||||||
|
fields.push(fieldState)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复滚动位置
|
||||||
|
*/
|
||||||
|
static restoreScrollPositions(positions: ScrollPosition[]): void {
|
||||||
positions.forEach(pos => {
|
positions.forEach(pos => {
|
||||||
if (pos.element === 'window') {
|
if (pos.element === 'window') {
|
||||||
window.scrollTo({ top: pos.y, left: pos.x, behavior: 'auto' })
|
window.scrollTo({ top: pos.y, left: pos.x, behavior: 'auto' })
|
||||||
@@ -167,232 +182,10 @@ export class EnhancedScrollManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
/**
|
||||||
if (this.debounceTimer) {
|
* 恢复弹窗状态
|
||||||
clearTimeout(this.debounceTimer)
|
|
||||||
}
|
|
||||||
this.scrollObservers.forEach(observer => observer.disconnect())
|
|
||||||
this.scrollObservers.clear()
|
|
||||||
this.scrollPositions.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 弹窗状态管理器
|
|
||||||
*/
|
*/
|
||||||
export class ModalStateManager {
|
static restoreModalStates(states: ModalState[]): void {
|
||||||
private modalStates = new Map<string, ModalState>()
|
|
||||||
private mutationObserver: MutationObserver | null = null
|
|
||||||
private saveCallback: (states: ModalState[]) => void
|
|
||||||
|
|
||||||
constructor(saveCallback: (states: ModalState[]) => void) {
|
|
||||||
this.saveCallback = saveCallback
|
|
||||||
this.initModalTracking()
|
|
||||||
}
|
|
||||||
|
|
||||||
private initModalTracking(): void {
|
|
||||||
// 监听DOM变化来检测弹窗的打开和关闭
|
|
||||||
this.mutationObserver = new MutationObserver((mutations) => {
|
|
||||||
let hasModalChanges = false
|
|
||||||
|
|
||||||
mutations.forEach((mutation) => {
|
|
||||||
if (mutation.type === 'childList') {
|
|
||||||
// 检查新添加的弹窗
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
const element = node as Element
|
|
||||||
if (this.isModalElement(element)) {
|
|
||||||
this.trackModal(element)
|
|
||||||
hasModalChanges = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (mutation.type === 'attributes') {
|
|
||||||
const element = mutation.target as Element
|
|
||||||
if (this.isModalElement(element)) {
|
|
||||||
this.updateModalState(element)
|
|
||||||
hasModalChanges = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (hasModalChanges) {
|
|
||||||
this.saveStates()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.mutationObserver.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['class', 'style', 'aria-hidden', 'data-*']
|
|
||||||
})
|
|
||||||
|
|
||||||
// 立即扫描已存在的弹窗
|
|
||||||
this.scanExistingModals()
|
|
||||||
}
|
|
||||||
|
|
||||||
private isModalElement(element: Element): boolean {
|
|
||||||
const modalSelectors = [
|
|
||||||
'.v-dialog',
|
|
||||||
'.v-menu',
|
|
||||||
'.v-overlay',
|
|
||||||
'.v-tooltip',
|
|
||||||
'.v-snackbar',
|
|
||||||
'.modal',
|
|
||||||
'.popup',
|
|
||||||
'.drawer',
|
|
||||||
'.v-navigation-drawer',
|
|
||||||
'[role="dialog"]',
|
|
||||||
'[role="alertdialog"]',
|
|
||||||
'[role="tooltip"]'
|
|
||||||
]
|
|
||||||
|
|
||||||
return modalSelectors.some(selector =>
|
|
||||||
element.matches(selector) || element.querySelector(selector)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private trackModal(element: Element): void {
|
|
||||||
const id = this.getModalId(element)
|
|
||||||
const isOpen = this.isModalOpen(element)
|
|
||||||
|
|
||||||
if (isOpen) {
|
|
||||||
const state: ModalState = {
|
|
||||||
id,
|
|
||||||
isOpen: true,
|
|
||||||
data: this.extractModalData(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.modalStates.set(id, state)
|
|
||||||
} else {
|
|
||||||
this.modalStates.delete(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateModalState(element: Element): void {
|
|
||||||
const id = this.getModalId(element)
|
|
||||||
const isOpen = this.isModalOpen(element)
|
|
||||||
|
|
||||||
if (isOpen) {
|
|
||||||
const state: ModalState = {
|
|
||||||
id,
|
|
||||||
isOpen: true,
|
|
||||||
data: this.extractModalData(element)
|
|
||||||
}
|
|
||||||
this.modalStates.set(id, state)
|
|
||||||
} else {
|
|
||||||
this.modalStates.delete(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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 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' &&
|
|
||||||
!element.classList.contains('v-overlay--active') === false
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractModalData(element: Element): any {
|
|
||||||
const data: any = {}
|
|
||||||
|
|
||||||
// 提取表单数据
|
|
||||||
const form = element.querySelector('form')
|
|
||||||
if (form) {
|
|
||||||
data.formData = this.extractFormData(form)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取输入字段
|
|
||||||
const inputs = element.querySelectorAll('input, select, textarea')
|
|
||||||
if (inputs.length > 0) {
|
|
||||||
data.inputData = this.extractInputData(inputs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取滚动位置
|
|
||||||
const scrollableElements = element.querySelectorAll('[class*="overflow"], .v-card-text')
|
|
||||||
if (scrollableElements.length > 0) {
|
|
||||||
data.scrollPositions = Array.from(scrollableElements).map(el => ({
|
|
||||||
selector: this.getElementSelector(el),
|
|
||||||
x: el.scrollLeft,
|
|
||||||
y: el.scrollTop
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractFormData(form: Element): Record<string, any> {
|
|
||||||
const formData: Record<string, any> = {}
|
|
||||||
const inputs = form.querySelectorAll('input, select, textarea')
|
|
||||||
|
|
||||||
inputs.forEach(input => {
|
|
||||||
const element = input as HTMLInputElement
|
|
||||||
if (element.name) {
|
|
||||||
formData[element.name] = element.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return formData
|
|
||||||
}
|
|
||||||
|
|
||||||
private extractInputData(inputs: NodeListOf<Element>): Record<string, any> {
|
|
||||||
const inputData: Record<string, any> = {}
|
|
||||||
|
|
||||||
inputs.forEach(input => {
|
|
||||||
const element = input as HTMLInputElement
|
|
||||||
const key = element.name || element.id || this.getElementSelector(element)
|
|
||||||
inputData[key] = element.value
|
|
||||||
})
|
|
||||||
|
|
||||||
return inputData
|
|
||||||
}
|
|
||||||
|
|
||||||
private getElementSelector(element: Element): string {
|
|
||||||
if (element.id) return `#${element.id}`
|
|
||||||
if (element.className) return `.${element.className.split(' ')[0]}`
|
|
||||||
return element.tagName.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
private scanExistingModals(): void {
|
|
||||||
const modalSelectors = [
|
|
||||||
'.v-dialog',
|
|
||||||
'.v-menu',
|
|
||||||
'.v-overlay',
|
|
||||||
'.modal',
|
|
||||||
'.popup',
|
|
||||||
'[role="dialog"]'
|
|
||||||
]
|
|
||||||
|
|
||||||
modalSelectors.forEach(selector => {
|
|
||||||
document.querySelectorAll(selector).forEach(element => {
|
|
||||||
if (this.isModalOpen(element)) {
|
|
||||||
this.trackModal(element)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.saveStates()
|
|
||||||
}
|
|
||||||
|
|
||||||
private saveStates(): void {
|
|
||||||
this.saveCallback(Array.from(this.modalStates.values()))
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreModalStates(states: ModalState[]): void {
|
|
||||||
// 此方法需要与应用的具体弹窗实现配合
|
|
||||||
// 可以通过事件系统通知应用恢复弹窗状态
|
|
||||||
states.forEach(state => {
|
states.forEach(state => {
|
||||||
const event = new CustomEvent('restoreModalState', {
|
const event = new CustomEvent('restoreModalState', {
|
||||||
detail: state
|
detail: state
|
||||||
@@ -401,154 +194,10 @@ export class ModalStateManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
/**
|
||||||
if (this.mutationObserver) {
|
* 恢复表单字段
|
||||||
this.mutationObserver.disconnect()
|
|
||||||
}
|
|
||||||
this.modalStates.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 实时表单数据管理器
|
|
||||||
*/
|
*/
|
||||||
export class RealTimeFormManager {
|
static restoreFormFields(fields: FormFieldState[]): void {
|
||||||
private formFields = new Map<string, FormFieldState>()
|
|
||||||
private debounceTimer: number | null = null
|
|
||||||
private saveCallback: (fields: FormFieldState[]) => void
|
|
||||||
private observers = new Set<MutationObserver>()
|
|
||||||
|
|
||||||
constructor(saveCallback: (fields: FormFieldState[]) => void) {
|
|
||||||
this.saveCallback = saveCallback
|
|
||||||
this.initFormTracking()
|
|
||||||
}
|
|
||||||
|
|
||||||
private initFormTracking(): void {
|
|
||||||
// 监听表单输入事件
|
|
||||||
const inputEvents = ['input', 'change', 'blur', 'focus']
|
|
||||||
inputEvents.forEach(eventType => {
|
|
||||||
document.addEventListener(eventType, this.handleFormInput.bind(this), true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听DOM变化,跟踪新添加的表单元素
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
mutations.forEach((mutation) => {
|
|
||||||
if (mutation.type === 'childList') {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
const element = node as Element
|
|
||||||
this.trackFormElements(element)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
observer.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
})
|
|
||||||
|
|
||||||
this.observers.add(observer)
|
|
||||||
|
|
||||||
// 立即扫描已存在的表单元素
|
|
||||||
this.scanExistingForms()
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleFormInput(event: Event): void {
|
|
||||||
const target = event.target as HTMLInputElement
|
|
||||||
if (this.isFormElement(target)) {
|
|
||||||
this.updateFormField(target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isFormElement(element: Element): boolean {
|
|
||||||
const formTags = ['INPUT', 'TEXTAREA', 'SELECT']
|
|
||||||
return formTags.includes(element.tagName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateFormField(element: HTMLInputElement): void {
|
|
||||||
const selector = this.getFieldSelector(element)
|
|
||||||
const fieldState: FormFieldState = {
|
|
||||||
selector,
|
|
||||||
value: element.value,
|
|
||||||
type: element.type,
|
|
||||||
checked: element.checked,
|
|
||||||
selectedIndex: element.tagName === 'SELECT' ? (element as unknown as HTMLSelectElement).selectedIndex : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
this.formFields.set(selector, fieldState)
|
|
||||||
this.debounceSave()
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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]}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const siblings = Array.from(current.parentNode?.children || [])
|
|
||||||
.filter(sibling => sibling.tagName === current.tagName)
|
|
||||||
|
|
||||||
if (siblings.length > 1) {
|
|
||||||
const index = siblings.indexOf(current) + 1
|
|
||||||
selector += `:nth-child(${index})`
|
|
||||||
}
|
|
||||||
|
|
||||||
path.unshift(selector)
|
|
||||||
current = current.parentNode as Element
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.join(' > ')
|
|
||||||
}
|
|
||||||
|
|
||||||
private trackFormElements(element: Element): void {
|
|
||||||
const formElements = element.querySelectorAll('input, textarea, select')
|
|
||||||
formElements.forEach(formElement => {
|
|
||||||
this.updateFormField(formElement as HTMLInputElement)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 如果元素本身是表单元素
|
|
||||||
if (this.isFormElement(element)) {
|
|
||||||
this.updateFormField(element as HTMLInputElement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private scanExistingForms(): void {
|
|
||||||
const formElements = document.querySelectorAll('input, textarea, select')
|
|
||||||
formElements.forEach(element => {
|
|
||||||
this.updateFormField(element as HTMLInputElement)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private debounceSave(): void {
|
|
||||||
if (this.debounceTimer) {
|
|
||||||
clearTimeout(this.debounceTimer)
|
|
||||||
}
|
|
||||||
this.debounceTimer = window.setTimeout(() => {
|
|
||||||
this.saveCallback(Array.from(this.formFields.values()))
|
|
||||||
}, 500) // 500ms防抖
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreFormFields(fields: FormFieldState[]): void {
|
|
||||||
fields.forEach(field => {
|
fields.forEach(field => {
|
||||||
const elements = document.querySelectorAll(field.selector)
|
const elements = document.querySelectorAll(field.selector)
|
||||||
elements.forEach(element => {
|
elements.forEach(element => {
|
||||||
@@ -572,13 +221,84 @@ export class RealTimeFormManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
// 辅助方法
|
||||||
if (this.debounceTimer) {
|
private static isModalOpen(element: Element): boolean {
|
||||||
clearTimeout(this.debounceTimer)
|
const computedStyle = window.getComputedStyle(element)
|
||||||
|
|
||||||
|
return computedStyle.display !== 'none' &&
|
||||||
|
computedStyle.visibility !== 'hidden' &&
|
||||||
|
computedStyle.opacity !== '0' &&
|
||||||
|
!element.hasAttribute('hidden') &&
|
||||||
|
element.getAttribute('aria-hidden') !== 'true'
|
||||||
}
|
}
|
||||||
this.observers.forEach(observer => observer.disconnect())
|
|
||||||
this.observers.clear()
|
private static getModalId(element: Element): string {
|
||||||
this.formFields.clear()
|
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, index) => ({
|
||||||
|
selector: `${element.className}:nth-of-type(${index + 1}) [class*="overflow"]`,
|
||||||
|
x: el.scrollLeft,
|
||||||
|
y: el.scrollTop
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
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(' > ')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1010,7 +730,7 @@ export class VisibilityStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 完整的PWA状态管理器 - 增强版本
|
* 完整的PWA状态管理器 - 轻量级版本
|
||||||
*/
|
*/
|
||||||
export class PWAStateController {
|
export class PWAStateController {
|
||||||
private stateManager: PWAStateManager
|
private stateManager: PWAStateManager
|
||||||
@@ -1018,9 +738,6 @@ export class PWAStateController {
|
|||||||
private swStateSync: ServiceWorkerStateSync
|
private swStateSync: ServiceWorkerStateSync
|
||||||
private visibilityManager: VisibilityStateManager
|
private visibilityManager: VisibilityStateManager
|
||||||
private restoreDecision: StateRestoreDecision
|
private restoreDecision: StateRestoreDecision
|
||||||
private enhancedScrollManager: EnhancedScrollManager
|
|
||||||
private modalStateManager: ModalStateManager
|
|
||||||
private realTimeFormManager: RealTimeFormManager
|
|
||||||
private stateRestorePromise: Promise<void> | null = null
|
private stateRestorePromise: Promise<void> | null = null
|
||||||
private stateRestoreResolve: (() => void) | null = null
|
private stateRestoreResolve: (() => void) | null = null
|
||||||
private isRestoring = false
|
private isRestoring = false
|
||||||
@@ -1032,19 +749,6 @@ export class PWAStateController {
|
|||||||
this.visibilityManager = new VisibilityStateManager(this.stateManager)
|
this.visibilityManager = new VisibilityStateManager(this.stateManager)
|
||||||
this.restoreDecision = new StateRestoreDecision()
|
this.restoreDecision = new StateRestoreDecision()
|
||||||
|
|
||||||
// 初始化增强管理器
|
|
||||||
this.enhancedScrollManager = new EnhancedScrollManager((positions) => {
|
|
||||||
this.saveScrollPositions(positions)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.modalStateManager = new ModalStateManager((states) => {
|
|
||||||
this.saveModalStates(states)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.realTimeFormManager = new RealTimeFormManager((fields) => {
|
|
||||||
this.saveFormFields(fields)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 创建状态恢复Promise
|
// 创建状态恢复Promise
|
||||||
this.stateRestorePromise = new Promise((resolve) => {
|
this.stateRestorePromise = new Promise((resolve) => {
|
||||||
this.stateRestoreResolve = resolve
|
this.stateRestoreResolve = resolve
|
||||||
@@ -1118,10 +822,10 @@ export class PWAStateController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveCurrentState(): Promise<void> {
|
async saveCurrentState(): Promise<void> {
|
||||||
// 从sessionStorage获取实时状态
|
// 使用轻量级收集器收集当前状态
|
||||||
const scrollPositions = this.getScrollPositionsFromStorage()
|
const scrollPositions = LightweightStateCollector.collectScrollPositions()
|
||||||
const modalStates = this.getModalStatesFromStorage()
|
const modalStates = LightweightStateCollector.collectModalStates()
|
||||||
const formFields = this.getFormFieldsFromStorage()
|
const formFields = LightweightStateCollector.collectFormFields()
|
||||||
|
|
||||||
const state: PWAState = {
|
const state: PWAState = {
|
||||||
url: window.location.href,
|
url: window.location.href,
|
||||||
@@ -1147,9 +851,9 @@ export class PWAStateController {
|
|||||||
const currentUrl = window.location.href
|
const currentUrl = window.location.href
|
||||||
const urlMatches = this.isUrlExactMatch(state.url, currentUrl)
|
const urlMatches = this.isUrlExactMatch(state.url, currentUrl)
|
||||||
|
|
||||||
// 使用增强滚动管理器恢复滚动位置
|
// 恢复滚动位置
|
||||||
if (state.scrollPositions && urlMatches) {
|
if (state.scrollPositions && urlMatches) {
|
||||||
this.enhancedScrollManager.restoreScrollPositions(state.scrollPositions)
|
LightweightStateCollector.restoreScrollPositions(state.scrollPositions)
|
||||||
} else if (state.scrollPosition && urlMatches) {
|
} else if (state.scrollPosition && urlMatches) {
|
||||||
// 向后兼容:如果没有新的滚动位置数据,使用旧的方式
|
// 向后兼容:如果没有新的滚动位置数据,使用旧的方式
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
@@ -1160,12 +864,12 @@ export class PWAStateController {
|
|||||||
|
|
||||||
// 恢复弹窗状态
|
// 恢复弹窗状态
|
||||||
if (state.modalStates) {
|
if (state.modalStates) {
|
||||||
this.modalStateManager.restoreModalStates(state.modalStates)
|
LightweightStateCollector.restoreModalStates(state.modalStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复表单字段
|
// 恢复表单字段
|
||||||
if (state.formFields && urlMatches) {
|
if (state.formFields && urlMatches) {
|
||||||
this.realTimeFormManager.restoreFormFields(state.formFields)
|
LightweightStateCollector.restoreFormFields(state.formFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复应用特定状态 - 过滤掉不适用的状态
|
// 恢复应用特定状态 - 过滤掉不适用的状态
|
||||||
@@ -1173,9 +877,6 @@ export class PWAStateController {
|
|||||||
this.restoreAppSpecificState(state.appData, urlMatches)
|
this.restoreAppSpecificState(state.appData, urlMatches)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从sessionStorage恢复额外的状态
|
|
||||||
this.restoreFromSessionStorage(urlMatches)
|
|
||||||
|
|
||||||
// 触发状态恢复事件
|
// 触发状态恢复事件
|
||||||
this.dispatchStateRestoreEvent(state)
|
this.dispatchStateRestoreEvent(state)
|
||||||
}
|
}
|
||||||
@@ -1191,22 +892,8 @@ export class PWAStateController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupPeriodicSave(): void {
|
private setupPeriodicSave(): void {
|
||||||
// 导入后台管理器
|
// 轻量级版本不需要定时保存
|
||||||
import('@/utils/backgroundManager').then(({ addBackgroundTimer }) => {
|
// 只在页面隐藏时保存状态
|
||||||
// 使用后台管理器,延长间隔
|
|
||||||
addBackgroundTimer(
|
|
||||||
'pwa-state-save',
|
|
||||||
() => {
|
|
||||||
// 只在前台时保存状态(由后台管理器自动处理)
|
|
||||||
this.saveCurrentState()
|
|
||||||
},
|
|
||||||
60000, // 改为60秒,减少频率
|
|
||||||
{
|
|
||||||
runInBackground: false, // 后台时不保存
|
|
||||||
skipInitialRun: true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAppSpecificState(): any {
|
private getAppSpecificState(): any {
|
||||||
@@ -1308,118 +995,10 @@ export class PWAStateController {
|
|||||||
window.dispatchEvent(event)
|
window.dispatchEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存滚动位置(被增强滚动管理器调用)
|
|
||||||
*/
|
|
||||||
private saveScrollPositions(positions: ScrollPosition[]): void {
|
|
||||||
// 实时保存滚动位置到sessionStorage
|
|
||||||
try {
|
|
||||||
sessionStorage.setItem('mp-scroll-positions', JSON.stringify(positions))
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存滚动位置失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存弹窗状态(被弹窗状态管理器调用)
|
|
||||||
*/
|
|
||||||
private saveModalStates(states: ModalState[]): void {
|
|
||||||
// 实时保存弹窗状态到sessionStorage
|
|
||||||
try {
|
|
||||||
sessionStorage.setItem('mp-modal-states', JSON.stringify(states))
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存弹窗状态失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存表单字段(被实时表单管理器调用)
|
|
||||||
*/
|
|
||||||
private saveFormFields(fields: FormFieldState[]): void {
|
|
||||||
// 实时保存表单字段到sessionStorage
|
|
||||||
try {
|
|
||||||
sessionStorage.setItem('mp-form-fields', JSON.stringify(fields))
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存表单字段失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从sessionStorage恢复额外的状态
|
|
||||||
*/
|
|
||||||
private restoreFromSessionStorage(urlMatches: boolean): void {
|
|
||||||
try {
|
|
||||||
// 恢复滚动位置
|
|
||||||
if (urlMatches) {
|
|
||||||
const scrollPositions = sessionStorage.getItem('mp-scroll-positions')
|
|
||||||
if (scrollPositions) {
|
|
||||||
const positions: ScrollPosition[] = JSON.parse(scrollPositions)
|
|
||||||
this.enhancedScrollManager.restoreScrollPositions(positions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 恢复弹窗状态
|
|
||||||
const modalStates = sessionStorage.getItem('mp-modal-states')
|
|
||||||
if (modalStates) {
|
|
||||||
const states: ModalState[] = JSON.parse(modalStates)
|
|
||||||
this.modalStateManager.restoreModalStates(states)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 恢复表单字段
|
|
||||||
if (urlMatches) {
|
|
||||||
const formFields = sessionStorage.getItem('mp-form-fields')
|
|
||||||
if (formFields) {
|
|
||||||
const fields: FormFieldState[] = JSON.parse(formFields)
|
|
||||||
this.realTimeFormManager.restoreFormFields(fields)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('从sessionStorage恢复状态失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从sessionStorage获取滚动位置
|
|
||||||
*/
|
|
||||||
private getScrollPositionsFromStorage(): ScrollPosition[] {
|
|
||||||
try {
|
|
||||||
const stored = sessionStorage.getItem('mp-scroll-positions')
|
|
||||||
return stored ? JSON.parse(stored) : []
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从sessionStorage获取弹窗状态
|
|
||||||
*/
|
|
||||||
private getModalStatesFromStorage(): ModalState[] {
|
|
||||||
try {
|
|
||||||
const stored = sessionStorage.getItem('mp-modal-states')
|
|
||||||
return stored ? JSON.parse(stored) : []
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从sessionStorage获取表单字段
|
|
||||||
*/
|
|
||||||
private getFormFieldsFromStorage(): FormFieldState[] {
|
|
||||||
try {
|
|
||||||
const stored = sessionStorage.getItem('mp-form-fields')
|
|
||||||
return stored ? JSON.parse(stored) : []
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 销毁管理器并清理资源
|
* 销毁管理器并清理资源
|
||||||
*/
|
*/
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
this.enhancedScrollManager.destroy()
|
// 轻量级版本无需清理资源
|
||||||
this.modalStateManager.destroy()
|
|
||||||
this.realTimeFormManager.destroy()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user