v ? new Date(v).toLocaleString() : '-';
+ const ensureStyle = () => { let style=$('#fcbx-style'); if(!style){style=document.createElement('style');style.id='fcbx-style';document.head.appendChild(style)} style.textContent=css; };
+ const updateMode = () => { const login=location.hash.includes('/login') || location.hash.includes('/admin/login'); document.body.classList.toggle('fcbx-admin', location.hash.includes('/admin') && !login); };
+ let cache = {key:'', data:null, at:0, promise:null};
+ const api = async (url) => { const t=token(); const res=await fetch(url,{headers:t?{Authorization:`Bearer ${t}`}:{}}); if(!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); };
+ const getAnalytics = async (start=daysAgo(120), end=today(), force=false) => { const key=`${start}|${end}`; const now=Date.now(); if(!force && cache.key===key && cache.data && now-cache.at<60000) return cache.data; if(!force && cache.key===key && cache.promise) return cache.promise; cache.key=key; cache.promise=api(`/admin/analytics?start=${start}&end=${end}`).then(r=>{cache.data=r.detail||r;cache.at=Date.now();cache.promise=null;return cache.data}).catch(e=>{cache.promise=null;throw e}); return cache.promise; };
+ const closeMobileDrawer = () => { if(innerWidth>=1024)return; const aside=$('aside'); if(!aside)return; aside.classList.remove('translate-x-0'); aside.classList.add('-translate-x-full'); };
+ const bindDrawer = () => { if(window.__fcbxDrawerBound)return; window.__fcbxDrawerBound=true; document.addEventListener('click',e=>{if(e.target?.closest?.('aside a, aside nav a')&&innerWidth<1024){setTimeout(closeMobileDrawer,0);setTimeout(closeMobileDrawer,160);setTimeout(closeMobileDrawer,420)}},true); window.addEventListener('hashchange',()=>{setTimeout(closeMobileDrawer,0);setTimeout(closeMobileDrawer,180)}); };
+ const dateSelect = (id, value=today()) => { const d=new Date(value); const y=d.getFullYear(), m=d.getMonth()+1, day=d.getDate(); const years=[]; for(let i=y-5;i<=y+1;i++) years.push(i); return ``; };
+ const readDate = id => `${$(`#${id}-y`)?.value}-${String($(`#${id}-m`)?.value||1).padStart(2,'0')}-${String($(`#${id}-d`)?.value||1).padStart(2,'0')}`;
+ const drawChart = (canvas, rows=[]) => { const ctx=canvas.getContext('2d'), dpr=devicePixelRatio||1, rect=canvas.getBoundingClientRect(); canvas.width=rect.width*dpr; canvas.height=rect.height*dpr; ctx.scale(dpr,dpr); ctx.clearRect(0,0,rect.width,rect.height); const pad=34, w=rect.width-pad*2, h=rect.height-pad*2; const vals=rows.map(r=>Number(r.downloads||0)); const max=Math.max(1,...vals); ctx.strokeStyle=getComputedStyle(document.body).getPropertyValue('--fcbx-border')||'#ddd'; ctx.lineWidth=1; for(let i=0;i<4;i++){const y=pad+h*i/3;ctx.beginPath();ctx.moveTo(pad,y);ctx.lineTo(pad+w,y);ctx.stroke()} const pts=rows.map((r,i)=>[pad+(rows.length<2?0:i*w/(rows.length-1)), pad+h-(Number(r.downloads||0)/max)*h]); ctx.strokeStyle='#4f46e5';ctx.lineWidth=3;ctx.beginPath();pts.forEach((p,i)=>i?ctx.lineTo(...p):ctx.moveTo(...p));ctx.stroke();ctx.fillStyle='#4f46e5';pts.forEach(p=>{ctx.beginPath();ctx.arc(p[0],p[1],3,0,7);ctx.fill()}); ctx.fillStyle=getComputedStyle(document.body).getPropertyValue('--fcbx-muted')||'#666';ctx.font='12px system-ui';ctx.fillText('下载趋势',pad,18); };
+ const pagerHtml = id => ``;
+ const attachPager = (id, rows, render, empty) => { let page=1; const paint=()=>{const size=Number($(`#${id}-size`)?.value||10), pages=Math.max(1,Math.ceil(rows.length/size)); page=Math.min(Math.max(1,page),pages); const slice=rows.slice((page-1)*size,page*size); render(slice, rows.length?null:empty); const p=$(`#${id}-pager`); if(!p)return; $('.fcbx-muted',p).textContent=`第 ${page} / ${pages} 页,共 ${rows.length} 条`; $(`#${id}-page`).value=page; $(`#${id}-prev`).disabled=page<=1; $(`#${id}-next`).disabled=page>=pages; }; $(`#${id}-size`).onchange=()=>{page=1;paint()}; $(`#${id}-prev`).onclick=()=>{page--;paint()}; $(`#${id}-next`).onclick=()=>{page++;paint()}; $(`#${id}-go`).onclick=()=>{page=Number($(`#${id}-page`).value)||1;paint()}; paint(); };
+ const renderDashboard = async () => { if(!location.hash.includes('/admin/dashboard')){ $('#fcbx-dashboard')?.remove(); return; } const main=$('main')||document.body; let box=$('#fcbx-dashboard'); if(!box){box=document.createElement('section');box.id='fcbx-dashboard';box.className='fcbx';main.appendChild(box)} box.innerHTML='加载统计中...
'; try{const data=await getAnalytics(); const t=data.totals||{}; box.innerHTML=`${dateSelect('fcbx-start',data.range?.start||daysAgo(120))}${dateSelect('fcbx-end',data.range?.end||today())}
历史文件
${t.historicalFiles??0}
下载次数
${t.totalDownloads??0}
下载流量
${fmtBytes(t.downloadTraffic)}
下载趋势
热门下载 Top 5
| 文件 | 下载 | 流量 | 状态 |
${(data.topFiles||[]).map(f=>`| ${esc(f.name)} | ${f.download_count||0} | ${fmtBytes(f.download_traffic)} | ${f.current?'现存':'历史'} |
`).join('')||'| 暂无下载记录 |
'}
`; $('#fcbx-refresh').onclick=async()=>{cache.data=null; await renderDashboard()}; drawChart($('#fcbx-chart'),data.daily||[]); }catch(e){box.innerHTML=`统计加载失败:${esc(e.message)}
`} };
+ const renderHistory = async () => { if(!location.hash.includes('/admin/files')){ $('#fcbx-file-history')?.remove(); return; } const card=findFilesTableCard(); const host=card?.parentElement || $('main') || document.body; let box=$('#fcbx-file-history'); if(!box){box=document.createElement('section');box.id='fcbx-file-history';box.className='fcbx';box.innerHTML=`历史文件
| 文件 | 大小 | 取件码 | 上传日期 | 过期时间 | 删除时间 | 下载 | 流量 | 状态 |
|---|
| 加载中... |
${pagerHtml('fcbx-history')}
`; host.appendChild(box)} try{const data=await getAnalytics(); const rows=data.historyFiles||[]; const row=f=>{const expired=!f.deleted&&f.expired_at&&new Date(f.expired_at)${esc(f.name)} | ${fmtBytes(f.size)} | ${esc(f.code)} | ${fmtDate(f.uploaded_at||f.created_at)} | ${fmtDate(f.expired_at)} | ${fmtDate(f.deleted_at)} | ${f.download_count??0} | ${fmtBytes(f.download_traffic)} | ${status} | `}; attachPager('fcbx-history',rows,(slice,empty)=>{$('#fcbx-history-body').innerHTML=empty||slice.map(row).join('')},'| 暂无历史文件 |
'); }catch(e){$('#fcbx-history-body').innerHTML=`| 历史文件加载失败:${esc(e.message)} |
`} };
+ const findFilesTableCard = () => { const table=$$('main table').find(t=>/Retrieve Code|取件码/.test($$('th',t).map(th=>th.textContent).join('|'))); return table?.closest('section,div[class*="rounded"]')||null; };
+ const mountFilesPager = () => { if(!location.hash.includes('/admin/files'))return; const native=$('.fcbx-native-pagination'); const card=findFilesTableCard(); if(!native&&!card)return; native?.classList.add('fcbx-native-pagination-hidden'); let pager=$('#fcbx-allfiles-pager'); if(!pager){pager=document.createElement('div');pager.id='fcbx-allfiles-pager';pager.className='fcbx-official-pager'; native?native.after(pager):card.appendChild(pager)} const text=native?.textContent?.replace(/\s+/g,' ')||card?.textContent||''; const m=text.match(/显示第\s*(\d+)\s*到\s*(\d+)\s*条,共\s*(\d+)\s*条/); const total=Number(m?.[3]||card?.querySelectorAll('tbody tr')?.length||0); const params=new URLSearchParams((location.hash.split('?')[1]||'')); const size=Number(params.get('size')||10), page=Number(params.get('page')||1), pages=Math.max(1,Math.ceil(total/size)); pager.innerHTML=`第 ${Math.min(page,pages)} / ${pages} 页,共 ${total} 条`; const setHash=patch=>{const [path,q='']=location.hash.split('?');const p=new URLSearchParams(q);Object.entries(patch).forEach(([k,v])=>p.set(k,String(v)));location.hash=`${path||'#/admin/files'}?${p.toString()}`}; $('#fcbx-allfiles-size').onchange=e=>setHash({page:1,size:e.target.value}); $('#fcbx-allfiles-prev').onclick=()=>setHash({page:Math.max(1,page-1),size}); $('#fcbx-allfiles-next').onclick=()=>setHash({page:Math.min(pages,page+1),size}); $('#fcbx-allfiles-go').onclick=()=>setHash({page:Math.min(pages,Math.max(1,Number($('#fcbx-allfiles-page').value)||1)),size}); };
+ let scheduled=false,lastRun=0; const run=()=>{ if(scheduled)return; scheduled=true; requestAnimationFrame(()=>{scheduled=false; const now=Date.now(); ensureStyle(); updateMode(); bindDrawer(); mountFilesPager(); if(now-lastRun<500)return; lastRun=now; renderDashboard(); renderHistory(); }); };
+ window.addEventListener('hashchange',run); window.addEventListener('resize',run); document.addEventListener('DOMContentLoaded',run); new MutationObserver(run).observe(document.body,{childList:true,subtree:true}); run();
})();'''
rewrite_gzip(assets / 'fcb-analytics.js', js)
index = Path('/app/themes/2024/index.html')
text = index.read_text(encoding='utf-8')
if '/assets/fcb-analytics.js' not in text:
- text = text.replace('