721 lines
38 KiB
HTML
721 lines
38 KiB
HTML
<!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');
|
||
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>
|