Files
gamer/public/js/order-list.js
T
lizhilunandClaude Opus 4.6 d551841035 refactor: 系统优化 - 错误处理、输入验证、无限滚动
主要改进:
- 新增配置管理 (server/config.js)
- 新增日志工具 (server/utils/logger.js)
- 添加全局错误处理中间件,统一错误响应格式
- 添加输入验证和 AppError 错误类
- 重构 db.js 迁移逻辑,代码更清晰
- 前端列表改为无限滚动加载,每页 20 条
- 封装 AppState 状态管理模块

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 10:39:58 +08:00

209 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================
// 通用列表管理 - 无限滚动加载
// ============================================
const config = {
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 }
};
const PAGE_SIZE = 20;
const listState = {};
function initList(listId) {
listState[listId] = {
loading: false,
records: [],
page: 1,
hasMore: true,
totalCount: 0
};
const container = document.getElementById(listId);
if (container) container.innerHTML = '';
}
async function loadListData(listId) {
const cfg = config[listId];
const state = listState[listId];
if (!cfg || !state) return;
if (state.loading || !state.hasMore) return;
state.loading = true;
showLoading(listId, true);
try {
const res = await fetch(
API + cfg.endpoint + '?page=' + state.page + '&limit=' + PAGE_SIZE + '&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();
const newRecords = data.records || [];
const total = data.total || 0;
state.records = state.records.concat(newRecords);
state.totalCount = total;
state.hasMore = state.records.length < total;
state.page++;
renderList(listId);
setupScrollObserver(listId);
} 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';
if (!show && listState[listId]) {
const state = listState[listId];
if (state.hasMore) {
loadingEl.textContent = '下拉加载更多...';
} else if (state.records.length > 0) {
loadingEl.textContent = '已加载全部';
} else {
loadingEl.textContent = '';
}
}
}
}
function renderList(listId) {
const cfg = config[listId];
const state = listState[listId];
if (!cfg || !state) return;
const container = document.getElementById(listId);
if (!container) return;
if (state.records.length === 0) {
container.innerHTML = '<div class="ranking-empty">' + cfg.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 = '';
for (const [date, records] of Object.entries(grouped)) {
const group = document.createElement('div');
group.className = 'order-date-group';
group.setAttribute('data-date', date);
const label = document.createElement('div');
label.className = 'order-date-label';
label.textContent = date;
group.appendChild(label);
records.forEach(r => {
const item = document.createElement('div');
item.innerHTML = cfg.renderItem(r);
group.appendChild(item.firstChild);
});
container.appendChild(group);
}
}
function setupScrollObserver(listId) {
const state = listState[listId];
if (!state || !state.hasMore) return;
const container = document.getElementById(listId);
if (!container) return;
// 移除旧的 sentinel
const oldSentinel = container.querySelector('.scroll-sentinel');
if (oldSentinel) oldSentinel.remove();
// 创建新的 sentinel
const sentinel = document.createElement('div');
sentinel.className = 'scroll-sentinel';
sentinel.style.height = '1px';
container.appendChild(sentinel);
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !state.loading && state.hasMore) {
loadListData(listId);
}
}, { rootMargin: '100px' });
observer.observe(sentinel);
// 存储 observer 以便清理
state.observer = observer;
}
// ============================================
// 兼容接口
// ============================================
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;") + ')\'>' +
'<div class="order-item-top"><span class="order-item-boss">' + (r.boss || '-') + '</span><span class="order-item-amount">' + (r.amount || 0).toFixed(2) + '</span></div>' +
'<div class="order-item-bottom"><span>' + (r.quantity || 0) + '单 × ' + (r.unit_price || 0).toFixed(2) + '</span><span>' + time + (r.partner ? ' · ' + r.partner : '') + '</span></div></div>';
}
function renderOtherIncomeItem(r) {
const time = formatLocalTime(r.created_at);
const displayCategory = r.category === '红包收入' ? '红包' : (r.category || '-');
return '<div class="order-item" onclick=\'openEditModal(' + JSON.stringify(r).replace(/'/g, "&#39;") + ')\'>' +
'<div class="order-item-top"><span class="order-item-boss">' + displayCategory + (r.boss ? ' · ' + r.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>';
}
function renderDispatchItem(r) {
const time = formatLocalTime(r.created_at);
const unitPrice = r.unit_price || (r.quantity ? r.amount / r.quantity : 0);
return '<div class="order-item" onclick=\'openEditModal(' + JSON.stringify(r).replace(/'/g, "&#39;") + ')\'>' +
'<div class="order-item-top"><span class="order-item-boss">' + (r.boss || '-') + '</span><span class="order-item-amount">' + (r.amount || 0).toFixed(2) + '</span></div>' +
'<div class="order-item-bottom"><span>' + (r.quantity || 0) + '单 × ' + (r.rebate || 0).toFixed(2) + '(单价' + unitPrice.toFixed(2) + '</span><span>' + time + (r.partner ? ' · ' + r.partner : '') + '</span></div></div>';
}
function renderRedEnvelopeItem(r) {
const time = formatLocalTime(r.created_at);
return '<div class="order-item" onclick=\'openEditModal(' + JSON.stringify(r).replace(/'/g, "&#39;") + ')\'>' +
'<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>';
}
function renderMilkTeaItem(r) {
const time = formatLocalTime(r.created_at);
return '<div class="order-item" onclick=\'openEditModal(' + JSON.stringify(r).replace(/'/g, "&#39;") + ')\'>' +
'<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>';
}