Files
zTools2/static/pdf_admin.js
zikai 1c0f776571 feat(pdf-admin): 支持全选/多选批量硬删并去掉删除二次确认
- 工具栏新增「批量硬删」按钮,实时显示选中数,无选中时禁用
- 表头复选框全选/反选,行复选框多选,行级选中状态同步全选框
- 批量删除并发执行 DELETE,逐项淡出移除并汇总结果 toast
- 单条与批量删除均直接执行,移除 confirm() 二次确认
- 新增 .col-check 复选框列样式(窄宽居中)
2026-07-28 09:39:39 +08:00

178 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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();
})();