DSP-daily 日报: 总日报+PKG波动+客户分类

This commit is contained in:
2026-09-11 16:41:53 +08:00
commit 0cd8d81f47
4 changed files with 545 additions and 0 deletions
+261
View File
@@ -0,0 +1,261 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DSP Daily 日报</title>
<style>
:root{--bg:#f5f6f8;--card:#fff;--line:#e3e6ea;--head:#1f2937;--sub:#6b7280;--accent:#2563eb;}
*{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:1600px;margin:0 auto;padding:22px 18px 60px;}
h1{font-size:20px;margin:0 0 4px;}
.meta{color:var(--sub);font-size:12px;margin-bottom:16px;}
.tabs{display:flex;gap:6px;margin-bottom:18px;flex-wrap:wrap;}
.tab{padding:8px 16px;border:1px solid var(--line);border-radius:8px;background:#fff;cursor:pointer;font-size:13px;}
.tab.active{background:var(--accent);color:#fff;border-color:var(--accent);}
.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;}
.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;margin-left:6px;}
.scroll{overflow:auto;max-height:640px;border:1px solid var(--line);border-radius:8px;}
table{border-collapse:collapse;width:100%;font-variant-numeric:tabular-nums;}
th,td{border:1px solid var(--line);padding:6px 9px;text-align:right;white-space:nowrap;}
th{background:var(--head);color:#fff;font-weight:600;text-align:center;position:sticky;top:0;z-index:2;}
td.txt,th.txt{text-align:left;}
tbody tr:nth-child(even){background:#fafbfc;}
tbody tr:hover{background:#eff4ff;}
.pos{color:#059669;} .neg{color:#dc2626;}
.hide{display:none;}
.reason-inp{width:260px;border:1px solid var(--line);border-radius:5px;padding:4px 6px;font-size:12px;}
.sticky-col{position:sticky;left:0;background:#fff;z-index:1;text-align:left;}
tbody tr:nth-child(even) .sticky-col{background:#fafbfc;}
.dod-hi-pos{background:#dcfce7;color:#059669;font-weight:600;}
.dod-hi-neg{background:#fee2e2;color:#dc2626;font-weight:600;}
.sub-note{color:var(--sub);font-size:12px;margin:4px 2px 10px;}
.total-row td{font-weight:700;background:#eef2f7;}
</style>
</head>
<body>
<div class="wrap">
<h1>DSP Daily 日报</h1>
<div class="meta" id="meta">加载中…</div>
<div class="tabs">
<div class="tab active" data-tab="overview">总日报</div>
<div class="tab" data-tab="pkg">PKG 收入波动</div>
<div class="tab" data-tab="cust">客户分类</div>
</div>
<!-- ===== 总日报 ===== -->
<div id="tab-overview">
<div class="card">
<h2>1.1 Overall <span class="tag">持续叠加</span></h2>
<div class="scroll"><table id="tOverall"><thead><tr>
<th>dt</th><th>DSP total revenue</th><th>DSP CN revenue</th><th>DSP international</th><th>波动(与前一天total差)</th>
</tr></thead><tbody></tbody></table></div>
</div>
<div class="card">
<h2>1.2 CN <span class="tag">持续叠加 · 各account收入</span></h2>
<div class="scroll"><table id="tCN"><thead></thead><tbody></tbody></table></div>
</div>
<div class="card">
<h2>1.3 International <span class="tag">持续叠加 · 各account收入</span></h2>
<div class="scroll"><table id="tIntl"><thead></thead><tbody></tbody></table></div>
</div>
</div>
<!-- ===== PKG 收入波动 ===== -->
<div id="tab-pkg" class="hide">
<div class="card">
<h2>2.1 PKG 收入波动 - Midas <span class="tag">保留14天</span></h2>
<div class="sub-note">DoD=最新日与前一日收入差;|DoD|&gt;100 标注,+绿 −红。波动原因可编辑(暂存本浏览器)。</div>
<div class="scroll"><table id="tPkgMidas"><thead></thead><tbody></tbody></table></div>
</div>
<div class="card">
<h2>2.2 PKG 收入波动 - MS <span class="tag">保留14天</span></h2>
<div class="sub-note">DoD=最新日与前一日收入差;|DoD|&gt;100 标注,+绿 −红。波动原因可编辑(暂存本浏览器)。</div>
<div class="scroll"><table id="tPkgMS"><thead></thead><tbody></tbody></table></div>
</div>
</div>
<!-- ===== 客户分类 ===== -->
<div id="tab-cust" class="hide">
<div class="card">
<h2>3.1 客户分类日报</h2>
<div class="sub-note">DoD=T-1−T-2,DoD%=T-1/T-2−1。波动原因可编辑(暂存本浏览器)。</div>
<div class="scroll"><table id="tCustDaily"><thead><tr>
<th class="txt">客户分类</th><th>iDSP个数</th><th>base daily revenue</th>
<th>T-4收入</th><th>T-3收入</th><th>T-2收入</th><th>T-1收入</th><th>DoD</th><th>DoD%</th><th class="txt">波动原因</th>
</tr></thead><tbody></tbody></table></div>
</div>
<div class="card">
<h2>3.2 客户明细 <span class="tag">保留14天</span></h2>
<div class="scroll"><table id="tCustDetail"><thead><tr>
<th class="txt">客户分类</th><th class="txt">客户</th><th>base收入</th><th>T-1收入</th><th>DoD(与T-2差)</th>
</tr></thead><tbody></tbody></table></div>
</div>
</div>
</div>
<script>
var D=null;
function n2(x){ if(x==null||x===''||isNaN(x))return''; return Number(x).toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2}); }
function pct(x){ if(x==null||x===''||isNaN(x))return''; return (Number(x)*100).toFixed(2)+'%'; }
function dkey(s){ var p=String(s).split('/'); return parseInt(p[0])*10000+parseInt(p[1])*100+parseInt(p[2]); }
function sortedDates(obj){ return Object.keys(obj).sort(function(a,b){return dkey(a)-dkey(b);}); }
// 波动原因本地持久化
function noteKey(tbl,id){ return 'dspdaily_note::'+tbl+'::'+id; }
function getNote(tbl,id){ try{return localStorage.getItem(noteKey(tbl,id))||'';}catch(e){return'';} }
function setNote(tbl,id,v){ try{localStorage.setItem(noteKey(tbl,id),v);}catch(e){} }
function reasonCell(tbl,id){
var v=getNote(tbl,id).replace(/"/g,'&quot;');
return '<td class="txt"><input class="reason-inp" data-tbl="'+tbl+'" data-id="'+id+'" value="'+v+'" placeholder="点击填写"></td>';
}
// ===== Tab 切换 =====
document.querySelectorAll('.tab').forEach(function(t){
t.onclick=function(){
document.querySelectorAll('.tab').forEach(function(x){x.classList.remove('active');});
t.classList.add('active');
['overview','pkg','cust'].forEach(function(name){
document.getElementById('tab-'+name).classList.toggle('hide', name!==t.getAttribute('data-tab'));
});
};
});
// ===== 1.1 Overall =====
function renderOverall(){
var rows=D.overall.slice().sort(function(a,b){return dkey(a.dt)-dkey(b.dt);});
var tb=document.querySelector('#tOverall tbody');
var html='';
for(var i=rows.length-1;i>=0;i--){
var r=rows[i]; var prev=rows[i-1];
var wave = prev? (r['DSP total revenue']-prev['DSP total revenue']) : null;
var wc = wave==null?'':(wave>=0?'pos':'neg');
html+='<tr><td>'+r.dt+'</td><td>'+n2(r['DSP total revenue'])+'</td><td>'+n2(r['DSP CN revenue'])+'</td><td>'+n2(r['DSP international'])+'</td>'
+'<td class="'+wc+'">'+(wave==null?'—':n2(wave))+'</td></tr>';
}
tb.innerHTML=html;
}
// ===== 1.2/1.3 CN & International (dt为行, account为列; 最新在上) =====
function renderAccTable(tblId, rowsData, totalKey){
var rows=rowsData.slice().sort(function(a,b){return dkey(b.dt)-dkey(a.dt);});
if(!rows.length) return;
// 列 = dt + totalKey + 所有account(除dt/totalKey)
var accs=Object.keys(rows[0]).filter(function(k){return k!=='dt'&&k!==totalKey;});
var thead=document.querySelector('#'+tblId+' thead');
var th='<tr><th>dt</th><th>'+totalKey+'</th>';
accs.forEach(function(a){ th+='<th>'+a+'</th>'; });
th+='</tr>'; thead.innerHTML=th;
var body='';
rows.forEach(function(r){
body+='<tr><td>'+r.dt+'</td><td class="pos">'+n2(r[totalKey])+'</td>';
accs.forEach(function(a){ body+='<td>'+n2(r[a])+'</td>'; });
body+='</tr>';
});
document.querySelector('#'+tblId+' tbody').innerHTML=body;
}
// ===== 2.1/2.2 PKG波动 (pkg为行, dt为列, 末列DoD+波动原因) =====
function renderPkg(tblId, byDt, noteTbl){
var dts=sortedDates(byDt); // 升序
var showDts=dts.slice(-14);
var latest=showDts[showDts.length-1], prev=showDts[showDts.length-2];
// 收集所有pkg
var pkgset={};
showDts.forEach(function(dt){ Object.keys(byDt[dt]).forEach(function(p){pkgset[p]=1;}); });
var pkgs=Object.keys(pkgset);
// 按最新日收入降序
pkgs.sort(function(a,b){ return (byDt[latest][b]||0)-(byDt[latest][a]||0); });
var thead=document.querySelector('#'+tblId+' thead');
var th='<tr><th class="txt sticky-col">PKG</th>';
showDts.forEach(function(dt){ th+='<th>'+dt+'</th>'; });
th+='<th>DoD</th><th class="txt">波动原因</th></tr>';
thead.innerHTML=th;
var body='';
pkgs.forEach(function(p){
var row='<tr><td class="txt sticky-col">'+p+'</td>';
showDts.forEach(function(dt){ var v=byDt[dt][p]; row+='<td>'+(v==null?'':n2(v))+'</td>'; });
var lv=byDt[latest]&&byDt[latest][p]||0, pv=(prev&&byDt[prev]&&byDt[prev][p])||0;
var dod=lv-pv;
var cls='';
if(Math.abs(dod)>100){ cls = dod>0?'dod-hi-pos':'dod-hi-neg'; }
else { cls = dod>=0?'pos':'neg'; }
row+='<td class="'+cls+'">'+n2(dod)+'</td>';
row+=reasonCell(noteTbl, p);
row+='</tr>';
body+=row;
});
document.querySelector('#'+tblId+' tbody').innerHTML=body;
}
// ===== 3.1 客户分类日报 =====
function renderCustDaily(){
var dts=sortedDates(D.cust_daily); // 升序
var t1=dts[dts.length-1], t2=dts[dts.length-2], t3=dts[dts.length-3], t4=dts[dts.length-4];
var cats=Object.keys(D.cust_idsp);
// base = 该分类各account base之和
var baseByCat={};
(D.cust_base||[]).forEach(function(x){ baseByCat[x.cat]=(baseByCat[x.cat]||0)+(+x.base||0); });
var body='';
cats.forEach(function(cat){
function val(dt){ return dt&&D.cust_daily[dt]? (D.cust_daily[dt][cat]||0):null; }
var v1=val(t1),v2=val(t2),v3=val(t3),v4=val(t4);
var dod = (v1!=null&&v2!=null)? v1-v2 : null;
var dodp= (v1!=null&&v2)? (v1/v2-1) : null;
var dc = dod==null?'':(dod>=0?'pos':'neg');
body+='<tr><td class="txt">'+cat+'</td><td>'+(D.cust_idsp[cat]||'')+'</td><td>'+n2(baseByCat[cat])+'</td>'
+'<td>'+(v4==null?'':n2(v4))+'</td><td>'+(v3==null?'':n2(v3))+'</td><td>'+(v2==null?'':n2(v2))+'</td>'
+'<td class="pos">'+(v1==null?'':n2(v1))+'</td>'
+'<td class="'+dc+'">'+(dod==null?'':n2(dod))+'</td>'
+'<td class="'+dc+'">'+(dodp==null?'':pct(dodp))+'</td>'
+reasonCell('cust_daily', cat)+'</tr>';
});
document.querySelector('#tCustDaily tbody').innerHTML=body;
}
// ===== 3.2 客户明细 =====
function renderCustDetail(){
var dts=sortedDates(D.cust_detail);
var t1=dts[dts.length-1], t2=dts[dts.length-2];
var t1list=D.cust_detail[t1]||[];
var t2map={};
(D.cust_detail[t2]||[]).forEach(function(x){ t2map[x.cat+'||'+x.acc]=x.t1; });
// 按分类、再按T-1收入降序
var rows=t1list.slice().sort(function(a,b){ return a.cat<b.cat?-1:a.cat>b.cat?1:(b.t1-a.t1); });
var body='';
rows.forEach(function(r){
var prev=t2map[r.cat+'||'+r.acc];
var dod = prev!=null? r.t1-prev : null;
var dc=dod==null?'':(dod>=0?'pos':'neg');
body+='<tr><td class="txt">'+r.cat+'</td><td class="txt">'+r.acc+'</td><td>'+n2(r.base)+'</td>'
+'<td>'+n2(r.t1)+'</td><td class="'+dc+'">'+(dod==null?'':n2(dod))+'</td></tr>';
});
document.querySelector('#tCustDetail tbody').innerHTML=body;
}
// 波动原因输入监听(事件委托)
document.addEventListener('input',function(e){
var el=e.target;
if(el.classList&&el.classList.contains('reason-inp')){
setNote(el.getAttribute('data-tbl'), el.getAttribute('data-id'), el.value);
}
});
fetch('data.json?_='+Date.now()).then(function(r){return r.json();}).then(function(d){
D=d;
document.getElementById('meta').textContent='数据更新时间:'+(d.updated_at||'—')+' | Overall '+(d.overall||[]).length+' 天';
renderOverall();
renderAccTable('tCN', d.cn, 'DSP CN revenue');
renderAccTable('tIntl', d.international, 'DSP international');
renderPkg('tPkgMidas', d.pkg_midas, 'midas');
renderPkg('tPkgMS', d.pkg_ms, 'ms');
renderCustDaily();
renderCustDetail();
}).catch(function(e){
document.getElementById('meta').textContent='数据加载失败:'+e;
});
</script>
</body>
</html>