diff --git a/index.html b/index.html
index f8715c5..1ad866f 100755
--- a/index.html
+++ b/index.html
@@ -22,7 +22,6 @@
--error-text: #a51d2d;
--shadow: 0 8px 24px rgba(33, 52, 78, 0.07);
}
-
* { box-sizing: border-box; }
body {
margin: 0;
@@ -31,104 +30,55 @@
color: var(--text);
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
- .page {
- width: min(1440px, calc(100% - 32px));
- margin: 0 auto;
- padding: 28px 0 36px;
- }
- .page-header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 24px;
- margin-bottom: 20px;
- }
- h1 {
- margin: 0 0 6px;
- font-size: clamp(24px, 3vw, 34px);
- letter-spacing: -0.02em;
- line-height: 1.2;
- }
+ .page { width: min(1440px, calc(100% - 32px)); margin: 0 auto; padding: 28px 0 36px; }
+ .page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 20px; }
+ h1 { margin: 0 0 6px; font-size: clamp(24px, 3vw, 34px); letter-spacing: -0.02em; line-height: 1.2; }
.subtitle { margin: 0; color: var(--muted); }
- .status {
- flex: 0 0 auto;
- margin-top: 3px;
- color: var(--muted);
- text-align: right;
- }
+ .header-actions { display: grid; gap: 8px; justify-items: end; }
+ .raw-link { color: var(--primary); font-weight: 600; text-decoration: none; }
+ .raw-link:hover, .raw-link:focus { text-decoration: underline; }
+ .status { color: var(--muted); text-align: right; }
.status[data-state="error"] { color: var(--error-text); }
.status[data-state="empty"] { color: var(--muted); }
- .alert {
- margin: 0 0 18px;
- padding: 11px 14px;
- border: 1px solid var(--warning-border);
- border-radius: 10px;
- background: var(--warning-bg);
- color: var(--warning-text);
- }
+ .alert { margin: 0 0 18px; padding: 11px 14px; border: 1px solid var(--warning-border); border-radius: 10px; background: var(--warning-bg); color: var(--warning-text); }
.alert[hidden] { display: none; }
- .metrics {
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 14px;
- margin-bottom: 20px;
- }
- .metric, .chart-card {
- border: 1px solid var(--border);
- border-radius: 14px;
- background: var(--surface);
- box-shadow: var(--shadow);
- }
+ .filters { display: grid; gap: 12px; margin: 0 0 20px; padding: 16px 18px; border: 1px solid var(--border); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); }
+ .filter-row { display: flex; flex-wrap: wrap; align-items: end; gap: 14px 20px; }
+ .filter-group { display: grid; gap: 5px; }
+ .filter-group > span, .filter-group > label { color: var(--muted); font-size: 13px; }
+ .filter-group input, .filter-group select { min-height: 36px; padding: 6px 9px; border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); font: inherit; }
+ .filter-group select:disabled { background: #f1f4f8; color: var(--muted); }
+ .range-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
+ .range-button { min-height: 36px; padding: 6px 11px; border: 1px solid var(--border); border-radius: 8px; background: #fff; color: var(--text); cursor: pointer; font: inherit; }
+ .range-button:hover, .range-button:focus, .range-button[data-active="true"] { border-color: var(--primary); color: var(--primary); }
+ .currency-help, .rate-info { color: var(--muted); font-size: 12px; }
+ .rate-info { display: flex; flex-wrap: wrap; gap: 5px 10px; align-items: center; }
+ .rate-info a { color: var(--primary); }
+ .metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-bottom: 20px; }
+ .metric, .chart-card { border: 1px solid var(--border); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); }
.metric { padding: 16px 18px; }
- .metric-label {
- display: block;
- margin-bottom: 5px;
- color: var(--muted);
- font-size: 13px;
- }
+ .metric-label { display: block; margin-bottom: 5px; color: var(--muted); font-size: 13px; }
.metric-value { display: block; font-size: 23px; font-weight: 700; }
- .charts {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 20px;
- }
+ .charts { display: grid; grid-template-columns: 1fr; gap: 20px; }
.chart-card { min-width: 0; padding: 18px 18px 10px; }
.chart-card h2 { margin: 0 0 2px; font-size: 18px; }
.chart-card p { margin: 0; color: var(--muted); font-size: 13px; }
.chart { width: 100%; height: 450px; margin-top: 8px; }
- .empty-state {
- display: grid;
- min-height: 260px;
- place-items: center;
- padding: 28px;
- border: 1px dashed var(--border);
- border-radius: 14px;
- background: var(--surface);
- color: var(--muted);
- text-align: center;
- }
- .error-state {
- margin-top: 20px;
- padding: 16px 18px;
- border: 1px solid var(--error-border);
- border-radius: 12px;
- background: var(--error-bg);
- color: var(--error-text);
- }
- .error-state[hidden], .metrics[hidden], .charts[hidden], .empty-state[hidden] { display: none; }
- @media (max-width: 980px) {
- .charts { grid-template-columns: 1fr; }
- .chart { height: 400px; }
- }
+ .empty-state { display: grid; min-height: 260px; place-items: center; padding: 28px; border: 1px dashed var(--border); border-radius: 14px; background: var(--surface); color: var(--muted); text-align: center; }
+ .error-state { margin-top: 20px; padding: 16px 18px; border: 1px solid var(--error-border); border-radius: 12px; background: var(--error-bg); color: var(--error-text); }
+ .error-state[hidden], .filters[hidden], .metrics[hidden], .charts[hidden], .empty-state[hidden] { display: none; }
@media (max-width: 640px) {
.page { width: min(100% - 20px, 1440px); padding-top: 18px; }
.page-header { display: block; }
- .status { margin-top: 10px; text-align: left; }
+ .header-actions { justify-items: start; margin-top: 10px; }
+ .status { text-align: left; }
.metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.metric { padding: 13px 14px; }
.metric-value { font-size: 20px; }
.chart-card { padding: 14px 10px 8px; }
.chart { height: 360px; }
+ .filter-row { align-items: stretch; }
+ .filter-group { flex: 1 1 145px; }
}
@@ -139,10 +89,46 @@
TTSHOP 销售看板
Commission 与 GMV 趋势
- 正在加载数据…
+
-
+
+
+
+
+
+
+
+
+
+
日期范围
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
尚未需要汇率换算
+
汇率来源
+
公开接口通常每日更新,不承诺分钟级实时。
+
+
数据更新时间—
@@ -174,19 +160,28 @@
'use strict';
var DATA_URL = 'data/merged.html';
+ var RATES_URL = 'https://open.er-api.com/v6/latest/USD';
var REQUIRED_FIELDS = ['Date', 'Commission', 'Order', 'GMV', 'Currency', 'LinkName'];
var charts = [];
+ var ratesData = null;
+ var ratesPromise = null;
+ var renderToken = 0;
var numberFormatter = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 });
var dateFormatter = new Intl.DateTimeFormat('zh-CN', {
- year: 'numeric', month: '2-digit', day: '2-digit',
- hour: '2-digit', minute: '2-digit', second: '2-digit'
+ year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'
});
-
var elements = {
title: document.getElementById('page-title'),
subtitle: document.getElementById('page-subtitle'),
status: document.getElementById('status'),
- warning: document.getElementById('currency-warning'),
+ warning: document.getElementById('data-warning'),
+ filters: document.getElementById('filters'),
+ currency: document.getElementById('currency-select'),
+ currencyHelp: document.getElementById('currency-help'),
+ startDate: document.getElementById('start-date'),
+ endDate: document.getElementById('end-date'),
+ rangeButtons: Array.from(document.querySelectorAll('[data-range]')),
+ rateStatus: document.getElementById('rate-status'),
metrics: document.getElementById('metrics'),
updatedAt: document.getElementById('updated-at'),
recordCount: document.getElementById('record-count'),
@@ -200,21 +195,20 @@
overview: document.getElementById('overview-chart'),
link: document.getElementById('link-chart')
};
+ var state = { rawRecords: [], sourceCurrencies: [], selectedCurrency: '', latestDate: null, dateOnlyRows: 0, invalidCurrencyRows: 0 };
- function setStatus(message, state) {
+ function setStatus(message, status) {
elements.status.textContent = message;
- elements.status.dataset.state = state || '';
+ elements.status.dataset.state = status || '';
}
- function formatNumber(value) {
- return numberFormatter.format(Number(value) || 0);
- }
+ function formatNumber(value) { return numberFormatter.format(Number(value) || 0); }
function parseNumber(value) {
var normalized = String(value == null ? '' : value).trim().replace(/,/g, '');
- if (!normalized) return 0;
+ if (!normalized) return null;
var parsed = Number(normalized);
- return Number.isFinite(parsed) ? parsed : 0;
+ return Number.isFinite(parsed) ? parsed : null;
}
function parseDate(value) {
@@ -226,28 +220,23 @@
var day = Number(match[3]);
var date = new Date(Date.UTC(year, month - 1, day));
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return null;
- return {
- key: year + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0'),
- label: year + '/' + month + '/' + day,
- time: date.getTime()
- };
+ return { key: year + '-' + String(month).padStart(2, '0') + '-' + String(day).padStart(2, '0'), label: year + '/' + month + '/' + day, time: date.getTime() };
}
function readTable(htmlText) {
- var documentParser = new DOMParser();
- var parsedDocument = documentParser.parseFromString(htmlText, 'text/html');
+ var parsedDocument = new DOMParser().parseFromString(htmlText, 'text/html');
var tables = parsedDocument.querySelectorAll('table');
if (tables.length !== 1) throw new Error('数据文件必须包含唯一 table。');
-
var rows = Array.from(tables[0].querySelectorAll('tr'));
- if (!rows.length) return [];
+ if (!rows.length) return { records: [], dateOnlyRows: 0, invalidCurrencyRows: 0 };
var headers = Array.from(rows[0].children).map(function (cell) { return cell.textContent.trim(); });
var positions = {};
headers.forEach(function (header, index) { positions[header] = index; });
var missing = REQUIRED_FIELDS.filter(function (field) { return positions[field] === undefined; });
if (missing.length) throw new Error('数据缺少字段:' + missing.join('、'));
-
- return rows.slice(1).map(function (row) {
+ var dateOnlyRows = 0;
+ var invalidCurrencyRows = 0;
+ var records = rows.slice(1).map(function (row) {
var cells = Array.from(row.children);
function cell(field) {
var index = positions[field];
@@ -255,160 +244,245 @@
}
var parsedDate = parseDate(cell('Date'));
if (!parsedDate) return null;
- return {
- date: parsedDate,
- commission: parseNumber(cell('Commission')),
- gmv: parseNumber(cell('GMV')),
- currency: cell('Currency') || 'Unknown',
- linkName: cell('LinkName') || 'Unknown'
- };
+ var commission = parseNumber(cell('Commission'));
+ var gmv = parseNumber(cell('GMV'));
+ if (commission === null && gmv === null) {
+ dateOnlyRows += 1;
+ return null;
+ }
+ var currency = cell('Currency').toUpperCase();
+ if (!/^[A-Z]{3}$/.test(currency) || currency === 'UNK' || currency === 'UNKNOWN') {
+ invalidCurrencyRows += 1;
+ return null;
+ }
+ return { date: parsedDate, commission: commission, gmv: gmv, currency: currency, linkName: cell('LinkName') || 'Unknown' };
}).filter(Boolean);
+ return { records: records, dateOnlyRows: dateOnlyRows, invalidCurrencyRows: invalidCurrencyRows };
+ }
+
+ function disposeCharts() {
+ charts.forEach(function (chart) { chart.dispose(); });
+ charts = [];
+ }
+
+ function updateNotice() {
+ var messages = [];
+ if (state.dateOnlyRows) messages.push('已过滤 ' + formatNumber(state.dateOnlyRows) + ' 条只有 Date、Commission 和 GMV 均为空的日期行。');
+ if (state.invalidCurrencyRows) messages.push('已排除 ' + formatNumber(state.invalidCurrencyRows) + ' 条币种为空、Unknown 或格式无效且包含金额的记录,请检查原始数据。');
+ if (state.sourceCurrencies.length > 1) messages.push('检测到多个已知源币种(' + state.sourceCurrencies.join('、') + '),已统一换算为 USD,不能切换到 IDR。');
+ elements.warning.textContent = messages.join(' ');
+ elements.warning.hidden = messages.length === 0;
+ }
+
+ function setRateStatus(message) { elements.rateStatus.textContent = message; }
+
+ function updateCurrencyOptions() {
+ elements.currency.replaceChildren();
+ var sourceCurrencies = state.sourceCurrencies;
+ var options = [];
+ if (sourceCurrencies.length > 1) {
+ options = ['USD'];
+ state.selectedCurrency = 'USD';
+ elements.currency.disabled = true;
+ elements.currencyHelp.textContent = '多个源币种已自动统一为 USD。';
+ } else if (sourceCurrencies.length === 1) {
+ var source = sourceCurrencies[0];
+ options = source === 'USD' ? ['USD'] : [source, 'USD'];
+ state.selectedCurrency = source === 'IDR' ? 'IDR' : source;
+ elements.currency.disabled = options.length === 1;
+ elements.currencyHelp.textContent = options.length > 1 ? '可在源币种与 USD 之间切换。' : '当前数据已经是 USD。';
+ } else {
+ options = ['USD'];
+ state.selectedCurrency = 'USD';
+ elements.currency.disabled = true;
+ elements.currencyHelp.textContent = '';
+ }
+ options.forEach(function (currency) {
+ var option = document.createElement('option');
+ option.value = currency;
+ option.textContent = currency;
+ elements.currency.appendChild(option);
+ });
+ elements.currency.value = state.selectedCurrency;
+ }
+
+ function updateRangeInputs() {
+ var dates = state.rawRecords.map(function (record) { return record.date; }).sort(function (a, b) { return a.time - b.time; });
+ if (!dates.length) return;
+ var first = dates[0];
+ var latest = dates[dates.length - 1];
+ state.latestDate = latest;
+ elements.startDate.min = first.key;
+ elements.startDate.max = latest.key;
+ elements.endDate.min = first.key;
+ elements.endDate.max = latest.key;
+ setRange('month');
+ }
+
+ function setRange(rangeName) {
+ if (!state.latestDate || !state.rawRecords.length) return;
+ var dates = state.rawRecords.map(function (record) { return record.date; }).sort(function (a, b) { return a.time - b.time; });
+ var first = dates[0];
+ var latest = dates[dates.length - 1];
+ var start = first;
+ if (rangeName === 'month') start = parseDate(latest.key.slice(0, 7) + '-01');
+ if (rangeName === 'recent') start = new Date(latest.time - 29 * 24 * 60 * 60 * 1000);
+ var startKey = rangeName === 'recent' ? new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate())).toISOString().slice(0, 10) : start.key;
+ elements.startDate.value = startKey < first.key ? first.key : startKey;
+ elements.endDate.value = latest.key;
+ elements.rangeButtons.forEach(function (button) { button.dataset.active = button.dataset.range === rangeName ? 'true' : 'false'; });
+ }
+
+ function currentRange() {
+ var start = parseDate(elements.startDate.value);
+ var end = parseDate(elements.endDate.value);
+ if (!start || !end || start.time > end.time) throw new Error('开始日期必须早于或等于结束日期。');
+ return { start: start.time, end: end.time };
+ }
+
+ function needsRates() {
+ return state.rawRecords.some(function (record) { return record.currency !== state.selectedCurrency; });
+ }
+
+ function rateTimestamp(data) {
+ if (data.time_last_update_utc) return new Date(data.time_last_update_utc);
+ if (data.time_last_update_unix) return new Date(data.time_last_update_unix * 1000);
+ return null;
+ }
+
+ function fetchRates() {
+ if (ratesData) return Promise.resolve(ratesData);
+ if (ratesPromise) return ratesPromise;
+ setRateStatus('正在加载公开汇率…');
+ ratesPromise = fetch(RATES_URL, { cache: 'no-store' }).then(function (response) {
+ if (!response.ok) throw new Error('汇率接口 HTTP ' + response.status + '。');
+ return response.json();
+ }).then(function (data) {
+ if (!data || data.result !== 'success' || !data.rates) throw new Error('汇率接口返回无效数据。');
+ ratesData = data;
+ var updated = rateTimestamp(data);
+ setRateStatus('汇率已更新:' + (updated && !Number.isNaN(updated.getTime()) ? dateFormatter.format(updated) : '接口未提供时间'));
+ return data;
+ }).catch(function (error) {
+ ratesPromise = null;
+ setRateStatus('汇率加载失败');
+ throw error;
+ });
+ return ratesPromise;
+ }
+
+ function convertValue(value, source, target, data) {
+ if (value === null || source === target) return value;
+ var sourceRate = Number(data.rates[source]);
+ var targetRate = Number(data.rates[target]);
+ if (source !== 'USD' && (!Number.isFinite(sourceRate) || sourceRate <= 0)) throw new Error('汇率接口不支持源币种 ' + source + '。');
+ if (target !== 'USD' && (!Number.isFinite(targetRate) || targetRate <= 0)) throw new Error('汇率接口不支持目标币种 ' + target + '。');
+ if (target === 'USD') return value / sourceRate;
+ if (source === 'USD') return value * targetRate;
+ return value / sourceRate * targetRate;
+ }
+
+ function convertedRecords() {
+ var conversion = needsRates() ? fetchRates() : Promise.resolve(null);
+ return conversion.then(function (data) {
+ return state.rawRecords.map(function (record) {
+ return {
+ date: record.date,
+ commission: convertValue(record.commission, record.currency, state.selectedCurrency, data),
+ gmv: convertValue(record.gmv, record.currency, state.selectedCurrency, data),
+ linkName: record.linkName
+ };
+ });
+ });
+ }
+
+ function updateSummary(records) {
+ var links = new Set(records.map(function (record) { return record.linkName; }));
+ elements.title.textContent = 'TTSHOP 销售看板 · ' + state.selectedCurrency;
+ elements.subtitle.textContent = 'Commission 与 GMV 趋势(当前加载时间:' + dateFormatter.format(new Date()) + ')';
+ elements.updatedAt.textContent = dateFormatter.format(new Date());
+ elements.recordCount.textContent = formatNumber(records.length);
+ elements.linkCount.textContent = formatNumber(links.size);
+ elements.currencyCount.textContent = state.sourceCurrencies.join('、') || '—';
}
function aggregate(records) {
var byDate = new Map();
var byLink = new Map();
records.forEach(function (record) {
- if (!byDate.has(record.date.key)) {
- byDate.set(record.date.key, { date: record.date, commission: 0, gmv: 0 });
- }
+ if (!byDate.has(record.date.key)) byDate.set(record.date.key, { date: record.date, commission: 0, gmv: 0 });
var dateTotal = byDate.get(record.date.key);
- dateTotal.commission += record.commission;
- dateTotal.gmv += record.gmv;
-
+ dateTotal.commission += record.commission === null ? 0 : record.commission;
+ dateTotal.gmv += record.gmv === null ? 0 : record.gmv;
if (!byLink.has(record.linkName)) byLink.set(record.linkName, new Map());
var linkDates = byLink.get(record.linkName);
- if (!linkDates.has(record.date.key)) {
- linkDates.set(record.date.key, { date: record.date, commission: 0, gmv: 0 });
- }
+ if (!linkDates.has(record.date.key)) linkDates.set(record.date.key, { date: record.date, commission: 0, gmv: 0 });
var linkTotal = linkDates.get(record.date.key);
- linkTotal.commission += record.commission;
- linkTotal.gmv += record.gmv;
+ linkTotal.commission += record.commission === null ? 0 : record.commission;
+ linkTotal.gmv += record.gmv === null ? 0 : record.gmv;
+ });
+ return { dates: Array.from(byDate.values()).sort(function (a, b) { return a.date.time - b.date.time; }), byLink: byLink };
+ }
+
+ function escapeHtml(value) {
+ return String(value == null ? '' : value).replace(/[&<>\"']/g, function (character) {
+ return { '&': '&', '<': '<', '>': '>', '\"': '"', "'": ''' }[character];
});
- var dates = Array.from(byDate.values()).sort(function (a, b) { return a.date.time - b.date.time; });
- return { dates: dates, byLink: byLink };
}
function tooltipFormatter(params) {
var items = Array.isArray(params) ? params : [params];
var date = items.length && items[0].axisValue ? items[0].axisValue : '';
- var lines = [date];
+ var lines = [escapeHtml(date)];
items.forEach(function (item) {
var value = Array.isArray(item.value) ? item.value[item.value.length - 1] : item.value;
var metric = /GMV$/.test(item.seriesName) ? 'GMV' : 'Commission';
- lines.push(item.marker + ' ' + item.seriesName + ':' + metric + ' ' + formatNumber(value));
+ lines.push(item.marker + ' ' + escapeHtml(item.seriesName) + ':' + metric + ' ' + formatNumber(value));
});
return lines.join('
');
}
- function axisLabelFormatter(value) { return value; }
-
function makeCommonXAxis(categories) {
- return {
- type: 'category',
- data: categories,
- boundaryGap: true,
- axisLabel: { interval: categories.length > 30 ? Math.ceil(categories.length / 15) - 1 : 0, rotate: categories.length > 14 ? 30 : 0 }
- };
+ return { type: 'category', data: categories, boundaryGap: true, axisLabel: { interval: categories.length > 30 ? Math.ceil(categories.length / 15) - 1 : 0, rotate: categories.length > 14 ? 30 : 0 } };
}
- function renderCharts(records, currencyLabel) {
+ function renderCharts(records) {
+ if (!window.echarts) throw new Error('ECharts 未加载。');
var aggregated = aggregate(records);
var dates = aggregated.dates.map(function (item) { return item.date.label; });
- var commissionAxisName = 'Commission (' + currencyLabel + ')';
- var gmvAxisName = 'GMV (' + currencyLabel + ')';
-
- var overviewOption = {
- animation: false,
- color: ['#2563eb', '#f59e0b'],
- tooltip: { trigger: 'axis', axisPointer: { type: 'cross' }, formatter: tooltipFormatter },
- legend: { type: 'scroll', top: 2, data: ['Commission', 'GMV'] },
- grid: { left: 70, right: 76, top: 52, bottom: 62 },
- xAxis: makeCommonXAxis(dates),
- yAxis: [
- { type: 'value', name: commissionAxisName, nameLocation: 'middle', nameGap: 52, axisLabel: { formatter: axisLabelFormatter } },
- { type: 'value', name: gmvAxisName, nameLocation: 'middle', nameGap: 58, axisLabel: { formatter: axisLabelFormatter } }
- ],
- series: [
- {
- name: 'Commission', type: 'line', yAxisIndex: 0, stack: 'commission-total', smooth: true,
- symbol: 'circle', symbolSize: 6,
- data: aggregated.dates.map(function (item) { return item.commission; })
- },
- {
- name: 'GMV', type: 'bar', yAxisIndex: 1, stack: 'gmv-total', barMaxWidth: 34,
- data: aggregated.dates.map(function (item) { return item.gmv; })
- }
- ]
- };
-
+ var commissionAxisName = 'Commission (' + state.selectedCurrency + ')';
+ var gmvAxisName = 'GMV (' + state.selectedCurrency + ')';
+ var yAxes = [
+ { type: 'value', name: commissionAxisName, nameLocation: 'middle', nameGap: 52 },
+ { type: 'value', name: gmvAxisName, nameLocation: 'middle', nameGap: 58 }
+ ];
+ var common = { animation: false, tooltip: { trigger: 'axis', axisPointer: { type: 'cross' }, formatter: tooltipFormatter }, grid: { left: 70, right: 76, top: 52, bottom: 62 }, xAxis: makeCommonXAxis(dates), yAxis: yAxes };
+ var overviewSeries = [
+ { name: 'Commission', type: 'line', yAxisIndex: 0, smooth: true, symbol: 'circle', symbolSize: 6, data: aggregated.dates.map(function (item) { return item.commission; }) },
+ { name: 'GMV', type: 'bar', yAxisIndex: 1, stack: 'gmv-total', barMaxWidth: 34, data: aggregated.dates.map(function (item) { return item.gmv; }) }
+ ];
var links = Array.from(aggregated.byLink.keys()).sort(function (a, b) { return a.localeCompare(b); });
var linkSeries = [];
- links.forEach(function (linkName, linkIndex) {
+ links.forEach(function (linkName) {
var linkDates = aggregated.byLink.get(linkName);
- linkSeries.push({
- name: linkName + ' · Commission', type: 'line', yAxisIndex: 0, stack: 'commission-by-link', smooth: true,
- symbol: 'circle', symbolSize: 5,
- data: dates.map(function (_, index) {
- var item = aggregated.dates[index];
- var value = linkDates.get(item.date.key);
- return value ? value.commission : 0;
- })
- });
- linkSeries.push({
- name: linkName + ' · GMV', type: 'bar', yAxisIndex: 1, stack: 'gmv-by-link', barMaxWidth: 34,
- data: dates.map(function (_, index) {
- var item = aggregated.dates[index];
- var value = linkDates.get(item.date.key);
- return value ? value.gmv : 0;
- })
- });
+ linkSeries.push({ name: linkName + ' · Commission', type: 'line', yAxisIndex: 0, stack: 'commission-by-link', smooth: true, symbol: 'circle', symbolSize: 5, data: dates.map(function (_, index) { var item = aggregated.dates[index]; var value = linkDates.get(item.date.key); return value ? value.commission : 0; }) });
+ linkSeries.push({ name: linkName + ' · GMV', type: 'bar', yAxisIndex: 1, stack: 'gmv-by-link', barMaxWidth: 34, data: dates.map(function (_, index) { var item = aggregated.dates[index]; var value = linkDates.get(item.date.key); return value ? value.gmv : 0; }) });
});
- var linkOption = {
- animation: false,
- tooltip: { trigger: 'axis', axisPointer: { type: 'cross' }, formatter: tooltipFormatter },
- legend: { type: 'scroll', top: 2, data: linkSeries.map(function (series) { return series.name; }) },
- grid: { left: 70, right: 76, top: 52, bottom: 62 },
- xAxis: makeCommonXAxis(dates),
- yAxis: [
- { type: 'value', name: commissionAxisName, nameLocation: 'middle', nameGap: 52, axisLabel: { formatter: axisLabelFormatter } },
- { type: 'value', name: gmvAxisName, nameLocation: 'middle', nameGap: 58, axisLabel: { formatter: axisLabelFormatter } }
- ],
- series: linkSeries
- };
-
- charts.forEach(function (chart) { chart.dispose(); });
+ disposeCharts();
charts = [echarts.init(elements.overview), echarts.init(elements.link)];
- charts[0].setOption(overviewOption);
- charts[1].setOption(linkOption);
+ charts[0].setOption(Object.assign({}, common, { color: ['#2563eb', '#f59e0b'], legend: { type: 'scroll', top: 2, data: ['Commission', 'GMV'] }, series: overviewSeries }));
+ charts[1].setOption(Object.assign({}, common, { legend: { type: 'scroll', top: 2, data: linkSeries.map(function (series) { return series.name; }) }, series: linkSeries }));
elements.overviewDescription.textContent = '按日期汇总 · 主轴 ' + commissionAxisName + ' · 副轴 ' + gmvAxisName;
elements.linkDescription.textContent = '按日期和 LinkName 堆积 · 主轴 ' + commissionAxisName + ' · 副轴 ' + gmvAxisName;
}
- function updateSummary(records, currencies) {
- var links = new Set(records.map(function (record) { return record.linkName; }));
- var currencyList = Array.from(currencies).sort();
- var currencyLabel = currencyList.length === 1 ? currencyList[0] : 'Mixed';
- elements.title.textContent = 'TTSHOP 销售看板 · ' + currencyLabel;
- elements.subtitle.textContent = 'Commission 与 GMV 趋势(当前加载时间:' + dateFormatter.format(new Date()) + ')';
- elements.updatedAt.textContent = dateFormatter.format(new Date());
- elements.recordCount.textContent = formatNumber(records.length);
- elements.linkCount.textContent = formatNumber(links.size);
- elements.currencyCount.textContent = currencyList.join('、');
- if (currencyList.length > 1) {
- elements.warning.textContent = '检测到多个币种:' + currencyList.join('、') + '。页面不会静默合并币种,图表仅按原始数值展示,请结合币种核对数据。';
- elements.warning.hidden = false;
- } else {
- elements.warning.hidden = true;
- }
- return currencyLabel;
- }
-
- function showEmpty() {
+ function showEmpty(message) {
+ disposeCharts();
elements.metrics.hidden = false;
+ elements.empty.textContent = message || '没有可展示的数据。';
elements.empty.hidden = false;
elements.charts.hidden = true;
- setStatus('没有有效记录', 'empty');
+ setStatus('当前范围没有有效记录', 'empty');
}
function showError(error) {
@@ -420,33 +494,63 @@
setStatus('数据加载失败', 'error');
}
- function loadData() {
- fetch(DATA_URL, { cache: 'no-store' })
- .then(function (response) {
- if (!response.ok) throw new Error('无法读取 data/merged.html(HTTP ' + response.status + ')。');
- return response.text();
- })
- .then(function (htmlText) {
- var records = readTable(htmlText);
- elements.error.hidden = true;
- if (!records.length) {
- showEmpty();
- return;
- }
- var currencies = new Set(records.map(function (record) { return record.currency; }));
- var currencyLabel = updateSummary(records, currencies);
- elements.metrics.hidden = false;
- elements.empty.hidden = true;
- elements.charts.hidden = false;
- renderCharts(records, currencyLabel);
- setStatus('已加载 ' + formatNumber(records.length) + ' 条有效记录', 'ready');
- })
- .catch(showError);
+ function renderCurrentView() {
+ var token = ++renderToken;
+ var range;
+ try { range = currentRange(); } catch (error) { showError(error); return; }
+ setStatus(needsRates() ? '正在准备汇率与图表…' : '正在准备图表…', 'loading');
+ convertedRecords().then(function (records) {
+ if (token !== renderToken) return;
+ var filtered = records.filter(function (record) { return record.date.time >= range.start && record.date.time <= range.end; });
+ updateSummary(filtered);
+ elements.error.hidden = true;
+ elements.metrics.hidden = false;
+ if (!filtered.length) { showEmpty(); return; }
+ elements.empty.hidden = true;
+ elements.charts.hidden = false;
+ renderCharts(filtered);
+ setStatus('已加载 ' + formatNumber(filtered.length) + ' 条有效记录', 'ready');
+ }).catch(function (error) {
+ if (token === renderToken) showError(error);
+ });
}
- window.addEventListener('resize', function () {
- charts.forEach(function (chart) { chart.resize(); });
+ function loadData() {
+ fetch(DATA_URL, { cache: 'no-store' }).then(function (response) {
+ if (!response.ok) throw new Error('无法读取 data/merged.html(HTTP ' + response.status + ')。');
+ return response.text();
+ }).then(function (htmlText) {
+ var result = readTable(htmlText);
+ state.rawRecords = result.records;
+ state.dateOnlyRows = result.dateOnlyRows;
+ state.invalidCurrencyRows = result.invalidCurrencyRows;
+ state.sourceCurrencies = Array.from(new Set(result.records.map(function (record) { return record.currency; }))).sort();
+ updateNotice();
+ elements.error.hidden = true;
+ elements.filters.hidden = false;
+ updateCurrencyOptions();
+ updateRangeInputs();
+ if (!state.rawRecords.length) { showEmpty('没有可展示的有效数据。'); return; }
+ renderCurrentView();
+ }).catch(showError);
+ }
+
+ elements.currency.addEventListener('change', function () {
+ state.selectedCurrency = elements.currency.value;
+ renderCurrentView();
});
+ elements.startDate.addEventListener('change', function () {
+ elements.rangeButtons.forEach(function (button) { button.dataset.active = 'false'; });
+ renderCurrentView();
+ });
+ elements.endDate.addEventListener('change', function () {
+ elements.rangeButtons.forEach(function (button) { button.dataset.active = 'false'; });
+ renderCurrentView();
+ });
+ elements.rangeButtons.forEach(function (button) {
+ button.addEventListener('click', function () { setRange(button.dataset.range); renderCurrentView(); });
+ });
+ window.addEventListener('resize', function () { charts.forEach(function (chart) { chart.resize(); }); });
loadData();
}());