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);
}
// 渲染单个货源列表