前端整理: 合并文件管理页 + 新增导航页
1. 合并 files-page 与 pdf-admin 为统一文件管理页 /api/files-page:
- 新增 GET /api/admin/files/with-pdf 接口,以 uploaded_files 为基础,
用 pdf_jobs.source_file_id / output_file_id 内存匹配,为关联文件标注
转换状态、用户软删标记与角色(源epub/产物PDF)
- PdfJobOut 补 source_file_id / output_file_id 字段
- file_browser 新增 PDF 任务列(状态徽标/软删标记/硬删任务按钮)
- 删除 /api/pdf-admin 路由及 static/pdf_admin.* 三个文件
2. 新增导航页 /api/index,卡片式收集所有页面入口;
各子页(upload/whiteboard/system_status/files-page)脚注加返回导航链接
3. 更新 docs/routes.md、docs/configuration.md 同步说明
测试: pytest tests/test_pdf_service.py 7 passed; 手动校验各页面路由与合并接口响应
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
.files-table th.col-size { width: 9em; }
|
||||
.files-table th.col-src { width: 6em; }
|
||||
.files-table th.col-time { width: 11em; }
|
||||
.files-table th.col-pdf { width: 13em; }
|
||||
.files-table th.col-act { width: 9em; text-align: right; }
|
||||
.files-table td.col-act { text-align: right; white-space: nowrap; }
|
||||
.files-table td.col-name .fname { font-weight: 600; word-break: break-all; }
|
||||
@@ -12,9 +13,24 @@
|
||||
.files-table td.col-act .btn { padding: 0.3em 0.8em; font-size: 0.85em; margin-left: 0.3em; }
|
||||
.files-table tbody tr.sel { background: var(--primary-soft); }
|
||||
.files-table tbody tr.sel:hover td { background: var(--primary-soft); }
|
||||
/* 关联到已软删 PDF 任务的行:淡化背景提示 */
|
||||
.files-table tbody tr.row-pdf-deleted td { background: var(--danger-soft); }
|
||||
.files-table tbody tr.row-pdf-deleted:hover td { background: var(--danger-soft); }
|
||||
.files-table input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; accent-color: var(--primary); }
|
||||
.sha-short { cursor: pointer; }
|
||||
.sha-short:hover { color: var(--primary); }
|
||||
/* PDF 任务列:角色徽标 + 状态徽标堆叠 */
|
||||
.pdf-cell { display: flex; flex-direction: column; gap: 0.2em; font-size: 0.82em; }
|
||||
.pdf-cell .pdf-roles { display: flex; flex-wrap: wrap; gap: 0.3em; align-items: center; }
|
||||
.pdf-cell .role-tag {
|
||||
display: inline-block; padding: 0.05em 0.5em; border-radius: 8px;
|
||||
font-size: 0.78em; background: var(--surface-2); color: var(--text-dim);
|
||||
}
|
||||
.pdf-cell .role-tag.source { background: var(--warn-soft); color: var(--warn); }
|
||||
.pdf-cell .role-tag.output { background: var(--success-soft); color: var(--success); }
|
||||
.pdf-cell .del-mark { color: var(--danger); font-size: 0.78em; }
|
||||
.pdf-cell .pdf-act { display: flex; gap: 0.3em; flex-wrap: wrap; }
|
||||
.pdf-cell .pdf-act .btn { padding: 0.2em 0.6em; font-size: 0.8em; }
|
||||
.row-removed { opacity: 0; transition: opacity 0.25s; }
|
||||
|
||||
.toolbar-spacer { flex: 1; }
|
||||
@@ -47,5 +63,6 @@
|
||||
.files-table th, .files-table td { padding: 0.5em 0.4em; }
|
||||
.files-table th.col-src, .files-table td.col-src { display: none; }
|
||||
.files-table th.col-time, .files-table td.col-time { font-size: 0.78em; }
|
||||
.files-table th.col-pdf, .files-table td.col-pdf { font-size: 0.78em; }
|
||||
.batchbar { font-size: 0.85em; }
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>文件浏览 - zikai</title>
|
||||
<title>文件管理 - zikai</title>
|
||||
<link rel="stylesheet" href="/api/static/common.css">
|
||||
<link rel="stylesheet" href="/api/static/file_browser.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>文件浏览</h1>
|
||||
<p class="sub">查看已上传的文件、下载或删除。支持多选与分页。删除后不再显示。</p>
|
||||
<h1>文件管理</h1>
|
||||
<p class="sub">浏览 / 下载 / 删除已上传文件,并合并 PDF 转换管理:关联文件会标注转换状态与用户软删标记,可硬删任务。支持多选与分页。</p>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn primary" id="refresh">刷新</button>
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
<div id="pager" class="pager hidden"></div>
|
||||
|
||||
<p class="foot"><a class="link" href="/api/upload">上传文件</a> · zikai file service</p>
|
||||
<p class="foot"><a class="link" href="/api/index">导航</a> · <a class="link" href="/api/upload">上传文件</a> · zikai file service</p>
|
||||
</div>
|
||||
<script src="/api/static/common.js"></script>
|
||||
<script src="/api/static/file_browser.js"></script>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* 文件浏览页:分页拉取 /api/admin/files、复选框多选 + 全选、批量下载/删除、复制 sha。
|
||||
/* 文件管理页:分页拉取 /api/admin/files-with-pdf、复选框多选 + 全选、批量下载/删除、复制 sha。
|
||||
合并 PDF 转换管理:每个文件附带 pdf_jobs(role=source epub / role=output 产物 PDF),
|
||||
渲染转换状态徽标、用户软删标记,并支持硬删任务(DELETE /api/admin/pdf/jobs/{id})。
|
||||
state.items 缓存当前页数据;切换页/页大小重新拉取;删除后若当前页空则回退一页。 */
|
||||
(function () {
|
||||
"use strict";
|
||||
@@ -53,7 +55,7 @@
|
||||
countEl.textContent = "";
|
||||
pagerEl.classList.add("hidden");
|
||||
try {
|
||||
const res = await api(`/api/admin/files?limit=${limit}&offset=${offset}`);
|
||||
const res = await api(`/api/admin/files/with-pdf?limit=${limit}&offset=${offset}`);
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
const body = await res.json();
|
||||
state.items = body.items || [];
|
||||
@@ -88,13 +90,15 @@
|
||||
el("th", { class: "col-size" }, "大小"),
|
||||
el("th", { class: "col-src" }, "来源"),
|
||||
el("th", { class: "col-time" }, "上传时间"),
|
||||
el("th", { class: "col-pdf" }, "PDF 任务"),
|
||||
el("th", { class: "col-act" }, "操作")
|
||||
)
|
||||
);
|
||||
const tbody = el("tbody", null);
|
||||
for (const f of state.items) {
|
||||
const checked = state.selected.has(f.id);
|
||||
const row = el("tr", { class: checked ? "sel" : "", dataset: { id: f.id } },
|
||||
const pdfDeleted = (f.pdf_jobs || []).some((j) => j.user_deleted);
|
||||
const row = el("tr", { class: [checked ? "sel" : "", pdfDeleted ? "row-pdf-deleted" : ""].filter(Boolean).join(" "), dataset: { id: f.id } },
|
||||
el("td", { class: "col-sel" },
|
||||
el("input", { type: "checkbox", class: "row-sel", checked, dataset: { id: f.id } })
|
||||
),
|
||||
@@ -105,6 +109,7 @@
|
||||
el("td", { class: "col-size mono" }, fmtBytes(f.size_bytes)),
|
||||
el("td", { class: "col-src" }, el("span", { class: "tag" }, f.source || "-")),
|
||||
el("td", { class: "col-time muted" }, fmtTime(f.uploaded_at)),
|
||||
el("td", { class: "col-pdf" }, renderPdfCell(f)),
|
||||
el("td", { class: "col-act" },
|
||||
el("a", { class: "btn primary", href: `/api/admin/files/${f.id}/download`, download: "" }, "下载"),
|
||||
el("button", { class: "btn danger", onclick: () => removeOne(f) }, "删除")
|
||||
@@ -190,6 +195,58 @@
|
||||
return sha.length > 16 ? sha.slice(0, 12) + "…" + sha.slice(-4) : sha;
|
||||
}
|
||||
|
||||
// 渲染 PDF 任务列:展示角色(源 epub / 产物 PDF)徽标、转换状态、用户软删标记与硬删按钮。
|
||||
// 一个文件可能同时被多个 job 引用(罕见),全列出;无关联则显示 -。
|
||||
function renderPdfCell(f) {
|
||||
const jobs = f.pdf_jobs || [];
|
||||
if (!jobs.length) return el("span", { class: "muted" }, "-");
|
||||
const cell = el("div", { class: "pdf-cell" });
|
||||
const roles = el("div", { class: "pdf-roles" });
|
||||
for (const j of jobs) {
|
||||
roles.appendChild(el("span", { class: "role-tag " + j.role, title: j.role === "source" ? "PDF 转换的 epub 源文件" : "PDF 转换的产物 PDF" },
|
||||
j.role === "source" ? "源" : "产物"));
|
||||
roles.appendChild(statusTag(j));
|
||||
if (j.user_deleted) {
|
||||
roles.appendChild(el("span", { class: "del-mark", title: "用户已软删:" + fmtTime(j.deleted_at) }, "已删"));
|
||||
}
|
||||
roles.appendChild(el("span", { class: "muted" }, "#"+j.job_id));
|
||||
}
|
||||
cell.appendChild(roles);
|
||||
// 硬删按钮:对每个关联 job 提供(source 与 output 共属同一 job,去重后只显示一个)
|
||||
const seenJobIds = new Set();
|
||||
const actBar = el("div", { class: "pdf-act" });
|
||||
for (const j of jobs) {
|
||||
if (seenJobIds.has(j.job_id)) continue;
|
||||
seenJobIds.add(j.job_id);
|
||||
actBar.appendChild(el("button", {
|
||||
class: "btn danger", onclick: () => removePdfJob(j, f)
|
||||
}, "硬删任务"));
|
||||
}
|
||||
cell.appendChild(actBar);
|
||||
return cell;
|
||||
}
|
||||
|
||||
function statusTag(j) {
|
||||
if (j.status === "done") return el("span", { class: "tag ok" }, "完成");
|
||||
if (j.status === "failed") return el("span", { class: "tag err", title: j.error_message || "" }, "失败");
|
||||
if (j.status === "converting") return el("span", { class: "tag warn" }, "转换中 " + j.progress + "%");
|
||||
return el("span", { class: "tag" }, "排队中");
|
||||
}
|
||||
|
||||
// 硬删 PDF 任务:删 job + 其源/产物文件。删除后重载当前页。
|
||||
async function removePdfJob(j, f) {
|
||||
if (!confirm(`确定硬删 PDF 任务 #${j.job_id}?\n将同时删除关联的源文件与产物 PDF,不可恢复。`)) return;
|
||||
try {
|
||||
const res = await api(`/api/admin/pdf/jobs/${j.job_id}`, { method: "DELETE" });
|
||||
if (res.status === 404) { toast("任务已不存在"); }
|
||||
else if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
toast("已硬删任务");
|
||||
await load(state.page);
|
||||
} catch (e) {
|
||||
toast("硬删失败:" + (e.message || e), "err");
|
||||
}
|
||||
}
|
||||
|
||||
async function removeOne(f) {
|
||||
if (!confirm(`确定删除「${f.original_filename}」?\n此操作不可恢复,将同时删除磁盘文件。`)) return;
|
||||
try {
|
||||
|
||||
74
static/index.html
Normal file
74
static/index.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>导航 - zikai</title>
|
||||
<link rel="stylesheet" href="/api/static/common.css">
|
||||
<style>
|
||||
/* 导航页:卡片网格,复用 common.css 变量 */
|
||||
.nav-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 1em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
.nav-card {
|
||||
display: flex; flex-direction: column; gap: 0.3em;
|
||||
padding: 1.2em 1.3em;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
text-decoration: none; color: var(--text);
|
||||
transition: transform 0.05s, border-color 0.15s;
|
||||
}
|
||||
.nav-card:hover { transform: translateY(-2px); border-color: var(--primary); }
|
||||
.nav-card .ico { font-size: 1.6em; }
|
||||
.nav-card .name { font-size: 1.05em; font-weight: 600; }
|
||||
.nav-card .desc { color: var(--text-dim); font-size: 0.82em; line-height: 1.4; }
|
||||
.nav-card .lock { font-size: 0.75em; color: var(--text-dim); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>zikai 工具箱</h1>
|
||||
<p class="sub">个人 Web 服务入口导航。带 🔒 标记的页面需 Basic Auth(凭据见 config.yaml 的 docs 段)。</p>
|
||||
|
||||
<div class="nav-grid">
|
||||
<a class="nav-card" href="/api/upload">
|
||||
<span class="ico">📤</span>
|
||||
<span class="name">上传文件</span>
|
||||
<span class="desc">拖拽 / 多文件 / 分片(4 MiB)/ 断点续传</span>
|
||||
<span class="lock"></span>
|
||||
</a>
|
||||
<a class="nav-card" href="/api/files-page">
|
||||
<span class="ico">📁</span>
|
||||
<span class="name">文件管理</span>
|
||||
<span class="desc">浏览 / 下载 / 删除已上传文件,合并 PDF 转换管理(状态、软删标记、硬删任务)</span>
|
||||
<span class="lock">🔒 需鉴权</span>
|
||||
</a>
|
||||
<a class="nav-card" href="/api/wb-admin">
|
||||
<span class="ico">📝</span>
|
||||
<span class="name">记事本管理</span>
|
||||
<span class="desc">查看 / 删除共享文本记事本,可新建并打开</span>
|
||||
<span class="lock">🔒 需鉴权</span>
|
||||
</a>
|
||||
<a class="nav-card" href="/api/system/status">
|
||||
<span class="ico">📊</span>
|
||||
<span class="name">系统状态</span>
|
||||
<span class="desc">CPU / 内存 / 磁盘实时使用率(?format=json 切 JSON)</span>
|
||||
<span class="lock"></span>
|
||||
</a>
|
||||
<a class="nav-card" href="/docs">
|
||||
<span class="ico">📚</span>
|
||||
<span class="name">API 文档</span>
|
||||
<span class="desc">Swagger UI,全部 REST 接口在线调试</span>
|
||||
<span class="lock">🔒 需鉴权</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="foot">zikai file service · <a class="link" href="/api/health">健康探针</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,46 +0,0 @@
|
||||
/* PDF 转换管理页样式(含任务表格、进度条、软删行标注)。
|
||||
用户侧 PDF 转换页由 zMainPage 的 zPDF_package 组件提供(构建期 import),
|
||||
zTools2 不再托管用户 UI;本文件仅服务 /api/pdf-admin 管理页。 */
|
||||
.jobs-table th.col-size { width: 7em; }
|
||||
.jobs-table th.col-status { width: 9em; }
|
||||
.jobs-table th.col-del { width: 8em; }
|
||||
.jobs-table th.col-time { width: 11em; }
|
||||
.jobs-table th.col-act { width: 11em; text-align: right; }
|
||||
.jobs-table td.col-act { text-align: right; white-space: nowrap; }
|
||||
.jobs-table td.col-act .btn { padding: 0.3em 0.8em; font-size: 0.85em; margin-left: 0.3em; }
|
||||
.jobs-table td.col-name { font-weight: 600; word-break: break-all; }
|
||||
|
||||
/* 复选框列:窄宽居中,避免视觉噪声 */
|
||||
.jobs-table th.col-check,
|
||||
.jobs-table td.col-check { width: 2.5em; text-align: center; vertical-align: middle; }
|
||||
.jobs-table td.col-check input { cursor: pointer; }
|
||||
|
||||
.bar { height: 8px; background: var(--surface-2); border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
|
||||
.bar__fill { height: 100%; width: 0; background: var(--primary); border-radius: 6px; transition: width 0.2s; }
|
||||
.bar--sm { display: inline-block; width: 90px; vertical-align: middle; height: 6px; }
|
||||
|
||||
.prog { display: inline-flex; align-items: center; gap: 0.5em; font-size: 0.82em; }
|
||||
.prog__label { color: var(--text-dim); white-space: nowrap; }
|
||||
|
||||
/* 管理页专属:覆盖 jobs-table 列宽 + 软删行标注 */
|
||||
.admin-table th.col-size { width: 7em; }
|
||||
.admin-table th.col-status { width: 9em; }
|
||||
.admin-table th.col-del { width: 8em; }
|
||||
.admin-table th.col-time { width: 11em; }
|
||||
.admin-table th.col-act { width: 11em; text-align: right; }
|
||||
.admin-table td.col-act { text-align: right; white-space: nowrap; }
|
||||
.admin-table td.col-act .btn { padding: 0.3em 0.8em; font-size: 0.85em; margin-left: 0.3em; }
|
||||
|
||||
/* 用户已软删的行:淡化背景提示 */
|
||||
.admin-table tbody tr.row-soft-deleted td { background: var(--danger-soft); }
|
||||
.admin-table tbody tr.row-soft-deleted:hover td { background: var(--danger-soft); }
|
||||
.row-removed { opacity: 0; transition: opacity 0.25s; }
|
||||
|
||||
.skel, .empty { padding: 1.6em; text-align: center; color: var(--text-dim); font-size: 0.9em; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.jobs-table th, .jobs-table td { padding: 0.5em 0.4em; }
|
||||
.jobs-table th.col-time, .jobs-table td.col-time { font-size: 0.78em; }
|
||||
.admin-table th, .admin-table td { padding: 0.5em 0.4em; }
|
||||
.admin-table th.col-time, .admin-table td.col-time { font-size: 0.78em; }
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PDF 转换管理 - zikai</title>
|
||||
<link rel="stylesheet" href="/api/static/common.css">
|
||||
<link rel="stylesheet" href="/api/static/pdf_admin.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>PDF 转换管理</h1>
|
||||
<p class="sub">查看全部转换任务(含用户已软删的,标注是否已删除),可硬删(真正删除磁盘与记录)。</p>
|
||||
|
||||
<div class="toolbar">
|
||||
<button class="btn primary" id="refresh">刷新</button>
|
||||
<button class="btn danger" id="bulkDelete" disabled>批量硬删 <span id="selCount"></span></button>
|
||||
<span class="muted" id="count"></span>
|
||||
</div>
|
||||
|
||||
<div id="list">
|
||||
<div class="skel">加载中…</div>
|
||||
</div>
|
||||
|
||||
<p class="foot">zikai file service</p>
|
||||
</div>
|
||||
<script src="/api/static/common.js"></script>
|
||||
<script src="/api/static/pdf_admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,177 +0,0 @@
|
||||
/* PDF 转换管理页:拉取 /api/admin/pdf/jobs、渲染表格(含「是否已删除」列)、硬删。
|
||||
支持全选/多选批量硬删;删除直接执行,无二次确认。 */
|
||||
(function () {
|
||||
"use strict";
|
||||
const { el, toast, fmtBytes, fmtTime, api } = window.ZK;
|
||||
const listEl = document.getElementById("list");
|
||||
const countEl = document.getElementById("count");
|
||||
const refreshBtn = document.getElementById("refresh");
|
||||
const bulkDeleteBtn = document.getElementById("bulkDelete");
|
||||
const selCountEl = document.getElementById("selCount");
|
||||
|
||||
// 选中集合:以 job id 为键,值为对应行元素,便于 O(1) 增删与遍历
|
||||
const selected = new Map();
|
||||
|
||||
refreshBtn.addEventListener("click", load);
|
||||
bulkDeleteBtn.addEventListener("click", bulkRemove);
|
||||
|
||||
async function load() {
|
||||
listEl.innerHTML = '<div class="skel">加载中…</div>';
|
||||
countEl.textContent = "";
|
||||
selected.clear();
|
||||
updateBulkBtn();
|
||||
try {
|
||||
const res = await api("/api/admin/pdf/jobs?limit=500&offset=0");
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
const body = await res.json();
|
||||
render(body.items || []);
|
||||
countEl.textContent = `共 ${body.total} 个任务`;
|
||||
} catch (e) {
|
||||
listEl.innerHTML = '<div class="empty">加载失败:' + (e.message || e) + "</div>";
|
||||
}
|
||||
}
|
||||
|
||||
function render(items) {
|
||||
if (!items.length) {
|
||||
listEl.innerHTML = '<div class="empty">还没有任务。</div>';
|
||||
return;
|
||||
}
|
||||
const table = el("table", { class: "jobs-table admin-table" });
|
||||
const thead = el("thead", null,
|
||||
el("tr", null,
|
||||
el("th", { class: "col-check" },
|
||||
el("input", { type: "checkbox", id: "selectAll", onchange: onToggleAll })
|
||||
),
|
||||
el("th", { class: "col-name" }, "文件名"),
|
||||
el("th", { class: "col-size" }, "大小"),
|
||||
el("th", { class: "col-status" }, "状态"),
|
||||
el("th", { class: "col-del" }, "是否已删除"),
|
||||
el("th", { class: "col-time" }, "创建时间"),
|
||||
el("th", { class: "col-act" }, "操作")
|
||||
)
|
||||
);
|
||||
const tbody = el("tbody", null);
|
||||
for (const j of items) tbody.appendChild(renderRow(j));
|
||||
table.appendChild(thead);
|
||||
table.appendChild(tbody);
|
||||
listEl.innerHTML = "";
|
||||
listEl.appendChild(table);
|
||||
}
|
||||
|
||||
function renderRow(j) {
|
||||
const checkbox = el("input", { type: "checkbox", value: j.id, onchange: () => onToggleRow(j, checkbox, row) });
|
||||
const row = el("tr", { class: j.user_deleted ? "row-soft-deleted" : "" },
|
||||
el("td", { class: "col-check" }, checkbox),
|
||||
el("td", { class: "col-name" }, j.source_filename),
|
||||
el("td", { class: "col-size mono" }, fmtBytes(j.source_size)),
|
||||
el("td", { class: "col-status" }, statusCell(j)),
|
||||
el("td", { class: "col-del" }, deletedCell(j)),
|
||||
el("td", { class: "col-time muted" }, fmtTime(j.created_at)),
|
||||
el("td", { class: "col-act" },
|
||||
j.status === "done"
|
||||
? el("a", { class: "btn", href: `/api/admin/files/${j.output_file_id}/download`, download: "" }, "下载产物")
|
||||
: el("span", { class: "muted" }, "-"),
|
||||
el("button", { class: "btn danger", onclick: () => remove(j, row) }, "硬删")
|
||||
)
|
||||
);
|
||||
// 行元素上缓存 job/checkbox 引用,供全选回调 O(1) 取用
|
||||
row.__job = j;
|
||||
row.__checkbox = checkbox;
|
||||
return row;
|
||||
}
|
||||
|
||||
function onToggleAll(e) {
|
||||
const checked = e.target.checked;
|
||||
selected.clear();
|
||||
const rows = listEl.querySelectorAll("tbody tr");
|
||||
rows.forEach((row) => {
|
||||
if (row.__checkbox) row.__checkbox.checked = checked;
|
||||
if (checked && row.__job) selected.set(row.__job.id, row);
|
||||
});
|
||||
updateBulkBtn();
|
||||
}
|
||||
|
||||
function onToggleRow(j, box, row) {
|
||||
if (box.checked) selected.set(j.id, row);
|
||||
else selected.delete(j.id);
|
||||
// 同步表头全选框状态
|
||||
const allBoxes = listEl.querySelectorAll('tbody input[type="checkbox"]');
|
||||
const selectAll = document.getElementById("selectAll");
|
||||
if (selectAll) selectAll.checked = allBoxes.length > 0 && [...allBoxes].every((b) => b.checked);
|
||||
updateBulkBtn();
|
||||
}
|
||||
|
||||
function updateBulkBtn() {
|
||||
const n = selected.size;
|
||||
bulkDeleteBtn.disabled = n === 0;
|
||||
selCountEl.textContent = n > 0 ? `(${n})` : "";
|
||||
}
|
||||
|
||||
function statusCell(j) {
|
||||
if (j.status === "done") return el("span", { class: "tag ok" }, "完成");
|
||||
if (j.status === "failed")
|
||||
return el("span", { class: "tag err", title: j.error_message || "" }, "失败");
|
||||
return el("span", { class: "tag warn" }, j.status === "converting" ? `转换中 ${j.progress}%` : "排队中");
|
||||
}
|
||||
|
||||
function deletedCell(j) {
|
||||
if (j.user_deleted) {
|
||||
return el("span", { class: "tag err", title: fmtTime(j.deleted_at) }, "已删除");
|
||||
}
|
||||
return el("span", { class: "muted" }, "否");
|
||||
}
|
||||
|
||||
// 单条硬删:直接执行,无二次确认
|
||||
async function remove(j, row) {
|
||||
try {
|
||||
const res = await api(`/api/admin/pdf/jobs/${j.id}`, { method: "DELETE" });
|
||||
if (res.status === 404) { toast("任务已不存在"); }
|
||||
else if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
selected.delete(j.id);
|
||||
row.classList.add("row-removed");
|
||||
setTimeout(() => row.remove(), 250);
|
||||
toast("已硬删");
|
||||
decCount();
|
||||
updateBulkBtn();
|
||||
} catch (e) {
|
||||
toast("删除失败:" + (e.message || e), "err");
|
||||
}
|
||||
}
|
||||
|
||||
// 批量硬删:并发删除所有选中项,无二次确认
|
||||
async function bulkRemove() {
|
||||
if (!selected.size) return;
|
||||
bulkDeleteBtn.disabled = true;
|
||||
const entries = [...selected.entries()];
|
||||
let ok = 0, fail = 0;
|
||||
await Promise.all(entries.map(async ([id, row]) => {
|
||||
try {
|
||||
const res = await api(`/api/admin/pdf/jobs/${id}`, { method: "DELETE" });
|
||||
if (res.ok || res.status === 404) {
|
||||
selected.delete(id);
|
||||
row.classList.add("row-removed");
|
||||
setTimeout(() => row.remove(), 250);
|
||||
ok++;
|
||||
} else {
|
||||
fail++;
|
||||
}
|
||||
} catch {
|
||||
fail++;
|
||||
}
|
||||
}));
|
||||
selected.clear();
|
||||
if (ok) { decCount(ok); toast(`已硬删 ${ok} 项` + (fail ? `,${fail} 项失败` : "")); }
|
||||
else if (fail) { toast(`批量删除失败(${fail} 项)`, "err"); }
|
||||
// 同步全选框
|
||||
const selectAll = document.getElementById("selectAll");
|
||||
if (selectAll) selectAll.checked = false;
|
||||
updateBulkBtn();
|
||||
}
|
||||
|
||||
function decCount(n = 1) {
|
||||
const m = (countEl.textContent || "").match(/(\d+)/);
|
||||
if (m) countEl.textContent = `共 ${Math.max(0, Number(m[1]) - n)} 个任务`;
|
||||
}
|
||||
|
||||
load();
|
||||
})();
|
||||
@@ -20,6 +20,7 @@
|
||||
<span class="wb-online" id="online" title="在线人数">●</span>
|
||||
</div>
|
||||
<div class="wb-bar-right">
|
||||
<a class="btn" href="/api/index" target="_top" title="返回导航">导航</a>
|
||||
<button class="btn" id="copyLinkBtn" title="复制分享链接">复制链接</button>
|
||||
<button class="btn" id="copyBtn" title="复制全部文本">复制文本</button>
|
||||
<button class="btn danger" id="clearBtn" title="清空全部内容(所有人)">清空</button>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="skel">加载中…</div>
|
||||
</div>
|
||||
|
||||
<p class="foot"><a class="link" href="/api/upload">上传文件</a> · <a class="link" href="/api/files-page">文件浏览</a> · zikai</p>
|
||||
<p class="foot"><a class="link" href="/api/index">导航</a> · <a class="link" href="/api/upload">上传文件</a> · <a class="link" href="/api/files-page">文件管理</a> · zikai</p>
|
||||
</div>
|
||||
<script src="/api/static/common.js"></script>
|
||||
<script src="/api/static/whiteboard_admin.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user