fix security and import validation
This commit is contained in:
+42
-7
@@ -62,6 +62,39 @@ 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);
|
||||
@@ -81,7 +114,7 @@ function formatLocalDate(input) {
|
||||
|
||||
async function loadTotalStats() {
|
||||
try {
|
||||
const res = await fetch(API + '/api/stats/total', { headers: headers() });
|
||||
const res = await apiFetch(API + '/api/stats/total');
|
||||
if (!res.ok) return;
|
||||
const s = await res.json();
|
||||
document.getElementById('totalTakeCount').textContent = s.take_count;
|
||||
@@ -99,10 +132,11 @@ function renderHorizontalChart(containerId, rows, clickHandler, fromPage) {
|
||||
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}')"` : '';
|
||||
const onclick = clickHandler ? ` onclick="${clickHandler}(${jsString(r.name)}, ${jsString(fromPage)})"` : '';
|
||||
const name = escapeHtml(r.name);
|
||||
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-label" title="${escapeAttr(r.name)}">${name}</div>
|
||||
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
|
||||
<div class="hbar-value">${r.count}单</div>
|
||||
</div>`;
|
||||
@@ -112,7 +146,7 @@ function renderHorizontalChart(containerId, rows, clickHandler, fromPage) {
|
||||
async function openTakeRanking() {
|
||||
showPage('takeRankingPage');
|
||||
try {
|
||||
const res = await fetch(API + '/api/stats/take-ranking', { headers: headers() });
|
||||
const res = await apiFetch(API + '/api/stats/take-ranking');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const rows = data.rows || [];
|
||||
@@ -141,7 +175,7 @@ async function openTakeRanking() {
|
||||
async function openDispatchRanking() {
|
||||
showPage('dispatchRankingPage');
|
||||
try {
|
||||
const res = await fetch(API + '/api/stats/dispatch-ranking', { headers: headers() });
|
||||
const res = await apiFetch(API + '/api/stats/dispatch-ranking');
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const rows = data.rows || [];
|
||||
@@ -178,9 +212,10 @@ async function openDispatchRanking() {
|
||||
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')">
|
||||
const name = escapeHtml(r.name);
|
||||
return `<div class="hbar-row" onclick="openPartnerDetail(${jsString(r.name)}, 'dispatchRankingPage')">
|
||||
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
||||
<div class="hbar-label" title="${r.name}">${r.name}</div>
|
||||
<div class="hbar-label" title="${escapeAttr(r.name)}">${name}</div>
|
||||
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
|
||||
<div class="hbar-value">${r.dispatch_count}单</div>
|
||||
</div>`;
|
||||
|
||||
@@ -4,7 +4,7 @@ const _acCache = {};
|
||||
async function acFetchNames(field) {
|
||||
if (_acCache[field] && Date.now() - _acCache[field].ts < 60000) return _acCache[field].data;
|
||||
try {
|
||||
const res = await fetch(API + '/api/contacts?field=' + field, { headers: headers() });
|
||||
const res = await apiFetch(API + '/api/contacts?field=' + field);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
_acCache[field] = { data, ts: Date.now() };
|
||||
@@ -45,14 +45,16 @@ function acSetup(inputId, field) {
|
||||
if (filtered.length === 0) { list.classList.remove('show'); return; }
|
||||
activeIdx = -1;
|
||||
list.innerHTML = filtered.slice(0, 10).map((n, i) => {
|
||||
let display = n;
|
||||
let display = escapeHtml(n);
|
||||
if (q) {
|
||||
const idx = n.toLowerCase().indexOf(q);
|
||||
if (idx >= 0) {
|
||||
display = n.slice(0, idx) + '<span class="ac-highlight">' + n.slice(idx, idx + q.length) + '</span>' + n.slice(idx + q.length);
|
||||
display = escapeHtml(n.slice(0, idx)) +
|
||||
'<span class="ac-highlight">' + escapeHtml(n.slice(idx, idx + q.length)) + '</span>' +
|
||||
escapeHtml(n.slice(idx + q.length));
|
||||
}
|
||||
}
|
||||
return `<div class="ac-item" data-idx="${i}" data-value="${n.replace(/"/g, '"')}">${display}</div>`;
|
||||
return `<div class="ac-item" data-idx="${i}" data-value="${escapeAttr(n)}">${display}</div>`;
|
||||
}).join('');
|
||||
list.classList.add('show');
|
||||
}
|
||||
|
||||
+16
-17
@@ -38,9 +38,8 @@ async function loadListData(listId) {
|
||||
showLoading(listId, true);
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
API + cfg.endpoint + '?page=' + state.page + '&limit=' + PAGE_SIZE + '&year=' + currentYear + '&month=' + currentMonth,
|
||||
{ headers: headers() }
|
||||
const res = await apiFetch(
|
||||
API + cfg.endpoint + '?page=' + state.page + '&limit=' + PAGE_SIZE + '&year=' + currentYear + '&month=' + currentMonth
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -173,37 +172,37 @@ async function loadMilkTea() { return loadListData('milkTeaList'); }
|
||||
|
||||
function renderOrderItem(r) {
|
||||
const time = formatLocalTime(r.created_at);
|
||||
return '<div class="order-item" onclick=\'openEditModal(' + JSON.stringify(r).replace(/'/g, "'") + ')\'>' +
|
||||
'<div class="order-item-top"><span class="order-item-boss">' + (r.boss || '-') + '</span><span class="order-item-amount">' + (r.amount || 0).toFixed(2) + '</span></div>' +
|
||||
'<div class="order-item-bottom"><span>' + (r.quantity || 0) + '单 × ' + (r.unit_price || 0).toFixed(2) + '</span><span>' + time + (r.partner ? ' · ' + r.partner : '') + '</span></div></div>';
|
||||
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(r.boss || '-') + '</span><span class="order-item-amount">' + (r.amount || 0).toFixed(2) + '</span></div>' +
|
||||
'<div class="order-item-bottom"><span>' + (r.quantity || 0) + '单 × ' + (r.unit_price || 0).toFixed(2) + '</span><span>' + escapeHtml(time + (r.partner ? ' · ' + r.partner : '')) + '</span></div></div>';
|
||||
}
|
||||
|
||||
function renderOtherIncomeItem(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.source || '') + '</span><span>' + time + '</span></div></div>';
|
||||
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 + (r.boss ? ' · ' + r.boss : '')) + '</span><span class="order-item-amount">' + (r.amount || 0).toFixed(2) + '</span></div>' +
|
||||
'<div class="order-item-bottom"><span>' + escapeHtml(r.source || '') + '</span><span>' + escapeHtml(time) + '</span></div></div>';
|
||||
}
|
||||
|
||||
function renderDispatchItem(r) {
|
||||
const time = formatLocalTime(r.created_at);
|
||||
const unitPrice = r.unit_price || (r.quantity ? r.amount / r.quantity : 0);
|
||||
return '<div class="order-item" onclick=\'openEditModal(' + JSON.stringify(r).replace(/'/g, "'") + ')\'>' +
|
||||
'<div class="order-item-top"><span class="order-item-boss">' + (r.boss || '-') + '</span><span class="order-item-amount">' + (r.amount || 0).toFixed(2) + '</span></div>' +
|
||||
'<div class="order-item-bottom"><span>' + (r.quantity || 0) + '单 × ' + (r.rebate || 0).toFixed(2) + '(单价' + unitPrice.toFixed(2) + ')</span><span>' + time + (r.partner ? ' · ' + r.partner : '') + '</span></div></div>';
|
||||
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(r.boss || '-') + '</span><span class="order-item-amount">' + (r.amount || 0).toFixed(2) + '</span></div>' +
|
||||
'<div class="order-item-bottom"><span>' + (r.quantity || 0) + '单 × ' + (r.rebate || 0).toFixed(2) + '(单价' + unitPrice.toFixed(2) + ')</span><span>' + escapeHtml(time + (r.partner ? ' · ' + r.partner : '')) + '</span></div></div>';
|
||||
}
|
||||
|
||||
function renderRedEnvelopeItem(r) {
|
||||
const time = formatLocalTime(r.created_at);
|
||||
return '<div class="order-item" onclick=\'openEditModal(' + JSON.stringify(r).replace(/'/g, "'") + ')\'>' +
|
||||
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">红包</span><span class="order-item-amount">' + (r.amount || 0).toFixed(2) + '</span></div>' +
|
||||
'<div class="order-item-bottom"><span>' + (r.source || '') + '</span><span>' + time + '</span></div></div>';
|
||||
'<div class="order-item-bottom"><span>' + escapeHtml(r.source || '') + '</span><span>' + escapeHtml(time) + '</span></div></div>';
|
||||
}
|
||||
|
||||
function renderMilkTeaItem(r) {
|
||||
const time = formatLocalTime(r.created_at);
|
||||
return '<div class="order-item" onclick=\'openEditModal(' + JSON.stringify(r).replace(/'/g, "'") + ')\'>' +
|
||||
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">奶茶</span><span class="order-item-amount">' + (r.amount || 0).toFixed(2) + '</span></div>' +
|
||||
'<div class="order-item-bottom"><span>' + (r.source || '') + '</span><span>' + time + '</span></div></div>';
|
||||
}
|
||||
'<div class="order-item-bottom"><span>' + escapeHtml(r.source || '') + '</span><span>' + escapeHtml(time) + '</span></div></div>';
|
||||
}
|
||||
|
||||
+10
-7
@@ -17,7 +17,7 @@ async function loadRankingDetail() {
|
||||
url += '&endDate=' + formatLocalDate(d);
|
||||
}
|
||||
try {
|
||||
const res = await fetch(url, { headers: headers() });
|
||||
const res = await apiFetch(url);
|
||||
if (!res.ok) return;
|
||||
const rows = await res.json();
|
||||
const container = document.getElementById('rankingDetailList');
|
||||
@@ -29,9 +29,10 @@ async function loadRankingDetail() {
|
||||
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="openBossDetail('${r.boss.replace(/'/g, "\\'")}', 'rankingPage')">
|
||||
const boss = escapeHtml(r.boss);
|
||||
return `<div class="hbar-row" onclick="openBossDetail(${jsString(r.boss)}, 'rankingPage')">
|
||||
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
||||
<div class="hbar-label" title="${r.boss}">${r.boss}</div>
|
||||
<div class="hbar-label" title="${escapeAttr(r.boss)}">${boss}</div>
|
||||
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
|
||||
<div class="hbar-value">${r.dispatch_count}单</div>
|
||||
</div>`;
|
||||
@@ -40,9 +41,10 @@ async function loadRankingDetail() {
|
||||
const max = Math.max(...rows.map(r => r.take_count), 1);
|
||||
container.innerHTML = rows.map((r, i) => {
|
||||
const pct = Math.max((r.take_count / max) * 100, 2);
|
||||
return `<div class="hbar-row" onclick="openBossDetail('${r.boss.replace(/'/g, "\\'")}', 'rankingPage')">
|
||||
const boss = escapeHtml(r.boss);
|
||||
return `<div class="hbar-row" onclick="openBossDetail(${jsString(r.boss)}, 'rankingPage')">
|
||||
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
||||
<div class="hbar-label" title="${r.boss}">${r.boss}</div>
|
||||
<div class="hbar-label" title="${escapeAttr(r.boss)}">${boss}</div>
|
||||
<div class="hbar-track"><div class="hbar-fill" style="width:${pct}%"></div></div>
|
||||
<div class="hbar-value">${r.take_count}单</div>
|
||||
</div>`;
|
||||
@@ -51,9 +53,10 @@ async function loadRankingDetail() {
|
||||
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, "\\'")}', 'rankingPage')">
|
||||
const boss = escapeHtml(r.boss);
|
||||
return `<div class="hbar-row" onclick="openBossDetail(${jsString(r.boss)}, 'rankingPage')">
|
||||
<div class="hbar-index ${i < 3 ? 'top' : ''}">${i + 1}</div>
|
||||
<div class="hbar-label" title="${r.boss}">${r.boss}</div>
|
||||
<div class="hbar-label" title="${escapeAttr(r.boss)}">${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>`;
|
||||
|
||||
+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