feat(web): regroup console nav into 7 groups, split downloads/plans/tasks, add legacy redirects
- Sidebar: 概览/资源/订阅/下载/整理/Agent/系统, 18 items (+ 定时任务); single active item via findNavItem; 待处理 badge; collapsed rail groups; mobile drawer shares NAV_GROUPS. - New pages: /downloads (queue + cache watermark header), /downloads/plans, /downloads/:id, /tasks (business tasks only) + /tasks/:id detail, /rules. - Legacy links: ?tab= on tasks/subscriptions/sources redirect via clientLoader; /tasks/:downloadId falls back to /downloads/:id. - List filters in URL query; settingsPath() helper for settings deep links. - Copy per glossary (待处理, 收录策略, 引擎面板, 文件管理, 版本目标). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
5843527317
commit
909cd2ad49
31 files changed
+1247
-609
No files matched your search
@@ -0,0 +1,155 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { DownloadStatus, DownloadTask } from "@umt/contracts";
|
||||
import { EventType } from "@umt/contracts";
|
||||
import { Download, RefreshCw } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { DOWNLOAD_STATUS_LABEL, useDownloaderNames, useDownloadTasks } from "~/api/downloads.ts";
|
||||
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
|
||||
import { DetailSheet } from "~/components/console/detail-sheet.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 { useQueryState } from "~/components/console/use-query-state.ts";
|
||||
import { CacheVolumes } from "~/components/downloads/cache-volumes.tsx";
|
||||
import {
|
||||
DownloadDetail,
|
||||
DownloadStatusBadges,
|
||||
PauseResume,
|
||||
ProgressLine,
|
||||
} from "~/components/downloads/download-detail.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { useSse } from "~/lib/sse.ts";
|
||||
import { formatBytes } from "~/lib/utils.ts";
|
||||
|
||||
export const meta = () => [{ title: "下载队列 · UHD 媒体自动化控制台" }];
|
||||
|
||||
export default function DownloadsPage() {
|
||||
const f = useQueryState(["status", "managed"] as const);
|
||||
const [openId, setOpenId] = useState<string | null>(null);
|
||||
const qc = useQueryClient();
|
||||
const downloads = useDownloadTasks({
|
||||
status: (f.values.status || undefined) as DownloadStatus | undefined,
|
||||
managed: (f.values.managed || undefined) as "true" | "false" | undefined,
|
||||
});
|
||||
const names = useDownloaderNames();
|
||||
|
||||
// 下载进度/状态、阈值事件到达时刷新列表(SSE 为主,轮询兜底)
|
||||
useSse(
|
||||
[
|
||||
EventType.downloadProgress,
|
||||
EventType.downloadStatusChanged,
|
||||
EventType.downloadSubmitted,
|
||||
EventType.downloadThreshold,
|
||||
],
|
||||
() => void qc.invalidateQueries({ queryKey: ["downloads"] }),
|
||||
);
|
||||
|
||||
const rows = downloads.data?.pages.flatMap((p) => p.items);
|
||||
const open = rows?.find((t) => t.id === openId) ?? null;
|
||||
|
||||
const columns: DataColumn<DownloadTask>[] = [
|
||||
{
|
||||
key: "name",
|
||||
header: "下载任务",
|
||||
cell: (t) => (
|
||||
<div className="min-w-0 max-w-sm">
|
||||
<p className="line-clamp-2 break-all text-foreground">{t.name}</p>
|
||||
<p className="mt-0.5 truncate text-meta text-muted-foreground">
|
||||
{names.data?.get(t.instanceId) ?? "下载器"} · {t.tags.join(", ") || "无标签"}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ key: "status", header: "状态", cell: (t) => <DownloadStatusBadges t={t} /> },
|
||||
{ key: "progress", header: "进度", hideBelow: "sm", cell: (t) => <ProgressLine t={t} /> },
|
||||
{
|
||||
key: "size",
|
||||
header: "大小",
|
||||
hideBelow: "lg",
|
||||
align: "right",
|
||||
cell: (t) => (
|
||||
<span className="whitespace-nowrap tabular-nums">
|
||||
{t.sizeBytes === null ? "未知" : formatBytes(t.sizeBytes, 1)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "at",
|
||||
header: "更新",
|
||||
hideBelow: "md",
|
||||
cell: (t) => (
|
||||
<span className="whitespace-nowrap text-muted-foreground">{relTime(t.lastSyncedAt ?? t.updatedAt)}</span>
|
||||
),
|
||||
},
|
||||
{ key: "ops", header: "", align: "right", cell: (t) => <PauseResume t={t} size="xs" /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="下载队列"
|
||||
description="阈值暂停不能手动绕过;无进展只记录,不自动换源。"
|
||||
actions={
|
||||
<Button variant="outline" size="sm" onClick={() => void qc.invalidateQueries({ queryKey: ["downloads"] })}>
|
||||
<RefreshCw className={downloads.isFetching ? "animate-spin" : undefined} />
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<CacheVolumes />
|
||||
<FilterBar onReset={f.active ? f.reset : undefined}>
|
||||
<FilterSelect
|
||||
label="状态"
|
||||
value={f.values.status}
|
||||
onChange={(v) => f.set("status", v)}
|
||||
options={Object.entries(DOWNLOAD_STATUS_LABEL).map(([value, label]) => ({ value, label }))}
|
||||
/>
|
||||
<FilterSelect
|
||||
label="纳管"
|
||||
value={f.values.managed}
|
||||
onChange={(v) => f.set("managed", v)}
|
||||
options={[
|
||||
{ value: "true", label: "纳管任务" },
|
||||
{ value: "false", label: "未纳管" },
|
||||
]}
|
||||
/>
|
||||
</FilterBar>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
rows={rows}
|
||||
rowKey={(t) => t.id}
|
||||
loading={downloads.isPending}
|
||||
error={downloads.error}
|
||||
onRetry={() => void downloads.refetch()}
|
||||
onRowClick={(t) => setOpenId(t.id)}
|
||||
selectedKey={openId}
|
||||
hasMore={downloads.hasNextPage}
|
||||
onLoadMore={() => void downloads.fetchNextPage()}
|
||||
loadingMore={downloads.isFetchingNextPage}
|
||||
empty={{
|
||||
icon: Download,
|
||||
title: f.active ? "没有符合筛选的下载任务" : "还没有下载任务",
|
||||
description: f.active ? undefined : "自动收录或在站点工作台下载后出现在这里。",
|
||||
}}
|
||||
/>
|
||||
|
||||
<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} /> : null}
|
||||
</DetailSheet>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user