Files
taurusx_tracking/taurusx.html
T

357 lines
19 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;
--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: 30px 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);
}
.hero-nav { position: relative; z-index: 1; display: flex; justify-content: flex-end; margin-bottom: 16px; }
.nav-link {
display: inline-flex; align-items: center; min-height: 36px; padding: 0 13px;
border: 1px solid rgba(255,255,255,.3); border-radius: 9px;
color: #effbf6; background: rgba(255,255,255,.08); font-size: 12px; font-weight: 700; text-decoration: none;
}
.nav-link:hover { background: rgba(255,255,255,.16); }
.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: 18px; color: #bcdacf; font-size: 12px; }
.page { max-width: 1440px; margin: 0 auto; padding: 24px; }
.kpis { display: grid; grid-template-columns: repeat(4, 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; }
.workspace { margin-top: 16px; padding: 0; overflow: hidden; }
.workspace-head { padding: 18px 18px 0; }
.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; }
.filters { display: grid; grid-template-columns: minmax(220px, 1.2fr) repeat(4, minmax(125px, .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: 320px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; overflow-wrap: anywhere; }
.product { max-width: 260px; 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 { display: inline-flex; align-items: center; padding: 5px 9px; border-radius: 999px; white-space: nowrap; font-size: 11px; font-weight: 750; }
.badge.covered { color: var(--green); background: var(--green-soft); }
.badge.unmatched { color: var(--amber); background: var(--amber-soft); }
.status-list { display: flex; flex-wrap: wrap; gap: 5px; min-width: 130px; }
.status-pill { padding: 4px 8px; border-radius: 999px; background: #eff3f1; color: #4d5e57; font-size: 11px; }
.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; }
footer { padding: 22px 0 8px; color: #7b8581; font-size: 11px; text-align: center; }
@media (max-width: 1050px) {
.kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.filters { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 650px) {
.hero { padding: 24px 18px; }
.page { padding: 14px; }
.kpis { grid-template-columns: 1fr; gap: 10px; }
.kpi { min-height: 108px; padding: 14px; }
.kpi .value { font-size: 26px; }
.filters { grid-template-columns: 1fr; }
.result-meta { flex-direction: column; }
}
</style>
</head>
<body>
<header class="hero">
<nav class="hero-nav"><a class="nav-link" href="index.html">← 返回首页</a></nav>
<div class="eyebrow">AgentStage · Performance</div>
<h1>TaurusX 投放数据</h1>
<p>查看 TaurusX 近 7 日 Campaign 投放表现,包含 Impressions、Clicks、Conversions 和 Cost。</p>
<div class="hero-meta"><span id="rangeLabel">日期加载中</span><span id="updatedLabel">更新时间加载中</span></div>
</header>
<main class="page" id="app">
<div class="loading">正在读取 TaurusX 投放数据…</div>
</main>
<script>
'use strict';
const PAGE_SIZE = 40;
const state = { data: null, filtered: [], page: 1 };
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 percent(value) { return `${(value * 100).toFixed(1)}%`; }
function money(value) {
return `$${Number(value || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
}
function unitCost(value) {
return value === null || value === undefined
? '-'
: `$${Number(value).toLocaleString('en-US', { minimumFractionDigits: 4, maximumFractionDigits: 4 })}`;
}
function formatTime(value) {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false });
}
function selectControl(id, firstLabel, firstValue = 'all') {
const select = el('select', 'control');
select.id = id;
if (firstLabel) select.append(new Option(firstLabel, firstValue));
return select;
}
function kpi(label, value, sub) {
const card = el('article', 'card kpi');
card.append(el('div', 'label', label), el('div', 'value', value), el('div', 'sub', sub));
return card;
}
function renderShell() {
const { meta, records } = state.data;
document.getElementById('rangeLabel').textContent = `日期:${meta.start_date} 至 ${meta.end_date} · UTC${meta.timezone}`;
document.getElementById('updatedLabel').textContent = `拉取于:${formatTime(meta.fetched_at)}`;
const app = document.getElementById('app');
app.textContent = '';
const summary = el('section', 'kpis');
summary.append(
kpi('Impressions', nf.format(meta.impressions), `${nf.format(meta.campaigns)} 个 Campaign`),
kpi('Clicks', nf.format(meta.clicks), `${nf.format(meta.pkg_country_count)} 个 Pkg_国家`),
kpi('Conversions', nf.format(meta.conversions), `${nf.format(meta.countries)} 个国家`),
kpi('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 header = el('div', 'workspace-head');
const heading = el('div', 'panel-head');
const title = el('div');
title.append(el('h2', '', '近 7 日投放明细'), el('div', 'panel-desc', `${nf.format(meta.rows)} 条数据 · 支持筛选、排序与 CSV 导出`));
heading.append(title);
const filters = el('div', 'filters');
filters.style.gridTemplateColumns = 'minmax(220px, 1.2fr) repeat(3, minmax(125px, .55fr)) auto';
const search = el('input', 'control');
search.id = 'search'; search.type = 'search'; search.placeholder = '搜索 Campaign ID、Campaign 或 Pkg'; search.autocomplete = 'off';
const reportDate = selectControl('dateFilter', '全部日期');
[...new Set(records.map(row => row.date))].sort().reverse().forEach(value => reportDate.append(new Option(value, value)));
const country = selectControl('countryFilter', '全部国家');
[...new Set(records.map(row => row.country).filter(Boolean))].sort().forEach(value => country.append(new Option(value, value)));
const sort = selectControl('sortControl', '');
[
['date-desc','日期:新到旧'], ['cost-desc','Cost:高到低'],
['impressions-desc','Impressions:高到低'], ['clicks-desc','Clicks:高到低'],
['conversions-desc','Conversions:高到低'], ['cpm-desc','CPM:高到低'],
['cpc-desc','CPC:高到低'], ['cpi-desc','CPI:高到低'], ['pkg-asc','Pkg:A-Z']
].forEach(([value, label]) => sort.append(new Option(`排序 · ${label}`, value)));
const exportButton = el('button', 'button', '导出 TaurusX 数据');
exportButton.type = 'button'; exportButton.id = 'exportButton';
filters.append(search, reportDate, country, sort, exportButton);
header.append(heading, filters);
panel.append(header, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination'));
app.append(summary, panel, el('footer', '', 'TaurusX 投放数据 · Impressions、Clicks、Conversions、Cost、CPM、CPC、CPI'));
search.addEventListener('input', resetAndFilter);
[reportDate, country, sort].forEach(control => control.addEventListener('change', resetAndFilter));
exportButton.addEventListener('click', exportCsv);
applyFilters();
}
function resetAndFilter() {
state.page = 1;
applyFilters();
}
function applyFilters() {
const query = document.getElementById('search').value.trim().toLocaleLowerCase();
const reportDate = document.getElementById('dateFilter').value;
const country = document.getElementById('countryFilter').value;
const sort = document.getElementById('sortControl').value;
state.filtered = state.data.records.filter(row => {
const searchable = `${row.campaign_id} ${row.campaign_name} ${row.pkg}`.toLocaleLowerCase();
return (!query || searchable.includes(query))
&& (reportDate === 'all' || row.date === reportDate)
&& (country === 'all' || row.country === country);
});
const [field, direction] = sort.split('-');
const multiplier = direction === 'asc' ? 1 : -1;
state.filtered.sort((left, right) => {
const a = left[field]; const b = right[field];
if (typeof a === 'string' || typeof b === 'string') {
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' }) * multiplier
|| String(left.campaign_id).localeCompare(String(right.campaign_id), undefined, { numeric: true });
}
return (Number(a) - Number(b)) * multiplier
|| String(left.campaign_id).localeCompare(String(right.campaign_id), undefined, { numeric: true });
});
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 impressions = state.filtered.reduce((sum, row) => sum + Number(row.impressions || 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 cost = state.filtered.reduce((sum, row) => sum + Number(row.cost || 0), 0);
const cards = document.querySelectorAll('.kpi');
const setCard = (index, value, sub) => {
cards[index].querySelector('.value').textContent = value;
cards[index].querySelector('.sub').textContent = sub;
};
setCard(0, nf.format(impressions), `${nf.format(state.filtered.length)} 条筛选明细`);
setCard(1, nf.format(clicks), `CTR ${impressions ? percent(clicks / impressions) : '-'}`);
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');
meta.textContent = '';
meta.append(el('span', '', `筛选结果 ${nf.format(state.filtered.length)} 条`), el('span', '', `第 ${state.page} / ${totalPages} 页`));
renderTable(rows);
renderPagination(totalPages);
}
function renderTable(rows) {
const wrap = document.querySelector('.table-wrap');
wrap.textContent = '';
const table = el('table');
const thead = el('thead'); const header = el('tr');
['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Impressions', 'Clicks', 'Conversions', 'Cost', 'CPM', 'CPC', 'CPI']
.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', '没有符合当前条件的 TaurusX 记录'); cell.colSpan = 12;
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 || '-'));
row.append(
el('td', '', record.date || '-'),
el('td', 'pkg', record.campaign_id || '-'),
el('td', 'product', record.campaign_name || '-'),
el('td', 'pkg', record.pkg || '-'),
country,
el('td', '', nf.format(record.impressions || 0)),
el('td', '', nf.format(record.clicks || 0)),
el('td', '', nf.format(record.conversions || 0)),
el('td', '', money(record.cost)),
el('td', '', unitCost(record.cpm)),
el('td', '', unitCost(record.cpc)),
el('td', '', unitCost(record.cpi))
);
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 = ['日期', 'Campaign ID', 'Campaign', 'Pkg', '国家', 'Impressions', 'Clicks', 'Conversions', 'Cost', 'CPM', 'CPC', 'CPI'];
const rows = [headers, ...state.filtered.map(record => [
record.date, record.campaign_id, record.campaign_name, record.pkg, record.country,
record.impressions, record.clicks, record.conversions, record.cost,
record.cpm ?? '', record.cpc ?? '', record.cpi ?? ''
])];
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 = `TaurusX近7日_${new Date().toISOString().slice(0, 10)}.csv`;
document.body.append(link); link.click(); link.remove(); URL.revokeObjectURL(url);
}
fetch(`taurusx_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('taurusx_data.json 格式不正确');
state.data = data;
renderShell();
})
.catch(error => {
const app = document.getElementById('app');
app.textContent = '';
const box = el('div', 'error');
box.append(el('h2', '', '数据加载失败'), el('p', '', `无法读取 taurusx_data.json:${error.message}`));
app.append(box);
});
</script>
</body>
</html>