Files
Manta-tracking/index.html
T

13 lines
14 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"><title>Manta 在投人群包表现</title><style>
:root{--bg:#f5f7fb;--panel:#fff;--ink:#152238;--muted:#69778c;--line:#e5eaf2;--blue:#356ae6;--orange:#ee8b2d;--red:#d95757;--shadow:0 10px 30px rgba(32,55,88,.08)}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}.wrap{max-width:1560px;margin:auto;padding:30px}.top{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:22px}.eyebrow{color:var(--blue);font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}h1{font-size:30px;line-height:1.15;margin:7px 0 8px}.sub,.desc{color:var(--muted)}.fresh{background:#eaf1ff;color:#2858bf;border-radius:999px;padding:9px 13px;white-space:nowrap;font-weight:700}.filters{display:grid;grid-template-columns:1.5fr repeat(3,1fr) 1.05fr 1.05fr auto;gap:10px;background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px;box-shadow:var(--shadow);margin-bottom:18px}label{display:flex;flex-direction:column;gap:5px;color:var(--muted);font-size:12px}select,input,button{height:39px;border:1px solid var(--line);border-radius:9px;background:#fff;color:var(--ink);padding:0 11px;font:inherit}button{cursor:pointer;font-weight:700}.filters button{align-self:end}.period-bar{display:flex;justify-content:space-between;align-items:center;gap:15px;margin:3px 0 12px}.seg{display:inline-flex;background:#edf1f7;padding:3px;border-radius:10px}.seg button{border:0;background:transparent;height:34px;color:var(--muted)}.seg button.on{background:#fff;color:var(--blue);box-shadow:0 3px 10px rgba(30,60,100,.1)}.period-select{min-width:180px}.kpis{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin-bottom:18px}.card,.panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}.card{padding:16px}.card .label{color:var(--muted);font-size:12px}.card .value{font-size:25px;font-weight:800;margin-top:4px}.card .hint{color:var(--muted);font-size:11px;margin-top:2px}.compare{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:18px}.group-card{padding:17px;border-left:4px solid var(--blue)}.group-card.control{border-left-color:var(--orange)}.group-card h3{margin:0 0 11px}.mini{display:grid;grid-template-columns:repeat(5,1fr);gap:8px}.mini span{background:#f7f9fc;border-radius:8px;padding:8px;color:var(--muted);font-size:11px}.mini b{display:block;color:var(--ink);font-size:14px;margin-top:2px}.panel{padding:18px;min-width:0;margin-bottom:18px}.panel h2{font-size:16px;margin:0 0 3px}.desc{font-size:12px}.table-panel{padding:0;overflow:hidden}.table-head{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:18px}.table-wrap{overflow:auto;max-height:510px;border-top:1px solid var(--line)}table{border-collapse:collapse;width:100%;min-width:1380px}th,td{padding:10px 11px;border-bottom:1px solid var(--line);text-align:right;white-space:nowrap}th{position:sticky;top:0;background:#f9fbfe;color:var(--muted);font-size:11px;text-transform:uppercase;cursor:pointer;z-index:2}th:first-child,td:first-child{text-align:left;position:sticky;left:0;background:inherit;z-index:1}tbody tr:nth-child(even){background:#fbfcff}tbody tr:hover{background:#eef4ff}.pkg{font-weight:700}.badge{display:inline-block;border-radius:999px;padding:2px 8px;font-size:11px;font-weight:700}.exp{background:#e8f1ff;color:#2858bf}.ctl{background:#fff0e2;color:#b66113}.daily-tools{display:flex;align-items:end;gap:10px}.method{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:18px;color:var(--muted);font-size:12px}.method strong{color:var(--ink)}.error,.empty{padding:30px;text-align:center;color:var(--red)}.empty{color:var(--muted)}@media(max-width:1100px){.filters{grid-template-columns:repeat(3,1fr)}.kpis{grid-template-columns:repeat(3,1fr)}.compare{grid-template-columns:1fr}.mini{grid-template-columns:repeat(3,1fr)}}@media(max-width:700px){.wrap{padding:18px}.top,.period-bar,.table-head{flex-direction:column;align-items:flex-start}.filters{grid-template-columns:1fr 1fr}.kpis{grid-template-columns:1fr 1fr}.mini{grid-template-columns:1fr 1fr}.method{grid-template-columns:1fr}}
</style></head><body><main class="wrap" id="app"><div class="error">正在加载数据…</div></main><script>
const $=s=>document.querySelector(s),fmt={int:n=>n==null?'—':Math.round(n).toLocaleString('en-US'),money:n=>n==null?'—':'$'+n.toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2}),pct:n=>n==null?'—':(n*100).toFixed(2)+'%'};let DATA,grain='week',sort={key:'spend',dir:-1};
fetch('data.json',{cache:'no-store'}).then(r=>{if(!r.ok)throw Error('data.json '+r.status);return r.json()}).then(d=>{DATA=d;boot()}).catch(e=>{$('#app').innerHTML='<div class="error">数据加载失败:'+e.message+'</div>'});
function boot(){let m=DATA.meta,pkgs=uniq('pkg'),geos=uniq('geo'),plats=uniq('platform');$('#app').innerHTML=`<section class="top"><div><div class="eyebrow">Manta · Experiment Tracking</div><h1>在投 pkg 实验组 / 对照组表现</h1><div class="sub">覆盖 ${m.date_start||'—'} 至 ${m.date_end||'—'} · ${m.active_campaign_count} 个 Active campaign</div></div><div class="fresh">更新于 ${new Date(m.generated_at).toLocaleString('zh-CN',{timeZone:'Asia/Shanghai'})}</div></section><section class="filters"><label>pkg<select id="pkg"><option value="">全部 pkg</option>${pkgs.map(opt).join('')}</select></label><label>组别<select id="group"><option value="">实验组 + 对照组</option><option>实验组</option><option>对照组</option></select></label><label>GEO<select id="geo"><option value="">全部 GEO</option>${geos.map(opt).join('')}</select></label><label>Platform<select id="platform"><option value="">全部 Platform</option>${plats.map(opt).join('')}</select></label><label>开始日期<input id="start" type="date" value="${m.date_start||''}"></label><label>结束日期<input id="end" type="date" value="${m.date_end||''}"></label><button id="reset">重置筛选</button></section><section class="period-bar"><div><h2 style="margin:0">周期指标表现</h2><div class="desc">按 campaign(adping_id)查看</div></div><div style="display:flex;gap:10px;align-items:center"><div class="seg"><button id="week" class="on">Weekly</button><button id="month">Monthly</button></div><select id="period" class="period-select"></select></div></section><section id="kpis" class="kpis"></section><section id="compare" class="compare"></section>${tableSection('Campaign 周期表现','period-desc','period-body',false)}<section class="panel table-panel"><div class="table-head"><div><h2>每天实验组 / 对照组表现</h2><div class="desc" id="daily-desc"></div></div><div class="daily-tools"><label>日期<select id="day"></select></label></div></div><div class="table-wrap"><table><thead>${head(true)}</thead><tbody id="daily-body"></tbody></table></div></section>${tableSection('pkg 汇总','pkg-desc','pkg-body','pkg')}<section class="method"><div><strong>计算口径</strong><br>CTR = Click / Impression × 100%;CVR = Conversion / Click × 100%;IVR = Conversion / Impression × 100%;CAC = Spend / Conversion;eCPI = Spend / Conversion。聚合时先汇总基础量,再重算比率。</div><div><strong>分组与基础信息</strong><br>pkg、adping_id、实验/对照组、GEO、Platform、投放状态来自钉钉「Campaign info」;仅纳入 G 列为 Active 的 campaign。表现数据来自 Tableau「DSP Daily Data」。</div></section>`;['pkg','group','geo','platform','start','end'].forEach(id=>$('#'+id).onchange=filtersChanged);$('#reset').onclick=()=>{['pkg','group','geo','platform'].forEach(id=>$('#'+id).value='');$('#start').value=m.date_start||'';$('#end').value=m.date_end||'';filtersChanged()};$('#week').onclick=()=>setGrain('week');$('#month').onclick=()=>setGrain('month');$('#period').onchange=render;$('#day').onchange=renderDaily;document.querySelectorAll('th[data-k]').forEach(th=>th.onclick=()=>{let k=th.dataset.k;sort=k===sort.key?{key:k,dir:-sort.dir}:{key:k,dir:k==='pkg'?1:-1};render()});filtersChanged()}
function tableSection(title,desc,body,type){return `<section class="panel table-panel"><div class="table-head"><div><h2>${title}</h2><div class="desc" id="${desc}"></div></div><div class="sub">点击表头排序</div></div><div class="table-wrap"><table><thead>${type==='pkg'?pkgHead():head()}</thead><tbody id="${body}"></tbody></table></div></section>`}function uniq(k){return [...new Set(DATA.campaigns.map(x=>x[k]).filter(Boolean))].sort()}function opt(v){return `<option>${esc(v)}</option>`}function head(day=false){let c=[['pkg','pkg'],['adping_id','Campaign'],['group','组别'],['geo','GEO'],['platform','Platform']];if(day)c.unshift(['date','日期']);return `<tr>${c.concat(metrics()).map(([k,l])=>`<th data-k="${k}">${l}</th>`).join('')}</tr>`}function pkgHead(){return `<tr>${[['pkg','pkg'],...metrics()].map(([k,l])=>`<th data-k="${k}">${l}</th>`).join('')}</tr>`}function metrics(){return [['impression','Impression'],['click','Click'],['conversion','Conversion'],['spend','Spend'],['ctr','CTR'],['cvr','CVR'],['ivr','IVR'],['cac','CAC'],['ecpi','eCPI']]}
function filtersChanged(){fillPeriods();fillDays();render()}function setGrain(g){grain=g;$('#week').classList.toggle('on',g==='week');$('#month').classList.toggle('on',g==='month');fillPeriods();render()}function baseRows(){let f={pkg:$('#pkg').value,group:$('#group').value,geo:$('#geo').value,platform:$('#platform').value},s=$('#start').value,e=$('#end').value;return DATA.campaign_daily.filter(r=>(!f.pkg||r.pkg===f.pkg)&&(!f.group||r.group===f.group)&&(!f.geo||r.geo===f.geo)&&(!f.platform||r.platform===f.platform)&&(!s||r.date>=s)&&(!e||r.date<=e))}function pkey(date){if(grain==='month')return date.slice(0,7);let d=new Date(date+'T00:00:00Z'),n=d.getUTCDay()||7;d.setUTCDate(d.getUTCDate()-n+1);return d.toISOString().slice(0,10)}function plabel(k){if(grain==='month')return k;let e=new Date(k+'T00:00:00Z');e.setUTCDate(e.getUTCDate()+6);return k+' — '+e.toISOString().slice(0,10)}function fillPeriods(){let cur=$('#period').value,ks=[...new Set(baseRows().map(r=>pkey(r.date)))].sort().reverse();$('#period').innerHTML=ks.map(k=>`<option value="${k}">${plabel(k)}</option>`).join('');if(ks.includes(cur))$('#period').value=cur}function fillDays(){let cur=$('#day').value,ds=[...new Set(baseRows().map(r=>r.date))].sort().reverse();$('#day').innerHTML=ds.map(opt).join('');if(ds.includes(cur))$('#day').value=cur}
function calc(x){x.ctr=x.impression?x.click/x.impression:null;x.cvr=x.click?x.conversion/x.click:null;x.ivr=x.impression?x.conversion/x.impression:null;x.cac=x.conversion?x.spend/x.conversion:null;x.ecpi=x.cac;return x}function agg(rows,keys){let m=new Map;rows.forEach(r=>{let id=keys.map(k=>r[k]).join('|'),x=m.get(id);if(!x){x={};keys.forEach(k=>x[k]=r[k]);Object.assign(x,{impression:0,click:0,conversion:0,spend:0});m.set(id,x)}metrics().slice(0,4).forEach(([k])=>x[k]+=r[k])});return [...m.values()].map(calc)}function total(rows){return calc(rows.reduce((a,r)=>{metrics().slice(0,4).forEach(([k])=>a[k]+=r[k]);return a},{impression:0,click:0,conversion:0,spend:0}))}
function render(){let rows=baseRows(),pk=$('#period').value,pr=rows.filter(r=>pkey(r.date)===pk),campaign=agg(pr,['pkg','adping_id','group','geo','platform']),sum=total(pr);$('#period-desc').textContent=(pk?plabel(pk):'当前筛选无数据')+' · '+campaign.length+' 个 campaign';$('#kpis').innerHTML=[['Impression',fmt.int(sum.impression),'曝光量'],['Click',fmt.int(sum.click),'CTR '+fmt.pct(sum.ctr)],['Conversion',fmt.int(sum.conversion),'CVR '+fmt.pct(sum.cvr)],['Spend',fmt.money(sum.spend),'总消耗'],['CAC / eCPI',fmt.money(sum.cac),'IVR '+fmt.pct(sum.ivr)]].map(x=>`<article class="card"><div class="label">${x[0]}</div><div class="value">${x[1]}</div><div class="hint">${x[2]}</div></article>`).join('');renderCompare(pr);renderRows($('#period-body'),campaign);renderDaily();renderPkg(rows)}function renderCompare(rows){let map=Object.fromEntries(agg(rows,['group']).map(x=>[x.group,x]));$('#compare').innerHTML=['实验组','对照组'].map((g,i)=>{let x=map[g]||total([]);return `<article class="panel group-card ${i?'control':''}"><h3><span class="badge ${i?'ctl':'exp'}">${g}</span></h3><div class="mini">${[['CTR',fmt.pct(x.ctr)],['CVR',fmt.pct(x.cvr)],['IVR',fmt.pct(x.ivr)],['CAC',fmt.money(x.cac)],['eCPI',fmt.money(x.ecpi)]].map(v=>`<span>${v[0]}<b>${v[1]}</b></span>`).join('')}</div></article>`}).join('')}function renderDaily(){let d=$('#day').value,items=agg(baseRows().filter(r=>r.date===d),['date','pkg','adping_id','group','geo','platform']);$('#daily-desc').textContent=(d||'当前筛选无数据')+' · '+items.length+' 个 campaign';renderRows($('#daily-body'),items)}function renderPkg(rows){let items=agg(rows,['pkg']).sort((a,b)=>b.spend-a.spend);$('#pkg-desc').textContent=items.length+' 个 pkg · 当前日期与维度筛选汇总';$('#pkg-body').innerHTML=items.map(x=>`<tr><td class="pkg">${esc(x.pkg)}</td>${cells(x)}</tr>`).join('')||empty(10)}
function renderRows(el,rows){rows.sort((a,b)=>{let x=a[sort.key],y=b[sort.key];if(x==null)x=-Infinity;if(y==null)y=-Infinity;return typeof x==='string'?x.localeCompare(y)*sort.dir:(x-y)*sort.dir});el.innerHTML=rows.map(x=>`<tr>${x.date?`<td>${x.date}</td>`:''}<td class="pkg">${esc(x.pkg)}</td><td>${esc(x.adping_id)}</td><td><span class="badge ${x.group==='实验组'?'exp':'ctl'}">${x.group}</span></td><td>${esc(x.geo||'—')}</td><td>${esc(x.platform||'—')}</td>${cells(x)}</tr>`).join('')||empty(rows.some(x=>x.date)?15:14)}function cells(x){return `<td>${fmt.int(x.impression)}</td><td>${fmt.int(x.click)}</td><td>${fmt.int(x.conversion)}</td><td>${fmt.money(x.spend)}</td><td>${fmt.pct(x.ctr)}</td><td>${fmt.pct(x.cvr)}</td><td>${fmt.pct(x.ivr)}</td><td>${fmt.money(x.cac)}</td><td>${fmt.money(x.ecpi)}</td>`}function empty(n){return `<tr><td colspan="${n}" class="empty">当前筛选无数据</td></tr>`}function esc(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))}
</script></body></html>