import { useQueryClient } from "@tanstack/react-query"; import type { DownloadBoardItem, DownloadBoardRow, DownloadBoardState } from "@umt/contracts"; import { EventType } from "@umt/contracts"; import { RefreshCw } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { Link, useSearchParams } from "react-router"; import { useBoardPlanRow, useDownloadBoard, usePlan } from "~/api/downloads.ts"; import { type DataColumn, DataTable } from "~/components/console/data-table.tsx"; import { DetailSheet } from "~/components/console/detail-sheet.tsx"; import { LoadingState } from "~/components/console/empty-state.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 { readBoardState, writeBoardState } from "~/components/downloads/board-format.ts"; import { CacheVolumes } from "~/components/downloads/cache-volumes.tsx"; import { activeColumns, BOARD_EMPTY, BoardSegments, completedColumns, TransferLine, } from "~/components/downloads/download-board.tsx"; import { DownloadDetail } from "~/components/downloads/download-detail.tsx"; import { DROPPED_STATUS, droppedReason } from "~/components/downloads/plan-format.ts"; import { PlanDetail, WhySection } from "~/components/downloads/plan-row.tsx"; import { Button } from "~/components/ui/button.tsx"; import { useSse } from "~/lib/sse.ts"; export const meta = () => [{ title: "下载面板 · UHD 媒体自动化控制台" }]; // DL-2:默认只看进行中;已完成单独一段(本浏览器记住选择)。页面可见时每 4 秒刷新速度,切到后台停。 // DL-3:没提交到下载器的下载(等批准 / 等磁盘空间…)也是「进行中」的行;?plan=<计划 id> 深链打开对应行。 const COMPLETED = new Set(["completed", "importing", "imported"]); export default function DownloadsPage() { // SPA 模式:路由组件只在浏览器渲染,可直接读本地记住的分段 const [state, setStateRaw] = useState(readBoardState); const setState = (s: DownloadBoardState) => { setStateRaw(s); writeBoardState(s); }; const [openId, setOpenId] = useState(null); const [params, setParams] = useSearchParams(); const deepPlan = params.get("plan"); const qc = useQueryClient(); const board = useDownloadBoard(state); const deep = useBoardPlanRow(deepPlan); // 深链:找到行就切到它所在的分段并打开;没有行(已结束的下载)打开结束说明 useEffect(() => { const row = deep.data; if (!row) return; setStateRaw(row.kind === "task" && COMPLETED.has(row.status) ? "completed" : "active"); setOpenId(row.id); }, [deep.data]); // 状态变化、阈值事件到达时立即刷新(速度靠轮询) useSse( [ EventType.downloadProgress, EventType.downloadStatusChanged, EventType.downloadSubmitted, EventType.downloadThreshold, ], () => void qc.invalidateQueries({ queryKey: ["downloads"] }), ); const pages = board.data?.pages; const allRows = pages?.flatMap((p) => p.items); const rows: DownloadBoardRow[] | undefined = state === "completed" ? allRows?.filter((r): r is DownloadBoardItem => r.kind === "task") : allRows; const first = pages?.[0]; const open = rows?.find((t) => t.id === openId) ?? (deep.data?.id === openId ? deep.data : null) ?? null; const columns = useMemo( () => (state === "active" ? activeColumns() : (completedColumns() as unknown as DataColumn[])), [state], ); const empty = BOARD_EMPTY[state]; const closeDeep = () => { if (!deepPlan) return; const next = new URLSearchParams(params); next.delete("plan"); setParams(next, { replace: true }); }; const endedPlan = deepPlan && deep.isSuccess && deep.data === null ? deepPlan : null; return ( <> void qc.invalidateQueries({ queryKey: ["downloads"] })}> 刷新 } />
t.id} loading={board.isPending} error={board.error} onRetry={() => void board.refetch()} onRowClick={(t) => setOpenId(t.id)} selectedKey={openId} hasMore={board.hasNextPage} onLoadMore={() => void board.fetchNextPage()} loadingMore={board.isFetchingNextPage} empty={empty} /> { if (o) return; setOpenId(null); closeDeep(); }} width="lg" title={open?.name ?? ""} footer={ open?.kind === "task" ? ( ) : null } > {open?.kind === "task" ? : null} {open?.kind === "plan" ? : null} ); } /** 深链指向已结束的下载(没提交成功 / 已取消 / 已过期):说明原因与来由;作品的下载历史里也有 */ function EndedPlanSheet({ planId, onClose }: { planId: string | null; onClose: () => void }) { const plan = usePlan(planId); const p = plan.data; const ended = p && (p.status === "failed" || p.status === "cancelled" || p.status === "expired") ? p.status : null; return ( !o && onClose()} title={ended ? "这次下载没有提交" : "下载"} description={p ? (droppedReason(p.statusReason) ?? undefined) : undefined} > {plan.isPending ? ( ) : !p ? (

没找到这次下载,可能已被清理。

) : (
{ended ? ( {DROPPED_STATUS[ended].label} ) : (

这次下载已经结束。

)}
)}
); }