Files
DSP-daily/index.html
T

474 lines
23 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;}
.ctrl{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-bottom:10px;}
.ctrl label{font-size:13px;cursor:pointer;user-select:none;}
.ctrl input[type=text]{border:1px solid var(--line);border-radius:6px;padding:5px 8px;font-size:12px;min-width:150px;}
th.sortable{cursor:pointer;user-select:none;}
th.sortable:hover{background:#374151;}
th .arw{display:inline-block;width:9px;margin-left:3px;font-size:10px;color:#93c5fd;}
th.asc .arw::after{content:"\25B2";} th.desc .arw::after{content:"\25BC";}
tr.acc-sub td{background:#f3f6fb;color:#475569;font-size:12px;}
tr.acc-sub td.txt{padding-left:26px;}
tr.acc-sub:hover td{background:#e8eef7;}
</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="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">
<span class="sub-note" style="margin:0;">|DoD|&gt;100 标注(+绿−红);表头点击排序;波动原因暂存本浏览器</span>
</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="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">
<span class="sub-note" style="margin:0;">|DoD|&gt;100 标注(+绿−红);表头点击排序;波动原因暂存本浏览器</span>
</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></thead><tbody></tbody></table></div>
</div>
<div class="card">
<h2>3.2 客户明细 <span class="tag">保留14天</span></h2>
<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><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>
<th class="de-sort" data-k="t1" data-ty="num">T-1收入<span class="arw"></span></th>
<th class="de-sort" data-k="dod" data-ty="num">DoD(与T-2差)<span class="arw"></span></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波动 =====
// 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);
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; });
var fCat=(document.getElementById('deCat')||{}).value||'';
var fAcc=((document.getElementById('deAcc')||{}).value||'').trim().toLowerCase();
var rows=t1list.map(function(r){
var prev=t2map[r.cat+'||'+r.acc];
return {cat:r.cat,acc:r.acc,base:r.base,t1:r.t1,dod:(prev!=null?r.t1-prev:null)};
}).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){
if(DE_SORT.ty==='str') return dir*String(a[DE_SORT.k]).localeCompare(String(b[DE_SORT.k]));
var va=a[DE_SORT.k]==null?-Infinity:a[DE_SORT.k], vb=b[DE_SORT.k]==null?-Infinity:b[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.t1-a.t1); });
}
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 class="txt">'+r.acc+'</td><td>'+n2(r.base)+'</td>'
+'<td>'+n2(r.t1)+'</td><td class="'+dc+'">'+(r.dod==null?'':n2(r.dod))+'</td></tr>';
}).join('');
document.querySelector('#tCustDetail tbody').innerHTML=body;
document.querySelectorAll('#tCustDetail th.de-sort').forEach(function(th){
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 的表头排序绑定在 renderCustDaily 内部动态处理(表头是动态生成的)
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();
};
});
// 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+' 天';
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>