feat(downloads): live speeds and active/completed split on the download panel (DL-2)

- 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]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 21:44:34 +08:00
1 parent 8086250ec2
commit 3b340745bf
17 files changed
+1113 -93

No files matched your search

+25
View File
@@ -3,6 +3,8 @@ import {
AcquisitionPlan,
cursorPage,
Dashboard,
DownloadBoard,
type DownloadBoardState,
DownloaderInstance,
type DownloadStatus,
DownloadTask,
@@ -26,6 +28,7 @@ export const downloadKeys = {
plans: (status?: PlanStatus) => ["downloads", "plans", status ?? "all"] as const,
plan: (id: string) => ["downloads", "plan", id] as const,
cache: () => ["downloads", "cache"] as const,
board: (state: DownloadBoardState) => ["downloads", "board", state] as const,
};
const ACTIVE: DownloadStatus[] = ["queued", "submitting", "downloading", "importing", "stalled"];
@@ -47,6 +50,28 @@ export function useDownloadTasks(f: DownloadFilters) {
});
}
/** 下载面板轮询间隔:页面可见时刷新,标签页在后台时 React Query 自动暂停(refetchIntervalInBackground=false) */
export const BOARD_POLL_MS = 4 * TTL_SECOND;
/**
* 下载面板(DL-2):分组列表 + 实时速度 + 两组计数 + 下载器总速度。
* 不保留上一个分组的数据:两组列不同,切换时先显示加载态。
*/
export function useDownloadBoard(state: DownloadBoardState) {
return useInfiniteQuery({
queryKey: downloadKeys.board(state),
initialPageParam: undefined as string | undefined,
queryFn: ({ pageParam, signal }) =>
apiParsed(DownloadBoard, "/api/v1/download-tasks/board", {
params: cleanParams({ state, cursor: pageParam, limit: 50 }),
signal,
}),
getNextPageParam: (last) => last.nextCursor ?? undefined,
refetchInterval: BOARD_POLL_MS,
refetchIntervalInBackground: false,
});
}
export function useDownloadTask(id: string | undefined) {
return useQuery({
queryKey: downloadKeys.one(id ?? ""),