refactor: 重构代码减少重复并添加分页
后端 (records.js): - 提取通用分页查询函数 queryPagedRecords - 5个列表接口 (orders/other-income/red-envelope/milk-tea/dispatches) 复用同一函数 后端 (stats.js): - 为 monthly-records, boss-records, partner-records, daily-detail 添加分页支持 - 新增 page, limit, totalCount 返回字段 前端 (order-list.js): - 提取通用列表配置 listConfig - 统一渲染函数 renderOrderItem/renderOtherIncomeItem 等 - 大幅减少代码重复 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a03e92d437
commit
2ebac98f07
+97
-263
@@ -1,20 +1,38 @@
|
|||||||
let orderPage = 1;
|
// 全局状态变量(供 app.js 引用)
|
||||||
let orderHasMore = true;
|
var orderPage = 1, orderHasMore = true, orderLoading = false;
|
||||||
let 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;
|
||||||
|
|
||||||
|
// 通用分页状态管理
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通用列表加载函数
|
||||||
|
async function loadList(listId, append) {
|
||||||
|
const config = listConfig[listId];
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
|
if (config.getLoading() || (!append && !config.getHasMore())) return;
|
||||||
|
config.setLoading(true);
|
||||||
|
document.getElementById(listId.replace('List', 'Loading')).style.display = 'block';
|
||||||
|
|
||||||
async function loadOrders(append) {
|
|
||||||
if (orderLoading || (!append && !orderHasMore)) return;
|
|
||||||
orderLoading = true;
|
|
||||||
document.getElementById('orderLoading').style.display = 'block';
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(API + '/api/records/orders?page=' + orderPage + '&limit=20&year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
const res = await fetch(API + config.endpoint + '?page=' + config.getPage() + '&limit=20&year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const container = document.getElementById('orderList');
|
const container = document.getElementById(listId);
|
||||||
if (!append) container.innerHTML = '';
|
if (!append) container.innerHTML = '';
|
||||||
|
|
||||||
if (data.records.length === 0) {
|
if (data.records.length === 0) {
|
||||||
if (orderPage === 1) container.innerHTML = '<div class="ranking-empty">暂无接单记录</div>';
|
if (config.getPage() === 1) container.innerHTML = '<div class="ranking-empty">' + config.emptyMsg + '</div>';
|
||||||
orderHasMore = false;
|
config.setHasMore(false);
|
||||||
} else {
|
} else {
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
data.records.forEach(r => {
|
data.records.forEach(r => {
|
||||||
@@ -22,272 +40,88 @@ async function loadOrders(append) {
|
|||||||
if (!grouped[date]) grouped[date] = [];
|
if (!grouped[date]) grouped[date] = [];
|
||||||
grouped[date].push(r);
|
grouped[date].push(r);
|
||||||
});
|
});
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
for (const [date, records] of Object.entries(grouped)) {
|
for (const [date, records] of Object.entries(grouped)) {
|
||||||
html += `<div class="order-date-group" data-date="${date}">
|
html += '<div class="order-date-group" data-date="' + date + '"><div class="order-date-label">' + date + '</div>';
|
||||||
<div class="order-date-label">${date}</div>`;
|
|
||||||
records.forEach(r => {
|
records.forEach(r => {
|
||||||
const time = formatLocalTime(r.created_at);
|
html += config.renderItem(r);
|
||||||
html += `<div class="order-item" onclick='openEditModal(${JSON.stringify(r).replace(/'/g, "'")})'>
|
|
||||||
<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>`;
|
|
||||||
});
|
});
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
}
|
}
|
||||||
container.insertAdjacentHTML('beforeend', html);
|
container.insertAdjacentHTML('beforeend', html);
|
||||||
orderHasMore = data.records.length >= 20;
|
config.setHasMore(data.records.length >= 20);
|
||||||
orderPage++;
|
config.setPage(config.getPage() + 1);
|
||||||
}
|
}
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
orderLoading = false;
|
|
||||||
document.getElementById('orderLoading').style.display = 'none';
|
config.setLoading(false);
|
||||||
|
document.getElementById(listId.replace('List', 'Loading')).style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用渲染函数
|
||||||
|
function renderOrderItem(r) {
|
||||||
|
const time = formatLocalTime(r.created_at);
|
||||||
|
return '<div class="order-item" onclick=\'openEditModal(' + JSON.stringify(r).replace(/'/g, "'") + ')\'>' +
|
||||||
|
'<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, "'") + ')\'>' +
|
||||||
|
'<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);
|
||||||
|
return '<div class="order-item" onclick=\'openEditModal(' + JSON.stringify(r).replace(/'/g, "'") + ')\'>' +
|
||||||
|
'<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 || r.amount / r.quantity) || 0).toFixed(2) + '(单价' + (r.unit_price || 0).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, "'") + ')\'>' +
|
||||||
|
'<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, "'") + ')\'>' +
|
||||||
|
'<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>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滚动加载
|
// 滚动加载
|
||||||
window.addEventListener('scroll', () => {
|
window.addEventListener('scroll', () => {
|
||||||
if (document.getElementById('orderListPage').classList.contains('active')) {
|
const pageListMap = [
|
||||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
|
{ page: 'orderListPage', list: 'orderList' },
|
||||||
if (orderHasMore && !orderLoading) loadOrders(true);
|
{ page: 'dispatchListPage', list: 'dispatchList' },
|
||||||
}
|
{ page: 'otherIncomeListPage', list: 'otherIncomeList' },
|
||||||
}
|
{ page: 'redEnvelopeListPage', list: 'redEnvelopeList' },
|
||||||
if (document.getElementById('dispatchListPage').classList.contains('active')) {
|
{ page: 'milkTeaListPage', list: 'milkTeaList' }
|
||||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
|
];
|
||||||
if (dispatchHasMore && !dispatchLoading) loadDispatches(true);
|
|
||||||
}
|
for (const m of pageListMap) {
|
||||||
}
|
if (document.getElementById(m.page).classList.contains('active')) {
|
||||||
if (document.getElementById('otherIncomeListPage').classList.contains('active')) {
|
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
|
||||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
|
const config = listConfig[m.list];
|
||||||
if (otherIncomeHasMore && !otherIncomeLoading) loadOtherIncome(true);
|
if (config && config.getHasMore() && !config.getLoading()) {
|
||||||
}
|
loadList(m.list, true);
|
||||||
}
|
}
|
||||||
if (document.getElementById('redEnvelopeListPage').classList.contains('active')) {
|
}
|
||||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
|
break;
|
||||||
if (redEnvelopeHasMore && !redEnvelopeLoading) loadRedEnvelope(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (document.getElementById('milkTeaListPage').classList.contains('active')) {
|
|
||||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 100) {
|
|
||||||
if (milkTeaHasMore && !milkTeaLoading) loadMilkTea(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 红包及其他收入列表
|
// 兼容旧接口
|
||||||
let otherIncomePage = 1;
|
async function loadOrders(append) { return loadList('orderList', append); }
|
||||||
let otherIncomeHasMore = true;
|
async function loadOtherIncome(append) { return loadList('otherIncomeList', append); }
|
||||||
let otherIncomeLoading = false;
|
async function loadDispatches(append) { return loadList('dispatchList', append); }
|
||||||
|
async function loadRedEnvelope(append) { return loadList('redEnvelopeList', append); }
|
||||||
async function loadOtherIncome(append) {
|
async function loadMilkTea(append) { return loadList('milkTeaList', append); }
|
||||||
if (otherIncomeLoading || (!append && !otherIncomeHasMore)) return;
|
|
||||||
otherIncomeLoading = true;
|
|
||||||
document.getElementById('otherIncomeLoading').style.display = 'block';
|
|
||||||
try {
|
|
||||||
const res = await fetch(API + '/api/records/other-income?page=' + otherIncomePage + '&limit=20&year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
|
||||||
if (!res.ok) return;
|
|
||||||
const data = await res.json();
|
|
||||||
const container = document.getElementById('otherIncomeList');
|
|
||||||
if (!append) container.innerHTML = '';
|
|
||||||
if (data.records.length === 0) {
|
|
||||||
if (otherIncomePage === 1) container.innerHTML = '<div class="ranking-empty">暂无红包及其他收入记录</div>';
|
|
||||||
otherIncomeHasMore = 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 => {
|
|
||||||
const time = formatLocalTime(r.created_at);
|
|
||||||
const displayCategory = r.category === '红包收入' ? '红包' : (r.category || '-');
|
|
||||||
html += `<div class="order-item" onclick='openEditModal(${JSON.stringify(r).replace(/'/g, "'")})'>
|
|
||||||
<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>`;
|
|
||||||
});
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
container.insertAdjacentHTML('beforeend', html);
|
|
||||||
otherIncomeHasMore = data.records.length >= 20;
|
|
||||||
otherIncomePage++;
|
|
||||||
}
|
|
||||||
} catch (e) { console.error(e); }
|
|
||||||
otherIncomeLoading = false;
|
|
||||||
document.getElementById('otherIncomeLoading').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 派单列表
|
|
||||||
let dispatchPage = 1;
|
|
||||||
let dispatchHasMore = true;
|
|
||||||
let dispatchLoading = false;
|
|
||||||
|
|
||||||
async function loadDispatches(append) {
|
|
||||||
if (dispatchLoading || (!append && !dispatchHasMore)) return;
|
|
||||||
dispatchLoading = true;
|
|
||||||
document.getElementById('dispatchLoading').style.display = 'block';
|
|
||||||
try {
|
|
||||||
const res = await fetch(API + '/api/records/dispatches?page=' + dispatchPage + '&limit=20&year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
|
||||||
if (!res.ok) return;
|
|
||||||
const data = await res.json();
|
|
||||||
const container = document.getElementById('dispatchList');
|
|
||||||
if (!append) container.innerHTML = '';
|
|
||||||
if (data.records.length === 0) {
|
|
||||||
if (dispatchPage === 1) container.innerHTML = '<div class="ranking-empty">暂无派单记录</div>';
|
|
||||||
dispatchHasMore = 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 => {
|
|
||||||
const time = formatLocalTime(r.created_at);
|
|
||||||
html += `<div class="order-item" onclick='openEditModal(${JSON.stringify(r).replace(/'/g, "'")})'>
|
|
||||||
<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 || r.amount / r.quantity).toFixed(2)}(单价${(r.unit_price || 0).toFixed(2)})</span>
|
|
||||||
<span>${time}${r.partner ? ' · ' + r.partner : ''}</span>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
container.insertAdjacentHTML('beforeend', html);
|
|
||||||
dispatchHasMore = data.records.length >= 20;
|
|
||||||
dispatchPage++;
|
|
||||||
}
|
|
||||||
} catch (e) { console.error(e); }
|
|
||||||
dispatchLoading = false;
|
|
||||||
document.getElementById('dispatchLoading').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 红包收入列表
|
|
||||||
let redEnvelopePage = 1;
|
|
||||||
let redEnvelopeHasMore = true;
|
|
||||||
let redEnvelopeLoading = false;
|
|
||||||
|
|
||||||
async function loadRedEnvelope(append) {
|
|
||||||
if (redEnvelopeLoading || (!append && !redEnvelopeHasMore)) return;
|
|
||||||
redEnvelopeLoading = true;
|
|
||||||
document.getElementById('redEnvelopeLoading').style.display = 'block';
|
|
||||||
try {
|
|
||||||
const res = await fetch(API + '/api/records/red-envelope?page=' + redEnvelopePage + '&limit=20&year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
|
||||||
if (!res.ok) return;
|
|
||||||
const data = await res.json();
|
|
||||||
const container = document.getElementById('redEnvelopeList');
|
|
||||||
if (!append) container.innerHTML = '';
|
|
||||||
if (data.records.length === 0) {
|
|
||||||
if (redEnvelopePage === 1) container.innerHTML = '<div class="ranking-empty">暂无红包收入记录</div>';
|
|
||||||
redEnvelopeHasMore = 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 => {
|
|
||||||
const time = formatLocalTime(r.created_at);
|
|
||||||
html += `<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>`;
|
|
||||||
});
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
container.insertAdjacentHTML('beforeend', html);
|
|
||||||
redEnvelopeHasMore = data.records.length >= 20;
|
|
||||||
redEnvelopePage++;
|
|
||||||
}
|
|
||||||
} catch (e) { console.error(e); }
|
|
||||||
redEnvelopeLoading = false;
|
|
||||||
document.getElementById('redEnvelopeLoading').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 奶茶收入列表
|
|
||||||
let milkTeaPage = 1;
|
|
||||||
let milkTeaHasMore = true;
|
|
||||||
let milkTeaLoading = false;
|
|
||||||
|
|
||||||
async function loadMilkTea(append) {
|
|
||||||
if (milkTeaLoading || (!append && !milkTeaHasMore)) return;
|
|
||||||
milkTeaLoading = true;
|
|
||||||
document.getElementById('milkTeaLoading').style.display = 'block';
|
|
||||||
try {
|
|
||||||
const res = await fetch(API + '/api/records/milk-tea?page=' + milkTeaPage + '&limit=20&year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
|
||||||
if (!res.ok) return;
|
|
||||||
const data = await res.json();
|
|
||||||
const container = document.getElementById('milkTeaList');
|
|
||||||
if (!append) container.innerHTML = '';
|
|
||||||
if (data.records.length === 0) {
|
|
||||||
if (milkTeaPage === 1) container.innerHTML = '<div class="ranking-empty">暂无奶茶收入记录</div>';
|
|
||||||
milkTeaHasMore = 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 => {
|
|
||||||
const time = formatLocalTime(r.created_at);
|
|
||||||
html += `<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>`;
|
|
||||||
});
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
container.insertAdjacentHTML('beforeend', html);
|
|
||||||
milkTeaHasMore = data.records.length >= 20;
|
|
||||||
milkTeaPage++;
|
|
||||||
}
|
|
||||||
} catch (e) { console.error(e); }
|
|
||||||
milkTeaLoading = false;
|
|
||||||
document.getElementById('milkTeaLoading').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|||||||
+52
-61
@@ -15,89 +15,80 @@ router.post('/', (req, res) => {
|
|||||||
res.json({ id: result.lastInsertRowid });
|
res.json({ id: result.lastInsertRowid });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 接单列表
|
// 通用分页查询函数
|
||||||
router.get('/orders', (req, res) => {
|
function queryPagedRecords(userId, year, month, options) {
|
||||||
const { page = 1, limit = 20 } = req.query;
|
const { page = 1, limit = 20 } = options;
|
||||||
const offset = (Number(page) - 1) * Number(limit);
|
const offset = (Number(page) - 1) * Number(limit);
|
||||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
const { startDate, endDate } = getMonthRange(year, month);
|
||||||
|
|
||||||
|
let whereClause = `user_id = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?`;
|
||||||
|
const params = [userId, startDate, endDate];
|
||||||
|
|
||||||
|
if (options.category) {
|
||||||
|
whereClause += ` AND category = ?`;
|
||||||
|
params.push(options.category);
|
||||||
|
}
|
||||||
|
if (options.type) {
|
||||||
|
whereClause += ` AND type = ?`;
|
||||||
|
params.push(options.type);
|
||||||
|
}
|
||||||
|
if (options.excludeCategories && options.excludeCategories.length > 0) {
|
||||||
|
whereClause += ` AND category NOT IN (${options.excludeCategories.map(() => '?').join(',')})`;
|
||||||
|
params.push(...options.excludeCategories);
|
||||||
|
}
|
||||||
|
|
||||||
const total = db.prepare(
|
const total = db.prepare(
|
||||||
`SELECT COUNT(*) as count FROM records WHERE user_id = ? AND category = '接单' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?`
|
`SELECT COUNT(*) as count FROM records WHERE ${whereClause}`
|
||||||
).get(req.user.id, startDate, endDate).count;
|
).get(...params).count;
|
||||||
|
|
||||||
const records = db.prepare(
|
const records = db.prepare(
|
||||||
`SELECT * FROM records WHERE user_id = ? AND category = '接单' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
`SELECT * FROM records WHERE ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
||||||
).all(req.user.id, startDate, endDate, Number(limit), offset);
|
).all(...params, Number(limit), offset);
|
||||||
|
|
||||||
res.json({ records, total, page: Number(page), limit: Number(limit) });
|
return { records, total, page: Number(page), limit: Number(limit) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接单列表
|
||||||
|
router.get('/orders', (req, res) => {
|
||||||
|
const result = queryPagedRecords(req.user.id, req.query.year, req.query.month, {
|
||||||
|
category: '接单'
|
||||||
|
});
|
||||||
|
res.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 红包及其他收入列表
|
// 红包及其他收入列表
|
||||||
router.get('/other-income', (req, res) => {
|
router.get('/other-income', (req, res) => {
|
||||||
const { page = 1, limit = 20 } = req.query;
|
const result = queryPagedRecords(req.user.id, req.query.year, req.query.month, {
|
||||||
const offset = (Number(page) - 1) * Number(limit);
|
type: 'income',
|
||||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
excludeCategories: ['接单', '派单']
|
||||||
|
});
|
||||||
const total = db.prepare(
|
res.json(result);
|
||||||
`SELECT COUNT(*) as count FROM records WHERE user_id = ? AND type = 'income' AND category != '接单' AND category != '派单' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?`
|
|
||||||
).get(req.user.id, startDate, endDate).count;
|
|
||||||
|
|
||||||
const records = db.prepare(
|
|
||||||
`SELECT * FROM records WHERE user_id = ? AND type = 'income' AND category != '接单' AND category != '派单' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
||||||
).all(req.user.id, startDate, endDate, Number(limit), offset);
|
|
||||||
|
|
||||||
res.json({ records, total, page: Number(page), limit: Number(limit) });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 红包收入列表
|
// 红包收入列表
|
||||||
router.get('/red-envelope', (req, res) => {
|
router.get('/red-envelope', (req, res) => {
|
||||||
const { page = 1, limit = 20 } = req.query;
|
const result = queryPagedRecords(req.user.id, req.query.year, req.query.month, {
|
||||||
const offset = (Number(page) - 1) * Number(limit);
|
type: 'income',
|
||||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
category: '红包收入'
|
||||||
|
});
|
||||||
const total = db.prepare(
|
res.json(result);
|
||||||
`SELECT COUNT(*) as count FROM records WHERE user_id = ? AND type = 'income' AND category = '红包收入' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?`
|
|
||||||
).get(req.user.id, startDate, endDate).count;
|
|
||||||
|
|
||||||
const records = db.prepare(
|
|
||||||
`SELECT * FROM records WHERE user_id = ? AND type = 'income' AND category = '红包收入' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
||||||
).all(req.user.id, startDate, endDate, Number(limit), offset);
|
|
||||||
|
|
||||||
res.json({ records, total, page: Number(page), limit: Number(limit) });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 奶茶收入列表
|
// 奶茶收入列表
|
||||||
router.get('/milk-tea', (req, res) => {
|
router.get('/milk-tea', (req, res) => {
|
||||||
const { page = 1, limit = 20 } = req.query;
|
const result = queryPagedRecords(req.user.id, req.query.year, req.query.month, {
|
||||||
const offset = (Number(page) - 1) * Number(limit);
|
type: 'income',
|
||||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
category: '奶茶'
|
||||||
|
});
|
||||||
const total = db.prepare(
|
res.json(result);
|
||||||
`SELECT COUNT(*) as count FROM records WHERE user_id = ? AND type = 'income' AND category = '奶茶' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?`
|
|
||||||
).get(req.user.id, startDate, endDate).count;
|
|
||||||
|
|
||||||
const records = db.prepare(
|
|
||||||
`SELECT * FROM records WHERE user_id = ? AND type = 'income' AND category = '奶茶' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
||||||
).all(req.user.id, startDate, endDate, Number(limit), offset);
|
|
||||||
|
|
||||||
res.json({ records, total, page: Number(page), limit: Number(limit) });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 派单列表
|
// 派单列表
|
||||||
router.get('/dispatches', (req, res) => {
|
router.get('/dispatches', (req, res) => {
|
||||||
const { page = 1, limit = 20 } = req.query;
|
const result = queryPagedRecords(req.user.id, req.query.year, req.query.month, {
|
||||||
const offset = (Number(page) - 1) * Number(limit);
|
category: '派单'
|
||||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
});
|
||||||
|
res.json(result);
|
||||||
const total = db.prepare(
|
|
||||||
`SELECT COUNT(*) as count FROM records WHERE user_id = ? AND category = '派单' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?`
|
|
||||||
).get(req.user.id, startDate, endDate).count;
|
|
||||||
|
|
||||||
const records = db.prepare(
|
|
||||||
`SELECT * FROM records WHERE user_id = ? AND category = '派单' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
||||||
).all(req.user.id, startDate, endDate, Number(limit), offset);
|
|
||||||
|
|
||||||
res.json({ records, total, page: Number(page), limit: Number(limit) });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取记录列表
|
// 获取记录列表
|
||||||
|
|||||||
+47
-14
@@ -35,22 +35,28 @@ router.get('/monthly', (req, res) => {
|
|||||||
|
|
||||||
// 月收入/支出记录列表
|
// 月收入/支出记录列表
|
||||||
router.get('/monthly-records', (req, res) => {
|
router.get('/monthly-records', (req, res) => {
|
||||||
const { type } = req.query;
|
const { type, page = 1, limit = 20 } = req.query;
|
||||||
if (!type || !['income', 'expense'].includes(type)) return res.status(400).json({ error: '缺少类型参数' });
|
if (!type || !['income', 'expense'].includes(type)) return res.status(400).json({ error: '缺少类型参数' });
|
||||||
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
const { startDate, endDate } = getMonthRange(req.query.year, req.query.month);
|
||||||
|
const offset = (Number(page) - 1) * Number(limit);
|
||||||
|
|
||||||
const summary = db.prepare(`
|
const summary = db.prepare(`
|
||||||
SELECT COALESCE(SUM(amount), 0) as total
|
SELECT COALESCE(SUM(amount), 0) as total
|
||||||
FROM records WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
FROM records WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
`).get(req.user.id, type, startDate, endDate);
|
`).get(req.user.id, type, startDate, endDate);
|
||||||
|
|
||||||
|
const total = db.prepare(`
|
||||||
|
SELECT COUNT(*) as count FROM records
|
||||||
|
WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
|
`).get(req.user.id, type, startDate, endDate).count;
|
||||||
|
|
||||||
const records = db.prepare(`
|
const records = db.prepare(`
|
||||||
SELECT * FROM records
|
SELECT * FROM records
|
||||||
WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
WHERE user_id = ? AND type = ? AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC LIMIT ? OFFSET ?
|
||||||
`).all(req.user.id, type, startDate, endDate);
|
`).all(req.user.id, type, startDate, endDate, Number(limit), offset);
|
||||||
|
|
||||||
res.json({ total: summary.total, records });
|
res.json({ total: summary.total, records, page: Number(page), limit: Number(limit), totalCount: total });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 年度净收入按年统计
|
// 年度净收入按年统计
|
||||||
@@ -236,7 +242,9 @@ router.get('/total', (req, res) => {
|
|||||||
// 老板交易记录
|
// 老板交易记录
|
||||||
router.get('/boss-records', (req, res) => {
|
router.get('/boss-records', (req, res) => {
|
||||||
const boss = req.query.boss;
|
const boss = req.query.boss;
|
||||||
|
const { page = 1, limit = 20 } = req.query;
|
||||||
if (!boss) return res.status(400).json({ error: '缺少老板参数' });
|
if (!boss) return res.status(400).json({ error: '缺少老板参数' });
|
||||||
|
const offset = (Number(page) - 1) * Number(limit);
|
||||||
|
|
||||||
// 我的收入:只统计 type='income' 的记录(接单、派单返点、红包收入等)
|
// 我的收入:只统计 type='income' 的记录(接单、派单返点、红包收入等)
|
||||||
// 使用 COALESCE(boss, source) 兼容旧数据(之前的红包收入只有 source 字段)
|
// 使用 COALESCE(boss, source) 兼容旧数据(之前的红包收入只有 source 字段)
|
||||||
@@ -256,21 +264,30 @@ router.get('/boss-records', (req, res) => {
|
|||||||
|
|
||||||
const boss_total_expense = expenseSummary.dispatch_expense + expenseSummary.order_expense + expenseSummary.redEnvelope_expense;
|
const boss_total_expense = expenseSummary.dispatch_expense + expenseSummary.order_expense + expenseSummary.redEnvelope_expense;
|
||||||
|
|
||||||
|
const total = db.prepare(`
|
||||||
|
SELECT COUNT(*) as count FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?
|
||||||
|
`).get(req.user.id, boss).count;
|
||||||
|
|
||||||
const records = db.prepare(`
|
const records = db.prepare(`
|
||||||
SELECT * FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? ORDER BY created_at DESC
|
SELECT * FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? ORDER BY created_at DESC LIMIT ? OFFSET ?
|
||||||
`).all(req.user.id, boss);
|
`).all(req.user.id, boss, Number(limit), offset);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
my_income: myIncomeSummary.total,
|
my_income: myIncomeSummary.total,
|
||||||
boss_total_expense: boss_total_expense,
|
boss_total_expense: boss_total_expense,
|
||||||
records
|
records,
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
totalCount: total
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 陪玩交易记录
|
// 陪玩交易记录
|
||||||
router.get('/partner-records', (req, res) => {
|
router.get('/partner-records', (req, res) => {
|
||||||
const partner = req.query.partner;
|
const partner = req.query.partner;
|
||||||
|
const { page = 1, limit = 20 } = req.query;
|
||||||
if (!partner) return res.status(400).json({ error: '缺少陪玩参数' });
|
if (!partner) return res.status(400).json({ error: '缺少陪玩参数' });
|
||||||
|
const offset = (Number(page) - 1) * Number(limit);
|
||||||
|
|
||||||
const dispatchStats = db.prepare(`
|
const dispatchStats = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -285,20 +302,31 @@ router.get('/partner-records', (req, res) => {
|
|||||||
FROM records WHERE user_id = ? AND type = 'income' AND category != '派单' AND category != '接单' AND source = ?
|
FROM records WHERE user_id = ? AND type = 'income' AND category != '派单' AND category != '接单' AND source = ?
|
||||||
`).get(req.user.id, partner);
|
`).get(req.user.id, partner);
|
||||||
|
|
||||||
|
const total = db.prepare(`
|
||||||
|
SELECT COUNT(*) as count FROM records
|
||||||
|
WHERE user_id = ? AND (
|
||||||
|
(category = '派单' AND partner = ?) OR
|
||||||
|
(type = 'income' AND category != '派单' AND category != '接单' AND source = ?)
|
||||||
|
)
|
||||||
|
`).get(req.user.id, partner, partner).count;
|
||||||
|
|
||||||
const records = db.prepare(`
|
const records = db.prepare(`
|
||||||
SELECT * FROM records
|
SELECT * FROM records
|
||||||
WHERE user_id = ? AND (
|
WHERE user_id = ? AND (
|
||||||
(category = '派单' AND partner = ?) OR
|
(category = '派单' AND partner = ?) OR
|
||||||
(type = 'income' AND category != '派单' AND category != '接单' AND source = ?)
|
(type = 'income' AND category != '派单' AND category != '接单' AND source = ?)
|
||||||
) ORDER BY created_at DESC
|
) ORDER BY created_at DESC LIMIT ? OFFSET ?
|
||||||
`).all(req.user.id, partner, partner);
|
`).all(req.user.id, partner, partner, Number(limit), offset);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
dispatch_count: dispatchStats.dispatch_count,
|
dispatch_count: dispatchStats.dispatch_count,
|
||||||
spread_income: dispatchStats.spread_income,
|
spread_income: dispatchStats.spread_income,
|
||||||
rebate_income: dispatchStats.rebate_income,
|
rebate_income: dispatchStats.rebate_income,
|
||||||
other_income: otherIncome.total,
|
other_income: otherIncome.total,
|
||||||
records
|
records,
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
totalCount: total
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -329,8 +357,9 @@ router.get('/daily-income', (req, res) => {
|
|||||||
|
|
||||||
// 某天收入记录详情
|
// 某天收入记录详情
|
||||||
router.get('/daily-detail', (req, res) => {
|
router.get('/daily-detail', (req, res) => {
|
||||||
const { date } = req.query;
|
const { date, page = 1, limit = 20 } = req.query;
|
||||||
if (!date) return res.status(400).json({ error: '缺少日期参数' });
|
if (!date) return res.status(400).json({ error: '缺少日期参数' });
|
||||||
|
const offset = (Number(page) - 1) * Number(limit);
|
||||||
const nextDay = new Date(date);
|
const nextDay = new Date(date);
|
||||||
nextDay.setDate(nextDay.getDate() + 1);
|
nextDay.setDate(nextDay.getDate() + 1);
|
||||||
const endDate = formatLocalDate(nextDay);
|
const endDate = formatLocalDate(nextDay);
|
||||||
@@ -340,12 +369,16 @@ router.get('/daily-detail', (req, res) => {
|
|||||||
FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
`).get(req.user.id, date, endDate);
|
`).get(req.user.id, date, endDate);
|
||||||
|
|
||||||
|
const total = db.prepare(`
|
||||||
|
SELECT COUNT(*) as count FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
|
`).get(req.user.id, date, endDate).count;
|
||||||
|
|
||||||
const records = db.prepare(`
|
const records = db.prepare(`
|
||||||
SELECT * FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
SELECT * FROM records WHERE user_id = ? AND type = 'income' AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') < ?
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC LIMIT ? OFFSET ?
|
||||||
`).all(req.user.id, date, endDate);
|
`).all(req.user.id, date, endDate, Number(limit), offset);
|
||||||
|
|
||||||
res.json({ total_income: summary.total_income, records });
|
res.json({ total_income: summary.total_income, records, page: Number(page), limit: Number(limit), totalCount: total });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 老板收入排行 - 首页预览
|
// 老板收入排行 - 首页预览
|
||||||
|
|||||||
Reference in New Issue
Block a user