Compare commits
10
Commits
a0d7bfdc3d
...
3a4048a29e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a4048a29e | ||
|
|
f381f93507 | ||
|
|
d473e3a431 | ||
|
|
0b450a65ed | ||
|
|
14927f74aa | ||
|
|
3d8d03a66e | ||
|
|
a0466fda3b | ||
|
|
2ebac98f07 | ||
|
|
a03e92d437 | ||
|
|
4656f3f9ca |
+16
-1
@@ -159,7 +159,7 @@ body {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.weekly-chart-bars {
|
.weekly-chart-bars {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -215,6 +215,21 @@ body {
|
|||||||
.daily-nav-date {
|
.daily-nav-date {
|
||||||
font-size: 15px; font-weight: 600; color: #333;
|
font-size: 15px; font-weight: 600; color: #333;
|
||||||
}
|
}
|
||||||
|
.scope-toggle {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #667eea;
|
||||||
|
background: #fff;
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.scope-toggle.active {
|
||||||
|
background: #667eea;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
.daily-summary {
|
.daily-summary {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|||||||
@@ -284,9 +284,16 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<button class="back" onclick="bossDetailGoBack()">←</button>
|
<button class="back" onclick="bossDetailGoBack()">←</button>
|
||||||
<h2 id="bossDetailTitle">交易记录</h2>
|
<h2 id="bossDetailTitle">交易记录</h2>
|
||||||
|
<button class="scope-toggle" id="bossDetailAllBtn" onclick="toggleBossDetailAll()">全部</button>
|
||||||
|
</div>
|
||||||
|
<div class="daily-nav">
|
||||||
|
<button class="daily-nav-btn" onclick="switchBossDetailScope(-1)">← 上月</button>
|
||||||
|
<span class="daily-nav-date" id="bossDetailScope">2026年3月</span>
|
||||||
|
<button class="daily-nav-btn" onclick="switchBossDetailScope(1)">下月 →</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="daily-summary" id="bossDetailSummary"></div>
|
<div class="daily-summary" id="bossDetailSummary"></div>
|
||||||
<div id="bossDetailList" class="order-list"></div>
|
<div id="bossDetailList" class="order-list"></div>
|
||||||
|
<div id="bossDetailMore" class="order-loading" style="display:none;text-align:center;padding:12px;color:#667eea">加载更多</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 陪玩交易详情页 -->
|
<!-- 陪玩交易详情页 -->
|
||||||
@@ -294,9 +301,16 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<button class="back" onclick="partnerDetailGoBack()">←</button>
|
<button class="back" onclick="partnerDetailGoBack()">←</button>
|
||||||
<h2 id="partnerDetailTitle">交易记录</h2>
|
<h2 id="partnerDetailTitle">交易记录</h2>
|
||||||
|
<button class="scope-toggle" id="partnerDetailAllBtn" onclick="togglePartnerDetailAll()">全部</button>
|
||||||
|
</div>
|
||||||
|
<div class="daily-nav">
|
||||||
|
<button class="daily-nav-btn" onclick="switchPartnerDetailScope(-1)">← 上月</button>
|
||||||
|
<span class="daily-nav-date" id="partnerDetailScope">2026年3月</span>
|
||||||
|
<button class="daily-nav-btn" onclick="switchPartnerDetailScope(1)">下月 →</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="daily-summary" id="partnerDetailSummary"></div>
|
<div class="daily-summary" id="partnerDetailSummary"></div>
|
||||||
<div id="partnerDetailList" class="order-list"></div>
|
<div id="partnerDetailList" class="order-list"></div>
|
||||||
|
<div id="partnerDetailMore" class="order-loading" style="display:none;text-align:center;padding:12px;color:#667eea">加载更多</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 红包及其他收入列表页 -->
|
<!-- 红包及其他收入列表页 -->
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ async function openTakeRanking() {
|
|||||||
const res = await fetch(API + '/api/stats/take-ranking', { headers: headers() });
|
const res = await fetch(API + '/api/stats/take-ranking', { headers: headers() });
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
console.log('take-ranking data:', data);
|
|
||||||
const rows = data.rows || [];
|
const rows = data.rows || [];
|
||||||
|
|
||||||
// 显示统计卡片
|
// 显示统计卡片
|
||||||
|
|||||||
+98
-270
@@ -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,279 +40,89 @@ 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);
|
||||||
if (!append) {
|
|
||||||
const today = formatLocalDate(new Date());
|
|
||||||
setTimeout(() => {
|
|
||||||
const el = container.querySelector(`[data-date="${today}"]`);
|
|
||||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} 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);
|
||||||
|
const unitPrice = r.unit_price || (r.quantity ? r.amount / r.quantity : 0);
|
||||||
|
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 || 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, "'") + ')\'>' +
|
||||||
|
'<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';
|
|
||||||
}
|
|
||||||
|
|||||||
+404
-114
@@ -1,22 +1,42 @@
|
|||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(API + '/api/stats/monthly?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
// 并行加载所有数据
|
||||||
if (!res.ok) return;
|
const [monthlyRes, bossRankRes, partnerRankRes, weeklyRes] = await Promise.all([
|
||||||
const s = await res.json();
|
fetch(API + '/api/stats/monthly?year=' + currentYear + '&month=' + currentMonth, { headers: headers() }),
|
||||||
document.getElementById('takeCount').textContent = s.take_order_count;
|
fetch(API + '/api/stats/boss-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() }),
|
||||||
document.getElementById('dispatchCount').textContent = s.dispatch_order_count;
|
fetch(API + '/api/stats/partner-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() }),
|
||||||
document.getElementById('takeIncome').textContent = '' + s.take_order_income.toFixed(2);
|
fetch(API + '/api/stats/daily-income', { headers: headers() })
|
||||||
document.getElementById('dispatchIncome').textContent = '' + s.dispatch_order_income.toFixed(2);
|
]);
|
||||||
document.getElementById('redEnvelopeCount').textContent = s.redEnvelope_count || 0;
|
|
||||||
document.getElementById('redEnvelopeIncome').textContent = '' + (s.redEnvelope_income || 0).toFixed(2);
|
if (monthlyRes.ok) {
|
||||||
document.getElementById('milkTeaCount').textContent = s.milkTea_count || 0;
|
const s = await monthlyRes.json();
|
||||||
document.getElementById('milkTeaIncome').textContent = '' + (s.milkTea_income || 0).toFixed(2);
|
document.getElementById('takeCount').textContent = s.take_order_count;
|
||||||
document.getElementById('totalIncome').textContent = '' + s.total_income.toFixed(2);
|
document.getElementById('dispatchCount').textContent = s.dispatch_order_count;
|
||||||
document.getElementById('totalExpense').textContent = '' + s.total_expense.toFixed(2);
|
document.getElementById('takeIncome').textContent = '' + s.take_order_income.toFixed(2);
|
||||||
document.getElementById('netIncome').textContent = '' + (s.total_income - s.total_expense).toFixed(2);
|
document.getElementById('dispatchIncome').textContent = '' + s.dispatch_order_income.toFixed(2);
|
||||||
loadBossRankingPreview();
|
document.getElementById('redEnvelopeCount').textContent = s.redEnvelope_count || 0;
|
||||||
loadPartnerRankingPreview();
|
document.getElementById('redEnvelopeIncome').textContent = '' + (s.redEnvelope_income || 0).toFixed(2);
|
||||||
loadWeeklyChart();
|
document.getElementById('milkTeaCount').textContent = s.milkTea_count || 0;
|
||||||
|
document.getElementById('milkTeaIncome').textContent = '' + (s.milkTea_income || 0).toFixed(2);
|
||||||
|
document.getElementById('totalIncome').textContent = '' + s.total_income.toFixed(2);
|
||||||
|
document.getElementById('totalExpense').textContent = '' + s.total_expense.toFixed(2);
|
||||||
|
document.getElementById('netIncome').textContent = '' + (s.total_income - s.total_expense).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bossRankRes.ok) {
|
||||||
|
const data = await bossRankRes.json();
|
||||||
|
renderBossRankingPreview(data || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partnerRankRes.ok) {
|
||||||
|
const data = await partnerRankRes.json();
|
||||||
|
renderPartnerRankingPreview(data || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weeklyRes.ok) {
|
||||||
|
const days = await weeklyRes.json();
|
||||||
|
renderWeeklyChart(days);
|
||||||
|
}
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,21 +45,30 @@ async function loadWeeklyChart() {
|
|||||||
const res = await fetch(API + '/api/stats/daily-income', { headers: headers() });
|
const res = await fetch(API + '/api/stats/daily-income', { headers: headers() });
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const days = await res.json();
|
const days = await res.json();
|
||||||
const container = document.getElementById('weeklyChartBars');
|
renderWeeklyChart(days);
|
||||||
const max = Math.max(...days.map(d => d.income), 1);
|
|
||||||
container.innerHTML = days.map(d => {
|
|
||||||
const pct = Math.max((d.income / max) * 100, 3);
|
|
||||||
const label = d.date.slice(5).replace('-', '/');
|
|
||||||
const amount = d.income > 0 ? '' + d.income.toFixed(0) : '0';
|
|
||||||
return `<div class="weekly-bar-col" onclick="openDailyDetail('${d.date}')">
|
|
||||||
<span class="weekly-bar-amount">${amount}</span>
|
|
||||||
<div class="weekly-bar" style="height:${pct}%"></div>
|
|
||||||
<span class="weekly-bar-date">${label}</span>
|
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderWeeklyChart(days) {
|
||||||
|
const container = document.getElementById('weeklyChartBars');
|
||||||
|
if (!days || days.length === 0) {
|
||||||
|
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const max = Math.max(...days.map(d => d.income), 1);
|
||||||
|
const barMaxHeight = 110; // 柱状图最大可用像素高度
|
||||||
|
container.innerHTML = days.map(d => {
|
||||||
|
const height = Math.max((d.income / max) * barMaxHeight, 4);
|
||||||
|
const label = d.date.slice(5).replace('-', '/');
|
||||||
|
const amount = d.income > 0 ? '' + d.income.toFixed(0) : '0';
|
||||||
|
return `<div class="weekly-bar-col" onclick="openDailyDetail('${d.date}')">
|
||||||
|
<span class="weekly-bar-amount">${amount}</span>
|
||||||
|
<div class="weekly-bar" style="height:${height}px"></div>
|
||||||
|
<span class="weekly-bar-date">${label}</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
let currentDailyDate = '';
|
let currentDailyDate = '';
|
||||||
|
|
||||||
async function openDailyDetail(date) {
|
async function openDailyDetail(date) {
|
||||||
@@ -232,66 +261,181 @@ async function loadBossRankingPreview() {
|
|||||||
const res = await fetch(API + '/api/stats/boss-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
const res = await fetch(API + '/api/stats/boss-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const rows = await res.json();
|
const rows = await res.json();
|
||||||
const container = document.getElementById('rankingPreviewList');
|
renderBossRankingPreview(rows);
|
||||||
if (rows.length === 0) {
|
|
||||||
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const max = Math.max(...rows.map(r => r.total_income), 1);
|
|
||||||
container.innerHTML = rows.map((r, i) => {
|
|
||||||
const pct = Math.max((r.total_income / max) * 100, 2);
|
|
||||||
return `<div class="hbar-row" onclick="openBossDetail('${r.boss.replace(/'/g, "\\'")}', 'homePage')">
|
|
||||||
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
|
||||||
<div class="hbar-label" title="${r.boss}">${r.boss}</div>
|
|
||||||
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
|
|
||||||
<div class="hbar-value">${r.total_income.toFixed(2)}</div>
|
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 渲染横向柱状图排行榜的通用函数
|
||||||
|
function renderHBarRanking(rows, { containerId, getValue, getLabel, onClick, formatValue }) {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (!rows || rows.length === 0) {
|
||||||
|
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const max = Math.max(...rows.map(r => getValue(r)), 1);
|
||||||
|
container.innerHTML = rows.map((r, i) => {
|
||||||
|
const pct = Math.max((getValue(r) / max) * 100, 2);
|
||||||
|
const label = getLabel(r).replace(/'/g, "\\'");
|
||||||
|
const value = formatValue ? formatValue(getValue(r)) : (typeof getValue(r) === 'number' ? getValue(r).toFixed(2) : getValue(r));
|
||||||
|
return `<div class="hbar-row" onclick="${onClick}('${label}', 'homePage')">
|
||||||
|
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
||||||
|
<div class="hbar-label" title="${label}">${label}</div>
|
||||||
|
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
|
||||||
|
<div class="hbar-value">${value}</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBossRankingPreview(rows) {
|
||||||
|
renderHBarRanking(rows, {
|
||||||
|
containerId: 'rankingPreviewList',
|
||||||
|
getValue: r => r.total_income,
|
||||||
|
getLabel: r => r.boss,
|
||||||
|
onClick: 'openBossDetail'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let currentBossName = '';
|
let currentBossName = '';
|
||||||
let bossDetailFrom = 'homePage';
|
let bossDetailFrom = 'homePage';
|
||||||
|
let bossDetailYear = 0;
|
||||||
|
let bossDetailMonth = 0;
|
||||||
|
let bossDetailScopeAll = false;
|
||||||
|
let bossDetailPage = 1;
|
||||||
|
let bossDetailLimit = 20;
|
||||||
|
let bossDetailTotalCount = 0;
|
||||||
|
let bossDetailAllRecords = [];
|
||||||
|
let bossDetailLoading = false;
|
||||||
|
let bossDetailScrollHandler = null;
|
||||||
|
|
||||||
function openBossDetail(boss, from) {
|
function openBossDetail(boss, from) {
|
||||||
currentBossName = boss;
|
currentBossName = boss;
|
||||||
bossDetailFrom = from || 'homePage';
|
bossDetailFrom = from || 'homePage';
|
||||||
|
bossDetailYear = currentYear;
|
||||||
|
bossDetailMonth = currentMonth;
|
||||||
|
// 从排行榜进入时默认显示全部
|
||||||
|
bossDetailScopeAll = (from === 'rankingPage' || from === 'takeRankingPage');
|
||||||
|
bossDetailPage = 1;
|
||||||
|
bossDetailLimit = bossDetailScopeAll ? 500 : 20;
|
||||||
|
bossDetailTotalCount = 0;
|
||||||
|
bossDetailAllRecords = [];
|
||||||
|
updateBossDetailNav();
|
||||||
loadBossDetail();
|
loadBossDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
function bossDetailGoBack() {
|
function bossDetailGoBack() {
|
||||||
|
if (bossDetailScrollHandler) {
|
||||||
|
window.removeEventListener('scroll', bossDetailScrollHandler);
|
||||||
|
bossDetailScrollHandler = null;
|
||||||
|
}
|
||||||
showPage(bossDetailFrom);
|
showPage(bossDetailFrom);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadBossDetail() {
|
function updateBossDetailNav() {
|
||||||
document.getElementById('bossDetailTitle').textContent = currentBossName + ' 交易记录';
|
const scopeEl = document.getElementById('bossDetailScope');
|
||||||
document.getElementById('bossDetailSummary').innerHTML = '<div class="ranking-empty">加载中...</div>';
|
const allBtn = document.getElementById('bossDetailAllBtn');
|
||||||
document.getElementById('bossDetailList').innerHTML = '';
|
if (bossDetailScopeAll) {
|
||||||
showPage('bossDetailPage');
|
scopeEl.textContent = '全部';
|
||||||
|
allBtn.textContent = '当月';
|
||||||
|
allBtn.classList.add('active');
|
||||||
|
} else {
|
||||||
|
scopeEl.textContent = `${bossDetailYear}年${bossDetailMonth}月`;
|
||||||
|
allBtn.textContent = '全部';
|
||||||
|
allBtn.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchBossDetailScope(offset) {
|
||||||
|
if (bossDetailScopeAll) return;
|
||||||
|
bossDetailMonth += offset;
|
||||||
|
if (bossDetailMonth > 12) { bossDetailMonth = 1; bossDetailYear++; }
|
||||||
|
if (bossDetailMonth < 1) { bossDetailMonth = 12; bossDetailYear--; }
|
||||||
|
bossDetailPage = 1;
|
||||||
|
bossDetailAllRecords = [];
|
||||||
|
updateBossDetailNav();
|
||||||
|
loadBossDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBossDetailAll() {
|
||||||
|
bossDetailScopeAll = !bossDetailScopeAll;
|
||||||
|
bossDetailPage = 1;
|
||||||
|
bossDetailAllRecords = [];
|
||||||
|
if (bossDetailScopeAll) {
|
||||||
|
bossDetailLimit = 500;
|
||||||
|
} else {
|
||||||
|
bossDetailLimit = 20;
|
||||||
|
bossDetailYear = currentYear;
|
||||||
|
bossDetailMonth = currentMonth;
|
||||||
|
}
|
||||||
|
updateBossDetailNav();
|
||||||
|
loadBossDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMoreBossDetail() {
|
||||||
|
bossDetailPage++;
|
||||||
|
loadBossDetail(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBossDetail(append) {
|
||||||
|
if (bossDetailLoading) return;
|
||||||
|
bossDetailLoading = true;
|
||||||
|
|
||||||
|
if (!append) {
|
||||||
|
document.getElementById('bossDetailTitle').textContent = currentBossName + ' 交易记录';
|
||||||
|
document.getElementById('bossDetailSummary').innerHTML = '<div class="ranking-empty">加载中...</div>';
|
||||||
|
document.getElementById('bossDetailList').innerHTML = '';
|
||||||
|
showPage('bossDetailPage');
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = API + '/api/stats/boss-records?boss=' + encodeURIComponent(currentBossName) + '&page=' + bossDetailPage + '&limit=' + bossDetailLimit;
|
||||||
|
if (!bossDetailScopeAll) {
|
||||||
|
url += '&year=' + bossDetailYear + '&month=' + bossDetailMonth;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(API + '/api/stats/boss-records?boss=' + encodeURIComponent(currentBossName), { headers: headers() });
|
const res = await fetch(url, { headers: headers() });
|
||||||
if (!res.ok) return;
|
if (!res.ok) { bossDetailLoading = false; return; }
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
document.getElementById('bossDetailSummary').innerHTML = `
|
|
||||||
<div style="display:flex;justify-content:space-around;flex-wrap:wrap;gap:8px">
|
if (!append) {
|
||||||
<div style="text-align:center">
|
document.getElementById('bossDetailSummary').innerHTML = `
|
||||||
<div class="daily-summary-amount" style="font-size:24px;color:#e74c3c">${data.boss_total_expense.toFixed(2)}</div>
|
<div style="display:flex;justify-content:space-around;flex-wrap:wrap;gap:8px">
|
||||||
<div class="daily-summary-label">老板总支出</div>
|
<div style="text-align:center">
|
||||||
|
<div class="daily-summary-amount" style="font-size:24px;color:#e74c3c">${data.boss_total_expense.toFixed(2)}</div>
|
||||||
|
<div class="daily-summary-label">老板总支出</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center">
|
||||||
|
<div class="daily-summary-amount" style="font-size:24px;color:#27ae60">${data.my_income.toFixed(2)}</div>
|
||||||
|
<div class="daily-summary-label">我的总收入</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center">
|
||||||
|
<div class="daily-summary-amount" style="font-size:24px;color:#3498db">${data.take_count || 0}</div>
|
||||||
|
<div class="daily-summary-label">接单数</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center">
|
||||||
|
<div class="daily-summary-amount" style="font-size:24px;color:#9b59b6">${data.dispatch_count || 0}</div>
|
||||||
|
<div class="daily-summary-label">派单数</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:center">
|
`;
|
||||||
<div class="daily-summary-amount" style="font-size:24px;color:#27ae60">${data.my_income.toFixed(2)}</div>
|
bossDetailAllRecords = data.records || [];
|
||||||
<div class="daily-summary-label">我的总收入</div>
|
setupBossDetailScroll();
|
||||||
</div>
|
} else {
|
||||||
</div>
|
bossDetailAllRecords = bossDetailAllRecords.concat(data.records || []);
|
||||||
`;
|
}
|
||||||
|
bossDetailTotalCount = data.totalCount || 0;
|
||||||
|
|
||||||
const container = document.getElementById('bossDetailList');
|
const container = document.getElementById('bossDetailList');
|
||||||
if (data.records.length === 0) {
|
const moreBtn = document.getElementById('bossDetailMore');
|
||||||
|
|
||||||
|
if (bossDetailAllRecords.length === 0) {
|
||||||
container.innerHTML = '<div class="ranking-empty">暂无交易记录</div>';
|
container.innerHTML = '<div class="ranking-empty">暂无交易记录</div>';
|
||||||
|
if (moreBtn) moreBtn.style.display = 'none';
|
||||||
|
bossDetailLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
data.records.forEach(r => {
|
bossDetailAllRecords.forEach(r => {
|
||||||
const d = new Date(r.created_at);
|
const d = new Date(r.created_at);
|
||||||
const key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
|
const key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
|
||||||
if (!grouped[key]) grouped[key] = { records: [], total: 0 };
|
if (!grouped[key]) grouped[key] = { records: [], total: 0 };
|
||||||
@@ -318,7 +462,42 @@ async function loadBossDetail() {
|
|||||||
html += '</div>';
|
html += '</div>';
|
||||||
}
|
}
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
} catch (e) { console.error(e); }
|
|
||||||
|
// Show/hide load more indicator
|
||||||
|
if (moreBtn) {
|
||||||
|
if (bossDetailAllRecords.length < bossDetailTotalCount) {
|
||||||
|
moreBtn.style.display = 'block';
|
||||||
|
moreBtn.textContent = '加载中... (' + bossDetailAllRecords.length + '/' + bossDetailTotalCount + ')';
|
||||||
|
} else {
|
||||||
|
moreBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bossDetailLoading = false;
|
||||||
|
} catch (e) {
|
||||||
|
bossDetailLoading = false;
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupBossDetailScroll() {
|
||||||
|
if (bossDetailScrollHandler) {
|
||||||
|
window.removeEventListener('scroll', bossDetailScrollHandler);
|
||||||
|
}
|
||||||
|
bossDetailScrollHandler = function() {
|
||||||
|
if (bossDetailLoading) return;
|
||||||
|
if (bossDetailScopeAll && bossDetailAllRecords.length >= bossDetailTotalCount) {
|
||||||
|
window.removeEventListener('scroll', bossDetailScrollHandler);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
const docHeight = document.documentElement.scrollHeight;
|
||||||
|
if (scrollY + windowHeight >= docHeight - 200) {
|
||||||
|
bossDetailPage++;
|
||||||
|
loadBossDetail(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', bossDetailScrollHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPartnerRankingPreview() {
|
async function loadPartnerRankingPreview() {
|
||||||
@@ -326,24 +505,20 @@ async function loadPartnerRankingPreview() {
|
|||||||
const res = await fetch(API + '/api/stats/partner-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
const res = await fetch(API + '/api/stats/partner-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const rows = await res.json();
|
const rows = await res.json();
|
||||||
const container = document.getElementById('partnerRankingPreviewList');
|
renderPartnerRankingPreview(rows);
|
||||||
if (rows.length === 0) {
|
|
||||||
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const max = Math.max(...rows.map(r => r.dispatch_count), 1);
|
|
||||||
container.innerHTML = rows.map((r, i) => {
|
|
||||||
const pct = Math.max((r.dispatch_count / max) * 100, 2);
|
|
||||||
return `<div class="hbar-row" onclick="openPartnerDetail('${r.partner.replace(/'/g, "\\'")}', 'homePage')">
|
|
||||||
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
|
||||||
<div class="hbar-label" title="${r.partner}">${r.partner}</div>
|
|
||||||
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
|
|
||||||
<div class="hbar-value">${r.dispatch_count}单</div>
|
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPartnerRankingPreview(rows) {
|
||||||
|
renderHBarRanking(rows, {
|
||||||
|
containerId: 'partnerRankingPreviewList',
|
||||||
|
getValue: r => r.dispatch_count,
|
||||||
|
getLabel: r => r.partner,
|
||||||
|
onClick: 'openPartnerDetail',
|
||||||
|
formatValue: v => v + '单'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function openPartnerRankingPage() {
|
function openPartnerRankingPage() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const y = now.getFullYear();
|
const y = now.getFullYear();
|
||||||
@@ -366,20 +541,15 @@ async function loadPartnerRankingDetail() {
|
|||||||
params.push('endDate=' + formatLocalDate(d));
|
params.push('endDate=' + formatLocalDate(d));
|
||||||
}
|
}
|
||||||
if (params.length) url += '?' + params.join('&');
|
if (params.length) url += '?' + params.join('&');
|
||||||
console.log('partner-ranking url:', url);
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, { headers: headers() });
|
const res = await fetch(url, { headers: headers() });
|
||||||
console.log('partner-ranking res.ok:', res.ok);
|
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
console.log('partner-ranking data:', data);
|
|
||||||
const rows = data.rows || [];
|
const rows = data.rows || [];
|
||||||
console.log('rows:', rows);
|
|
||||||
const summary = data.summary || { total_partner_income: 0, total_rebate_income: 0, total_milkTea_income: 0 };
|
const summary = data.summary || { total_partner_income: 0, total_rebate_income: 0, total_milkTea_income: 0 };
|
||||||
|
|
||||||
// 计算派单总数(从列表)
|
// 计算派单总数(从列表)
|
||||||
const totalDispatch = rows.reduce((sum, r) => sum + (r.dispatch_count || 0), 0);
|
const totalDispatch = rows.reduce((sum, r) => sum + (r.dispatch_count || 0), 0);
|
||||||
console.log('totalDispatch:', totalDispatch);
|
|
||||||
|
|
||||||
// 更新统计卡片
|
// 更新统计卡片
|
||||||
document.getElementById('partnerTotalDispatchCount').textContent = totalDispatch;
|
document.getElementById('partnerTotalDispatchCount').textContent = totalDispatch;
|
||||||
@@ -407,54 +577,140 @@ async function loadPartnerRankingDetail() {
|
|||||||
|
|
||||||
let currentPartnerName = '';
|
let currentPartnerName = '';
|
||||||
let partnerDetailFrom = 'homePage';
|
let partnerDetailFrom = 'homePage';
|
||||||
|
let partnerDetailYear = 0;
|
||||||
|
let partnerDetailMonth = 0;
|
||||||
|
let partnerDetailScopeAll = false;
|
||||||
|
let partnerDetailPage = 1;
|
||||||
|
let partnerDetailLimit = 20;
|
||||||
|
let partnerDetailTotalCount = 0;
|
||||||
|
let partnerDetailAllRecords = [];
|
||||||
|
let partnerDetailLoading = false;
|
||||||
|
let partnerDetailScrollHandler = null;
|
||||||
|
|
||||||
function openPartnerDetail(partner, from) {
|
function openPartnerDetail(partner, from) {
|
||||||
currentPartnerName = partner;
|
currentPartnerName = partner;
|
||||||
partnerDetailFrom = from || 'homePage';
|
partnerDetailFrom = from || 'homePage';
|
||||||
|
partnerDetailYear = currentYear;
|
||||||
|
partnerDetailMonth = currentMonth;
|
||||||
|
// 从排行榜进入时默认显示全部
|
||||||
|
partnerDetailScopeAll = (from === 'dispatchRankingPage' || from === 'partnerRankingPage');
|
||||||
|
partnerDetailPage = 1;
|
||||||
|
partnerDetailLimit = partnerDetailScopeAll ? 500 : 20;
|
||||||
|
partnerDetailTotalCount = 0;
|
||||||
|
partnerDetailAllRecords = [];
|
||||||
|
updatePartnerDetailNav();
|
||||||
loadPartnerDetail();
|
loadPartnerDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
function partnerDetailGoBack() {
|
function partnerDetailGoBack() {
|
||||||
|
if (partnerDetailScrollHandler) {
|
||||||
|
window.removeEventListener('scroll', partnerDetailScrollHandler);
|
||||||
|
partnerDetailScrollHandler = null;
|
||||||
|
}
|
||||||
showPage(partnerDetailFrom);
|
showPage(partnerDetailFrom);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPartnerDetail() {
|
function updatePartnerDetailNav() {
|
||||||
document.getElementById('partnerDetailTitle').textContent = currentPartnerName + ' 交易记录';
|
const scopeEl = document.getElementById('partnerDetailScope');
|
||||||
document.getElementById('partnerDetailSummary').innerHTML = '<div class="ranking-empty">加载中...</div>';
|
const allBtn = document.getElementById('partnerDetailAllBtn');
|
||||||
document.getElementById('partnerDetailList').innerHTML = '';
|
if (partnerDetailScopeAll) {
|
||||||
showPage('partnerDetailPage');
|
scopeEl.textContent = '全部';
|
||||||
|
allBtn.textContent = '当月';
|
||||||
|
allBtn.classList.add('active');
|
||||||
|
} else {
|
||||||
|
scopeEl.textContent = `${partnerDetailYear}年${partnerDetailMonth}月`;
|
||||||
|
allBtn.textContent = '全部';
|
||||||
|
allBtn.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchPartnerDetailScope(offset) {
|
||||||
|
if (partnerDetailScopeAll) return;
|
||||||
|
partnerDetailMonth += offset;
|
||||||
|
if (partnerDetailMonth > 12) { partnerDetailMonth = 1; partnerDetailYear++; }
|
||||||
|
if (partnerDetailMonth < 1) { partnerDetailMonth = 12; partnerDetailYear--; }
|
||||||
|
partnerDetailPage = 1;
|
||||||
|
partnerDetailAllRecords = [];
|
||||||
|
updatePartnerDetailNav();
|
||||||
|
loadPartnerDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePartnerDetailAll() {
|
||||||
|
partnerDetailScopeAll = !partnerDetailScopeAll;
|
||||||
|
partnerDetailPage = 1;
|
||||||
|
partnerDetailAllRecords = [];
|
||||||
|
if (partnerDetailScopeAll) {
|
||||||
|
partnerDetailLimit = 500;
|
||||||
|
} else {
|
||||||
|
partnerDetailLimit = 20;
|
||||||
|
partnerDetailYear = currentYear;
|
||||||
|
partnerDetailMonth = currentMonth;
|
||||||
|
}
|
||||||
|
updatePartnerDetailNav();
|
||||||
|
loadPartnerDetail();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPartnerDetail(append) {
|
||||||
|
if (partnerDetailLoading) return;
|
||||||
|
partnerDetailLoading = true;
|
||||||
|
|
||||||
|
if (!append) {
|
||||||
|
document.getElementById('partnerDetailTitle').textContent = currentPartnerName + ' 交易记录';
|
||||||
|
document.getElementById('partnerDetailSummary').innerHTML = '<div class="ranking-empty">加载中...</div>';
|
||||||
|
document.getElementById('partnerDetailList').innerHTML = '';
|
||||||
|
showPage('partnerDetailPage');
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = API + '/api/stats/partner-records?partner=' + encodeURIComponent(currentPartnerName) + '&page=' + partnerDetailPage + '&limit=' + partnerDetailLimit;
|
||||||
|
if (!partnerDetailScopeAll) {
|
||||||
|
url += '&year=' + partnerDetailYear + '&month=' + partnerDetailMonth;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(API + '/api/stats/partner-records?partner=' + encodeURIComponent(currentPartnerName), { headers: headers() });
|
const res = await fetch(url, { headers: headers() });
|
||||||
if (!res.ok) return;
|
if (!res.ok) { partnerDetailLoading = false; return; }
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
document.getElementById('partnerDetailSummary').innerHTML = `
|
|
||||||
<div style="display:flex;justify-content:space-around;flex-wrap:wrap;gap:8px">
|
if (!append) {
|
||||||
<div style="text-align:center">
|
document.getElementById('partnerDetailSummary').innerHTML = `
|
||||||
<div class="daily-summary-amount" style="font-size:24px">${data.dispatch_count}</div>
|
<div style="display:flex;justify-content:space-around;flex-wrap:wrap;gap:8px">
|
||||||
<div class="daily-summary-label">总派单数</div>
|
<div style="text-align:center">
|
||||||
|
<div class="daily-summary-amount" style="font-size:24px">${data.dispatch_count}</div>
|
||||||
|
<div class="daily-summary-label">总派单数</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center">
|
||||||
|
<div class="daily-summary-amount" style="font-size:24px;color:#e67e22">${data.spread_income.toFixed(2)}</div>
|
||||||
|
<div class="daily-summary-label">陪玩总收入</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center">
|
||||||
|
<div class="daily-summary-amount" style="font-size:24px;color:#27ae60">${data.rebate_income.toFixed(2)}</div>
|
||||||
|
<div class="daily-summary-label">总返点收入</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center">
|
||||||
|
<div class="daily-summary-amount" style="font-size:24px;color:#3498db">${data.other_income.toFixed(2)}</div>
|
||||||
|
<div class="daily-summary-label">红包及其他</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:center">
|
`;
|
||||||
<div class="daily-summary-amount" style="font-size:24px;color:#e67e22">${data.spread_income.toFixed(2)}</div>
|
partnerDetailAllRecords = data.records || [];
|
||||||
<div class="daily-summary-label">陪玩总收入</div>
|
setupPartnerDetailScroll();
|
||||||
</div>
|
} else {
|
||||||
<div style="text-align:center">
|
partnerDetailAllRecords = partnerDetailAllRecords.concat(data.records || []);
|
||||||
<div class="daily-summary-amount" style="font-size:24px;color:#27ae60">${data.rebate_income.toFixed(2)}</div>
|
}
|
||||||
<div class="daily-summary-label">总返点收入</div>
|
partnerDetailTotalCount = data.totalCount || 0;
|
||||||
</div>
|
|
||||||
<div style="text-align:center">
|
|
||||||
<div class="daily-summary-amount" style="font-size:24px;color:#3498db">${data.other_income.toFixed(2)}</div>
|
|
||||||
<div class="daily-summary-label">红包及其他</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
const container = document.getElementById('partnerDetailList');
|
const container = document.getElementById('partnerDetailList');
|
||||||
if (data.records.length === 0) {
|
const moreBtn = document.getElementById('partnerDetailMore');
|
||||||
|
|
||||||
|
if (partnerDetailAllRecords.length === 0) {
|
||||||
container.innerHTML = '<div class="ranking-empty">暂无交易记录</div>';
|
container.innerHTML = '<div class="ranking-empty">暂无交易记录</div>';
|
||||||
|
if (moreBtn) moreBtn.style.display = 'none';
|
||||||
|
partnerDetailLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
data.records.forEach(r => {
|
partnerDetailAllRecords.forEach(r => {
|
||||||
const d = new Date(r.created_at);
|
const d = new Date(r.created_at);
|
||||||
const key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
|
const key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
|
||||||
if (!grouped[key]) grouped[key] = { records: [], total: 0 };
|
if (!grouped[key]) grouped[key] = { records: [], total: 0 };
|
||||||
@@ -481,5 +737,39 @@ async function loadPartnerDetail() {
|
|||||||
html += '</div>';
|
html += '</div>';
|
||||||
}
|
}
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
} catch (e) { console.error(e); }
|
|
||||||
|
if (moreBtn) {
|
||||||
|
if (partnerDetailAllRecords.length < partnerDetailTotalCount) {
|
||||||
|
moreBtn.style.display = 'block';
|
||||||
|
moreBtn.textContent = '加载中... (' + partnerDetailAllRecords.length + '/' + partnerDetailTotalCount + ')';
|
||||||
|
} else {
|
||||||
|
moreBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
partnerDetailLoading = false;
|
||||||
|
} catch (e) {
|
||||||
|
partnerDetailLoading = false;
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupPartnerDetailScroll() {
|
||||||
|
if (partnerDetailScrollHandler) {
|
||||||
|
window.removeEventListener('scroll', partnerDetailScrollHandler);
|
||||||
|
}
|
||||||
|
partnerDetailScrollHandler = function() {
|
||||||
|
if (partnerDetailLoading) return;
|
||||||
|
if (partnerDetailScopeAll && partnerDetailAllRecords.length >= partnerDetailTotalCount) {
|
||||||
|
window.removeEventListener('scroll', partnerDetailScrollHandler);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
const docHeight = document.documentElement.scrollHeight;
|
||||||
|
if (scrollY + windowHeight >= docHeight - 200) {
|
||||||
|
partnerDetailPage++;
|
||||||
|
loadPartnerDetail(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', partnerDetailScrollHandler);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ db.exec(`
|
|||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_records_user_id ON records(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_records_category ON records(category);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_records_created_at ON records(created_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_records_type ON records(type);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_records_boss ON records(boss);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_records_partner ON records(partner);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_records_source ON records(source);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 迁移:去掉 category 的 CHECK 约束
|
// 迁移:去掉 category 的 CHECK 约束
|
||||||
|
|||||||
+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) });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取记录列表
|
// 获取记录列表
|
||||||
|
|||||||
+82
-46
@@ -35,22 +35,24 @@ 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(`
|
// 合并 summary 和 total 为一次查询
|
||||||
SELECT COALESCE(SUM(amount), 0) as total
|
const stats = db.prepare(`
|
||||||
|
SELECT COALESCE(SUM(amount), 0) as total, COUNT(*) as count
|
||||||
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 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: stats.total, records, page: Number(page), limit: Number(limit), totalCount: stats.count });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 年度净收入按年统计
|
// 年度净收入按年统计
|
||||||
@@ -236,69 +238,98 @@ 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, year, month } = 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' 的记录(接单、派单返点、红包收入等)
|
// 日期过滤条件
|
||||||
// 使用 COALESCE(boss, source) 兼容旧数据(之前的红包收入只有 source 字段)
|
let dateFilter = '';
|
||||||
const myIncomeSummary = db.prepare(`
|
let dateParams = [];
|
||||||
SELECT COALESCE(SUM(amount), 0) as total
|
if (year && month) {
|
||||||
FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? AND type = 'income'
|
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
|
||||||
`).get(req.user.id, boss);
|
const endDate = `${year}-${String(month).padStart(2, '0')}-31`;
|
||||||
|
dateFilter = ` AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') <= ?`;
|
||||||
|
dateParams = [startDate, endDate];
|
||||||
|
}
|
||||||
|
|
||||||
// 老板支出:派单支出 + 接单支出 + 红包收入(老板发的红包也算老板支出)
|
// 合并 summary 查询为一次
|
||||||
const expenseSummary = db.prepare(`
|
const summary = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
|
COALESCE(SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END), 0) as my_income,
|
||||||
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity * COALESCE(unit_price, 0) ELSE 0 END), 0) as dispatch_expense,
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity * COALESCE(unit_price, 0) ELSE 0 END), 0) as dispatch_expense,
|
||||||
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity * COALESCE(unit_price, 0) ELSE 0 END), 0) as order_expense,
|
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity * COALESCE(unit_price, 0) ELSE 0 END), 0) as order_expense,
|
||||||
COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_expense
|
COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_expense,
|
||||||
FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_count,
|
||||||
`).get(req.user.id, boss);
|
COALESCE(SUM(CASE WHEN category = '接单' THEN quantity ELSE 0 END), 0) as take_count,
|
||||||
|
COUNT(*) as count
|
||||||
const boss_total_expense = expenseSummary.dispatch_expense + expenseSummary.order_expense + expenseSummary.redEnvelope_expense;
|
FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter}
|
||||||
|
`).get(req.user.id, boss, ...dateParams);
|
||||||
|
|
||||||
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) = ?${dateFilter} ORDER BY created_at DESC LIMIT ? OFFSET ?
|
||||||
`).all(req.user.id, boss);
|
`).all(req.user.id, boss, ...dateParams, Number(limit), offset);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
my_income: myIncomeSummary.total,
|
my_income: summary.my_income,
|
||||||
boss_total_expense: boss_total_expense,
|
boss_total_expense: summary.dispatch_expense + summary.order_expense + summary.redEnvelope_expense,
|
||||||
records
|
take_count: summary.take_count,
|
||||||
|
dispatch_count: summary.dispatch_count,
|
||||||
|
records,
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
totalCount: summary.count
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 陪玩交易记录
|
// 陪玩交易记录
|
||||||
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, year, month } = 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(`
|
// 日期过滤条件
|
||||||
|
let dateFilter = '';
|
||||||
|
let dateParams = [];
|
||||||
|
if (year && month) {
|
||||||
|
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
|
||||||
|
const endDate = `${year}-${String(month).padStart(2, '0')}-31`;
|
||||||
|
dateFilter = ` AND DATE(created_at, 'localtime') >= ? AND DATE(created_at, 'localtime') <= ?`;
|
||||||
|
dateParams = [startDate, endDate];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并 summary 查询为一次
|
||||||
|
// 注意: source 在 CASE 和 WHERE 中各出现一次,都需要 partner 值
|
||||||
|
const summary = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(quantity), 0) as dispatch_count,
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_count,
|
||||||
COALESCE(SUM(quantity * (COALESCE(unit_price, 0) - COALESCE(rebate, amount * 1.0 / CASE WHEN quantity > 0 THEN quantity ELSE 1 END))), 0) as spread_income,
|
COALESCE(SUM(CASE WHEN category = '派单' THEN quantity * (COALESCE(unit_price, 0) - COALESCE(rebate, amount * 1.0 / CASE WHEN quantity > 0 THEN quantity ELSE 1 END)) ELSE 0 END), 0) as spread_income,
|
||||||
COALESCE(SUM(amount), 0) as rebate_income
|
COALESCE(SUM(CASE WHEN category = '派单' THEN amount ELSE 0 END), 0) as rebate_income,
|
||||||
FROM records WHERE user_id = ? AND category = '派单' AND partner = ?
|
COALESCE(SUM(CASE WHEN type = 'income' AND category != '派单' AND category != '接单' AND source = ? THEN amount ELSE 0 END), 0) as other_income,
|
||||||
`).get(req.user.id, partner);
|
COUNT(*) as count
|
||||||
|
FROM records WHERE user_id = ? AND (
|
||||||
const otherIncome = db.prepare(`
|
(category = '派单' AND partner = ?) OR
|
||||||
SELECT COALESCE(SUM(amount), 0) as total
|
(type = 'income' AND category != '派单' AND category != '接单' AND source = ?)
|
||||||
FROM records WHERE user_id = ? AND type = 'income' AND category != '派单' AND category != '接单' AND source = ?
|
)${dateFilter}
|
||||||
`).get(req.user.id, partner);
|
`).get(partner, req.user.id, partner, partner, ...dateParams);
|
||||||
|
|
||||||
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
|
)${dateFilter} ORDER BY created_at DESC LIMIT ? OFFSET ?
|
||||||
`).all(req.user.id, partner, partner);
|
`).all(req.user.id, partner, partner, ...dateParams, Number(limit), offset);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
dispatch_count: dispatchStats.dispatch_count,
|
dispatch_count: summary.dispatch_count,
|
||||||
spread_income: dispatchStats.spread_income,
|
spread_income: summary.spread_income,
|
||||||
rebate_income: dispatchStats.rebate_income,
|
rebate_income: summary.rebate_income,
|
||||||
other_income: otherIncome.total,
|
other_income: summary.other_income,
|
||||||
records
|
records,
|
||||||
|
page: Number(page),
|
||||||
|
limit: Number(limit),
|
||||||
|
totalCount: summary.count
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -329,8 +360,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 +372,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