refactor: 系统优化 - 错误处理、输入验证、无限滚动
主要改进: - 新增配置管理 (server/config.js) - 新增日志工具 (server/utils/logger.js) - 添加全局错误处理中间件,统一错误响应格式 - 添加输入验证和 AppError 错误类 - 重构 db.js 迁移逻辑,代码更清晰 - 前端列表改为无限滚动加载,每页 20 条 - 封装 AppState 状态管理模块 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
19e5a48f07
commit
d551841035
+58
-7
@@ -1,11 +1,62 @@
|
||||
// ============================================
|
||||
// 应用状态管理
|
||||
// ============================================
|
||||
const API = '';
|
||||
let token = localStorage.getItem('token');
|
||||
let username = localStorage.getItem('username');
|
||||
let currentType = 'income';
|
||||
let currentCategory = '接单';
|
||||
let currentYear = new Date().getFullYear();
|
||||
let currentMonth = new Date().getMonth() + 1;
|
||||
let currentNav = 'stats';
|
||||
|
||||
const AppState = {
|
||||
// 认证状态
|
||||
auth: {
|
||||
token: localStorage.getItem('token'),
|
||||
username: localStorage.getItem('username')
|
||||
},
|
||||
|
||||
// 导航状态
|
||||
nav: {
|
||||
currentType: 'income',
|
||||
currentCategory: '接单',
|
||||
currentYear: new Date().getFullYear(),
|
||||
currentMonth: new Date().getMonth() + 1,
|
||||
currentNav: 'stats'
|
||||
},
|
||||
|
||||
// 状态更新方法
|
||||
setAuth(token, username) {
|
||||
this.auth.token = token;
|
||||
this.auth.username = username;
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('username', username);
|
||||
},
|
||||
|
||||
clearAuth() {
|
||||
this.auth.token = null;
|
||||
this.auth.username = null;
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('username');
|
||||
},
|
||||
|
||||
setCategory(type, category) {
|
||||
this.nav.currentType = type;
|
||||
this.nav.currentCategory = category;
|
||||
},
|
||||
|
||||
setYearMonth(year, month) {
|
||||
this.nav.currentYear = year;
|
||||
this.nav.currentMonth = month;
|
||||
},
|
||||
|
||||
setNav(nav) {
|
||||
this.nav.currentNav = nav;
|
||||
}
|
||||
};
|
||||
|
||||
// 兼容旧代码的全局变量
|
||||
let token = AppState.auth.token;
|
||||
let username = AppState.auth.username;
|
||||
let currentType = AppState.nav.currentType;
|
||||
let currentCategory = AppState.nav.currentCategory;
|
||||
let currentYear = AppState.nav.currentYear;
|
||||
let currentMonth = AppState.nav.currentMonth;
|
||||
let currentNav = AppState.nav.currentNav;
|
||||
|
||||
function headers() {
|
||||
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };
|
||||
|
||||
+88
-45
@@ -1,9 +1,8 @@
|
||||
// ============================================
|
||||
// 通用列表管理 - 一次性加载
|
||||
// 通用列表管理 - 无限滚动加载
|
||||
// ============================================
|
||||
|
||||
// 通用列表配置
|
||||
const listConfig = {
|
||||
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 },
|
||||
@@ -11,88 +10,96 @@ const listConfig = {
|
||||
milkTeaList: { endpoint: '/api/records/milk-tea', emptyMsg: '暂无奶茶收入记录', renderItem: renderMilkTeaItem }
|
||||
};
|
||||
|
||||
// 通用列表加载状态(按 listId 存储)
|
||||
const listState = {
|
||||
orderList: { loading: false, records: [] },
|
||||
otherIncomeList: { loading: false, records: [] },
|
||||
dispatchList: { loading: false, records: [] },
|
||||
redEnvelopeList: { loading: false, records: [] },
|
||||
milkTeaList: { loading: false, records: [] }
|
||||
};
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
const listState = {};
|
||||
|
||||
// 初始化列表 - 清空状态
|
||||
function initList(listId) {
|
||||
const state = listState[listId];
|
||||
if (!state) return;
|
||||
|
||||
state.loading = false;
|
||||
state.records = [];
|
||||
listState[listId] = {
|
||||
loading: false,
|
||||
records: [],
|
||||
page: 1,
|
||||
hasMore: true,
|
||||
totalCount: 0
|
||||
};
|
||||
|
||||
const container = document.getElementById(listId);
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
if (container) container.innerHTML = '';
|
||||
}
|
||||
|
||||
// 加载列表数据
|
||||
async function loadListData(listId) {
|
||||
const config = listConfig[listId];
|
||||
const cfg = config[listId];
|
||||
const state = listState[listId];
|
||||
if (!config || !state) return;
|
||||
if (!cfg || !state) return;
|
||||
|
||||
// 防止重复加载
|
||||
if (state.loading) return;
|
||||
if (state.loading || !state.hasMore) return;
|
||||
|
||||
state.loading = true;
|
||||
showLoading(listId, true);
|
||||
|
||||
try {
|
||||
const res = await fetch(API + config.endpoint + '?page=1&limit=1000&year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
||||
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}`);
|
||||
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 = data.records || [];
|
||||
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);
|
||||
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 config = listConfig[listId];
|
||||
const cfg = config[listId];
|
||||
const state = listState[listId];
|
||||
if (!config || !state) return;
|
||||
if (!cfg || !state) return;
|
||||
|
||||
const container = document.getElementById(listId);
|
||||
if (!container) return;
|
||||
|
||||
if (state.records.length === 0) {
|
||||
container.innerHTML = '<div class="ranking-empty">' + config.emptyMsg + '</div>';
|
||||
container.innerHTML = '<div class="ranking-empty">' + cfg.emptyMsg + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 按日期分组
|
||||
const grouped = {};
|
||||
state.records.forEach(r => {
|
||||
const date = formatLocalDate(r.created_at);
|
||||
@@ -102,17 +109,53 @@ function renderList(listId) {
|
||||
|
||||
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>';
|
||||
}
|
||||
const group = document.createElement('div');
|
||||
group.className = 'order-date-group';
|
||||
group.setAttribute('data-date', date);
|
||||
|
||||
container.innerHTML = html;
|
||||
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;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -163,4 +206,4 @@ function renderMilkTeaItem(r) {
|
||||
return '<div class="order-item" onclick=\'openEditModal(' + JSON.stringify(r).replace(/'/g, "'") + ')\'>' +
|
||||
'<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>';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user