优化html目录生成效率
This commit is contained in:
@@ -254,20 +254,42 @@ async def lifespan(app: FastAPI):
|
|||||||
global_logger.propagate = False
|
global_logger.propagate = False
|
||||||
global_logger.setLevel(logging.INFO)
|
global_logger.setLevel(logging.INFO)
|
||||||
print("应用启动完成,多任务状态已初始化。")
|
print("应用启动完成,多任务状态已初始化。")
|
||||||
|
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}/docs")
|
||||||
print(f"请用浏览器访问 http://127.0.0.1:{app.state.port_to_use}\n")
|
print(f"请用浏览器访问 http://127.0.0.1:{app.state.port_to_use}\n")
|
||||||
yield
|
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():
|
for task_id, task_state in tasks_state.items():
|
||||||
temp_dir = task_state.get("temp_dir")
|
temp_dir = task_state.get("temp_dir")
|
||||||
if temp_dir and os.path.isdir(temp_dir):
|
if temp_dir and os.path.isdir(temp_dir):
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(temp_dir)
|
# ignore_errors=True 防止 Windows 上因文件被短暂占用导致的删除失败报错
|
||||||
print(f"应用关闭,清理任务 '{task_id}' 的临时目录: {temp_dir}")
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||||
|
print(f"[{task_id}] 临时目录已清理: {temp_dir}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"清理任务 '{task_id}' 的临时目录 '{temp_dir}' 时出错: {e}")
|
print(f"[{task_id}] 清理临时目录 '{temp_dir}' 时出错: {e}")
|
||||||
await httpx_client.aclose()
|
print("应用关闭,资源已彻底释放。")
|
||||||
print("应用关闭,资源已清理。")
|
|
||||||
|
|
||||||
|
|
||||||
# --- FastAPI 应用和路由设置 ---
|
# --- FastAPI 应用和路由设置 ---
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
|
will-change: transform; /* 性能优化提示 */
|
||||||
}
|
}
|
||||||
#toc-panel.show { transform: translateX(0); }
|
#toc-panel.show { transform: translateX(0); }
|
||||||
|
|
||||||
@@ -77,10 +78,9 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
user-select: none; /* 防止频繁点击时选中文本 */
|
||||||
}
|
}
|
||||||
.toc-link:hover { background: #f5f5f5; }
|
.toc-link:hover { background: #f5f5f5; }
|
||||||
.toc-link.active { background: #007bff; color: #fff; }
|
|
||||||
|
|
||||||
.toc-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.toc-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.toc-expander {
|
.toc-expander {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
@@ -92,12 +92,12 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
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 { display: none; padding-left: 12px; }
|
||||||
.toc-children.expanded { display: block; }
|
.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="2"] > .toc-link { padding-left: 20px; }
|
||||||
.toc-item[data-level="3"] > .toc-link { padding-left: 30px; }
|
.toc-item[data-level="3"] > .toc-link { padding-left: 30px; }
|
||||||
.toc-item[data-level="4"] > .toc-link { padding-left: 40px; }
|
.toc-item[data-level="4"] > .toc-link { padding-left: 40px; }
|
||||||
@@ -115,81 +115,125 @@
|
|||||||
(function() {
|
(function() {
|
||||||
const panel = document.getElementById('toc-panel');
|
const panel = document.getElementById('toc-panel');
|
||||||
const list = document.getElementById('toc-list');
|
const list = document.getElementById('toc-list');
|
||||||
let headings = [];
|
|
||||||
|
|
||||||
function init() {
|
// UI交互逻辑
|
||||||
headings = Array.from(document.querySelectorAll('main h1, main h2, main h3, main h4, main h5, main h6'));
|
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;
|
if (headings.length === 0) return;
|
||||||
|
|
||||||
|
// 确保所有标题都有ID
|
||||||
headings.forEach((h, i) => { if (!h.id) h.id = 'h-' + i; });
|
headings.forEach((h, i) => { if (!h.id) h.id = 'h-' + i; });
|
||||||
|
|
||||||
|
// 构建树状结构
|
||||||
const root = { level: 0, children: [] };
|
const root = { level: 0, children: [] };
|
||||||
const stack = [root];
|
const stack = [root];
|
||||||
|
|
||||||
headings.forEach(h => {
|
headings.forEach(h => {
|
||||||
const level = parseInt(h.tagName[1]);
|
const level = parseInt(h.tagName[1]);
|
||||||
const item = { id: h.id, text: h.textContent.trim(), level, children: [] };
|
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[stack.length - 1].children.push(item);
|
||||||
stack.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 => {
|
items.forEach(item => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'toc-item';
|
li.className = 'toc-item';
|
||||||
li.dataset.level = item.level;
|
li.dataset.level = item.level;
|
||||||
|
|
||||||
|
// 链接部分
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'toc-link';
|
div.className = 'toc-link';
|
||||||
div.innerHTML = `<span class="toc-text">${item.text}</span>`;
|
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) {
|
if (item.children.length) {
|
||||||
const expander = document.createElement('span');
|
expander = document.createElement('span');
|
||||||
expander.className = 'toc-expander';
|
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.appendChild(expander);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.onclick = function() { scrollTo(item.id); };
|
|
||||||
li.appendChild(div);
|
li.appendChild(div);
|
||||||
|
|
||||||
|
// 子菜单
|
||||||
if (item.children.length) {
|
if (item.children.length) {
|
||||||
const ul = document.createElement('ul');
|
const ul = document.createElement('ul');
|
||||||
ul.className = 'toc-children';
|
ul.className = 'toc-children';
|
||||||
render(item.children, ul);
|
if (item.level === 1) ul.classList.add('expanded');
|
||||||
|
|
||||||
|
renderRecursive(item.children, ul);
|
||||||
li.appendChild(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');
|
const idleCallback = window.requestIdleCallback || function(cb) { setTimeout(cb, 10); };
|
||||||
ul.classList.toggle('expanded');
|
idleCallback(buildToc);
|
||||||
btn.textContent = ul.classList.contains('expanded') ? '-' : '+';
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollTo(id) {
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
if (el) el.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
|
|
||||||
window.toggleToc = function() { panel.classList.toggle('show'); };
|
|
||||||
|
|
||||||
init();
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// 辅助脚本
|
||||||
setTimeout(() => {
|
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);
|
}, 200);
|
||||||
|
|
||||||
mermaid.initialize({ securityLevel: 'loose', startOnLoad: true });
|
// 确保 Mermaid 存在再初始化
|
||||||
|
if (typeof mermaid !== 'undefined') {
|
||||||
|
mermaid.initialize({ securityLevel: 'loose', startOnLoad: true });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user