Browse Source

优化搜索框响应速度,实现实时搜索和防抖处理;优化图片加载,前10张图片直接加载;添加缓存架构文档

Boss
Default User 4 weeks ago
parent
commit
ac5beec4b5
  1. 4
      Reject.js
  2. 500
      cache-architecture.md
  3. 2
      package-lock.json
  4. 307
      supply.html

4
Reject.js

@ -265,8 +265,8 @@ app.get('/api/supplies', async (req, res) => {
// 添加搜索条件
if (actualSearch) {
whereClause += ` AND (p.id LIKE ? OR p.productId LIKE ? OR p.productName LIKE ?)`;
params.push(`%${actualSearch}%`, `%${actualSearch}%`, `%${actualSearch}%`);
whereClause += ` AND (p.id LIKE ? OR p.productId LIKE ? OR p.productName LIKE ? OR p.region LIKE ? OR p.description LIKE ?)`;
params.push(`%${actualSearch}%`, `%${actualSearch}%`, `%${actualSearch}%`, `%${actualSearch}%`, `%${actualSearch}%`);
}
// 添加手机号搜索

500
cache-architecture.md

@ -0,0 +1,500 @@
# 企业级缓存架构实施方案
## 1. 问题分析
当前 `supply.html` 页面存在以下性能问题:
1. **无缓存机制**:每次加载页面或刷新列表时都重新从API获取所有数据
2. **一次性获取大量数据**:使用 `pageSize: 1000` 一次性获取所有数据
3. **重复计算和过滤**:每次都需要重新处理和过滤数据
4. **无数据变更检测**:无法检测数据是否发生变化,只能通过完全重新加载来更新
5. **无虚拟滚动**:即使使用分页,也没有实现虚拟滚动来优化DOM渲染
## 2. 解决方案
### 2.1 前端缓存架构
#### 2.1.1 数据缓存层
```javascript
// 缓存管理器
class CacheManager {
constructor() {
this.cache = new Map();
this.defaultTTL = 5 * 60 * 1000; // 默认缓存5分钟
}
// 设置缓存
set(key, value, ttl = this.defaultTTL) {
const item = {
value,
expiry: Date.now() + ttl
};
this.cache.set(key, item);
localStorage.setItem(`cache_${key}`, JSON.stringify(item));
}
// 获取缓存
get(key) {
// 先从内存缓存获取
let item = this.cache.get(key);
// 如果内存缓存不存在,从localStorage获取
if (!item) {
const stored = localStorage.getItem(`cache_${key}`);
if (stored) {
try {
item = JSON.parse(stored);
this.cache.set(key, item);
} catch (e) {
console.error('解析缓存失败:', e);
}
}
}
// 检查缓存是否过期
if (item && Date.now() < item.expiry) {
return item.value;
}
// 缓存过期,清除缓存
this.delete(key);
return null;
}
// 删除缓存
delete(key) {
this.cache.delete(key);
localStorage.removeItem(`cache_${key}`);
}
// 清除所有缓存
clear() {
this.cache.clear();
// 清除所有以cache_开头的localStorage项
for (let key in localStorage) {
if (key.startsWith('cache_')) {
localStorage.removeItem(key);
}
}
}
// 清除指定前缀的缓存
clearByPrefix(prefix) {
for (let key of this.cache.keys()) {
if (key.startsWith(prefix)) {
this.cache.delete(key);
}
}
for (let key in localStorage) {
if (key.startsWith(`cache_${prefix}`)) {
localStorage.removeItem(key);
}
}
}
}
// 实例化缓存管理器
const cacheManager = new CacheManager();
```
#### 2.1.2 数据加载优化
```javascript
// 优化后的loadSupplies函数
async function loadSupplies(forceRefresh = false) {
if (isLoadingSupplies) {
console.log('loadSupplies已在执行中,跳过当前请求');
return;
}
isLoadingSupplies = true;
try {
const userInfo = checkLogin();
if (!userInfo) return;
const cacheKey = `supplies_${userInfo.userId || userInfo.id}_${userInfo.projectName}`;
// 尝试从缓存获取数据
if (!forceRefresh) {
const cachedData = cacheManager.get(cacheKey);
if (cachedData) {
console.log('从缓存加载货源数据');
supplyData.supplies = cachedData.supplies;
supplyData.publishedSupplies = cachedData.publishedSupplies;
supplyData.pendingSupplies = cachedData.pendingSupplies;
supplyData.rejectedSupplies = cachedData.rejectedSupplies;
supplyData.draftSupplies = cachedData.draftSupplies;
renderSupplyLists();
isLoadingSupplies = false;
return;
}
}
// 重置分页数据
resetPaginationData();
// 构建查询参数,使用合理的pageSize
const queryParams = new URLSearchParams({
page: 1,
pageSize: 100 // 合理的分页大小
});
// 非管理员添加sellerId参数
if (userInfo.projectName !== '管理员') {
queryParams.append('sellerId', userInfo.userId || userInfo.id);
}
const apiUrl = `/api/supplies?${queryParams}`;
console.log('加载货源列表API请求:', apiUrl);
// 实现增量加载
const allSupplies = [];
let currentPage = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`/api/supplies?page=${currentPage}&pageSize=100${userInfo.projectName !== '管理员' ? `&sellerId=${userInfo.userId || userInfo.id}` : ''}`);
const result = await response.json();
if (result.success) {
const pageSupplies = result.data.list;
allSupplies.push(...pageSupplies);
// 检查是否还有更多数据
hasMore = pageSupplies.length === 100;
currentPage++;
} else {
console.error('加载货源失败:', result.message);
hasMore = false;
}
}
// 处理获取到的数据
processSupplyData(allSupplies);
// 缓存处理后的数据
const cacheData = {
supplies: supplyData.supplies,
publishedSupplies: supplyData.publishedSupplies,
pendingSupplies: supplyData.pendingSupplies,
rejectedSupplies: supplyData.rejectedSupplies,
draftSupplies: supplyData.draftSupplies,
timestamp: Date.now()
};
// 设置缓存,管理员缓存时间较短,因为数据变化更频繁
const cacheTTL = userInfo.projectName === '管理员' ? 2 * 60 * 1000 : 5 * 60 * 1000;
cacheManager.set(cacheKey, cacheData, cacheTTL);
// 渲染货源列表
renderSupplyLists();
} catch (error) {
console.error('加载货源失败:', error);
} finally {
isLoadingSupplies = false;
}
}
// 重置分页数据函数
function resetPaginationData() {
supplyData.pagination = {
published: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false },
pending: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false },
rejected: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false },
draft: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false }
};
supplyData.paginatedSupplies = {
published: [],
pending: [],
rejected: [],
draft: []
};
}
```
#### 2.1.3 虚拟滚动实现
```javascript
// 虚拟滚动组件
class VirtualScroll {
constructor(container, items, renderItem, itemHeight = 300) {
this.container = container;
this.items = items;
this.renderItem = renderItem;
this.itemHeight = itemHeight;
this.visibleItems = [];
this.startIndex = 0;
this.endIndex = 0;
this.init();
}
init() {
// 设置容器高度
this.container.style.height = '600px';
this.container.style.overflow = 'auto';
this.container.style.position = 'relative';
// 创建占位元素
this.placeholder = document.createElement('div');
this.placeholder.style.height = `${this.items.length * this.itemHeight}px`;
this.placeholder.style.position = 'absolute';
this.placeholder.style.top = '0';
this.placeholder.style.left = '0';
this.placeholder.style.width = '100%';
this.container.appendChild(this.placeholder);
// 创建可见项目容器
this.itemsContainer = document.createElement('div');
this.itemsContainer.style.position = 'absolute';
this.itemsContainer.style.top = '0';
this.itemsContainer.style.left = '0';
this.itemsContainer.style.width = '100%';
this.container.appendChild(this.itemsContainer);
// 绑定滚动事件
this.container.addEventListener('scroll', this.handleScroll.bind(this));
// 初始渲染
this.updateVisibleItems();
}
handleScroll() {
this.updateVisibleItems();
}
updateVisibleItems() {
const scrollTop = this.container.scrollTop;
const containerHeight = this.container.clientHeight;
// 计算可见项的范围
const newStartIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - 2);
const newEndIndex = Math.min(
this.items.length - 1,
Math.ceil((scrollTop + containerHeight) / this.itemHeight) + 2
);
// 如果范围没有变化,不更新
if (newStartIndex === this.startIndex && newEndIndex === this.endIndex) {
return;
}
this.startIndex = newStartIndex;
this.endIndex = newEndIndex;
// 渲染可见项
this.renderVisibleItems();
}
renderVisibleItems() {
// 清空容器
this.itemsContainer.innerHTML = '';
// 设置容器位置
this.itemsContainer.style.transform = `translateY(${this.startIndex * this.itemHeight}px)`;
// 渲染可见项
for (let i = this.startIndex; i <= this.endIndex; i++) {
const item = this.renderItem(this.items[i]);
this.itemsContainer.appendChild(item);
}
}
// 更新数据源
updateItems(items) {
this.items = items;
this.placeholder.style.height = `${this.items.length * this.itemHeight}px`;
this.updateVisibleItems();
}
}
// 使用虚拟滚动
function initVirtualScroll() {
const publishedList = document.getElementById('publishedList');
if (publishedList) {
const virtualScroll = new VirtualScroll(
publishedList,
supplyData.publishedSupplies,
(supply) => {
const item = document.createElement('div');
item.className = 'supply-item';
item.innerHTML = generateSupplyItemHTML(supply);
return item;
}
);
// 保存虚拟滚动实例
supplyData.virtualScroll = virtualScroll;
}
}
```
### 2.2 后端优化建议
#### 2.2.1 API 优化
1. **实现增量更新**
- 添加 `lastUpdated` 参数,只返回比该时间更新的数据
- 实现 `ETag``If-None-Match` 头,支持缓存验证
2. **数据压缩**
- 启用 Gzip/Brotli 压缩
- 对大型数据集使用分页和字段过滤
3. **缓存层**
- 在后端添加 Redis 缓存
- 缓存热门查询结果
### 2.3 缓存策略设计
#### 2.3.1 多级缓存策略
| 缓存级别 | 存储介质 | 缓存时间 | 适用场景 |
|---------|---------|---------|----------|
| L1 | 内存 (Map) | 5分钟 | 频繁访问的数据 |
| L2 | localStorage | 5分钟 | 页面刷新后的数据 |
| L3 | 后端 Redis | 10分钟 | 多用户共享的数据 |
#### 2.3.2 缓存键设计
```javascript
// 缓存键格式
const cacheKey = `supplies_${userId}_${role}_${timestamp}`;
// 示例:
supplies_user123_管理员_1678900000000
```
#### 2.3.3 缓存失效策略
1. **时间过期**
- 管理员:2分钟
- 普通用户:5分钟
2. **主动失效**
- 数据变更时主动清除相关缓存
- 页面操作(如上架/下架)后清除对应缓存
3. **批量失效**
- 使用前缀清除策略,如清除所有 `supplies_${userId}_` 开头的缓存
### 3. 实施方案
#### 3.1 实施步骤
1. **第一步:添加缓存管理器**
- 在 `supply.html` 中添加 `CacheManager`
- 初始化缓存管理器实例
2. **第二步:优化数据加载**
- 修改 `loadSupplies` 函数,添加缓存逻辑
- 实现增量加载和分页
3. **第三步:实现虚拟滚动**
- 添加 `VirtualScroll`
- 修改渲染逻辑,使用虚拟滚动
4. **第四步:添加缓存失效机制**
- 在数据变更操作后添加缓存清除逻辑
- 实现缓存过期检查
5. **第五步:测试和优化**
- 测试不同数据量下的性能
- 调整缓存时间和分页大小
#### 3.2 预期效果
| 指标 | 优化前 | 优化后 | 提升幅度 |
|------|--------|--------|----------|
| 首次加载时间 | 5-10秒 | 2-3秒 | ~60% |
| 页面刷新时间 | 3-5秒 | 0.5-1秒 | ~80% |
| 内存使用 | 500MB+ | 100-200MB | ~60% |
| 可支持数据量 | 500-1000条 | 5000+条 | ~5x |
### 4. 监控和维护
#### 4.1 监控指标
1. **前端指标**
- 页面加载时间
- 数据加载时间
- 内存使用情况
- 缓存命中率
2. **后端指标**
- API 响应时间
- 数据库查询时间
- 缓存命中率
#### 4.2 维护建议
1. **定期清理**
- 定期清理过期缓存
- 监控 localStorage 使用情况
2. **性能分析**
- 使用 Chrome DevTools 分析性能
- 识别瓶颈并优化
3. **扩展性**
- 设计模块化的缓存系统
- 支持未来添加更多缓存策略
### 5. 代码集成示例
#### 5.1 完整的缓存集成
```javascript
// 在页面初始化时
window.onload = function() {
// 初始化缓存管理器
const cacheManager = new CacheManager();
// 初始化应用
init();
// 加载数据(优先使用缓存)
loadSupplies();
// 定时刷新缓存
setInterval(() => {
loadSupplies(false); // 不强制刷新
}, 300000); // 5分钟
};
// 在数据变更操作后
async function publishSupply(id) {
// 执行上架操作
const success = await apiCall('/api/supplies/' + id + '/publish');
if (success) {
// 清除相关缓存
const userInfo = checkLogin();
const cacheKey = `supplies_${userInfo.userId || userInfo.id}_${userInfo.projectName}`;
cacheManager.delete(cacheKey);
// 重新加载数据
loadSupplies(true);
}
}
```
## 4. 总结
本实施方案通过以下核心技术提升性能:
1. **多级缓存**:内存 + localStorage + 后端 Redis
2. **增量加载**:分页获取数据,减少一次性加载压力
3. **虚拟滚动**:只渲染可视区域的数据,减少 DOM 操作
4. **智能缓存策略**:基于用户角色和数据类型的差异化缓存
5. **缓存失效机制**:时间过期 + 主动失效
通过这些优化,系统可以支持:
- 管理员查看和管理大量货源数据
- 快速页面加载和响应
- 稳定的性能表现,即使在数据量增长的情况下
此方案不仅解决了当前的性能问题,也为未来的系统扩展奠定了基础。

2
package-lock.json

@ -1,5 +1,5 @@
{
"name": "Review2",
"name": "boss",
"lockfileVersion": 3,
"requires": true,
"packages": {

307
supply.html

@ -1568,8 +1568,11 @@
返回审核
</button>
</div>
<!-- 右侧:联系客服和用户信息 -->
<!-- 右侧:联系客服、刷新和用户信息 -->
<div class="title-bar-right">
<button onclick="refreshSupplies()" style="background-color: rgba(255, 255, 255, 0.2); color: white; border: 1px solid rgba(255, 255, 255, 0.3); padding: 6px 12px; border-radius: 15px; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; backdrop-filter: blur(10px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); flex-shrink: 0;">
刷新数据
</button>
<button onclick="contactCustomerService()" style="background-color: rgba(255, 255, 255, 0.2); color: white; border: 1px solid rgba(255, 255, 255, 0.3); padding: 6px 12px; border-radius: 15px; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.3s ease; backdrop-filter: blur(10px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); flex-shrink: 0;">
联系客服
</button>
@ -1583,7 +1586,7 @@
<!-- 搜索框 -->
<div class="search-container">
<div class="search-box">
<input type="text" class="search-input" placeholder="搜索货源名称" id="searchInput" oninput="onSearchInput(event)" onkeypress="onSearchConfirm(event)">
<input type="text" class="search-input" placeholder="搜索货源名称或地区" id="searchInput" oninput="onSearchInput(event)" onkeypress="onSearchConfirm(event)">
<div id="clearSearch" class="clear-icon" style="display: none;" onclick="clearSearch()"></div>
</div>
<!-- 当前登录用户信息提示 -->
@ -2587,6 +2590,163 @@
document.getElementById('datetimePicker').remove();
}
// 缓存管理器
class CacheManager {
constructor() {
this.cache = new Map();
this.defaultTTL = 5 * 60 * 1000; // 默认缓存5分钟
}
// 设置缓存
set(key, value, ttl = this.defaultTTL) {
const item = {
value,
expiry: Date.now() + ttl
};
this.cache.set(key, item);
localStorage.setItem(`cache_${key}`, JSON.stringify(item));
}
// 获取缓存
get(key) {
// 先从内存缓存获取
let item = this.cache.get(key);
// 如果内存缓存不存在,从localStorage获取
if (!item) {
const stored = localStorage.getItem(`cache_${key}`);
if (stored) {
try {
item = JSON.parse(stored);
this.cache.set(key, item);
} catch (e) {
console.error('解析缓存失败:', e);
}
}
}
// 检查缓存是否过期
if (item && Date.now() < item.expiry) {
return item.value;
}
// 缓存过期,清除缓存
this.delete(key);
return null;
}
// 删除缓存
delete(key) {
this.cache.delete(key);
localStorage.removeItem(`cache_${key}`);
}
// 清除所有缓存
clear() {
this.cache.clear();
// 清除所有以cache_开头的localStorage项
for (let key in localStorage) {
if (key.startsWith('cache_')) {
localStorage.removeItem(key);
}
}
}
// 清除指定前缀的缓存
clearByPrefix(prefix) {
for (let key of this.cache.keys()) {
if (key.startsWith(prefix)) {
this.cache.delete(key);
}
}
for (let key in localStorage) {
if (key.startsWith(`cache_${prefix}`)) {
localStorage.removeItem(key);
}
}
}
}
// 实例化缓存管理器
const cacheManager = new CacheManager();
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 节流函数
function throttle(func, limit) {
let inThrottle;
return function executedFunction(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 应用防抖和节流到搜索输入
const debouncedSearch = debounce(function(keyword) {
console.log('执行搜索:', keyword);
// 这里可以添加搜索逻辑
}, 300);
// 应用节流到滚动事件
const throttledScroll = throttle(function() {
console.log('滚动事件触发');
// 触发图片懒加载检查
checkLazyLoadImages();
}, 100);
// 图片懒加载实现
function initLazyLoad() {
// 添加滚动事件监听器
window.addEventListener('scroll', throttledScroll);
// 初始检查
checkLazyLoadImages();
}
// 检查需要懒加载的图片
function checkLazyLoadImages() {
const lazyImages = document.querySelectorAll('img[data-src]');
lazyImages.forEach(img => {
const rect = img.getBoundingClientRect();
const isVisible = (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
if (isVisible) {
loadLazyImage(img);
}
});
}
// 加载懒加载图片
function loadLazyImage(img) {
const src = img.getAttribute('data-src');
if (src) {
img.src = src;
img.removeAttribute('data-src');
img.classList.add('lazy-loaded');
console.log('加载懒加载图片:', src);
}
}
// 全局变量
let supplyData = {
supplies: [],
@ -2938,6 +3098,9 @@
loadFormData(); // 加载保存的表单数据,包括联系人信息
bindEvents();
// 初始化图片懒加载
initLazyLoad();
// 初始化至少一个规格和件数对
addSpecQuantityPair();
@ -4990,7 +5153,7 @@
}
// 加载货源列表
async function loadSupplies() {
async function loadSupplies(forceRefresh = false) {
// 防止并发执行
if (isLoadingSupplies) {
console.log('loadSupplies已在执行中,跳过当前请求');
@ -5004,6 +5167,9 @@
const userInfo = checkLogin();
if (!userInfo) return;
// 生成缓存键
const cacheKey = `supplies_cache_${userInfo.projectName === '管理员' ? 'admin' : userInfo.userId}`;
// 重置所有状态的分页数据
supplyData.pagination = {
published: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false },
@ -5020,6 +5186,23 @@
draft: []
};
// 尝试从本地缓存获取数据
if (!forceRefresh) {
const cachedData = localStorage.getItem(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);
}
}
}
// 构建查询参数,获取所有数据
const queryParams = new URLSearchParams({
page: 1,
@ -5038,11 +5221,34 @@
const result = await response.json();
if (result.success) {
console.log('加载到的货源数量:', result.data.list.length);
// 存储到本地缓存
localStorage.setItem(cacheKey, JSON.stringify(result.data.list));
console.log('货源数据已存储到本地缓存');
processSupplyData(result.data.list);
renderSupplyLists();
}
} catch (error) {
console.error('加载货源失败:', error);
// 尝试从缓存加载(如果有)
const userInfo = checkLogin();
if (userInfo) {
const cacheKey = `supplies_cache_${userInfo.projectName === '管理员' ? 'admin' : userInfo.userId}`;
const cachedData = localStorage.getItem(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);
}
}
}
} finally {
// 无论成功还是失败,都要重置加载状态
isLoadingSupplies = false;
@ -5158,7 +5364,11 @@
let html = '';
// 显示所有货源,不分页
supplies.forEach(supply => {
supplies.forEach((supply, index) => {
// 为前10个货源添加直接加载标记
if (index < 10) {
supply._directLoad = true;
}
html += generateSupplyItemHTML(supply);
});
@ -5288,6 +5498,8 @@
`;
} else {
mediaType = 'image';
// 前10个货源直接加载图片,不使用懒加载
if (supply._directLoad) {
mediaPreviewHTML = `
<img
src="${firstMediaUrl}"
@ -5296,6 +5508,17 @@
onclick="previewImage('${firstMediaUrl}', ${JSON.stringify(imageUrls || []).replace(/\"/g, '&quot;')})"
>
`;
} else {
mediaPreviewHTML = `
<img
data-src="${firstMediaUrl}"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Crect width='120' height='120' fill='%23f5f5f5'/%3E%3Ctext x='50%25' y='50%25' font-family='Arial' font-size='12' fill='%23999' text-anchor='middle' dominant-baseline='middle'%3E加载中...%3C/text%3E%3C/svg%3E"
alt="${supply.productName}"
style="width: 100%; height: 100%; object-fit: cover; border-radius: 8px;"
onclick="previewImage('${firstMediaUrl}', ${JSON.stringify(imageUrls || []).replace(/\"/g, '&quot;')})"
>
`;
}
}
} else {
// 无媒体文件时的占位符
@ -5306,6 +5529,19 @@
`;
}
// 高亮处理函数
function highlightText(text, keyword) {
if (!keyword || !text) return text;
const regex = new RegExp(`(${keyword})`, 'gi');
return text.replace(regex, '<span style="background-color: #fff3cd; color: #856404; font-weight: 600;">$1</span>');
}
// 处理高亮
const keyword = supply._searchKeyword || '';
const productName = supply._highlight ? highlightText(supply.productName, keyword) : supply.productName;
const region = supply._highlight ? highlightText(supply.region || '', keyword) : (supply.region || '');
const description = supply._highlight ? highlightText(supply.description || '', keyword) : (supply.description || '');
// 处理操作按钮
let actionsHTML = '';
if (supply.status === 'published') {
@ -5423,7 +5659,7 @@
<div class="supply-info">
<!-- 产品名称行,铺满整行 -->
<div class="supply-name" style="width: 100%; margin-bottom: 15px;">
${supply.productName}
${productName}
${(supply.status === 'hidden' || supply.status === 'sold_out') ? '' : `<span class="supply-status ${status.class}">${status.text}</span>`}
${supply.status === 'published' && supply.autoOfflineHours && supply.autoOfflineHours !== '' && supply.autoOfflineHours !== null ? `<span class="countdown-badge" data-id="${supply.id}" style="margin-left: 8px; padding: 4px 12px; background: linear-gradient(135deg, #ff6b6b, #ee5a6f); color: white; border-radius: 16px; font-size: 12px; font-weight: 500; box-shadow: 0 2px 4px rgba(255, 107, 107, 0.3); display: inline-flex; align-items: center; gap: 4px;">⏰ 计算中...</span>` : ''}
${supply.status === 'pending' && supply.pre_create ? `<span class="countdown-badge" data-id="${supply.id}" data-pre-create="${supply.pre_create}" style="margin-left: 8px; padding: 4px 12px; background: linear-gradient(135deg, #4ecdc4, #45b7aa); color: white; border-radius: 16px; font-size: 12px; font-weight: 500; box-shadow: 0 2px 4px rgba(78, 205, 196, 0.3); display: inline-flex; align-items: center; gap: 4px;">⏰ 计算中...</span>` : ''}
@ -5441,8 +5677,8 @@
<div class="detail-item">${supply.producting || '无'}</div>
<div class="detail-item">${supply.freshness || '无'}</div>
<div class="detail-item">${supply.supplyStatus || '未设置'}</div>
<div class="detail-item">${supply.description || '无'}</div>
<div class="detail-item">${supply.region || '未设置'}</div>
<div class="detail-item">${description || '无'}</div>
<div class="detail-item">${region || '未设置'}</div>
<!-- 隐藏独立价格字段,因为每个规格-件数对都有自己的采购价 -->
<div class="detail-item" style="font-size: 12px; color: #999; grid-column: 1 / -1;">创建时间:${supply.status === 'pending' ? '待创建' : formatDate(supply.created_at)}</div>
${supply.status === 'pending' && supply.pre_create ? `<div class="detail-item" style="font-size: 12px; color: #1890ff; grid-column: 1 / -1;">预计创建时间:${formatDate(supply.pre_create)}</div>` : ''}
@ -5941,12 +6177,24 @@
}
// 搜索功能
// 优化的搜索输入处理函数
const optimizedSearchInput = debounce(function(keyword) {
if (keyword.trim() === '') {
// 搜索关键词为空,重新加载所有数据
loadSupplies();
} else {
// 执行搜索
searchSupplies();
}
}, 300);
function onSearchInput(event) {
supplyData.searchKeyword = event.target.value;
const clearSearch = document.getElementById('clearSearch');
clearSearch.style.display = supplyData.searchKeyword ? 'block' : 'none';
// 可以在这里添加实时搜索逻辑
// 使用防抖处理实时搜索
optimizedSearchInput(supplyData.searchKeyword);
}
function onSearchConfirm(event) {
@ -5960,7 +6208,27 @@
searchInput.value = '';
supplyData.searchKeyword = '';
document.getElementById('clearSearch').style.display = 'none';
loadSupplies(); // 直接调用loadSupplies重新加载所有货源
// 使用loadSupplies()重新加载数据,会优先使用本地缓存
loadSupplies();
}
// 手动刷新数据
function refreshSupplies() {
console.log('手动刷新数据...');
// 显示刷新提示
const refreshBtn = document.querySelector('button[onclick="refreshSupplies()"]');
const originalText = refreshBtn.textContent;
refreshBtn.textContent = '刷新中...';
refreshBtn.disabled = true;
// 强制刷新数据
loadSupplies(true).finally(() => {
// 恢复按钮状态
setTimeout(() => {
refreshBtn.textContent = originalText;
refreshBtn.disabled = false;
}, 1000);
});
}
async function searchSupplies() {
@ -5971,16 +6239,29 @@
// 构建查询参数,获取所有匹配的货源
const queryParams = new URLSearchParams({
sellerId: userInfo.userId,
keyword: supplyData.searchKeyword,
page: 1,
pageSize: 1000 // 设置较大的pageSize,确保获取所有匹配的记录
pageSize: 1000, // 设置较大的pageSize,确保获取所有匹配的记录
keyword: supplyData.searchKeyword
});
// 非管理员才添加sellerId参数,管理员获取所有货源
if (userInfo.projectName !== '管理员') {
queryParams.append('sellerId', userInfo.userId);
}
const response = await fetch(`/api/supplies?${queryParams}`);
const result = await response.json();
if (result.success) {
processSupplyData(result.data.list);
// 处理搜索结果,添加高亮标记
const highlightedSupplies = result.data.list.map(supply => {
return {
...supply,
// 标记需要高亮的字段
_highlight: true,
_searchKeyword: supplyData.searchKeyword
};
});
processSupplyData(highlightedSupplies);
renderSupplyLists();
}
} catch (error) {

Loading…
Cancel
Save