From 14927f74aa0062e3f13edda00ffa8e8a8b84de43 Mon Sep 17 00:00:00 2001 From: lizhilun Date: Thu, 19 Mar 2026 10:13:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=9F=B1=E7=8A=B6?= =?UTF-8?q?=E5=9B=BE=E9=AB=98=E5=BA=A6=E6=AF=94=E4=BE=8B=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E5=B9=B6=E6=B7=BB=E5=8A=A0=E6=9C=88=E5=BA=A6=E7=AD=9B=E9=80=89?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复最近7天收入柱状图高度使用百分比导致比例失真问题,改为像素计算 - 老板/陪玩交易详情页添加"全部/当月"切换按钮 - 老板/陪玩交易详情页添加月度导航切换功能 - 老板/陪玩交易详情页添加自动加载更多数据功能 Co-Authored-By: Claude Opus 4.6 --- public/css/style.css | 17 +- public/index.html | 14 ++ public/js/stats.js | 359 +++++++++++++++++++++++++++++++++++------ server/routes/stats.js | 65 +++++--- 4 files changed, 380 insertions(+), 75 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index b258b9a..0624cc5 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -159,7 +159,7 @@ body { font-size: 15px; font-weight: 600; color: #333; - margin-bottom: 16px; + margin-bottom: 0; } .weekly-chart-bars { display: flex; @@ -215,6 +215,21 @@ body { .daily-nav-date { 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 { background: #fff; border-radius: 12px; diff --git a/public/index.html b/public/index.html index 5ee2b31..0b5846f 100644 --- a/public/index.html +++ b/public/index.html @@ -284,9 +284,16 @@ +
+ + 2026年3月 +
+ @@ -294,9 +301,16 @@ +
+ + 2026年3月 +
+ diff --git a/public/js/stats.js b/public/js/stats.js index 8dec6d5..85dfee9 100644 --- a/public/js/stats.js +++ b/public/js/stats.js @@ -56,13 +56,14 @@ function renderWeeklyChart(days) { return; } const max = Math.max(...days.map(d => d.income), 1); + const barMaxHeight = 110; // 柱状图最大可用像素高度 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 amount = d.income > 0 ? '' + d.income.toFixed(0) : '0'; return `
${amount} -
+
${label}
`; }).join(''); @@ -284,46 +285,145 @@ function renderBossRankingPreview(rows) { let currentBossName = ''; 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) { currentBossName = boss; bossDetailFrom = from || 'homePage'; + bossDetailYear = currentYear; + bossDetailMonth = currentMonth; + // 从排行榜进入时默认显示全部 + bossDetailScopeAll = (from === 'rankingPage' || from === 'takeRankingPage'); + bossDetailPage = 1; + bossDetailLimit = bossDetailScopeAll ? 500 : 20; + bossDetailTotalCount = 0; + bossDetailAllRecords = []; + updateBossDetailNav(); loadBossDetail(); } function bossDetailGoBack() { + if (bossDetailScrollHandler) { + window.removeEventListener('scroll', bossDetailScrollHandler); + bossDetailScrollHandler = null; + } showPage(bossDetailFrom); } -async function loadBossDetail() { - document.getElementById('bossDetailTitle').textContent = currentBossName + ' 交易记录'; - document.getElementById('bossDetailSummary').innerHTML = '
加载中...
'; - document.getElementById('bossDetailList').innerHTML = ''; - showPage('bossDetailPage'); +function updateBossDetailNav() { + const scopeEl = document.getElementById('bossDetailScope'); + const allBtn = document.getElementById('bossDetailAllBtn'); + if (bossDetailScopeAll) { + 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 = '
加载中...
'; + 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 { - const res = await fetch(API + '/api/stats/boss-records?boss=' + encodeURIComponent(currentBossName), { headers: headers() }); - if (!res.ok) return; + const res = await fetch(url, { headers: headers() }); + if (!res.ok) { bossDetailLoading = false; return; } const data = await res.json(); - document.getElementById('bossDetailSummary').innerHTML = ` -
-
-
${data.boss_total_expense.toFixed(2)}
-
老板总支出
+ + if (!append) { + document.getElementById('bossDetailSummary').innerHTML = ` +
+
+
${data.boss_total_expense.toFixed(2)}
+
老板总支出
+
+
+
${data.my_income.toFixed(2)}
+
我的总收入
+
+
+
${data.take_count || 0}
+
接单数
+
+
+
${data.dispatch_count || 0}
+
派单数
+
-
-
${data.my_income.toFixed(2)}
-
我的总收入
-
-
- `; + `; + bossDetailAllRecords = data.records || []; + setupBossDetailScroll(); + } else { + bossDetailAllRecords = bossDetailAllRecords.concat(data.records || []); + } + bossDetailTotalCount = data.totalCount || 0; + const container = document.getElementById('bossDetailList'); - if (data.records.length === 0) { + const moreBtn = document.getElementById('bossDetailMore'); + + if (bossDetailAllRecords.length === 0) { container.innerHTML = '
暂无交易记录
'; + if (moreBtn) moreBtn.style.display = 'none'; + bossDetailLoading = false; return; } + const grouped = {}; - data.records.forEach(r => { + bossDetailAllRecords.forEach(r => { const d = new Date(r.created_at); const key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0'); if (!grouped[key]) grouped[key] = { records: [], total: 0 }; @@ -350,7 +450,42 @@ async function loadBossDetail() { 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() { @@ -443,54 +578,140 @@ async function loadPartnerRankingDetail() { let currentPartnerName = ''; 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) { currentPartnerName = partner; partnerDetailFrom = from || 'homePage'; + partnerDetailYear = currentYear; + partnerDetailMonth = currentMonth; + // 从排行榜进入时默认显示全部 + partnerDetailScopeAll = (from === 'dispatchRankingPage' || from === 'partnerRankingPage'); + partnerDetailPage = 1; + partnerDetailLimit = partnerDetailScopeAll ? 500 : 20; + partnerDetailTotalCount = 0; + partnerDetailAllRecords = []; + updatePartnerDetailNav(); loadPartnerDetail(); } function partnerDetailGoBack() { + if (partnerDetailScrollHandler) { + window.removeEventListener('scroll', partnerDetailScrollHandler); + partnerDetailScrollHandler = null; + } showPage(partnerDetailFrom); } -async function loadPartnerDetail() { - document.getElementById('partnerDetailTitle').textContent = currentPartnerName + ' 交易记录'; - document.getElementById('partnerDetailSummary').innerHTML = '
加载中...
'; - document.getElementById('partnerDetailList').innerHTML = ''; - showPage('partnerDetailPage'); +function updatePartnerDetailNav() { + const scopeEl = document.getElementById('partnerDetailScope'); + const allBtn = document.getElementById('partnerDetailAllBtn'); + if (partnerDetailScopeAll) { + 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 = '
加载中...
'; + 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 { - const res = await fetch(API + '/api/stats/partner-records?partner=' + encodeURIComponent(currentPartnerName), { headers: headers() }); - if (!res.ok) return; + const res = await fetch(url, { headers: headers() }); + if (!res.ok) { partnerDetailLoading = false; return; } const data = await res.json(); - document.getElementById('partnerDetailSummary').innerHTML = ` -
-
-
${data.dispatch_count}
-
总派单数
+ + if (!append) { + document.getElementById('partnerDetailSummary').innerHTML = ` +
+
+
${data.dispatch_count}
+
总派单数
+
+
+
${data.spread_income.toFixed(2)}
+
陪玩总收入
+
+
+
${data.rebate_income.toFixed(2)}
+
总返点收入
+
+
+
${data.other_income.toFixed(2)}
+
红包及其他
+
-
-
${data.spread_income.toFixed(2)}
-
陪玩总收入
-
-
-
${data.rebate_income.toFixed(2)}
-
总返点收入
-
-
-
${data.other_income.toFixed(2)}
-
红包及其他
-
-
- `; + `; + partnerDetailAllRecords = data.records || []; + setupPartnerDetailScroll(); + } else { + partnerDetailAllRecords = partnerDetailAllRecords.concat(data.records || []); + } + partnerDetailTotalCount = data.totalCount || 0; + const container = document.getElementById('partnerDetailList'); - if (data.records.length === 0) { + const moreBtn = document.getElementById('partnerDetailMore'); + + if (partnerDetailAllRecords.length === 0) { container.innerHTML = '
暂无交易记录
'; + if (moreBtn) moreBtn.style.display = 'none'; + partnerDetailLoading = false; return; } + const grouped = {}; - data.records.forEach(r => { + partnerDetailAllRecords.forEach(r => { const d = new Date(r.created_at); const key = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0'); if (!grouped[key]) grouped[key] = { records: [], total: 0 }; @@ -517,5 +738,39 @@ async function loadPartnerDetail() { 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); } diff --git a/server/routes/stats.js b/server/routes/stats.js index da02d33..96ce653 100644 --- a/server/routes/stats.js +++ b/server/routes/stats.js @@ -242,39 +242,50 @@ router.get('/total', (req, res) => { // 老板交易记录 router.get('/boss-records', (req, res) => { 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: '缺少老板参数' }); 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(` SELECT COALESCE(SUM(amount), 0) as total - FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? AND type = 'income' - `).get(req.user.id, boss); + FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? AND type = 'income'${dateFilter} + `).get(req.user.id, boss, ...dateParams); - // 老板支出:派单支出 + 接单支出 + 红包收入(老板发的红包也算老板支出) const expenseSummary = db.prepare(` 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 order_expense, - COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_expense - FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? - `).get(req.user.id, boss); + COALESCE(SUM(CASE WHEN category = '红包收入' THEN amount ELSE 0 END), 0) as redEnvelope_expense, + COALESCE(SUM(CASE WHEN category = '派单' THEN quantity ELSE 0 END), 0) as dispatch_count, + 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 total = db.prepare(` - SELECT COUNT(*) as count FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? - `).get(req.user.id, boss).count; + SELECT COUNT(*) as count FROM records WHERE user_id = ? AND COALESCE(boss, source) = ?${dateFilter} + `).get(req.user.id, boss, ...dateParams).count; const records = db.prepare(` - SELECT * FROM records WHERE user_id = ? AND COALESCE(boss, source) = ? ORDER BY created_at DESC LIMIT ? OFFSET ? - `).all(req.user.id, boss, Number(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, ...dateParams, Number(limit), offset); res.json({ my_income: myIncomeSummary.total, boss_total_expense: boss_total_expense, + take_count: expenseSummary.take_count, + dispatch_count: expenseSummary.dispatch_count, records, page: Number(page), limit: Number(limit), @@ -285,38 +296,48 @@ router.get('/boss-records', (req, res) => { // 陪玩交易记录 router.get('/partner-records', (req, res) => { 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: '缺少陪玩参数' }); 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(` SELECT 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(amount), 0) as rebate_income - FROM records WHERE user_id = ? AND category = '派单' AND partner = ? - `).get(req.user.id, partner); + FROM records WHERE user_id = ? AND category = '派单' AND partner = ?${dateFilter} + `).get(req.user.id, partner, ...dateParams); const otherIncome = db.prepare(` SELECT COALESCE(SUM(amount), 0) as total - FROM records WHERE user_id = ? AND type = 'income' AND category != '派单' AND category != '接单' AND source = ? - `).get(req.user.id, partner); + FROM records WHERE user_id = ? AND type = 'income' AND category != '派单' AND category != '接单' AND source = ?${dateFilter} + `).get(req.user.id, partner, ...dateParams); const total = db.prepare(` SELECT COUNT(*) as count FROM records WHERE user_id = ? AND ( (category = '派单' AND partner = ?) OR (type = 'income' AND category != '派单' AND category != '接单' AND source = ?) - ) - `).get(req.user.id, partner, partner).count; + )${dateFilter} + `).get(req.user.id, partner, partner, ...dateParams).count; const records = db.prepare(` SELECT * FROM records WHERE user_id = ? AND ( (category = '派单' AND partner = ?) OR (type = 'income' AND category != '派单' AND category != '接单' AND source = ?) - ) ORDER BY created_at DESC LIMIT ? OFFSET ? - `).all(req.user.id, partner, partner, Number(limit), offset); + )${dateFilter} ORDER BY created_at DESC LIMIT ? OFFSET ? + `).all(req.user.id, partner, partner, ...dateParams, Number(limit), offset); res.json({ dispatch_count: dispatchStats.dispatch_count,