Files
filecodebox/scripts/patch_admin_timeline_ui.py

176 lines
8.9 KiB
Python
Raw Permalink 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.
#!/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-lifecycle-collapse-v3.js'
js = r'''(() => {
const LIFE_RE = /(生命周期|Lifecycle)/i;
const STATE = new WeakMap();
const getSection = section => {
const title = section.querySelector('h1,h2,h3,h4,h5');
if (!title || !LIFE_RE.test(title.textContent || '')) return null;
const box = title.nextElementSibling;
if (!box) return null;
return { title, box };
};
const getRows = section => {
const ctx = getSection(section);
if (!ctx) return [];
return [...ctx.box.children].filter(el => el instanceof HTMLElement);
};
const apply = section => {
const ctx = getSection(section);
if (!ctx) return;
const rows = [...ctx.box.children].filter(el => el instanceof HTMLElement);
if (rows.length <= 3) return;
const expanded = STATE.get(section) === true;
section.classList.toggle('fcbx-life-expanded', expanded);
rows.forEach((row, idx) => {
const shouldHide = idx >= 3 && !expanded;
row.classList.toggle('fcbx-life-row-extra', idx >= 3);
if (shouldHide) {
if (row.style.display !== 'none') row.style.setProperty('display', 'none', 'important');
} else if (row.style.display) {
row.style.removeProperty('display');
}
});
const btn = section.querySelector(':scope > .fcbx-life-toggle') || section.querySelector('.fcbx-life-toggle');
if (btn) {
btn.textContent = expanded ? '收起生命周期记录' : `展开全部生命周期记录(共 ${rows.length} 条)`;
btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
}
};
const ensureSection = section => {
const ctx = getSection(section);
if (!ctx) return;
const rows = [...ctx.box.children].filter(el => el instanceof HTMLElement);
if (rows.length <= 3) return;
if (!STATE.has(section)) STATE.set(section, false);
let btn = section.querySelector(':scope > .fcbx-life-toggle');
if (!btn) {
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;touch-action:manipulation;user-select:none;cursor:pointer;';
btn.onclick = event => {
event.preventDefault();
event.stopPropagation();
STATE.set(section, STATE.get(section) !== true);
apply(section);
};
section.appendChild(btn);
}
apply(section);
};
const markRows = root => {
const scope = root?.querySelectorAll ? root : document;
scope.querySelectorAll('section').forEach(ensureSection);
};
let raf = 0;
const schedule = root => { cancelAnimationFrame(raf); raf = requestAnimationFrame(() => markRows(root)); };
window.addEventListener('hashchange', () => schedule(document));
document.addEventListener('DOMContentLoaded', () => schedule(document));
new MutationObserver(records => {
for (const rec of records) {
if ([...rec.addedNodes].some(n => n.nodeType === 1 && ((n.matches?.('section')) || n.querySelector?.('section')))) {
schedule(document);
break;
}
}
}).observe(document.body || document.documentElement, { childList: true, subtree: true });
schedule(document);
})();'''
rewrite(js_path, js)
html = index.read_text(encoding='utf-8')
# Remove old detail-collapse injection so browsers cannot keep loading the stale “取件记录” script.
html = html.replace('<script src="assets/fcb-detail-collapse.js?v=20260709"></script>\n', '')
html = html.replace('<script src="assets/fcb-detail-collapse.js?v=20260709"></script>', '')
html = html.replace('<script src="assets/fcb-lifecycle-collapse.js?v=20260709-2"></script>\n', '')
html = html.replace('<script src="assets/fcb-lifecycle-collapse.js?v=20260709-2"></script>', '')
tag = '<script src="assets/fcb-lifecycle-collapse-v3.js"></script>'
if 'fcb-lifecycle-collapse-v3.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-lifecycle-collapse-v3.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()