fix: 修复分页和缓存相关bug

- 前端: 移除无限滚动,改为一次性加载全部数据
- 前端: monthly-records接口添加limit=1000参数
- 前端: 修复edit-modal.js中loadListData调用参数
- 后端: 修复stats.js中getCached函数Promise处理
- 后端: 修复records.js各接口正确传递limit参数
- 后端: 修复partner-records SQL参数数量问题
- 统一版本号强制浏览器刷新缓存

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lizhilun
2026-03-25 22:12:11 +08:00
co-authored by Claude Opus 4.6
parent df3a9daa65
commit 9126348d1b
8 changed files with 181 additions and 129 deletions
+142 -93
View File
@@ -1,74 +1,155 @@
// 防抖工具函数
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// ============================================
// 通用列表管理 - 一次性加载
// ============================================
// 全局状态变量(供 app.js 引用)
var orderPage = 1, orderHasMore = true, orderLoading = false;
var otherIncomePage = 1, otherIncomeHasMore = true, otherIncomeLoading = false;
var dispatchPage = 1, dispatchHasMore = true, dispatchLoading = false;
var redEnvelopePage = 1, redEnvelopeHasMore = true, redEnvelopeLoading = false;
var milkTeaPage = 1, milkTeaHasMore = true, milkTeaLoading = false;
// 当前活动的列表状态
let currentListId = null;
// 通用分页状态管理
// 通用列表配置
const listConfig = {
orderList: { getPage: () => orderPage, setPage: (v) => orderPage = v, getHasMore: () => orderHasMore, setHasMore: (v) => orderHasMore = v, getLoading: () => orderLoading, setLoading: (v) => orderLoading = v, emptyMsg: '暂无接单记录', endpoint: '/api/records/orders', renderItem: renderOrderItem },
otherIncomeList: { getPage: () => otherIncomePage, setPage: (v) => otherIncomePage = v, getHasMore: () => otherIncomeHasMore, setHasMore: (v) => otherIncomeHasMore = v, getLoading: () => otherIncomeLoading, setLoading: (v) => otherIncomeLoading = v, emptyMsg: '暂无红包及其他收入记录', endpoint: '/api/records/other-income', renderItem: renderOtherIncomeItem },
dispatchList: { getPage: () => dispatchPage, setPage: (v) => dispatchPage = v, getHasMore: () => dispatchHasMore, setHasMore: (v) => dispatchHasMore = v, getLoading: () => dispatchLoading, setLoading: (v) => dispatchLoading = v, emptyMsg: '暂无派单记录', endpoint: '/api/records/dispatches', renderItem: renderDispatchItem },
redEnvelopeList: { getPage: () => redEnvelopePage, setPage: (v) => redEnvelopePage = v, getHasMore: () => redEnvelopeHasMore, setHasMore: (v) => redEnvelopeHasMore = v, getLoading: () => redEnvelopeLoading, setLoading: (v) => redEnvelopeLoading = v, emptyMsg: '暂无红包收入记录', endpoint: '/api/records/red-envelope', renderItem: renderRedEnvelopeItem },
milkTeaList: { getPage: () => milkTeaPage, setPage: (v) => milkTeaPage = v, getHasMore: () => milkTeaHasMore, setHasMore: (v) => milkTeaHasMore = v, getLoading: () => milkTeaLoading, setLoading: (v) => milkTeaLoading = v, emptyMsg: '暂无奶茶收入记录', endpoint: '/api/records/milk-tea', renderItem: renderMilkTeaItem }
orderList: { endpoint: '/api/records/orders', emptyMsg: '暂无接单记录', renderItem: renderOrderItem },
otherIncomeList: { endpoint: '/api/records/other-income', emptyMsg: '暂无红包及其他收入记录', renderItem: renderOtherIncomeItem },
dispatchList: { endpoint: '/api/records/dispatches', emptyMsg: '暂无派单记录', renderItem: renderDispatchItem },
redEnvelopeList: { endpoint: '/api/records/red-envelope', emptyMsg: '暂无红包收入记录', renderItem: renderRedEnvelopeItem },
milkTeaList: { endpoint: '/api/records/milk-tea', emptyMsg: '暂无奶茶收入记录', renderItem: renderMilkTeaItem }
};
// 通用列表加载函数
async function loadList(listId, append) {
const config = listConfig[listId];
if (!config) return;
// 通用列表加载状态(按 listId 存储)
const listState = {
orderList: { loading: false, records: [] },
otherIncomeList: { loading: false, records: [] },
dispatchList: { loading: false, records: [] },
redEnvelopeList: { loading: false, records: [] },
milkTeaList: { loading: false, records: [] }
};
if (config.getLoading() || (!append && !config.getHasMore())) return;
config.setLoading(true);
document.getElementById(listId.replace('List', 'Loading')).style.display = 'block';
// 初始化列表 - 清空状态
function initList(listId) {
const state = listState[listId];
if (!state) return;
try {
const res = await fetch(API + config.endpoint + '?page=' + config.getPage() + '&limit=20&year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
if (!res.ok) return;
const data = await res.json();
const container = document.getElementById(listId);
if (!append) container.innerHTML = '';
state.loading = false;
state.records = [];
if (data.records.length === 0) {
if (config.getPage() === 1) container.innerHTML = '<div class="ranking-empty">' + config.emptyMsg + '</div>';
config.setHasMore(false);
} else {
const grouped = {};
data.records.forEach(r => {
const date = formatLocalDate(r.created_at);
if (!grouped[date]) grouped[date] = [];
grouped[date].push(r);
});
let html = '';
for (const [date, records] of Object.entries(grouped)) {
html += '<div class="order-date-group" data-date="' + date + '"><div class="order-date-label">' + date + '</div>';
records.forEach(r => {
html += config.renderItem(r);
});
html += '</div>';
}
container.insertAdjacentHTML('beforeend', html);
config.setHasMore(data.records.length >= 20);
config.setPage(config.getPage() + 1);
}
} catch (e) { console.error(e); }
config.setLoading(false);
document.getElementById(listId.replace('List', 'Loading')).style.display = 'none';
const container = document.getElementById(listId);
if (container) {
container.innerHTML = '';
}
}
// 通用渲染函数
// 加载列表数据
async function loadListData(listId) {
const config = listConfig[listId];
const state = listState[listId];
if (!config || !state) return;
// 防止重复加载
if (state.loading) {
console.log('[loadListData] already loading, skip');
return;
}
state.loading = true;
showLoading(listId, true);
try {
console.log(`[loadListData] fetching ${listId}`);
// 一次性加载全部数据,不做分页
const res = await fetch(API + config.endpoint + '?page=1&limit=1000&year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
if (!res.ok) {
console.error(`[loadListData] fetch failed: ${res.status}`);
state.loading = false;
showLoading(listId, false);
return;
}
const data = await res.json();
console.log(`[loadListData] got ${data.records?.length || 0} records`);
state.records = data.records || [];
// 渲染
renderList(listId);
console.log(`[loadListData] ${listId} done, total records=${state.records.length}`);
} catch (e) {
console.error(`[loadListData] error:`, e);
} finally {
state.loading = false;
showLoading(listId, false);
}
}
// 显示/隐藏加载指示器
function showLoading(listId, show) {
const loadingId = listId.replace('List', 'Loading');
const loadingEl = document.getElementById(loadingId);
if (loadingEl) {
loadingEl.style.display = show ? 'block' : 'none';
}
}
// 渲染列表
function renderList(listId) {
const config = listConfig[listId];
const state = listState[listId];
if (!config || !state) return;
const container = document.getElementById(listId);
if (!container) return;
if (state.records.length === 0) {
container.innerHTML = '<div class="ranking-empty">' + config.emptyMsg + '</div>';
return;
}
// 按日期分组
const grouped = {};
state.records.forEach(r => {
const date = formatLocalDate(r.created_at);
if (!grouped[date]) grouped[date] = [];
grouped[date].push(r);
});
container.innerHTML = '';
// 生成 HTML
let html = '';
for (const [date, records] of Object.entries(grouped)) {
html += '<div class="order-date-group" data-date="' + date + '"><div class="order-date-label">' + date + '</div>';
records.forEach(r => {
html += config.renderItem(r);
});
html += '</div>';
}
container.innerHTML = html;
}
// ============================================
// 列表滚动(已简化为一次性加载)
// ============================================
function setupInfiniteScroll(listId) {
// 不再需要无限滚动,一次性加载全部数据
}
// ============================================
// 兼容接口
// ============================================
async function loadOrders() { return loadListData('orderList'); }
async function loadOtherIncome() { return loadListData('otherIncomeList'); }
async function loadDispatches() { return loadListData('dispatchList'); }
async function loadRedEnvelope() { return loadListData('redEnvelopeList'); }
async function loadMilkTea() { return loadListData('milkTeaList'); }
// ============================================
// 渲染函数
// ============================================
function renderOrderItem(r) {
const time = formatLocalTime(r.created_at);
return '<div class="order-item" onclick=\'openEditModal(' + JSON.stringify(r).replace(/'/g, "&#39;") + ')\'>' +
@@ -105,35 +186,3 @@ function renderMilkTeaItem(r) {
'<div class="order-item-top"><span class="order-item-boss">奶茶</span><span class="order-item-amount">' + (r.amount || 0).toFixed(2) + '</span></div>' +
'<div class="order-item-bottom"><span>' + (r.source || '') + '</span><span>' + time + '</span></div></div>';
}
// 滚动加载(带防抖)
const handleListScroll = debounce(() => {
const pageListMap = [
{ page: 'orderListPage', list: 'orderList' },
{ page: 'dispatchListPage', list: 'dispatchList' },
{ page: 'otherIncomeListPage', list: 'otherIncomeList' },
{ page: 'redEnvelopeListPage', list: 'redEnvelopeList' },
{ page: 'milkTeaListPage', list: 'milkTeaList' }
];
for (const m of pageListMap) {
if (document.getElementById(m.page).classList.contains('active')) {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
const config = listConfig[m.list];
if (config && config.getHasMore() && !config.getLoading()) {
loadList(m.list, true);
}
}
break;
}
}
}, 100);
window.addEventListener('scroll', handleListScroll);
// 兼容旧接口
async function loadOrders(append) { return loadList('orderList', append); }
async function loadOtherIncome(append) { return loadList('otherIncomeList', append); }
async function loadDispatches(append) { return loadList('dispatchList', append); }
async function loadRedEnvelope(append) { return loadList('redEnvelopeList', append); }
async function loadMilkTea(append) { return loadList('milkTeaList', append); }