From fd227c1f006d84cc49bd2928378670fe06828bab Mon Sep 17 00:00:00 2001 From: Fendy Date: Tue, 8 Sep 2026 21:45:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20table-style=20=E5=BA=93=E7=AE=A1?= =?UTF-8?q?=E7=90=86,=20admin=20entries=20under=20avatar=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=AE=A1=E7=90=86,=20shelf-view=20upload=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/CHANGELOG_web.md | 3 + frontend/src/components/app-shell.tsx | 24 ++++-- frontend/src/components/upload-button.tsx | 59 ++++++++++++++ frontend/src/pages/Shelf.tsx | 12 ++- frontend/src/pages/admin/Libraries.tsx | 98 +++++++++-------------- 5 files changed, 124 insertions(+), 72 deletions(-) create mode 100644 frontend/src/components/upload-button.tsx diff --git a/docs/CHANGELOG_web.md b/docs/CHANGELOG_web.md index 351b7f6..6d7f398 100644 --- a/docs/CHANGELOG_web.md +++ b/docs/CHANGELOG_web.md @@ -31,6 +31,9 @@ The format loosely follows Keep a Changelog and can be adapted to the team's hab ### Changed / 变更 +- Admin polish: 库管理 is now a table styled like 用户管理 (scan/upload per row, drag-to-row upload kept); both admin pages moved out of the sidebar and bottom tabs into a 系统管理 group under the avatar menu; admins can upload files straight from a library's shelf view via a new 上传文件 button next to 扫描此库. +- 后台入口统一:库管理改为用户管理同款表格(行内扫描/上传,仍支持拖文件到行);两个管理页从侧边栏与移动端底 Tab 收进头像菜单的「系统管理」分组;书架进入某个库时,管理员可在「扫描此库」旁直接「上传文件」。 + - Reader chrome is no longer text-only: lucide icons accompany 导航/上一章/下一章/翻页/抽屉选项卡/书签操作, while 连读 and 预读 (now a 0–3 slider) collapse into a 更多 menu in the CBZ bar. - 阅读器工具条告别纯文字:导航/上一章/下一章/翻页/抽屉选项卡/书签操作均配 lucide 图标;连读与预读(改为 0–3 滑条)收进 CBZ 工具条的「更多」菜单。 diff --git a/frontend/src/components/app-shell.tsx b/frontend/src/components/app-shell.tsx index 9352366..8e7b4f5 100644 --- a/frontend/src/components/app-shell.tsx +++ b/frontend/src/components/app-shell.tsx @@ -58,6 +58,22 @@ export function AppShell() { {user?.username}{isAdmin ? " · admin" : ""} + {isAdmin && ( + <> + + 系统管理 + + + 库管理 + + + + + 用户管理 + + + + )} 退出登录 @@ -73,12 +89,6 @@ export function AppShell() { {libs.length > 0 && (
@@ -112,8 +122,6 @@ export function AppShell() { {/* 移动端底 Tab */}
diff --git a/frontend/src/components/upload-button.tsx b/frontend/src/components/upload-button.tsx new file mode 100644 index 0000000..350a792 --- /dev/null +++ b/frontend/src/components/upload-button.tsx @@ -0,0 +1,59 @@ +import { useQueryClient, type QueryClient } from "@tanstack/react-query"; +import { useRef, useState, type ComponentProps } from "react"; +import { Upload } from "lucide-react"; +import { api } from "@/api/client"; +import { Button } from "@/components/ui/button"; +import { toast } from "./Toaster"; + +export async function uploadToLib(qc: QueryClient, libId: number, list: FileList | File[]): Promise { + const files = Array.from(list); // FileList 活引用,先快照 + if (!files.length) return false; + try { + for (const f of files) await api.uploadBook(libId, f); + toast("ok", `已上传 ${files.length} 个文件,等待 scanner 收编`); + void qc.invalidateQueries({ queryKey: ["books"] }); + setTimeout(() => void qc.invalidateQueries({ queryKey: ["books"] }), 5000); + return true; + } catch (e) { + toast("err", e instanceof Error ? e.message : "上传失败"); + return false; + } +} + +/** 上传按钮(隐藏 file input),白名单:cbz/zip/pdf/epub/txt/md. */ +export function UploadButton({ + libId, + variant, + size = "sm", + label = "上传", +}: { + libId: number; + variant?: ComponentProps["variant"]; + size?: ComponentProps["size"]; + label?: string; +}) { + const qc = useQueryClient(); + const ref = useRef(null); + const [busy, setBusy] = useState(false); + return ( + <> + { + const files = Array.from(e.target.files ?? []); + e.target.value = ""; + setBusy(true); + await uploadToLib(qc, libId, files); + setBusy(false); + }} + /> + + + ); +} diff --git a/frontend/src/pages/Shelf.tsx b/frontend/src/pages/Shelf.tsx index 5d8d027..9931404 100644 --- a/frontend/src/pages/Shelf.tsx +++ b/frontend/src/pages/Shelf.tsx @@ -7,6 +7,7 @@ import type { Book } from "@/api/types"; import { useAuth } from "@/auth/AuthContext"; import { Cover } from "@/components/Cover"; import { toast } from "@/components/Toaster"; +import { UploadButton } from "@/components/upload-button"; import { formatBadge } from "@/components/ui"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, @@ -177,10 +178,13 @@ export default function Shelf() { ))} {isAdmin && lib > LIB0 && ( - + <> + + + )} - {libsQ.isPending && ( -
- {Array.from({ length: 3 }).map((_, i) => ( - - ))} -
+ {libsQ.isPending ? ( + + ) : libs.length === 0 ? ( +

还没有库,用上方表单创建。

+ ) : ( + + + + ID + 库名 + 服务端路径 + + + + + {libs.map((l) => ( + e.preventDefault()} onDrop={(e) => onDrop(e, l.id)}> + {l.id} + {l.name} + {l.root_path} + +
+ + +
+
+
+ ))} +
+
)} -
- {libs.map((l) => ( - e.preventDefault()} onDrop={(e) => onDrop(e, l.id)}> - - - {l.name} - #{l.id} - -

{l.root_path}

-
- - - { - fileRefs.current[l.id] = el; - }} - type="file" - multiple - accept=".cbz,.zip,.pdf,.epub,.txt,.md" - className="hidden" - onChange={(e) => { - void onFiles(l.id, e.target.files); - e.target.value = ""; - }} - /> - - 白名单:cbz/zip/pdf/epub/txt/md · 也可直接拖文件进卡片 - -
- ))} - {libs.length === 0 && !libsQ.isPending &&

还没有库,用上方表单创建。

} -
+

上传白名单:cbz/zip/pdf/epub/txt/md · 也可直接拖文件进行

); }