Files
taurusx_tracking/index.html
T

721 lines
38 KiB
HTML
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">
<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><a href="taurusx.html" style="color:#e8fff6;font-weight:700;text-decoration:none;border-bottom:1px solid rgba(232,255,246,.55);">TaurusX 投放数据 →</a></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');
filters.style.gridTemplateColumns = 'minmax(220px, 1.3fr) repeat(4, minmax(125px, .55fr)) auto';
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', ''),
selectControl('orderStatusFilter', '全部订单状态'),
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';
const orderStatus = document.getElementById('orderStatusFilter');
['投放中', '暂停', '未标注', '其他/不可投'].forEach(value => orderStatus.append(new Option(value, value)));
}
function bindEvents() {
['search', 'countryFilter', 'platformFilter', 'statusFilter', 'orderStatusFilter'].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 search = document.getElementById('search');
search.placeholder = '搜索 Adpingid、Pkg 或产品名';
const query = search.value.trim().toLocaleLowerCase();
const country = document.getElementById('countryFilter').value;
const platform = document.getElementById('platformFilter').value;
const status = document.getElementById('statusFilter').value;
const orderStatus = document.getElementById('orderStatusFilter').value;
state.filtered = state.data.records.filter(row => {
const recordStatus = statusOf(row).key;
const searchable = `${row.adpingids.join(' ')} ${row.pkg} ${row.product}`.toLocaleLowerCase();
return (!query || searchable.includes(query))
&& (country === 'all' || row.country === country)
&& (platform === 'all' || row.platforms.includes(platform))
&& (status === 'all' || recordStatus === status)
&& (orderStatus === 'all' || row.order_statuses.includes(orderStatus));
});
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');
['搬单状态', 'Adpingid', '产品', '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 = 8;
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 adpingids = el('td', 'pkg', record.adpingids.join(' / ') || '-');
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 orderStatuses = el('td');
const statusPills = el('div', 'platforms');
record.order_statuses.forEach(name => statusPills.append(el('span', 'platform-pill', `${name} ${record.order_status_counts[name]}`)));
orderStatuses.append(statusPills);
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, adpingids, product, pkg, country, orderStatuses, 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),
el('div', 'panel-desc', `Adpingid:${record.adpingids.join(' / ') || '-'}`)
);
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 = ['搬单状态', 'Adpingid', '产品', 'Pkg', '投放国家', '订单状态', '已出现平台', '是否有TaurusxDSP', '源订单行数'];
const csvRows = [headers, ...state.filtered.map(record => [
statusOf(record).label,
record.adpingids.join(' / '),
record.product,
record.pkg,
record.country,
record.order_statuses.join(' / '),
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);
}
const mtgState = { filtered: [], page: 1 };
function money(value) {
return `$${Number(value || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
function mtgMatchStatus(record) {
if (!record.order_match) return { key: 'unmatched', label: '未关联订单' };
if (record.missing_target) return { key: 'missing', label: '待搬单' };
if (record.target_present) return { key: 'covered', label: '已覆盖' };
return { key: 'matched', label: '已关联' };
}
function renderMtg1013() {
const data = state.data.mtg1013;
if (!data || !Array.isArray(data.records)) return;
const panel = el('section', 'panel workspace');
panel.id = 'mtg1013Panel';
const header = el('div', 'workspace-head');
const head = el('div', 'panel-head');
const title = el('div');
title.append(
el('h2', '', 'MTG 10/13 投放表现'),
el('div', 'panel-desc', `数据源:${data.meta.source} · ${nf.format(data.meta.rows)} 条 Adping 明细 · 按 Pkg + 国家关联订单`)
);
head.append(title);
const summary = el('section', 'kpis');
summary.append(
kpi('Valid Clicks', nf.format(data.meta.valid_clicks), `${nf.format(data.meta.rows)} 条明细`),
kpi('Postback Installs', nf.format(data.meta.installs), `安装率 ${data.meta.install_rate === null ? '-' : percent(data.meta.install_rate)}`),
kpi('Cost', money(data.meta.cost), `${nf.format(data.meta.matched_order_rows)} 条已关联订单`)
);
summary.style.marginBottom = '16px';
const filters = el('div', 'filters');
const search = el('input', 'control');
search.id = 'mtgSearch'; search.type = 'search'; search.placeholder = '搜索 Adping ID 或 Pkg'; search.autocomplete = 'off';
const country = selectControl('mtgCountry', '全部国家');
[...new Set(data.records.map(row => row.country))].sort().forEach(value => country.append(new Option(value, value)));
const match = selectControl('mtgMatch', '');
[['all','全部关联状态'], ['matched','已关联订单'], ['missing','关联且待搬单'], ['covered','关联且已覆盖'], ['unmatched','未关联订单']]
.forEach(([value,label]) => match.append(new Option(label, value)));
const sort = selectControl('mtgSort', '');
[
['cost-desc','Cost:高到低'], ['cost-asc','Cost:低到高'],
['valid_clicks-desc','Clicks:高到低'], ['valid_clicks-asc','Clicks:低到高'],
['installs-desc','Installs:高到低'], ['installs-asc','Installs:低到高'],
['adpingid-asc','Adping ID:升序'], ['pkg-asc','Pkg:A-Z']
].forEach(([value,label]) => sort.append(new Option(`排序 · ${label}`, value)));
const exportButton = el('button', 'button', '导出 MTG 数据');
exportButton.id = 'mtgExport'; exportButton.type = 'button';
filters.append(search, country, match, sort, exportButton);
header.append(head, summary, filters);
panel.append(header, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination'));
const footer = document.querySelector('footer');
document.getElementById('app').insertBefore(panel, footer);
search.addEventListener('input', () => { mtgState.page = 1; applyMtgFilters(); });
country.addEventListener('change', () => { mtgState.page = 1; applyMtgFilters(); });
match.addEventListener('change', () => { mtgState.page = 1; applyMtgFilters(); });
sort.addEventListener('change', () => { mtgState.page = 1; applyMtgFilters(); });
['search', 'countryFilter', 'platformFilter', 'statusFilter', 'orderStatusFilter'].forEach(id => {
const control = document.getElementById(id);
control.addEventListener(id === 'search' ? 'input' : 'change', () => {
mtgState.page = 1;
applyMtgFilters();
});
});
exportButton.addEventListener('click', exportMtgCsv);
applyMtgFilters();
}
function applyMtgFilters() {
const query = document.getElementById('mtgSearch').value.trim().toLocaleLowerCase();
const country = document.getElementById('mtgCountry').value;
const match = document.getElementById('mtgMatch').value;
const sortControl = document.getElementById('mtgSort');
const sort = sortControl.value;
const mainSearch = document.getElementById('search').value.trim().toLocaleLowerCase();
const mainCountry = document.getElementById('countryFilter').value;
const mainPlatform = document.getElementById('platformFilter').value;
const mainStatusControl = document.getElementById('statusFilter');
const mainStatus = mainStatusControl.value;
const mainOrderStatusControl = document.getElementById('orderStatusFilter');
const mainOrderStatus = mainOrderStatusControl.value;
const coverageByKey = new Map(
state.data.records.map(row => [`${row.pkg.toLocaleLowerCase()}|${row.country}`, row])
);
mtgState.filtered = state.data.mtg1013.records.filter(row => {
const status = mtgMatchStatus(row).key;
const coverage = coverageByKey.get(`${row.pkg.toLocaleLowerCase()}|${row.country}`);
const coverageStatus = coverage ? statusOf(coverage).key : 'unmatched';
const matched = match === 'all'
|| status === match
|| (match === 'matched' && row.order_match);
const mainSearchable = `${row.adpingid} ${row.pkg} ${coverage ? coverage.product : ''} ${coverage ? coverage.adpingids.join(' ') : ''}`.toLocaleLowerCase();
return (!query || `${row.adpingid} ${row.pkg}`.toLocaleLowerCase().includes(query))
&& (country === 'all' || row.country === country)
&& matched
&& (!mainSearch || mainSearchable.includes(mainSearch))
&& (mainCountry === 'all' || row.country === mainCountry)
&& (mainPlatform === 'all' || Boolean(coverage && coverage.platforms.includes(mainPlatform)))
&& (mainStatus === 'all' || coverageStatus === mainStatus)
&& (mainOrderStatus === 'all' || Boolean(coverage && coverage.order_statuses.includes(mainOrderStatus)));
});
const [field, direction] = sort.split('-');
const multiplier = direction === 'asc' ? 1 : -1;
mtgState.filtered.sort((left, right) => {
const a = left[field];
const b = right[field];
if (a === null && b !== null) return 1;
if (a !== null && b === null) return -1;
if (typeof a === 'string' || typeof b === 'string') {
const result = String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' });
return result * multiplier || left.adpingid.localeCompare(right.adpingid, undefined, { numeric: true });
}
return (Number(a) - Number(b)) * multiplier
|| left.adpingid.localeCompare(right.adpingid, undefined, { numeric: true });
});
mtgState.sortLabel = sortControl.options[sortControl.selectedIndex].text.replace('排序 · ', '');
const linked = [mainStatusControl.options[mainStatusControl.selectedIndex].text];
if (mainOrderStatus !== 'all') linked.push(`订单状态 ${mainOrderStatus}`);
if (mainCountry !== 'all') linked.push(`国家 ${mainCountry}`);
if (mainPlatform !== 'all') linked.push(`平台 ${mainPlatform}`);
if (mainSearch) linked.push(`搜索 ${mainSearch}`);
mtgState.linkedLabel = linked.join(' · ');
renderMtgResults();
}
function renderMtgResults() {
const panel = document.getElementById('mtg1013Panel');
const totalPages = Math.max(1, Math.ceil(mtgState.filtered.length / PAGE_SIZE));
mtgState.page = Math.min(mtgState.page, totalPages);
const start = (mtgState.page - 1) * PAGE_SIZE;
const rows = mtgState.filtered.slice(start, start + PAGE_SIZE);
const clicks = mtgState.filtered.reduce((sum, row) => sum + row.valid_clicks, 0);
const installs = mtgState.filtered.reduce((sum, row) => sum + row.installs, 0);
const cost = mtgState.filtered.reduce((sum, row) => sum + row.cost, 0);
const matchedRows = mtgState.filtered.filter(row => row.order_match).length;
const cards = panel.querySelectorAll('.kpi');
const setCard = (index, value, sub) => {
cards[index].querySelector('.value').textContent = value;
cards[index].querySelector('.sub').textContent = sub;
};
setCard(0, nf.format(clicks), `${nf.format(mtgState.filtered.length)} 条筛选明细`);
setCard(1, nf.format(installs), `安装率 ${clicks ? percent(installs / clicks) : '-'}`);
setCard(2, money(cost), `${nf.format(matchedRows)} 条已关联订单`);
const meta = panel.querySelector('.result-meta');
meta.textContent = '';
meta.append(
el('span', '', `筛选结果 ${nf.format(mtgState.filtered.length)} 条 · 联动:${mtgState.linkedLabel}`),
el('span', '', `第 ${mtgState.page} / ${totalPages} 页`)
);
const wrap = panel.querySelector('.table-wrap');
wrap.textContent = '';
const table = el('table');
const thead = el('thead'); const header = el('tr');
['Adping ID', 'Pkg', '国家', 'Valid Clicks', 'Postback Installs', '安装率', 'Cost', '订单关联', 'TaurusxDSP 状态']
.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', '没有符合当前条件的 MTG 记录'); cell.colSpan = 9;
const row = el('tr'); row.append(cell); tbody.append(row);
}
rows.forEach(record => {
const row = el('tr');
const country = el('td'); country.append(el('span', 'country-pill', record.country));
const match = mtgMatchStatus(record);
const orderStatus = el('td');
orderStatus.append(el('span', `badge ${record.order_match ? 'covered' : 'target-only'}`, record.order_match ? (record.adpingid_match ? 'ID 已匹配' : '仅 Pkg_国家匹配') : '未关联'));
const targetStatus = el('td');
targetStatus.append(el('span', `badge ${match.key === 'unmatched' ? 'target-only' : match.key}`, match.label));
row.append(
el('td', 'pkg', record.adpingid),
el('td', 'pkg', record.pkg),
country,
el('td', '', nf.format(record.valid_clicks)),
el('td', '', nf.format(record.installs)),
el('td', '', record.install_rate === null ? '-' : percent(record.install_rate)),
el('td', '', money(record.cost)),
orderStatus,
targetStatus
);
tbody.append(row);
});
table.append(tbody); wrap.append(table);
const pager = panel.querySelector('.pagination');
pager.textContent = '';
const prev = el('button', 'page-button', '‹'); prev.type = 'button'; prev.disabled = mtgState.page <= 1;
const next = el('button', 'page-button', '›'); next.type = 'button'; next.disabled = mtgState.page >= totalPages;
prev.addEventListener('click', () => { mtgState.page -= 1; renderMtgResults(); panel.scrollIntoView({ behavior: 'smooth' }); });
next.addEventListener('click', () => { mtgState.page += 1; renderMtgResults(); panel.scrollIntoView({ behavior: 'smooth' }); });
pager.append(prev, el('span', 'panel-desc', `${mtgState.page} / ${totalPages}`), next);
}
function exportMtgCsv() {
const headers = ['Adping ID', 'Pkg', '国家', 'Valid Clicks', 'Postback Installs', '安装率', 'Cost', '订单关联', 'TaurusxDSP状态'];
const rows = [headers, ...mtgState.filtered.map(record => [
record.adpingid,
record.pkg,
record.country,
record.valid_clicks,
record.installs,
record.install_rate === null ? '' : record.install_rate,
record.cost,
record.order_match ? (record.adpingid_match ? 'ID已匹配' : '仅Pkg_国家匹配') : '未关联',
mtgMatchStatus(record).label
])];
const csv = '\ufeff' + rows.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 = `MTG1013_${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();
renderMtg1013();
})
.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>