From 96e9404a763c0eb47fad5919708cfacad4042fa7 Mon Sep 17 00:00:00 2001 From: xunbu Date: Sun, 11 Jan 2026 19:31:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96html=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E7=94=9F=E6=88=90=E6=95=88=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docutranslate/app.py | 40 +++++++-- docutranslate/template/markdown.html | 120 ++++++++++++++++++--------- 更新日志.txt | 1 + 3 files changed, 114 insertions(+), 47 deletions(-) diff --git a/docutranslate/app.py b/docutranslate/app.py index 3d6ef63..fe943af 100644 --- a/docutranslate/app.py +++ b/docutranslate/app.py @@ -254,20 +254,42 @@ async def lifespan(app: FastAPI): global_logger.propagate = False global_logger.setLevel(logging.INFO) print("应用启动完成,多任务状态已初始化。") - print(f"服务接口文档: http://127.0.0.1:{app.state.port_to_use}/docs") - print(f"请用浏览器访问 http://127.0.0.1:{app.state.port_to_use}\n") - yield - # 清理任何可能残留的临时目录 + if hasattr(app.state, "port_to_use"): + print(f"服务接口文档: http://127.0.0.1:{app.state.port_to_use}/docs") + print(f"请用浏览器访问 http://127.0.0.1:{app.state.port_to_use}\n") + yield # 应用运行中... + + # --- 关闭阶段 --- + print("正在关闭应用,开始清理资源...") + + # 1. 优先取消所有正在进行的后台任务 + # 如果不取消,uvicorn 会等待它们完成,导致进程挂起 + pending_tasks = [] + for task_id, task_state in tasks_state.items(): + task_ref = task_state.get("current_task_ref") + if task_ref and not task_ref.done(): + print(f"[{task_id}] 检测到未完成任务,正在强制取消...") + task_ref.cancel() + pending_tasks.append(task_ref) + + # 等待所有任务完成取消操作 (捕获 CancelledError 避免报错) + if pending_tasks: + await asyncio.gather(*pending_tasks, return_exceptions=True) + + # 2. 关闭 HTTP 客户端连接 + await httpx_client.aclose() + + # 3. 清理所有任务的临时目录 for task_id, task_state in tasks_state.items(): temp_dir = task_state.get("temp_dir") if temp_dir and os.path.isdir(temp_dir): try: - shutil.rmtree(temp_dir) - print(f"应用关闭,清理任务 '{task_id}' 的临时目录: {temp_dir}") + # ignore_errors=True 防止 Windows 上因文件被短暂占用导致的删除失败报错 + shutil.rmtree(temp_dir, ignore_errors=True) + print(f"[{task_id}] 临时目录已清理: {temp_dir}") except Exception as e: - print(f"清理任务 '{task_id}' 的临时目录 '{temp_dir}' 时出错: {e}") - await httpx_client.aclose() - print("应用关闭,资源已清理。") + print(f"[{task_id}] 清理临时目录 '{temp_dir}' 时出错: {e}") + print("应用关闭,资源已彻底释放。") # --- FastAPI 应用和路由设置 --- diff --git a/docutranslate/template/markdown.html b/docutranslate/template/markdown.html index e2e6a27..4b04b06 100644 --- a/docutranslate/template/markdown.html +++ b/docutranslate/template/markdown.html @@ -63,6 +63,7 @@ overflow-y: auto; transform: translateX(-100%); transition: transform 0.2s ease; + will-change: transform; /* 性能优化提示 */ } #toc-panel.show { transform: translateX(0); } @@ -77,10 +78,9 @@ font-size: 13px; border-radius: 3px; cursor: pointer; + user-select: none; /* 防止频繁点击时选中文本 */ } .toc-link:hover { background: #f5f5f5; } - .toc-link.active { background: #007bff; color: #fff; } - .toc-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .toc-expander { width: 18px; @@ -92,12 +92,12 @@ cursor: pointer; flex-shrink: 0; } - .toc-expander:hover { color: #333; } + .toc-expander:hover { color: #333; background: rgba(0,0,0,0.05); border-radius: 2px; } .toc-children { display: none; padding-left: 12px; } .toc-children.expanded { display: block; } - .toc-item[data-level="1"] > .toc-link { font-weight: 500; } + .toc-item[data-level="1"] > .toc-link { font-weight: 500; color: #333; } .toc-item[data-level="2"] > .toc-link { padding-left: 20px; } .toc-item[data-level="3"] > .toc-link { padding-left: 30px; } .toc-item[data-level="4"] > .toc-link { padding-left: 40px; } @@ -115,81 +115,125 @@ (function() { const panel = document.getElementById('toc-panel'); const list = document.getElementById('toc-list'); - let headings = []; - function init() { - headings = Array.from(document.querySelectorAll('main h1, main h2, main h3, main h4, main h5, main h6')); + // UI交互逻辑 + window.toggleToc = function() { panel.classList.toggle('show'); }; + + // 事件委托:处理目录点击与折叠 + list.addEventListener('click', function(e) { + // 1. 处理折叠/展开 (+/- 按钮) + if (e.target.classList.contains('toc-expander')) { + e.stopPropagation(); + const ul = e.target.closest('li').querySelector('.toc-children'); + if (ul) { + ul.classList.toggle('expanded'); + e.target.textContent = ul.classList.contains('expanded') ? '-' : '+'; + } + return; + } + + // 2. 处理点击跳转 + const link = e.target.closest('.toc-link'); + if (link && link.dataset.target) { + const targetEl = document.getElementById(link.dataset.target); + if (targetEl) { + targetEl.scrollIntoView({ behavior: 'smooth' }); + // 在移动端点击后自动关闭面板 + if (window.innerWidth < 768) panel.classList.remove('show'); + } + } + }); + + // 核心生成逻辑 + function buildToc() { + const headings = Array.from(document.querySelectorAll('main h1, main h2, main h3, main h4, main h5, main h6')); if (headings.length === 0) return; + // 确保所有标题都有ID headings.forEach((h, i) => { if (!h.id) h.id = 'h-' + i; }); + // 构建树状结构 const root = { level: 0, children: [] }; const stack = [root]; + headings.forEach(h => { const level = parseInt(h.tagName[1]); const item = { id: h.id, text: h.textContent.trim(), level, children: [] }; - while (stack.length > 1 && stack[stack.length - 1].level >= level) stack.pop(); + + while (stack.length > 1 && stack[stack.length - 1].level >= level) { + stack.pop(); + } stack[stack.length - 1].children.push(item); stack.push(item); }); - render(root.children, list); + + // 使用 DocumentFragment 进行离线 DOM 构建 (性能关键点) + const fragment = document.createDocumentFragment(); + renderRecursive(root.children, fragment); + list.appendChild(fragment); } - function render(items, parent) { + // 递归渲染函数 + function renderRecursive(items, container) { items.forEach(item => { const li = document.createElement('li'); li.className = 'toc-item'; li.dataset.level = item.level; + // 链接部分 const div = document.createElement('div'); div.className = 'toc-link'; - div.innerHTML = `${item.text}`; + div.dataset.target = item.id; // 存储目标ID供事件委托使用 + const textSpan = document.createElement('span'); + textSpan.className = 'toc-text'; + textSpan.textContent = item.text; + div.appendChild(textSpan); + + // 折叠按钮 + let expander = null; if (item.children.length) { - const expander = document.createElement('span'); + expander = document.createElement('span'); expander.className = 'toc-expander'; - expander.textContent = '+'; - expander.onclick = function(e) { e.stopPropagation(); toggle(this); }; + // 默认展开一级菜单,其他折叠 + const isRoot = item.level === 1; + expander.textContent = isRoot ? '-' : '+'; div.appendChild(expander); } - - div.onclick = function() { scrollTo(item.id); }; li.appendChild(div); + // 子菜单 if (item.children.length) { const ul = document.createElement('ul'); ul.className = 'toc-children'; - render(item.children, ul); + if (item.level === 1) ul.classList.add('expanded'); + + renderRecursive(item.children, ul); li.appendChild(ul); - if (item.level === 1) { - ul.classList.add('expanded'); - expander.textContent = '-'; - } } - parent.appendChild(li); + + container.appendChild(li); }); } - function toggle(btn) { - const ul = btn.closest('li').querySelector('.toc-children'); - ul.classList.toggle('expanded'); - btn.textContent = ul.classList.contains('expanded') ? '-' : '+'; - } + // 调度执行:优先显示内容,空闲时生成目录 + const idleCallback = window.requestIdleCallback || function(cb) { setTimeout(cb, 10); }; + idleCallback(buildToc); - function scrollTo(id) { - const el = document.getElementById(id); - if (el) el.scrollIntoView({ behavior: 'smooth' }); - } - - window.toggleToc = function() { panel.classList.toggle('show'); }; - - init(); })(); +// 辅助脚本 setTimeout(() => { - for (const e of document.getElementsByClassName('katex-error')) e.innerHTML = e.title; + // 修复 Katex 错误显示 + const errors = document.getElementsByClassName('katex-error'); + for (let i = 0; i < errors.length; i++) { + errors[i].innerHTML = errors[i].title; + } }, 200); -mermaid.initialize({ securityLevel: 'loose', startOnLoad: true }); +// 确保 Mermaid 存在再初始化 +if (typeof mermaid !== 'undefined') { + mermaid.initialize({ securityLevel: 'loose', startOnLoad: true }); +} - + \ No newline at end of file diff --git a/更新日志.txt b/更新日志.txt index 8f1e9f2..6467931 100644 --- a/更新日志.txt +++ b/更新日志.txt @@ -7,6 +7,7 @@ v1.6.2版 2025.1.11 - 自动生成术语表不覆盖用户术语表,最终下载的是合并术语表 优化 - 移除tiktoken依赖 +- 优化html目录生成效率 - 其它优化 ---------------- v1.6.0版 2025.12.31