import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { api } from "../api/client"; import type { Book } from "../api/types"; import { useAuth } from "../auth/AuthContext"; import { Cover } from "../components/Cover"; import { TopBar } from "../components/TopBar"; import { toast } from "../components/Toaster"; import { btn, btnPrimary, card, input } from "../components/ui"; import { groupByDir } from "../lib/group"; function BookCard({ book, onOpen, onDelete, }: { book: Book; onOpen: () => void; onDelete: () => void; }) { const { isAdmin } = useAuth(); return (
{book.state === "error" && ( 损坏 )} {book.percent > 0 && (
)} {isAdmin && ( )}
); } export default function Shelf() { const qc = useQueryClient(); const nav = useNavigate(); const { isAdmin } = useAuth(); const [lib, setLib] = useState(0); // 0 = 全部库 const [qRaw, setQRaw] = useState(""); const [q, setQ] = useState(""); useEffect(() => { const t = setTimeout(() => setQ(qRaw.trim()), 300); // 搜索防抖 return () => clearTimeout(t); }, [qRaw]); const libsQ = useQuery({ queryKey: ["libraries"], queryFn: api.listLibraries }); const booksQ = useQuery({ queryKey: ["books", lib, q], queryFn: () => api.listBooks({ library: lib || undefined, q }), }); const progQ = useQuery({ queryKey: ["progress"], queryFn: api.listProgress }); // 继续阅读行 → book id 解析用(键与 booksQ 默认态一致,天然共享缓存) const allQ = useQuery({ queryKey: ["books", 0, ""], queryFn: () => api.listBooks() }); const del = useMutation({ mutationFn: (id: number) => api.deleteBook(id), onSuccess: () => { toast("ok", "已删除"); qc.invalidateQueries({ queryKey: ["books"] }); }, onError: (e) => toast("err", e instanceof Error ? e.message : "删除失败"), }); const scan = useMutation({ mutationFn: (id: number) => api.scanLibrary(id), onSuccess: () => { toast("ok", "扫描已触发"); setTimeout(() => qc.invalidateQueries({ queryKey: ["books"] }), 3000); // 给周期扫描留出时间 }, onError: (e) => toast("err", e instanceof Error ? e.message : "扫描失败"), }); const groups = useMemo(() => groupByDir(booksQ.data ?? []), [booksQ.data]); const booksByKey = useMemo(() => { const m = new Map(); for (const b of allQ.data ?? []) m.set(`${b.library_id}/${b.path}`, b); return m; }, [allQ.data]); const continueRows = (progQ.data ?? []).slice(0, 12); const libs = libsQ.data ?? []; return (
{isAdmin && lib > 0 && ( )} setQRaw(e.target.value)} /> } />
{libs.length === 0 && (

还没有书库。admin 请到「库管理」建库并扫描。

)}
{libs.map((l) => ( ))}
{continueRows.length > 0 && (

继续阅读

{continueRows.map((r) => { const b = booksByKey.get(`${r.library_id}/${r.path}`); if (!b) return (

{r.title || "已删除的书"}

{Math.round(r.percent * 100)}%

); return ( nav(`/book/${b.id}`)} onDelete={() => del.mutate(b.id)} /> ); })}
)} {booksQ.isError && (

加载失败:{booksQ.error instanceof Error ? booksQ.error.message : ""}{" "}

)} {!booksQ.isPending && groups.length === 0 && !booksQ.isError && (

此库暂无书目(放入文件后等待扫描或点「扫描此库」)。

)} {groups.map((g) => (

{g.dir || "根目录"}

{g.books.map((b) => ( nav(`/book/${b.id}`)} onDelete={() => del.mutate(b.id)} /> ))}
))}
); }