@@ -0,0 +1,134 @@
#!/usr/bin/env python3
from pathlib import Path
import gzip
ASSETS_ROOT = Path ( ' /app ' )
def rewrite ( path : Path , text : str ) - > None :
path . write_text ( text , encoding = ' utf-8 ' )
gz_path = path . with_suffix ( path . suffix + ' .gz ' )
if gz_path . exists ( ) :
with gzip . open ( gz_path , ' wb ' , compresslevel = 9 ) as gz :
gz . write ( text . encode ( ' utf-8 ' ) )
def patch_file_manage ( path : Path ) - > bool :
text = path . read_text ( encoding = ' utf-8 ' )
old = text
if ' detailTimeline:It(s.timeline||[]) ' in text and ' fcbxTimelineCollapsed ' not in text :
text = text . replace (
' detailTimeline:It(s.timeline||[])}},nt=t=> ' ,
' detailTimeline:It(s.timeline||[]),fcbxTimelineCollapsed:!0}},nt=t=> ' ,
1 ,
)
render_old = ' N(e(b).detailTimeline,(n,K)=>(p(),f( " div " , { key:`$ {n.key} -$ {K} `,class: " grid grid-cols-[auto_minmax(0,1fr)] gap-3 " } '
render_new = ' N((e(b).fcbxTimelineCollapsed&&e(b).detailTimeline.filter(ga=>ga.key=== " retrieved_event " ).length>3)?e(b).detailTimeline.filter((ga,Qa)=>ga.key!== " retrieved_event " ||e(b).detailTimeline.filter(Ja=>Ja.key=== " retrieved_event " ).indexOf(ga)<3):e(b).detailTimeline,(n,K)=>(p(),f( " div " , { key:`$ {n.key} -$ {K} `,class: " grid grid-cols-[auto_minmax(0,1fr)] gap-3 " } '
if render_old in text and ' ga.key=== " retrieved_event " ' not in text :
text = text . replace ( render_old , render_new , 1 )
close_old = ' ]))),128))],2)])):j( " " ,!0),l( " section " ,kn, '
close_new = ' ]))),128)),e(b).detailTimeline.filter(n=>n.key=== " retrieved_event " ).length>3?(p(),f( " button " , { key:0,type: " button " ,class:o([ " mt-3 rounded-lg border px-3 py-2 text-sm font-medium transition " ,[e(u)? " border-gray-600 text-gray-200 hover:bg-gray-700 " : " border-gray-200 text-gray-700 hover:bg-gray-50 " ]]),onClick:m[99]||(m[99]=n=>e(b).fcbxTimelineCollapsed=!e(b).fcbxTimelineCollapsed)},r(e(b).fcbxTimelineCollapsed?`展开全部取件记录(共 $ { e(b).detailTimeline.filter(n=>n.key=== " retrieved_event " ).length} 次) " : " 收起取件记录 " ),3)):j( " " ,!0)],2)])):j( " " ,!0),l( " section " ,kn, '
if close_old in text and ' 展开全部取件记录 ' not in text :
text = text . replace ( close_old , close_new , 1 )
if text != old :
rewrite ( path , text )
return True
return False
def patch_clipboard ( path : Path ) - > bool :
text = path . read_text ( encoding = ' utf-8 ' )
old = text
marker = ' const y=async(a,e= {} )=> { '
if marker not in text or ' fcbx-copy-fallback ' in text :
return False
start = text . index ( marker )
end_marker = ' },T=async(a,e= {} )=> { '
end = text . index ( end_marker , start ) + 1
replacement = ''' const y=async(a,e= {} )=> { const { successMsg:t= " 复制成功 " ,errorMsg:n= " 复制失败,请手动复制 " ,showMsg:s=!0,notify:c}=e,l=(r,d)=> { s&&c?.(r,d)},q=()=> { try { window.prompt( " 复制下面内容 " ,a)}catch {} };try { if(navigator.clipboard&&window.isSecureContext)try { await navigator.clipboard.writeText(a);return l(t, " success " ),!0}catch {} const r=document.createElement( " textarea " );r.value=a,r.setAttribute( " readonly " , " " );r.className= " fcbx-copy-fallback " ;r.style.cssText= " position:fixed;left:0;top:0;width:1px;height:1px;opacity:0;z-index:-1 " ;document.body.appendChild(r);r.focus();r.select();r.setSelectionRange(0,r.value.length);let d=!1;try { d=document.execCommand( " copy " )}finally { document.body.removeChild(r)}if(d)return l(t, " success " ),!0;q();l( " 已打开手动复制窗口 " , " warning " );return!1}catch(r) { return console.error( " 复制失败: " ,r),q(),l(n, " error " ),!1}} '''
text = text [ : start ] + replacement + text [ end : ]
if text != old :
rewrite ( path , text )
return True
return False
def write_detail_collapse_script ( theme_dir : Path ) - > bool :
assets = theme_dir / ' assets '
index = theme_dir / ' index.html '
if not assets . exists ( ) or not index . exists ( ) :
return False
js_path = assets / ' fcb-detail-collapse.js '
js = r ''' (() => {
const EVENT_RE = /(第 \ s* \ d+ \ s*次取件|取件 \ s*[## ] \ s* \ d+|Retrieval \ s*# \ s* \ d+)/i;
const LIFE_RE = /(生命周期|Lifecycle)/i;
const markRows = () => {
document.querySelectorAll( ' section ' ).forEach(section => {
const title = section.querySelector( ' h1,h2,h3,h4,h5 ' );
if (!title || !LIFE_RE.test(title.textContent || ' ' )) return;
const rows = [...section.querySelectorAll( ' div.grid ' )].filter(row => EVENT_RE.test(row.textContent || ' ' ));
if (rows.length <= 3) return;
if (section.dataset.fcbxLifecycleReady !== ' 1 ' ) {
section.dataset.fcbxLifecycleReady = ' 1 ' ;
section.dataset.fcbxLifecycleCollapsed = ' 1 ' ;
const btn = document.createElement( ' button ' );
btn.type = ' button ' ;
btn.className = ' fcbx-life-toggle mt-3 rounded-lg border px-3 py-2 text-sm font-medium transition ' ;
btn.style.cssText = ' border-color:rgba(148,163,184,.55);display:inline-flex;align-items:center;gap:.35rem; ' ;
btn.addEventListener( ' click ' , event => {
event.preventDefault();
event.stopPropagation();
section.dataset.fcbxLifecycleCollapsed = section.dataset.fcbxLifecycleCollapsed === ' 1 ' ? ' 0 ' : ' 1 ' ;
apply(section);
});
(section.lastElementChild || section).appendChild(btn);
}
apply(section);
});
};
const apply = section => {
const rows = [...section.querySelectorAll( ' div.grid ' )].filter(row => EVENT_RE.test(row.textContent || ' ' ));
const collapsed = section.dataset.fcbxLifecycleCollapsed !== ' 0 ' ;
rows.forEach((row, idx) => { row.style.display = collapsed && idx >= 3 ? ' none ' : ' ' ; });
const btn = section.querySelector( ' .fcbx-life-toggle ' );
if (btn) btn.textContent = collapsed ? `展开全部取件记录(共 $ {rows.length} 次)` : ' 收起取件记录 ' ;
};
let timer = 0;
const schedule = () => { clearTimeout(timer); timer = setTimeout(markRows, 80); };
window.addEventListener( ' hashchange ' , schedule);
document.addEventListener( ' DOMContentLoaded ' , schedule);
new MutationObserver(schedule).observe(document.documentElement, { childList: true, subtree: true });
schedule();
})(); '''
rewrite ( js_path , js )
html = index . read_text ( encoding = ' utf-8 ' )
tag = ' <script src= " assets/fcb-detail-collapse.js?v=20260709 " ></script> '
if ' fcb-detail-collapse.js ' not in html :
html = html . replace ( ' </body> ' , tag + ' \n </body> ' ) if ' </body> ' in html else html + ' \n ' + tag + ' \n '
index . write_text ( html , encoding = ' utf-8 ' )
gz = index . with_suffix ( index . suffix + ' .gz ' )
if gz . exists ( ) :
with gzip . open ( gz , ' wb ' , compresslevel = 9 ) as fh :
fh . write ( html . encode ( ' utf-8 ' ) )
return True
def main ( ) - > None :
changed = [ ]
theme = ASSETS_ROOT / ' themes/2026 '
assets = theme / ' assets '
if assets . exists ( ) :
# 只用独立 DOM 脚本处理生命周期收起,避免再改编译后的 FileManageView,
# 否则 Vue 内部状态和外部补丁会互相抢状态,导致展开/收起需要点多次。
for path in assets . glob ( ' clipboard-*.js ' ) :
if patch_clipboard ( path ) :
changed . append ( str ( path . relative_to ( ASSETS_ROOT ) ) )
if write_detail_collapse_script ( theme ) :
changed . append ( str ( ( assets / ' fcb-detail-collapse.js ' ) . relative_to ( ASSETS_ROOT ) ) )
if not changed :
raise SystemExit ( ' admin timeline/clipboard patch markers not found or already patched ' )
print ( ' patched admin assets: ' , ' , ' . join ( changed ) )
if __name__ == ' __main__ ' :
main ( )