feat: enhance click gap dashboard

This commit is contained in:
SHAREit
2026-10-16 15:44:44 +08:00
parent 1d7328e7c7
commit e7a4d4c2f2
+33 -10
View File
@@ -2,19 +2,42 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Click Gap 数据看板</title> <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Click Gap 数据看板</title>
<style>body{font:14px system-ui,sans-serif;margin:2rem;color:#223}label{margin-right:1rem}input,select,button{padding:.35rem}table{border-collapse:collapse;width:100%;margin-top:1rem}th,td{border:1px solid #ccd;padding:.45rem;text-align:right}th{cursor:pointer;background:#eef}th:first-child,td:first-child,th:nth-child(2),td:nth-child(2),th:nth-child(3),td:nth-child(3){text-align:left}.status{padding:.7rem;background:#f5f5f5}.error{color:#a00}.sort{font-size:.8em}</style> <style>
body{font:14px system-ui,sans-serif;margin:2rem;color:#223}label{margin-right:1rem}input,select,button{padding:.35rem}select[multiple]{min-width:18rem;vertical-align:middle}button{cursor:pointer}table{border-collapse:collapse;width:100%;margin-top:1rem}th,td{border:1px solid #ccd;padding:.45rem;text-align:right}th{cursor:pointer;background:#eef}th:first-child,td:first-child,th:nth-child(2),td:nth-child(2),th:nth-child(3),td:nth-child(3){text-align:left}.status{padding:.7rem;background:#f5f5f5}.error{color:#a00}.sort{font-size:.8em}.controls{display:flex;flex-wrap:wrap;gap:.7rem 1rem;align-items:center}.pagination{display:flex;align-items:center;gap:.7rem;margin-top:1rem}.pagination button:disabled{cursor:not-allowed;opacity:.5}.rate-high{color:#c00;font-weight:700}.hint{color:#667;font-size:.9em}
</style>
</head> </head>
<body><h1>Click Gap 数据看板</h1><div><label>开始 <input id="start" type="date"></label><label>结束 <input id="end" type="date"></label><label>pkg_name <select id="pkg"><option value="">全部</option></select></label><button id="apply">查询</button><button id="reset">重置</button></div><p id="status" class="status">加载中…</p><table><thead><tr id="head"></tr></thead><tbody id="body"></tbody></table> <body>
<h1>Click Gap 数据看板</h1>
<div class="controls">
<label>开始 <input id="start" type="date"></label><label>结束 <input id="end" type="date"></label>
<label>pkg_name <select id="pkg" multiple size="4" aria-label="选择一个或多个 pkg_name"></select></label>
<button id="clearPkg" type="button">清空包名选择</button><button id="apply" type="button">查询</button><button id="reset" type="button">重置</button><button id="download" type="button">下载原始数据</button>
</div>
<p class="hint">按住 Command(macOS)或 Ctrl 可多选包名;当前选择会显示在下方状态中。</p>
<p id="status" class="status">加载中…</p>
<table><thead><tr id="head"></tr></thead><tbody id="body"></tbody></table>
<div class="pagination" aria-label="分页"><button id="previous" type="button">上一页</button><span id="pageInfo">第 0 / 0 页</span><button id="next" type="button">下一页</button></div>
<script> <script>
(() => {"use strict"; (() => {"use strict";
const columns=["dt","attr_platform","pkg_name","shareit_clicks","af_clicks","installs_ua","clicks_diff","clicks_diff_rate"];let rows=[], failed=[];let sort={key:"dt",direction:-1}; const columns=["dt","attr_platform","pkg_name","shareit_clicks","af_clicks","installs_ua","clicks_diff","clicks_diff_rate"];
const $=id=>document.getElementById(id), status=$('status'); const pageSize=200;let rows=[],failed=[],page=1,sort={key:"dt",direction:-1};
const $=id=>document.getElementById(id),status=$("status");
function text(value){return value===null||value===undefined?"—":String(value)} function text(value){return value===null||value===undefined?"—":String(value)}
function validDate(v){return /^\d{4}-\d{2}-\d{2}$/.test(v)&&!Number.isNaN(Date.parse(v))} function validDate(value){return /^\d{4}-\d{2}-\d{2}$/.test(value)&&!Number.isNaN(Date.parse(value))}
function date8(v){return v.replaceAll('-','')} function date8(value){return value.replaceAll("-","")}
function show(){const start=$('start').value,end=$('end').value,pkg=$('pkg').value;if(!validDate(start)||!validDate(end)||start>end){status.textContent='请输入有效日期范围,且开始日期不得晚于结束日期。';status.className='status error';return}const filtered=rows.filter(r=>r.dt>=date8(start)&&r.dt<=date8(end)&&(!pkg||r.pkg_name===pkg));filtered.sort((a,b)=>{const av=a[sort.key],bv=b[sort.key];if(av===bv)return 0;if(av===null||av===undefined)return 1;if(bv===null||bv===undefined)return -1;const n=typeof av==='number'&&typeof bv==='number'?av-bv:String(av).localeCompare(String(bv));return n*sort.direction});$('body').replaceChildren();filtered.forEach(r=>{const tr=document.createElement('tr');columns.forEach(c=>{const td=document.createElement('td');td.textContent=text(r[c]);tr.append(td)});$('body').append(tr)});status.textContent=(failed.length?`数据不完整:跳过 ${failed.length} 个文件。`:'')+`显示 ${filtered.length} 行。`;status.className='status'+(failed.length?' error':'')} function localDate(value){return `${value.getFullYear()}-${String(value.getMonth()+1).padStart(2,"0")}-${String(value.getDate()).padStart(2,"0")}`}
function setupHeaders(){const h=$('head');columns.forEach(c=>{const th=document.createElement('th');th.dataset.key=c;th.textContent=c;th.addEventListener('click',()=>{if(sort.key===c)sort.direction*=-1;else{sort.key=c;sort.direction=-1}setupHeaders();show()});h.append(th)});} function selectedPackages(){return [...$("pkg").selectedOptions].map(option=>option.value)}
async function load(){setupHeaders();try{const response=await fetch('data/index.json',{cache:'no-store'});if(!response.ok)throw Error('index.json 加载失败');const index=await response.json();if(!Array.isArray(index.files))throw Error('数据清单格式错误');const safe=index.files.filter(f=>/^\d{8}$/.test(f.date)&&f.path===`data/${f.date}.json`);const bad=index.files.length-safe.length;failed=bad?['清单路径']:[];const loaded=await Promise.all(safe.map(async f=>{try{const r=await fetch(f.path,{cache:'no-store'});if(!r.ok)throw Error('HTTP');const data=await r.json();if(!Array.isArray(data)||data.some(x=>!x||typeof x!=='object'||x.dt!==f.date))throw Error('格式');return data}catch(e){failed.push(f.date);return []}}));rows=loaded.flat();const dates=rows.map(r=>r.dt).sort();if(!dates.length){$('start').value=$('end').value='';status.textContent='暂无数据。';return}const end=dates.at(-1), endDate=new Date(`${end.slice(0,4)}-${end.slice(4,6)}-${end.slice(6)}T00:00:00`);endDate.setDate(endDate.getDate()-29);$('end').value=`${end.slice(0,4)}-${end.slice(4,6)}-${end.slice(6)}`;$('start').value=endDate.toISOString().slice(0,10);[...new Set(rows.map(r=>r.pkg_name).filter(Boolean))].sort().forEach(p=>{const o=document.createElement('option');o.value=p;o.textContent=p;$('pkg').append(o)});show()}catch(e){status.textContent=`无法加载数据:${e.message}`;status.className='status error'}} function rateValue(value){if(typeof value!=="number"||!Number.isFinite(value))return null;return Math.abs(value)<=1?value*100:value}
$('apply').addEventListener('click',show);$('reset').addEventListener('click',()=>{if(rows.length){$('pkg').value='';const d=rows.map(r=>r.dt).sort().at(-1);$('end').value=`${d.slice(0,4)}-${d.slice(4,6)}-${d.slice(6)}`;const x=new Date($('end').value);x.setDate(x.getDate()-29);$('start').value=x.toISOString().slice(0,10);show()}});load(); function formatRate(value){const rate=rateValue(value);return rate===null?"—":`${rate.toFixed(2)}%`}
function filteredRows(){const start=$("start").value,end=$("end").value,packages=selectedPackages();return rows.filter(row=>row.dt>=date8(start)&&row.dt<=date8(end)&&(!packages.length||packages.includes(row.pkg_name)))}
function compare(a,b){const av=a[sort.key],bv=b[sort.key];if(av===bv)return 0;if(av===null||av===undefined)return 1;if(bv===null||bv===undefined)return -1;const an=sort.key==="clicks_diff_rate"?rateValue(av):av,bn=sort.key==="clicks_diff_rate"?rateValue(bv):bv;const result=typeof an==="number"&&typeof bn==="number"?an-bn:String(an).localeCompare(String(bn));return result*sort.direction}
function show(){const start=$("start").value,end=$("end").value;if(!validDate(start)||!validDate(end)||start>end){status.textContent="请输入有效日期范围,且开始日期不得晚于结束日期。";status.className="status error";$("body").replaceChildren();updatePagination(0);return}const filtered=filteredRows().sort(compare),totalPages=Math.max(1,Math.ceil(filtered.length/pageSize));page=Math.min(page,totalPages);const visible=filtered.slice((page-1)*pageSize,page*pageSize);$("body").replaceChildren();visible.forEach(row=>{const tr=document.createElement("tr");columns.forEach(column=>{const td=document.createElement("td");td.textContent=column==="clicks_diff_rate"?formatRate(row[column]):text(row[column]);if(column==="clicks_diff_rate"&&rateValue(row[column])!==null&&rateValue(row[column])>10)td.className="rate-high";tr.append(td)});$("body").append(tr)});updatePagination(filtered.length);const selected=selectedPackages();status.textContent=(failed.length?`数据不完整:跳过 ${failed.length} 个文件。 `:"")+`显示 ${filtered.length} 行,第 ${page} / ${totalPages} 页(每页 ${pageSize} 行)。`+(selected.length?`已选择 ${selected.length} 个包名。`:"未筛选包名。");status.className="status"+(failed.length?" error":"")}
function updatePagination(total){const totalPages=Math.max(1,Math.ceil(total/pageSize));$("pageInfo").textContent=`第 ${total?`${page} / ${totalPages}`:"0 / 0"} 页`;$('previous').disabled=!total||page<=1;$('next').disabled=!total||page>=totalPages}
function setupHeaders(){const header=$("head");header.replaceChildren();columns.forEach(column=>{const th=document.createElement("th");th.dataset.key=column;th.textContent=column;th.addEventListener("click",()=>{if(sort.key===column)sort.direction*=-1;else{sort.key=column;sort.direction=-1}page=1;setupHeaders();show()});header.append(th)})}
function resetPage(){page=1;show()}
function setDefaultDates(){const dates=rows.map(row=>row.dt).sort();if(!dates.length){$("start").value=$("end").value="";return}const end=dates.at(-1),endDate=new Date(`${end.slice(0,4)}-${end.slice(4,6)}-${end.slice(6)}T00:00:00`);endDate.setDate(endDate.getDate()-29);$("end").value=`${end.slice(0,4)}-${end.slice(4,6)}-${end.slice(6)}`;$("start").value=localDate(endDate)}
function downloadRaw(){const blob=new Blob([JSON.stringify(rows,null,2)],{type:"application/json;charset=utf-8"}),url=URL.createObjectURL(blob),link=document.createElement("a"),stamp=new Date().toISOString().replaceAll(/[:.]/g,"-");link.href=url;link.download=`af-click-gap-raw-${stamp}.json`;link.click();URL.revokeObjectURL(url)}
async function load(){setupHeaders();try{const response=await fetch("data/index.json",{cache:"no-store"});if(!response.ok)throw Error("index.json 加载失败");const index=await response.json();if(!Array.isArray(index.files))throw Error("数据清单格式错误");const safe=index.files.filter(file=>/^\d{8}$/.test(file.date)&&file.path===`data/${file.date}.json`);failed=safe.length===index.files.length?[]:["清单路径"];const loaded=await Promise.all(safe.map(async file=>{try{const response=await fetch(file.path,{cache:"no-store"});if(!response.ok)throw Error("HTTP");const data=await response.json();if(!Array.isArray(data)||data.some(row=>!row||typeof row!=="object"||row.dt!==file.date))throw Error("格式");return data}catch(error){failed.push(file.date);return []}}));rows=loaded.flat();setDefaultDates();[...new Set(rows.map(row=>row.pkg_name).filter(Boolean))].sort().forEach(pkg=>{const option=document.createElement("option");option.value=pkg;option.textContent=pkg;$("pkg").append(option)});if(rows.length)show();else{status.textContent="暂无数据。";updatePagination(0)}}catch(error){status.textContent=`无法加载数据:${error.message}`;status.className="status error"}}
$("apply").addEventListener("click",resetPage);$("start").addEventListener("change",resetPage);$("end").addEventListener("change",resetPage);$("clearPkg").addEventListener("click",()=>{$("pkg").selectedIndex=-1;resetPage()});$("reset").addEventListener("click",()=>{if(rows.length){$("pkg").selectedIndex=-1;setDefaultDates();resetPage()}});$("previous").addEventListener("click",()=>{if(page>1){page--;show()}});$("next").addEventListener("click",()=>{if(page<Math.ceil(filteredRows().length/pageSize)){page++;show()}});$("download").addEventListener("click",downloadRaw);load();
})(); })();
</script></body></html> </script></body></html>