/* PDF 转换页:上传 epub + 进度轮询 + 下载 + 用户软删。
复用 common.js 的 el/toast/fmtBytes/fmtTime/api 工具。
state.items 缓存当前用户任务列表;轮询未完成任务进度直至终态。 */
(function () {
"use strict";
const { el, toast, fmtBytes, fmtTime, api } = window.ZK;
const dropzone = document.getElementById("dropzone");
const fileInput = document.getElementById("fileInput");
const uploadProgress = document.getElementById("uploadProgress");
const uploadName = document.getElementById("uploadName");
const uploadBar = document.getElementById("uploadBar");
const refreshBtn = document.getElementById("refresh");
const countEl = document.getElementById("count");
const listEl = document.getElementById("list");
const state = { items: [], polling: null };
refreshBtn.addEventListener("click", () => load());
// ---------- 上传 ----------
dropzone.addEventListener("dragover", (e) => {
e.preventDefault();
dropzone.classList.add("is-drag");
});
dropzone.addEventListener("dragleave", () => dropzone.classList.remove("is-drag"));
dropzone.addEventListener("drop", (e) => {
e.preventDefault();
dropzone.classList.remove("is-drag");
if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
});
fileInput.addEventListener("change", () => {
if (fileInput.files.length) handleFile(fileInput.files[0]);
fileInput.value = "";
});
function handleFile(file) {
const name = file.name || "";
if (!name.toLowerCase().endsWith(".epub")) {
toast("仅支持 epub 文件", "err");
return;
}
if (file.size > 250 * 1024 * 1024) {
toast("文件超过 250MB 上限", "err");
return;
}
uploadFile(file);
}
function uploadFile(file) {
uploadProgress.classList.remove("hidden");
uploadName.textContent = file.name;
uploadBar.style.width = "0%";
const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/pdf/jobs");
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
uploadBar.style.width = Math.round((e.loaded / e.total) * 100) + "%";
}
};
xhr.onload = () => {
uploadProgress.classList.add("hidden");
if (xhr.status === 200) {
toast("已提交,转换中…");
load();
} else {
let msg = "上传失败";
try {
const b = JSON.parse(xhr.responseText);
msg = b.detail || msg;
} catch {}
toast(msg, "err");
}
};
xhr.onerror = () => {
uploadProgress.classList.add("hidden");
toast("网络错误", "err");
};
const fd = new FormData();
fd.append("file", file);
xhr.send(fd);
}
// ---------- 列表 ----------
async function load() {
listEl.innerHTML = '
加载中…
';
countEl.textContent = "";
try {
const res = await api("/api/pdf/jobs");
if (!res.ok) throw new Error("HTTP " + res.status);
const body = await res.json();
state.items = body.items || [];
countEl.textContent = `共 ${body.total} 个任务`;
render();
schedulePoll();
} catch (e) {
listEl.innerHTML = '加载失败:' + (e.message || e) + "
";
}
}
function render() {
if (!state.items.length) {
listEl.innerHTML = '还没有任务。上传一个 epub 试试吧。
';
return;
}
const tbl = el("table", { class: "jobs-table" });
tbl.appendChild(el("thead", null,
el("tr", null,
el("th", { class: "col-name" }, "文件名"),
el("th", { class: "col-size" }, "大小"),
el("th", { class: "col-status" }, "状态"),
el("th", { class: "col-time" }, "创建时间"),
el("th", { class: "col-act" }, "操作")
)
));
const tbody = el("tbody", null);
for (const j of state.items) tbody.appendChild(renderRow(j));
tbl.appendChild(tbody);
listEl.innerHTML = "";
listEl.appendChild(tbl);
}
function renderRow(j) {
return el("tr", { dataset: { id: j.id } },
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-time muted" }, fmtTime(j.created_at)),
el("td", { class: "col-act" }, actionsCell(j))
);
}
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 || "" }, "失败");
// pending / converting 显示进度条
const wrap = el("span", { class: "prog" });
wrap.appendChild(el("span", { class: "prog__label" }, j.status === "converting" ? "转换中" : "排队中"));
wrap.appendChild(el("span", { class: "bar bar--sm" },
el("span", { class: "bar__fill", style: "width:" + (j.progress || 0) + "%" })
));
return wrap;
}
function actionsCell(j) {
const cell = el("span", null);
if (j.status === "done") {
cell.appendChild(el("a", { class: "btn primary", href: `/api/pdf/jobs/${j.id}/download`, download: "" }, "下载"));
}
cell.appendChild(el("button", { class: "btn danger", onclick: () => removeOne(j) }, "删除"));
return cell;
}
// ---------- 轮询未完成任务 ----------
function schedulePoll() {
if (state.polling) clearInterval(state.polling);
const pending = state.items.filter((j) => j.status === "pending" || j.status === "converting");
if (!pending.length) return;
state.polling = setInterval(async () => {
let stillPending = false;
for (const j of pending) {
try {
const res = await api(`/api/pdf/jobs/${j.id}`);
if (res.ok) {
const fresh = await res.json();
Object.assign(j, fresh);
}
} catch {}
if (j.status === "pending" || j.status === "converting") stillPending = true;
}
render();
if (!stillPending) {
clearInterval(state.polling);
state.polling = null;
}
}, 1500);
}
async function removeOne(j) {
if (!confirm(`确定删除「${j.source_filename}」?\n删除后将不再显示(管理员仍可见,需管理员彻底删除才会清除)。`)) return;
try {
const res = await api(`/api/pdf/jobs/${j.id}`, { method: "DELETE" });
if (!res.ok) throw new Error("HTTP " + res.status);
await load();
toast("已删除");
} catch (e) {
toast("删除失败:" + (e.message || e), "err");
}
}
load();
})();