Files
taurusx_tracking/taurusx.html
T

448 lines
23 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: #17211e;
--muted: #707b77;
--line: #e3e9e6;
--line-strong: #d4dfda;
--surface: #fff;
--canvas: #f5f7f6;
--green: #0c7454;
--green-dark: #0c4f3d;
--green-soft: #e7f5ef;
--blue: #3569a8;
--amber: #a9660a;
--shadow: 0 8px 24px rgba(28, 53, 43, .055);
}
* { 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;
-webkit-font-smoothing: antialiased;
}
button, input, select { font: inherit; }
button { cursor: pointer; }
.hero {
position: relative;
overflow: hidden;
min-height: 188px;
padding: 25px max(24px, calc((100vw - 1320px) / 2));
color: #f7fffb;
background: linear-gradient(118deg, #153d32 0%, #0b6048 62%, #16795d 100%);
}
.hero::after {
content: "TX";
position: absolute;
right: max(24px, calc((100vw - 1320px) / 2));
bottom: -45px;
font: 800 168px/1 Inter, sans-serif;
letter-spacing: -16px;
color: rgba(255,255,255,.05);
transform: rotate(-5deg);
pointer-events: none;
}
.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 {
display: inline-flex; align-items: center; min-height: 34px; padding: 0 12px;
border: 1px solid rgba(255,255,255,.25); border-radius: 9px;
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,.14); }
.hero-body { position: relative; z-index: 1; margin-top: 22px; }
h1 { margin: 0; font-size: clamp(29px, 3.5vw, 43px); letter-spacing: -.04em; line-height: 1.08; }
.hero p { max-width: 680px; margin: 10px 0 0; color: #d2e8e0; line-height: 1.6; }
.hero-meta { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 17px; }
.meta-chip { padding: 5px 9px; border-radius: 7px; color: #cbe5dc; background: rgba(255,255,255,.08); font-size: 11px; }
.page { max-width: 1320px; margin: 0 auto; padding: 20px 24px 28px; }
.primary-kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.stat-card {
position: relative;
min-height: 116px;
padding: 17px 18px;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--surface);
box-shadow: var(--shadow);
}
.stat-card::before { content: ""; position: absolute; top: 18px; right: 18px; width: 7px; height: 7px; border-radius: 50%; background: #83baa5; }
.stat-card.blue::before { background: #7fa6d5; }
.stat-card.amber::before { background: #d5a45f; }
.stat-label { color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: .035em; text-transform: uppercase; }
.stat-value { margin-top: 9px; font-size: clamp(25px, 2.5vw, 32px); font-weight: 760; letter-spacing: -.035em; font-variant-numeric: tabular-nums; }
.stat-sub { margin-top: 7px; color: var(--muted); font-size: 11px; }
.efficiency {
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 { width: 100%; min-width: 1080px; border-collapse: collapse; }
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; }
th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; }
td { padding: 12px; border-top: 1px solid #edf1ef; vertical-align: middle; }
tbody tr:nth-child(even) td { background: #fcfdfc; }
tbody tr:hover td { background: #f5faf7; }
.date-cell { color: #57645f; font-size: 11px; white-space: nowrap; }
.campaign-cell { min-width: 310px; max-width: 390px; }
.campaign-name { overflow: hidden; color: #25322d; font-size: 12px; font-weight: 670; text-overflow: ellipsis; white-space: nowrap; }
.campaign-meta { display: flex; flex-wrap: wrap; gap: 4px 10px; margin-top: 5px; color: #89938f; font-size: 9px; }
.campaign-id, .pkg { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.pkg { overflow-wrap: anywhere; }
.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; }
.unit-col { color: var(--green-dark); font-weight: 650; }
.zero { color: #b0b7b4; }
.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; }
.error { max-width: 700px; margin: 70px auto; padding: 22px; border: 1px solid #e4aaa6; border-radius: 13px; background: #fff4f2; color: #8c2c28; }
footer { padding: 20px 0 5px; color: #929b97; font-size: 10px; text-align: center; }
@media (max-width: 980px) {
.primary-kpis { 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: 620px) {
.hero { min-height: 170px; padding: 20px 16px; }
.hero-body { margin-top: 18px; }
.page { padding: 14px 12px 22px; }
.primary-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.stat-card { min-height: 100px; padding: 14px; }
.stat-value { font-size: 23px; }
.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>
</head>
<body>
<header class="hero">
<div class="hero-top">
<div class="brand">AgentStage · Performance</div>
<a class="nav-link" href="index.html">← 返回首页</a>
</div>
<div class="hero-body">
<h1>TaurusX 投放数据</h1>
<p>近 7 日 Campaign 投放表现与单位成本效率。</p>
<div class="hero-meta"><span class="meta-chip" id="rangeLabel">日期加载中</span><span class="meta-chip" id="updatedLabel">更新时间加载中</span></div>
</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 statCard(key, label, value, sub, tone = '') {
const card = el('article', `stat-card ${tone}`.trim());
card.dataset.metric = key;
card.append(el('div', 'stat-label', label), el('div', 'stat-value', value), el('div', 'stat-sub', sub));
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() {
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 primary = el('section', 'primary-kpis');
primary.append(
statCard('impressions', 'Impressions', nf.format(meta.impressions), `${nf.format(meta.campaigns)} 个 Campaign`),
statCard('clicks', 'Clicks', nf.format(meta.clicks), `${nf.format(meta.pkg_country_count)} 个 Pkg_国家`, 'blue'),
statCard('conversions', 'Conversions', nf.format(meta.conversions), `${nf.format(meta.countries)} 个国家`, 'amber'),
statCard('cost', 'Cost', money(meta.cost), `${nf.format(meta.rows)} 条明细`)
);
const efficiency = el('section', 'efficiency');
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 title = el('div');
title.append(el('h2', '', '投放明细'), el('div', 'panel-desc', '按日期与 Campaign 查看表现,筛选后上方指标同步更新'));
heading.append(title);
const toolbar = el('div', 'toolbar');
const searchWrap = el('div', 'search');
const search = el('input', 'control');
search.id = 'search'; search.type = 'search'; search.placeholder = '搜索 Campaign ID、Campaign 或 Pkg'; search.autocomplete = 'off';
searchWrap.append(search);
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', '导出 CSV');
exportButton.type = 'button'; exportButton.id = 'exportButton';
toolbar.append(searchWrap, reportDate, country, sort, exportButton);
panel.append(heading, toolbar, el('div', 'result-meta'), el('div', 'table-wrap'), el('div', 'pagination'));
app.append(primary, efficiency, panel, el('footer', '', 'TaurusX Performance · Impressions · Clicks · Conversions · Cost'));
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 (a === null && b !== null) return 1;
if (a !== null && b === null) return -1;
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);
updateMetric('impressions', nf.format(impressions), `${nf.format(state.filtered.length)} 条筛选明细`);
updateMetric('clicks', nf.format(clicks), `CTR ${impressions ? percent(clicks / impressions) : '-'}`);
updateMetric('conversions', nf.format(conversions), `CVR ${clicks ? percent(conversions / clicks) : '-'}`);
updateMetric('cost', money(cost), `${nf.format(new Set(state.filtered.map(row => row.country)).size)} 个国家`);
updateMetric('cpm', unitCost(impressions ? cost / impressions * 1000 : null));
updateMetric('cpc', unitCost(clicks ? cost / clicks : null));
updateMetric('cpi', unitCost(conversions ? cost / conversions : null));
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');
const columns = [
['日期', ''], ['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);
const tbody = el('tbody');
if (!rows.length) {
const cell = el('td', 'empty', '没有符合当前条件的记录'); cell.colSpan = 10;
const row = el('tr'); row.append(cell); tbody.append(row);
}
rows.forEach(record => {
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 numeric = (value, formatter = nf.format, extraClass = '') => {
const zero = !value;
return el('td', `num ${extraClass} ${zero ? 'zero' : ''}`.trim(), formatter(value));
};
row.append(
el('td', 'date-cell', record.date || '-'),
campaign,
country,
numeric(record.impressions || 0),
numeric(record.clicks || 0),
numeric(record.conversions || 0),
numeric(record.cost || 0, money),
numeric(record.cpm, unitCost, 'unit-col'),
numeric(record.cpc, unitCost, 'unit-col'),
numeric(record.cpi, unitCost, 'unit-col')
);
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; prev.title = '上一页';
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('.panel').scrollIntoView({ behavior: 'smooth' }); });
next.addEventListener('click', () => { state.page += 1; renderResults(); document.querySelector('.panel').scrollIntoView({ behavior: 'smooth' }); });
box.append(prev, el('span', 'page-label', `${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>