✨ 恢复测试环境交互与更新脚本
This commit is contained in:
@@ -446,7 +446,7 @@ body.fcbx-admin #app button,body.fcbx-admin #app input:not([type="checkbox"]),bo
|
||||
body.fcbx-admin #app table{border-radius:0!important;overflow:visible!important}
|
||||
body.fcbx-admin main section[class*="rounded"],body.fcbx-admin main div[class*="rounded"][class*="border"],body.fcbx-admin main div[class*="shadow"]{box-shadow:var(--fcbx-card-shadow)!important;border-color:var(--fcbx-border)!important}
|
||||
body.fcbx-admin main section[class*="rounded"]:hover,body.fcbx-admin main div[class*="rounded"][class*="border"]:hover{box-shadow:var(--fcbx-hover-shadow)!important}
|
||||
body.fcbx-admin main .grid{gap:1.5rem!important}.fcbx{margin:24px 0 0;color:var(--fcbx-text)}.fcbx,.fcbx *{box-sizing:border-box}.fcbx-panel,.fcbx-card{background:linear-gradient(180deg,var(--fcbx-surface),var(--fcbx-surface-2));border:1px solid var(--fcbx-border);border-radius:16px;box-shadow:var(--fcbx-card-shadow);transition:box-shadow .18s ease,transform .18s ease}.fcbx-panel:hover,.fcbx-card:hover{box-shadow:var(--fcbx-hover-shadow);transform:translateY(-1px)}.fcbx-title{font-size:18px;font-weight:800;margin:0 0 16px}.fcbx-muted{color:var(--fcbx-muted);font-size:13px}.fcbx-dashboard-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:24px;margin:24px 0}.fcbx-card{padding:20px}.fcbx-card-label{font-size:13px;color:var(--fcbx-muted);font-weight:700}.fcbx-card-value{font-size:28px;line-height:1.25;font-weight:900;margin-top:8px}.fcbx-panel{padding:20px;margin-top:24px}.fcbx-bar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:16px}.fcbx-bar select,.fcbx-official-pager select,.fcbx-official-pager input,.fcbx-page-input{height:40px;border:1px solid var(--fcbx-border);padding:0 12px;background:var(--fcbx-surface);color:var(--fcbx-text);border-radius:16px}.fcbx-btn,.fcbx-official-pager button{height:40px;border:1px solid #4f46e5;background:#4f46e5;color:#fff;padding:0 14px;font-weight:800;border-radius:16px;cursor:pointer}.fcbx-btn:disabled,.fcbx-official-pager button:disabled{opacity:.45;cursor:not-allowed;background:var(--fcbx-surface-2);color:var(--fcbx-muted);border-color:var(--fcbx-border)}.fcbx-chart{width:100%;height:260px;display:block}.fcbx-table-wrap{overflow:auto;border:1px solid var(--fcbx-border);border-radius:12px}.fcbx-table-wrap table{width:100%;border-collapse:collapse}.fcbx-table-wrap th,.fcbx-table-wrap td{padding:12px 14px;border-bottom:1px solid var(--fcbx-border);text-align:left;white-space:nowrap}.fcbx-table-wrap th{font-size:12px;text-transform:uppercase;color:var(--fcbx-muted);font-weight:800}.fcbx-empty{text-align:center;color:var(--fcbx-muted);padding:24px!important}.fcbx-status-pill{display:inline-flex;align-items:center;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:800}.fcbx-status-current{background:#dcfce7;color:#166534}.fcbx-status-expired{background:#fef3c7;color:#92400e}.fcbx-status-deleted{background:#fee2e2;color:#991b1b}.fcbx-native-pagination-hidden{display:none!important}.fcbx-official-pager{display:flex!important;visibility:visible!important;opacity:1!important;align-items:center!important;justify-content:flex-end!important;gap:10px!important;flex-wrap:wrap!important;margin:14px 0 0!important;padding:14px!important;border-top:1px solid var(--fcbx-border)!important;background:var(--fcbx-surface-2)!important;border-radius:0 0 16px 16px!important}.fcbx-official-pager .fcbx-muted{margin-right:auto!important}
|
||||
body.fcbx-admin main .grid{gap:1.5rem!important}.fcbx{margin:24px 0 0;color:var(--fcbx-text)}.fcbx,.fcbx *{box-sizing:border-box}.fcbx-panel,.fcbx-card{background:linear-gradient(180deg,var(--fcbx-surface),var(--fcbx-surface-2));border:1px solid var(--fcbx-border);border-radius:16px;box-shadow:var(--fcbx-card-shadow);transition:box-shadow .18s ease,transform .18s ease}.fcbx-panel:hover,.fcbx-card:hover{box-shadow:var(--fcbx-hover-shadow);transform:translateY(-1px)}.fcbx-title{font-size:18px;font-weight:800;margin:0 0 16px}.fcbx-muted{color:var(--fcbx-muted);font-size:13px}.fcbx-dashboard-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:24px;margin:24px 0}.fcbx-card{padding:20px}.fcbx-card-label{font-size:13px;color:var(--fcbx-muted);font-weight:700}.fcbx-card-value{font-size:28px;line-height:1.25;font-weight:900;margin-top:8px}.fcbx-panel{padding:20px;margin-top:24px}.fcbx-bar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:16px}.fcbx-bar select,.fcbx-official-pager select,.fcbx-official-pager input,.fcbx-page-input{height:40px;border:1px solid var(--fcbx-border);padding:0 12px;background:var(--fcbx-surface);color:var(--fcbx-text);border-radius:16px}.fcbx-btn,.fcbx-official-pager button{height:40px;border:1px solid #4f46e5;background:#4f46e5;color:#fff;padding:0 14px;font-weight:800;border-radius:16px;cursor:pointer}.fcbx-btn:disabled,.fcbx-official-pager button:disabled{opacity:.45;cursor:not-allowed;background:var(--fcbx-surface-2);color:var(--fcbx-muted);border-color:var(--fcbx-border)}.fcbx-chart-wrap{position:relative;overflow:hidden;touch-action:none}.fcbx-chart{width:100%;height:260px;display:block;touch-action:none;cursor:grab}.fcbx-chart:active{cursor:grabbing}.fcbx-chart-controls{display:flex;gap:10px;align-items:center;justify-content:flex-end;flex-wrap:wrap;margin:-6px 0 12px}.fcbx-chart-window{margin-right:auto}.fcbx-tooltip{position:absolute;z-index:3;min-width:130px;max-width:min(240px,calc(100vw - 48px));padding:9px 11px;border:1px solid var(--fcbx-border);border-radius:12px;background:var(--fcbx-surface);box-shadow:var(--fcbx-card-shadow);font-size:12px;pointer-events:none;transform:translate(-50%,-112%);transition:left .08s linear,top .08s linear,opacity .12s ease}.fcbx-history-deleted{opacity:.58;filter:grayscale(.35)}.fcbx-history-deleted td{color:var(--fcbx-muted)!important}.fcbx-filter-toggle{display:inline-flex!important;align-items:center;gap:6px}.fcbx-filter-body-collapsed{display:none!important}.fcbx-filter-card{position:relative}.fcbx-table-wrap{overflow:auto;border:1px solid var(--fcbx-border);border-radius:12px}.fcbx-table-wrap table{width:100%;border-collapse:collapse}.fcbx-table-wrap th,.fcbx-table-wrap td{padding:12px 14px;border-bottom:1px solid var(--fcbx-border);text-align:left;white-space:nowrap}.fcbx-table-wrap th{font-size:12px;text-transform:uppercase;color:var(--fcbx-muted);font-weight:800}.fcbx-empty{text-align:center;color:var(--fcbx-muted);padding:24px!important}.fcbx-status-pill{display:inline-flex;align-items:center;padding:4px 10px;border-radius:999px;font-size:12px;font-weight:800}.fcbx-status-current{background:#dcfce7;color:#166534}.fcbx-status-expired{background:#fef3c7;color:#92400e}.fcbx-status-deleted{background:#fee2e2;color:#991b1b}.fcbx-native-pagination-hidden{display:none!important}.fcbx-official-pager{display:flex!important;visibility:visible!important;opacity:1!important;align-items:center!important;justify-content:flex-end!important;gap:10px!important;flex-wrap:wrap!important;margin:14px 0 0!important;padding:14px!important;border-top:1px solid var(--fcbx-border)!important;background:var(--fcbx-surface-2)!important;border-radius:0 0 16px 16px!important}.fcbx-official-pager .fcbx-muted{margin-right:auto!important}
|
||||
@media(max-width:768px){body.fcbx-admin main .grid{gap:1rem!important}.fcbx-dashboard-grid{grid-template-columns:1fr;gap:16px;margin:16px 0}.fcbx{margin-top:16px}.fcbx-panel,.fcbx-card{padding:16px}.fcbx-chart{height:220px}.fcbx-official-pager{justify-content:stretch!important}.fcbx-official-pager>*{flex:1 1 auto!important}.fcbx-official-pager .fcbx-muted{flex:1 1 100%!important}.fcbx-official-pager input{width:76px!important;flex:0 0 76px!important}.fcbx-table-wrap table,.fcbx-table-wrap thead,.fcbx-table-wrap tbody,.fcbx-table-wrap tr,.fcbx-table-wrap th,.fcbx-table-wrap td{display:block}.fcbx-table-wrap thead{display:none}.fcbx-table-wrap tr{padding:10px 0;border-bottom:1px solid var(--fcbx-border)}.fcbx-table-wrap td{border:0;white-space:normal;display:flex;justify-content:space-between;gap:12px}.fcbx-table-wrap td:before{content:attr(data-label);color:var(--fcbx-muted);font-weight:800}}`;
|
||||
const $ = (s, r=document) => r.querySelector(s); const $$ = (s, r=document) => [...r.querySelectorAll(s)];
|
||||
const esc = v => String(v ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
@@ -463,23 +463,44 @@ body.fcbx-admin main .grid{gap:1.5rem!important}.fcbx{margin:24px 0 0;color:var(
|
||||
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 `<span class="fcbx-bar"><select id="${id}-y">${years.map(v=>`<option ${v===y?'selected':''}>${v}</option>`).join('')}</select><select id="${id}-m">${Array.from({length:12},(_,i)=>i+1).map(v=>`<option value="${v}" ${v===m?'selected':''}>${v}月</option>`).join('')}</select><select id="${id}-d">${Array.from({length:31},(_,i)=>i+1).map(v=>`<option value="${v}" ${v===day?'selected':''}>${v}日</option>`).join('')}</select></span>`; };
|
||||
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 chartStates = new WeakMap();
|
||||
const drawChart = (canvas, rows=[]) => {
|
||||
if(!canvas) return; const wrap=canvas.closest('.fcbx-chart-wrap')||canvas.parentElement; const total=rows.length;
|
||||
let st=chartStates.get(canvas); if(!st){const win=Math.min(total||1, innerWidth<640?35:70); st={start:Math.max(0,total-win), end:total, drag:null, pinch:null, hover:null}; chartStates.set(canvas,st)}
|
||||
st.end=Math.min(total,Math.max(st.end,1)); st.start=Math.max(0,Math.min(st.start,Math.max(0,st.end-1))); if(st.end-st.start<2){st.start=Math.max(0,st.end-2)}
|
||||
const visible=rows.slice(st.start,st.end); const ctx=canvas.getContext('2d'), dpr=devicePixelRatio||1, rect=canvas.getBoundingClientRect(); if(!rect.width||!rect.height)return;
|
||||
canvas.width=rect.width*dpr; canvas.height=rect.height*dpr; ctx.setTransform(dpr,0,0,dpr,0,0); ctx.clearRect(0,0,rect.width,rect.height);
|
||||
const padL=38,padR=18,padT=24,padB=34,w=Math.max(1,rect.width-padL-padR),h=Math.max(1,rect.height-padT-padB); const vals=visible.map(r=>Number(r.downloads||0)); const max=Math.max(1,...vals);
|
||||
const border=getComputedStyle(document.body).getPropertyValue('--fcbx-border')||'#ddd', muted=getComputedStyle(document.body).getPropertyValue('--fcbx-muted')||'#777';
|
||||
ctx.strokeStyle=border; ctx.lineWidth=1; ctx.fillStyle=muted; ctx.font='12px system-ui';
|
||||
for(let i=0;i<4;i++){const y=padT+h*i/3;ctx.beginPath();ctx.moveTo(padL,y);ctx.lineTo(padL+w,y);ctx.stroke();ctx.fillText(String(Math.round(max*(1-i/3))),4,y+4)}
|
||||
const pts=visible.map((r,i)=>({x:padL+(visible.length<2?0:i*w/(visible.length-1)), y:padT+h-(Number(r.downloads||0)/max)*h, r}));
|
||||
ctx.strokeStyle='#4f46e5'; ctx.lineWidth=3; ctx.lineJoin='round'; ctx.lineCap='round'; ctx.beginPath(); pts.forEach((p,i)=>i?ctx.lineTo(p.x,p.y):ctx.moveTo(p.x,p.y)); ctx.stroke();
|
||||
ctx.fillStyle='#4f46e5'; pts.forEach(p=>{ctx.beginPath();ctx.arc(p.x,p.y,3,0,Math.PI*2);ctx.fill()});
|
||||
if(pts.length){ctx.fillStyle=muted; ctx.fillText(String(visible[0].date||visible[0].day||''),padL,rect.height-8); const last=String(visible.at(-1).date||visible.at(-1).day||''); ctx.fillText(last,Math.max(padL,rect.width-padR-ctx.measureText(last).width),rect.height-8)}
|
||||
const label=wrap?.querySelector('.fcbx-chart-window'); if(label) label.textContent = total ? `${st.start+1}-${st.end} / ${total}` : '无数据';
|
||||
let tip=wrap?.querySelector('.fcbx-tooltip'); if(wrap&&!tip){tip=document.createElement('div');tip.className='fcbx-tooltip';tip.style.opacity='0';wrap.appendChild(tip)}
|
||||
if(tip&&st.hover&&pts.length){const near=pts.reduce((a,b)=>Math.abs(b.x-st.hover.x)<Math.abs(a.x-st.hover.x)?b:a,pts[0]); tip.innerHTML=`<b>${esc(near.r.date||near.r.day||'-')}</b><br>下载:${Number(near.r.downloads||0)}<br>上传:${Number(near.r.uploads||0)}`; const left=Math.min(rect.width-80,Math.max(80,near.x)); const top=Math.max(42,near.y); tip.style.left=left+'px'; tip.style.top=top+'px'; tip.style.opacity='1'; ctx.fillStyle='#fff'; ctx.strokeStyle='#4f46e5'; ctx.lineWidth=3; ctx.beginPath();ctx.arc(near.x,near.y,6,0,Math.PI*2);ctx.fill();ctx.stroke();} else if(tip){tip.style.opacity='0'}
|
||||
};
|
||||
const bindChart = (canvas, rows=[]) => { if(!canvas||canvas.dataset.fcbxChartBound==='1') return; canvas.dataset.fcbxChartBound='1'; const zoom=(factor,center=.5)=>{const st=chartStates.get(canvas); if(!st)return; const total=rows.length,min=Math.min(total,7),cur=st.end-st.start,next=Math.max(min,Math.min(total,Math.round(cur*factor))); const anchor=st.start+cur*center; st.start=Math.round(anchor-next*center); st.start=Math.max(0,Math.min(st.start,total-next)); st.end=st.start+next; drawChart(canvas,rows)}; const pan=(dx)=>{const st=chartStates.get(canvas); if(!st)return; const rect=canvas.getBoundingClientRect(), cur=st.end-st.start, step=Math.round(-dx/Math.max(1,rect.width)*cur*1.15); if(!step)return; st.start=Math.max(0,Math.min(st.start+step,rows.length-cur)); st.end=st.start+cur; drawChart(canvas,rows)}; canvas.addEventListener('wheel',e=>{e.preventDefault(); const r=canvas.getBoundingClientRect(); zoom(e.deltaY>0?1.18:.84,(e.clientX-r.left)/r.width)},{passive:false}); canvas.addEventListener('pointerdown',e=>{e.preventDefault(); canvas.setPointerCapture?.(e.pointerId); const st=chartStates.get(canvas); st.drag={x:e.clientX,last:e.clientX};},{passive:false}); canvas.addEventListener('pointermove',e=>{const st=chartStates.get(canvas); const r=canvas.getBoundingClientRect(); st.hover={x:e.clientX-r.left}; if(st.drag){pan(e.clientX-st.drag.last); st.drag.last=e.clientX}else drawChart(canvas,rows)},{passive:false}); canvas.addEventListener('pointerup',e=>{const st=chartStates.get(canvas); if(st)st.drag=null},{passive:false}); canvas.addEventListener('pointerleave',()=>{const st=chartStates.get(canvas); if(st){st.hover=null;st.drag=null;drawChart(canvas,rows)}}); canvas.addEventListener('dblclick',e=>{e.preventDefault(); const st=chartStates.get(canvas); const win=Math.min(rows.length||1, innerWidth<640?35:70); st.start=Math.max(0,rows.length-win); st.end=rows.length; drawChart(canvas,rows)},{passive:false}); canvas.closest('.fcbx-panel')?.querySelector('[data-chart-zoom-in]')?.addEventListener('click',()=>zoom(.75)); canvas.closest('.fcbx-panel')?.querySelector('[data-chart-zoom-out]')?.addEventListener('click',()=>zoom(1.33)); canvas.closest('.fcbx-panel')?.querySelector('[data-chart-reset]')?.addEventListener('click',()=>{const st=chartStates.get(canvas); const win=Math.min(rows.length||1, innerWidth<640?35:70); st.start=Math.max(0,rows.length-win); st.end=rows.length; drawChart(canvas,rows)}); };
|
||||
const pagerHtml = id => `<div class="fcbx-official-pager" id="${id}-pager"><span class="fcbx-muted"></span><select id="${id}-size">${[5,10,20,50].map(n=>`<option value="${n}">${n}/页</option>`).join('')}</select><button id="${id}-prev">上一页</button><input class="fcbx-page-input" id="${id}-page" type="number" min="1" value="1"><button id="${id}-go">跳转</button><button id="${id}-next">下一页</button></div>`;
|
||||
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='<div class="fcbx-panel">加载统计中...</div>'; try{const data=await getAnalytics(); const t=data.totals||{}; box.innerHTML=`<div class="fcbx-bar">${dateSelect('fcbx-start',data.range?.start||daysAgo(120))}${dateSelect('fcbx-end',data.range?.end||today())}<button class="fcbx-btn" id="fcbx-refresh">刷新</button></div><div class="fcbx-dashboard-grid"><div class="fcbx-card"><div class="fcbx-card-label">总文件数</div><div class="fcbx-card-value">${t.currentFiles??0}</div></div><div class="fcbx-card"><div class="fcbx-card-label">历史文件</div><div class="fcbx-card-value">${t.historicalFiles??0}</div></div><div class="fcbx-card"><div class="fcbx-card-label">下载次数</div><div class="fcbx-card-value">${t.totalDownloads??0}</div></div><div class="fcbx-card"><div class="fcbx-card-label">下载流量</div><div class="fcbx-card-value">${fmtBytes(t.downloadTraffic)}</div></div></div><div class="fcbx-panel"><h3 class="fcbx-title">下载趋势</h3><canvas class="fcbx-chart" id="fcbx-chart"></canvas></div><div class="fcbx-panel"><h3 class="fcbx-title">热门下载 Top 5</h3><div class="fcbx-table-wrap"><table><thead><tr><th>文件</th><th>下载</th><th>流量</th><th>状态</th></tr></thead><tbody>${(data.topFiles||[]).map(f=>`<tr><td data-label="文件">${esc(f.name)}</td><td data-label="下载">${f.download_count||0}</td><td data-label="流量">${fmtBytes(f.download_traffic)}</td><td data-label="状态">${f.current?'现存':'历史'}</td></tr>`).join('')||'<tr><td class="fcbx-empty" colspan="4">暂无下载记录</td></tr>'}</tbody></table></div></div>`; $('#fcbx-refresh').onclick=async()=>{cache.data=null; await renderDashboard()}; drawChart($('#fcbx-chart'),data.daily||[]); }catch(e){box.innerHTML=`<div class="fcbx-panel">统计加载失败:${esc(e.message)}</div>`} };
|
||||
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=`<div class="fcbx-panel"><h3 class="fcbx-title">历史文件</h3><div class="fcbx-table-wrap"><table><thead><tr><th>文件</th><th>大小</th><th>取件码</th><th>上传日期</th><th>过期时间</th><th>删除时间</th><th>下载</th><th>流量</th><th>状态</th></tr></thead><tbody id="fcbx-history-body"><tr><td class="fcbx-empty" colspan="9">加载中...</td></tr></tbody></table></div>${pagerHtml('fcbx-history')}</div>`; 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)<new Date(); const status=f.deleted?'已删除':expired?'已过期':f.current?'现存':'历史'; const pill=status==='现存'?'fcbx-status-current':status==='已过期'?'fcbx-status-expired':'fcbx-status-deleted'; return `<tr><td data-label="文件">${esc(f.name)}</td><td data-label="大小">${fmtBytes(f.size)}</td><td data-label="取件码">${esc(f.code)}</td><td data-label="上传日期">${fmtDate(f.uploaded_at||f.created_at)}</td><td data-label="过期时间">${fmtDate(f.expired_at)}</td><td data-label="删除时间">${fmtDate(f.deleted_at)}</td><td data-label="下载">${f.download_count??0}</td><td data-label="流量">${fmtBytes(f.download_traffic)}</td><td data-label="状态"><span class="fcbx-status-pill ${pill}">${status}</span></td></tr>`}; attachPager('fcbx-history',rows,(slice,empty)=>{$('#fcbx-history-body').innerHTML=empty||slice.map(row).join('')},'<tr><td class="fcbx-empty" colspan="9">暂无历史文件</td></tr>'); }catch(e){$('#fcbx-history-body').innerHTML=`<tr><td class="fcbx-empty" colspan="9">历史文件加载失败:${esc(e.message)}</td></tr>`} };
|
||||
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='<div class="fcbx-panel">加载统计中...</div>'; try{const data=await getAnalytics(); const t=data.totals||{}; box.innerHTML=`<div class="fcbx-bar">${dateSelect('fcbx-start',data.range?.start||daysAgo(120))}${dateSelect('fcbx-end',data.range?.end||today())}<button class="fcbx-btn" id="fcbx-refresh">刷新</button></div><div class="fcbx-dashboard-grid"><div class="fcbx-card"><div class="fcbx-card-label">总文件数</div><div class="fcbx-card-value">${t.currentFiles??0}</div></div><div class="fcbx-card"><div class="fcbx-card-label">历史文件</div><div class="fcbx-card-value">${t.historicalFiles??0}</div></div><div class="fcbx-card"><div class="fcbx-card-label">下载次数</div><div class="fcbx-card-value">${t.totalDownloads??0}</div></div><div class="fcbx-card"><div class="fcbx-card-label">下载流量</div><div class="fcbx-card-value">${fmtBytes(t.downloadTraffic)}</div></div></div><div class="fcbx-panel"><h3 class="fcbx-title">下载趋势</h3><div class="fcbx-chart-controls"><span class="fcbx-muted fcbx-chart-window"></span><button class="fcbx-btn" data-chart-zoom-in>放大</button><button class="fcbx-btn" data-chart-zoom-out>缩小</button><button class="fcbx-btn" data-chart-reset>重置</button></div><div class="fcbx-chart-wrap"><canvas class="fcbx-chart" id="fcbx-chart"></canvas></div></div><div class="fcbx-panel"><h3 class="fcbx-title">热门下载 Top 5</h3><div class="fcbx-table-wrap"><table><thead><tr><th>文件</th><th>下载</th><th>流量</th><th>状态</th></tr></thead><tbody>${(data.topFiles||[]).map(f=>`<tr><td data-label="文件">${esc(f.name)}</td><td data-label="下载">${f.download_count||0}</td><td data-label="流量">${fmtBytes(f.download_traffic)}</td><td data-label="状态">${f.current?'现存':'历史'}</td></tr>`).join('')||'<tr><td class="fcbx-empty" colspan="4">暂无下载记录</td></tr>'}</tbody></table></div></div>`; $('#fcbx-refresh').onclick=async()=>{cache.data=null; await renderDashboard()}; drawChart($('#fcbx-chart'),data.daily||[]); bindChart($('#fcbx-chart'),data.daily||[]); }catch(e){box.innerHTML=`<div class="fcbx-panel">统计加载失败:${esc(e.message)}</div>`} };
|
||||
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=`<div class="fcbx-panel"><h3 class="fcbx-title">历史文件</h3><div class="fcbx-table-wrap"><table><thead><tr><th>文件</th><th>大小</th><th>取件码</th><th>上传日期</th><th>过期时间</th><th>删除时间</th><th>下载</th><th>流量</th><th>状态</th></tr></thead><tbody id="fcbx-history-body"><tr><td class="fcbx-empty" colspan="9">加载中...</td></tr></tbody></table></div>${pagerHtml('fcbx-history')}</div>`; 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)<new Date(); const status=f.deleted?'已删除':expired?'已过期':f.current?'现存':'历史'; const pill=status==='现存'?'fcbx-status-current':status==='已过期'?'fcbx-status-expired':'fcbx-status-deleted'; return `<tr class="${status==='现存'?'': 'fcbx-history-deleted'}"><td data-label="文件">${esc(f.name)}</td><td data-label="大小">${fmtBytes(f.size)}</td><td data-label="取件码">${esc(f.code)}</td><td data-label="上传日期">${fmtDate(f.uploaded_at||f.created_at)}</td><td data-label="过期时间">${fmtDate(f.expired_at)}</td><td data-label="删除时间">${fmtDate(f.deleted_at)}</td><td data-label="下载">${f.download_count??0}</td><td data-label="流量">${fmtBytes(f.download_traffic)}</td><td data-label="状态"><span class="fcbx-status-pill ${pill}">${status}</span></td></tr>`}; attachPager('fcbx-history',rows,(slice,empty)=>{$('#fcbx-history-body').innerHTML=empty||slice.map(row).join('')},'<tr><td class="fcbx-empty" colspan="9">暂无历史文件</td></tr>'); }catch(e){$('#fcbx-history-body').innerHTML=`<tr><td class="fcbx-empty" colspan="9">历史文件加载失败:${esc(e.message)}</td></tr>`} };
|
||||
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=`<span class="fcbx-muted">第 ${Math.min(page,pages)} / ${pages} 页,共 ${total} 条</span><select id="fcbx-allfiles-size">${[10,20,50,100].map(n=>`<option value="${n}" ${n===size?'selected':''}>${n}/页</option>`).join('')}</select><button id="fcbx-allfiles-prev" ${page<=1?'disabled':''}>上一页</button><input id="fcbx-allfiles-page" type="number" min="1" max="${pages}" value="${Math.min(page,pages)}"><button id="fcbx-allfiles-go">跳转</button><button id="fcbx-allfiles-next" ${page>=pages?'disabled':''}>下一页</button>`; 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(); }); };
|
||||
|
||||
const preCollapseFilesFilter = () => { if(!location.hash.includes('/admin/files'))return; const cards=$$('main section, main div[class*="rounded"]'); const card=cards.find(el=>!el.querySelector('table') && (el.querySelector('input')||el.querySelector('select')) && /搜索|筛选|Search|Filter|文件名|取件码/.test(el.innerText||'')); if(!card||card.dataset.fcbxFilterReady==='1')return; card.dataset.fcbxFilterReady='1'; card.classList.add('fcbx-filter-card'); const kids=[...card.children]; const body=kids.length>1?kids.slice(1):kids; body.forEach(el=>el.classList.add('fcbx-filter-body-collapsed')); const btn=document.createElement('button'); btn.type='button'; btn.className='fcbx-btn fcbx-filter-toggle'; btn.textContent='展开筛选'; btn.onclick=()=>{const collapsed=body.some(el=>el.classList.contains('fcbx-filter-body-collapsed')); body.forEach(el=>el.classList.toggle('fcbx-filter-body-collapsed',!collapsed)); btn.textContent=collapsed?'收起筛选':'展开筛选'}; (card.firstElementChild||card).appendChild(btn); };
|
||||
let scheduled=false,lastRun=0; const run=()=>{ if(scheduled)return; scheduled=true; requestAnimationFrame(()=>{scheduled=false; const now=Date.now(); ensureStyle(); updateMode(); bindDrawer(); preCollapseFilesFilter(); 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('</body>', '<script src="/assets/fcb-analytics.js?v=20260605-test-full07"></script>\n</body>')
|
||||
text = text.replace('</body>', '<script src="/assets/fcb-analytics.js?v=20260605-test-full08"></script>\n</body>')
|
||||
else:
|
||||
text = re.sub(r'<script src="/assets/fcb-analytics\.js(?:\?v=[^"]+)?"></script>', '<script src="/assets/fcb-analytics.js?v=20260605-test-full07"></script>', text)
|
||||
text = re.sub(r'<script src="/assets/fcb-analytics\.js(?:\?v=[^"]+)?"></script>', '<script src="/assets/fcb-analytics.js?v=20260605-test-full08"></script>', text)
|
||||
index.write_text(text, encoding='utf-8')
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user