| @ -0,0 +1,420 @@ | |||
| // import MemoryStorage from './storage/MemoryStorage.js'; | |||
| import LocalStorage from './storage/LocalStorage.js'; | |||
| // import SessionStorage from './storage/SessionStorage.js'; | |||
| import IndexedDBStorage from './storage/IndexedDBStorage.js'; | |||
| import ServerStorage from './storage/ServerStorage.js'; | |||
| /** | |||
| * 自动保存管理器 | |||
| * 协调各种存储方式,处理冲突合并 | |||
| */ | |||
| class AutoSaveManager { | |||
| constructor(key, serverOptions = {}) { | |||
| this.key = key; | |||
| try { | |||
| this.storages = { | |||
| // memory: new MemoryStorage(key), | |||
| localStorage: new LocalStorage(key), | |||
| // sessionStorage: new SessionStorage(key), | |||
| indexedDB: new IndexedDBStorage(key), | |||
| server: new ServerStorage(key, serverOptions) | |||
| }; | |||
| } catch (error) { | |||
| console.error('初始化存储失败:', error); | |||
| // 如果某个存储初始化失败,只使用可用的存储 | |||
| this.storages = { | |||
| // memory: new MemoryStorage(key), | |||
| localStorage: new LocalStorage(key), | |||
| // sessionStorage: new SessionStorage(key) | |||
| }; | |||
| } | |||
| this.states = {}; | |||
| this.initStates(); | |||
| this.isSaving = false; | |||
| this.saveTimer = null; | |||
| this.debounceDelay = 1000;// 防抖延迟 | |||
| this.currentContent = ''; | |||
| this.lastSavedContent = ''; | |||
| // 异步初始化 | |||
| this.initPromise = this.init(); | |||
| } | |||
| /** | |||
| * 初始化状态对象 | |||
| */ | |||
| initStates() { | |||
| for (const type in this.storages) { | |||
| this.states[type] = { | |||
| status: 'idle', | |||
| lastSaved: null, | |||
| error: null, | |||
| initialized: false | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 等待初始化完成 | |||
| */ | |||
| async waitForInit() { | |||
| await this.initPromise; | |||
| } | |||
| /** | |||
| * 初始化管理器 | |||
| */ | |||
| async init() { | |||
| try { | |||
| // 加载所有存储的数据 | |||
| await this.loadFromAllStorages(); | |||
| // 设置页面关闭前保存 | |||
| this.setupBeforeUnload(); | |||
| // 设置页面崩溃检测 | |||
| this.setupCrashDetection(); | |||
| console.log(`AutoSaveManager 初始化完成,key: ${this.key}`); | |||
| } catch (error) { | |||
| console.error('AutoSaveManager 初始化失败:', error); | |||
| } | |||
| } | |||
| /** | |||
| * 从所有存储加载数据并合并 | |||
| */ | |||
| async loadFromAllStorages() { | |||
| const results = {}; | |||
| // 并行加载所有存储 | |||
| const promises = Object.entries(this.storages).map(async ([type, storage]) => { | |||
| try { | |||
| // 确保存储已初始化 | |||
| if (storage.waitForInit) { | |||
| await storage.waitForInit(); | |||
| } | |||
| const result = await storage.load(); | |||
| results[type] = result; | |||
| this.updateState(type, 'loaded', { | |||
| ...result, | |||
| initialized: true | |||
| }); | |||
| return result; | |||
| } catch (error) { | |||
| console.error(`加载${type}存储失败:`, error); | |||
| this.updateState(type, 'error', { | |||
| error: error.message, | |||
| initialized: false | |||
| }); | |||
| return null; | |||
| } | |||
| }); | |||
| await Promise.allSettled(promises); | |||
| // 选择最新版本的内容 | |||
| // this.currentContent = this.resolveConflicts(results); | |||
| this.lastSavedContent = this.currentContent; | |||
| return this.currentContent; | |||
| } | |||
| /** | |||
| * 解决冲突:选择最新版本的内容 | |||
| * @param {object} results - 所有存储的加载结果 | |||
| * @returns {string} 合并后的内容 | |||
| */ | |||
| resolveConflicts(results) { | |||
| let latestContent = ''; | |||
| let latestTimestamp = 0; | |||
| Object.entries(results).forEach(([type, result]) => { | |||
| if (result && result.success && result.data) { | |||
| let data = result.data; | |||
| let timestamp = 0; | |||
| let content = ''; | |||
| if (typeof data === 'object' && data !== null) { | |||
| timestamp = data.timestamp || data.lastModified || 0; | |||
| content = data.content || ''; | |||
| } else if (typeof data === 'string') { | |||
| content = data; | |||
| timestamp = Date.now(); // 字符串数据给一个默认时间戳 | |||
| } | |||
| if (timestamp > latestTimestamp) { | |||
| latestTimestamp = timestamp; | |||
| latestContent = content; | |||
| } | |||
| } | |||
| }); | |||
| return latestContent || ''; | |||
| } | |||
| /** | |||
| * 保存内容(防抖处理) | |||
| * @param {string} content - 要保存的内容 | |||
| */ | |||
| save(content) { | |||
| this.currentContent = content; | |||
| // 如果内容没有变化,不保存 | |||
| if (content === this.lastSavedContent) { | |||
| return; | |||
| } | |||
| // 清除之前的定时器 | |||
| if (this.saveTimer) { | |||
| clearTimeout(this.saveTimer); | |||
| } | |||
| // 设置新的定时器 | |||
| this.saveTimer = setTimeout(() => { | |||
| this.doSave(content); | |||
| }, this.debounceDelay); | |||
| } | |||
| /** | |||
| * 执行保存操作 | |||
| * @param {string} content - 要保存的内容 | |||
| */ | |||
| async doSave(content) { | |||
| if (this.isSaving) { | |||
| return; | |||
| } | |||
| this.isSaving = true; | |||
| this.lastSavedContent = content; | |||
| try { | |||
| // 并行保存到所有存储 | |||
| const promises = Object.entries(this.storages).map(async ([type, storage]) => { | |||
| try { | |||
| // 检查存储是否可用 | |||
| if (!storage || this.states[type]?.error) { | |||
| this.updateState(type, 'error', { | |||
| error: '存储不可用', | |||
| retry: true | |||
| }); | |||
| return null; | |||
| } | |||
| this.updateState(type, 'saving'); | |||
| const result = await storage.save(content); | |||
| if (result && result.success) { | |||
| this.updateState(type, 'saved', result); | |||
| } else { | |||
| this.updateState(type, 'error', { | |||
| error: result?.error || '保存失败', | |||
| retry: true | |||
| }); | |||
| } | |||
| return result; | |||
| } catch (error) { | |||
| console.error(`保存到${type}失败:`, error); | |||
| this.updateState(type, 'error', { | |||
| error: error.message, | |||
| retry: true | |||
| }); | |||
| return null; | |||
| } | |||
| }); | |||
| await Promise.allSettled(promises); | |||
| } finally { | |||
| this.isSaving = false; | |||
| } | |||
| } | |||
| /** | |||
| * 更新存储状态 | |||
| * @param {string} type - 存储类型 | |||
| * @param {string} status - 状态 | |||
| * @param {object} data - 相关数据 | |||
| */ | |||
| updateState(type, status, data = {}) { | |||
| this.states[type] = { | |||
| ...this.states[type], | |||
| status, | |||
| ...data, | |||
| lastUpdated: Date.now() | |||
| }; | |||
| if (status === 'saved' && data.timestamp) { | |||
| this.states[type].lastSaved = data.timestamp; | |||
| } | |||
| } | |||
| /** | |||
| * 获取所有存储状态 | |||
| * @returns {object} 状态对象 | |||
| */ | |||
| getAllStates() { | |||
| return { ...this.states }; | |||
| } | |||
| /** | |||
| * 获取指定存储的历史版本 | |||
| * @param {string} storageType - 存储类型 | |||
| * @returns {Promise<Array>} 历史版本列表 | |||
| */ | |||
| async getHistory(storageType) { | |||
| if (this.storages[storageType]) { | |||
| return await this.storages[storageType].getHistory(); | |||
| } | |||
| return { success: false, error: '存储类型不存在' }; | |||
| } | |||
| /** | |||
| * 从历史版本恢复 | |||
| * @param {string} storageType - 存储类型 | |||
| * @param {object} versionData - 版本数据 | |||
| */ | |||
| async restoreFromHistory(storageType, versionData) { | |||
| if (this.storages[storageType]) { | |||
| const content = versionData.content || versionData; | |||
| await this.doSave(content); | |||
| this.currentContent = content; | |||
| return { success: true, content }; | |||
| } | |||
| return { success: false, error: '存储类型不存在' }; | |||
| } | |||
| /** | |||
| * 设置页面关闭前保存 | |||
| */ | |||
| setupBeforeUnload() { | |||
| window.addEventListener('beforeunload', (event) => { | |||
| if (this.currentContent !== this.lastSavedContent) { | |||
| // 同步保存(不防抖) | |||
| this.doSaveSync(this.currentContent); | |||
| } | |||
| }); | |||
| } | |||
| /** | |||
| * 同步保存(用于页面关闭前) | |||
| * @param {string} content - 要保存的内容 | |||
| */ | |||
| async doSaveSync(content) { | |||
| // 只保存到本地存储,确保数据不丢失 | |||
| const localStorages = ['memory', 'localStorage', 'sessionStorage']; | |||
| localStorages.forEach(async (type) => { | |||
| try { | |||
| await this.storages[type].save(content); | |||
| } catch (error) { | |||
| console.error(`页面关闭前保存到${type}失败:`, error); | |||
| } | |||
| }); | |||
| } | |||
| /** | |||
| * 设置页面崩溃检测 | |||
| */ | |||
| setupCrashDetection() { | |||
| // 使用 Page Visibility API 检测页面状态 | |||
| document.addEventListener('visibilitychange', () => { | |||
| if (document.visibilityState === 'visible') { | |||
| // 页面恢复显示,检查是否需要同步 | |||
| this.checkAndSync(); | |||
| } | |||
| }); | |||
| // 监听页面冻结和恢复 | |||
| document.addEventListener('freeze', () => { | |||
| localStorage.setItem(`pageFrozen_${this.key}`, Date.now()); | |||
| }); | |||
| document.addEventListener('resume', () => { | |||
| const frozenTime = localStorage.getItem(`pageFrozen_${this.key}`); | |||
| if (frozenTime && Date.now() - frozenTime > 5000) { | |||
| // 页面被冻结超过5秒,可能是崩溃恢复 | |||
| this.showCrashNotification(); | |||
| } | |||
| localStorage.removeItem(`pageFrozen_${this.key}`); | |||
| }); | |||
| } | |||
| /** | |||
| * 检查并同步数据 | |||
| */ | |||
| async checkAndSync() { | |||
| // 重新加载所有数据并合并 | |||
| await this.loadFromAllStorages(); | |||
| // 如果有服务器存储,尝试同步离线队列 | |||
| if (this.storages.server) { | |||
| this.storages.server.processQueue(); | |||
| } | |||
| } | |||
| /** | |||
| * 显示崩溃通知 | |||
| */ | |||
| showCrashNotification() { | |||
| const notification = document.createElement('div'); | |||
| notification.style.cssText = ` | |||
| position: fixed; | |||
| top: 20px; | |||
| right: 20px; | |||
| background: #ff4444; | |||
| color: white; | |||
| padding: 15px; | |||
| border-radius: 5px; | |||
| z-index: 10000; | |||
| box-shadow: 0 2px 10px rgba(0,0,0,0.2); | |||
| `; | |||
| notification.innerHTML = ` | |||
| <strong>页面恢复</strong><br> | |||
| 检测到页面可能发生了异常,已自动恢复您的数据。 | |||
| `; | |||
| document.body.appendChild(notification); | |||
| setTimeout(() => { | |||
| notification.remove(); | |||
| }, 5000); | |||
| } | |||
| /** | |||
| * 获取备份文件列表 | |||
| * @returns {Promise<Array>} 备份文件列表 | |||
| */ | |||
| async getBackupFiles() { | |||
| const backupPromises = Object.entries(this.storages).map(async ([type, storage]) => { | |||
| if (typeof storage.getBackupFiles === 'function') { | |||
| try { | |||
| const result = await storage.getBackupFiles(); | |||
| return { type, ...result }; | |||
| } catch (error) { | |||
| return { type, success: false, error: error.message }; | |||
| } | |||
| } | |||
| return { type, success: false, error: '不支持备份管理' }; | |||
| }); | |||
| const results = await Promise.all(backupPromises); | |||
| return results; | |||
| } | |||
| /** | |||
| * 立即保存当前内容 | |||
| */ | |||
| async saveImmediately(currentContent) { | |||
| if (this.saveTimer) { | |||
| clearTimeout(this.saveTimer); | |||
| } | |||
| await this.doSave(currentContent); | |||
| } | |||
| } | |||
| export default AutoSaveManager; | |||
| @ -0,0 +1,703 @@ | |||
| /** | |||
| * IndexedDB存储策略 - 修复复合索引问题 | |||
| */ | |||
| class IndexedDBStorage { | |||
| constructor(key) { | |||
| this.key = key; | |||
| this.dbName = 'hxhq'; | |||
| this.storeName = 'textContents'; | |||
| this.historyStoreName = 'textHistory'; | |||
| this.db = null; | |||
| this.isInitialized = false; | |||
| this.initPromise = this.initDB(); | |||
| } | |||
| /** | |||
| * 初始化IndexedDB数据库 | |||
| */ | |||
| async initDB() { | |||
| return new Promise((resolve, reject) => { | |||
| if (!window.indexedDB) { | |||
| reject(new Error('浏览器不支持IndexedDB')); | |||
| return; | |||
| } | |||
| const request = indexedDB.open(this.dbName, 5); // 升级版本到5 | |||
| request.onerror = (event) => { | |||
| console.error('IndexedDB打开失败:', event.target.error); | |||
| reject(event.target.error); | |||
| }; | |||
| request.onsuccess = (event) => { | |||
| this.db = event.target.result; | |||
| this.isInitialized = true; | |||
| console.log('IndexedDB初始化成功'); | |||
| // 延迟检查以确保事务完成 | |||
| setTimeout(() => { | |||
| this.ensureStoresExist() | |||
| .then(() => resolve()) | |||
| .catch(error => { | |||
| console.warn('确保存储存在失败,但继续初始化:', error); | |||
| resolve(); // 即使失败也继续 | |||
| }); | |||
| }, 100); | |||
| }; | |||
| request.onupgradeneeded = (event) => { | |||
| console.log('IndexedDB升级:', '旧版本:', event.oldVersion, '新版本:', event.newVersion); | |||
| const db = event.target.result; | |||
| const transaction = event.target.transaction; | |||
| this.createOrUpgradeStores(db, transaction, event.oldVersion); | |||
| }; | |||
| request.onblocked = (event) => { | |||
| console.warn('IndexedDB升级被阻塞'); | |||
| }; | |||
| }); | |||
| } | |||
| /** | |||
| * 创建或升级对象存储(修复版) | |||
| */ | |||
| createOrUpgradeStores(db, transaction, oldVersion) { | |||
| // 创建主数据存储 | |||
| if (!db.objectStoreNames.contains(this.storeName)) { | |||
| const store = db.createObjectStore(this.storeName, { keyPath: 'key' }); | |||
| store.createIndex('timestamp', 'timestamp', { unique: false }); | |||
| console.log('创建主存储:', this.storeName); | |||
| } | |||
| // 创建历史数据存储 | |||
| if (!db.objectStoreNames.contains(this.historyStoreName)) { | |||
| const historyStore = db.createObjectStore(this.historyStoreName, { | |||
| keyPath: 'id', | |||
| autoIncrement: true | |||
| }); | |||
| historyStore.createIndex('timestamp', 'timestamp', { unique: false }); | |||
| historyStore.createIndex('key', 'key', { unique: false }); | |||
| // 不再创建唯一性复合索引,避免冲突 | |||
| console.log('创建历史存储:', this.historyStoreName); | |||
| } else if (oldVersion < 5) { | |||
| // 版本5升级:删除可能存在的唯一性索引,避免冲突 | |||
| const historyStore = transaction.objectStore(this.historyStoreName); | |||
| // 删除旧的复合索引(如果存在) | |||
| if (historyStore.indexNames.contains('key_version')) { | |||
| try { | |||
| historyStore.deleteIndex('key_version'); | |||
| console.log('删除冲突的key_version索引'); | |||
| } catch (error) { | |||
| console.warn('删除索引失败:', error); | |||
| } | |||
| } | |||
| // 添加非唯一的复合索引 | |||
| if (!historyStore.indexNames.contains('key_version_composite')) { | |||
| try { | |||
| historyStore.createIndex('key_version_composite', ['key', 'version'], { unique: false }); | |||
| console.log('创建非唯一的key_version_composite索引'); | |||
| } catch (error) { | |||
| console.warn('创建非唯一索引失败:', error); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * 确保对象存储存在 | |||
| */ | |||
| async ensureStoresExist() { | |||
| if (!this.db) { | |||
| throw new Error('数据库未初始化'); | |||
| } | |||
| const missingStores = [this.storeName, this.historyStoreName] | |||
| .filter(name => !this.db.objectStoreNames.contains(name)); | |||
| if (missingStores.length > 0) { | |||
| console.warn('缺少对象存储:', missingStores); | |||
| throw new Error(`缺少对象存储: ${missingStores.join(', ')}`); | |||
| } | |||
| } | |||
| /** | |||
| * 等待数据库初始化完成 | |||
| */ | |||
| async waitForInit() { | |||
| try { | |||
| await this.initPromise; | |||
| } catch (error) { | |||
| console.error('数据库初始化失败:', error); | |||
| throw new Error('IndexedDB初始化失败: ' + error.message); | |||
| } | |||
| if (!this.isInitialized || !this.db) { | |||
| throw new Error('数据库未正确初始化'); | |||
| } | |||
| } | |||
| /** | |||
| * 保存数据到IndexedDB(修复版) | |||
| */ | |||
| async save(content) { | |||
| try { | |||
| await this.waitForInit(); | |||
| const timestamp = Date.now(); | |||
| const version = await this.getNextVersion(); | |||
| const data = { | |||
| key: this.key, | |||
| content, | |||
| timestamp, | |||
| version, | |||
| lastModified: timestamp | |||
| }; | |||
| // 保存当前版本 | |||
| await this.putData(this.storeName, data); | |||
| // 保存到历史记录 - 使用时间戳作为ID的一部分确保唯一性 | |||
| const historyId = `history_${this.key}_${timestamp}_${Math.random().toString(36).substr(2, 9)}`; | |||
| const historyData = { | |||
| ...data, | |||
| id: historyId, | |||
| historyTimestamp: timestamp, | |||
| uniqueId: historyId // 确保唯一性 | |||
| }; | |||
| await this.putData(this.historyStoreName, historyData); | |||
| return { | |||
| success: true, | |||
| timestamp, | |||
| version, | |||
| storage: 'indexedDB', | |||
| historyId | |||
| }; | |||
| } catch (error) { | |||
| console.error('IndexedDB保存失败:', error); | |||
| // 优雅降级到LocalStorage | |||
| return await this.saveToFallback(content, error); | |||
| } | |||
| } | |||
| /** | |||
| * 保存到后备存储(LocalStorage) | |||
| */ | |||
| async saveToFallback(content, originalError) { | |||
| try { | |||
| const timestamp = Date.now(); | |||
| const version = 1; // 后备存储使用简单版本号 | |||
| // 保存到LocalStorage | |||
| const fallbackKey = `indexedDB_fallback_${this.key}`; | |||
| localStorage.setItem(fallbackKey, JSON.stringify({ | |||
| content, | |||
| timestamp, | |||
| version, | |||
| originalError: originalError?.message | |||
| })); | |||
| // 保存历史 | |||
| this.saveFallbackHistory(content, timestamp); | |||
| return { | |||
| success: true, | |||
| timestamp, | |||
| version, | |||
| storage: 'indexedDB_fallback', | |||
| warning: '数据已保存到LocalStorage作为后备', | |||
| originalError: originalError?.message | |||
| }; | |||
| } catch (fallbackError) { | |||
| console.error('后备存储也失败:', fallbackError); | |||
| return { | |||
| success: false, | |||
| error: `主存储和后备存储都失败: ${originalError?.message}, ${fallbackError.message}`, | |||
| storage: 'indexedDB' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 保存后备历史记录 | |||
| */ | |||
| saveFallbackHistory(content, timestamp) { | |||
| try { | |||
| const historyKey = `indexedDB_history_${this.key}`; | |||
| const historyStr = localStorage.getItem(historyKey); | |||
| let history = historyStr ? JSON.parse(historyStr) : []; | |||
| history.unshift({ | |||
| content, | |||
| timestamp, | |||
| version: history.length + 1, | |||
| fromFallback: true | |||
| }); | |||
| // 限制历史记录数量 | |||
| if (history.length > 10) { | |||
| history = history.slice(0, 10); | |||
| } | |||
| localStorage.setItem(historyKey, JSON.stringify(history)); | |||
| } catch (error) { | |||
| console.error('保存后备历史失败:', error); | |||
| } | |||
| } | |||
| /** | |||
| * 将数据存入指定存储 | |||
| */ | |||
| async putData(storeName, data) { | |||
| return new Promise((resolve, reject) => { | |||
| try { | |||
| // 检查对象存储是否存在 | |||
| if (!this.db.objectStoreNames.contains(storeName)) { | |||
| reject(new Error(`对象存储 ${storeName} 不存在`)); | |||
| return; | |||
| } | |||
| const transaction = this.db.transaction([storeName], 'readwrite'); | |||
| const store = transaction.objectStore(storeName); | |||
| // 添加错误处理 | |||
| transaction.onerror = (event) => { | |||
| console.error(`事务错误 [${storeName}]:`, event.target.error); | |||
| reject(event.target.error); | |||
| }; | |||
| transaction.oncomplete = () => { | |||
| resolve(); | |||
| }; | |||
| const request = store.put(data); | |||
| request.onsuccess = () => { | |||
| // 不要在这里resolve,等待事务完成 | |||
| console.log(`数据已保存到 ${storeName}:`, data.key); | |||
| }; | |||
| request.onerror = (event) => { | |||
| console.error(`保存请求失败 [${storeName}]:`, event.target.error); | |||
| reject(event.target.error); | |||
| }; | |||
| } catch (error) { | |||
| reject(error); | |||
| } | |||
| }); | |||
| } | |||
| /** | |||
| * 从IndexedDB加载数据 | |||
| */ | |||
| async load() { | |||
| try { | |||
| await this.waitForInit(); | |||
| // 首先尝试从IndexedDB加载 | |||
| const data = await this.loadFromIndexedDB(); | |||
| if (data) { | |||
| return { | |||
| success: true, | |||
| data, | |||
| storage: 'indexedDB' | |||
| }; | |||
| } | |||
| // 如果没有数据,尝试从后备存储加载 | |||
| return await this.loadFromFallback(); | |||
| } catch (error) { | |||
| console.error('加载数据失败:', error); | |||
| return await this.loadFromFallbackWithError(error); | |||
| } | |||
| } | |||
| /** | |||
| * 从IndexedDB加载 | |||
| */ | |||
| async loadFromIndexedDB() { | |||
| return new Promise((resolve, reject) => { | |||
| try { | |||
| if (!this.db.objectStoreNames.contains(this.storeName)) { | |||
| resolve(null); | |||
| return; | |||
| } | |||
| const transaction = this.db.transaction([this.storeName], 'readonly'); | |||
| const store = transaction.objectStore(this.storeName); | |||
| const request = store.get(this.key); | |||
| request.onsuccess = () => { | |||
| resolve(request.result || null); | |||
| }; | |||
| request.onerror = (event) => { | |||
| console.error('从IndexedDB加载失败:', event.target.error); | |||
| reject(event.target.error); | |||
| }; | |||
| } catch (error) { | |||
| reject(error); | |||
| } | |||
| }); | |||
| } | |||
| /** | |||
| * 从后备存储加载 | |||
| */ | |||
| async loadFromFallback() { | |||
| try { | |||
| const fallbackKey = `indexedDB_fallback_${this.key}`; | |||
| const fallbackStr = localStorage.getItem(fallbackKey); | |||
| if (fallbackStr) { | |||
| const data = JSON.parse(fallbackStr); | |||
| console.log('从后备存储加载数据'); | |||
| return { | |||
| success: true, | |||
| data, | |||
| storage: 'indexedDB_fallback', | |||
| fromFallback: true | |||
| }; | |||
| } | |||
| return { | |||
| success: true, | |||
| data: null, | |||
| storage: 'indexedDB', | |||
| message: '无数据' | |||
| }; | |||
| } catch (error) { | |||
| console.error('从后备存储加载失败:', error); | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'indexedDB_fallback' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 带错误的从后备存储加载 | |||
| */ | |||
| async loadFromFallbackWithError(originalError) { | |||
| try { | |||
| const result = await this.loadFromFallback(); | |||
| if (result.success && result.data) { | |||
| return { | |||
| ...result, | |||
| originalError: originalError?.message | |||
| }; | |||
| } | |||
| return { | |||
| success: false, | |||
| error: `加载失败: ${originalError?.message}`, | |||
| storage: 'indexedDB' | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: `主存储和后备存储都失败: ${originalError?.message}, ${error.message}`, | |||
| storage: 'indexedDB' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 获取历史版本(修复版) | |||
| */ | |||
| async getHistory(limit = 100) { | |||
| try { | |||
| await this.waitForInit(); | |||
| // 首先尝试从IndexedDB获取历史 | |||
| const indexedDBHistory = await this.getHistoryFromIndexedDB(limit); | |||
| if (indexedDBHistory.success && indexedDBHistory.data.length > 0) { | |||
| return indexedDBHistory; | |||
| } | |||
| // 如果没有历史,尝试从后备存储获取 | |||
| return await this.getHistoryFromFallback(limit); | |||
| } catch (error) { | |||
| console.error('获取历史失败:', error); | |||
| return await this.getHistoryFromFallback(limit); | |||
| } | |||
| } | |||
| /** | |||
| * 从IndexedDB获取历史 | |||
| */ | |||
| async getHistoryFromIndexedDB(limit) { | |||
| return new Promise((resolve) => { | |||
| try { | |||
| if (!this.db.objectStoreNames.contains(this.historyStoreName)) { | |||
| resolve({ | |||
| success: true, | |||
| data: [], | |||
| storage: 'indexedDB', | |||
| message: '历史存储不存在' | |||
| }); | |||
| return; | |||
| } | |||
| const transaction = this.db.transaction([this.historyStoreName], 'readonly'); | |||
| const store = transaction.objectStore(this.historyStoreName); | |||
| // 尝试使用key索引 | |||
| let index; | |||
| if (store.indexNames.contains('key')) { | |||
| index = store.index('key'); | |||
| } else { | |||
| // 如果没有key索引,使用全表扫描 | |||
| index = null; | |||
| } | |||
| const history = []; | |||
| const request = index ? | |||
| index.openCursor(IDBKeyRange.only(this.key), 'prev') : | |||
| store.openCursor(null, 'prev'); | |||
| request.onsuccess = (event) => { | |||
| const cursor = event.target.result; | |||
| if (cursor) { | |||
| const item = cursor.value; | |||
| // 过滤出当前key的历史记录 | |||
| if (item.key === this.key && history.length < limit) { | |||
| history.push(item); | |||
| } | |||
| cursor.continue(); | |||
| } else { | |||
| // 按时间戳排序 | |||
| history.sort((a, b) => b.timestamp - a.timestamp); | |||
| resolve({ | |||
| success: true, | |||
| data: history, | |||
| storage: 'indexedDB' | |||
| }); | |||
| } | |||
| }; | |||
| request.onerror = (event) => { | |||
| console.error('从IndexedDB获取历史失败:', event.target.error); | |||
| resolve({ | |||
| success: false, | |||
| error: event.target.error?.message, | |||
| storage: 'indexedDB' | |||
| }); | |||
| }; | |||
| } catch (error) { | |||
| resolve({ | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'indexedDB' | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| /** | |||
| * 从后备存储获取历史 | |||
| */ | |||
| async getHistoryFromFallback(limit) { | |||
| try { | |||
| const historyKey = `indexedDB_history_${this.key}`; | |||
| const historyStr = localStorage.getItem(historyKey); | |||
| const history = historyStr ? JSON.parse(historyStr) : []; | |||
| return { | |||
| success: true, | |||
| data: history.slice(0, limit), | |||
| storage: 'indexedDB_fallback', | |||
| fromFallback: true | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'indexedDB_fallback' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 获取下一个版本号 | |||
| */ | |||
| async getNextVersion() { | |||
| try { | |||
| const result = await this.load(); | |||
| if (result.success && result.data) { | |||
| const data = result.data; | |||
| return (data.version || 0) + 1; | |||
| } | |||
| return 1; | |||
| } catch (error) { | |||
| console.error('获取版本号失败:', error); | |||
| return 1; | |||
| } | |||
| } | |||
| /** | |||
| * 清理重复的历史记录 | |||
| */ | |||
| async cleanupDuplicateHistory() { | |||
| try { | |||
| await this.waitForInit(); | |||
| if (!this.db.objectStoreNames.contains(this.historyStoreName)) { | |||
| return { success: true, cleaned: 0 }; | |||
| } | |||
| return new Promise((resolve) => { | |||
| const transaction = this.db.transaction([this.historyStoreName], 'readwrite'); | |||
| const store = transaction.objectStore(this.historyStoreName); | |||
| // 找出当前key的所有历史记录 | |||
| const index = store.index('key'); | |||
| const keyRange = IDBKeyRange.only(this.key); | |||
| const request = index.openCursor(keyRange); | |||
| const seenVersions = new Set(); | |||
| const duplicates = []; | |||
| const toDelete = []; | |||
| request.onsuccess = (event) => { | |||
| const cursor = event.target.result; | |||
| if (cursor) { | |||
| const item = cursor.value; | |||
| const versionKey = `${item.key}_${item.version}`; | |||
| if (seenVersions.has(versionKey)) { | |||
| duplicates.push(item); | |||
| toDelete.push(item.id); | |||
| } else { | |||
| seenVersions.add(versionKey); | |||
| } | |||
| cursor.continue(); | |||
| } else { | |||
| // 删除重复项 | |||
| if (toDelete.length === 0) { | |||
| resolve({ success: true, cleaned: 0, duplicates: [] }); | |||
| return; | |||
| } | |||
| this.deleteHistoryItems(toDelete) | |||
| .then(() => { | |||
| console.log(`清理了 ${toDelete.length} 条重复历史记录`); | |||
| resolve({ | |||
| success: true, | |||
| cleaned: toDelete.length, | |||
| duplicates: duplicates.map(d => ({ id: d.id, version: d.version })) | |||
| }); | |||
| }) | |||
| .catch(error => { | |||
| resolve({ | |||
| success: false, | |||
| error: error.message, | |||
| duplicatesFound: duplicates.length | |||
| }); | |||
| }); | |||
| } | |||
| }; | |||
| request.onerror = (event) => { | |||
| resolve({ | |||
| success: false, | |||
| error: event.target.error?.message | |||
| }); | |||
| }; | |||
| }); | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 删除历史记录项 | |||
| */ | |||
| async deleteHistoryItems(ids) { | |||
| return new Promise((resolve, reject) => { | |||
| const transaction = this.db.transaction([this.historyStoreName], 'readwrite'); | |||
| const store = transaction.objectStore(this.historyStoreName); | |||
| let completed = 0; | |||
| let errors = []; | |||
| ids.forEach(id => { | |||
| const request = store.delete(id); | |||
| request.onerror = (event) => { | |||
| errors.push({ id, error: event.target.error }); | |||
| }; | |||
| }); | |||
| transaction.oncomplete = () => { | |||
| if (errors.length > 0) { | |||
| reject(new Error(`删除失败: ${errors.map(e => e.error.message).join(', ')}`)); | |||
| } else { | |||
| resolve(); | |||
| } | |||
| }; | |||
| transaction.onerror = (event) => { | |||
| reject(event.target.error); | |||
| }; | |||
| }); | |||
| } | |||
| /** | |||
| * 重置数据库(用于修复) | |||
| */ | |||
| async resetDatabase() { | |||
| try { | |||
| // 备份现有数据 | |||
| const backup = await this.load(); | |||
| // 关闭当前连接 | |||
| if (this.db) { | |||
| this.db.close(); | |||
| } | |||
| // 删除数据库 | |||
| await new Promise((resolve, reject) => { | |||
| const deleteRequest = indexedDB.deleteDatabase(this.dbName); | |||
| deleteRequest.onsuccess = () => resolve(); | |||
| deleteRequest.onerror = (event) => reject(event.target.error); | |||
| }); | |||
| console.log('数据库已重置'); | |||
| // 重新初始化 | |||
| this.db = null; | |||
| this.isInitialized = false; | |||
| this.initPromise = this.initDB(); | |||
| await this.waitForInit(); | |||
| // 恢复数据(如果有) | |||
| if (backup.success && backup.data) { | |||
| await this.save(backup.data.content); | |||
| } | |||
| return { | |||
| success: true, | |||
| message: '数据库重置成功', | |||
| dataRestored: backup.success && !!backup.data | |||
| }; | |||
| } catch (error) { | |||
| console.error('重置数据库失败:', error); | |||
| return { | |||
| success: false, | |||
| error: error.message | |||
| }; | |||
| } | |||
| } | |||
| } | |||
| export default IndexedDBStorage; | |||
| @ -0,0 +1,163 @@ | |||
| /** | |||
| * LocalStorage存储策略 | |||
| * 将数据保存在浏览器的localStorage中,持久化存储 | |||
| */ | |||
| class LocalStorage { | |||
| constructor(key) { | |||
| this.key = `hxhq_local_${key}`; | |||
| this.historyKey = `hxhq_local_history_${key}`; | |||
| } | |||
| /** | |||
| * 保存数据到localStorage | |||
| * @param {string} content - 要保存的内容 | |||
| * @returns {Promise<object>} 保存结果 | |||
| */ | |||
| async save(content) { | |||
| try { | |||
| const timestamp = Date.now(); | |||
| const version = this.getNextVersion(); | |||
| const data = { | |||
| content, | |||
| timestamp, | |||
| version | |||
| }; | |||
| // 保存当前版本 | |||
| localStorage.setItem(this.key, JSON.stringify(data)); | |||
| // 保存到历史记录 | |||
| this.saveToHistory(data); | |||
| return { | |||
| success: true, | |||
| timestamp, | |||
| version, | |||
| storage: 'localStorage' | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'localStorage' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 从localStorage加载数据 | |||
| * @returns {Promise<object>} 加载的数据 | |||
| */ | |||
| async load() { | |||
| try { | |||
| const dataStr = localStorage.getItem(this.key); | |||
| const data = dataStr ? JSON.parse(dataStr) : null; | |||
| return { | |||
| success: true, | |||
| data, | |||
| storage: 'localStorage' | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'localStorage' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 获取历史版本 | |||
| * @returns {Promise<Array>} 历史版本列表 | |||
| */ | |||
| async getHistory() { | |||
| try { | |||
| const historyStr = localStorage.getItem(this.historyKey); | |||
| const history = historyStr ? JSON.parse(historyStr) : []; | |||
| return { | |||
| success: true, | |||
| data: history, | |||
| storage: 'localStorage' | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'localStorage' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 保存到历史记录 | |||
| * @param {object} data - 要保存的数据 | |||
| */ | |||
| saveToHistory(data) { | |||
| try { | |||
| const historyStr = localStorage.getItem(this.historyKey); | |||
| let history = historyStr ? JSON.parse(historyStr) : []; | |||
| // 添加到历史记录开头 | |||
| history.unshift({...data}); | |||
| // 限制历史记录数量 | |||
| if (history.length > 100) { | |||
| history = history.slice(0, 100); | |||
| } | |||
| localStorage.setItem(this.historyKey, JSON.stringify(history)); | |||
| } catch (error) { | |||
| console.error('保存历史记录失败:', error); | |||
| } | |||
| } | |||
| /** | |||
| * 获取下一个版本号 | |||
| * @returns {number} 版本号 | |||
| */ | |||
| getNextVersion() { | |||
| try { | |||
| const dataStr = localStorage.getItem(this.key); | |||
| if (!dataStr) return 1; | |||
| const data = JSON.parse(dataStr); | |||
| return data.version + 1; | |||
| } catch (error) { | |||
| return 1; | |||
| } | |||
| } | |||
| /** | |||
| * 获取备份文件列表 | |||
| * @returns {Promise<Array>} 备份文件列表 | |||
| */ | |||
| async getBackupFiles() { | |||
| try { | |||
| const files = []; | |||
| for (let i = 0; i < localStorage.length; i++) { | |||
| const key = localStorage.key(i); | |||
| if (key.startsWith('backup_')) { | |||
| const data = JSON.parse(localStorage.getItem(key)); | |||
| files.push({ | |||
| key, | |||
| timestamp: data.timestamp, | |||
| version: data.version, | |||
| content: data.content.substring(0, 100) + '...' | |||
| }); | |||
| } | |||
| } | |||
| return { | |||
| success: true, | |||
| data: files | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message | |||
| }; | |||
| } | |||
| } | |||
| } | |||
| export default LocalStorage; | |||
| @ -0,0 +1,106 @@ | |||
| /** | |||
| * 内存存储策略 | |||
| * 将数据保存在内存中,页面刷新后数据丢失 | |||
| */ | |||
| class MemoryStorage { | |||
| constructor(key) { | |||
| this.key = `hxhq_memory_${key}`; | |||
| this.storage = new Map(); // 使用Map存储多个key的数据 | |||
| this.history = new Map(); // 存储历史版本 | |||
| } | |||
| /** | |||
| * 保存数据到内存 | |||
| * @param {string} content - 要保存的内容 | |||
| * @returns {Promise<object>} 保存结果 | |||
| */ | |||
| async save(content) { | |||
| try { | |||
| const timestamp = Date.now(); | |||
| const data = { | |||
| content, | |||
| timestamp, | |||
| version: this.getNextVersion() | |||
| }; | |||
| // 保存当前版本 | |||
| this.storage.set(this.key, data); | |||
| // 保存到历史记录(限制最多保存20个历史版本) | |||
| if (!this.history.has(this.key)) { | |||
| this.history.set(this.key, []); | |||
| } | |||
| const history = this.history.get(this.key); | |||
| history.unshift({...data}); | |||
| if (history.length > 20) { | |||
| history.pop(); | |||
| } | |||
| return { | |||
| success: true, | |||
| timestamp, | |||
| version: data.version, | |||
| storage: 'memory' | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'memory' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 从内存加载数据 | |||
| * @returns {Promise<object>} 加载的数据 | |||
| */ | |||
| async load() { | |||
| try { | |||
| const data = this.storage.get(this.key); | |||
| return { | |||
| success: true, | |||
| data: data || null, | |||
| storage: 'memory' | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'memory' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 获取历史版本 | |||
| * @returns {Promise<Array>} 历史版本列表 | |||
| */ | |||
| async getHistory() { | |||
| try { | |||
| const history = this.history.get(this.key) || []; | |||
| return { | |||
| success: true, | |||
| data: history, | |||
| storage: 'memory' | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'memory' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 获取下一个版本号 | |||
| * @returns {number} 版本号 | |||
| */ | |||
| getNextVersion() { | |||
| const current = this.storage.get(this.key); | |||
| return current ? current.version + 1 : 1; | |||
| } | |||
| } | |||
| export default MemoryStorage; | |||
| @ -0,0 +1,286 @@ | |||
| /** | |||
| * 服务器存储策略 | |||
| * 支持离线队列,网络恢复后自动重传 | |||
| */ | |||
| class ServerStorage { | |||
| constructor(key, options = {}) { | |||
| this.key = key; | |||
| this.options = { | |||
| endpoint: options.endpoint || '/api/save-text', | |||
| syncEndpoint: options.syncEndpoint || '/api/sync-text', | |||
| historyEndpoint: options.historyEndpoint || '/api/text-history', | |||
| retryCount: options.retryCount || 3, //最多重试次数 | |||
| retryDelay: options.retryDelay || 1000,//重试等待 毫秒 | |||
| ...options | |||
| }; | |||
| this.queue = []; | |||
| this.isOnline = navigator.onLine; | |||
| this.isSyncing = false; | |||
| this.initOfflineQueue(); | |||
| this.setupNetworkListeners(); | |||
| } | |||
| /** | |||
| * 初始化离线队列 | |||
| */ | |||
| initOfflineQueue() { | |||
| const queueStr = localStorage.getItem(`hxhq_serverQueue_${this.key}`); | |||
| if (queueStr) { | |||
| try { | |||
| this.queue = JSON.parse(queueStr); | |||
| } catch (error) { | |||
| this.queue = []; | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * 设置网络状态监听 | |||
| */ | |||
| setupNetworkListeners() { | |||
| window.addEventListener('online', () => { | |||
| this.isOnline = true; | |||
| this.processQueue(); | |||
| }); | |||
| window.addEventListener('offline', () => { | |||
| this.isOnline = false; | |||
| }); | |||
| } | |||
| /** | |||
| * 保存队列到本地存储 | |||
| */ | |||
| saveQueue() { | |||
| localStorage.setItem(`hxhq_serverQueue_${this.key}`, JSON.stringify(this.queue)); | |||
| } | |||
| /** | |||
| * 处理离线队列 | |||
| */ | |||
| async processQueue() { | |||
| if (this.isSyncing || !this.isOnline || this.queue.length === 0) { | |||
| return; | |||
| } | |||
| this.isSyncing = true; | |||
| while (this.queue.length > 0 && this.isOnline) { | |||
| const item = this.queue[0]; | |||
| try { | |||
| await this.sendToServer(item.content, item.timestamp, item.version); | |||
| this.queue.shift(); // 发送成功,从队列移除 | |||
| this.saveQueue(); | |||
| } catch (error) { | |||
| console.error('同步失败:', error); | |||
| item.retryCount = (item.retryCount || 0) + 1; | |||
| if (item.retryCount >= this.options.retryCount) { | |||
| this.queue.shift(); // 超过重试次数,移除 | |||
| this.saveQueue(); | |||
| } else { | |||
| // 重试等待 | |||
| await new Promise(resolve => | |||
| setTimeout(resolve, this.options.retryDelay * item.retryCount) | |||
| ); | |||
| } | |||
| } | |||
| } | |||
| this.isSyncing = false; | |||
| } | |||
| /** | |||
| * 发送数据到服务器 | |||
| * @param {string} content - 内容 | |||
| * @param {number} timestamp - 时间戳 | |||
| * @param {number} version - 版本号 | |||
| */ | |||
| async sendToServer(content, timestamp, version) { | |||
| return new Promise((resolve, reject) => { | |||
| const xhr = new XMLHttpRequest(); | |||
| xhr.open('POST', this.options.endpoint); | |||
| xhr.setRequestHeader('Content-Type', 'application/json'); | |||
| xhr.onload = () => { | |||
| if (xhr.status === 200 || xhr.status === 201) { | |||
| resolve(JSON.parse(xhr.responseText)); | |||
| } else { | |||
| reject(new Error(`服务器错误: ${xhr.status}`)); | |||
| } | |||
| }; | |||
| xhr.onerror = () => reject(new Error('网络请求失败')); | |||
| xhr.ontimeout = () => reject(new Error('请求超时')); | |||
| xhr.send(JSON.stringify({ | |||
| key: this.key, | |||
| content, | |||
| timestamp, | |||
| version, | |||
| syncedAt: Date.now() | |||
| })); | |||
| }); | |||
| } | |||
| /** | |||
| * 保存数据(优先尝试服务器,失败则加入离线队列) | |||
| * @param {string} content - 要保存的内容 | |||
| * @returns {Promise<object>} 保存结果 | |||
| */ | |||
| async save(content) { | |||
| try { | |||
| const timestamp = Date.now(); | |||
| const version = await this.getNextVersion(); | |||
| if (this.isOnline) { | |||
| try { | |||
| const result = await this.sendToServer(content, timestamp, version); | |||
| return { | |||
| success: true, | |||
| timestamp, | |||
| version, | |||
| storage: 'server', | |||
| synced: true | |||
| }; | |||
| } catch (error) { | |||
| // 服务器保存失败,加入离线队列 | |||
| console.warn('服务器保存失败,加入离线队列:', error); | |||
| } | |||
| } | |||
| // 离线或服务器失败,加入队列 | |||
| this.queue.push({ | |||
| content, | |||
| timestamp, | |||
| version, | |||
| addedAt: Date.now() | |||
| }); | |||
| this.saveQueue(); | |||
| return { | |||
| success: true, | |||
| timestamp, | |||
| version, | |||
| storage: 'server', | |||
| synced: false, | |||
| queued: true | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'server' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 从服务器加载数据(失败则返回离线数据) | |||
| * @returns {Promise<object>} 加载的数据 | |||
| */ | |||
| async load() { | |||
| try { | |||
| if (this.isOnline) { | |||
| try { | |||
| const response = await fetch(`${this.options.syncEndpoint}?key=${this.key}`); | |||
| if (response.ok) { | |||
| const data = await response.json(); | |||
| return { | |||
| success: true, | |||
| data, | |||
| storage: 'server', | |||
| fromServer: true | |||
| }; | |||
| } | |||
| } catch (error) { | |||
| console.warn('从服务器加载失败:', error); | |||
| } | |||
| } | |||
| // 返回队列中的最新数据 | |||
| const latestQueued = this.queue.length > 0 | |||
| ? this.queue[this.queue.length - 1] | |||
| : null; | |||
| return { | |||
| success: true, | |||
| data: latestQueued, | |||
| storage: 'server', | |||
| fromServer: false | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'server' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 获取服务器历史版本 | |||
| * @returns {Promise<Array>} 历史版本列表 | |||
| */ | |||
| async getHistory() { | |||
| try { | |||
| if (!this.isOnline) { | |||
| return { | |||
| success: false, | |||
| error: '网络未连接', | |||
| storage: 'server' | |||
| }; | |||
| } | |||
| const response = await fetch(`${this.options.historyEndpoint}?key=${this.key}`); | |||
| if (!response.ok) { | |||
| throw new Error(`服务器错误: ${response.status}`); | |||
| } | |||
| const data = await response.json(); | |||
| return { | |||
| success: true, | |||
| data, | |||
| storage: 'server' | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'server' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 获取下一个版本号 | |||
| * @returns {Promise<number>} 版本号 | |||
| */ | |||
| async getNextVersion() { | |||
| try { | |||
| const result = await this.load(); | |||
| if (result.success && result.data) { | |||
| return result.data.version + 1; | |||
| } | |||
| return 1; | |||
| } catch (error) { | |||
| return 1; | |||
| } | |||
| } | |||
| /** | |||
| * 获取队列状态 | |||
| * @returns {object} 队列状态 | |||
| */ | |||
| getQueueStatus() { | |||
| return { | |||
| queuedCount: this.queue.length, | |||
| isOnline: this.isOnline, | |||
| isSyncing: this.isSyncing, | |||
| nextRetry: this.queue.length > 0 ? this.queue[0].retryCount || 0 : 0 | |||
| }; | |||
| } | |||
| } | |||
| export default ServerStorage; | |||
| @ -0,0 +1,128 @@ | |||
| /** | |||
| * SessionStorage存储策略 | |||
| * 将数据保存在浏览器的sessionStorage中,标签页关闭后数据丢失 | |||
| */ | |||
| class SessionStorage { | |||
| constructor(key) { | |||
| this.key = `hxhq_session_${key}`; | |||
| this.historyKey = `hxhq_session_history_${key}`; | |||
| } | |||
| /** | |||
| * 保存数据到sessionStorage | |||
| * @param {string} content - 要保存的内容 | |||
| * @returns {Promise<object>} 保存结果 | |||
| */ | |||
| async save(content) { | |||
| try { | |||
| const timestamp = Date.now(); | |||
| const version = this.getNextVersion(); | |||
| const data = { | |||
| content, | |||
| timestamp, | |||
| version | |||
| }; | |||
| sessionStorage.setItem(this.key, JSON.stringify(data)); | |||
| this.saveToHistory(data); | |||
| return { | |||
| success: true, | |||
| timestamp, | |||
| version, | |||
| storage: 'sessionStorage' | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'sessionStorage' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 从sessionStorage加载数据 | |||
| * @returns {Promise<object>} 加载的数据 | |||
| */ | |||
| async load() { | |||
| try { | |||
| const dataStr = sessionStorage.getItem(this.key); | |||
| const data = dataStr ? JSON.parse(dataStr) : null; | |||
| return { | |||
| success: true, | |||
| data, | |||
| storage: 'sessionStorage' | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'sessionStorage' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 获取历史版本 | |||
| * @returns {Promise<Array>} 历史版本列表 | |||
| */ | |||
| async getHistory() { | |||
| try { | |||
| const historyStr = sessionStorage.getItem(this.historyKey); | |||
| const history = historyStr ? JSON.parse(historyStr) : []; | |||
| return { | |||
| success: true, | |||
| data: history, | |||
| storage: 'sessionStorage' | |||
| }; | |||
| } catch (error) { | |||
| return { | |||
| success: false, | |||
| error: error.message, | |||
| storage: 'sessionStorage' | |||
| }; | |||
| } | |||
| } | |||
| /** | |||
| * 保存到历史记录 | |||
| * @param {object} data - 要保存的数据 | |||
| */ | |||
| saveToHistory(data) { | |||
| try { | |||
| const historyStr = sessionStorage.getItem(this.historyKey); | |||
| let history = historyStr ? JSON.parse(historyStr) : []; | |||
| history.unshift({...data}); | |||
| if (history.length > 100) { | |||
| history = history.slice(0, 100); | |||
| } | |||
| sessionStorage.setItem(this.historyKey, JSON.stringify(history)); | |||
| } catch (error) { | |||
| console.error('保存历史记录失败:', error); | |||
| } | |||
| } | |||
| /** | |||
| * 获取下一个版本号 | |||
| * @returns {number} 版本号 | |||
| */ | |||
| getNextVersion() { | |||
| try { | |||
| const dataStr = sessionStorage.getItem(this.key); | |||
| if (!dataStr) return 1; | |||
| const data = JSON.parse(dataStr); | |||
| return data.version + 1; | |||
| } catch (error) { | |||
| return 1; | |||
| } | |||
| } | |||
| } | |||
| export default SessionStorage; | |||
| @ -0,0 +1,252 @@ | |||
| <template> | |||
| <div class="app-container"> | |||
| <div class="editor-grid"> | |||
| <div class="editor-section"> | |||
| <div style="display: flex;flex-wrap: wrap;"> | |||
| <div style="line-height:40px; line-height: 40px;">步骤1</div> | |||
| <AutoSaveTextarea v-model="content1" data-key="note1" data-name="步骤1" data-type="input" placeholder="" | |||
| :server-config="serverConfig" @content-restored="handleContentRestored" dataWidth="200px" /> | |||
| <div style="line-height:40px; line-height: 40px;">选择</div> | |||
| <AutoSaveTextarea v-model="content2" data-key="note2" data-name="涡旋仪" data-type="select" | |||
| :data-select="dataSelect" placeholder="点击选择" :server-config="serverConfig" | |||
| @content-restored="handleContentRestored" dataWidth="200px" /> | |||
| <div style="line-height:40px; line-height: 40px;">涡旋仪,温度理论值</div> | |||
| <AutoSaveTextarea v-model="content3" data-key="note3" data-name="温度理论值" data-type="input" placeholder="" | |||
| :server-config="serverConfig" @content-restored="handleContentRestored" dataWidth="100px" /> | |||
| <div style="line-height:40px; line-height: 40px;">℃,实际值:</div> | |||
| <AutoSaveTextarea v-model="content4" data-key="note4" data-name="温度实际值" data-type="input" placeholder="" | |||
| :server-config="serverConfig" @content-restored="handleContentRestored" dataWidth="100px" /> | |||
| <div style="line-height:40px; line-height: 40px;">℃,涡旋混匀理论值:</div> | |||
| <AutoSaveTextarea v-model="content5" data-key="note5" data-name="涡旋混匀理论值" data-type="input" placeholder="" | |||
| :server-config="serverConfig" @content-restored="handleContentRestored" dataWidth="100px" /> | |||
| <AutoSaveTextarea v-model="content6" data-key="note6" data-name="涡旋混匀理论值单位" data-type="input" placeholder="" | |||
| :server-config="serverConfig" @content-restored="handleContentRestored" dataWidth="100px" /> | |||
| <div style="line-height:40px; line-height: 40px;">实际值:</div> | |||
| <AutoSaveTextarea v-model="content7" data-key="note7" data-name="涡旋混匀实际值" data-type="input" placeholder="" | |||
| :server-config="serverConfig" @content-restored="handleContentRestored" dataWidth="100px" /> | |||
| <AutoSaveTextarea v-model="content8" data-key="note8" data-name="涡旋混匀实际值单位" data-type="input" placeholder="" | |||
| :server-config="serverConfig" @content-restored="handleContentRestored" dataWidth="100px" /> | |||
| <AutoSaveTextarea v-model="content2" data-key="note9" data-name="总结" data-type="textarea" | |||
| :data-select="dataSelect" placeholder="请输入总结" :server-config="serverConfig" | |||
| @content-restored="handleContentRestored" dataWidth="100%" :textarea-row="5"/> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <h3 style="color: red;">通过多层存储,确保在各种异常情况下(网络中断、浏览器崩溃、存储限制等)用户内容都能得到妥善保存。</h3> | |||
| <h2>分层存储(数据流向:用户输入 → 本地持久化 → 云端)</h2> | |||
| <p> 1.服务器存储:云端同步,跨设备访问</p> | |||
| <p> 2.IndexedDB存储:浏览器内置 NoSQL 数据库,所有pc浏览器都支持,移动端浏览器也支持(存储数据可达50%的磁盘容量,并且支持事务,索引)(关闭标签页/窗口 → 数据保留;刷新页面 → 数据保留)</p> | |||
| <p> 3.LocalStorage:本地存储, 持久化,跨会话保存(关闭标签页/窗口 → 数据保留;刷新页面 → 数据保留)</p> | |||
| <h2>实时保存</h2> | |||
| <p>3.输入结束,失去焦点后触发多存储并行保存</p> | |||
| <p>4.页面关闭前强制同步保存</p> | |||
| <p>5.状态监控:实时跟踪各存储状态</p> | |||
| <h2>云端同步</h2> | |||
| <p>1.失败操作加入离线队列</p> | |||
| <p>2.网络恢复时自动重试</p> | |||
| <p>3.失败重试策略(可配置重试等待延迟,以及重试次数,避免太多请求,造成服务器压力过大)</p> | |||
| <!-- 网络状态提示 --> | |||
| <!-- <div class="network-status" :class="networkClass"> | |||
| <i :class="networkIcon"></i> | |||
| 网络状态:{{ networkText }} | |||
| </div> --> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import AutoSaveTextarea from './components/AutoSaveTextarea.vue'; | |||
| export default { | |||
| name: 'App', | |||
| components: { | |||
| AutoSaveTextarea | |||
| }, | |||
| data() { | |||
| return { | |||
| content1: '漩涡混匀', | |||
| content2: '', | |||
| content3: '30', | |||
| content4: '', | |||
| content5: '10', | |||
| content6: 'min', | |||
| content7: '', | |||
| content8: '', | |||
| dataSelect: [{ value: '迈瑞', label: '迈瑞' },{ value: '大疆', label: '大疆' }], | |||
| serverConfig: { | |||
| endpoint: '/api/save-text', | |||
| syncEndpoint: '/api/sync-text', | |||
| historyEndpoint: '/api/text-history', | |||
| retryCount: 3, | |||
| retryDelay: 1000 | |||
| }, | |||
| isOnline: navigator.onLine, | |||
| editors: [] // 存储编辑器实例 | |||
| }; | |||
| }, | |||
| computed: { | |||
| networkClass() { | |||
| return this.isOnline ? 'online' : 'offline'; | |||
| }, | |||
| networkIcon() { | |||
| return this.isOnline ? 'el-icon-success' : 'el-icon-warning'; | |||
| }, | |||
| networkText() { | |||
| return this.isOnline ? '在线' : '离线'; | |||
| } | |||
| }, | |||
| mounted() { | |||
| // 监听网络状态 | |||
| window.addEventListener('online', this.updateNetworkStatus); | |||
| window.addEventListener('offline', this.updateNetworkStatus); | |||
| // 监听自定义事件来收集编辑器实例 | |||
| this.$root.$on('editor-registered', this.registerEditor); | |||
| }, | |||
| beforeDestroy() { | |||
| window.removeEventListener('online', this.updateNetworkStatus); | |||
| window.removeEventListener('offline', this.updateNetworkStatus); | |||
| this.$root.$off('editor-registered', this.registerEditor); | |||
| }, | |||
| methods: { | |||
| handleContentRestored(content) { | |||
| console.log('内容已恢复:', content); | |||
| this.$message.success('内容恢复成功'); | |||
| }, | |||
| updateNetworkStatus() { | |||
| this.isOnline = navigator.onLine; | |||
| }, | |||
| registerEditor(editor) { | |||
| this.editors.push(editor); | |||
| }, | |||
| } | |||
| }; | |||
| </script> | |||
| <style scoped> | |||
| .app-container { | |||
| max-width: 1200px; | |||
| margin: 0 auto; | |||
| padding: 20px; | |||
| } | |||
| h1 { | |||
| text-align: center; | |||
| margin-bottom: 40px; | |||
| color: #303133; | |||
| } | |||
| .editor-grid { | |||
| display: grid; | |||
| grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); | |||
| gap: 30px; | |||
| margin-bottom: 40px; | |||
| } | |||
| .editor-section { | |||
| background: white; | |||
| border-radius: 8px; | |||
| padding: 20px; | |||
| box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); | |||
| } | |||
| .editor-section h2 { | |||
| margin: 0 0 20px 0; | |||
| font-size: 18px; | |||
| color: #409eff; | |||
| border-bottom: 2px solid #409eff; | |||
| padding-bottom: 10px; | |||
| } | |||
| .network-status { | |||
| position: fixed; | |||
| top: 20px; | |||
| right: 20px; | |||
| padding: 8px 16px; | |||
| border-radius: 20px; | |||
| font-size: 14px; | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 8px; | |||
| z-index: 1000; | |||
| } | |||
| .network-status.online { | |||
| background: #f0f9eb; | |||
| color: #67c23a; | |||
| border: 1px solid #e1f3d8; | |||
| } | |||
| .network-status.offline { | |||
| background: #fef0f0; | |||
| color: #f56c6c; | |||
| border: 1px solid #fde2e2; | |||
| } | |||
| .batch-actions { | |||
| display: flex; | |||
| justify-content: center; | |||
| gap: 20px; | |||
| margin-top: 30px; | |||
| padding-top: 30px; | |||
| border-top: 1px solid #ebeef5; | |||
| } | |||
| .btn { | |||
| padding: 10px 20px; | |||
| border: none; | |||
| border-radius: 4px; | |||
| font-size: 14px; | |||
| cursor: pointer; | |||
| display: flex; | |||
| align-items: center; | |||
| gap: 8px; | |||
| transition: all 0.3s ease; | |||
| } | |||
| .btn-primary { | |||
| background: #409eff; | |||
| color: white; | |||
| } | |||
| .btn-primary:hover { | |||
| background: #66b1ff; | |||
| } | |||
| .btn-secondary { | |||
| background: #f5f7fa; | |||
| color: #606266; | |||
| border: 1px solid #dcdfe6; | |||
| } | |||
| .btn-secondary:hover { | |||
| background: #e4e7ed; | |||
| } | |||
| @media (max-width: 768px) { | |||
| .editor-grid { | |||
| grid-template-columns: 1fr; | |||
| } | |||
| .editor-section { | |||
| padding: 15px; | |||
| } | |||
| .batch-actions { | |||
| flex-direction: column; | |||
| } | |||
| .btn { | |||
| width: 100%; | |||
| justify-content: center; | |||
| } | |||
| } | |||
| </style> | |||