Files
gamer/public/js/stats.js
T
2026-03-12 11:23:10 +08:00

486 lines
22 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 res = await fetch(API + '/api/stats/monthly?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
if (!res.ok) return;
const s = await res.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);
loadBossRankingPreview();
loadPartnerRankingPreview();
loadWeeklyChart();
} 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();
const container = document.getElementById('weeklyChartBars');
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); }
}
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, { 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();
const container = document.getElementById('rankingPreviewList');
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); }
}
let currentBossName = '';
let bossDetailFrom = 'homePage';
function openBossDetail(boss, from) {
currentBossName = boss;
bossDetailFrom = from || 'homePage';
loadBossDetail();
}
function bossDetailGoBack() {
showPage(bossDetailFrom);
}
async function loadBossDetail() {
document.getElementById('bossDetailTitle').textContent = currentBossName + ' 交易记录';
document.getElementById('bossDetailSummary').innerHTML = '<div class="ranking-empty">加载中...</div>';
document.getElementById('bossDetailList').innerHTML = '';
showPage('bossDetailPage');
try {
const res = await fetch(API + '/api/stats/boss-records?boss=' + encodeURIComponent(currentBossName), { headers: headers() });
if (!res.ok) return;
const data = await res.json();
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>
`;
const container = document.getElementById('bossDetailList');
if (data.records.length === 0) {
container.innerHTML = '<div class="ranking-empty">暂无交易记录</div>';
return;
}
const grouped = {};
data.records.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;
} catch (e) { console.error(e); }
}
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();
const container = document.getElementById('partnerRankingPreviewList');
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); }
}
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('&');
console.log('partner-ranking url:', url);
try {
const res = await fetch(url, { headers: headers() });
console.log('partner-ranking res.ok:', res.ok);
if (!res.ok) return;
const data = await res.json();
console.log('partner-ranking data:', data);
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 totalDispatch = rows.reduce((sum, r) => sum + (r.dispatch_count || 0), 0);
console.log('totalDispatch:', totalDispatch);
// 更新统计卡片
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';
function openPartnerDetail(partner, from) {
currentPartnerName = partner;
partnerDetailFrom = from || 'homePage';
loadPartnerDetail();
}
function partnerDetailGoBack() {
showPage(partnerDetailFrom);
}
async function loadPartnerDetail() {
document.getElementById('partnerDetailTitle').textContent = currentPartnerName + ' 交易记录';
document.getElementById('partnerDetailSummary').innerHTML = '<div class="ranking-empty">加载中...</div>';
document.getElementById('partnerDetailList').innerHTML = '';
showPage('partnerDetailPage');
try {
const res = await fetch(API + '/api/stats/partner-records?partner=' + encodeURIComponent(currentPartnerName), { headers: headers() });
if (!res.ok) return;
const data = await res.json();
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>
`;
const container = document.getElementById('partnerDetailList');
if (data.records.length === 0) {
container.innerHTML = '<div class="ranking-empty">暂无交易记录</div>';
return;
}
const grouped = {};
data.records.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;
} catch (e) { console.error(e); }
}