Files
Manta-tracking-new/index.html
T

57 lines
9.9 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Campaign 表现</title>
<style>
:root{--bg:#f4f6f8;--panel:#fff;--text:#17212b;--muted:#687481;--line:#e4e9ee;--brand:#1769e0;--good:#16825d;--warn:#c46a08}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}
main{max-width:1500px;margin:auto;padding:28px}.head{display:flex;justify-content:space-between;gap:24px;align-items:flex-end;margin-bottom:20px}
h1{font-size:28px;margin:0 0 4px}.sub,.fresh{color:var(--muted)}.fresh{text-align:right}.panel{background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:0 1px 2px #16202a0a}
.filters{display:grid;grid-template-columns:repeat(7,minmax(130px,1fr));gap:12px;padding:16px;margin-bottom:16px}.field label{display:block;color:var(--muted);font-size:12px;margin-bottom:5px}
select,input{width:100%;height:36px;border:1px solid #cfd7df;border-radius:7px;background:#fff;padding:0 9px;color:var(--text)}
.cards{display:grid;grid-template-columns:repeat(7,1fr);gap:12px;margin-bottom:16px}.card{padding:16px}.card .label{color:var(--muted);font-size:12px}.card .value{font-size:23px;font-weight:650;margin-top:5px;font-variant-numeric:tabular-nums}
.grid{display:grid;grid-template-columns:1.15fr .85fr;gap:16px;margin-bottom:16px}.chart{padding:18px;min-height:310px}.title{font-size:16px;font-weight:650;margin-bottom:12px}
svg{width:100%;height:245px}.axis{stroke:#d8dfe6}.spend-line{fill:none;stroke:var(--brand);stroke-width:2.5}.conv-line{fill:none;stroke:var(--good);stroke-width:2.5}.legend{display:flex;gap:18px;color:var(--muted);font-size:12px}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px}
.barrow{display:grid;grid-template-columns:minmax(160px,1.4fr) 3fr 90px;gap:10px;align-items:center;margin:10px 0}.barlabel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.track{height:12px;background:#eef2f5;border-radius:8px;overflow:hidden}.fill{height:100%;background:var(--brand);border-radius:8px}.num{text-align:right;font-variant-numeric:tabular-nums}
.table-panel{padding:18px}.table-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.table-note{color:var(--muted);font-size:12px}.table-wrap{overflow:auto;max-height:640px;border:1px solid var(--line);border-radius:8px}
table{width:100%;border-collapse:collapse;white-space:nowrap}th{position:sticky;top:0;background:#f7f9fb;color:#52606d;text-align:left;font-size:12px;z-index:1}th,td{padding:9px 10px;border-bottom:1px solid var(--line)}td.num,th.num{text-align:right}tbody tr:hover{background:#f7faff}.control{color:#536171}.experiment{color:#7b3fc6}.empty{padding:48px;text-align:center;color:var(--muted)}
.foot{margin-top:14px;color:var(--muted);font-size:12px}.status{display:inline-block;padding:2px 7px;border-radius:9px;background:#e8f5ef;color:var(--good)}
@media(max-width:1050px){.filters{grid-template-columns:repeat(3,1fr)}.cards{grid-template-columns:repeat(3,1fr)}.grid{grid-template-columns:1fr}}
@media(max-width:640px){main{padding:14px}.head{display:block}.fresh{text-align:left;margin-top:8px}.filters,.cards{grid-template-columns:1fr 1fr}.card .value{font-size:19px}}
</style>
</head>
<body><main>
<div class="head"><div><h1>Campaign 表现</h1><div class="sub">按日期、组别和 Campaign 查看投放效果</div></div><div class="fresh" id="fresh">正在加载数据…</div></div>
<section class="panel filters">
<div class="field"><label>日期</label><select id="date"></select></div>
<div class="field"><label>pkg</label><select id="pkg"></select></div>
<div class="field"><label>GEO</label><select id="geo"></select></div>
<div class="field"><label>渠道</label><select id="source"></select></div>
<div class="field"><label>组别</label><select id="group"></select></div>
<div class="field"><label>订单状态(G列)</label><select id="status"></select></div>
<div class="field"><label>Campaign / adping ID</label><input id="search" placeholder="搜索"></div>
</section>
<section class="cards" id="cards"></section>
<section class="grid">
<div class="panel chart"><div class="title">每日趋势</div><div class="legend"><span><i class="dot" style="background:#1769e0"></i>Spend</span><span><i class="dot" style="background:#16825d"></i>Conversions</span></div><svg id="trend" viewBox="0 0 720 245" preserveAspectRatio="none"></svg></div>
<div class="panel chart"><div class="title">Campaign 花费排名</div><div id="ranking"></div></div>
</section>
<section class="panel table-panel"><div class="table-head"><div class="title" style="margin:0">Detail</div><div class="table-note" id="count"></div></div><div class="table-wrap"><table><thead><tr><th>Date</th><th>组别</th><th>Campaign name</th><th>pkg</th><th>GEO</th><th>渠道</th><th>adping ID</th><th>KPI 事件</th><th class="num">Impression</th><th class="num">Click</th><th class="num">Conversions</th><th class="num">Spend</th><th class="num">CTR</th><th class="num">CVR</th><th class="num">CPA</th></tr></thead><tbody id="body"></tbody></table></div></section>
<div class="foot">数据源:Tableau · DSP Daily Data。组别、GEO、渠道、状态和 KPI 来自 Campaign info;订单状态以 G 列为准。对照组统一显示为 General campaign。比率均使用汇总后的分子与分母重新计算。</div>
</main><script>
const $=id=>document.getElementById(id), dims=['date','pkg','geo','source','group','status']; let all=[];
const n=(v,d=0)=>Number(v||0).toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d});
const rate=(a,b)=>b?a/b:0, pct=v=>(100*v).toFixed(2)+'%', money=v=>'$'+n(v,2);
function options(id,values,preferred){const el=$(id), old=el.value; el.innerHTML='<option value="">All</option>'+[...new Set(values.filter(Boolean))].sort().map(v=>`<option>${v}</option>`).join(''); el.value=old&&values.includes(old)?old:(preferred||'')}
function filtered(){const q=$('search').value.trim().toLowerCase();return all.filter(r=>dims.every(k=>!$(k).value||String(r[k])===$(k).value)&&(!q||(`${r.campaign} ${r.adpingId}`).toLowerCase().includes(q)))}
function aggregate(rows,keys){const m=new Map;for(const r of rows){const k=keys.map(x=>r[x]).join('\u0001');if(!m.has(k))m.set(k,{...Object.fromEntries(keys.map(x=>[x,r[x]])),impression:0,click:0,conversions:0,spend:0,kpi:r.kpi||''});const o=m.get(k);for(const x of ['impression','click','conversions','spend'])o[x]+=+r[x]||0}return [...m.values()]}
function cards(rows){const t=aggregate(rows,[])[0]||{impression:0,click:0,conversions:0,spend:0};const vals=[['Spend',money(t.spend)],['Conversions',n(t.conversions)],['CPA',t.conversions?money(t.spend/t.conversions):'—'],['Impression',n(t.impression)],['Click',n(t.click)],['CTR',pct(rate(t.click,t.impression))],['CVR',pct(rate(t.conversions,t.click))]];$('cards').innerHTML=vals.map(x=>`<div class="panel card"><div class="label">${x[0]}</div><div class="value">${x[1]}</div></div>`).join('')}
function trend(rows){const d=aggregate(rows,['date']).sort((a,b)=>a.date.localeCompare(b.date)),svg=$('trend');if(!d.length){svg.innerHTML='';return}const W=720,H=245,p=28,maxS=Math.max(...d.map(x=>x.spend),1),maxC=Math.max(...d.map(x=>x.conversions),1),x=i=>p+(W-p*2)*(d.length===1?.5:i/(d.length-1)),path=(key,max)=>d.map((r,i)=>(i?'L':'M')+x(i).toFixed(1)+' '+(H-p-(H-p*2)*r[key]/max).toFixed(1)).join(' ');const labels=d.filter((_,i)=>i===0||i===d.length-1||i===Math.floor(d.length/2)).map((r,i,a)=>`<text x="${x(d.indexOf(r))}" y="238" text-anchor="middle" fill="#74808c" font-size="11">${r.date.slice(5)}</text>`).join('');svg.innerHTML=`<line class="axis" x1="${p}" y1="${H-p}" x2="${W-p}" y2="${H-p}"/><path class="spend-line" d="${path('spend',maxS)}"/><path class="conv-line" d="${path('conversions',maxC)}"/>${labels}`}
function ranking(rows){const d=aggregate(rows,['campaign']).sort((a,b)=>b.spend-a.spend).slice(0,10),max=d[0]?.spend||1;$('ranking').innerHTML=d.length?d.map(r=>`<div class="barrow" title="${r.campaign}"><div class="barlabel">${r.campaign}</div><div class="track"><div class="fill" style="width:${100*r.spend/max}%"></div></div><div class="num">${money(r.spend)}</div></div>`).join(''):'<div class="empty">暂无数据</div>'}
function table(rows){const d=aggregate(rows,['date','group','campaign','pkg','geo','source','adpingId','kpi']).sort((a,b)=>b.date.localeCompare(a.date)||(a.group==='对照组'?-1:1)-(b.group==='对照组'?-1:1)||b.spend-a.spend);$('count').textContent=`${n(d.length)} 行`;$('body').innerHTML=d.slice(0,2000).map(r=>`<tr><td>${r.date}</td><td class="${r.group==='对照组'?'control':'experiment'}">${r.group}</td><td>${r.campaign}</td><td>${r.pkg}</td><td>${r.geo}</td><td>${r.source}</td><td>${r.adpingId}</td><td>${r.kpi||'—'}</td><td class="num">${n(r.impression)}</td><td class="num">${n(r.click)}</td><td class="num">${n(r.conversions)}</td><td class="num">${money(r.spend)}</td><td class="num">${pct(rate(r.click,r.impression))}</td><td class="num">${pct(rate(r.conversions,r.click))}</td><td class="num">${r.conversions?money(r.spend/r.conversions):'—'}</td></tr>`).join('')||'<tr><td colspan="15" class="empty">当前筛选没有数据</td></tr>'}
function render(){const rows=filtered();cards(rows);trend(rows);ranking(rows);table(rows)}
fetch('data.json',{cache:'no-store'}).then(r=>r.ok?r.json():Promise.reject()).then(d=>{all=d.rows||[];dims.forEach(k=>options(k,all.map(r=>r[k]),k==='status'?'Active':''));$('fresh').innerHTML=`数据截至 <strong>${d.latestDate||'—'}</strong><br>更新于 ${new Date(d.generatedAt).toLocaleString('zh-CN',{hour12:false})}`;dims.forEach(k=>$(k).addEventListener('change',render));$('search').addEventListener('input',render);render()}).catch(()=>{$('fresh').textContent='数据加载失败';$('body').innerHTML='<tr><td colspan="15" class="empty">无法读取 data.json</td></tr>'});
</script></body></html>