diff --git a/public/index.html b/public/index.html
index 0b5846f..bcb292c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -460,9 +460,9 @@
-
+
-
+
diff --git a/public/js/app.js b/public/js/app.js
index 04ed447..6360819 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -175,31 +175,26 @@ function showPage(pageId) {
}
if (pageId === 'orderListPage') {
document.getElementById('orderListTitle').textContent = `${currentYear}年${currentMonth}月接单列表`;
- orderPage = 1; orderHasMore = true;
- document.getElementById('orderList').innerHTML = '';
- loadOrders();
+ initList('orderList');
+ loadListData('orderList');
}
if (pageId === 'otherIncomeListPage') {
document.getElementById('otherIncomeListTitle').textContent = `${currentYear}年${currentMonth}月红包及其他收入`;
- otherIncomePage = 1; otherIncomeHasMore = true;
- document.getElementById('otherIncomeList').innerHTML = '';
- loadOtherIncome();
+ initList('otherIncomeList');
+ loadListData('otherIncomeList');
}
if (pageId === 'dispatchListPage') {
document.getElementById('dispatchListTitle').textContent = `${currentYear}年${currentMonth}月派单列表`;
- dispatchPage = 1; dispatchHasMore = true;
- document.getElementById('dispatchList').innerHTML = '';
- loadDispatches();
+ initList('dispatchList');
+ loadListData('dispatchList');
}
if (pageId === 'redEnvelopeListPage') {
- redEnvelopePage = 1; redEnvelopeHasMore = true;
- document.getElementById('redEnvelopeList').innerHTML = '';
- loadRedEnvelope();
+ initList('redEnvelopeList');
+ loadListData('redEnvelopeList');
}
if (pageId === 'milkTeaListPage') {
- milkTeaPage = 1; milkTeaHasMore = true;
- document.getElementById('milkTeaList').innerHTML = '';
- loadMilkTea();
+ initList('milkTeaList');
+ loadListData('milkTeaList');
}
}
diff --git a/public/js/edit-modal.js b/public/js/edit-modal.js
index 8dd28d0..efce8f6 100644
--- a/public/js/edit-modal.js
+++ b/public/js/edit-modal.js
@@ -74,17 +74,14 @@ function closeEditModal() {
function refreshCurrentList() {
const cat = document.getElementById('editCategory').value;
if (cat === '派单') {
- dispatchPage = 1; dispatchHasMore = true;
- document.getElementById('dispatchList').innerHTML = '';
- loadDispatches();
+ initList('dispatchList');
+ loadListData('dispatchList');
} else if (cat === '接单') {
- orderPage = 1; orderHasMore = true;
- document.getElementById('orderList').innerHTML = '';
- loadOrders();
+ initList('orderList');
+ loadListData('orderList');
} else {
- otherIncomePage = 1; otherIncomeHasMore = true;
- document.getElementById('otherIncomeList').innerHTML = '';
- loadOtherIncome();
+ initList('otherIncomeList');
+ loadListData('otherIncomeList');
}
// 如果在日详情页也刷新
if (document.getElementById('dailyDetailPage').classList.contains('active') && currentDailyDate) {
diff --git a/public/js/order-list.js b/public/js/order-list.js
index 21bab66..504735d 100644
--- a/public/js/order-list.js
+++ b/public/js/order-list.js
@@ -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 = '
' + config.emptyMsg + '
';
- 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 += '' + date + '
';
- records.forEach(r => {
- html += config.renderItem(r);
- });
- html += '
';
- }
- 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 = '' + config.emptyMsg + '
';
+ 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 += '' + date + '
';
+ records.forEach(r => {
+ html += config.renderItem(r);
+ });
+ html += '
';
+ }
+
+ 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 '' +
@@ -105,35 +186,3 @@ function renderMilkTeaItem(r) {
'
奶茶' + (r.amount || 0).toFixed(2) + '
' +
'
' + (r.source || '') + '' + time + '
';
}
-
-// 滚动加载(带防抖)
-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); }
diff --git a/public/js/record-form.js b/public/js/record-form.js
index f118391..5e36e73 100644
--- a/public/js/record-form.js
+++ b/public/js/record-form.js
@@ -123,11 +123,13 @@ async function submitRecord() {
const partner = document.getElementById('fPartner')?.value.trim() || '';
if (currentCategory === '派单') {
const rebate = parseFloat(document.getElementById('fRebate').value);
- if (!quantity || !unit_price || !rebate || !boss) { showToast('请填写完整信息'); return; }
+ const partner = document.getElementById('fPartner')?.value.trim() || '';
+ if (!quantity || !unit_price || !rebate || !boss || !partner) { showToast('请填写完整信息(包括负责打单的陪玩)'); return; }
body.quantity = quantity;
body.unit_price = unit_price;
body.rebate = rebate;
body.amount = quantity * rebate;
+ body.partner = partner;
} else {
if (!quantity || !unit_price || !boss) { showToast('请填写完整信息'); return; }
body.quantity = quantity;
@@ -135,7 +137,6 @@ async function submitRecord() {
body.amount = quantity * unit_price;
}
body.boss = boss;
- body.partner = partner;
} else if (currentCategory === '红包收入' || currentCategory === '奶茶') {
const amount = parseFloat(document.getElementById('fAmount').value);
const source = document.getElementById('fSource').value.trim();
diff --git a/public/js/stats.js b/public/js/stats.js
index 6584fca..b3f2043 100644
--- a/public/js/stats.js
+++ b/public/js/stats.js
@@ -214,7 +214,7 @@ async function loadMonthlyDetail(type) {
}
try {
- const res = await fetch(API + '/api/stats/monthly-records?type=' + type + '&year=' + monthlyDetailYear + '&month=' + monthlyDetailMonth, { headers: headers() });
+ const res = await fetch(API + '/api/stats/monthly-records?type=' + type + '&year=' + monthlyDetailYear + '&month=' + monthlyDetailMonth + '&limit=1000', { headers: headers() });
if (!res.ok) return;
const data = await res.json();
const color = type === 'income' ? '#27ae60' : '#e74c3c';
diff --git a/server/routes/records.js b/server/routes/records.js
index 7f5a8b4..0be5abb 100644
--- a/server/routes/records.js
+++ b/server/routes/records.js
@@ -51,7 +51,9 @@ function queryPagedRecords(userId, year, month, options) {
// 接单列表
router.get('/orders', (req, res) => {
const result = queryPagedRecords(req.user.id, req.query.year, req.query.month, {
- category: '接单'
+ category: '接单',
+ page: req.query.page,
+ limit: req.query.limit
});
res.json(result);
});
@@ -60,7 +62,9 @@ router.get('/orders', (req, res) => {
router.get('/other-income', (req, res) => {
const result = queryPagedRecords(req.user.id, req.query.year, req.query.month, {
type: 'income',
- excludeCategories: ['接单', '派单']
+ excludeCategories: ['接单', '派单'],
+ page: req.query.page,
+ limit: req.query.limit
});
res.json(result);
});
@@ -69,7 +73,9 @@ router.get('/other-income', (req, res) => {
router.get('/red-envelope', (req, res) => {
const result = queryPagedRecords(req.user.id, req.query.year, req.query.month, {
type: 'income',
- category: '红包收入'
+ category: '红包收入',
+ page: req.query.page,
+ limit: req.query.limit
});
res.json(result);
});
@@ -78,7 +84,9 @@ router.get('/red-envelope', (req, res) => {
router.get('/milk-tea', (req, res) => {
const result = queryPagedRecords(req.user.id, req.query.year, req.query.month, {
type: 'income',
- category: '奶茶'
+ category: '奶茶',
+ page: req.query.page,
+ limit: req.query.limit
});
res.json(result);
});
@@ -86,7 +94,9 @@ router.get('/milk-tea', (req, res) => {
// 派单列表
router.get('/dispatches', (req, res) => {
const result = queryPagedRecords(req.user.id, req.query.year, req.query.month, {
- category: '派单'
+ category: '派单',
+ page: req.query.page,
+ limit: req.query.limit
});
res.json(result);
});
diff --git a/server/routes/stats.js b/server/routes/stats.js
index 3a8b8ab..2e9a33d 100644
--- a/server/routes/stats.js
+++ b/server/routes/stats.js
@@ -22,12 +22,12 @@ function getCached(key, fetchFn) {
const firstKey = statsCache.keys().next().value;
statsCache.delete(firstKey);
}
- return fetchFn().then(data => {
+ return Promise.resolve(fetchFn()).then(data => {
statsCache.set(key, { data, ts: Date.now() });
return data;
}).catch(err => {
console.error('Cache fetch error:', err.message);
- return fetchFn(); // fallback to direct fetch
+ throw err;
});
}