diff --git a/supply.html b/supply.html index 774aed1..568f80a 100644 --- a/supply.html +++ b/supply.html @@ -1671,9 +1671,6 @@
- @@ -2696,16 +2693,61 @@ constructor() { this.cache = new Map(); this.defaultTTL = 5 * 60 * 1000; // 默认缓存5分钟 + this.maxLocalStorageSize = 4 * 1024 * 1024; // 本地存储最大4MB + this.cacheVersion = 'v2'; // 缓存版本号 + this.initialize(); + } + + // 初始化 + initialize() { + this.migrateCache(); + } + + // 缓存迁移 + migrateCache() { + const oldVersion = localStorage.getItem('cache_version'); + if (oldVersion !== this.cacheVersion) { + // 清理旧版本缓存 + for (let key in localStorage) { + if (key.startsWith('cache_') && !key.startsWith(`cache_${this.cacheVersion}_`)) { + localStorage.removeItem(key); + } + } + localStorage.setItem('cache_version', this.cacheVersion); + } + } + + // 获取带版本的缓存键 + getVersionedKey(key) { + return `${this.cacheVersion}_${key}`; } // 设置缓存 set(key, value, ttl = this.defaultTTL) { + const versionedKey = this.getVersionedKey(key); const item = { value, - expiry: Date.now() + ttl + expiry: Date.now() + ttl, + _timestamp: Date.now() // 添加时间戳用于排序 }; + + // 更新内存缓存 this.cache.set(key, item); - localStorage.setItem(`cache_${key}`, JSON.stringify(item)); + + // 更新本地存储缓存 + try { + this.ensureStorageSpace(); + localStorage.setItem(`cache_${versionedKey}`, JSON.stringify(item)); + } catch (error) { + console.error('存储缓存数据失败:', error); + // 存储失败时清理部分缓存 + this.clearOldCache(); + try { + localStorage.setItem(`cache_${versionedKey}`, JSON.stringify(item)); + } catch (e) { + console.error('清理缓存后仍无法存储:', e); + } + } } // 获取缓存 @@ -2715,13 +2757,15 @@ // 如果内存缓存不存在,从localStorage获取 if (!item) { - const stored = localStorage.getItem(`cache_${key}`); + const versionedKey = this.getVersionedKey(key); + const stored = localStorage.getItem(`cache_${versionedKey}`); if (stored) { try { item = JSON.parse(stored); this.cache.set(key, item); } catch (e) { console.error('解析缓存失败:', e); + localStorage.removeItem(`cache_${versionedKey}`); } } } @@ -2739,7 +2783,8 @@ // 删除缓存 delete(key) { this.cache.delete(key); - localStorage.removeItem(`cache_${key}`); + const versionedKey = this.getVersionedKey(key); + localStorage.removeItem(`cache_${versionedKey}`); } // 清除所有缓存 @@ -2760,12 +2805,64 @@ this.cache.delete(key); } } + const versionedPrefix = this.getVersionedKey(prefix); for (let key in localStorage) { - if (key.startsWith(`cache_${prefix}`)) { + if (key.startsWith(`cache_${versionedPrefix}`)) { localStorage.removeItem(key); } } } + + // 确保存储空间 + ensureStorageSpace() { + if (this.getLocalStorageSize() > this.maxLocalStorageSize) { + this.clearOldCache(); + } + } + + // 获取本地存储大小 + getLocalStorageSize() { + let size = 0; + for (let key in localStorage) { + if (localStorage.hasOwnProperty(key)) { + size += localStorage[key].length; + } + } + return size; + } + + // 清理旧缓存 + clearOldCache() { + const cacheKeys = []; + + // 收集所有缓存键和时间戳 + for (let key in localStorage) { + if (key.startsWith('cache_')) { + try { + const stored = localStorage.getItem(key); + const item = JSON.parse(stored); + cacheKeys.push({ + key, + timestamp: item._timestamp || 0 + }); + } catch (error) { + // 解析失败的缓存直接删除 + localStorage.removeItem(key); + } + } + } + + // 按时间戳排序,删除最旧的缓存 + cacheKeys.sort((a, b) => a.timestamp - b.timestamp); + + // 删除一半最旧的缓存 + const deleteCount = Math.ceil(cacheKeys.length / 2); + for (let i = 0; i < deleteCount; i++) { + if (cacheKeys[i]) { + localStorage.removeItem(cacheKeys[i].key); + } + } + } } // 实例化缓存管理器 @@ -2982,15 +3079,23 @@ let ws = null; let wsReconnectAttempts = 0; const maxReconnectAttempts = 5; - const reconnectDelay = 3000; // 3秒 + const initialReconnectDelay = 1000; // 初始重连延迟1秒 + const maxReconnectDelay = 30000; // 最大重连延迟30秒 // 保存定时器引用,方便后续清理 let timers = { loadSupplies: null, updateCountdowns: null, - checkAutoOffline: null + checkAutoOffline: null, + heartbeat: null, + heartbeatTimeout: null }; + // WebSocket心跳配置 + const heartbeatInterval = 30000; // 心跳间隔30秒 + const heartbeatTimeout = 15000; // 心跳超时时间15秒 + let lastHeartbeatTime = 0; // 最后一次心跳时间 + // 防止loadSupplies并发执行的标志 let isLoadingSupplies = false; @@ -3113,6 +3218,8 @@ userId: userInfo.userId || userInfo.id })); } + // 启动心跳机制 + startHeartbeat(); }; // 消息接收事件 @@ -3128,6 +3235,8 @@ // 连接关闭事件 ws.onclose = function(event) { console.log('WebSocket连接已关闭:', event.code, event.reason); + // 停止心跳机制 + stopHeartbeat(); // 只有在非正常关闭时才重连 // 正常关闭码1000表示主动关闭,不需要重连 if (event.code !== 1000) { @@ -3151,13 +3260,126 @@ function attemptReconnect() { if (wsReconnectAttempts < maxReconnectAttempts) { wsReconnectAttempts++; - console.log(`尝试重新连接WebSocket (${wsReconnectAttempts}/${maxReconnectAttempts})...`); - setTimeout(initWebSocket, reconnectDelay); + // 计算指数退避延迟:初始延迟 * 2^(重连次数-1),但不超过最大延迟 + const delay = Math.min(initialReconnectDelay * Math.pow(2, wsReconnectAttempts - 1), maxReconnectDelay); + console.log(`尝试重新连接WebSocket (${wsReconnectAttempts}/${maxReconnectAttempts}),延迟 ${delay}ms...`); + setTimeout(initWebSocket, delay); } else { console.error('WebSocket重连失败,已达到最大重试次数'); + // 30秒后重置重连计数,允许再次尝试 + setTimeout(() => { + wsReconnectAttempts = 0; + console.log('WebSocket重连计数已重置,可再次尝试连接'); + }, 30000); } } + // 获取当前用户的基础缓存键 + function getBaseCacheKey() { + const userInfo = checkLogin(); + if (!userInfo) return null; + return `supplies_${userInfo.projectName === '管理员' ? 'admin' : userInfo.userId}`; + } + + // 获取完整货源列表的缓存键 + function getSuppliesListCacheKey() { + const baseKey = getBaseCacheKey(); + return baseKey ? `${baseKey}_list` : null; + } + + // 获取单个货源的缓存键 + function getSupplyCacheKey(supplyId) { + const baseKey = getBaseCacheKey(); + return baseKey ? `${baseKey}_supply_${supplyId}` : null; + } + + // 获取特定状态货源的缓存键 + function getSuppliesByStatusCacheKey(status) { + const baseKey = getBaseCacheKey(); + return baseKey ? `${baseKey}_status_${status}` : null; + } + + // 获取搜索结果的缓存键 + function getSearchCacheKey(keyword) { + const baseKey = getBaseCacheKey(); + return baseKey ? `${baseKey}_search_${encodeURIComponent(keyword)}` : null; + } + + // 更新缓存中的单个货源数据 + function updateSupplyInCache(supplyId, updatedData) { + try { + // 获取完整列表缓存键 + const listCacheKey = getSuppliesListCacheKey(); + if (!listCacheKey) { + console.error('无法获取缓存键,用户未登录'); + return null; + } + + // 获取单个货源缓存键 + const supplyCacheKey = getSupplyCacheKey(supplyId); + + // 1. 更新完整列表缓存 + const cachedSupplies = cacheManager.get(listCacheKey); + + if (cachedSupplies && Array.isArray(cachedSupplies)) { + const supplyIndex = cachedSupplies.findIndex(supply => String(supply.id) === String(supplyId)); + + if (supplyIndex !== -1) { + // 创建更新后的货源数据 + const updatedSupply = { ...cachedSupplies[supplyIndex], ...updatedData }; + // 创建新的货源数组,避免直接修改原数组 + const updatedSupplies = [...cachedSupplies]; + updatedSupplies[supplyIndex] = updatedSupply; + + // 更新完整列表缓存 + cacheManager.set(listCacheKey, updatedSupplies, 60 * 60 * 1000); + + // 2. 更新单个货源缓存 + if (supplyCacheKey) { + cacheManager.set(supplyCacheKey, updatedSupply, 60 * 60 * 1000); + } + + // 3. 如果状态发生变化,清除状态相关的缓存 + if (updatedData.status) { + const oldStatus = cachedSupplies[supplyIndex].status; + if (oldStatus !== updatedData.status) { + // 清除旧状态和新状态的缓存 + const oldStatusCacheKey = getSuppliesByStatusCacheKey(oldStatus); + const newStatusCacheKey = getSuppliesByStatusCacheKey(updatedData.status); + if (oldStatusCacheKey) cacheManager.delete(oldStatusCacheKey); + if (newStatusCacheKey) cacheManager.delete(newStatusCacheKey); + // 清除搜索结果缓存,因为状态变化可能影响搜索 + clearSearchCache(); + } + } + + console.log('缓存中的货源数据已更新:', supplyId, '更新字段:', Object.keys(updatedData)); + return updatedSupplies; + } else { + console.warn('在缓存中未找到货源:', supplyId); + return null; + } + } else { + console.warn('缓存中无货源数据或数据格式不正确'); + return null; + } + } catch (error) { + console.error('更新缓存中的货源数据失败:', error); + return null; + } + } + + // 清除搜索结果缓存 + function clearSearchCache() { + const baseKey = getBaseCacheKey(); + if (!baseKey) return; + + // 清除所有搜索相关的缓存 + const searchPrefix = `${baseKey}_search_`; + cacheManager.clearByPrefix(searchPrefix); + console.log('搜索结果缓存已清除'); + } + // 处理WebSocket消息 function handleWebSocketMessage(message) { try { @@ -3167,24 +3389,101 @@ switch (data.type) { case 'supply_update': - // 货源更新通知,重新加载数据 - console.log('收到货源更新通知,重新加载数据'); - loadSupplies(true); // 强制刷新,跳过缓存 + // 货源更新通知 + if (data.supplyId && data.updatedData) { + // 增量更新缓存 + console.log('收到货源更新通知,执行增量更新:', data.supplyId); + try { + const updatedSupplies = updateSupplyInCache(data.supplyId, data.updatedData); + if (updatedSupplies) { + // 更新内存中的数据 + supplyData.supplies = updatedSupplies; + processSupplyData(updatedSupplies); + renderSupplyLists(); + } else { + // 缓存更新失败,重新加载数据 + console.warn('缓存更新失败,执行全量加载'); + loadSupplies(true); + } + } catch (error) { + console.error('处理货源更新通知失败:', error); + // 发生错误时,重新加载数据 + loadSupplies(true); + } + } else { + // 没有具体的更新数据,重新加载 + console.log('收到货源更新通知,重新加载数据'); + loadSupplies(true); + } break; case 'supply_lock': // 货源锁定状态更新 - console.log('收到货源锁定状态更新,重新加载数据'); - loadSupplies(true); // 强制刷新,跳过缓存 + if (data.supplyId && data.locked !== undefined) { + console.log('收到货源锁定状态更新,执行增量更新:', data.supplyId, '锁定状态:', data.locked); + try { + const updatedSupplies = updateSupplyInCache(data.supplyId, { locked: data.locked }); + if (updatedSupplies) { + supplyData.supplies = updatedSupplies; + processSupplyData(updatedSupplies); + renderSupplyLists(); + } else { + console.warn('缓存更新失败,执行全量加载'); + loadSupplies(true); + } + } catch (error) { + console.error('处理货源锁定状态更新失败:', error); + loadSupplies(true); + } + } else { + console.log('收到货源锁定状态更新,重新加载数据'); + loadSupplies(true); + } break; case 'supply_status_change': // 货源状态变更 - console.log('收到货源状态变更,重新加载数据'); - loadSupplies(true); // 强制刷新,跳过缓存 + if (data.supplyId && data.status) { + console.log('收到货源状态变更,执行增量更新:', data.supplyId, '新状态:', data.status); + try { + const updatedSupplies = updateSupplyInCache(data.supplyId, { status: data.status }); + if (updatedSupplies) { + supplyData.supplies = updatedSupplies; + processSupplyData(updatedSupplies); + renderSupplyLists(); + } else { + console.warn('缓存更新失败,执行全量加载'); + loadSupplies(true); + } + } catch (error) { + console.error('处理货源状态变更失败:', error); + loadSupplies(true); + } + } else { + console.log('收到货源状态变更,重新加载数据'); + loadSupplies(true); + } break; case 'auto_offline': // 自动下架通知 - console.log('收到自动下架通知,重新加载数据'); - loadSupplies(true); // 强制刷新,跳过缓存 + if (data.supplyId) { + console.log('收到自动下架通知,执行增量更新:', data.supplyId); + try { + const updatedSupplies = updateSupplyInCache(data.supplyId, { status: 'draft' }); + if (updatedSupplies) { + supplyData.supplies = updatedSupplies; + processSupplyData(updatedSupplies); + renderSupplyLists(); + } else { + console.warn('缓存更新失败,执行全量加载'); + loadSupplies(true); + } + } catch (error) { + console.error('处理自动下架通知失败:', error); + loadSupplies(true); + } + } else { + console.log('收到自动下架通知,重新加载数据'); + loadSupplies(true); + } break; case 'ping': // 心跳响应 @@ -3192,6 +3491,15 @@ ws.send(JSON.stringify({ type: 'pong' })); } break; + case 'pong': + // 收到心跳响应 + console.log('收到WebSocket心跳响应'); + // 清除心跳超时定时器 + if (timers.heartbeatTimeout) { + clearTimeout(timers.heartbeatTimeout); + timers.heartbeatTimeout = null; + } + break; default: console.log('未知的WebSocket消息类型:', data.type); } @@ -3209,6 +3517,53 @@ } } + // 发送心跳 + function sendHeartbeat() { + if (ws && ws.readyState === WebSocket.OPEN) { + console.log('发送WebSocket心跳'); + sendWebSocketMessage({ type: 'ping' }); + lastHeartbeatTime = Date.now(); + + // 设置心跳超时定时器 + if (timers.heartbeatTimeout) { + clearTimeout(timers.heartbeatTimeout); + } + timers.heartbeatTimeout = setTimeout(() => { + console.error('WebSocket心跳超时,连接可能已断开'); + // 心跳超时,主动关闭连接并尝试重连 + if (ws) { + ws.close(1006, 'Heartbeat timeout'); + } + }, heartbeatTimeout); + } + } + + // 启动心跳 + function startHeartbeat() { + // 停止已有的心跳 + stopHeartbeat(); + + // 立即发送一次心跳 + sendHeartbeat(); + + // 设置心跳间隔 + timers.heartbeat = setInterval(sendHeartbeat, heartbeatInterval); + console.log('WebSocket心跳机制已启动,间隔', heartbeatInterval, 'ms'); + } + + // 停止心跳 + function stopHeartbeat() { + if (timers.heartbeat) { + clearInterval(timers.heartbeat); + timers.heartbeat = null; + } + if (timers.heartbeatTimeout) { + clearTimeout(timers.heartbeatTimeout); + timers.heartbeatTimeout = null; + } + console.log('WebSocket心跳机制已停止'); + } + // 移除复杂的触摸事件处理,使用浏览器默认滚动行为 function preventIOSDrag() { // 不再阻止任何默认行为,让浏览器处理滚动 @@ -5205,6 +5560,16 @@ try { console.log('开始加载联系人数据...'); + + // 尝试从缓存加载 + const cachedContacts = cacheManager.get('contacts'); + if (cachedContacts) { + contacts = cachedContacts; + console.log('从缓存加载联系人数据,共', contacts.length, '个联系人'); + updateContactSelects(); + return; + } + const response = await fetch('/api/contacts'); if (!response.ok) { throw new Error(`服务器响应异常: ${response.status} ${response.statusText}`); @@ -5215,36 +5580,20 @@ contacts = result.data || []; console.log('联系人数据加载成功,共', contacts.length, '个联系人:', contacts); - // 保存到本地缓存,添加时间戳和版本号 - const contactsCache = { - data: contacts, - version: '1.0', - timestamp: Date.now() - }; - localStorage.setItem('contactsCache', JSON.stringify(contactsCache)); + // 保存到缓存,设置7天过期时间 + cacheManager.set('contacts', contacts, 7 * 24 * 60 * 60 * 1000); updateContactSelects(); } catch (error) { console.error('加载联系人数据失败:', error); - // 尝试从本地缓存加载 - try { - const cachedContacts = localStorage.getItem('contactsCache'); - if (cachedContacts) { - const contactsCache = JSON.parse(cachedContacts); - // 检查缓存是否有效(7天内) - const cacheExpiry = 7 * 24 * 60 * 60 * 1000; - if (Date.now() - contactsCache.timestamp < cacheExpiry) { - contacts = contactsCache.data || []; - console.log('从本地缓存加载联系人数据,共', contacts.length, '个联系人:', contacts); - updateContactSelects(); - return; - } else { - console.log('联系人缓存已过期'); - } - } - } catch (cacheError) { - console.error('加载联系人缓存失败:', cacheError); + // 尝试从缓存加载 + const cachedContacts = cacheManager.get('contacts'); + if (cachedContacts) { + contacts = cachedContacts; + console.log('API失败,从缓存加载联系人数据,共', contacts.length, '个联系人'); + updateContactSelects(); + return; } // 出错且无有效缓存时,将contacts设为空数组 @@ -5303,7 +5652,8 @@ if (!userInfo) return; // 生成缓存键 - const cacheKey = `supplies_cache_${userInfo.projectName === '管理员' ? 'admin' : userInfo.userId}`; + const cacheKey = getSuppliesListCacheKey(); + if (!cacheKey) return; // 重置所有状态的分页数据 supplyData.pagination = { @@ -5321,20 +5671,14 @@ draft: [] }; - // 尝试从本地缓存获取数据 + // 尝试从缓存获取数据 if (!forceRefresh) { - const cachedData = localStorage.getItem(cacheKey); + const cachedData = cacheManager.get(cacheKey); if (cachedData) { - try { - const supplies = JSON.parse(cachedData); - console.log('从本地缓存加载货源数据:', supplies.length); - processSupplyData(supplies); - renderSupplyLists(); - return; - } catch (error) { - console.error('解析缓存数据失败:', error); - localStorage.removeItem(cacheKey); - } + console.log('从缓存加载货源数据:', cachedData.length); + processSupplyData(cachedData); + renderSupplyLists(); + return; } } @@ -5357,9 +5701,18 @@ if (result.success) { console.log('加载到的货源数量:', result.data.list.length); - // 存储到本地缓存 - localStorage.setItem(cacheKey, JSON.stringify(result.data.list)); - console.log('货源数据已存储到本地缓存'); + // 存储到缓存,设置1小时过期时间 + cacheManager.set(cacheKey, result.data.list, 60 * 60 * 1000); + + // 同时为每个货源创建单独的缓存 + result.data.list.forEach(supply => { + const supplyCacheKey = getSupplyCacheKey(supply.id); + if (supplyCacheKey) { + cacheManager.set(supplyCacheKey, supply, 60 * 60 * 1000); + } + }); + + console.log('货源数据已存储到缓存'); processSupplyData(result.data.list); renderSupplyLists(); @@ -5368,20 +5721,13 @@ console.error('加载货源失败:', error); // 尝试从缓存加载(如果有) - const userInfo = checkLogin(); - if (userInfo) { - const cacheKey = `supplies_cache_${userInfo.projectName === '管理员' ? 'admin' : userInfo.userId}`; - const cachedData = localStorage.getItem(cacheKey); + const cacheKey = getSuppliesListCacheKey(); + if (cacheKey) { + const cachedData = cacheManager.get(cacheKey); if (cachedData) { - try { - const supplies = JSON.parse(cachedData); - console.log('API失败,从本地缓存加载货源数据:', supplies.length); - processSupplyData(supplies); - renderSupplyLists(); - } catch (cacheError) { - console.error('解析缓存数据失败:', cacheError); - localStorage.removeItem(cacheKey); - } + console.log('API失败,从缓存加载货源数据:', cachedData.length); + processSupplyData(cachedData); + renderSupplyLists(); } } } finally { @@ -5474,6 +5820,11 @@ renderSupplyList('pending', supplyData.pendingSupplies); renderSupplyList('rejected', supplyData.rejectedSupplies); renderSupplyList('draft', supplyData.draftSupplies); + + // 渲染完成后检查并加载可见的懒加载图片 + setTimeout(() => { + checkLazyLoadImages(); + }, 100); } // 渲染单个货源列表