284 lines
14 KiB
HTML
284 lines
14 KiB
HTML
<!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>pkg</label><input type="text" id="fPkg" 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 pkg=document.getElementById('fPkg').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(pkg && String(d.pkg).toLowerCase().indexOf(pkg)<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','fPkg'].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>
|