31 lines
8.7 KiB
HTML
31 lines
8.7 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Ozon 国家投放表现</title>
|
|
<style>
|
|
:root{--bg:#f4f7fb;--card:#fff;--text:#14213d;--muted:#6b7280;--line:#e6eaf0;--blue:#5577ee;--shadow:0 8px 30px rgba(24,39,75,.06)}
|
|
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,"Microsoft YaHei",system-ui,sans-serif}.shell{max-width:1380px;margin:auto;padding:30px}.hero{background:linear-gradient(125deg,#111827,#26355e);color:white;border-radius:22px;padding:28px 30px;display:flex;justify-content:space-between;gap:20px;align-items:end;box-shadow:0 16px 40px rgba(17,24,39,.15)}h1{margin:0 0 8px;font-size:29px}.hero p{margin:0;color:#cbd5e1;font-size:13px}.updated{text-align:right;font-size:12px;color:#cbd5e1;line-height:1.6}.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:18px 0}.card,.panel{background:var(--card);border:1px solid var(--line);border-radius:16px;box-shadow:var(--shadow)}.card{padding:18px}.label{font-size:12px;color:var(--muted);margin-bottom:8px}.value{font-size:26px;font-weight:800}.hint{font-size:11px;color:#94a3b8;margin-top:6px}.panel{padding:20px;margin-top:15px}.panel-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:16px}.panel-title{font-size:17px;font-weight:800}.controls{display:flex;gap:9px;flex-wrap:wrap}button,select{border:1px solid var(--line);background:white;color:var(--text);padding:9px 12px;border-radius:9px;font:inherit;cursor:pointer}button.active{background:var(--blue);border-color:var(--blue);color:white}.chart-wrap{width:100%;overflow:auto}.chart{width:100%;min-width:760px;height:auto;display:block}.legend{display:flex;justify-content:center;gap:18px;flex-wrap:wrap;margin-top:8px;font-size:12px}.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px}.tables{display:grid;grid-template-columns:.8fr 1.2fr;gap:15px}.table-wrap{overflow:auto;max-height:560px;border:1px solid var(--line);border-radius:11px}table{border-collapse:separate;border-spacing:0;width:100%;font-size:12px}th,td{padding:10px 12px;border-bottom:1px solid #edf0f5;text-align:right;white-space:nowrap}th{position:sticky;top:0;background:#f8fafc;color:#64748b;z-index:1}th:first-child,td:first-child{text-align:left}.country{font-weight:800}.error{padding:50px;text-align:center;color:#b42318}.footer{text-align:center;color:#94a3b8;font-size:11px;margin:20px}.skeleton{animation:pulse 1.2s infinite;background:#e9edf5;color:transparent;border-radius:5px}@keyframes pulse{50%{opacity:.55}}@media(max-width:950px){.grid{grid-template-columns:repeat(2,1fr)}.tables{grid-template-columns:1fr}}@media(max-width:620px){.shell{padding:14px}.hero{display:block}.updated{text-align:left;margin-top:14px}.grid{grid-template-columns:1fr 1fr}.value{font-size:21px}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
<header class="hero"><div><h1>Ozon · 国家投放表现</h1><p id="period">正在加载过去 14 天数据…</p></div><div class="updated" id="updated">数据加载中</div></header>
|
|
<div class="grid" id="kpis"><div class="card"><div class="label">总安装</div><div class="value skeleton">00000</div></div></div>
|
|
<section class="panel"><div class="panel-head"><div class="panel-title">14 天趋势</div><div class="controls" id="metricButtons"><button data-metric="installs" class="active">安装数</button><button data-metric="spend">消耗</button><button data-metric="cac">CAC</button></div></div><div class="chart-wrap"><svg class="chart" id="chart" viewBox="0 0 1100 390" role="img" aria-label="国家趋势图"></svg></div><div class="legend" id="legend"></div></section>
|
|
<div class="tables"><section class="panel"><div class="panel-head"><div class="panel-title">国家汇总</div></div><div class="table-wrap"><table><thead><tr><th>国家</th><th>安装数</th><th>消耗</th><th>日均 CAC</th></tr></thead><tbody id="summaryRows"></tbody></table></div></section><section class="panel"><div class="panel-head"><div class="panel-title">每日明细</div><select id="countryFilter"><option value="">全部国家</option></select></div><div class="table-wrap"><table><thead><tr><th>日期</th><th>国家</th><th>安装数</th><th>消耗</th><th>CAC</th></tr></thead><tbody id="detailRows"></tbody></table></div></section></div>
|
|
<div class="footer">数据来源:Google Sheets · Ozon_IronSpace · 每 24 小时自动更新</div>
|
|
</div>
|
|
<script>
|
|
const colors={KZ:'#5577ee',UZ:'#32b67a',KG:'#f59e0b',AM:'#e4568b'};let report=null;let metric='installs';
|
|
const $=id=>document.getElementById(id);const number=(v,d=0)=>Number(v||0).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d});const money=v=>v==null?'—':'$'+number(v,2);const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
function render(){const totalI=report.summary.reduce((a,x)=>a+x.installs,0),totalS=report.summary.reduce((a,x)=>a+x.spend,0),cac=report.summary.map(x=>x.avg_cac).filter(x=>x!=null);$('period').textContent=`${report.period.start} 至 ${report.period.end} · ${report.period.days} 个完整自然日`;$('updated').innerHTML=`最近更新:${report.generated_at.replace('T',' ')}<br>时区:${report.timezone}`;$('kpis').innerHTML=[['总安装',number(totalI),'四国合计'],['总消耗',money(totalS),'USD'],['平均 CAC',cac.length?number(cac.reduce((a,b)=>a+b,0)/cac.length,2):'—','来源表原始单位 · 各国家日均值'],['覆盖国家',report.countries.length,'KZ · UZ · KG · AM']].map(x=>`<div class="card"><div class="label">${x[0]}</div><div class="value">${x[1]}</div><div class="hint">${x[2]}</div></div>`).join('');$('summaryRows').innerHTML=report.summary.map(x=>`<tr><td class="country" style="color:${colors[x.country]}">${x.country}</td><td>${number(x.installs)}</td><td>${money(x.spend)}</td><td>${number(x.avg_cac,2)}</td></tr>`).join('');$('countryFilter').innerHTML='<option value="">全部国家</option>'+report.countries.map(x=>`<option>${x}</option>`).join('');renderDetails();drawChart()}
|
|
function renderDetails(){const country=$('countryFilter').value,rows=report.records.filter(x=>!country||x.country===country).sort((a,b)=>b.date.localeCompare(a.date)||a.country.localeCompare(b.country));$('detailRows').innerHTML=rows.map(x=>`<tr><td>${x.date}</td><td class="country" style="color:${colors[x.country]}">${x.country}</td><td>${number(x.installs)}</td><td>${money(x.spend)}</td><td>${x.cac==null?'—':number(x.cac,2)}</td></tr>`).join('')}
|
|
function drawChart(){const svg=$('chart'),W=1100,H=390,m={l:70,r:25,t:30,b:58},iw=W-m.l-m.r,ih=H-m.t-m.b,dates=[...new Set(report.records.map(x=>x.date))].sort(),values=report.records.map(x=>x[metric]).filter(x=>x!=null),max=Math.max(...values,1)*1.12,x=i=>m.l+(dates.length===1?iw/2:i*iw/(dates.length-1)),y=v=>m.t+ih-(v/max)*ih,fmt=metric==='installs'?v=>number(v):(metric==='spend'?v=>money(v):v=>number(v,2));let h='';for(let i=0;i<=5;i++){const yy=m.t+ih*i/5,val=max*(1-i/5);h+=`<line x1="${m.l}" y1="${yy}" x2="${W-m.r}" y2="${yy}" stroke="#e8edf5"/><text x="${m.l-12}" y="${yy+4}" text-anchor="end" fill="#8190a5" font-size="11">${esc(fmt(val))}</text>`}dates.forEach((d,i)=>{if(i%2===0||dates.length<9)h+=`<text x="${x(i)}" y="${H-27}" text-anchor="middle" fill="#8190a5" font-size="11">${d.slice(5)}</text>`});for(const country of report.countries){const rows=dates.map(d=>report.records.find(r=>r.country===country&&r.date===d)),points=rows.map((r,i)=>r&&r[metric]!=null?`${x(i)},${y(r[metric])}`:null);let segment=[];const flush=()=>{if(segment.length>1)h+=`<polyline points="${segment.join(' ')}" fill="none" stroke="${colors[country]}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>`;segment=[]};points.forEach(p=>p?segment.push(p):flush());flush();rows.forEach((r,i)=>{if(r&&r[metric]!=null)h+=`<circle cx="${x(i)}" cy="${y(r[metric])}" r="4" fill="${colors[country]}" stroke="white" stroke-width="2"><title>${country} · ${r.date} · ${fmt(r[metric])}</title></circle>`})}svg.innerHTML=h;$('legend').innerHTML=report.countries.map(x=>`<span><i class="dot" style="background:${colors[x]}"></i>${x}</span>`).join('')}
|
|
document.querySelectorAll('#metricButtons button').forEach(btn=>btn.onclick=()=>{metric=btn.dataset.metric;document.querySelectorAll('#metricButtons button').forEach(x=>x.classList.toggle('active',x===btn));drawChart()});$('countryFilter').onchange=renderDetails;
|
|
fetch('data.json',{cache:'no-store'}).then(r=>{if(!r.ok)throw new Error(`HTTP ${r.status}`);return r.json()}).then(data=>{report=data;render()}).catch(error=>{document.querySelector('.shell').innerHTML=`<div class="panel error"><h2>数据加载失败</h2><p>${esc(error.message)}</p><p>请稍后刷新页面。</p></div>`});
|
|
</script>
|
|
</body>
|
|
</html>
|