Files
af-click-gap/index.html
T

59 lines
12 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
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"><title>Click Gap 数据看板</title>
<link rel="icon" type="image/png" href="icon.png">
<style>
body{font:14px system-ui,sans-serif;margin:2rem;color:#223}label{margin-right:1rem}input,button{padding:.35rem}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}.pkg-filter{position:relative;display:inline-block;vertical-align:middle}.pkg-trigger{min-width:18rem;text-align:left;background:#fff;border:1px solid #aaa}.pkg-trigger::after{content:"▾";float:right;margin-left:1rem}.pkg-panel{position:absolute;z-index:2;top:calc(100% + .25rem);left:0;width:19rem;padding:.6rem;background:#fff;border:1px solid #bbc;box-shadow:0 3px 10px #0002}.pkg-panel[hidden]{display:none}.pkg-search{box-sizing:border-box;width:100%;margin-bottom:.45rem}.pkg-options{max-height:12rem;overflow-y:auto;border:1px solid #ddd;padding:.2rem}.pkg-option{display:block;padding:.25rem}.pkg-option input{margin-right:.45rem}.pkg-actions{display:flex;justify-content:space-between;align-items:center;margin-top:.5rem}.pkg-actions button{padding:.25rem .5rem}.pkg-confirm{background:#3578c4;color:#fff;border:1px solid #28609c}
</style>
</head>
<body>
<h1>Click Gap 数据看板</h1>
<div class="controls">
<label>开始 <input id="start" type="date"></label><label>结束 <input id="end" type="date"></label>
<div class="pkg-filter" id="pkgFilter">
<button id="pkgTrigger" class="pkg-trigger" type="button" aria-haspopup="dialog" aria-expanded="false">App(全部)</button>
<div id="pkgPanel" class="pkg-panel" role="dialog" aria-label="选择 pkg_name" hidden>
<input id="pkgSearch" class="pkg-search" type="search" placeholder="搜索..." aria-label="搜索包名">
<div id="pkgOptions" class="pkg-options"></div>
<div class="pkg-actions"><span><button id="selectAllPkg" type="button">全选</button><button id="clearPkg" type="button">清空</button></span><button id="confirmPkg" class="pkg-confirm" type="button">确定</button></div>
</div></div>
<button id="apply" type="button">查询</button><button id="reset" type="button">重置</button><button id="download" type="button">下载原始数据</button>
</div>
<p class="hint">选择包名后点击“确定”应用筛选;搜索只过滤候选项。</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>
(() => {"use strict";
const columns=["dt","attr_platform","pkg_name","shareit_clicks","af_clicks","installs_ua","clicks_diff","clicks_diff_rate"];
const pageSize=200,maxConcurrent=6;let rows=[],failed=[],page=1,sort={key:"dt",direction:-1},files=[],loadVersion=0;
const dataCache=new Map();
const $=id=>document.getElementById(id),status=$("status");
function text(value){return value===null||value===undefined?"—":String(value)}
function validDate(value){return /^\d{4}-\d{2}-\d{2}$/.test(value)&&!Number.isNaN(Date.parse(value))}
function date8(value){return value.replaceAll("-","")}
function localDate(value){return `${value.getFullYear()}-${String(value.getMonth()+1).padStart(2,"0")}-${String(value.getDate()).padStart(2,"0")}`}
let appliedPackages=new Set(),draftPackages=new Set();
function selectedPackages(){return [...appliedPackages]}
function packageLabel(){return appliedPackages.size?`App(${appliedPackages.size} 个已选)`:"App(全部)"}
function renderPackageOptions(){const query=$("pkgSearch").value.trim().toLowerCase(),container=$("pkgOptions");container.replaceChildren();[...new Set(rows.map(row=>row.pkg_name).filter(Boolean))].sort().filter(pkg=>pkg.toLowerCase().includes(query)).forEach(pkg=>{const label=document.createElement("label");label.className="pkg-option";const checkbox=document.createElement("input");checkbox.type="checkbox";checkbox.value=pkg;checkbox.checked=draftPackages.has(pkg);checkbox.addEventListener("change",()=>{if(checkbox.checked)draftPackages.add(pkg);else draftPackages.delete(pkg)});label.append(checkbox,pkg);container.append(label)})}
function openPackages(){draftPackages=new Set(appliedPackages);renderPackageOptions();$("pkgPanel").hidden=false;$("pkgTrigger").setAttribute("aria-expanded","true");$("pkgSearch").focus()}
function closePackages(){$("pkgPanel").hidden=true;$("pkgTrigger").setAttribute("aria-expanded","false")}
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 rateValue(value){if(typeof value!=="number"||!Number.isFinite(value))return null;return Math.abs(value)<=1?value*100:value}
function formatRate(value){const rate=rateValue(value);return rate===null?"—":`${rate.toFixed(2)}%`}
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 yesterday=new Date();yesterday.setDate(yesterday.getDate()-1);const date=localDate(yesterday);$("start").value=date;$("end").value=date}
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 fetchDate(file){if(dataCache.has(file.path))return {date:file.date,data:dataCache.get(file.path)};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("格式");dataCache.set(file.path,data);return {date:file.date,data}}catch(error){return {date:file.date,data:null}}}
async function loadRange(){const version=++loadVersion,start=$("start").value,end=$("end").value;if(!validDate(start)||!validDate(end)||start>end){show();return}const selected=files.filter(file=>file.date>=date8(start)&&file.date<=date8(end));rows=[];failed=[];page=1;status.textContent=selected.length?`正在加载 ${selected.length} 个日期文件…`:`所选日期范围暂无数据文件。`;status.className="status";if(!selected.length){renderPackageOptions();show();return}for(let offset=0;offset<selected.length;offset+=maxConcurrent){const batch=selected.slice(offset,offset+maxConcurrent),loaded=await Promise.all(batch.map(fetchDate));if(version!==loadVersion)return;loaded.forEach(item=>{if(item.data)rows.push(...item.data);else failed.push(item.date)});renderPackageOptions();$("pkgTrigger").textContent=packageLabel();show();if(offset+batch.length<selected.length)status.textContent=`已加载 ${Math.min(offset+batch.length,selected.length)} / ${selected.length} 个日期文件…`}if(failed.length)status.textContent=`数据不完整:跳过 ${failed.length} 个文件。 ${status.textContent}`}
async function load(){setupHeaders();setDefaultDates();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("数据清单格式错误");files=index.files.filter(file=>/^\d{8}$/.test(file.date)&&file.path===`data/${file.date}.json`);await loadRange()}catch(error){status.textContent=`无法加载数据:${error.message}`;status.className="status error"}}
$("pkgTrigger").addEventListener("click",()=>$("pkgPanel").hidden?openPackages():closePackages());$("pkgSearch").addEventListener("input",renderPackageOptions);$("selectAllPkg").addEventListener("click",()=>{[...new Set(rows.map(row=>row.pkg_name).filter(Boolean))].forEach(pkg=>draftPackages.add(pkg));renderPackageOptions()});$("clearPkg").addEventListener("click",()=>{draftPackages.clear();renderPackageOptions()});$("confirmPkg").addEventListener("click",()=>{appliedPackages=new Set(draftPackages);$("pkgTrigger").textContent=packageLabel();closePackages();resetPage()});document.addEventListener("click",event=>{if(!$("pkgFilter").contains(event.target)&&!$("pkgPanel").hidden)closePackages()});document.addEventListener("keydown",event=>{if(event.key==="Escape"&&!$("pkgPanel").hidden)closePackages()});$("apply").addEventListener("click",loadRange);$("start").addEventListener("change",loadRange);$("end").addEventListener("change",loadRange);$("reset").addEventListener("click",()=>{appliedPackages.clear();$("pkgTrigger").textContent=packageLabel();setDefaultDates();loadRange()});$("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>