Files
DSP-routing-daily/index.html
T

281 lines
14 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. 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.0">
<title>DSP Routing Daily 报告</title>
<style>
:root{--bg:#f5f6f8;--card:#fff;--line:#e3e6ea;--head:#1f2937;--sub:#6b7280;--accent:#2563eb;--accent-bg:#eff4ff;}
*{box-sizing:border-box;}
body{margin:0;background:var(--bg);color:#1f2937;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;font-size:13px;}
.wrap{max-width:1500px;margin:0 auto;padding:24px 20px 60px;}
h1{font-size:20px;margin:0 0 4px;}
.meta{color:var(--sub);font-size:12px;margin-bottom:20px;}
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:16px 18px;margin-bottom:22px;box-shadow:0 1px 2px rgba(0,0,0,.03);}
.card h2{font-size:15px;margin:0 0 12px;display:flex;align-items:center;gap:8px;}
.card h2 .tag{font-size:11px;font-weight:normal;color:var(--sub);background:var(--bg);border:1px solid var(--line);padding:1px 8px;border-radius:20px;}
table{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums;}
th,td{border:1px solid var(--line);padding:7px 10px;text-align:right;white-space:nowrap;}
th{background:var(--head);color:#fff;font-weight:600;text-align:center;position:sticky;top:0;cursor:default;}
td:first-child,th:first-child{text-align:center;}
.detail-scroll{max-height:620px;overflow:auto;border:1px solid var(--line);border-radius:8px;}
.detail-scroll table th{top:0;}
tbody tr:nth-child(even){background:#fafbfc;}
tbody tr:hover{background:var(--accent-bg);}
.txt{text-align:left;}
.filters{display:flex;flex-wrap:wrap;gap:14px;align-items:flex-end;margin-bottom:14px;}
.fld{display:flex;flex-direction:column;gap:4px;}
.fld label{font-size:11px;color:var(--sub);}
.fld input,.fld select{border:1px solid var(--line);border-radius:6px;padding:6px 8px;font-size:13px;min-width:150px;background:#fff;}
.fld input[type=date]{min-width:140px;}
.btn{border:1px solid var(--accent);background:var(--accent);color:#fff;border-radius:6px;padding:7px 16px;font-size:13px;cursor:pointer;}
.btn.ghost{background:#fff;color:var(--accent);}
.count-note{color:var(--sub);font-size:12px;margin:6px 2px 0;}
.pos{color:#059669;} .neg{color:#dc2626;}
.empty{padding:30px;text-align:center;color:var(--sub);}
.sum-metric{color:var(--accent);font-weight:600;}
.note-list{margin:0 0 14px;padding:10px 14px 10px 28px;background:var(--bg);border:1px solid var(--line);border-radius:8px;color:#4b5563;font-size:12px;line-height:1.9;}
.note-list b{color:var(--head);}
.total-row td{position:sticky;top:33px;background:#eef2f7;font-weight:700;color:var(--head);border-top:2px solid #cbd5e1;z-index:1;}
.total-row td.txt{text-align:left;}
th.sortable{cursor:pointer;user-select:none;}
th.sortable:hover{background:#374151;}
th .arrow{display:inline-block;width:10px;margin-left:3px;font-size:10px;color:#93c5fd;}
th.sort-asc .arrow::after{content:"\25B2";}
th.sort-desc .arrow::after{content:"\25BC";}
</style>
</head>
<body>
<div class="wrap">
<h1>DSP Routing Daily 报告</h1>
<div class="meta" id="meta">加载中…</div>
<div class="card">
<h2>每日总表 <span class="tag">保留近3天</span></h2>
<ul class="note-list">
<li><b>日期</b>:查询当日,有点击订单等数据来自前一天</li>
<li><b>原始订单</b>:全部可融合订单【DSP Campaign(可融合)】</li>
<li><b>pkg</b>:原始订单按照 pkg+geo 去重后的订单数</li>
<li><b>中转订单</b>:已 link 订单【Order List】</li>
</ul>
<div style="overflow:auto;">
<table id="summaryTable">
<thead>
<tr>
<th>日期</th><th>原始订单</th><th>pkg</th><th>中转订单</th>
<th>有点击订单</th><th>无转化订单</th><th>无点击订单</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="card">
<h2>Kibana 监测明细表 <span class="tag">按 adping_id 聚合 · 近14天</span></h2>
<div class="filters">
<div class="fld"><label>dt 起</label><input type="date" id="fDtFrom"></div>
<div class="fld"><label>dt 止</label><input type="date" id="fDtTo"></div>
<div class="fld"><label>融合后campaign id (routing_campaign_id)</label><input type="text" id="fRc" placeholder="模糊匹配"></div>
<div class="fld"><label>融合前campaign id (campaign_id)</label><input type="text" id="fCid" placeholder="模糊匹配"></div>
<div class="fld"><label>second_channel</label><select id="fSc"><option value="">全部</option></select></div>
<div class="fld"><label>account</label><select id="fAcc"><option value="">全部</option></select></div>
<button class="btn" id="applyBtn">筛选</button>
<button class="btn ghost" id="resetBtn">重置</button>
</div>
<div class="count-note" id="detailNote"></div>
<div class="detail-scroll">
<table id="detailTable">
<thead>
<tr>
<th class="sortable" data-key="dt" data-type="dt">dt<span class="arrow"></span></th>
<th class="sortable" data-key="routing_campaign_id" data-type="str">融合后campaign id<span class="arrow"></span></th>
<th class="sortable" data-key="campaign_id" data-type="str">融合前campaign id<span class="arrow"></span></th>
<th class="sortable" data-key="adping_id" data-type="num">adping_id<span class="arrow"></span></th>
<th class="sortable" data-key="second_channel" data-type="str">second_channel<span class="arrow"></span></th>
<th class="sortable" data-key="account" data-type="str">account<span class="arrow"></span></th>
<th class="sortable" data-key="pkg" data-type="str">pkg<span class="arrow"></span></th>
<th class="sortable" data-key="clicks" data-type="num">clicks<span class="arrow"></span></th>
<th class="sortable" data-key="conversion" data-type="num">conversion<span class="arrow"></span></th>
<th class="sortable" data-key="cvr" data-type="num">cvr<span class="arrow"></span></th>
<th class="sortable" data-key="revenue" data-type="num">revenue<span class="arrow"></span></th>
<th class="sortable" data-key="cost" data-type="num">cost<span class="arrow"></span></th>
<th class="sortable" data-key="gp" data-type="num">GP<span class="arrow"></span></th>
<th class="sortable" data-key="ecpc" data-type="num">ecpc<span class="arrow"></span></th>
</tr>
<tr id="totalRow" class="total-row">
<td colspan="7" class="txt">Total</td>
<td id="tClicks"></td>
<td id="tConv"></td>
<td id="tCvr"></td>
<td id="tRev"></td>
<td id="tCost"></td>
<td id="tGp"></td>
<td id="tEcpc"></td>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<script>
var ALL = []; // 全部明细
var SUMMARY = [];
function num(n){ return (n==null||n==='')?'':Number(n).toLocaleString('en-US'); }
function pct(n){ if(n==null||n===''||isNaN(n)) return ''; return (Number(n)*100).toFixed(2)+'%'; }
function money(n){ if(n==null||n===''||isNaN(n)) return ''; return Number(n).toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2}); }
function dec(n,d){ if(n==null||n===''||isNaN(n)) return ''; return Number(n).toFixed(d==null?4:d); }
function dtKey(s){ // "2026/9/9" -> 20260909 数值,便于比较
var p=String(s).split('/'); return parseInt(p[0])*10000+parseInt(p[1])*100+parseInt(p[2]);
}
function dtToInput(s){ var p=String(s).split('/'); return p[0]+'-'+String(p[1]).padStart(2,'0')+'-'+String(p[2]).padStart(2,'0'); }
function inputToKey(v){ if(!v) return null; var p=v.split('-'); return parseInt(p[0])*10000+parseInt(p[1])*100+parseInt(p[2]); }
function renderSummary(){
var tb=document.querySelector('#summaryTable tbody');
if(!SUMMARY.length){ tb.innerHTML='<tr><td colspan="7" class="empty">暂无数据</td></tr>'; return; }
tb.innerHTML = SUMMARY.slice().sort(function(a,b){return dtKey(b.date)-dtKey(a.date);}).map(function(r){
return '<tr><td>'+r.date+'</td>'
+'<td class="sum-metric">'+num(r.raw_orders)+'</td>'
+'<td>'+num(r.pkg)+'</td>'
+'<td class="sum-metric">'+num(r.relay_orders)+'</td>'
+'<td>'+num(r.has_click_orders)+'</td>'
+'<td>'+num(r.no_conv_orders)+'</td>'
+'<td>'+num(r.no_click_orders)+'</td></tr>';
}).join('');
}
function fillSelects(){
var scSet={}, accSet={};
ALL.forEach(function(d){ if(d.second_channel) scSet[d.second_channel]=1; if(d.account) accSet[d.account]=1; });
var scSel=document.getElementById('fSc'), accSel=document.getElementById('fAcc');
Object.keys(scSet).sort().forEach(function(v){ scSel.insertAdjacentHTML('beforeend','<option>'+v+'</option>'); });
Object.keys(accSet).sort().forEach(function(v){ accSel.insertAdjacentHTML('beforeend','<option>'+v+'</option>'); });
}
function currentFilter(d){
var from=inputToKey(document.getElementById('fDtFrom').value);
var to=inputToKey(document.getElementById('fDtTo').value);
var rc=document.getElementById('fRc').value.trim().toLowerCase();
var cid=document.getElementById('fCid').value.trim().toLowerCase();
var sc=document.getElementById('fSc').value;
var acc=document.getElementById('fAcc').value;
var k=dtKey(d.dt);
if(from!=null && k<from) return false;
if(to!=null && k>to) return false;
if(rc && String(d.routing_campaign_id).toLowerCase().indexOf(rc)<0) return false;
if(cid && String(d.campaign_id).toLowerCase().indexOf(cid)<0) return false;
if(sc && d.second_channel!==sc) return false;
if(acc && d.account!==acc) return false;
return true;
}
var SORT_KEY='clicks', SORT_TYPE='num', SORT_DIR='desc';
function sortRows(rows){
var k=SORT_KEY, type=SORT_TYPE, dir=(SORT_DIR==='asc'?1:-1);
return rows.sort(function(a,b){
var va,vb;
if(type==='dt'){ va=dtKey(a.dt); vb=dtKey(b.dt); }
else if(type==='num'){ va=Number(a[k])||0; vb=Number(b[k])||0; }
else { va=String(a[k]||''); vb=String(b[k]||''); return dir*va.localeCompare(vb); }
if(va<vb) return -1*dir; if(va>vb) return 1*dir;
// 次级排序:dt 降序、clicks 降序,保持稳定观感
return dtKey(b.dt)-dtKey(a.dt) || (b.clicks-a.clicks);
});
}
function updateSortArrows(){
document.querySelectorAll('#detailTable th.sortable').forEach(function(th){
th.classList.remove('sort-asc','sort-desc');
if(th.getAttribute('data-key')===SORT_KEY){
th.classList.add(SORT_DIR==='asc'?'sort-asc':'sort-desc');
}
});
}
function renderDetail(){
var rows = sortRows(ALL.filter(currentFilter));
var tb=document.querySelector('#detailTable tbody');
if(!rows.length){ tb.innerHTML='<tr><td colspan="14" class="empty">无匹配数据</td></tr>'; }
else {
tb.innerHTML = rows.map(function(d){
var gpCls = (d.gp>=0)?'pos':'neg';
return '<tr>'
+'<td>'+d.dt+'</td>'
+'<td>'+d.routing_campaign_id+'</td>'
+'<td>'+d.campaign_id+'</td>'
+'<td>'+d.adping_id+'</td>'
+'<td class="txt">'+d.second_channel+'</td>'
+'<td class="txt">'+d.account+'</td>'
+'<td class="txt">'+d.pkg+'</td>'
+'<td>'+num(d.clicks)+'</td>'
+'<td>'+num(d.conversion)+'</td>'
+'<td>'+pct(d.cvr)+'</td>'
+'<td>'+money(d.revenue)+'</td>'
+'<td>'+money(d.cost)+'</td>'
+'<td class="'+gpCls+'">'+pct(d.gp)+'</td>'
+'<td>'+dec(d.ecpc,4)+'</td>'
+'</tr>';
}).join('');
}
document.getElementById('detailNote').textContent='显示 '+rows.length+' / '+ALL.length+' 行';
renderTotal(rows);
updateSortArrows();
}
function renderTotal(rows){
var c=0,cv=0,rev=0,cost=0;
rows.forEach(function(d){ c+=(+d.clicks||0); cv+=(+d.conversion||0); rev+=(+d.revenue||0); cost+=(+d.cost||0); });
var cvr = c? cv/c : 0;
var gp = rev? 1-cost/rev : 0;
var ecpc= c? rev/c : 0;
document.getElementById('tClicks').textContent=num(c);
document.getElementById('tConv').textContent=num(cv);
document.getElementById('tCvr').textContent=pct(cvr);
document.getElementById('tRev').textContent=money(rev);
document.getElementById('tCost').textContent=money(cost);
var tGp=document.getElementById('tGp');
tGp.textContent=pct(gp); tGp.className=(gp>=0)?'pos':'neg';
document.getElementById('tEcpc').textContent=dec(ecpc,4);
}
document.querySelectorAll('#detailTable th.sortable').forEach(function(th){
th.addEventListener('click',function(){
var k=th.getAttribute('data-key'), type=th.getAttribute('data-type');
if(SORT_KEY===k){ SORT_DIR=(SORT_DIR==='asc'?'desc':'asc'); }
else { SORT_KEY=k; SORT_TYPE=type; SORT_DIR='desc'; }
renderDetail();
});
});
document.getElementById('applyBtn').onclick=renderDetail;
document.getElementById('resetBtn').onclick=function(){
['fDtFrom','fDtTo','fRc','fCid'].forEach(function(id){document.getElementById(id).value='';});
document.getElementById('fSc').value=''; document.getElementById('fAcc').value='';
renderDetail();
};
fetch('data.json?_='+Date.now()).then(function(r){return r.json();}).then(function(d){
SUMMARY = d.summary||[];
ALL = d.detail||[];
document.getElementById('meta').textContent='数据更新时间:'+(d.updated_at||'—')+' | 总表 '+SUMMARY.length+' 天 | 明细 '+ALL.length+' 行';
renderSummary();
fillSelects();
// 默认 dt 范围 = 数据里的最早~最新
if(ALL.length){
var ks=ALL.map(function(x){return x.dt;}).sort(function(a,b){return dtKey(a)-dtKey(b);});
document.getElementById('fDtFrom').value=dtToInput(ks[0]);
document.getElementById('fDtTo').value=dtToInput(ks[ks.length-1]);
}
renderDetail();
}).catch(function(e){
document.getElementById('meta').textContent='数据加载失败:'+e;
});
</script>
</body>
</html>