style: simplify TaurusX performance layout

This commit is contained in:
王源
2026-10-15 17:58:55 +08:00
parent ecad390e63
commit 676245c987
+227 -136
View File
@@ -7,17 +7,18 @@
<title>TaurusX 投放数据</title> <title>TaurusX 投放数据</title>
<style> <style>
:root { :root {
--ink: #15201d; --ink: #17211e;
--muted: #6d7773; --muted: #707b77;
--line: #dfe6e2; --line: #e3e9e6;
--surface: #ffffff; --line-strong: #d4dfda;
--canvas: #f3f6f3; --surface: #fff;
--green: #0e7355; --canvas: #f5f7f6;
--green-soft: #ddf2e9; --green: #0c7454;
--amber: #a65c00; --green-dark: #0c4f3d;
--amber-soft: #fff0d5; --green-soft: #e7f5ef;
--red: #b5312d; --blue: #3569a8;
--shadow: 0 10px 30px rgba(30, 52, 43, .08); --amber: #a9660a;
--shadow: 0 8px 24px rgba(28, 53, 43, .055);
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
html { background: var(--canvas); } html { background: var(--canvas); }
@@ -26,98 +27,151 @@
color: var(--ink); color: var(--ink);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 14px; font-size: 14px;
-webkit-font-smoothing: antialiased;
} }
button, input, select { font: inherit; } button, input, select { font: inherit; }
button { cursor: pointer; } button { cursor: pointer; }
.hero { .hero {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
padding: 30px max(24px, calc((100vw - 1440px) / 2)); min-height: 188px;
padding: 25px max(24px, calc((100vw - 1320px) / 2));
color: #f7fffb; color: #f7fffb;
background: linear-gradient(125deg, #12392f 0%, #0d6049 60%, #187a5e 100%); background: linear-gradient(118deg, #153d32 0%, #0b6048 62%, #16795d 100%);
} }
.hero::after { .hero::after {
content: "TX"; content: "TX";
position: absolute; position: absolute;
right: 4%; top: -54px; right: max(24px, calc((100vw - 1320px) / 2));
font: 800 190px/1 Inter, sans-serif; bottom: -45px;
letter-spacing: -18px; font: 800 168px/1 Inter, sans-serif;
color: rgba(255,255,255,.055); letter-spacing: -16px;
transform: rotate(-7deg); color: rgba(255,255,255,.05);
transform: rotate(-5deg);
pointer-events: none;
} }
.hero-nav { position: relative; z-index: 1; display: flex; justify-content: flex-end; margin-bottom: 16px; } .hero-top { position: relative; z-index: 1; display: flex; align-items: center; justify-content: space-between; gap: 20px; }
.brand { color: #a8dbc8; font-size: 11px; font-weight: 750; letter-spacing: .15em; text-transform: uppercase; }
.nav-link { .nav-link {
display: inline-flex; align-items: center; min-height: 36px; padding: 0 13px; display: inline-flex; align-items: center; min-height: 34px; padding: 0 12px;
border: 1px solid rgba(255,255,255,.3); border-radius: 9px; border: 1px solid rgba(255,255,255,.25); border-radius: 9px;
color: #effbf6; background: rgba(255,255,255,.08); font-size: 12px; font-weight: 700; text-decoration: none; color: #effbf6; background: rgba(255,255,255,.07); font-size: 12px; font-weight: 700; text-decoration: none;
} }
.nav-link:hover { background: rgba(255,255,255,.16); } .nav-link:hover { background: rgba(255,255,255,.14); }
.eyebrow { margin-bottom: 9px; color: #aee2cf; font-size: 12px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; } .hero-body { position: relative; z-index: 1; margin-top: 22px; }
h1 { margin: 0; font-size: clamp(30px, 4vw, 47px); letter-spacing: -.04em; line-height: 1.08; } h1 { margin: 0; font-size: clamp(29px, 3.5vw, 43px); letter-spacing: -.04em; line-height: 1.08; }
.hero p { max-width: 760px; margin: 13px 0 0; color: #d2e9e0; line-height: 1.7; } .hero p { max-width: 680px; margin: 10px 0 0; color: #d2e8e0; line-height: 1.6; }
.hero-meta { display: flex; flex-wrap: wrap; gap: 8px 20px; margin-top: 18px; color: #bcdacf; font-size: 12px; } .hero-meta { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 17px; }
.page { max-width: 1440px; margin: 0 auto; padding: 24px; } .meta-chip { padding: 5px 9px; border-radius: 7px; color: #cbe5dc; background: rgba(255,255,255,.08); font-size: 11px; }
.kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; } .page { max-width: 1320px; margin: 0 auto; padding: 20px 24px 28px; }
.card, .panel { border: 1px solid var(--line); border-radius: 16px; background: var(--surface); box-shadow: var(--shadow); } .primary-kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.kpi { min-height: 126px; padding: 18px; } .stat-card {
.kpi .label { color: var(--muted); font-size: 12px; font-weight: 650; } position: relative;
.kpi .value { margin-top: 10px; font-size: 31px; font-weight: 760; letter-spacing: -.04em; } min-height: 116px;
.kpi .sub { margin-top: 8px; color: var(--muted); font-size: 12px; line-height: 1.5; } padding: 17px 18px;
.workspace { margin-top: 16px; padding: 0; overflow: hidden; } border: 1px solid var(--line);
.workspace-head { padding: 18px 18px 0; } border-radius: 14px;
.panel-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 15px; } background: var(--surface);
.panel h2 { margin: 0; font-size: 17px; letter-spacing: -.02em; } box-shadow: var(--shadow);
.panel-desc { margin-top: 5px; color: var(--muted); font-size: 12px; } }
.filters { display: grid; grid-template-columns: minmax(220px, 1.2fr) repeat(4, minmax(125px, .55fr)) auto; gap: 10px; margin-top: 16px; } .stat-card::before { content: ""; position: absolute; top: 18px; right: 18px; width: 7px; height: 7px; border-radius: 50%; background: #83baa5; }
.control { width: 100%; height: 40px; border: 1px solid var(--line); border-radius: 9px; outline: none; background: white; color: var(--ink); } .stat-card.blue::before { background: #7fa6d5; }
input.control { padding: 0 12px; } .stat-card.amber::before { background: #d5a45f; }
select.control { padding: 0 30px 0 10px; } .stat-label { color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: .035em; text-transform: uppercase; }
.control:focus { border-color: #63a68e; box-shadow: 0 0 0 3px rgba(14,115,85,.1); } .stat-value { margin-top: 9px; font-size: clamp(25px, 2.5vw, 32px); font-weight: 760; letter-spacing: -.035em; font-variant-numeric: tabular-nums; }
.button { height: 40px; padding: 0 14px; border: 1px solid #b8c9c2; border-radius: 9px; background: #f6faf8; color: #185a45; font-weight: 700; } .stat-sub { margin-top: 7px; color: var(--muted); font-size: 11px; }
.button:hover { background: #eaf5f0; } .efficiency {
.result-meta { display: flex; justify-content: space-between; gap: 12px; padding: 14px 18px 10px; color: var(--muted); font-size: 12px; } display: grid;
grid-template-columns: minmax(210px, .8fr) repeat(3, minmax(0, 1fr));
align-items: stretch;
margin-top: 12px;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--surface);
box-shadow: var(--shadow);
overflow: hidden;
}
.efficiency-intro { padding: 16px 18px; background: linear-gradient(135deg, #eef7f3, #f8fbfa); }
.efficiency-title { font-size: 13px; font-weight: 760; }
.efficiency-note { margin-top: 5px; color: var(--muted); font-size: 10px; line-height: 1.45; }
.eff-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 18px; border-left: 1px solid var(--line); }
.eff-label { color: var(--muted); font-size: 11px; font-weight: 700; }
.eff-formula { margin-top: 4px; color: #9aa39f; font-size: 9px; white-space: nowrap; }
.eff-value { color: var(--green-dark); font-size: 22px; font-weight: 760; letter-spacing: -.025em; font-variant-numeric: tabular-nums; }
.panel { margin-top: 14px; border: 1px solid var(--line); border-radius: 14px; background: var(--surface); box-shadow: var(--shadow); overflow: hidden; }
.panel-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 17px 18px 0; }
.panel h2 { margin: 0; font-size: 16px; letter-spacing: -.018em; }
.panel-desc { margin-top: 4px; color: var(--muted); font-size: 11px; }
.toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 14px 18px 16px; }
.search { flex: 1 1 280px; min-width: 220px; }
.control { height: 38px; border: 1px solid var(--line-strong); border-radius: 9px; outline: none; background: #fff; color: var(--ink); font-size: 12px; }
input.control { width: 100%; padding: 0 12px; }
select.control { min-width: 134px; padding: 0 30px 0 10px; }
.control:focus { border-color: #70a792; box-shadow: 0 0 0 3px rgba(12,116,84,.09); }
.button { height: 38px; padding: 0 14px; border: 1px solid var(--green); border-radius: 9px; background: var(--green); color: #fff; font-size: 12px; font-weight: 720; }
.button:hover { background: #0a6348; }
.result-meta { display: flex; justify-content: space-between; gap: 12px; padding: 10px 18px; border-top: 1px solid var(--line); color: var(--muted); background: #fafcfb; font-size: 11px; }
.table-wrap { width: 100%; overflow: auto; border-top: 1px solid var(--line); } .table-wrap { width: 100%; overflow: auto; border-top: 1px solid var(--line); }
table { width: 100%; border-collapse: collapse; } table { width: 100%; min-width: 1080px; 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; } th { position: sticky; top: 0; z-index: 1; padding: 10px 12px; background: #f5f8f6; color: #65716c; font-size: 10px; font-weight: 760; text-align: left; white-space: nowrap; letter-spacing: .02em; }
td { padding: 12px; border-top: 1px solid #edf0ee; vertical-align: top; } th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; }
tr:hover td { background: #fbfcfb; } td { padding: 12px; border-top: 1px solid #edf1ef; vertical-align: middle; }
.pkg { max-width: 320px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; overflow-wrap: anywhere; } tbody tr:nth-child(even) td { background: #fcfdfc; }
.product { max-width: 260px; font-weight: 650; } tbody tr:hover td { background: #f5faf7; }
.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; } .date-cell { color: #57645f; font-size: 11px; white-space: nowrap; }
.badge { display: inline-flex; align-items: center; padding: 5px 9px; border-radius: 999px; white-space: nowrap; font-size: 11px; font-weight: 750; } .campaign-cell { min-width: 310px; max-width: 390px; }
.badge.covered { color: var(--green); background: var(--green-soft); } .campaign-name { overflow: hidden; color: #25322d; font-size: 12px; font-weight: 670; text-overflow: ellipsis; white-space: nowrap; }
.badge.unmatched { color: var(--amber); background: var(--amber-soft); } .campaign-meta { display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 5px; color: #89938f; font-size: 9px; }
.status-list { display: flex; flex-wrap: wrap; gap: 5px; min-width: 130px; } .campaign-id, .pkg { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.status-pill { padding: 4px 8px; border-radius: 999px; background: #eff3f1; color: #4d5e57; font-size: 11px; } .pkg { overflow-wrap: anywhere; }
.pagination { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 14px 18px 18px; } .country-pill { display: inline-block; min-width: 34px; padding: 4px 7px; border-radius: 7px; background: #edf2f7; color: #3c5265; font-size: 10px; font-weight: 760; text-align: center; }
.page-button { min-width: 34px; height: 34px; border: 1px solid var(--line); border-radius: 8px; background: white; color: var(--ink); } .unit-col { color: var(--green-dark); font-weight: 650; }
.page-button:disabled { cursor: default; opacity: .42; } .zero { color: #b0b7b4; }
.empty { padding: 46px 20px !important; color: var(--muted); text-align: center; } .pagination { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 12px 18px 15px; border-top: 1px solid var(--line); }
.page-button { min-width: 32px; height: 32px; border: 1px solid var(--line-strong); border-radius: 8px; background: white; color: var(--ink); }
.page-button:hover:not(:disabled) { border-color: #8eaaa0; background: #f5faf7; }
.page-button:disabled { cursor: default; opacity: .4; }
.page-label { color: var(--muted); font-size: 11px; }
.empty { padding: 48px 20px !important; color: var(--muted); text-align: center !important; }
.loading { padding: 90px 24px; 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; } .error { max-width: 700px; margin: 70px auto; padding: 22px; border: 1px solid #e4aaa6; border-radius: 13px; background: #fff4f2; color: #8c2c28; }
footer { padding: 22px 0 8px; color: #7b8581; font-size: 11px; text-align: center; } footer { padding: 20px 0 5px; color: #929b97; font-size: 10px; text-align: center; }
@media (max-width: 1050px) { @media (max-width: 980px) {
.kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } .primary-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.filters { grid-template-columns: repeat(2, minmax(0, 1fr)); } .efficiency { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.efficiency-intro { grid-column: 1 / -1; }
.eff-item:first-of-type { border-left: 0; }
.toolbar { align-items: stretch; }
select.control { flex: 1 1 140px; }
} }
@media (max-width: 650px) { @media (max-width: 620px) {
.hero { padding: 24px 18px; } .hero { min-height: 170px; padding: 20px 16px; }
.page { padding: 14px; } .hero-body { margin-top: 18px; }
.kpis { grid-template-columns: 1fr; gap: 10px; } .page { padding: 14px 12px 22px; }
.kpi { min-height: 108px; padding: 14px; } .primary-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.kpi .value { font-size: 26px; } .stat-card { min-height: 100px; padding: 14px; }
.filters { grid-template-columns: 1fr; } .stat-value { font-size: 23px; }
.result-meta { flex-direction: column; } .efficiency { grid-template-columns: 1fr; }
.efficiency-intro { grid-column: auto; }
.eff-item { border-top: 1px solid var(--line); border-left: 0; }
.toolbar { padding: 12px; }
.search, select.control, .button { flex: 1 1 100%; width: 100%; }
.panel-head { padding: 14px 12px 0; }
.result-meta { flex-direction: column; padding-inline: 12px; }
} }
</style> </style>
</head> </head>
<body> <body>
<header class="hero"> <header class="hero">
<nav class="hero-nav"><a class="nav-link" href="index.html">← 返回首页</a></nav> <div class="hero-top">
<div class="eyebrow">AgentStage · Performance</div> <div class="brand">AgentStage · Performance</div>
<a class="nav-link" href="index.html">← 返回首页</a>
</div>
<div class="hero-body">
<h1>TaurusX 投放数据</h1> <h1>TaurusX 投放数据</h1>
<p>查看 TaurusX 近 7 日 Campaign 投放表现,包含 Impressions、Clicks、Conversions 和 Cost。</p> <p>近 7 日 Campaign 投放表现与单位成本效率。</p>
<div class="hero-meta"><span id="rangeLabel">日期加载中</span><span id="updatedLabel">更新时间加载中</span></div> <div class="hero-meta"><span class="meta-chip" id="rangeLabel">日期加载中</span><span class="meta-chip" id="updatedLabel">更新时间加载中</span></div>
</div>
</header> </header>
<main class="page" id="app"> <main class="page" id="app">
@@ -157,41 +211,67 @@
if (firstLabel) select.append(new Option(firstLabel, firstValue)); if (firstLabel) select.append(new Option(firstLabel, firstValue));
return select; return select;
} }
function kpi(label, value, sub) { function statCard(key, label, value, sub, tone = '') {
const card = el('article', 'card kpi'); const card = el('article', `stat-card ${tone}`.trim());
card.append(el('div', 'label', label), el('div', 'value', value), el('div', 'sub', sub)); card.dataset.metric = key;
card.append(el('div', 'stat-label', label), el('div', 'stat-value', value), el('div', 'stat-sub', sub));
return card; return card;
} }
function efficiencyItem(key, label, value, formula) {
const item = el('div', 'eff-item');
item.dataset.metric = key;
const copy = el('div');
copy.append(el('div', 'eff-label', label), el('div', 'eff-formula', formula));
item.append(copy, el('div', 'eff-value', value));
return item;
}
function updateMetric(key, value, sub) {
const node = document.querySelector(`[data-metric="${key}"]`);
if (!node) return;
const valueNode = node.querySelector('.stat-value, .eff-value');
if (valueNode) valueNode.textContent = value;
if (sub !== undefined) {
const subNode = node.querySelector('.stat-sub');
if (subNode) subNode.textContent = sub;
}
}
function renderShell() { function renderShell() {
const { meta, records } = state.data; const { meta, records } = state.data;
document.getElementById('rangeLabel').textContent = `日期:${meta.start_date} 至 ${meta.end_date} · UTC${meta.timezone}`; document.getElementById('rangeLabel').textContent = `${meta.start_date} — ${meta.end_date} · UTC${meta.timezone}`;
document.getElementById('updatedLabel').textContent = `拉取于:${formatTime(meta.fetched_at)}`; document.getElementById('updatedLabel').textContent = `更新 ${formatTime(meta.fetched_at)}`;
const app = document.getElementById('app'); const app = document.getElementById('app');
app.textContent = ''; app.textContent = '';
const summary = el('section', 'kpis'); const primary = el('section', 'primary-kpis');
summary.append( primary.append(
kpi('Impressions', nf.format(meta.impressions), `${nf.format(meta.campaigns)} 个 Campaign`), statCard('impressions', 'Impressions', nf.format(meta.impressions), `${nf.format(meta.campaigns)} 个 Campaign`),
kpi('Clicks', nf.format(meta.clicks), `${nf.format(meta.pkg_country_count)} 个 Pkg_国家`), statCard('clicks', 'Clicks', nf.format(meta.clicks), `${nf.format(meta.pkg_country_count)} 个 Pkg_国家`, 'blue'),
kpi('Conversions', nf.format(meta.conversions), `${nf.format(meta.countries)} 个国家`), statCard('conversions', 'Conversions', nf.format(meta.conversions), `${nf.format(meta.countries)} 个国家`, 'amber'),
kpi('Cost', money(meta.cost), `${nf.format(meta.rows)} 条投放明细`), statCard('cost', 'Cost', money(meta.cost), `${nf.format(meta.rows)} 条明细`)
kpi('CPM', unitCost(meta.cpm), 'Cost / Impressions × 1000'),
kpi('CPC', unitCost(meta.cpc), 'Cost / Clicks'),
kpi('CPI', unitCost(meta.cpi), 'Cost / Conversions')
); );
const panel = el('section', 'panel workspace'); const efficiency = el('section', 'efficiency');
const header = el('div', 'workspace-head'); const intro = el('div', 'efficiency-intro');
intro.append(el('div', 'efficiency-title', '效率指标'), el('div', 'efficiency-note', 'CPI 以 Conversions 作为安装量口径;分母为 0 时显示 “-”'));
efficiency.append(
intro,
efficiencyItem('cpm', 'CPM', unitCost(meta.cpm), 'Cost / Impressions × 1000'),
efficiencyItem('cpc', 'CPC', unitCost(meta.cpc), 'Cost / Clicks'),
efficiencyItem('cpi', 'CPI', unitCost(meta.cpi), 'Cost / Conversions')
);
const panel = el('section', 'panel');
const heading = el('div', 'panel-head'); const heading = el('div', 'panel-head');
const title = el('div'); const title = el('div');
title.append(el('h2', '', '近 7 日投放明细'), el('div', 'panel-desc', `${nf.format(meta.rows)} 条数据 · 支持筛选、排序与 CSV 导出`)); title.append(el('h2', '', '投放明细'), el('div', 'panel-desc', '按日期与 Campaign 查看表现,筛选后上方指标同步更新'));
heading.append(title); heading.append(title);
const filters = el('div', 'filters'); const toolbar = el('div', 'toolbar');
filters.style.gridTemplateColumns = 'minmax(220px, 1.2fr) repeat(3, minmax(125px, .55fr)) auto'; const searchWrap = el('div', 'search');
const search = el('input', 'control'); const search = el('input', 'control');
search.id = 'search'; search.type = 'search'; search.placeholder = '搜索 Campaign ID、Campaign 或 Pkg'; search.autocomplete = 'off'; search.id = 'search'; search.type = 'search'; search.placeholder = '搜索 Campaign ID、Campaign 或 Pkg'; search.autocomplete = 'off';
searchWrap.append(search);
const reportDate = selectControl('dateFilter', '全部日期'); const reportDate = selectControl('dateFilter', '全部日期');
[...new Set(records.map(row => row.date))].sort().reverse().forEach(value => reportDate.append(new Option(value, value))); [...new Set(records.map(row => row.date))].sort().reverse().forEach(value => reportDate.append(new Option(value, value)));
const country = selectControl('countryFilter', '全部国家'); const country = selectControl('countryFilter', '全部国家');
@@ -203,12 +283,11 @@
['conversions-desc','Conversions:高到低'], ['cpm-desc','CPM:高到低'], ['conversions-desc','Conversions:高到低'], ['cpm-desc','CPM:高到低'],
['cpc-desc','CPC:高到低'], ['cpi-desc','CPI:高到低'], ['pkg-asc','Pkg:A-Z'] ['cpc-desc','CPC:高到低'], ['cpi-desc','CPI:高到低'], ['pkg-asc','Pkg:A-Z']
].forEach(([value, label]) => sort.append(new Option(`排序 · ${label}`, value))); ].forEach(([value, label]) => sort.append(new Option(`排序 · ${label}`, value)));
const exportButton = el('button', 'button', '导出 TaurusX 数据'); const exportButton = el('button', 'button', '导出 CSV');
exportButton.type = 'button'; exportButton.id = 'exportButton'; exportButton.type = 'button'; exportButton.id = 'exportButton';
filters.append(search, reportDate, country, sort, exportButton); toolbar.append(searchWrap, reportDate, country, sort, exportButton);
header.append(heading, filters); panel.append(heading, toolbar, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination'));
panel.append(header, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination')); app.append(primary, efficiency, panel, el('footer', '', 'TaurusX Performance · Impressions · Clicks · Conversions · Cost'));
app.append(summary, panel, el('footer', '', 'TaurusX 投放数据 · Impressions、Clicks、Conversions、Cost、CPM、CPC、CPI'));
search.addEventListener('input', resetAndFilter); search.addEventListener('input', resetAndFilter);
[reportDate, country, sort].forEach(control => control.addEventListener('change', resetAndFilter)); [reportDate, country, sort].forEach(control => control.addEventListener('change', resetAndFilter));
@@ -236,6 +315,8 @@
const multiplier = direction === 'asc' ? 1 : -1; const multiplier = direction === 'asc' ? 1 : -1;
state.filtered.sort((left, right) => { state.filtered.sort((left, right) => {
const a = left[field]; const b = right[field]; 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') { if (typeof a === 'string' || typeof b === 'string') {
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' }) * multiplier return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' }) * multiplier
|| String(left.campaign_id).localeCompare(String(right.campaign_id), undefined, { numeric: true }); || String(left.campaign_id).localeCompare(String(right.campaign_id), undefined, { numeric: true });
@@ -255,22 +336,17 @@
const clicks = state.filtered.reduce((sum, row) => sum + Number(row.clicks || 0), 0); const clicks = state.filtered.reduce((sum, row) => sum + Number(row.clicks || 0), 0);
const conversions = state.filtered.reduce((sum, row) => sum + Number(row.conversions || 0), 0); const conversions = state.filtered.reduce((sum, row) => sum + Number(row.conversions || 0), 0);
const cost = state.filtered.reduce((sum, row) => sum + Number(row.cost || 0), 0); const cost = state.filtered.reduce((sum, row) => sum + Number(row.cost || 0), 0);
const cards = document.querySelectorAll('.kpi'); updateMetric('impressions', nf.format(impressions), `${nf.format(state.filtered.length)} 条筛选明细`);
const setCard = (index, value, sub) => { updateMetric('clicks', nf.format(clicks), `CTR ${impressions ? percent(clicks / impressions) : '-'}`);
cards[index].querySelector('.value').textContent = value; updateMetric('conversions', nf.format(conversions), `CVR ${clicks ? percent(conversions / clicks) : '-'}`);
cards[index].querySelector('.sub').textContent = sub; updateMetric('cost', money(cost), `${nf.format(new Set(state.filtered.map(row => row.country)).size)} 个国家`);
}; updateMetric('cpm', unitCost(impressions ? cost / impressions * 1000 : null));
setCard(0, nf.format(impressions), `${nf.format(state.filtered.length)} 条筛选明细`); updateMetric('cpc', unitCost(clicks ? cost / clicks : null));
setCard(1, nf.format(clicks), `CTR ${impressions ? percent(clicks / impressions) : '-'}`); updateMetric('cpi', unitCost(conversions ? cost / conversions : null));
setCard(2, nf.format(conversions), `CVR ${clicks ? percent(conversions / clicks) : '-'}`);
setCard(3, money(cost), `${nf.format(new Set(state.filtered.map(row => row.country)).size)} 个国家`);
setCard(4, unitCost(impressions ? cost / impressions * 1000 : null), 'Cost / Impressions × 1000');
setCard(5, unitCost(clicks ? cost / clicks : null), 'Cost / Clicks');
setCard(6, unitCost(conversions ? cost / conversions : null), 'Cost / Conversions');
const meta = document.querySelector('.result-meta'); const meta = document.querySelector('.result-meta');
meta.textContent = ''; meta.textContent = '';
meta.append(el('span', '', `筛选结果 ${nf.format(state.filtered.length)} 条`), el('span', '', `第 ${state.page} / ${totalPages} 页`)); meta.append(el('span', '', `${nf.format(state.filtered.length)} 条结果`), el('span', '', `第 ${state.page} / ${totalPages} 页`));
renderTable(rows); renderTable(rows);
renderPagination(totalPages); renderPagination(totalPages);
} }
@@ -280,30 +356,45 @@
wrap.textContent = ''; wrap.textContent = '';
const table = el('table'); const table = el('table');
const thead = el('thead'); const header = el('tr'); const thead = el('thead'); const header = el('tr');
['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Impressions', 'Clicks', 'Conversions', 'Cost', 'CPM', 'CPC', 'CPI'] const columns = [
.forEach(label => header.append(el('th', '', label))); ['日期', ''], ['Campaign / Pkg', ''], ['国家', ''],
['Impressions', 'num'], ['Clicks', 'num'], ['Conversions', 'num'], ['Cost', 'num'],
['CPM', 'num'], ['CPC', 'num'], ['CPI', 'num']
];
columns.forEach(([label, className]) => header.append(el('th', className, label)));
thead.append(header); table.append(thead); thead.append(header); table.append(thead);
const tbody = el('tbody'); const tbody = el('tbody');
if (!rows.length) { if (!rows.length) {
const cell = el('td', 'empty', '没有符合当前条件的 TaurusX 记录'); cell.colSpan = 12; const cell = el('td', 'empty', '没有符合当前条件的记录'); cell.colSpan = 10;
const row = el('tr'); row.append(cell); tbody.append(row); const row = el('tr'); row.append(cell); tbody.append(row);
} }
rows.forEach(record => { rows.forEach(record => {
const row = el('tr'); const row = el('tr');
const campaign = el('td', 'campaign-cell');
campaign.append(
el('div', 'campaign-name', record.campaign_name || '-'),
(() => {
const meta = el('div', 'campaign-meta');
meta.append(el('span', 'campaign-id', `#${record.campaign_id || '-'}`), el('span', 'pkg', record.pkg || '-'));
return meta;
})()
);
const country = el('td'); country.append(el('span', 'country-pill', record.country || '-')); const country = el('td'); country.append(el('span', 'country-pill', record.country || '-'));
const numeric = (value, formatter = nf.format, extraClass = '') => {
const zero = !value;
return el('td', `num ${extraClass} ${zero ? 'zero' : ''}`.trim(), formatter(value));
};
row.append( row.append(
el('td', '', record.date || '-'), el('td', 'date-cell', record.date || '-'),
el('td', 'pkg', record.campaign_id || '-'), campaign,
el('td', 'product', record.campaign_name || '-'),
el('td', 'pkg', record.pkg || '-'),
country, country,
el('td', '', nf.format(record.impressions || 0)), numeric(record.impressions || 0),
el('td', '', nf.format(record.clicks || 0)), numeric(record.clicks || 0),
el('td', '', nf.format(record.conversions || 0)), numeric(record.conversions || 0),
el('td', '', money(record.cost)), numeric(record.cost || 0, money),
el('td', '', unitCost(record.cpm)), numeric(record.cpm, unitCost, 'unit-col'),
el('td', '', unitCost(record.cpc)), numeric(record.cpc, unitCost, 'unit-col'),
el('td', '', unitCost(record.cpi)) numeric(record.cpi, unitCost, 'unit-col')
); );
tbody.append(row); tbody.append(row);
}); });
@@ -313,11 +404,11 @@
function renderPagination(totalPages) { function renderPagination(totalPages) {
const box = document.querySelector('.pagination'); const box = document.querySelector('.pagination');
box.textContent = ''; box.textContent = '';
const prev = el('button', 'page-button', '‹'); prev.type = 'button'; prev.disabled = state.page <= 1; const prev = el('button', 'page-button', '‹'); prev.type = 'button'; prev.disabled = state.page <= 1; prev.title = '上一页';
const next = el('button', 'page-button', '›'); next.type = 'button'; next.disabled = state.page >= totalPages; const next = el('button', 'page-button', '›'); next.type = 'button'; next.disabled = state.page >= totalPages; next.title = '下一页';
prev.addEventListener('click', () => { state.page -= 1; renderResults(); document.querySelector('.workspace').scrollIntoView({ behavior: 'smooth' }); }); prev.addEventListener('click', () => { state.page -= 1; renderResults(); document.querySelector('.panel').scrollIntoView({ behavior: 'smooth' }); });
next.addEventListener('click', () => { state.page += 1; renderResults(); document.querySelector('.workspace').scrollIntoView({ behavior: 'smooth' }); }); next.addEventListener('click', () => { state.page += 1; renderResults(); document.querySelector('.panel').scrollIntoView({ behavior: 'smooth' }); });
box.append(prev, el('span', 'panel-desc', `${state.page} / ${totalPages}`), next); box.append(prev, el('span', 'page-label', `${state.page} / ${totalPages}`), next);
} }
function exportCsv() { function exportCsv() {