import { useQueryClient } from "@tanstack/react-query"; import type { AcquisitionPlan, DownloadStatus, DownloadTask, PlanStatus } from "@umt/contracts"; import { EventType } from "@umt/contracts"; import { Download, ListChecks, RefreshCw } from "lucide-react"; import { useState } from "react"; import { Link, useNavigate } from "react-router"; import { DOWNLOAD_STATUS_LABEL, PLAN_STATUS_LABEL, useDownloaderNames, useDownloadTasks, usePlans, } from "~/api/downloads.ts"; import { type DataColumn, DataTable } from "~/components/console/data-table.tsx"; import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx"; import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx"; import { JsonView } from "~/components/console/json-view.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { PageHeader } from "~/components/console/page-header.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { BusinessTasks } from "~/components/downloads/business-tasks.tsx"; 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs.tsx"; import { relTime } from "~/components/workbench/release-fields.ts"; import { useSse } from "~/lib/sse.ts"; import { formatBytes, formatDateTime } from "~/lib/utils.ts"; const PURPOSE = { fill: "补缺", upgrade: "升级", replace_source: "换源" } as const; export default function TasksPage() { const [status, setStatus] = useState(""); const [managed, setManaged] = useState(""); const [planStatus, setPlanStatus] = useState(""); const [openId, setOpenId] = useState(null); const [openPlan, setOpenPlan] = useState(null); const navigate = useNavigate(); const qc = useQueryClient(); const downloads = useDownloadTasks({ status: (status || undefined) as DownloadStatus | undefined, managed: (managed || undefined) as "true" | "false" | undefined, }); const plans = usePlans((planStatus || undefined) as PlanStatus | 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 planRows = plans.data?.pages.flatMap((p) => p.items); const open = rows?.find((t) => t.id === openId) ?? null; const linkedDownload = openPlan ? rows?.find((t) => t.acquisitionPlanId === openPlan.id) : undefined; const columns: DataColumn[] = [ { key: "name", header: "任务", cell: (t) => (

{t.name}

{names.data?.get(t.instanceId) ?? "下载器"} · {t.tags.join(", ") || "无标签"}

), }, { key: "status", header: "状态", cell: (t) => }, { key: "progress", header: "进度", hideBelow: "sm", cell: (t) => }, { key: "size", header: "大小", hideBelow: "lg", align: "right", cell: (t) => ( {t.sizeBytes === null ? "未知" : formatBytes(t.sizeBytes, 1)} ), }, { key: "at", header: "更新", hideBelow: "md", cell: (t) => ( {relTime(t.lastSyncedAt ?? t.updatedAt)} ), }, { key: "ops", header: "", align: "right", cell: (t) => }, ]; const planColumns: DataColumn[] = [ { key: "purpose", header: "计划", cell: (p) => (

{PURPOSE[p.purpose]} · 优先级 {p.priority}

{p.id.slice(0, 8)}

), }, { key: "status", header: "状态", cell: (p) => ( {PLAN_STATUS_LABEL[p.status]} ), }, { key: "reserved", header: "预留空间", hideBelow: "md", align: "right", cell: (p) => {formatBytes(p.reservedBytes, 1)}, }, { key: "reason", header: "说明", hideBelow: "lg", cell: (p) => {p.statusReason ?? "—"}, }, { key: "at", header: "创建", hideBelow: "sm", cell: (p) => {relTime(p.createdAt)}, }, ]; return ( <> void qc.invalidateQueries({ queryKey: ["downloads"] })}> 刷新 } /> 下载任务 下载计划 业务任务 { setStatus(""); setManaged(""); } : undefined } > ({ value, label }))} /> 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: status || managed ? "没有符合筛选的下载" : "还没有下载任务", description: "自动收录或在站点工作台手动下载后,任务会出现在这里。", }} /> setPlanStatus("") : undefined}> ({ value, label }))} /> p.id} loading={plans.isPending} error={plans.error} onRetry={() => void plans.refetch()} onRowClick={setOpenPlan} selectedKey={openPlan?.id} hasMore={plans.hasNextPage} onLoadMore={() => void plans.fetchNextPage()} loadingMore={plans.isFetchingNextPage} empty={{ icon: ListChecks, title: "还没有下载计划", description: "规则评估选中候选后生成计划并预留 Slot 与预算。", }} /> !o && setOpenId(null)} width="lg" title={open?.name ?? ""} footer={ open ? ( ) : null } > {open ? : null} !o && setOpenPlan(null)} title={openPlan ? `${PURPOSE[openPlan.purpose]}计划` : ""} description={openPlan?.statusReason ?? undefined} footer={ linkedDownload ? ( ) : null } > {openPlan ? ( <> ) : null} ); }