310 lines
12 KiB
JavaScript
310 lines
12 KiB
JavaScript
const API = '';
|
|||
|
|
let token = localStorage.getItem('token');
|
||
|
|
let username = localStorage.getItem('username');
|
||
|
|
let currentType = 'income';
|
||
|
|
let currentCategory = '接单';
|
||
|
|
let currentYear = new Date().getFullYear();
|
||
|
|
let currentMonth = new Date().getMonth() + 1;
|
||
|
|
let currentNav = 'stats';
|
||
|
|
|
||
|
|
function headers() {
|
||
|
|
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatLocalTime(isoStr) {
|
||
|
|
if (!isoStr) return '';
|
||
|
|
const d = new Date(isoStr);
|
||
|
|
const h = String(d.getHours()).padStart(2, '0');
|
||
|
|
const m = String(d.getMinutes()).padStart(2, '0');
|
||
|
|
return h + ':' + m;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatLocalDate(input) {
|
||
|
|
if (!input) return '未知日期';
|
||
|
|
const d = input instanceof Date ? input : new Date(input);
|
||
|
|
const y = d.getFullYear();
|
||
|
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||
|
|
const day = String(d.getDate()).padStart(2, '0');
|
||
|
|
return y + '-' + m + '-' + day;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadTotalStats() {
|
||
|
|
try {
|
||
|
|
const res = await fetch(API + '/api/stats/total', { headers: headers() });
|
||
|
|
if (!res.ok) return;
|
||
|
|
const s = await res.json();
|
||
|
|
document.getElementById('totalTakeCount').textContent = s.take_count;
|
||
|
|
document.getElementById('totalDispatchCount').textContent = s.dispatch_count;
|
||
|
|
document.getElementById('totalNetIncome').textContent = '' + s.net_income.toFixed(2);
|
||
|
|
} catch (e) { console.error(e); }
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderHorizontalChart(containerId, rows, clickHandler, fromPage) {
|
||
|
|
const container = document.getElementById(containerId);
|
||
|
|
if (rows.length === 0) {
|
||
|
|
container.innerHTML = '<div class="ranking-empty">暂无数据</div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const max = Math.max(...rows.map(r => r.count), 1);
|
||
|
|
container.innerHTML = rows.map((r, i) => {
|
||
|
|
const pct = Math.max((r.count / max) * 100, 2);
|
||
|
|
const onclick = clickHandler ? ` onclick="${clickHandler}('${r.name.replace(/'/g, "\\'")}', '${fromPage}')"` : '';
|
||
|
|
return `<div class="hbar-row"${onclick}>
|
||
|
|
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
||
|
|
<div class="hbar-label" title="${r.name}">${r.name}</div>
|
||
|
|
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
|
||
|
|
<div class="hbar-value">${r.count}单</div>
|
||
|
|
</div>`;
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
async function openTakeRanking() {
|
||
|
|
showPage('takeRankingPage');
|
||
|
|
try {
|
||
|
|
const res = await fetch(API + '/api/stats/take-ranking', { headers: headers() });
|
||
|
|
if (!res.ok) return;
|
||
|
|
const data = await res.json();
|
||
|
|
console.log('take-ranking data:', data);
|
||
|
|
const rows = data.rows || [];
|
||
|
|
|
||
|
|
// 显示统计卡片
|
||
|
|
const summaryContainer = document.getElementById('takeRankingSummary');
|
||
|
|
summaryContainer.innerHTML = `
|
||
|
|
<div class="item">
|
||
|
|
<div class="label">接单总数</div>
|
||
|
|
<div class="val">${data.total_take_count || 0}</div>
|
||
|
|
</div>
|
||
|
|
<div class="item">
|
||
|
|
<div class="label">接单总收入</div>
|
||
|
|
<div class="val green">${(data.total_take_income || 0).toFixed(2)}</div>
|
||
|
|
</div>
|
||
|
|
<div class="item">
|
||
|
|
<div class="label">红包收入</div>
|
||
|
|
<div class="val purple">${(data.total_redEnvelope_income || 0).toFixed(2)}</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
renderHorizontalChart('takeRankingChart', rows, 'openBossDetail', 'takeRankingPage');
|
||
|
|
} catch (e) { console.error(e); }
|
||
|
|
}
|
||
|
|
|
||
|
|
async function openDispatchRanking() {
|
||
|
|
showPage('dispatchRankingPage');
|
||
|
|
try {
|
||
|
|
const res = await fetch(API + '/api/stats/dispatch-ranking', { headers: headers() });
|
||
|
|
if (!res.ok) return;
|
||
|
|
const data = await res.json();
|
||
|
|
const rows = data.rows || [];
|
||
|
|
|
||
|
|
// 计算总计
|
||
|
|
const totalDispatch = data.total_dispatch_count !== undefined ? data.total_dispatch_count : (rows.reduce((sum, r) => sum + (r.dispatch_count || 0), 0));
|
||
|
|
const totalPartnerIncome = rows.reduce((sum, r) => sum + (r.partner_income || 0), 0);
|
||
|
|
const totalRebateIncome = rows.reduce((sum, r) => sum + (r.rebate_income || 0), 0);
|
||
|
|
const totalMilkTeaIncome = data.total_milkTea_income || 0;
|
||
|
|
|
||
|
|
// 显示4个卡片
|
||
|
|
const summaryContainer = document.getElementById('dispatchRankingSummary');
|
||
|
|
summaryContainer.innerHTML = `
|
||
|
|
<div class="item">
|
||
|
|
<div class="label">派单总数</div>
|
||
|
|
<div class="val">${totalDispatch}</div>
|
||
|
|
</div>
|
||
|
|
<div class="item">
|
||
|
|
<div class="label">陪玩总收入</div>
|
||
|
|
<div class="val green">${totalPartnerIncome.toFixed(2)}</div>
|
||
|
|
</div>
|
||
|
|
<div class="item">
|
||
|
|
<div class="label">返点总收入</div>
|
||
|
|
<div class="val orange">${totalRebateIncome.toFixed(2)}</div>
|
||
|
|
</div>
|
||
|
|
<div class="item">
|
||
|
|
<div class="label">奶茶收入</div>
|
||
|
|
<div class="val purple">${totalMilkTeaIncome.toFixed(2)}</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
// 显示排行列表
|
||
|
|
const max = Math.max(...rows.map(r => r.dispatch_count || 0), 1);
|
||
|
|
const container = document.getElementById('dispatchRankingChart');
|
||
|
|
container.innerHTML = rows.map((r, i) => {
|
||
|
|
const pct = Math.max(((r.dispatch_count || 0) / max) * 100, 2);
|
||
|
|
return `<div class="hbar-row" onclick="openPartnerDetail('${r.name.replace(/'/g, "\\'")}', 'dispatchRankingPage')">
|
||
|
|
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
||
|
|
<div class="hbar-label" title="${r.name}">${r.name}</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 showToast(msg) {
|
||
|
|
const t = document.getElementById('toast');
|
||
|
|
t.textContent = msg;
|
||
|
|
t.classList.add('show');
|
||
|
|
setTimeout(() => t.classList.remove('show'), 2000);
|
||
|
|
}
|
||
|
|
|
||
|
|
function showPage(pageId) {
|
||
|
|
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
||
|
|
document.getElementById(pageId).classList.add('active');
|
||
|
|
window.scrollTo(0, 0);
|
||
|
|
if (pageId === 'homePage') {
|
||
|
|
updateMonthDisplay();
|
||
|
|
loadStats();
|
||
|
|
document.getElementById('profileName').textContent = username;
|
||
|
|
document.getElementById('avatarText').textContent = username.charAt(0).toUpperCase();
|
||
|
|
switchNav(currentNav);
|
||
|
|
}
|
||
|
|
if (pageId === 'recordPage') {
|
||
|
|
currentType = 'income';
|
||
|
|
currentCategory = '接单';
|
||
|
|
document.querySelectorAll('.type-btn').forEach((b, i) => b.classList.toggle('active', i === 0));
|
||
|
|
document.getElementById('incomeCats').style.display = 'grid';
|
||
|
|
document.getElementById('expenseCats').style.display = 'none';
|
||
|
|
selectCategory(document.querySelector('#incomeCats .category-item'), '接单');
|
||
|
|
}
|
||
|
|
if (pageId === 'rankingPage') {
|
||
|
|
const startDate = `${currentYear}-${String(currentMonth).padStart(2, '0')}-01`;
|
||
|
|
const now = new Date();
|
||
|
|
const endDay = (currentYear === now.getFullYear() && currentMonth === now.getMonth() + 1) ? formatLocalDate(now) : `${currentYear}-${String(currentMonth).padStart(2, '0')}-28`;
|
||
|
|
document.getElementById('rankStartDate').value = startDate;
|
||
|
|
document.getElementById('rankEndDate').value = endDay;
|
||
|
|
currentSort = 'total_income';
|
||
|
|
document.querySelectorAll('.sort-btn').forEach((b, i) => b.classList.toggle('active', i === 0));
|
||
|
|
loadRankingDetail();
|
||
|
|
}
|
||
|
|
if (pageId === 'orderListPage') {
|
||
|
|
document.getElementById('orderListTitle').textContent = `${currentYear}年${currentMonth}月接单列表`;
|
||
|
|
orderPage = 1; orderHasMore = true;
|
||
|
|
document.getElementById('orderList').innerHTML = '';
|
||
|
|
loadOrders();
|
||
|
|
}
|
||
|
|
if (pageId === 'otherIncomeListPage') {
|
||
|
|
document.getElementById('otherIncomeListTitle').textContent = `${currentYear}年${currentMonth}月红包及其他收入`;
|
||
|
|
otherIncomePage = 1; otherIncomeHasMore = true;
|
||
|
|
document.getElementById('otherIncomeList').innerHTML = '';
|
||
|
|
loadOtherIncome();
|
||
|
|
}
|
||
|
|
if (pageId === 'dispatchListPage') {
|
||
|
|
document.getElementById('dispatchListTitle').textContent = `${currentYear}年${currentMonth}月派单列表`;
|
||
|
|
dispatchPage = 1; dispatchHasMore = true;
|
||
|
|
document.getElementById('dispatchList').innerHTML = '';
|
||
|
|
loadDispatches();
|
||
|
|
}
|
||
|
|
if (pageId === 'redEnvelopeListPage') {
|
||
|
|
redEnvelopePage = 1; redEnvelopeHasMore = true;
|
||
|
|
document.getElementById('redEnvelopeList').innerHTML = '';
|
||
|
|
loadRedEnvelope();
|
||
|
|
}
|
||
|
|
if (pageId === 'milkTeaListPage') {
|
||
|
|
milkTeaPage = 1; milkTeaHasMore = true;
|
||
|
|
document.getElementById('milkTeaList').innerHTML = '';
|
||
|
|
loadMilkTea();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function switchNav(nav) {
|
||
|
|
currentNav = nav;
|
||
|
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||
|
|
document.getElementById('navStats').classList.toggle('active', nav === 'stats');
|
||
|
|
document.getElementById('navMine').classList.toggle('active', nav === 'mine');
|
||
|
|
if (nav === 'stats') {
|
||
|
|
document.getElementById('statsPanel').classList.add('active');
|
||
|
|
} else {
|
||
|
|
document.getElementById('minePanel').classList.add('active');
|
||
|
|
loadTotalStats();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateMonthDisplay() {
|
||
|
|
document.getElementById('monthDisplay').textContent = `${currentYear}年${currentMonth}月`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 头像连续点击5次显示隐藏按钮
|
||
|
|
let avatarTapCount = 0;
|
||
|
|
let avatarTapTimer = null;
|
||
|
|
function onAvatarTap() {
|
||
|
|
avatarTapCount++;
|
||
|
|
clearTimeout(avatarTapTimer);
|
||
|
|
avatarTapTimer = setTimeout(() => { avatarTapCount = 0; }, 1500);
|
||
|
|
if (avatarTapCount >= 5) {
|
||
|
|
avatarTapCount = 0;
|
||
|
|
const el = document.getElementById('secretBtns');
|
||
|
|
const visible = el.style.display !== 'none';
|
||
|
|
el.style.display = visible ? 'none' : 'block';
|
||
|
|
showToast(visible ? '已隐藏' : '已解锁');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 下拉刷新
|
||
|
|
(function() {
|
||
|
|
let startY = 0, pulling = false, triggered = false;
|
||
|
|
const threshold = 60;
|
||
|
|
|
||
|
|
function getPanel() { return document.getElementById('statsPanel'); }
|
||
|
|
function getIndicator() { return document.getElementById('pullIndicator'); }
|
||
|
|
|
||
|
|
document.addEventListener('touchstart', function(e) {
|
||
|
|
const panel = getPanel();
|
||
|
|
if (!panel || !panel.classList.contains('active')) return;
|
||
|
|
if (currentNav !== 'stats') return;
|
||
|
|
if (window.scrollY > 0) return;
|
||
|
|
startY = e.touches[0].clientY;
|
||
|
|
pulling = true;
|
||
|
|
triggered = false;
|
||
|
|
}, { passive: true });
|
||
|
|
|
||
|
|
document.addEventListener('touchmove', function(e) {
|
||
|
|
if (!pulling) return;
|
||
|
|
const panel = getPanel();
|
||
|
|
if (!panel || !panel.classList.contains('active')) { pulling = false; return; }
|
||
|
|
if (window.scrollY > 0) { pulling = false; return; }
|
||
|
|
const dy = e.touches[0].clientY - startY;
|
||
|
|
if (dy < 0) return;
|
||
|
|
const indicator = getIndicator();
|
||
|
|
const arrow = document.getElementById('pullArrow');
|
||
|
|
const text = document.getElementById('pullText');
|
||
|
|
if (dy > 10) {
|
||
|
|
indicator.classList.add('visible');
|
||
|
|
if (dy >= threshold) {
|
||
|
|
arrow.classList.add('flipped');
|
||
|
|
text.textContent = '松手刷新';
|
||
|
|
triggered = true;
|
||
|
|
} else {
|
||
|
|
arrow.classList.remove('flipped');
|
||
|
|
text.textContent = '下拉刷新';
|
||
|
|
triggered = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, { passive: true });
|
||
|
|
|
||
|
|
document.addEventListener('touchend', function() {
|
||
|
|
if (!pulling) return;
|
||
|
|
pulling = false;
|
||
|
|
const indicator = getIndicator();
|
||
|
|
const arrow = document.getElementById('pullArrow');
|
||
|
|
const text = document.getElementById('pullText');
|
||
|
|
if (triggered) {
|
||
|
|
text.textContent = '刷新中...';
|
||
|
|
arrow.classList.remove('flipped');
|
||
|
|
indicator.classList.add('refreshing');
|
||
|
|
loadStats().then(() => {
|
||
|
|
setTimeout(() => {
|
||
|
|
indicator.classList.remove('visible', 'refreshing');
|
||
|
|
showToast('已刷新');
|
||
|
|
}, 500);
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
indicator.classList.remove('visible');
|
||
|
|
}
|
||
|
|
}, { passive: true });
|
||
|
|
})();
|
||
|
|
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
if (token && username) {
|
||
|
|
showPage('homePage');
|
||
|
|
}
|
||
|
|
renderForm();
|
||
|
|
});
|