// ============================================
// 应用状态管理
// ============================================
const API = '';
const AppState = {
// 认证状态
auth: {
token: localStorage.getItem('token'),
username: localStorage.getItem('username')
},
// 导航状态
nav: {
currentType: 'income',
currentCategory: '接单',
currentYear: new Date().getFullYear(),
currentMonth: new Date().getMonth() + 1,
currentNav: 'stats'
},
// 状态更新方法
setAuth(token, username) {
this.auth.token = token;
this.auth.username = username;
localStorage.setItem('token', token);
localStorage.setItem('username', username);
},
clearAuth() {
this.auth.token = null;
this.auth.username = null;
localStorage.removeItem('token');
localStorage.removeItem('username');
},
setCategory(type, category) {
this.nav.currentType = type;
this.nav.currentCategory = category;
},
setYearMonth(year, month) {
this.nav.currentYear = year;
this.nav.currentMonth = month;
},
setNav(nav) {
this.nav.currentNav = nav;
}
};
// 兼容旧代码的全局变量
let token = AppState.auth.token;
let username = AppState.auth.username;
let currentType = AppState.nav.currentType;
let currentCategory = AppState.nav.currentCategory;
let currentYear = AppState.nav.currentYear;
let currentMonth = AppState.nav.currentMonth;
let currentNav = AppState.nav.currentNav;
function headers() {
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, (ch) => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[ch]));
}
function escapeAttr(value) {
return escapeHtml(value);
}
function jsString(value) {
return escapeAttr(JSON.stringify(String(value ?? '')));
}
function recordArg(record) {
return escapeAttr(JSON.stringify(record));
}
// 统一的 API 请求函数,自动处理 401 跳转到登录页
async function apiFetch(url, options = {}) {
const res = await fetch(url, { ...options, headers: { ...headers(), ...(options.headers || {}) } });
if (res.status === 401) {
AppState.clearAuth();
showPage('loginPage');
throw new Error('Unauthorized');
}
return res;
}
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 apiFetch(API + '/api/stats/total');
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 = '
暂无数据
';
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}(${jsString(r.name)}, ${jsString(fromPage)})"` : '';
const name = escapeHtml(r.name);
return `
${i + 1}
${name}
${r.count}单
`;
}).join('');
}
async function openTakeRanking() {
showPage('takeRankingPage');
try {
const res = await apiFetch(API + '/api/stats/take-ranking');
if (!res.ok) return;
const data = await res.json();
const rows = data.rows || [];
// 显示统计卡片
const summaryContainer = document.getElementById('takeRankingSummary');
summaryContainer.innerHTML = `
接单总数
${data.total_take_count || 0}
接单总收入
${(data.total_take_income || 0).toFixed(2)}
红包收入
${(data.total_redEnvelope_income || 0).toFixed(2)}
`;
renderHorizontalChart('takeRankingChart', rows, 'openBossDetail', 'takeRankingPage');
} catch (e) { console.error(e); }
}
async function openDispatchRanking() {
showPage('dispatchRankingPage');
try {
const res = await apiFetch(API + '/api/stats/dispatch-ranking');
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 = `
陪玩总收入
${totalPartnerIncome.toFixed(2)}
返点总收入
${totalRebateIncome.toFixed(2)}
奶茶收入
${totalMilkTeaIncome.toFixed(2)}
`;
// 显示排行列表
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);
const name = escapeHtml(r.name);
return `
${i + 1}
${name}
${r.dispatch_count}单
`;
}).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}月接单列表`;
initList('orderList');
loadListData('orderList');
}
if (pageId === 'otherIncomeListPage') {
document.getElementById('otherIncomeListTitle').textContent = `${currentYear}年${currentMonth}月红包及其他收入`;
initList('otherIncomeList');
loadListData('otherIncomeList');
}
if (pageId === 'dispatchListPage') {
document.getElementById('dispatchListTitle').textContent = `${currentYear}年${currentMonth}月派单列表`;
initList('dispatchList');
loadListData('dispatchList');
}
if (pageId === 'redEnvelopeListPage') {
initList('redEnvelopeList');
loadListData('redEnvelopeList');
}
if (pageId === 'milkTeaListPage') {
initList('milkTeaList');
loadListData('milkTeaList');
}
}
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();
});