2026-03-12 11:23:10 +08:00
|
|
|
|
async function loadStats() {
|
|
|
|
|
|
try {
|
2026-03-18 09:45:36 +08:00
|
|
|
|
// 并行加载所有数据
|
|
|
|
|
|
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();
|
2026-03-18 10:06:33 +08:00
|
|
|
|
renderBossRankingPreview(data || []);
|
2026-03-18 09:45:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (partnerRankRes.ok) {
|
|
|
|
|
|
const data = await partnerRankRes.json();
|
2026-03-18 10:06:33 +08:00
|
|
|
|
renderPartnerRankingPreview(data || []);
|
2026-03-18 09:45:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (weeklyRes.ok) {
|
|
|
|
|
|
const days = await weeklyRes.json();
|
|
|
|
|
|
renderWeeklyChart(days);
|
|
|
|
|
|
}
|
2026-03-12 11:23:10 +08:00
|
|
|
|
} 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();
|
2026-03-18 09:45:36 +08:00
|
|
|
|
renderWeeklyChart(days);
|
2026-03-12 11:23:10 +08:00
|
|
|
|
} catch (e) { console.error(e); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 09:45:36 +08:00
|
|
|
|
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);
|
2026-03-19 10:13:02 +08:00
|
|
|
|
const barMaxHeight = 110; // 柱状图最大可用像素高度
|
2026-03-18 09:45:36 +08:00
|
|
|
|
container.innerHTML = days.map(d => {
|
2026-03-19 10:13:02 +08:00
|
|
|
|
const height = Math.max((d.income / max) * barMaxHeight, 4);
|
2026-03-18 09:45:36 +08:00
|
|
|
|
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>
|
2026-03-19 10:13:02 +08:00
|
|
|
|
<div class="weekly-bar" style="height:${height}px"></div>
|
2026-03-18 09:45:36 +08:00
|
|
|
|
<span class="weekly-bar-date">${label}</span>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 11:23:10 +08:00
|
|
|
|
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, "'")})'>
|
|
|
|
|
|
<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, { 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, "'")})'>
|
|
|
|
|
|
<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();
|
2026-03-18 09:45:36 +08:00
|
|
|
|
renderBossRankingPreview(rows);
|
2026-03-12 11:23:10 +08:00
|
|
|
|
} catch (e) { console.error(e); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 10:22:15 +08:00
|
|
|
|
// 渲染横向柱状图排行榜的通用函数
|
|
|
|
|
|
function renderHBarRanking(rows, { containerId, getValue, getLabel, onClick, formatValue }) {
|
|
|
|
|
|
const container = document.getElementById(containerId);
|
2026-03-18 09:45:36 +08:00
|
|
|
|
if (!rows || rows.length === 0) {
|
|
|
|
|
|
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-19 10:22:15 +08:00
|
|
|
|
const max = Math.max(...rows.map(r => getValue(r)), 1);
|
2026-03-18 09:45:36 +08:00
|
|
|
|
container.innerHTML = rows.map((r, i) => {
|
2026-03-19 10:22:15 +08:00
|
|
|
|
const pct = Math.max((getValue(r) / max) * 100, 2);
|
|
|
|
|
|
const label = getLabel(r).replace(/'/g, "\\'");
|
2026-03-19 10:27:39 +08:00
|
|
|
|
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')">
|
2026-03-18 09:45:36 +08:00
|
|
|
|
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
2026-03-19 10:22:15 +08:00
|
|
|
|
<div class="hbar-label" title="${label}">${label}</div>
|
2026-03-18 09:45:36 +08:00
|
|
|
|
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
|
2026-03-19 10:22:15 +08:00
|
|
|
|
<div class="hbar-value">${value}</div>
|
2026-03-18 09:45:36 +08:00
|
|
|
|
</div>`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 10:22:15 +08:00
|
|
|
|
function renderBossRankingPreview(rows) {
|
|
|
|
|
|
renderHBarRanking(rows, {
|
|
|
|
|
|
containerId: 'rankingPreviewList',
|
|
|
|
|
|
getValue: r => r.total_income,
|
|
|
|
|
|
getLabel: r => r.boss,
|
|
|
|
|
|
onClick: 'openBossDetail'
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 11:23:10 +08:00
|
|
|
|
let currentBossName = '';
|
|
|
|
|
|
let bossDetailFrom = 'homePage';
|
2026-03-19 10:13:02 +08:00
|
|
|
|
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;
|
2026-03-12 11:23:10 +08:00
|
|
|
|
|
|
|
|
|
|
function openBossDetail(boss, from) {
|
|
|
|
|
|
currentBossName = boss;
|
|
|
|
|
|
bossDetailFrom = from || 'homePage';
|
2026-03-19 10:13:02 +08:00
|
|
|
|
bossDetailYear = currentYear;
|
|
|
|
|
|
bossDetailMonth = currentMonth;
|
|
|
|
|
|
// 从排行榜进入时默认显示全部
|
|
|
|
|
|
bossDetailScopeAll = (from === 'rankingPage' || from === 'takeRankingPage');
|
|
|
|
|
|
bossDetailPage = 1;
|
|
|
|
|
|
bossDetailLimit = bossDetailScopeAll ? 500 : 20;
|
|
|
|
|
|
bossDetailTotalCount = 0;
|
|
|
|
|
|
bossDetailAllRecords = [];
|
|
|
|
|
|
updateBossDetailNav();
|
2026-03-12 11:23:10 +08:00
|
|
|
|
loadBossDetail();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function bossDetailGoBack() {
|
2026-03-19 10:13:02 +08:00
|
|
|
|
if (bossDetailScrollHandler) {
|
|
|
|
|
|
window.removeEventListener('scroll', bossDetailScrollHandler);
|
|
|
|
|
|
bossDetailScrollHandler = null;
|
|
|
|
|
|
}
|
2026-03-12 11:23:10 +08:00
|
|
|
|
showPage(bossDetailFrom);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 10:13:02 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-03-12 11:23:10 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-19 10:13:02 +08:00
|
|
|
|
const res = await fetch(url, { headers: headers() });
|
|
|
|
|
|
if (!res.ok) { bossDetailLoading = false; return; }
|
2026-03-12 11:23:10 +08:00
|
|
|
|
const data = await res.json();
|
2026-03-19 10:13:02 +08:00
|
|
|
|
|
|
|
|
|
|
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>
|
2026-03-12 11:23:10 +08:00
|
|
|
|
</div>
|
2026-03-19 10:13:02 +08:00
|
|
|
|
`;
|
|
|
|
|
|
bossDetailAllRecords = data.records || [];
|
|
|
|
|
|
setupBossDetailScroll();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
bossDetailAllRecords = bossDetailAllRecords.concat(data.records || []);
|
|
|
|
|
|
}
|
|
|
|
|
|
bossDetailTotalCount = data.totalCount || 0;
|
|
|
|
|
|
|
2026-03-12 11:23:10 +08:00
|
|
|
|
const container = document.getElementById('bossDetailList');
|
2026-03-19 10:13:02 +08:00
|
|
|
|
const moreBtn = document.getElementById('bossDetailMore');
|
|
|
|
|
|
|
|
|
|
|
|
if (bossDetailAllRecords.length === 0) {
|
2026-03-12 11:23:10 +08:00
|
|
|
|
container.innerHTML = '<div class="ranking-empty">暂无交易记录</div>';
|
2026-03-19 10:13:02 +08:00
|
|
|
|
if (moreBtn) moreBtn.style.display = 'none';
|
|
|
|
|
|
bossDetailLoading = false;
|
2026-03-12 11:23:10 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-19 10:13:02 +08:00
|
|
|
|
|
2026-03-12 11:23:10 +08:00
|
|
|
|
const grouped = {};
|
2026-03-19 10:13:02 +08:00
|
|
|
|
bossDetailAllRecords.forEach(r => {
|
2026-03-12 11:23:10 +08:00
|
|
|
|
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, "'")})'>
|
|
|
|
|
|
<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;
|
2026-03-19 10:13:02 +08:00
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-03-12 11:23:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
2026-03-18 09:45:36 +08:00
|
|
|
|
renderPartnerRankingPreview(rows);
|
2026-03-12 11:23:10 +08:00
|
|
|
|
} catch (e) { console.error(e); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 09:45:36 +08:00
|
|
|
|
function renderPartnerRankingPreview(rows) {
|
2026-03-19 10:22:15 +08:00
|
|
|
|
renderHBarRanking(rows, {
|
|
|
|
|
|
containerId: 'partnerRankingPreviewList',
|
|
|
|
|
|
getValue: r => r.dispatch_count,
|
|
|
|
|
|
getLabel: r => r.partner,
|
|
|
|
|
|
onClick: 'openPartnerDetail',
|
|
|
|
|
|
formatValue: v => v + '单'
|
|
|
|
|
|
});
|
2026-03-18 09:45:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 11:23:10 +08:00
|
|
|
|
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';
|
2026-03-19 10:13:02 +08:00
|
|
|
|
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;
|
2026-03-12 11:23:10 +08:00
|
|
|
|
|
|
|
|
|
|
function openPartnerDetail(partner, from) {
|
|
|
|
|
|
currentPartnerName = partner;
|
|
|
|
|
|
partnerDetailFrom = from || 'homePage';
|
2026-03-19 10:13:02 +08:00
|
|
|
|
partnerDetailYear = currentYear;
|
|
|
|
|
|
partnerDetailMonth = currentMonth;
|
|
|
|
|
|
// 从排行榜进入时默认显示全部
|
|
|
|
|
|
partnerDetailScopeAll = (from === 'dispatchRankingPage' || from === 'partnerRankingPage');
|
|
|
|
|
|
partnerDetailPage = 1;
|
|
|
|
|
|
partnerDetailLimit = partnerDetailScopeAll ? 500 : 20;
|
|
|
|
|
|
partnerDetailTotalCount = 0;
|
|
|
|
|
|
partnerDetailAllRecords = [];
|
|
|
|
|
|
updatePartnerDetailNav();
|
2026-03-12 11:23:10 +08:00
|
|
|
|
loadPartnerDetail();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function partnerDetailGoBack() {
|
2026-03-19 10:13:02 +08:00
|
|
|
|
if (partnerDetailScrollHandler) {
|
|
|
|
|
|
window.removeEventListener('scroll', partnerDetailScrollHandler);
|
|
|
|
|
|
partnerDetailScrollHandler = null;
|
|
|
|
|
|
}
|
2026-03-12 11:23:10 +08:00
|
|
|
|
showPage(partnerDetailFrom);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 10:13:02 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-03-12 11:23:10 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-19 10:13:02 +08:00
|
|
|
|
const res = await fetch(url, { headers: headers() });
|
|
|
|
|
|
if (!res.ok) { partnerDetailLoading = false; return; }
|
2026-03-12 11:23:10 +08:00
|
|
|
|
const data = await res.json();
|
2026-03-19 10:13:02 +08:00
|
|
|
|
|
|
|
|
|
|
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>
|
2026-03-12 11:23:10 +08:00
|
|
|
|
</div>
|
2026-03-19 10:13:02 +08:00
|
|
|
|
`;
|
|
|
|
|
|
partnerDetailAllRecords = data.records || [];
|
|
|
|
|
|
setupPartnerDetailScroll();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
partnerDetailAllRecords = partnerDetailAllRecords.concat(data.records || []);
|
|
|
|
|
|
}
|
|
|
|
|
|
partnerDetailTotalCount = data.totalCount || 0;
|
|
|
|
|
|
|
2026-03-12 11:23:10 +08:00
|
|
|
|
const container = document.getElementById('partnerDetailList');
|
2026-03-19 10:13:02 +08:00
|
|
|
|
const moreBtn = document.getElementById('partnerDetailMore');
|
|
|
|
|
|
|
|
|
|
|
|
if (partnerDetailAllRecords.length === 0) {
|
2026-03-12 11:23:10 +08:00
|
|
|
|
container.innerHTML = '<div class="ranking-empty">暂无交易记录</div>';
|
2026-03-19 10:13:02 +08:00
|
|
|
|
if (moreBtn) moreBtn.style.display = 'none';
|
|
|
|
|
|
partnerDetailLoading = false;
|
2026-03-12 11:23:10 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-19 10:13:02 +08:00
|
|
|
|
|
2026-03-12 11:23:10 +08:00
|
|
|
|
const grouped = {};
|
2026-03-19 10:13:02 +08:00
|
|
|
|
partnerDetailAllRecords.forEach(r => {
|
2026-03-12 11:23:10 +08:00
|
|
|
|
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, "'")})'>
|
|
|
|
|
|
<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;
|
2026-03-19 10:13:02 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-03-12 11:23:10 +08:00
|
|
|
|
}
|