feat: publish TaurusX order coverage dashboard

This commit is contained in:
王源
2026-10-14 17:12:54 +08:00
commit 90e485e478
4 changed files with 12573 additions and 0 deletions
+469
View File
@@ -0,0 +1,469 @@
<!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">
<title>TaurusX订单</title>
<style>
:root {
--ink: #15201d;
--muted: #6d7773;
--line: #dfe6e2;
--surface: #ffffff;
--canvas: #f3f6f3;
--green: #0e7355;
--green-soft: #ddf2e9;
--amber: #a65c00;
--amber-soft: #fff0d5;
--red: #b5312d;
--red-soft: #fde8e5;
--blue: #2b63b7;
--shadow: 0 10px 30px rgba(30, 52, 43, .08);
}
* { box-sizing: border-box; }
html { background: var(--canvas); }
body {
margin: 0;
color: var(--ink);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 14px;
}
button, input, select { font: inherit; }
button { cursor: pointer; }
.hero {
position: relative;
overflow: hidden;
padding: 34px max(24px, calc((100vw - 1440px) / 2));
color: #f7fffb;
background: linear-gradient(125deg, #12392f 0%, #0d6049 60%, #187a5e 100%);
}
.hero::after {
content: "TX";
position: absolute;
right: 4%; top: -54px;
font: 800 190px/1 Inter, sans-serif;
letter-spacing: -18px;
color: rgba(255,255,255,.055);
transform: rotate(-7deg);
}
.eyebrow { margin-bottom: 9px; color: #aee2cf; font-size: 12px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; }
h1 { margin: 0; font-size: clamp(30px, 4vw, 47px); letter-spacing: -.04em; line-height: 1.08; }
.hero p { max-width: 760px; margin: 13px 0 0; color: #d2e9e0; line-height: 1.7; }
.hero-meta { display: flex; flex-wrap: wrap; gap: 8px 20px; margin-top: 20px; color: #bcdacf; font-size: 12px; }
.page { max-width: 1440px; margin: 0 auto; padding: 24px; }
.notice {
display: flex; align-items: flex-start; gap: 10px;
margin-bottom: 18px; padding: 13px 15px;
border: 1px solid #bdd9cd; border-radius: 12px;
background: #edf8f3; color: #285d4c; line-height: 1.55;
}
.notice b { color: #114f3d; }
.kpis { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 14px; }
.card, .panel { border: 1px solid var(--line); border-radius: 16px; background: var(--surface); box-shadow: var(--shadow); }
.kpi { min-height: 126px; padding: 18px; }
.kpi .label { color: var(--muted); font-size: 12px; font-weight: 650; }
.kpi .value { margin-top: 10px; font-size: 31px; font-weight: 760; letter-spacing: -.04em; }
.kpi .sub { margin-top: 8px; color: var(--muted); font-size: 12px; line-height: 1.5; }
.kpi.danger { border-top: 4px solid var(--red); }
.kpi.success { border-top: 4px solid var(--green); }
.coverage-ring {
--value: 0deg;
display: inline-grid; width: 48px; height: 48px; margin-left: 8px;
place-items: center; border-radius: 50%; vertical-align: middle;
background: conic-gradient(var(--green) var(--value), #e5ebe8 0);
}
.coverage-ring::after { content: ""; width: 34px; height: 34px; border-radius: 50%; background: white; }
.split { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(300px, .8fr); gap: 16px; margin-top: 16px; }
.panel { padding: 18px; }
.panel-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 15px; }
.panel h2 { margin: 0; font-size: 17px; letter-spacing: -.02em; }
.panel-desc { margin-top: 5px; color: var(--muted); font-size: 12px; }
.platform-bars { display: grid; gap: 11px; }
.bar-row { display: grid; grid-template-columns: 118px 1fr 38px; gap: 10px; align-items: center; }
.bar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; font-weight: 650; }
.bar-track { height: 8px; overflow: hidden; border-radius: 99px; background: #edf1ef; }
.bar-fill { height: 100%; min-width: 2px; border-radius: inherit; background: linear-gradient(90deg, #38a27e, #0e7355); }
.bar-fill.target { background: linear-gradient(90deg, #e2a642, #bd6d00); }
.bar-value { color: var(--muted); font-size: 12px; text-align: right; }
.country-list { display: grid; gap: 9px; }
.country-row { display: grid; grid-template-columns: 36px 1fr auto; gap: 8px; align-items: center; }
.country-code { font-weight: 750; }
.country-track { height: 7px; border-radius: 99px; background: #f0f2f1; overflow: hidden; }
.country-missing { height: 100%; border-radius: inherit; background: #dc817b; }
.country-count { min-width: 35px; color: var(--red); font-size: 12px; text-align: right; }
.workspace { margin-top: 16px; padding: 0; overflow: hidden; }
.workspace-head { padding: 18px 18px 0; }
.tabs { display: flex; gap: 4px; padding: 4px; border-radius: 10px; background: #edf1ef; }
.tab { padding: 8px 13px; border: 0; border-radius: 8px; background: transparent; color: var(--muted); font-size: 12px; font-weight: 650; }
.tab.active { background: white; color: var(--ink); box-shadow: 0 2px 8px rgba(18,57,47,.09); }
.filters { display: grid; grid-template-columns: minmax(220px, 1.3fr) repeat(3, minmax(135px, .55fr)) auto; gap: 10px; margin-top: 16px; }
.control { width: 100%; height: 40px; border: 1px solid var(--line); border-radius: 9px; outline: none; background: white; color: var(--ink); }
input.control { padding: 0 12px; }
select.control { padding: 0 30px 0 10px; }
.control:focus { border-color: #63a68e; box-shadow: 0 0 0 3px rgba(14,115,85,.1); }
.button { height: 40px; padding: 0 14px; border: 1px solid #b8c9c2; border-radius: 9px; background: #f6faf8; color: #185a45; font-weight: 700; }
.button:hover { background: #eaf5f0; }
.result-meta { display: flex; justify-content: space-between; gap: 12px; padding: 14px 18px 10px; color: var(--muted); font-size: 12px; }
.table-wrap { width: 100%; overflow: auto; border-top: 1px solid var(--line); }
table { width: 100%; border-collapse: collapse; }
th { position: sticky; top: 0; z-index: 1; padding: 11px 12px; background: #f7f9f8; color: #5e6965; font-size: 11px; font-weight: 750; text-align: left; white-space: nowrap; }
td { padding: 12px; border-top: 1px solid #edf0ee; vertical-align: top; }
tr:hover td { background: #fbfcfb; }
.pkg { max-width: 330px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; overflow-wrap: anywhere; }
.product { max-width: 240px; font-weight: 650; }
.country-pill { display: inline-block; min-width: 34px; padding: 4px 7px; border-radius: 7px; background: #edf2f7; color: #395065; font-size: 11px; font-weight: 750; text-align: center; }
.badge, .platform-pill { display: inline-flex; align-items: center; border-radius: 999px; white-space: nowrap; }
.badge { padding: 5px 9px; font-size: 11px; font-weight: 750; }
.badge.missing { color: var(--red); background: var(--red-soft); }
.badge.covered { color: var(--green); background: var(--green-soft); }
.badge.target-only { color: var(--amber); background: var(--amber-soft); }
.platforms { display: flex; flex-wrap: wrap; gap: 5px; min-width: 220px; }
.platform-pill { padding: 4px 8px; background: #eff3f1; color: #4d5e57; font-size: 11px; }
.platform-pill.target { background: var(--green-soft); color: var(--green); font-weight: 750; }
.matrix-table th:first-child, .matrix-table td:first-child { position: sticky; left: 0; z-index: 2; background: white; }
.matrix-table th:first-child { z-index: 3; background: #f7f9f8; }
.matrix-dot { display: block; width: 18px; height: 18px; margin: auto; border-radius: 5px; background: #e9eeeb; }
.matrix-dot.yes { background: var(--green); box-shadow: inset 0 0 0 5px #dff1ea; }
.matrix-dot.target { background: var(--amber); box-shadow: inset 0 0 0 5px #ffefd2; }
.pagination { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 14px 18px 18px; }
.page-button { min-width: 34px; height: 34px; border: 1px solid var(--line); border-radius: 8px; background: white; color: var(--ink); }
.page-button:disabled { cursor: default; opacity: .42; }
.empty { padding: 46px 20px !important; color: var(--muted); text-align: center; }
.loading { padding: 90px 24px; color: var(--muted); text-align: center; }
.error { max-width: 740px; margin: 80px auto; padding: 24px; border: 1px solid #e4aaa6; border-radius: 14px; background: #fff4f2; color: #8c2c28; }
.hidden { display: none !important; }
footer { padding: 22px 0 8px; color: #7b8581; font-size: 11px; text-align: center; }
@media (max-width: 1050px) {
.kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.split { grid-template-columns: 1fr; }
.filters { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 650px) {
.hero { padding: 28px 18px; }
.page { padding: 14px; }
.kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.kpi { min-height: 112px; padding: 14px; }
.kpi .value { font-size: 26px; }
.filters { grid-template-columns: 1fr; }
.panel-head { flex-direction: column; }
.tabs { width: 100%; }
.tab { flex: 1; padding-inline: 7px; }
}
</style>
</head>
<body>
<header class="hero">
<div class="eyebrow">AgentStage · Order Coverage</div>
<h1>TaurusX订单</h1>
<p>以 <strong>TaurusxDSP</strong> 为目标平台,按「Pkg + 投放国家」聚合各平台订单,快速定位其他平台已出现、但尚未搬入 TaurusxDSP 的订单组合。</p>
<div class="hero-meta"><span id="sourceLabel">数据源加载中</span><span id="updatedLabel">更新时间加载中</span></div>
</header>
<main class="page" id="app">
<div class="loading">正在读取订单覆盖数据…</div>
</main>
<script>
'use strict';
const PAGE_SIZE = 40;
const state = { data: null, filtered: [], page: 1, view: 'detail' };
const nf = new Intl.NumberFormat('zh-CN');
const el = (tag, className, text) => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
};
function statusOf(record) {
if (record.missing_target) return { key: 'missing', label: '待搬单' };
if (record.target_present && !record.other_platforms.length) return { key: 'target-only', label: '仅 TaurusxDSP' };
if (record.target_present) return { key: 'covered', label: '已覆盖' };
return { key: 'other', label: '其他' };
}
function percent(value) { return `${(value * 100).toFixed(1)}%`; }
function formatTime(value) {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false });
}
function renderShell() {
const { meta } = state.data;
document.getElementById('sourceLabel').textContent = `数据源:${meta.source}`;
document.getElementById('updatedLabel').textContent = `数据生成:${formatTime(meta.generated_at)}`;
const app = document.getElementById('app');
app.textContent = '';
const notice = el('div', 'notice');
notice.append(el('span', '', '◎'));
const noticeText = el('div');
const strong = el('b', '', '判断口径:');
noticeText.append(strong, document.createTextNode('同一 Pkg + 国家存在至少一个非 TaurusxDSP 平台,且平台集合中没有 TaurusxDSP,即标记为“待搬单”。所有历史订单状态均计入平台出现关系。'));
notice.append(noticeText);
app.append(notice);
const kpis = el('section', 'kpis');
kpis.append(
kpi('待对比 Pkg_国家', nf.format(meta.comparison_count), `共 ${nf.format(meta.pkg_country_count)} 个唯一组合`),
kpi('待搬单', nf.format(meta.missing_count), '其他平台已有,TaurusxDSP 未出现', 'danger'),
kpi('已覆盖', nf.format(meta.covered_count), '其他平台与 TaurusxDSP 均出现', 'success'),
kpi('TaurusxDSP 覆盖率', percent(meta.coverage_rate), `${nf.format(meta.covered_count)} / ${nf.format(meta.comparison_count)}`),
kpi('有效源记录', nf.format(meta.valid_rows), `排除示例/缺失/无效 Pkg ${meta.excluded_examples + meta.excluded_incomplete + (meta.excluded_invalid_pkg || 0)} 行`)
);
app.append(kpis);
const split = el('section', 'split');
split.append(renderPlatformPanel(), renderCountryPanel());
app.append(split, renderWorkspace(), el('footer', '', 'TaurusX订单 · 数据仅包含覆盖分析所需字段,不含链接、负责人、价格或 KPI'));
populateFilters();
bindEvents();
applyFilters();
}
function kpi(label, value, sub, modifier = '') {
const card = el('article', `card kpi ${modifier}`.trim());
card.append(el('div', 'label', label), el('div', 'value', value), el('div', 'sub', sub));
return card;
}
function renderPlatformPanel() {
const panel = el('article', 'panel');
const head = el('div', 'panel-head');
const title = el('div');
title.append(el('h2', '', '平台覆盖规模'), el('div', 'panel-desc', '按唯一 Pkg + 国家组合计数,已合并平台名称大小写与账号备注'));
head.append(title);
panel.append(head);
const bars = el('div', 'platform-bars');
const rows = state.data.platform_summary.slice(0, 12);
const max = Math.max(...rows.map(row => row.key_count), 1);
rows.forEach(row => {
const line = el('div', 'bar-row');
const label = el('div', 'bar-label', row.platform);
label.title = row.platform;
const track = el('div', 'bar-track');
const fill = el('div', `bar-fill ${row.platform === state.data.meta.target_platform ? 'target' : ''}`.trim());
fill.style.width = `${Math.max(1.5, row.key_count / max * 100)}%`;
track.append(fill);
line.append(label, track, el('div', 'bar-value', nf.format(row.key_count)));
bars.append(line);
});
panel.append(bars);
return panel;
}
function renderCountryPanel() {
const panel = el('article', 'panel');
const head = el('div', 'panel-head');
const title = el('div');
title.append(el('h2', '', '缺口国家 Top 10'), el('div', 'panel-desc', '待搬单组合最多的国家'));
head.append(title);
panel.append(head);
const list = el('div', 'country-list');
const rows = state.data.country_summary.slice(0, 10);
const max = Math.max(...rows.map(row => row.missing_count), 1);
rows.forEach(row => {
const line = el('div', 'country-row');
const track = el('div', 'country-track');
const fill = el('div', 'country-missing');
fill.style.width = `${row.missing_count / max * 100}%`;
track.append(fill);
line.append(el('div', 'country-code', row.country), track, el('div', 'country-count', nf.format(row.missing_count)));
list.append(line);
});
panel.append(list);
return panel;
}
function renderWorkspace() {
const panel = el('section', 'panel workspace');
const header = el('div', 'workspace-head');
const head = el('div', 'panel-head');
const title = el('div');
title.append(el('h2', '', 'Pkg_投放国家覆盖明细'), el('div', 'panel-desc', '默认只展示待搬单记录;可切换到矩阵视图查看平台交叉覆盖'));
const tabs = el('div', 'tabs');
[['detail','明细'], ['matrix','平台矩阵']].forEach(([key, label]) => {
const button = el('button', `tab ${key === state.view ? 'active' : ''}`.trim(), label);
button.type = 'button'; button.dataset.view = key;
tabs.append(button);
});
head.append(title, tabs);
const filters = el('div', 'filters');
const search = el('input', 'control');
search.id = 'search'; search.type = 'search'; search.placeholder = '搜索 Pkg 或产品名'; search.autocomplete = 'off';
filters.append(search, selectControl('countryFilter', '全部国家'), selectControl('platformFilter', '全部平台'), selectControl('statusFilter', ''), el('button', 'button', '导出当前结果'));
filters.lastChild.id = 'exportButton'; filters.lastChild.type = 'button';
header.append(head, filters);
panel.append(header, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination'));
return panel;
}
function selectControl(id, firstLabel) {
const select = el('select', 'control');
select.id = id;
if (firstLabel) select.append(new Option(firstLabel, 'all'));
return select;
}
function populateFilters() {
const country = document.getElementById('countryFilter');
[...new Set(state.data.records.map(row => row.country))].sort().forEach(value => country.append(new Option(value, value)));
const platform = document.getElementById('platformFilter');
state.data.platform_summary.map(row => row.platform).sort((a,b) => a.localeCompare(b)).forEach(value => platform.append(new Option(value, value)));
const status = document.getElementById('statusFilter');
[['missing','只看待搬单'], ['all','全部覆盖状态'], ['covered','只看已覆盖'], ['target-only','仅 TaurusxDSP']].forEach(([value,label]) => status.append(new Option(label, value)));
status.value = 'missing';
}
function bindEvents() {
['search', 'countryFilter', 'platformFilter', 'statusFilter'].forEach(id => {
document.getElementById(id).addEventListener(id === 'search' ? 'input' : 'change', () => { state.page = 1; applyFilters(); });
});
document.getElementById('exportButton').addEventListener('click', exportCsv);
document.querySelectorAll('.tab').forEach(button => button.addEventListener('click', () => {
state.view = button.dataset.view;
document.querySelectorAll('.tab').forEach(node => node.classList.toggle('active', node === button));
renderResults();
}));
}
function applyFilters() {
const query = document.getElementById('search').value.trim().toLocaleLowerCase();
const country = document.getElementById('countryFilter').value;
const platform = document.getElementById('platformFilter').value;
const status = document.getElementById('statusFilter').value;
state.filtered = state.data.records.filter(row => {
const recordStatus = statusOf(row).key;
return (!query || `${row.pkg} ${row.product}`.toLocaleLowerCase().includes(query))
&& (country === 'all' || row.country === country)
&& (platform === 'all' || row.platforms.includes(platform))
&& (status === 'all' || recordStatus === status);
});
renderResults();
}
function renderResults() {
const totalPages = Math.max(1, Math.ceil(state.filtered.length / PAGE_SIZE));
state.page = Math.min(state.page, totalPages);
const start = (state.page - 1) * PAGE_SIZE;
const rows = state.filtered.slice(start, start + PAGE_SIZE);
const meta = document.querySelector('.result-meta');
meta.textContent = '';
meta.append(el('span', '', `筛选结果 ${nf.format(state.filtered.length)} 条`), el('span', '', `第 ${state.page} / ${totalPages} 页`));
if (state.view === 'detail') renderDetailTable(rows); else renderMatrixTable(rows);
renderPagination(totalPages);
}
function renderDetailTable(rows) {
const wrap = document.querySelector('.table-wrap');
wrap.textContent = '';
const table = el('table');
const thead = el('thead');
const header = el('tr');
['搬单状态', '产品', 'Pkg', '国家', '已出现平台', '源订单行数'].forEach(label => header.append(el('th', '', label)));
thead.append(header); table.append(thead);
const tbody = el('tbody');
if (!rows.length) {
const cell = el('td', 'empty', '没有符合当前条件的记录'); cell.colSpan = 6;
const row = el('tr'); row.append(cell); tbody.append(row);
}
rows.forEach(record => {
const row = el('tr');
const status = statusOf(record);
const badgeCell = el('td'); badgeCell.append(el('span', `badge ${status.key}`, status.label));
const product = el('td', 'product', record.product);
const pkg = el('td', 'pkg', record.pkg);
const country = el('td'); country.append(el('span', 'country-pill', record.country));
const platforms = el('td');
const pills = el('div', 'platforms');
record.platforms.forEach(name => pills.append(el('span', `platform-pill ${name === state.data.meta.target_platform ? 'target' : ''}`.trim(), name)));
platforms.append(pills);
row.append(badgeCell, product, pkg, country, platforms, el('td', '', nf.format(record.order_count)));
tbody.append(row);
});
table.append(tbody); wrap.append(table);
}
function renderMatrixTable(rows) {
const wrap = document.querySelector('.table-wrap');
wrap.textContent = '';
const platforms = state.data.platform_summary.map(row => row.platform);
const table = el('table', 'matrix-table');
const thead = el('thead'); const header = el('tr');
header.append(el('th', '', 'Pkg_国家'));
platforms.forEach(name => header.append(el('th', '', name)));
thead.append(header); table.append(thead);
const tbody = el('tbody');
if (!rows.length) {
const cell = el('td', 'empty', '没有符合当前条件的记录'); cell.colSpan = platforms.length + 1;
const row = el('tr'); row.append(cell); tbody.append(row);
}
rows.forEach(record => {
const row = el('tr');
const key = el('td');
key.append(el('div', 'pkg', record.pkg), el('div', 'panel-desc', record.country));
row.append(key);
platforms.forEach(name => {
const cell = el('td');
const active = record.platforms.includes(name);
const dot = el('span', `matrix-dot ${active ? 'yes' : ''} ${active && name === state.data.meta.target_platform ? 'target' : ''}`.trim());
dot.title = active ? `${name}:已出现` : `${name}:未出现`;
cell.append(dot); row.append(cell);
});
tbody.append(row);
});
table.append(tbody); wrap.append(table);
}
function renderPagination(totalPages) {
const box = document.querySelector('.pagination');
box.textContent = '';
const prev = el('button', 'page-button', '‹'); prev.type = 'button'; prev.disabled = state.page <= 1;
const next = el('button', 'page-button', '›'); next.type = 'button'; next.disabled = state.page >= totalPages;
prev.addEventListener('click', () => { state.page -= 1; renderResults(); document.querySelector('.workspace').scrollIntoView({ behavior: 'smooth' }); });
next.addEventListener('click', () => { state.page += 1; renderResults(); document.querySelector('.workspace').scrollIntoView({ behavior: 'smooth' }); });
box.append(prev, el('span', 'panel-desc', `${state.page} / ${totalPages}`), next);
}
function exportCsv() {
const headers = ['搬单状态', '产品', 'Pkg', '投放国家', '已出现平台', '是否有TaurusxDSP', '源订单行数'];
const csvRows = [headers, ...state.filtered.map(record => [
statusOf(record).label,
record.product,
record.pkg,
record.country,
record.platforms.join(' / '),
record.target_present ? '是' : '否',
record.order_count
])];
const csv = '\ufeff' + csvRows.map(row => row.map(value => `"${String(value).replaceAll('"', '""')}"`).join(',')).join('\n');
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
const link = document.createElement('a');
link.href = url; link.download = `TaurusX订单_${new Date().toISOString().slice(0,10)}.csv`;
document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url);
}
fetch(`data.json?v=${Date.now()}`, { cache: 'no-store' })
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(data => {
if (!data.meta || !Array.isArray(data.records)) throw new Error('data.json 格式不正确');
state.data = data;
renderShell();
})
.catch(error => {
const app = document.getElementById('app');
app.textContent = '';
const box = el('div', 'error');
box.append(el('h2', '', '数据加载失败'), el('p', '', `无法读取 data.json:${error.message}`));
app.append(box);
});
</script>
</body>
</html>