From e7b4a3d5e32e3b0268ac7384d7965a1ae71e0837 Mon Sep 17 00:00:00 2001
From: zikai <1621362626@qq.com>
Date: Mon, 27 Jul 2026 17:14:33 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=E5=86=97=E4=BD=99=20?=
=?UTF-8?q?PDF=20=E7=94=A8=E6=88=B7=E6=9C=8D=E5=8A=A1=E7=AB=AF=E9=A1=B5?=
=?UTF-8?q?=EF=BC=8C=E5=89=8D=E5=90=8E=E7=AB=AF=E5=88=86=E7=A6=BB=E7=94=B1?=
=?UTF-8?q?=20zMainPage=20=E7=BB=84=E4=BB=B6=E6=8E=A5=E7=AE=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
PDF 用户侧 UI 改由 zMainPage 的 zPDF_package 组件提供(构建期 import,非 iframe),
zTools2 仅保留 /api/pdf/* REST API 与 /api/pdf-admin 管理页。cookie 认证不受影响:
zk_pdf 仍在首次 POST /api/pdf/jobs 时种下,与已删的 /api/pdf 页面路由无关。
- app/main.py:删除 GET /api/pdf 页面路由(pdf_page)
- static/pdf.html、static/pdf.js、static/pdf.css:删除用户页 HTML/JS/CSS
(功能已被 zPDF_package 的 App.vue/Uploader.vue/JobList.vue 取代)
- static/pdf_admin.css:合并原 pdf.css 中管理页依赖的样式,成为自包含样式表
- static/pdf_admin.html:移除指向已删 /api/pdf 的页脚死链与 pdf.css 引用
- README.md:功能一览/访问入口表去 /api/pdf 用户页条目,路由约定去 iframe 表述
---
README.md | 9 +-
app/main.py | 10 ---
static/pdf.css | 40 ---------
static/pdf.html | 44 ----------
static/pdf.js | 191 ------------------------------------------
static/pdf_admin.css | 25 +++++-
static/pdf_admin.html | 3 +-
7 files changed, 30 insertions(+), 292 deletions(-)
delete mode 100644 static/pdf.css
delete mode 100644 static/pdf.html
delete mode 100644 static/pdf.js
diff --git a/README.md b/README.md
index 8b86a89..d1886a8 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,6 @@
| 记事本实时同步 | `WS /api/ws/wb/{id}`(心跳 3s,5 次失活移除) | 公开 |
| 记事本管理 | `GET /api/wb-admin`(查看/删除) | Basic Auth |
| 记事本管理 API | `GET /api/admin/wb`、`DELETE /api/admin/wb/{id}` | Basic Auth |
-| PDF 转换 | `GET /api/pdf`(上传 epub->PDF,进度轮询,下载;凭 cookie 记住任务) | 公开(cookie) |
| PDF 转换 API | `POST /api/pdf/jobs`、`GET /api/pdf/jobs[/{id}]`、`GET /api/pdf/jobs/{id}/download`、`DELETE /api/pdf/jobs/{id}` | 公开(cookie) |
| PDF 转换管理 | `GET /api/pdf-admin`(全部任务,含已软删标记,硬删) | Basic Auth |
| PDF 转换管理 API | `GET /api/admin/pdf/jobs`、`DELETE /api/admin/pdf/jobs/{id}` | Basic Auth |
@@ -26,7 +25,9 @@
## 路由约定
-**所有 zTools2 托管的入口(页面 / 静态资源 / 健康探针 / WebSocket / REST API)统一挂在 `/api/` 前缀下**,只有元信息/文档例外(`/`、`/docs`、`/redoc`、`/openapi.json`)。这样反向代理与 vite dev proxy 都只需一条 `/api/` 规则即可把请求转给当前环境的 zTools2,前端 iframe 用**同源相对路径** `/api/pdf` 即可,与运行环境(本地 / 测试 / 生产)无关,无需区分 dev/prod 指向。
+**所有 zTools2 托管的入口(页面 / 静态资源 / 健康探针 / WebSocket / REST API)统一挂在 `/api/` 前缀下**,只有元信息/文档例外(`/`、`/docs`、`/redoc`、`/openapi.json`)。这样反向代理与 vite dev proxy 都只需一条 `/api/` 规则即可把请求转给当前环境的 zTools2,前端用**同源相对路径**(如 `/api/pdf/jobs`、`/api/health`)即可,与运行环境(本地 / 测试 / 生产)无关,无需区分 dev/prod 指向。
+
+> PDF 转换的用户侧 UI 由 zMainPage 的 zPDF_package 组件提供(构建期 import,非 iframe);zTools2 仅提供 `/api/pdf/jobs` 等 REST API 与 `/api/pdf-admin` 管理页。
页面类入口为避免与同名 REST API 冲突,统一加 `-page` 后缀:
@@ -35,7 +36,7 @@
| 文件浏览 | `GET /api/files-page` | `GET /api/files`、`/api/files/{id}` 等 |
| 记事本 | `GET /api/wb-page/{id}` | `GET /api/wb/{id}` |
-其余入口:`/api/health`(探针)、`/api/static/*`(JS/CSS)、`/api/ws/wb/{id}`(WebSocket)、`/api/upload`、`/api/pdf`、`/api/pdf-admin`、`/api/wb-admin`。
+其余入口:`/api/health`(探针)、`/api/static/*`(JS/CSS)、`/api/ws/wb/{id}`(WebSocket)、`/api/upload`、`/api/pdf-admin`、`/api/wb-admin`。
## 项目结构
@@ -185,7 +186,7 @@ systemctl reload apache2
| 文件浏览 | https://f.zikai.wang/api/files-page(Basic Auth) |
| 共享记事本 | https://f.zikai.wang/api/wb-page/{id}(公开,`{id}` 为 `[a-zA-Z0-9_-]{1,64}`) |
| 记事本管理 | https://f.zikai.wang/api/wb-admin(Basic Auth) |
-| PDF 转换 | https://f.zikai.wang/api/pdf(公开,凭 cookie) |
+| PDF 转换 | 经 zMainPage 的 PDF 页签(zPDF_package 组件)调用 `/api/pdf/jobs` 等 REST API(公开,凭 cookie) |
| 系统状态 | https://f.zikai.wang/api/system/status(HTML,`?format=json` 切 JSON) |
| curl 上传 | `curl -F file=@big.iso https://f.zikai.wang/api/files/upload` |
| SFTP | `sftp -P 2022 uploader@f.zikai.wang` |
diff --git a/app/main.py b/app/main.py
index 78e0405..4ebed17 100644
--- a/app/main.py
+++ b/app/main.py
@@ -223,16 +223,6 @@ def create_app() -> FastAPI:
def whiteboard_page(board_id: str) -> HTMLResponse:
return _serve_static_html("whiteboard.html")
- @app.get(
- "/api/pdf",
- response_class=HTMLResponse,
- tags=["pages"],
- summary="PDF 转换页面",
- description="上传 epub 文件转为 PDF,显示进度并下载;凭 cookie 查看自己的任务。",
- )
- def pdf_page() -> HTMLResponse:
- return _serve_static_html("pdf.html")
-
@app.get(
"/api/pdf-admin",
response_class=HTMLResponse,
diff --git a/static/pdf.css b/static/pdf.css
deleted file mode 100644
index 9bb258f..0000000
--- a/static/pdf.css
+++ /dev/null
@@ -1,40 +0,0 @@
-/* PDF 转换页专属样式:上传区、进度条、任务表格。复用 common.css 的 .btn/.tag/table 等。 */
-.uploader { margin: 0 0 1.6em; }
-.dropzone {
- display: flex; flex-direction: column; align-items: center; justify-content: center;
- gap: 0.6em; padding: 2.4em 1.2em; min-height: 160px;
- border: 2px dashed var(--border); border-radius: var(--radius);
- background: var(--surface); text-align: center; transition: border-color 0.15s, background 0.15s;
-}
-.dropzone.is-drag { border-color: var(--primary); background: var(--primary-soft); }
-.dropzone__hint { color: var(--text-dim); font-size: 0.95em; }
-.dropzone__limit { color: var(--text-dim); font-size: 0.8em; }
-.dropzone .btn.primary { padding: 0.55em 1.4em; }
-
-.upload-progress { margin-top: 0.8em; }
-.upload-progress.hidden { display: none; }
-.upload-progress__name { font-size: 0.88em; color: var(--text); margin-bottom: 0.3em; word-break: break-all; }
-
-.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; }
-
-.jobs-table th.col-size { width: 8em; }
-.jobs-table th.col-status { width: 12em; }
-.jobs-table th.col-time { width: 11em; }
-.jobs-table th.col-act { width: 9em; text-align: right; }
-.jobs-table td.col-act { text-align: right; white-space: nowrap; }
-.jobs-table td.col-name { font-weight: 600; word-break: break-all; }
-.jobs-table td.col-act .btn { padding: 0.3em 0.8em; font-size: 0.85em; margin-left: 0.3em; }
-
-.prog { display: inline-flex; align-items: center; gap: 0.5em; font-size: 0.82em; }
-.prog__label { color: var(--text-dim); white-space: nowrap; }
-
-.hidden { display: none; }
-.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; }
- .dropzone { padding: 1.8em 0.8em; min-height: 130px; }
-}
diff --git a/static/pdf.html b/static/pdf.html
deleted file mode 100644
index 017aa82..0000000
--- a/static/pdf.html
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-PDF 转换 - zikai
-
-
-
-
-
-
PDF 转换
-
上传 epub 文件转为 PDF,转换完成后可下载。单文件上限 250MB。凭浏览器 cookie 记住你的任务。
-
-
-
-
拖入 .epub 文件,或
-
-
仅支持 epub,最大 250MB
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/static/pdf.js b/static/pdf.js
deleted file mode 100644
index cf01e38..0000000
--- a/static/pdf.js
+++ /dev/null
@@ -1,191 +0,0 @@
-/* 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();
-})();
diff --git a/static/pdf_admin.css b/static/pdf_admin.css
index a07afaf..6592e27 100644
--- a/static/pdf_admin.css
+++ b/static/pdf_admin.css
@@ -1,4 +1,23 @@
-/* PDF 转换管理页专属样式(叠加在 pdf.css 之上)。标注「已删除」行、列宽调整。 */
+/* 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; }
+
+.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; }
@@ -12,7 +31,11 @@
.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; }
}
diff --git a/static/pdf_admin.html b/static/pdf_admin.html
index 900d279..4c369c5 100644
--- a/static/pdf_admin.html
+++ b/static/pdf_admin.html
@@ -5,7 +5,6 @@
PDF 转换管理 - zikai
-
@@ -22,7 +21,7 @@
加载中…
-
+