518 lines
26 KiB
HTML
518 lines
26 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 Daily 日报</title>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
|
||
/* roasbloom Brand System v1.0 */
|
||
:root{
|
||
--rose:#C4506A;--rose-50:#FDF5F6;--rose-100:#F9E0E5;--rose-200:#F0C2CC;--rose-600:#A83D55;--rose-700:#8C2D43;
|
||
--ink:#2D1B20;--ink-80:#57484C;--ink-60:#827578;--ink-40:#ADA2A4;--ink-20:#D8D0D2;--ink-10:#EBE7E8;--ink-5:#F5F3F4;
|
||
--success:#4A9E6B;--warning:#D4914A;--danger:#C4506A;
|
||
--font-brand:'DM Sans',sans-serif;--font-body:'Inter','PingFang SC','Microsoft YaHei',sans-serif;--font-mono:'JetBrains Mono',monospace;
|
||
--bg:#F5F3F4;--card:#FFFFFF;--line:#EBE7E8;--head:#4E1222;--sub:#827578;--accent:#C4506A;
|
||
}
|
||
*{box-sizing:border-box;}
|
||
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--font-body);font-size:13px;}
|
||
.wrap{max-width:1600px;margin:0 auto;padding:22px 18px 60px;}
|
||
h1{font-family:var(--font-brand);font-size:20px;font-weight:600;margin:0 0 4px;color:var(--ink);}
|
||
.meta{color:var(--ink-60);font-size:12px;margin-bottom:16px;}
|
||
.logo{font-family:var(--font-brand);font-weight:700;font-size:22px;letter-spacing:-.5px;display:inline-flex;align-items:center;margin-bottom:2px;}
|
||
.logo .roas{color:var(--rose);} .logo .bloom{color:var(--ink);}
|
||
.logo .ring{display:inline-block;width:.6em;height:.6em;border:.18em solid var(--rose);border-radius:50%;margin:0 .02em;position:relative;top:.05em;}
|
||
.tabs{display:flex;gap:6px;margin-bottom:18px;flex-wrap:wrap;}
|
||
.tab{padding:8px 16px;border:1px solid var(--ink-10);border-radius:8px;background:#fff;cursor:pointer;font-size:13px;font-family:var(--font-brand);font-weight:500;color:var(--ink-80);}
|
||
.tab.active{background:var(--rose);color:#fff;border-color:var(--rose);}
|
||
.card{background:var(--card);border:1px solid var(--ink-10);border-radius:10px;padding:16px 18px;margin-bottom:22px;box-shadow:0 1px 2px rgba(45,27,32,.04);}
|
||
.card h2{font-family:var(--font-brand);font-size:15px;font-weight:600;margin:0 0 12px;color:var(--ink);}
|
||
.card h2 .tag{font-family:var(--font-body);font-size:11px;font-weight:normal;color:var(--ink-60);background:var(--ink-5);border:1px solid var(--ink-10);padding:1px 8px;border-radius:20px;margin-left:6px;}
|
||
.scroll{overflow:auto;max-height:640px;border:1px solid var(--ink-10);border-radius:8px;}
|
||
table{border-collapse:collapse;width:100%;}
|
||
th,td{border:1px solid var(--ink-10);padding:6px 9px;text-align:right;white-space:nowrap;}
|
||
td{font-family:var(--font-mono);font-variant-numeric:tabular-nums;}
|
||
td.txt{font-family:var(--font-body);}
|
||
th{background:var(--head);color:#fff;font-weight:600;text-align:center;position:sticky;top:0;z-index:2;font-family:var(--font-brand);}
|
||
td.txt,th.txt{text-align:left;}
|
||
tbody tr:nth-child(even){background:var(--ink-5);}
|
||
tbody tr:hover{background:var(--rose-50);}
|
||
.pos{color:var(--success);} .neg{color:var(--danger);}
|
||
.hide{display:none;}
|
||
.reason-inp{width:260px;border:1px solid var(--ink-20);border-radius:5px;padding:4px 6px;font-size:12px;font-family:var(--font-body);}
|
||
.sticky-col{position:sticky;left:0;background:#fff;z-index:1;text-align:left;font-family:var(--font-body);}
|
||
tbody tr:nth-child(even) .sticky-col{background:var(--ink-5);}
|
||
.dod-hi-pos{background:var(--rose-50);color:var(--success);font-weight:600;}
|
||
.dod-hi-neg{background:var(--rose-100);color:var(--danger);font-weight:600;}
|
||
.sub-note{color:var(--ink-60);font-size:12px;margin:4px 2px 10px;}
|
||
.src-alert{background:#FBF0E4;border:1px solid var(--warning);color:#8a5a1f;border-radius:8px;padding:10px 14px;margin-bottom:14px;font-size:13px;font-weight:500;}
|
||
.src-alert b{color:var(--warning);}
|
||
.total-row td{font-weight:700;background:var(--rose-50);}
|
||
.ctrl{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:10px;}
|
||
.ctrl label{font-size:13px;cursor:pointer;user-select:none;color:var(--ink-80);}
|
||
.ctrl input[type=text],.ctrl select{border:1px solid var(--ink-20);border-radius:6px;padding:5px 8px;font-size:12px;min-width:150px;font-family:var(--font-body);}
|
||
th.sortable,th.cd-sort,th.de-sort{cursor:pointer;user-select:none;}
|
||
th.sortable:hover,th.cd-sort:hover,th.de-sort:hover{background:var(--rose-700);}
|
||
th .arw{display:inline-block;width:9px;margin-left:3px;font-size:10px;color:var(--rose-200);}
|
||
th.asc .arw::after{content:"\25B2";} th.desc .arw::after{content:"\25BC";}
|
||
tr.acc-sub td{background:var(--rose-50);color:var(--ink-80);font-size:12px;}
|
||
tr.acc-sub td.txt{padding-left:26px;}
|
||
tr.acc-sub:hover td{background:var(--rose-100);}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<div class="logo"><span class="roas">roas</span><span class="bloom">bl</span><span class="ring"></span><span class="bloom">m</span></div>
|
||
<h1>DSP Daily 日报</h1>
|
||
<div class="meta" id="meta">加载中…</div>
|
||
<div id="srcAlert" class="src-alert hide"></div>
|
||
<div class="tabs">
|
||
<div class="tab active" data-tab="overview">总日报</div>
|
||
<div class="tab" data-tab="cust">客户分类</div>
|
||
<div class="tab" data-tab="pkg">PKG 收入波动</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 收入波动 ===== -->
|
||
<!-- ===== 客户分类(板块2) ===== -->
|
||
<div id="tab-cust" class="hide">
|
||
<div class="card">
|
||
<h2>2.1 客户分类日报</h2>
|
||
<div class="sub-note">日期列为各日收入;DoD=最新日−前一日,DoD%=最新日/前一日−1。波动原因可编辑(暂存本浏览器)。</div>
|
||
<div class="scroll"><table id="tCustDaily"><thead></thead><tbody></tbody></table></div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>2.2 客户明细 <span class="tag">保留14天</span></h2>
|
||
<div class="sub-note">日期列为各客户各日收入;DoD=最新日−前一日。</div>
|
||
<div class="ctrl">
|
||
<label>客户分类 <select id="deCat"><option value="">全部</option></select></label>
|
||
<input type="text" id="deAcc" placeholder="筛选客户">
|
||
</div>
|
||
<div class="scroll"><table id="tCustDetail"><thead></thead><tbody></tbody></table></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== PKG 收入波动(板块3) ===== -->
|
||
<div id="tab-pkg" class="hide">
|
||
<div class="card">
|
||
<h2>3.1 PKG 收入波动 - Midas <span class="tag">保留14天</span></h2>
|
||
<div class="sub-note">|DoD|>100 标注(+绿−红);表头点击排序;波动原因暂存本浏览器。</div>
|
||
<div class="ctrl">
|
||
<label><input type="checkbox" class="m-pkg" data-t="midas" checked> 按 pkg</label>
|
||
<label><input type="checkbox" class="m-acc" data-t="midas"> 按 account</label>
|
||
<input type="text" class="f-pkg" data-t="midas" placeholder="筛选 pkg">
|
||
<input type="text" class="f-acc" data-t="midas" placeholder="筛选 account">
|
||
</div>
|
||
<div class="scroll"><table id="tPkgMidas"><thead></thead><tbody></tbody></table></div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>3.2 PKG 收入波动 - MS <span class="tag">保留14天</span></h2>
|
||
<div class="sub-note">|DoD|>100 标注(+绿−红);表头点击排序;波动原因暂存本浏览器。</div>
|
||
<div class="ctrl">
|
||
<label><input type="checkbox" class="m-pkg" data-t="ms" checked> 按 pkg</label>
|
||
<label><input type="checkbox" class="m-acc" data-t="ms"> 按 account</label>
|
||
<input type="text" class="f-pkg" data-t="ms" placeholder="筛选 pkg">
|
||
<input type="text" class="f-acc" data-t="ms" placeholder="筛选 account">
|
||
</div>
|
||
<div class="scroll"><table id="tPkgMS"><thead></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,'"');
|
||
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]!=null?r[a]:0)+'</td>'; }); // 缺失/找不到显示0
|
||
body+='</tr>';
|
||
});
|
||
document.querySelector('#'+tblId+' tbody').innerHTML=body;
|
||
}
|
||
|
||
// ===== 2.1/2.2 PKG波动 =====
|
||
// byDt: {dt: {pkg: {account: rev}}}
|
||
// 模式: byPkg / byAcc 两个复选框;排序: 按 latest(最新日收入) 或 dod
|
||
var PKG_STATE={
|
||
midas:{tbl:'tPkgMidas', note:'midas', byPkg:true, byAcc:false, fPkg:'', fAcc:'', sort:'latest', dir:'desc'},
|
||
ms:{tbl:'tPkgMS', note:'ms', byPkg:true, byAcc:false, fPkg:'', fAcc:'', sort:'latest', dir:'desc'}
|
||
};
|
||
|
||
function pkgDatesFor(byDt){ return sortedDates(byDt).slice(-14); }
|
||
|
||
// 聚合出:pkg -> {dt: total}, pkg -> {acc: {dt: rev}}
|
||
function buildPkgIndex(byDt, showDts){
|
||
var pkgTotal={}; // pkg -> {dt: sum}
|
||
var pkgAcc={}; // pkg -> {acc: {dt: rev}}
|
||
showDts.forEach(function(dt){
|
||
var m=byDt[dt]||{};
|
||
Object.keys(m).forEach(function(pkg){
|
||
var accs=m[pkg]||{};
|
||
Object.keys(accs).forEach(function(acc){
|
||
var v=accs[acc]||0;
|
||
(pkgTotal[pkg]=pkgTotal[pkg]||{})[dt]=((pkgTotal[pkg][dt]||0)+v);
|
||
((pkgAcc[pkg]=pkgAcc[pkg]||{})[acc]=pkgAcc[pkg][acc]||{})[dt]=v;
|
||
});
|
||
});
|
||
});
|
||
return {pkgTotal:pkgTotal, pkgAcc:pkgAcc};
|
||
}
|
||
// account视角: acc -> {dt: sum(over pkg)}
|
||
function buildAccIndex(byDt, showDts){
|
||
var accTotal={};
|
||
showDts.forEach(function(dt){
|
||
var m=byDt[dt]||{};
|
||
Object.keys(m).forEach(function(pkg){
|
||
var accs=m[pkg]||{};
|
||
Object.keys(accs).forEach(function(acc){
|
||
(accTotal[acc]=accTotal[acc]||{})[dt]=((accTotal[acc][dt]||0)+(accs[acc]||0));
|
||
});
|
||
});
|
||
});
|
||
return accTotal;
|
||
}
|
||
function dodOf(map,latest,prev){ return (map[latest]||0)-(prev?(map[prev]||0):0); }
|
||
function dodCls(dod){ if(Math.abs(dod)>100) return dod>0?'dod-hi-pos':'dod-hi-neg'; return dod>=0?'pos':'neg'; }
|
||
|
||
function renderPkg(which){
|
||
var st=PKG_STATE[which]; var byDt=(which==='midas'?D.pkg_midas:D.pkg_ms);
|
||
var showDts=pkgDatesFor(byDt);
|
||
var latest=showDts[showDts.length-1], prev=showDts[showDts.length-2];
|
||
var thead=document.querySelector('#'+st.tbl+' thead');
|
||
var tbody=document.querySelector('#'+st.tbl+' tbody');
|
||
var fp=st.fPkg.trim().toLowerCase(), fa=st.fAcc.trim().toLowerCase();
|
||
|
||
function sortArr(arr, valLatest, valDod){
|
||
var dir=st.dir==='asc'?1:-1;
|
||
arr.sort(function(a,b){
|
||
var va=st.sort==='dod'?valDod(a):valLatest(a);
|
||
var vb=st.sort==='dod'?valDod(b):valLatest(b);
|
||
return (va<vb?-1:va>vb?1:0)*dir;
|
||
});
|
||
return arr;
|
||
}
|
||
function headRow(firstCol){
|
||
var th='<tr><th class="txt sticky-col">'+firstCol+'</th>';
|
||
showDts.forEach(function(dt){ th+='<th>'+dt+'</th>'; });
|
||
var la=(st.sort==='latest'?(' '+st.dir):''), da=(st.sort==='dod'?(' '+st.dir):'');
|
||
th+='<th class="sortable'+la+'" data-sort="latest" data-t="'+which+'">最新收入<span class="arw"></span></th>';
|
||
th+='<th class="sortable'+da+'" data-sort="dod" data-t="'+which+'">DoD<span class="arw"></span></th>';
|
||
th+='<th class="txt">波动原因</th></tr>';
|
||
return th;
|
||
}
|
||
|
||
var html='';
|
||
if(st.byPkg){
|
||
var idx=buildPkgIndex(byDt,showDts);
|
||
var pkgs=Object.keys(idx.pkgTotal);
|
||
if(fp) pkgs=pkgs.filter(function(p){return p.toLowerCase().indexOf(fp)>=0;});
|
||
if(fa) pkgs=pkgs.filter(function(p){ return Object.keys(idx.pkgAcc[p]||{}).some(function(a){return a.toLowerCase().indexOf(fa)>=0;}); });
|
||
thead.innerHTML=headRow('PKG');
|
||
sortArr(pkgs, function(p){return idx.pkgTotal[p][latest]||0;}, function(p){return dodOf(idx.pkgTotal[p],latest,prev);});
|
||
pkgs.forEach(function(p){
|
||
var tot=idx.pkgTotal[p];
|
||
var row='<tr><td class="txt sticky-col">'+p+'</td>';
|
||
showDts.forEach(function(dt){ var v=tot[dt]; row+='<td>'+(v==null?'':n2(v))+'</td>'; });
|
||
var dod=dodOf(tot,latest,prev);
|
||
row+='<td>'+n2(tot[latest]||0)+'</td><td class="'+dodCls(dod)+'">'+n2(dod)+'</td>'+reasonCell(st.note,p)+'</tr>';
|
||
html+=row;
|
||
// 两者都选: 该pkg下各account子行
|
||
if(st.byAcc){
|
||
var accs=idx.pkgAcc[p]||{};
|
||
var alist=Object.keys(accs);
|
||
if(fa) alist=alist.filter(function(a){return a.toLowerCase().indexOf(fa)>=0;});
|
||
sortArr(alist, function(a){return accs[a][latest]||0;}, function(a){return dodOf(accs[a],latest,prev);});
|
||
alist.forEach(function(a){
|
||
var m=accs[a]; var sub='<tr class="acc-sub"><td class="txt">↳ '+a+'</td>';
|
||
showDts.forEach(function(dt){ var v=m[dt]; sub+='<td>'+(v==null?'':n2(v))+'</td>'; });
|
||
var d2=dodOf(m,latest,prev);
|
||
sub+='<td>'+n2(m[latest]||0)+'</td><td class="'+dodCls(d2)+'">'+n2(d2)+'</td><td></td></tr>';
|
||
html+=sub;
|
||
});
|
||
}
|
||
});
|
||
} else if(st.byAcc){
|
||
// 仅按account
|
||
var accTotal=buildAccIndex(byDt,showDts);
|
||
var accs=Object.keys(accTotal);
|
||
if(fa) accs=accs.filter(function(a){return a.toLowerCase().indexOf(fa)>=0;});
|
||
// fp在纯account模式下无意义,忽略
|
||
thead.innerHTML=headRow('account');
|
||
sortArr(accs, function(a){return accTotal[a][latest]||0;}, function(a){return dodOf(accTotal[a],latest,prev);});
|
||
accs.forEach(function(a){
|
||
var tot=accTotal[a];
|
||
var row='<tr><td class="txt sticky-col">'+a+'</td>';
|
||
showDts.forEach(function(dt){ var v=tot[dt]; row+='<td>'+(v==null?'':n2(v))+'</td>'; });
|
||
var dod=dodOf(tot,latest,prev);
|
||
row+='<td>'+n2(tot[latest]||0)+'</td><td class="'+dodCls(dod)+'">'+n2(dod)+'</td>'+reasonCell(st.note+'_acc',a)+'</tr>';
|
||
html+=row;
|
||
});
|
||
} else {
|
||
// 都没勾,默认回到按pkg
|
||
st.byPkg=true; return renderPkg(which);
|
||
}
|
||
tbody.innerHTML=html;
|
||
}
|
||
|
||
// PKG 控制交互
|
||
function bindPkgControls(){
|
||
document.querySelectorAll('.m-pkg').forEach(function(el){ el.onchange=function(){ PKG_STATE[el.getAttribute('data-t')].byPkg=el.checked; renderPkg(el.getAttribute('data-t')); }; });
|
||
document.querySelectorAll('.m-acc').forEach(function(el){ el.onchange=function(){ PKG_STATE[el.getAttribute('data-t')].byAcc=el.checked; renderPkg(el.getAttribute('data-t')); }; });
|
||
document.querySelectorAll('.f-pkg').forEach(function(el){ el.oninput=function(){ PKG_STATE[el.getAttribute('data-t')].fPkg=el.value; renderPkg(el.getAttribute('data-t')); }; });
|
||
document.querySelectorAll('.f-acc').forEach(function(el){ el.oninput=function(){ PKG_STATE[el.getAttribute('data-t')].fAcc=el.value; renderPkg(el.getAttribute('data-t')); }; });
|
||
// 表头排序(事件委托)
|
||
document.addEventListener('click',function(e){
|
||
var th=e.target.closest && e.target.closest('th.sortable');
|
||
if(!th || !th.getAttribute('data-t')) return;
|
||
var which=th.getAttribute('data-t'), s=th.getAttribute('data-sort'), st=PKG_STATE[which];
|
||
if(st.sort===s){ st.dir=(st.dir==='asc'?'desc':'asc'); } else { st.sort=s; st.dir='desc'; }
|
||
renderPkg(which);
|
||
});
|
||
}
|
||
|
||
// ===== 3.1 客户分类日报 =====
|
||
var CD_SORT={k:null,ty:'num',dir:'desc'};
|
||
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);
|
||
var baseByCat={};
|
||
(D.cust_base||[]).forEach(function(x){ baseByCat[x.cat]=(baseByCat[x.cat]||0)+(+x.base||0); });
|
||
var rows=cats.map(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;
|
||
return {cat:cat,idsp:D.cust_idsp[cat]||0,base:baseByCat[cat]||0,t4:v4,t3:v3,t2:v2,t1:v1,dod:dod,dodp:dodp};
|
||
});
|
||
if(CD_SORT.k){
|
||
var dir=CD_SORT.dir==='asc'?1:-1;
|
||
rows.sort(function(a,b){
|
||
if(CD_SORT.ty==='str') return dir*String(a[CD_SORT.k]).localeCompare(String(b[CD_SORT.k]));
|
||
var va=a[CD_SORT.k]==null?-Infinity:a[CD_SORT.k], vb=b[CD_SORT.k]==null?-Infinity:b[CD_SORT.k];
|
||
return (va<vb?-1:va>vb?1:0)*dir;
|
||
});
|
||
}
|
||
// 动态表头:用实际日期替代 T-4~T-1
|
||
function dLbl(dt){ return dt? dt : '—'; }
|
||
var thead=document.querySelector('#tCustDaily thead');
|
||
thead.innerHTML='<tr>'
|
||
+'<th class="txt cd-sort" data-k="cat" data-ty="str">客户分类<span class="arw"></span></th>'
|
||
+'<th class="cd-sort" data-k="idsp" data-ty="num">iDSP个数<span class="arw"></span></th>'
|
||
+'<th class="cd-sort" data-k="base" data-ty="num">base daily revenue<span class="arw"></span></th>'
|
||
+'<th class="cd-sort" data-k="t4" data-ty="num">'+dLbl(t4)+'<span class="arw"></span></th>'
|
||
+'<th class="cd-sort" data-k="t3" data-ty="num">'+dLbl(t3)+'<span class="arw"></span></th>'
|
||
+'<th class="cd-sort" data-k="t2" data-ty="num">'+dLbl(t2)+'<span class="arw"></span></th>'
|
||
+'<th class="cd-sort" data-k="t1" data-ty="num">'+dLbl(t1)+'<span class="arw"></span></th>'
|
||
+'<th class="cd-sort" data-k="dod" data-ty="num">DoD('+dLbl(t1)+'−'+dLbl(t2)+')<span class="arw"></span></th>'
|
||
+'<th class="cd-sort" data-k="dodp" data-ty="num">DoD%<span class="arw"></span></th>'
|
||
+'<th class="txt">波动原因</th></tr>';
|
||
var body=rows.map(function(r){
|
||
var dc=r.dod==null?'':(r.dod>=0?'pos':'neg');
|
||
return '<tr><td class="txt">'+r.cat+'</td><td>'+(r.idsp||'')+'</td><td>'+n2(r.base)+'</td>'
|
||
+'<td>'+(r.t4==null?'':n2(r.t4))+'</td><td>'+(r.t3==null?'':n2(r.t3))+'</td><td>'+(r.t2==null?'':n2(r.t2))+'</td>'
|
||
+'<td class="pos">'+(r.t1==null?'':n2(r.t1))+'</td>'
|
||
+'<td class="'+dc+'">'+(r.dod==null?'':n2(r.dod))+'</td>'
|
||
+'<td class="'+dc+'">'+(r.dodp==null?'':pct(r.dodp))+'</td>'
|
||
+reasonCell('cust_daily', r.cat)+'</tr>';
|
||
}).join('');
|
||
document.querySelector('#tCustDaily tbody').innerHTML=body;
|
||
// 表头动态生成后重新绑定排序点击 + 箭头
|
||
document.querySelectorAll('#tCustDaily th.cd-sort').forEach(function(th){
|
||
th.style.cursor='pointer';
|
||
th.onclick=function(){
|
||
var k=th.getAttribute('data-k'), ty=th.getAttribute('data-ty');
|
||
if(CD_SORT.k===k){ CD_SORT.dir=(CD_SORT.dir==='asc'?'desc':'asc'); } else { CD_SORT.k=k; CD_SORT.ty=ty; CD_SORT.dir='desc'; }
|
||
renderCustDaily();
|
||
};
|
||
th.classList.remove('asc','desc');
|
||
if(th.getAttribute('data-k')===CD_SORT.k) th.classList.add(CD_SORT.dir);
|
||
});
|
||
}
|
||
|
||
// ===== 3.2 客户明细 =====
|
||
var DE_SORT={k:null,ty:'num',dir:'desc'};
|
||
function renderCustDetail(){
|
||
var dts=sortedDates(D.cust_detail).slice(-14); // 升序, 最近14天
|
||
var latest=dts[dts.length-1], prev=dts[dts.length-2];
|
||
// 按 (cat,acc) 聚合各天收入 + base
|
||
var map={}; // key -> {cat,acc,base,byDt:{}}
|
||
dts.forEach(function(dt){
|
||
(D.cust_detail[dt]||[]).forEach(function(x){
|
||
var k=x.cat+'||'+x.acc;
|
||
if(!map[k]) map[k]={cat:x.cat,acc:x.acc,base:x.base,byDt:{}};
|
||
map[k].byDt[dt]=x.t1;
|
||
if(x.base!=null) map[k].base=x.base;
|
||
});
|
||
});
|
||
var fCat=(document.getElementById('deCat')||{}).value||'';
|
||
var fAcc=((document.getElementById('deAcc')||{}).value||'').trim().toLowerCase();
|
||
var rows=Object.keys(map).map(function(k){
|
||
var r=map[k];
|
||
var lv=r.byDt[latest], pv=prev?r.byDt[prev]:null;
|
||
r.dod=(lv!=null&&pv!=null)?lv-pv:(lv!=null?lv:null);
|
||
r.latest=lv;
|
||
return r;
|
||
}).filter(function(r){
|
||
if(fCat && r.cat!==fCat) return false;
|
||
if(fAcc && String(r.acc).toLowerCase().indexOf(fAcc)<0) return false;
|
||
return true;
|
||
});
|
||
// 排序
|
||
if(DE_SORT.k){
|
||
var dir=DE_SORT.dir==='asc'?1:-1;
|
||
rows.sort(function(a,b){
|
||
var va,vb;
|
||
if(DE_SORT.k==='cat'||DE_SORT.k==='acc'){ return dir*String(a[DE_SORT.k]).localeCompare(String(b[DE_SORT.k])); }
|
||
if(DE_SORT.k==='base'){ va=a.base||0; vb=b.base||0; }
|
||
else if(DE_SORT.k==='dod'){ va=a.dod==null?-Infinity:a.dod; vb=b.dod==null?-Infinity:b.dod; }
|
||
else { va=a.byDt[DE_SORT.k]==null?-Infinity:a.byDt[DE_SORT.k]; vb=b.byDt[DE_SORT.k]==null?-Infinity:b.byDt[DE_SORT.k]; } // 某日期列
|
||
return (va<vb?-1:va>vb?1:0)*dir;
|
||
});
|
||
} else {
|
||
rows.sort(function(a,b){ return a.cat<b.cat?-1:a.cat>b.cat?1:((b.latest||0)-(a.latest||0)); });
|
||
}
|
||
// 动态表头:客户分类 | 客户 | base | 各日期列 | DoD
|
||
var thead=document.querySelector('#tCustDetail thead');
|
||
var th='<tr><th class="txt de-sort" data-k="cat" data-ty="str">客户分类<span class="arw"></span></th>'
|
||
+'<th class="txt de-sort" data-k="acc" data-ty="str">客户<span class="arw"></span></th>'
|
||
+'<th class="de-sort" data-k="base" data-ty="num">base收入<span class="arw"></span></th>';
|
||
dts.forEach(function(dt){ th+='<th class="de-sort" data-k="'+dt+'" data-ty="num">'+dt+'<span class="arw"></span></th>'; });
|
||
th+='<th class="de-sort" data-k="dod" data-ty="num">DoD<span class="arw"></span></th></tr>';
|
||
thead.innerHTML=th;
|
||
var body=rows.map(function(r){
|
||
var s='<tr><td class="txt">'+r.cat+'</td><td class="txt">'+r.acc+'</td><td>'+n2(r.base)+'</td>';
|
||
dts.forEach(function(dt){ var v=r.byDt[dt]; s+='<td>'+(v==null?'':n2(v))+'</td>'; });
|
||
var dc=r.dod==null?'':(r.dod>=0?'pos':'neg');
|
||
s+='<td class="'+dc+'">'+(r.dod==null?'':n2(r.dod))+'</td></tr>';
|
||
return s;
|
||
}).join('');
|
||
document.querySelector('#tCustDetail tbody').innerHTML=body;
|
||
document.querySelectorAll('#tCustDetail th.de-sort').forEach(function(th){
|
||
th.style.cursor='pointer';
|
||
th.onclick=function(){
|
||
var k=th.getAttribute('data-k'), ty=th.getAttribute('data-ty');
|
||
if(DE_SORT.k===k){ DE_SORT.dir=(DE_SORT.dir==='asc'?'desc':'asc'); } else { DE_SORT.k=k; DE_SORT.ty=ty; DE_SORT.dir='desc'; }
|
||
renderCustDetail();
|
||
};
|
||
th.classList.remove('asc','desc');
|
||
if(th.getAttribute('data-k')===DE_SORT.k) th.classList.add(DE_SORT.dir);
|
||
});
|
||
}
|
||
|
||
// 3.1/3.2 表头排序绑定
|
||
function bindCustSort(){
|
||
// 3.1/3.2 表头排序绑定均在各自 render 内部动态处理(表头动态生成)
|
||
// 3.2 筛选: 填充客户分类下拉 + 绑定
|
||
var sel=document.getElementById('deCat');
|
||
if(sel && D && D.cust_idsp){
|
||
Object.keys(D.cust_idsp).forEach(function(c){ sel.insertAdjacentHTML('beforeend','<option>'+c+'</option>'); });
|
||
}
|
||
if(sel) sel.onchange=renderCustDetail;
|
||
var accInp=document.getElementById('deAcc');
|
||
if(accInp) accInp.oninput=renderCustDetail;
|
||
}
|
||
|
||
// 波动原因输入监听(事件委托)
|
||
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+' 天';
|
||
// 数据源状态告警
|
||
(function(){
|
||
var ss=d.source_status||{}; var msgs=[];
|
||
if(ss.report && String(ss.report).indexOf('fail')===0) msgs.push('MS 数据源(report/v2/list)凭证失效 —— MS 表与带 _API 的 account 数据未更新,请更新 MS token');
|
||
if(ss.superset && String(ss.superset).indexOf('fail')===0) msgs.push('Superset 数据源凭证失效 —— Midas 表与无 _API 的 account 数据未更新,请更新 Superset cookie');
|
||
var el=document.getElementById('srcAlert');
|
||
if(msgs.length){ el.innerHTML='<b>⚠ 数据源告警:</b>'+msgs.join(';')+'。'; el.classList.remove('hide'); }
|
||
else { el.classList.add('hide'); }
|
||
})();
|
||
renderOverall();
|
||
renderAccTable('tCN', d.cn, 'DSP CN revenue');
|
||
renderAccTable('tIntl', d.international, 'DSP international');
|
||
bindPkgControls();
|
||
bindCustSort();
|
||
renderPkg('midas');
|
||
renderPkg('ms');
|
||
renderCustDaily();
|
||
renderCustDetail();
|
||
}).catch(function(e){
|
||
document.getElementById('meta').textContent='数据加载失败:'+e;
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|