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
-
- -
- -
- - -
- -
-
加载中…
-
- -

zikai file service

-
- - - - 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 @@
加载中…
-

PDF 转换页 · zikai file service

+

zikai file service