fix security and import validation
This commit is contained in:
+73
-63
@@ -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, "'")})'>
|
||||
<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, "'")})'>
|
||||
<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, "'")})'>
|
||||
<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, "'")})'>
|
||||
<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>';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user