fix security and import validation

This commit is contained in:
lizhilun
2026-07-01 16:13:47 +08:00
parent 6467048f38
commit e69930799c
14 changed files with 498 additions and 163 deletions
+73 -63
View File
@@ -2,10 +2,10 @@ 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() })
apiFetch(API + '/api/stats/monthly?year=' + currentYear + '&month=' + currentMonth),
apiFetch(API + '/api/stats/boss-ranking/preview?year=' + currentYear + '&month=' + currentMonth),
apiFetch(API + '/api/stats/partner-ranking/preview?year=' + currentYear + '&month=' + currentMonth),
apiFetch(API + '/api/stats/daily-income')
]);
if (monthlyRes.ok) {
@@ -40,9 +40,38 @@ async function loadStats() {
} catch (e) { console.error(e); }
}
function recordLineText(r, fallbackField) {
if (r.quantity) {
if (r.category === '派单') {
const rebate = (r.rebate || r.amount / r.quantity).toFixed(2);
return r.quantity + '单 × ' + rebate + '(单价' + (r.unit_price || 0).toFixed(2) + '';
}
return r.quantity + '单 × ' + (r.unit_price || 0).toFixed(2);
}
return r[fallbackField] || '';
}
function renderRecordItem(r, opts = {}) {
const time = opts.time || formatLocalTime(r.created_at);
const displayCategory = r.category === '红包收入' ? '红包' : (r.category || '-');
const suffix = opts.suffix || '';
const line = opts.line !== undefined ? opts.line : recordLineText(r, 'note');
const amountPrefix = opts.amountPrefix || '';
return `<div class="order-item" onclick="openEditModal(JSON.parse(this.dataset.record))" data-record="${recordArg(r)}">
<div class="order-item-top">
<span class="order-item-boss">${escapeHtml(displayCategory + suffix)}</span>
<span class="order-item-amount">${amountPrefix}${(r.amount || 0).toFixed(2)}</span>
</div>
<div class="order-item-bottom">
<span>${escapeHtml(line)}</span>
<span>${escapeHtml(time + (opts.partnerTime && r.partner ? ' · ' + r.partner : ''))}</span>
</div>
</div>`;
}
async function loadWeeklyChart() {
try {
const res = await fetch(API + '/api/stats/daily-income', { headers: headers() });
const res = await apiFetch(API + '/api/stats/daily-income');
if (!res.ok) return;
const days = await res.json();
renderWeeklyChart(days);
@@ -61,7 +90,7 @@ function renderWeeklyChart(days) {
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}')">
return `<div class="weekly-bar-col" onclick="openDailyDetail(${jsString(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>
@@ -81,7 +110,7 @@ async function openDailyDetail(date) {
showPage('dailyDetailPage');
}
try {
const res = await fetch(API + '/api/stats/daily-detail?date=' + date, { headers: headers() });
const res = await apiFetch(API + '/api/stats/daily-detail?date=' + date);
if (!res.ok) return;
const data = await res.json();
document.getElementById('dailySummary').innerHTML = `
@@ -95,17 +124,12 @@ async function openDailyDetail(date) {
}
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>`;
return renderRecordItem(r, {
time,
suffix: r.boss ? ' · ' + r.boss : '',
line: recordLineText(r, 'source'),
partnerTime: true
});
}).join('');
} catch (e) { console.error(e); }
}
@@ -144,7 +168,7 @@ async function loadYearlyNetIncome() {
}
try {
const res = await fetch(API + '/api/stats/yearly-net-income', { headers: headers() });
const res = await apiFetch(API + '/api/stats/yearly-net-income');
if (!res.ok) return;
const data = await res.json();
@@ -214,7 +238,7 @@ async function loadMonthlyDetail(type) {
}
try {
const res = await fetch(API + '/api/stats/monthly-records?type=' + type + '&year=' + monthlyDetailYear + '&month=' + monthlyDetailMonth + '&limit=1000', { headers: headers() });
const res = await apiFetch(API + '/api/stats/monthly-records?type=' + type + '&year=' + monthlyDetailYear + '&month=' + monthlyDetailMonth + '&limit=1000');
if (!res.ok) return;
const data = await res.json();
const color = type === 'income' ? '#27ae60' : '#e74c3c';
@@ -238,17 +262,13 @@ async function loadMonthlyDetail(type) {
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 += renderRecordItem(r, {
time,
suffix: r.boss ? ' · ' + r.boss : r.source ? ' · ' + r.source : r.destination ? ' · ' + r.destination : '',
line: recordLineText(r, 'note'),
amountPrefix: type === 'expense' ? '-' : '',
partnerTime: true
});
});
html += '</div>';
}
@@ -258,7 +278,7 @@ async function loadMonthlyDetail(type) {
async function loadBossRankingPreview() {
try {
const res = await fetch(API + '/api/stats/boss-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
const res = await apiFetch(API + '/api/stats/boss-ranking/preview?year=' + currentYear + '&month=' + currentMonth);
if (!res.ok) return;
const rows = await res.json();
renderBossRankingPreview(rows);
@@ -275,11 +295,12 @@ function renderHBarRanking(rows, { containerId, getValue, getLabel, onClick, for
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 rawLabel = getLabel(r);
const label = escapeHtml(rawLabel);
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')">
return `<div class="hbar-row" onclick="${onClick}(${jsString(rawLabel)}, 'homePage')">
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
<div class="hbar-label" title="${label}">${label}</div>
<div class="hbar-label" title="${escapeAttr(rawLabel)}">${label}</div>
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
<div class="hbar-value">${value}</div>
</div>`;
@@ -392,7 +413,7 @@ async function loadBossDetail(append) {
}
try {
const res = await fetch(url, { headers: headers() });
const res = await apiFetch(url);
if (!res.ok) { bossDetailLoading = false; return; }
const data = await res.json();
@@ -447,17 +468,11 @@ async function loadBossDetail(append) {
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 += renderRecordItem(r, {
time,
suffix: r.partner ? ' · ' + r.partner : '',
line: recordLineText(r, 'note')
});
});
html += '</div>';
}
@@ -502,7 +517,7 @@ function setupBossDetailScroll() {
async function loadPartnerRankingPreview() {
try {
const res = await fetch(API + '/api/stats/partner-ranking/preview?year=' + currentYear + '&month=' + currentMonth, { headers: headers() });
const res = await apiFetch(API + '/api/stats/partner-ranking/preview?year=' + currentYear + '&month=' + currentMonth);
if (!res.ok) return;
const rows = await res.json();
renderPartnerRankingPreview(rows);
@@ -542,7 +557,7 @@ async function loadPartnerRankingDetail() {
}
if (params.length) url += '?' + params.join('&');
try {
const res = await fetch(url, { headers: headers() });
const res = await apiFetch(url);
if (!res.ok) return;
const data = await res.json();
const rows = data.rows || [];
@@ -565,9 +580,10 @@ async function loadPartnerRankingDetail() {
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')">
const partner = escapeHtml(r.partner);
return `<div class="hbar-row" onclick="openPartnerDetail(${jsString(r.partner)}, 'partnerRankingPage')">
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
<div class="hbar-label" title="${r.partner}">${r.partner}</div>
<div class="hbar-label" title="${escapeAttr(r.partner)}">${partner}</div>
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
<div class="hbar-value">${r.dispatch_count}单</div>
</div>`;
@@ -667,7 +683,7 @@ async function loadPartnerDetail(append) {
}
try {
const res = await fetch(url, { headers: headers() });
const res = await apiFetch(url);
if (!res.ok) { partnerDetailLoading = false; return; }
const data = await res.json();
@@ -722,17 +738,11 @@ async function loadPartnerDetail(append) {
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 += renderRecordItem(r, {
time,
suffix: r.boss ? ' · ' + r.boss : '',
line: recordLineText(r, 'source')
});
});
html += '</div>';
}