Files
gamer/public/js/stats.js
T
lizhilunandClaude Opus 4.6 9126348d1b fix: 修复分页和缓存相关bug
- 前端: 移除无限滚动,改为一次性加载全部数据
- 前端: monthly-records接口添加limit=1000参数
- 前端: 修复edit-modal.js中loadListData调用参数
- 后端: 修复stats.js中getCached函数Promise处理
- 后端: 修复records.js各接口正确传递limit参数
- 后端: 修复partner-records SQL参数数量问题
- 统一版本号强制浏览器刷新缓存

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 22:12:11 +08:00

776 lines
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
async function loadStats() {
try {
// 并行加载所有数据
const [monthlyRes, bossRankRes, partnerRankRes, weeklyRes] = await Promise.all([
fetch(API + '/api/stats/monthly?year=' + currentYear + '&month=' + currentMonth, { headers: headers() }),
fetch(API + '/api/stats/boss-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() }),
fetch(API + '/api/stats/partner-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() }),
fetch(API + '/api/stats/daily-income', { headers: headers() })
]);
if (monthlyRes.ok) {
const s = await monthlyRes.json();
document.getElementById('takeCount').textContent = s.take_order_count;
document.getElementById('dispatchCount').textContent = s.dispatch_order_count;
document.getElementById('takeIncome').textContent = '' + s.take_order_income.toFixed(2);
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);
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); }
}
async function loadWeeklyChart() {
try {
const res = await fetch(API + '/api/stats/daily-income', { headers: headers() });
if (!res.ok) return;
const days = await res.json();
renderWeeklyChart(days);
} 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 = '';
async function openDailyDetail(date) {
currentDailyDate = date;
document.getElementById('dailyDetailTitle').textContent = date + ' 收入';
document.getElementById('dailyNavDate').textContent = date;
document.getElementById('dailySummary').innerHTML = '<div class="ranking-empty">加载中...</div>';
document.getElementById('dailyDetailList').innerHTML = '';
if (!document.getElementById('dailyDetailPage').classList.contains('active')) {
showPage('dailyDetailPage');
}
try {
const res = await fetch(API + '/api/stats/daily-detail?date=' + date, { headers: headers() });
if (!res.ok) return;
const data = await res.json();
document.getElementById('dailySummary').innerHTML = `
<div class="daily-summary-amount">${data.total_income.toFixed(2)}</div>
<div class="daily-summary-label">当日总收入</div>
`;
const container = document.getElementById('dailyDetailList');
if (data.records.length === 0) {
container.innerHTML = '<div class="ranking-empty">当日暂无收入记录</div>';
return;
}
container.innerHTML = data.records.map(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.quantity ? r.quantity + '单 × ' + (r.category === '派单' ? (r.rebate || r.amount / r.quantity).toFixed(2) + '(单价' + (r.unit_price || 0).toFixed(2) + '' : (r.unit_price || 0).toFixed(2)) : (r.source || '')}</span>
<span>${time}${r.partner ? ' · ' + r.partner : ''}</span>
</div>
</div>`;
}).join('');
} catch (e) { console.error(e); }
}
function switchDailyDate(offset) {
const d = new Date(currentDailyDate);
d.setDate(d.getDate() + offset);
openDailyDetail(formatLocalDate(d));
}
let monthlyDetailYear, monthlyDetailMonth;
function openMonthlyDetail(type) {
monthlyDetailYear = currentYear;
monthlyDetailMonth = currentMonth;
loadMonthlyDetail(type);
}
function switchMonthlyDetail(type, offset) {
monthlyDetailMonth += offset;
if (monthlyDetailMonth > 12) { monthlyDetailMonth = 1; monthlyDetailYear++; }
if (monthlyDetailMonth < 1) { monthlyDetailMonth = 12; monthlyDetailYear--; }
loadMonthlyDetail(type);
}
function openYearlyIncome() {
loadYearlyNetIncome();
}
async function loadYearlyNetIncome() {
document.getElementById('yearlyNetIncomeSummary').innerHTML = '<div class="ranking-empty">加载中...</div>';
document.getElementById('yearlyNetIncomeList').innerHTML = '';
if (!document.getElementById('yearlyNetIncomePage').classList.contains('active')) {
showPage('yearlyNetIncomePage');
}
try {
const res = await fetch(API + '/api/stats/yearly-net-income', { headers: headers() });
if (!res.ok) return;
const data = await res.json();
const grandNetIncome = (data.grandTotal.total_income || 0) - (data.grandTotal.total_expense || 0);
const grandColor = grandNetIncome >= 0 ? '#27ae60' : '#e74c3c';
document.getElementById('yearlyNetIncomeSummary').innerHTML = `
<div class="daily-summary-amount" style="color:${grandColor}">${grandNetIncome.toFixed(2)}</div>
<div class="daily-summary-label">累计净收入</div>
`;
const container = document.getElementById('yearlyNetIncomeList');
const monthNames = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
if (data.yearlyStats.length === 0) {
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
return;
}
container.innerHTML = data.yearlyStats.map(y => {
const year = y.year;
const income = y.total_income || 0;
const expense = y.total_expense || 0;
const net = income - expense;
const netColor = net >= 0 ? '#27ae60' : '#e74c3c';
const monthlyData = data.monthlyData[year] || [];
const monthlyHtml = monthlyData.map(m => {
const mIncome = m.total_income || 0;
const mExpense = m.total_expense || 0;
const mNet = mIncome - mExpense;
const mColor = mNet >= 0 ? '#27ae60' : '#e74c3c';
return `<div style="display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #f0f0f0;font-size:14px;">
<span>${monthNames[parseInt(m.month) - 1]}</span>
<span style="color:${mColor}">${mNet.toFixed(2)}</span>
</div>`;
}).join('');
return `<div class="order-item" style="padding:0;">
<div class="order-item-top" style="padding:12px;">
<span class="order-item-boss">${year}年</span>
<span class="order-item-amount" style="color:${netColor}">${net.toFixed(2)}</span>
</div>
<div class="order-item-bottom" style="padding:0 12px 12px;">
<span>收入: ${income.toFixed(2)} | 支出: ${expense.toFixed(2)}</span>
</div>
<div style="border-top:1px solid #eee;">${monthlyHtml}</div>
</div>`;
}).join('');
} catch (e) { console.error(e); }
}
async function loadMonthlyDetail(type) {
const pageId = type === 'income' ? 'monthlyIncomeDetailPage' : 'monthlyExpenseDetailPage';
const titleId = type === 'income' ? 'monthlyIncomeTitle' : 'monthlyExpenseTitle';
const navId = type === 'income' ? 'monthlyIncomeNav' : 'monthlyExpenseNav';
const summaryId = type === 'income' ? 'monthlyIncomeSummary' : 'monthlyExpenseSummary';
const listId = type === 'income' ? 'monthlyIncomeList' : 'monthlyExpenseList';
const label = type === 'income' ? '收入' : '支出';
document.getElementById(titleId).textContent = `${monthlyDetailYear}${monthlyDetailMonth}${label}`;
document.getElementById(navId).textContent = `${monthlyDetailYear}${monthlyDetailMonth}月`;
document.getElementById(summaryId).innerHTML = '<div class="ranking-empty">加载中...</div>';
document.getElementById(listId).innerHTML = '';
if (!document.getElementById(pageId).classList.contains('active')) {
showPage(pageId);
}
try {
const res = await fetch(API + '/api/stats/monthly-records?type=' + type + '&year=' + monthlyDetailYear + '&month=' + monthlyDetailMonth + '&limit=1000', { headers: headers() });
if (!res.ok) return;
const data = await res.json();
const color = type === 'income' ? '#27ae60' : '#e74c3c';
document.getElementById(summaryId).innerHTML = `
<div class="daily-summary-amount" style="color:${color}">${data.total.toFixed(2)}</div>
<div class="daily-summary-label">当月总${label}</div>
`;
const container = document.getElementById(listId);
if (data.records.length === 0) {
container.innerHTML = '<div class="ranking-empty">当月暂无' + label + '记录</div>';
return;
}
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"><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 : r.source ? ' · ' + r.source : r.destination ? ' · ' + r.destination : ''}</span>
<span class="order-item-amount">${type === 'expense' ? '-' : ''}${(r.amount || 0).toFixed(2)}</span>
</div>
<div class="order-item-bottom">
<span>${r.quantity ? r.quantity + '单 × ' + (r.category === '派单' ? (r.rebate || r.amount / r.quantity).toFixed(2) + '(单价' + (r.unit_price || 0).toFixed(2) + '' : (r.unit_price || 0).toFixed(2)) : (r.note || '')}</span>
<span>${time}${r.partner ? ' · ' + r.partner : ''}</span>
</div>
</div>`;
});
html += '</div>';
}
container.innerHTML = html;
} catch (e) { console.error(e); }
}
async function loadBossRankingPreview() {
try {
const res = await fetch(API + '/api/stats/boss-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
if (!res.ok) return;
const rows = await res.json();
renderBossRankingPreview(rows);
} 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 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);
}
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 = '<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 {
const res = await fetch(url, { headers: headers() });
if (!res.ok) { bossDetailLoading = false; return; }
const data = await res.json();
if (!append) {
document.getElementById('bossDetailSummary').innerHTML = `
<div style="display:flex;justify-content:space-around;flex-wrap:wrap;gap:8px">
<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>
`;
bossDetailAllRecords = data.records || [];
setupBossDetailScroll();
} else {
bossDetailAllRecords = bossDetailAllRecords.concat(data.records || []);
}
bossDetailTotalCount = data.totalCount || 0;
const container = document.getElementById('bossDetailList');
const moreBtn = document.getElementById('bossDetailMore');
if (bossDetailAllRecords.length === 0) {
container.innerHTML = '<div class="ranking-empty">暂无交易记录</div>';
if (moreBtn) moreBtn.style.display = 'none';
bossDetailLoading = false;
return;
}
const grouped = {};
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 };
grouped[key].records.push(r);
grouped[key].total += r.amount || 0;
});
let html = '';
for (const [month, group] of Object.entries(grouped)) {
html += `<div class="order-date-group"><div class="order-date-label">${month} 收入 ${group.total.toFixed(2)}</div>`;
group.records.forEach(r => {
const time = formatLocalDate(r.created_at) + ' ' + 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.partner ? ' · ' + r.partner : ''}</span>
<span class="order-item-amount">${(r.amount || 0).toFixed(2)}</span>
</div>
<div class="order-item-bottom">
<span>${r.quantity ? r.quantity + '单 × ' + (r.category === '派单' ? (r.rebate || r.amount / r.quantity).toFixed(2) + '(单价' + (r.unit_price || 0).toFixed(2) + '' : (r.unit_price || 0).toFixed(2)) : (r.note || '')}</span>
<span>${time}</span>
</div>
</div>`;
});
html += '</div>';
}
container.innerHTML = html;
// 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() {
try {
const res = await fetch(API + '/api/stats/partner-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
if (!res.ok) return;
const rows = await res.json();
renderPartnerRankingPreview(rows);
} 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() {
const now = new Date();
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, '0');
document.getElementById('partnerRankStartDate').value = y + '-' + m + '-01';
document.getElementById('partnerRankEndDate').value = y + '-' + m + '-' + String(now.getDate()).padStart(2, '0');
showPage('partnerRankingPage');
loadPartnerRankingDetail();
}
async function loadPartnerRankingDetail() {
const startDate = document.getElementById('partnerRankStartDate').value;
const endDate = document.getElementById('partnerRankEndDate').value;
let url = API + '/api/stats/partner-ranking';
const params = [];
if (startDate) params.push('startDate=' + startDate);
if (endDate) {
const d = new Date(endDate);
d.setDate(d.getDate() + 1);
params.push('endDate=' + formatLocalDate(d));
}
if (params.length) url += '?' + params.join('&');
try {
const res = await fetch(url, { headers: headers() });
if (!res.ok) return;
const data = await res.json();
const rows = data.rows || [];
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);
// 更新统计卡片
document.getElementById('partnerTotalDispatchCount').textContent = totalDispatch;
document.getElementById('totalPartnerIncome').textContent = (summary.total_partner_income || 0).toFixed(2);
document.getElementById('totalRebateIncome').textContent = summary.total_rebate_income.toFixed(2);
document.getElementById('totalMilkTeaIncome').textContent = (summary.total_milkTea_income || 0).toFixed(2);
const container = document.getElementById('partnerRankingDetailList');
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, "\\'")}', 'partnerRankingPage')">
<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); }
}
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);
}
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 = '<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 {
const res = await fetch(url, { headers: headers() });
if (!res.ok) { partnerDetailLoading = false; return; }
const data = await res.json();
if (!append) {
document.getElementById('partnerDetailSummary').innerHTML = `
<div style="display:flex;justify-content:space-around;flex-wrap:wrap;gap:8px">
<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>
`;
partnerDetailAllRecords = data.records || [];
setupPartnerDetailScroll();
} else {
partnerDetailAllRecords = partnerDetailAllRecords.concat(data.records || []);
}
partnerDetailTotalCount = data.totalCount || 0;
const container = document.getElementById('partnerDetailList');
const moreBtn = document.getElementById('partnerDetailMore');
if (partnerDetailAllRecords.length === 0) {
container.innerHTML = '<div class="ranking-empty">暂无交易记录</div>';
if (moreBtn) moreBtn.style.display = 'none';
partnerDetailLoading = false;
return;
}
const grouped = {};
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 };
grouped[key].records.push(r);
grouped[key].total += r.amount || 0;
});
let html = '';
for (const [month, group] of Object.entries(grouped)) {
html += `<div class="order-date-group"><div class="order-date-label">${month} 收入 ${group.total.toFixed(2)}</div>`;
group.records.forEach(r => {
const time = formatLocalDate(r.created_at) + ' ' + 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.quantity ? r.quantity + '单 × ' + (r.category === '派单' ? (r.rebate || r.amount / r.quantity).toFixed(2) + '(单价' + (r.unit_price || 0).toFixed(2) + '' : (r.unit_price || 0).toFixed(2)) : (r.source || '')}</span>
<span>${time}</span>
</div>
</div>`;
});
html += '</div>';
}
container.innerHTML = html;
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);
}