Files
DSP-daily/index.html
T

262 lines
12 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.
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.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>