176 lines
8.9 KiB
Python
176 lines
8.9 KiB
Python
#!/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()
|