Compare commits

..
10 Commits
Author SHA1 Message Date
lizhilunandClaude Opus 4.6 3a4048a29e fix: 修复 partner-records 缺少 source 参数
CASE 表达式中的 source=? 需要单独的 partner 参数

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 10:37:23 +08:00
lizhilunandClaude Opus 4.6 f381f93507 fix: 修复 partner-records SQL 参数顺序错误
参数顺序从 user_id, partner, partner 改为 partner, user_id, partner
以匹配 SQL 中 source, user_id, partner, source 的占位符顺序

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 10:35:19 +08:00
lizhilunandClaude Opus 4.6 d473e3a431 fix: 修复重构引入的 bug
- partner-records: 修复 SQL 参数顺序错误
- partner-records: 移除对已删除变量的引用
- stats.js: 修复 toFixed 在数字原型上调用的问题
- stats.js: 简化 onclick 处理避免转义问题

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 10:27:39 +08:00
lizhilunandClaude Opus 4.6 0b450a65ed perf: 优化数据库查询和前端代码质量
- 添加 records 表常用字段索引提升查询性能
- 合并月度/老板/陪玩记录查询减少数据库往返
- 提取重复的横向柱状图渲染逻辑为通用函数
- 移除调试用的 console.log 语句

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 10:22:15 +08:00
lizhilunandClaude Opus 4.6 14927f74aa fix: 修复柱状图高度比例计算并添加月度筛选功能
- 修复最近7天收入柱状图高度使用百分比导致比例失真问题,改为像素计算
- 老板/陪玩交易详情页添加"全部/当月"切换按钮
- 老板/陪玩交易详情页添加月度导航切换功能
- 老板/陪玩交易详情页添加自动加载更多数据功能

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 10:13:02 +08:00
lizhilunandClaude Opus 4.6 3d8d03a66e fix: 修复老板/陪玩排行榜预览数据不显示
API 直接返回数组,前端错误地使用了 data.rows 导致数据为空。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 10:06:33 +08:00
lizhilunandClaude Opus 4.6 a0466fda3b fix: 修复 renderDispatchItem 除零问题
当 r.quantity 为 0 时,原代码 r.amount / r.quantity 会导致除零错误。
修复:优先使用 r.unit_price,仅在 unit_price 不存在时才计算,并添加防护。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 09:58:33 +08:00
lizhilunandClaude Opus 4.6 2ebac98f07 refactor: 重构代码减少重复并添加分页
后端 (records.js):
- 提取通用分页查询函数 queryPagedRecords
- 5个列表接口 (orders/other-income/red-envelope/milk-tea/dispatches) 复用同一函数

后端 (stats.js):
- 为 monthly-records, boss-records, partner-records, daily-detail 添加分页支持
- 新增 page, limit, totalCount 返回字段

前端 (order-list.js):
- 提取通用列表配置 listConfig
- 统一渲染函数 renderOrderItem/renderOtherIncomeItem 等
- 大幅减少代码重复

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 09:53:48 +08:00
lizhilunandClaude Opus 4.6 a03e92d437 perf: 并行加载首页统计数据
将原来顺序执行的4个API调用改为并行执行(Promise.all),
显著减少首页加载时间。同时提取了渲染函数 renderWeeklyChart、
renderBossRankingPreview、renderPartnerRankingPreview,提高代码复用性。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 09:45:36 +08:00
lizhilunandClaude Opus 4.6 4656f3f9ca fix: 移除接单列表页面的自动滚动行为
移除了进入接单列表页面时自动滚动到今天的逻辑,避免界面
在加载后自动往上滚动的问题。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 09:37:49 +08:00
8 changed files with 674 additions and 493 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>
<!-- 红包及其他收入列表页 --> <!-- 红包及其他收入列表页 -->
-1
View File
@@ -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
View File
@@ -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, "&#39;")})'>
<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, "&#39;") + ')\'>' +
'<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, "&#39;") + ')\'>' +
'<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, "&#39;") + ')\'>' +
'<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, "&#39;") + ')\'>' +
'<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, "&#39;") + ')\'>' +
'<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, "&#39;")})'>
<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, "&#39;")})'>
<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, "&#39;")})'>
<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, "&#39;")})'>
<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
View File
@@ -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);
} }
+8
View File
@@ -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
View File
@@ -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
View File
@@ -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 });
}); });
// 老板收入排行 - 首页预览 // 老板收入排行 - 首页预览