fix: 修复柱状图高度比例计算并添加月度筛选功能

- 修复最近7天收入柱状图高度使用百分比导致比例失真问题,改为像素计算
- 老板/陪玩交易详情页添加"全部/当月"切换按钮
- 老板/陪玩交易详情页添加月度导航切换功能
- 老板/陪玩交易详情页添加自动加载更多数据功能

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lizhilun
2026-03-19 10:13:02 +08:00
co-authored by Claude Opus 4.6
parent 3d8d03a66e
commit 14927f74aa
4 changed files with 380 additions and 75 deletions
+16 -1
View File
@@ -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;
+14
View File
@@ -284,9 +284,16 @@
<div class="page-header"> <div class="page-header">
<button class="back" onclick="bossDetailGoBack()">&larr;</button> <button class="back" onclick="bossDetailGoBack()">&larr;</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)">&larr; 上月</button>
<span class="daily-nav-date" id="bossDetailScope">2026年3月</span>
<button class="daily-nav-btn" onclick="switchBossDetailScope(1)">下月 &rarr;</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()">&larr;</button> <button class="back" onclick="partnerDetailGoBack()">&larr;</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)">&larr; 上月</button>
<span class="daily-nav-date" id="partnerDetailScope">2026年3月</span>
<button class="daily-nav-btn" onclick="switchPartnerDetailScope(1)">下月 &rarr;</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>
<!-- 红包及其他收入列表页 --> <!-- 红包及其他收入列表页 -->
+307 -52
View File
@@ -56,13 +56,14 @@ function renderWeeklyChart(days) {
return; return;
} }
const max = Math.max(...days.map(d => d.income), 1); const max = Math.max(...days.map(d => d.income), 1);
const barMaxHeight = 110; // 柱状图最大可用像素高度
container.innerHTML = days.map(d => { container.innerHTML = days.map(d => {
const pct = Math.max((d.income / max) * 100, 3); const height = Math.max((d.income / max) * barMaxHeight, 4);
const label = d.date.slice(5).replace('-', '/'); const label = d.date.slice(5).replace('-', '/');
const amount = d.income > 0 ? '' + d.income.toFixed(0) : '0'; const amount = d.income > 0 ? '' + d.income.toFixed(0) : '0';
return `<div class="weekly-bar-col" onclick="openDailyDetail('${d.date}')"> return `<div class="weekly-bar-col" onclick="openDailyDetail('${d.date}')">
<span class="weekly-bar-amount">${amount}</span> <span class="weekly-bar-amount">${amount}</span>
<div class="weekly-bar" style="height:${pct}%"></div> <div class="weekly-bar" style="height:${height}px"></div>
<span class="weekly-bar-date">${label}</span> <span class="weekly-bar-date">${label}</span>
</div>`; </div>`;
}).join(''); }).join('');
@@ -284,46 +285,145 @@ function renderBossRankingPreview(rows) {
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 };
@@ -350,7 +450,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() {
@@ -443,54 +578,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 };
@@ -517,5 +738,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);
} }
+43 -22
View File
@@ -242,39 +242,50 @@ 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; 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); const offset = (Number(page) - 1) * Number(limit);
// 我的收入:只统计 type='income' 的记录(接单、派单返点、红包收入等) // 日期过滤条件
// 使用 COALESCE(boss, source) 兼容旧数据(之前的红包收入只有 source 字段) 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];
}
const myIncomeSummary = db.prepare(` const myIncomeSummary = db.prepare(`
SELECT COALESCE(SUM(amount), 0) as total SELECT COALESCE(SUM(amount), 0) as total
FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? AND type = 'income' FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? AND type = 'income'${dateFilter}
`).get(req.user.id, boss); `).get(req.user.id, boss, ...dateParams);
// 老板支出:派单支出 + 接单支出 + 红包收入(老板发的红包也算老板支出)
const expenseSummary = db.prepare(` const expenseSummary = db.prepare(`
SELECT SELECT
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
FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter}
`).get(req.user.id, boss, ...dateParams);
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(` const total = db.prepare(`
SELECT COUNT(*) as count FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? SELECT COUNT(*) as count FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter}
`).get(req.user.id, boss).count; `).get(req.user.id, boss, ...dateParams).count;
const records = db.prepare(` const records = db.prepare(`
SELECT * FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? ORDER BY created_at DESC LIMIT ? OFFSET ? SELECT * FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter} ORDER BY created_at DESC LIMIT ? OFFSET ?
`).all(req.user.id, boss, Number(limit), offset); `).all(req.user.id, boss, ...dateParams, 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,
take_count: expenseSummary.take_count,
dispatch_count: expenseSummary.dispatch_count,
records, records,
page: Number(page), page: Number(page),
limit: Number(limit), limit: Number(limit),
@@ -285,38 +296,48 @@ router.get('/boss-records', (req, res) => {
// 陪玩交易记录 // 陪玩交易记录
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; 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 offset = (Number(page) - 1) * Number(limit);
// 日期过滤条件
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];
}
const dispatchStats = db.prepare(` const dispatchStats = db.prepare(`
SELECT SELECT
COALESCE(SUM(quantity), 0) as dispatch_count, COALESCE(SUM(quantity), 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(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(amount), 0) as rebate_income COALESCE(SUM(amount), 0) as rebate_income
FROM records WHERE user_id = ? AND category = '派单' AND partner = ? FROM records WHERE user_id = ? AND category = '派单' AND partner = ?${dateFilter}
`).get(req.user.id, partner); `).get(req.user.id, partner, ...dateParams);
const otherIncome = db.prepare(` const otherIncome = db.prepare(`
SELECT COALESCE(SUM(amount), 0) as total SELECT COALESCE(SUM(amount), 0) as total
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 = ?${dateFilter}
`).get(req.user.id, partner); `).get(req.user.id, partner, ...dateParams);
const total = db.prepare(` const total = db.prepare(`
SELECT COUNT(*) as count FROM records SELECT COUNT(*) as count 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 = ?)
) )${dateFilter}
`).get(req.user.id, partner, partner).count; `).get(req.user.id, partner, partner, ...dateParams).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 LIMIT ? OFFSET ? )${dateFilter} ORDER BY created_at DESC LIMIT ? OFFSET ?
`).all(req.user.id, partner, partner, Number(limit), offset); `).all(req.user.id, partner, partner, ...dateParams, Number(limit), offset);
res.json({ res.json({
dispatch_count: dispatchStats.dispatch_count, dispatch_count: dispatchStats.dispatch_count,