- GET /download-tasks/board?state=active|completed: per-torrent dl/up speed, ETA, seeds/leechs, downloader total speed (transfer/info), counts for both groups, latest organize result (status, reason code, open decision) - live qB data cached per instance for 2 s with in-flight coalescing - panel: total speed line, 进行中 / 已完成 segments remembered locally, 4 s polling while the page is visible, mobile layout, "—" for zero speed Co-Authored-By: Claude Opus 5.5 <[email protected]>
110 lines
4.0 KiB
TypeScript
110 lines
4.0 KiB
TypeScript
import { useQueryClient } from "@tanstack/react-query";
|
|
import type { DownloadBoardState } from "@umt/contracts";
|
|
import { EventType } from "@umt/contracts";
|
|
import { RefreshCw } from "lucide-react";
|
|
import { useMemo, useState } from "react";
|
|
import { Link } from "react-router";
|
|
import { useDownloadBoard } from "~/api/downloads.ts";
|
|
import { DataTable } from "~/components/console/data-table.tsx";
|
|
import { DetailSheet } from "~/components/console/detail-sheet.tsx";
|
|
import { consolePath } from "~/components/console/nav.ts";
|
|
import { PageHeader } from "~/components/console/page-header.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 { DownloadsTabs } from "~/components/downloads/downloads-tabs.tsx";
|
|
import { Button } from "~/components/ui/button.tsx";
|
|
import { useSse } from "~/lib/sse.ts";
|
|
|
|
export const meta = () => [{ title: "下载面板 · UHD 媒体自动化控制台" }];
|
|
|
|
// DL-2:默认只看进行中;已完成单独一段(本浏览器记住选择)。页面可见时每 4 秒刷新速度,切到后台停。
|
|
|
|
export default function DownloadsPage() {
|
|
// SPA 模式:路由组件只在浏览器渲染,可直接读本地记住的分段
|
|
const [state, setStateRaw] = useState<DownloadBoardState>(readBoardState);
|
|
const setState = (s: DownloadBoardState) => {
|
|
setStateRaw(s);
|
|
writeBoardState(s);
|
|
};
|
|
const [openId, setOpenId] = useState<string | null>(null);
|
|
const qc = useQueryClient();
|
|
const board = useDownloadBoard(state);
|
|
|
|
// 状态变化、阈值事件到达时立即刷新(速度靠轮询)
|
|
useSse(
|
|
[
|
|
EventType.downloadProgress,
|
|
EventType.downloadStatusChanged,
|
|
EventType.downloadSubmitted,
|
|
EventType.downloadThreshold,
|
|
],
|
|
() => void qc.invalidateQueries({ queryKey: ["downloads"] }),
|
|
);
|
|
|
|
const pages = board.data?.pages;
|
|
const rows = pages?.flatMap((p) => p.items);
|
|
const first = pages?.[0];
|
|
const open = rows?.find((t) => t.id === openId) ?? null;
|
|
const columns = useMemo(() => (state === "active" ? activeColumns() : completedColumns()), [state]);
|
|
const empty = BOARD_EMPTY[state];
|
|
|
|
return (
|
|
<>
|
|
<PageHeader
|
|
title="下载面板"
|
|
description="阈值暂停不能手动绕过;无进展只记录,不自动换源。"
|
|
actions={
|
|
<Button variant="outline" size="sm" onClick={() => void qc.invalidateQueries({ queryKey: ["downloads"] })}>
|
|
<RefreshCw className={board.isFetching ? "animate-spin" : undefined} />
|
|
刷新
|
|
</Button>
|
|
}
|
|
/>
|
|
<DownloadsTabs />
|
|
<CacheVolumes />
|
|
<div className="mb-3 flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
|
|
<BoardSegments value={state} counts={first?.counts} onChange={setState} />
|
|
<TransferLine transfer={first?.transfer} />
|
|
</div>
|
|
<DataTable
|
|
columns={columns}
|
|
rows={rows}
|
|
rowKey={(t) => 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}
|
|
/>
|
|
|
|
<DetailSheet
|
|
open={!!open}
|
|
onOpenChange={(o) => !o && setOpenId(null)}
|
|
width="lg"
|
|
title={open?.name ?? ""}
|
|
footer={
|
|
open ? (
|
|
<Button asChild variant="outline" size="sm">
|
|
<Link to={consolePath("downloads", open.id)}>打开详情页</Link>
|
|
</Button>
|
|
) : null
|
|
}
|
|
>
|
|
{open ? <DownloadDetail task={open} live={open.live} /> : null}
|
|
</DetailSheet>
|
|
</>
|
|
);
|
|
}
|