import { useQueryClient } from "@tanstack/react-query"; import type { DownloadBoardItem, DownloadBoardRow, DownloadBoardState, DownloadTask } from "@umt/contracts"; import { EventType } from "@umt/contracts"; import { RefreshCw } from "lucide-react"; import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; import { useSearchParams } from "react-router"; import { useBoardPlanRow, useDownloadBoard, useDownloadTask, usePlan } from "~/api/downloads.ts"; import { isMissingEndpoint } from "~/api/parse.ts"; import { type DataColumn, DataTable } from "~/components/console/data-table.tsx"; import { DetailSheet } from "~/components/console/detail-sheet.tsx"; import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { PageHeader } from "~/components/console/page-header.tsx"; import { useRecordSheet } from "~/components/console/record-sheet.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> 深链打开对应行。 // 记录抽屉(console-ia.md §9):行点开是右侧抽屉,?open=;旧的下载详情页 /console/downloads/: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); }; // 记录抽屉(console-ia.md §9):?open=<下载任务 id 或计划 id>;旧深链 ?plan=<计划 id> 同样打开 const sheet = useRecordSheet({ scoped: ["plan"] }); const openId = sheet.id; const [params] = useSearchParams(); const deepPlan = params.get("plan"); const qc = useQueryClient(); const board = useDownloadBoard(state); 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 inRows = openId ? allRows?.find((t) => t.id === openId) : undefined; // 不在当前分段里的:先当下载任务取,取不到再当计划找 const fetched = useDownloadTask(openId && !inRows && !board.isPending ? openId : undefined); const taskMissing = !!fetched.error && isMissingEndpoint(fetched.error); const planId = deepPlan ?? (taskMissing ? openId : null); const deep = useBoardPlanRow(inRows ? null : planId); // 旧深链 ?plan=:切到行所在的分段 useEffect(() => { const row = deep.data; if (!row || !deepPlan) return; setStateRaw(row.kind === "task" && COMPLETED.has(row.status) ? "completed" : "active"); }, [deep.data, deepPlan]); // 状态变化、阈值事件到达时立即刷新(速度靠轮询) useSse( [ EventType.downloadProgress, EventType.downloadStatusChanged, EventType.downloadSubmitted, EventType.downloadThreshold, ], () => void qc.invalidateQueries({ queryKey: ["downloads"] }), ); const first = pages?.[0]; const openRow = inRows ?? deep.data ?? null; const openTask: DownloadTask | null = openRow?.kind === "task" ? openRow : openId && fetched.data?.id === openId ? fetched.data : null; const openPlan = openRow?.kind === "plan" ? openRow : null; const sheetOpen = !!(openId || deepPlan); const columns = useMemo( () => (state === "active" ? activeColumns() : (completedColumns() as unknown as DataColumn[])), [state], ); const empty = BOARD_EMPTY[state]; const endedPlan = planId && deep.isSuccess && deep.data === null ? planId : null; // 关闭动画期间保留刚才的内容 const last = useRef<{ title: string; body: ReactNode }>({ title: "", body: null }); if (sheetOpen) last.current = { title: openTask?.name ?? openPlan?.name ?? "下载", body: openTask ? ( ) : openPlan ? ( ) : fetched.error && !taskMissing ? ( void fetched.refetch()} /> ) : ( ), }; const shown = last.current; return ( <> void qc.invalidateQueries({ queryKey: ["downloads"] })}> 刷新 } />
t.id} loading={board.isPending} error={board.error} onRetry={() => void board.refetch()} onRowClick={(t) => sheet.open(t.id)} selectedKey={openId} hasMore={board.hasNextPage} onLoadMore={() => void board.fetchNextPage()} loadingMore={board.isFetchingNextPage} empty={empty} /> !o && sheet.close()} width="lg" title={shown.title} > {shown.body} ); } /** 深链指向已结束的下载(没提交成功 / 已取消 / 已过期):说明原因与来由;作品的下载历史里也有 */ 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} ) : (

这次下载已经结束。

)}
)}
); }