diff --git a/src/api/business/public/public.js b/src/api/business/public/public.js index 85888af..41188ab 100644 --- a/src/api/business/public/public.js +++ b/src/api/business/public/public.js @@ -37,12 +37,3 @@ export function public_studyFormPreList(query) { params: query }) } - -// 试验区域-饲养间列表 -export function public_roomList(query) { - return request({ - url: '/system/business/public/roomList', - method: 'get', - params: query - }) -} diff --git a/src/lang/en.js b/src/lang/en.js index 61ebf07..4abd805 100644 --- a/src/lang/en.js +++ b/src/lang/en.js @@ -27,8 +27,7 @@ import studyFormFill from './en/business/study/studyFormFill' import studyFormPlan from './en/business/study/studyFormPlan' //申请表单 import studyFormApply from './en/business/study/studyFormApply' -// 饲养间 -import studyRoom from './en/business/study/studyRoom' + //表单 import form from './en/business/form/form' @@ -145,7 +144,7 @@ export default { studyFormFill: studyFormFill, studyFormPlan: studyFormPlan, studyFormApply: studyFormApply, - studyRoom: studyRoom, + nonTrial: nonTrial, drug: drug }, diff --git a/src/lang/zh.js b/src/lang/zh.js index b35cdd1..c70a6a8 100644 --- a/src/lang/zh.js +++ b/src/lang/zh.js @@ -27,8 +27,6 @@ import studyFormFill from './zh/business/study/studyFormFill' import studyFormPlan from './zh/business/study/studyFormPlan' //申请表单 import studyFormApply from './zh/business/study/studyFormApply' -//饲养间 -import studyRoom from './zh/business/study/studyRoom' //表单 import form from './zh/business/form/form' @@ -142,7 +140,7 @@ export default { studyFormFill: studyFormFill, studyFormPlan: studyFormPlan, studyFormApply: studyFormApply, - studyRoom: studyRoom, + nonTrial: nonTrial, drug: drug }, diff --git a/src/permission.js b/src/permission.js index b66190b..511cae5 100644 --- a/src/permission.js +++ b/src/permission.js @@ -9,7 +9,7 @@ import { isRelogin } from '@/utils/request' NProgress.configure({ showSpinner: false }) -const whiteList = ['/login', '/register'] +const whiteList = ['/login', '/lblh'] const isWhiteList = (path) => { return whiteList.some(pattern => isPathMatch(pattern, path)) diff --git a/src/router/index.js b/src/router/index.js index 2c37710..c7db4a8 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -46,6 +46,11 @@ export const constantRoutes = [ component: () => import('@/views/login'), hidden: true }, + { + path: '/lblh', + component: () => import('@/views/lblh/index'), + hidden: true + }, { path: '/register', component: () => import('@/views/register'), diff --git a/src/views/business/study/comp/enter.vue b/src/views/business/study/comp/enter.vue index 2de28e7..af54ac1 100644 --- a/src/views/business/study/comp/enter.vue +++ b/src/views/business/study/comp/enter.vue @@ -14,7 +14,6 @@ - @@ -26,11 +25,10 @@ import tbbd from './tbbd.vue' import syxx from './syxx.vue' import syff from './syff.vue' import wzlb from './wzlb.vue' -import syj from './syj.vue' export default { name: 'StudyEnter', props: {}, - components: { ytbd, tbbd ,syxx,syff,wzlb, syj}, + components: { ytbd, tbbd ,syxx,syff,wzlb}, computed: {}, filters: {}, data() { @@ -49,7 +47,7 @@ export default { study: {}, } }, - created() { + created() { // let x = this.$route.params // let y = this.$route.query // debugger diff --git a/src/views/lblh/components/AutoSaveManager.js b/src/views/lblh/components/AutoSaveManager.js new file mode 100644 index 0000000..f5fc5d5 --- /dev/null +++ b/src/views/lblh/components/AutoSaveManager.js @@ -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} 历史版本列表 + */ + 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 = ` + 页面恢复
+ 检测到页面可能发生了异常,已自动恢复您的数据。 + `; + document.body.appendChild(notification); + + setTimeout(() => { + notification.remove(); + }, 5000); + } + + /** + * 获取备份文件列表 + * @returns {Promise} 备份文件列表 + */ + 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; \ No newline at end of file diff --git a/src/views/lblh/components/AutoSaveTextarea.vue b/src/views/lblh/components/AutoSaveTextarea.vue new file mode 100644 index 0000000..204d0fc --- /dev/null +++ b/src/views/lblh/components/AutoSaveTextarea.vue @@ -0,0 +1,1349 @@ + + + + + \ No newline at end of file diff --git a/src/views/lblh/components/storage/IndexedDBStorage.js b/src/views/lblh/components/storage/IndexedDBStorage.js new file mode 100644 index 0000000..e9cd95b --- /dev/null +++ b/src/views/lblh/components/storage/IndexedDBStorage.js @@ -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; \ No newline at end of file diff --git a/src/views/lblh/components/storage/LocalStorage.js b/src/views/lblh/components/storage/LocalStorage.js new file mode 100644 index 0000000..3cf477b --- /dev/null +++ b/src/views/lblh/components/storage/LocalStorage.js @@ -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} 保存结果 + */ + 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} 加载的数据 + */ + 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} 历史版本列表 + */ + 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} 备份文件列表 + */ + 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; \ No newline at end of file diff --git a/src/views/lblh/components/storage/MemoryStorage.js b/src/views/lblh/components/storage/MemoryStorage.js new file mode 100644 index 0000000..9a18337 --- /dev/null +++ b/src/views/lblh/components/storage/MemoryStorage.js @@ -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} 保存结果 + */ + 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} 加载的数据 + */ + 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} 历史版本列表 + */ + 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; \ No newline at end of file diff --git a/src/views/lblh/components/storage/ServerStorage.js b/src/views/lblh/components/storage/ServerStorage.js new file mode 100644 index 0000000..9425886 --- /dev/null +++ b/src/views/lblh/components/storage/ServerStorage.js @@ -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} 保存结果 + */ + 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} 加载的数据 + */ + 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} 历史版本列表 + */ + 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} 版本号 + */ + 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; \ No newline at end of file diff --git a/src/views/lblh/components/storage/SessionStorage.js b/src/views/lblh/components/storage/SessionStorage.js new file mode 100644 index 0000000..9176e51 --- /dev/null +++ b/src/views/lblh/components/storage/SessionStorage.js @@ -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} 保存结果 + */ + 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} 加载的数据 + */ + 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} 历史版本列表 + */ + 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; \ No newline at end of file diff --git a/src/views/lblh/index.vue b/src/views/lblh/index.vue new file mode 100644 index 0000000..a5a6c50 --- /dev/null +++ b/src/views/lblh/index.vue @@ -0,0 +1,252 @@ + + + + + \ No newline at end of file