import type { LibraryItem, LibraryListQuery, MediaKind, SlotStatus } from "@umt/contracts"; import { Download, Library as LibraryIcon, RotateCw } from "lucide-react"; import { useDeferredValue } from "react"; import { Link, useNavigate, useSearchParams } from "react-router"; import { useLibraryList } from "~/api/library.ts"; import { type DataColumn, DataTable } from "~/components/console/data-table.tsx"; import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx"; import { consolePath } from "~/components/console/nav.ts"; import { PageHeader } from "~/components/console/page-header.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { ENTITY_STATUS, MEDIA_KIND, opts, SLOT_STATUS, SLOT_STATUS_ORDER } from "~/components/library/labels.ts"; import { CoverPlaceholder, relTime, Tone } from "~/components/library/parts.tsx"; import { Button } from "~/components/ui/button.tsx"; const FILTERS: { value: NonNullable; label: string }[] = [ { value: "gap", label: "有缺口" }, { value: "attention", label: "需处理" }, { value: "downloading", label: "下载中" }, { value: "satisfied", label: "已满足" }, ]; /** 各 Slot 状态计数:只列非零项,文字 + 语气 */ function SlotSummary({ counts }: { counts: LibraryItem["slotCounts"] }) { const parts = SLOT_STATUS_ORDER.filter((s) => counts[s] > 0); if (parts.length === 0) return 未设目标; return (
{parts.map((s: SlotStatus) => ( {SLOT_STATUS[s].label} {counts[s]} ))}
); } export default function LibraryPage() { const [sp, setSp] = useSearchParams(); const navigate = useNavigate(); const kind = (sp.get("kind") ?? "") as MediaKind | ""; const filter = (sp.get("filter") ?? "") as LibraryListQuery["filter"] | ""; const q = sp.get("q") ?? ""; const deferredQ = useDeferredValue(q.trim()); const set = (k: string, v: string) => setSp( (prev) => { const next = new URLSearchParams(prev); if (v) next.set(k, v); else next.delete(k); return next; }, { replace: true }, ); const list = useLibraryList({ kind: kind || undefined, filter: filter || undefined, q: deferredQ || undefined, }); const rows = list.data?.pages.flatMap((p) => p.items); const filtered = !!(kind || filter || q); const columns: DataColumn[] = [ { key: "title", header: "作品", cell: (r) => (
e.stopPropagation()} className="block max-w-[14rem] truncate font-medium text-foreground hover:underline sm:max-w-xs" > {r.entity.title}

{MEDIA_KIND[r.entity.kind]} {r.entity.year ? ` · ${r.entity.year}` : ""} {r.entity.originalTitle && r.entity.originalTitle !== r.entity.title ? ` · ${r.entity.originalTitle}` : ""}

{r.entity.status !== "active" ? : null}
), }, { key: "slots", header: "收录目标", cell: (r) => }, { key: "assets", header: "资产", align: "right", hideBelow: "sm", cell: (r) => {r.assetCount}, }, { key: "dl", header: "下载中", align: "right", hideBelow: "md", cell: (r) => r.activeDownloads ? ( {r.activeDownloads} ) : ( — ), }, { key: "org", header: "最近入库", hideBelow: "lg", cell: (r) => {relTime(r.lastOrganizedAt)}, }, ]; return ( <> list.refetch()} disabled={list.isFetching}> 刷新 } /> set("q", v), placeholder: "标题 / 原名 / 别名" }} onReset={filtered ? () => setSp(new URLSearchParams(), { replace: true }) : undefined} > set("kind", v)} options={opts(MEDIA_KIND)} /> set("filter", v)} options={FILTERS} /> r.entity.id} onRowClick={(r) => navigate(consolePath("library", r.entity.id))} loading={list.isPending} error={list.error} onRetry={() => list.refetch()} empty={ filtered ? { title: "没有符合条件的作品", description: "换个关键字或清除筛选试试。", icon: LibraryIcon } : { title: "媒体库还是空的", description: "登记库根并完成首次扫描,或新建订阅后,作品会出现在这里。", icon: LibraryIcon, action: ( ), } } hasMore={!!list.hasNextPage} onLoadMore={() => list.fetchNextPage()} loadingMore={list.isFetchingNextPage} /> ); }