"""历史任务页:分页表格展示所有任务,可按文件名搜索、下载完成的字幕。 共享 _shared.py 的 BASE_CSS / SHARED_JS。 页面专属:搜索框(防抖)、分页表格、手动刷新(不自动轮询)。 """ from __future__ import annotations from ._shared import render_page PAGE_SIZE = 50 SEARCH_DEBOUNCE_MS = 350 _PAGE_JS = f""" const PAGE_SIZE = {PAGE_SIZE}; const SEARCH_DEBOUNCE_MS = {SEARCH_DEBOUNCE_MS}; let currentOffset = 0; let total = 0; let searchQuery = ""; let debounceTimer = null; const tbody = document.getElementById("task-body"); const infoEl = document.getElementById("info"); const emptyEl = document.getElementById("empty"); const paginationEl = document.getElementById("pagination"); const searchInput = document.getElementById("search"); document.getElementById("refresh-btn").addEventListener("click", () => load(currentOffset)); // 搜索:输入防抖,改动后回到第一页 searchInput.addEventListener("input", () => {{ clearTimeout(debounceTimer); debounceTimer = setTimeout(() => {{ searchQuery = searchInput.value.trim(); load(0); }}, SEARCH_DEBOUNCE_MS); }}); async function load(offset) {{ currentOffset = offset; try {{ const params = new URLSearchParams({{ limit: PAGE_SIZE, offset: offset }}); if (searchQuery) params.set("q", searchQuery); const r = await fetch("/api/tasks?" + params.toString()); if (!r.ok) {{ infoEl.textContent = "HTTP " + r.status; return; }} const data = await r.json(); total = data.total; renderTable(data.tasks); renderPagination(); const end = Math.min(offset + data.tasks.length, total); const prefix = searchQuery ? `搜索“${{escapeHtml(searchQuery)}}”匹配 ` : "共 "; infoEl.textContent = total === 0 ? (searchQuery ? "无匹配任务" : "暂无任务") : `${{prefix}}${{total}} 条,显示 ${{offset + 1}}–${{end}}`; }} catch (e) {{ infoEl.textContent = "加载失败"; }} }} function renderTable(tasks) {{ tbody.innerHTML = ""; if (tasks.length === 0) {{ emptyEl.style.display = "block"; return; }} emptyEl.style.display = "none"; for (const task of tasks) {{ const tr = document.createElement("tr"); const label = STATUS_LABEL[task.status] || task.status; const stateClass = "st-" + (task.status === "done" ? "done" : task.status === "failed" ? "fail" : "running"); let created; try {{ created = fmtDateTime24(new Date(task.created_at + "Z")); }} catch (e) {{ created = task.created_at; }} let idDisplay = task.is_upload ? "—" : "#" + task.id; let action; if (task.status === "done") {{ action = `双语` + `` + ``; }} else if (task.status === "failed") {{ action = `查看错误`; }} else {{ action = `-`; }} // done/failed 且非上传中:加删除按钮 const canDelete = (task.status === "done" || task.status === "failed") && !task.is_upload; if (canDelete) {{ action += ` `; }} let progress; if (task.status === "done") progress = "100%"; else if (task.status === "failed") progress = "—"; else {{ const pct = (task.progress == null) ? 0 : task.progress; progress = `
${{pct.toFixed(0)}}%`; }} tr.innerHTML = ` ${{idDisplay}} ${{escapeHtml(task.filename)}} ${{label}} ${{progress}} ${{created}} ${{action}}`; tbody.appendChild(tr); }} }} function renderPagination() {{ const pages = Math.ceil(total / PAGE_SIZE); const currentPage = Math.floor(currentOffset / PAGE_SIZE) + 1; if (pages <= 1) {{ paginationEl.innerHTML = ""; return; }} let html = ""; if (currentPage > 1) html += ` `; html += `第 ${{currentPage}} / ${{pages}} 页`; if (currentPage < pages) html += ` `; paginationEl.innerHTML = html; }} async function deleteTask(taskId) {{ if (!confirm("确认删除任务 #" + taskId + "?字幕和中间文件将被清除。")) return; try {{ const r = await fetch("/api/tasks/" + taskId, {{ method: "DELETE" }}); if (!r.ok) {{ alert("删除失败:" + await r.text()); return; }} load(currentOffset); }} catch (e) {{ alert("删除失败:" + e); }} }} load(0); """ _PAGE_CSS = """ .search-input { flex: 1; min-width: 180px; max-width: 360px; padding: 0.42em 0.7em; border: 1px solid var(--border); border-radius: 6px; background: var(--card-bg); color: var(--fg); font-size: 0.9em; transition: border-color 0.15s, box-shadow 0.15s; } .search-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); } /* 搜索框主导工具栏,刷新/信息靠右不收缩 */ .toolbar { justify-content: flex-start; } .toolbar .btn-sm, .toolbar .info { flex-shrink: 0; } """ _BODY = """

历史任务

所有转写任务记录。可按文件名搜索,完成任务可下载字幕,进行中显示进度。

ID 文件名 状态 进度 创建时间 操作
""" def render() -> str: return render_page( title="历史任务 — audio2text", nav_active="history", body=_BODY, page_js=_PAGE_JS, page_css=_PAGE_CSS, )