Files
ttshop/index.html
T

585 lines
31 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<link rel="icon" type="image/png" href="https://storage.googleapis.com/msgsndr/lW87lVmHdMr8CFmSlKXl/media/68402c8a28def01375942a3a.png">
<title>TTSHOP 销售看板</title>
<style>
:root {
color-scheme: light;
--bg: #f4f7fb;
--surface: #ffffff;
--text: #182230;
--muted: #65748b;
--border: #e4eaf2;
--primary: #2563eb;
--warning-bg: #fff7e6;
--warning-border: #f4c46d;
--warning-text: #8a5b00;
--error-bg: #fff1f2;
--error-border: #f5a6b1;
--error-text: #a51d2d;
--shadow: 0 8px 24px rgba(33, 52, 78, 0.07);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-width: 320px;
background: var(--bg);
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; }
.subtitle { margin: 0; color: var(--muted); }
.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[hidden] { display: none; }
.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-value { display: block; font-size: 23px; font-weight: 700; }
.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], .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; }
.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; }
}
</style>
</head>
<body>
<main class="page">
<header class="page-header">
<div>
<h1 id="page-title">TTSHOP 销售看板</h1>
<p class="subtitle" id="page-subtitle">Commission 与 GMV 趋势</p>
</div>
<div class="header-actions">
<a class="raw-link" href="data/merged.html" target="_blank" rel="noopener">查看原始数据</a>
<div class="status" id="status" data-state="loading" role="status" aria-live="polite">正在加载数据…</div>
</div>
</header>
<div class="alert" id="data-warning" role="alert" hidden></div>
<section class="filters" id="filters" aria-label="筛选与换算" hidden>
<div class="filter-row">
<div class="filter-group">
<label for="currency-select">显示币种</label>
<select id="currency-select"></select>
</div>
<div class="filter-group">
<span>日期范围</span>
<div class="range-actions">
<button class="range-button" type="button" data-range="month">当月(按数据最新月份)</button>
<button class="range-button" type="button" data-range="recent">最近30天(按数据最新日期)</button>
<button class="range-button" type="button" data-range="all">全部</button>
</div>
</div>
</div>
<div class="filter-row">
<div class="filter-group">
<label for="start-date">开始日期</label>
<input id="start-date" type="date">
</div>
<div class="filter-group">
<label for="end-date">结束日期</label>
<input id="end-date" type="date">
</div>
<div class="currency-help" id="currency-help"></div>
</div>
<div class="rate-info" id="rate-info">
<span id="rate-status">尚未需要汇率换算</span>
<a href="https://www.exchangerate-api.com" target="_blank" rel="noopener">汇率来源</a>
<span>公开接口通常每日更新,不承诺分钟级实时。</span>
</div>
</section>
<section class="metrics" id="metrics" aria-label="数据概览" hidden>
<div class="metric"><span class="metric-label">数据更新时间</span><strong class="metric-value" id="updated-at">—</strong></div>
<div class="metric"><span class="metric-label">记录数</span><strong class="metric-value" id="record-count">—</strong></div>
<div class="metric"><span class="metric-label">LinkName 数</span><strong class="metric-value" id="link-count">—</strong></div>
<div class="metric"><span class="metric-label">币种</span><strong class="metric-value" id="currency-count">—</strong></div>
</section>
<div class="empty-state" id="empty-state" hidden>没有可展示的数据。</div>
<div class="error-state" id="error-state" role="alert" hidden></div>
<section class="charts" id="charts" aria-label="数据图表" hidden>
<article class="chart-card">
<h2>总体趋势</h2>
<p id="overview-description">按日期汇总 Commission 与 GMV</p>
<div class="chart" id="overview-chart" role="img" aria-label="按日期汇总的 Commission 柱状图和 GMV 折线图"></div>
</article>
<article class="chart-card">
<h2>LinkName Commission 趋势</h2>
<p id="link-commission-description">按日期和 LinkName 堆积显示 Commission</p>
<div class="chart" id="link-commission-chart" role="img" aria-label="按日期和 LinkName 堆积的 Commission 柱状图"></div>
</article>
<article class="chart-card">
<h2>LinkName GMV 趋势</h2>
<p id="link-gmv-description">按日期和 LinkName 堆积显示 GMV</p>
<div class="chart" id="link-gmv-chart" role="img" aria-label="按日期和 LinkName 堆积的 GMV 柱状图"></div>
</article>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script>
(function () {
'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'
});
var elements = {
title: document.getElementById('page-title'),
subtitle: document.getElementById('page-subtitle'),
status: document.getElementById('status'),
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'),
linkCount: document.getElementById('link-count'),
currencyCount: document.getElementById('currency-count'),
empty: document.getElementById('empty-state'),
error: document.getElementById('error-state'),
charts: document.getElementById('charts'),
overviewDescription: document.getElementById('overview-description'),
linkCommissionDescription: document.getElementById('link-commission-description'),
linkGmvDescription: document.getElementById('link-gmv-description'),
overview: document.getElementById('overview-chart'),
linkCommission: document.getElementById('link-commission-chart'),
linkGmv: document.getElementById('link-gmv-chart')
};
var state = { rawRecords: [], sourceCurrencies: [], selectedCurrency: '', latestDate: null, dateOnlyRows: 0, invalidCurrencyRows: 0 };
function setStatus(message, status) {
elements.status.textContent = message;
elements.status.dataset.state = status || '';
}
function formatNumber(value) { return numberFormatter.format(Number(value) || 0); }
function parseNumber(value) {
var normalized = String(value == null ? '' : value).trim().replace(/,/g, '');
if (!normalized) return null;
var parsed = Number(normalized);
return Number.isFinite(parsed) ? parsed : null;
}
function parseDate(value) {
var text = String(value == null ? '' : value).trim();
var match = /^(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})$/.exec(text);
if (!match) return null;
var year = Number(match[1]);
var month = Number(match[2]);
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() };
}
function readTable(htmlText) {
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 { 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('、'));
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];
return index < cells.length ? cells[index].textContent.trim() : '';
}
var parsedDate = parseDate(cell('Date'));
if (!parsedDate) return null;
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 });
var dateTotal = byDate.get(record.date.key);
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 });
var linkTotal = linkDates.get(record.date.key);
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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', "'": '&#39;' }[character];
});
}
function tooltipFormatter(params) {
var items = Array.isArray(params) ? params : [params];
var date = items.length && items[0].axisValue ? items[0].axisValue : '';
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 + ' ' + escapeHtml(item.seriesName) + ':' + metric + ' ' + formatNumber(value));
});
return lines.join('<br>');
}
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 } };
}
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 (' + state.selectedCurrency + ')';
var gmvAxisName = 'GMV (' + state.selectedCurrency + ')';
var dualAxes = [
{ 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) };
var overviewSeries = [
{ name: 'Commission', type: 'bar', yAxisIndex: 0, barMaxWidth: 34, data: aggregated.dates.map(function (item) { return item.commission; }) },
{ name: 'GMV', type: 'line', yAxisIndex: 1, smooth: true, symbol: 'circle', symbolSize: 6, 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 linkCommissionSeries = [];
var linkGmvSeries = [];
links.forEach(function (linkName) {
var linkDates = aggregated.byLink.get(linkName);
var values = dates.map(function (_, index) {
var item = aggregated.dates[index];
var value = linkDates.get(item.date.key);
return value || { commission: 0, gmv: 0 };
});
linkCommissionSeries.push({ name: linkName + ' · Commission', type: 'bar', stack: 'commission-by-link', smooth: true, symbol: 'circle', symbolSize: 5, data: values.map(function (value) { return value.commission; }) });
linkGmvSeries.push({ name: linkName + ' · GMV', type: 'bar', stack: 'gmv-by-link', barMaxWidth: 34, data: values.map(function (value) { return value.gmv; }) });
});
var commissionChart = Object.assign({}, common, {
yAxis: [{ type: 'value', name: commissionAxisName, nameLocation: 'middle', nameGap: 52 }],
legend: { type: 'scroll', top: 2, data: linkCommissionSeries.map(function (series) { return series.name; }) },
series: linkCommissionSeries
});
var gmvChart = Object.assign({}, common, {
yAxis: [{ type: 'value', name: gmvAxisName, nameLocation: 'middle', nameGap: 52 }],
legend: { type: 'scroll', top: 2, data: linkGmvSeries.map(function (series) { return series.name; }) },
series: linkGmvSeries
});
disposeCharts();
charts = [echarts.init(elements.overview), echarts.init(elements.linkCommission), echarts.init(elements.linkGmv)];
charts[0].setOption(Object.assign({}, common, { yAxis: dualAxes, color: ['#2563eb', '#f59e0b'], legend: { type: 'scroll', top: 2, data: ['Commission', 'GMV'] }, series: overviewSeries }));
charts[1].setOption(commissionChart);
charts[2].setOption(gmvChart);
elements.overviewDescription.textContent = '按日期汇总 · Commission 柱状图使用主轴 · GMV 折线图使用副轴 · ' + state.selectedCurrency;
elements.linkCommissionDescription.textContent = '按日期和 LinkName 堆积 · Commission 柱状图 · 主轴 ' + commissionAxisName;
elements.linkGmvDescription.textContent = '按日期和 LinkName 堆积 · GMV 柱状图 · 主轴 ' + gmvAxisName;
}
function showEmpty(message) {
disposeCharts();
elements.metrics.hidden = false;
elements.empty.textContent = message || '没有可展示的数据。';
elements.empty.hidden = false;
elements.charts.hidden = true;
setStatus('当前范围没有有效记录', 'empty');
}
function showError(error) {
elements.metrics.hidden = true;
elements.empty.hidden = true;
elements.charts.hidden = true;
elements.error.textContent = '加载失败:' + (error && error.message ? error.message : '未知错误');
elements.error.hidden = false;
setStatus('数据加载失败', 'error');
}
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);
});
}
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();
}());
</script>
</body>
</html>