✨ 恢复测试环境交互与更新脚本
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 #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"],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 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}}`;
|
@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 $ = (s, r=document) => r.querySelector(s); const $$ = (s, r=document) => [...r.querySelectorAll(s)];
|
||||||
const esc = v => String(v ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
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 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 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 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 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 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 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><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 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 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}); };
|
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();
|
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)
|
rewrite_gzip(assets / 'fcb-analytics.js', js)
|
||||||
index = Path('/app/themes/2024/index.html')
|
index = Path('/app/themes/2024/index.html')
|
||||||
text = index.read_text(encoding='utf-8')
|
text = index.read_text(encoding='utf-8')
|
||||||
if '/assets/fcb-analytics.js' not in text:
|
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:
|
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')
|
index.write_text(text, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_URL="${REPO_URL:-http://127.0.0.1:3000/orion/filecodebox.git}"
|
||||||
|
TOKEN_FILE="${TOKEN_FILE:-/root/.cache/gitea-filecodebox-token}"
|
||||||
|
PROD_REPO_DIR="${PROD_REPO_DIR:-/opt/src/filecodebox-production}"
|
||||||
|
PROD_DIR="${PROD_DIR:-/opt/1panel/apps/filecodebox/filecodebox}"
|
||||||
|
BRANCH="${BRANCH:-master}"
|
||||||
|
IMAGE="${IMAGE:-local/filecodebox:master}"
|
||||||
|
PORT="${PORT:-40157}"
|
||||||
|
|
||||||
|
log() { printf '[FileCodeBox更新] %s\n' "$*"; }
|
||||||
|
fail() { printf '[FileCodeBox更新失败] %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v git >/dev/null 2>&1 || fail '缺少 git'
|
||||||
|
command -v docker >/dev/null 2>&1 || fail '缺少 docker'
|
||||||
|
command -v curl >/dev/null 2>&1 || fail '缺少 curl'
|
||||||
|
[[ -d "$PROD_DIR" ]] || fail "生产目录不存在:$PROD_DIR"
|
||||||
|
|
||||||
|
AUTH_REPO_URL="$REPO_URL"
|
||||||
|
if [[ -f "$TOKEN_FILE" && "$REPO_URL" == http://127.0.0.1:3000/* ]]; then
|
||||||
|
TOKEN="$(tr -d '\n' < "$TOKEN_FILE")"
|
||||||
|
AUTH_REPO_URL="http://orion:${TOKEN}@127.0.0.1:3000/orion/filecodebox.git"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "拉取 $BRANCH 分支"
|
||||||
|
if [[ ! -d "$PROD_REPO_DIR/.git" ]]; then
|
||||||
|
rm -rf "$PROD_REPO_DIR"
|
||||||
|
git clone "$AUTH_REPO_URL" "$PROD_REPO_DIR"
|
||||||
|
fi
|
||||||
|
cd "$PROD_REPO_DIR"
|
||||||
|
git remote set-url origin "$AUTH_REPO_URL"
|
||||||
|
git fetch origin "$BRANCH"
|
||||||
|
git checkout -B "$BRANCH" "origin/$BRANCH"
|
||||||
|
git reset --hard "origin/$BRANCH"
|
||||||
|
COMMIT="$(git rev-parse HEAD)"
|
||||||
|
log "当前提交:$COMMIT"
|
||||||
|
|
||||||
|
cd "$PROD_DIR"
|
||||||
|
mkdir -p backups
|
||||||
|
TS="$(date +%Y%m%d%H%M%S)"
|
||||||
|
cp -a docker-compose.yml "backups/docker-compose.yml.before-update.$TS"
|
||||||
|
[[ -f data/filecodebox.db ]] && cp -a data/filecodebox.db "backups/filecodebox.db.before-update.$TS"
|
||||||
|
|
||||||
|
log "同步 1Panel Compose 为本地 master 构建"
|
||||||
|
python3 - <<PY
|
||||||
|
from pathlib import Path
|
||||||
|
p=Path('docker-compose.yml')
|
||||||
|
s=p.read_text()
|
||||||
|
lines=[]
|
||||||
|
for line in s.splitlines():
|
||||||
|
stripped=line.strip()
|
||||||
|
if stripped.startswith('image:'):
|
||||||
|
indent=line[:len(line)-len(line.lstrip())]
|
||||||
|
lines.append(indent+'image: $IMAGE')
|
||||||
|
lines.append(indent+'build:')
|
||||||
|
lines.append(indent+' context: $PROD_REPO_DIR')
|
||||||
|
elif stripped.startswith('build:') or stripped.startswith('context:'):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
lines.append(line)
|
||||||
|
p.write_text('\n'.join(lines)+'\n')
|
||||||
|
PY
|
||||||
|
|
||||||
|
log "构建并重建生产容器"
|
||||||
|
docker compose config >/dev/null
|
||||||
|
docker compose up -d --build filecodebox
|
||||||
|
|
||||||
|
log "等待服务恢复"
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if curl -fsS "http://127.0.0.1:$PORT/" >/tmp/filecodebox-update-index.html; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
[[ "$i" == "60" ]] && { docker logs --tail 120 filecodebox || true; fail '服务未恢复'; }
|
||||||
|
done
|
||||||
|
|
||||||
|
if grep -q 'fcb-analytics.js' /tmp/filecodebox-update-index.html; then
|
||||||
|
fail '生产页面检测到测试增强脚本,已停止'
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "同步 1Panel 安装实例记录"
|
||||||
|
python3 - <<'PY'
|
||||||
|
import sqlite3
|
||||||
|
from pathlib import Path
|
||||||
|
DB='/opt/1panel/db/agent.db'
|
||||||
|
compose=Path('/opt/1panel/apps/filecodebox/filecodebox/docker-compose.yml').read_text()
|
||||||
|
conn=sqlite3.connect(DB)
|
||||||
|
conn.execute("update app_installs set docker_compose=?, status='Running', container_name='filecodebox', service_name='filecodebox', http_port=40157, updated_at=datetime('now','localtime') where name='filecodebox'", (compose,))
|
||||||
|
conn.commit(); conn.close()
|
||||||
|
PY
|
||||||
|
|
||||||
|
(systemctl restart 1panel-agent >/dev/null 2>&1 || true)
|
||||||
|
log "完成:生产已更新为 $BRANCH@$COMMIT,数据目录保持不变"
|
||||||
|
docker ps --filter name=filecodebox --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
|
||||||
Reference in New Issue
Block a user