import { useQueryClient } from "@tanstack/react-query"; import { ExternalLink, RotateCw } from "lucide-react"; import { Link, useSearchParams } from "react-router"; import { useEngineRun, useEngineStats } from "~/api/engine.ts"; 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 { ScheduleBoard } from "~/components/schedules/schedule-board.tsx"; import { runTitle } from "~/components/tasks/labels.ts"; import { RunActions, TaskDetailBody, TaskStatusLine } from "~/components/tasks/task-detail.tsx"; import { type StreamFilters, type StreamTab, TaskStream } from "~/components/tasks/task-stream.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs.tsx"; import { qk } from "~/lib/query-keys.ts"; import { legacyRedirectLoader } from "./redirects.ts"; export const meta = () => [{ title: "任务 · UHD 媒体自动化控制台" }]; // 旧的 ?tab=downloads / plans / business 跳到下载面板 / 下载计划 / 任务流 export const clientLoader = legacyRedirectLoader; type Tab = StreamTab | "plan"; const TABS: Tab[] = ["all", "active", "failed", "plan"]; const FILTER_KEYS: (keyof StreamFilters)[] = ["status", "kind", "ref", "source", "since", "workflow"]; function Count({ n }: { n: number | undefined }) { if (!n) return null; return {n > 999 ? "999+" : n}; } function RunSheet({ runId, onOpen, onClose, }: { runId: string | null; onOpen: (id: string) => void; onClose: () => void; }) { const q = useEngineRun(runId ?? undefined); const r = q.data; return ( !o && onClose()} width="lg" title={r ? runTitle(r) : "任务"} description={r ? : undefined} footer={ r ? ( <> ) : null } > {q.isPending ? ( ) : q.isError ? ( q.refetch()} /> ) : r ? ( ) : null} ); } export default function TasksPage() { const qc = useQueryClient(); const [params, setParams] = useSearchParams(); const raw = params.get("tab") as Tab | null; const tab: Tab = raw && TABS.includes(raw) ? raw : "all"; const runId = params.get("run"); const filters = Object.fromEntries(FILTER_KEYS.map((k) => [k, params.get(k) ?? ""])) as StreamFilters; const stats = useEngineStats(); const by = stats.data?.byStatus; const update = (fn: (p: URLSearchParams) => void) => setParams( (prev) => { const next = new URLSearchParams(prev); fn(next); return next; }, { replace: true }, ); const setFilter = (k: keyof StreamFilters, v: string) => update((p) => { if (v) p.set(k, v); else p.delete(k); }); return ( <> { qc.invalidateQueries({ queryKey: qk.all("engine") }); qc.invalidateQueries({ queryKey: qk.all("schedules") }); }} > 刷新 } /> update((p) => { p.set("tab", v); p.delete("job"); if (v !== "all") p.delete("status"); }) } > 全部 进行中 失败 计划
{tab === "plan" ? ( ) : ( update((p) => { for (const k of FILTER_KEYS) p.delete(k); }) } onOpen={(id) => update((p) => p.set("run", id))} selectedId={runId} /> )}
update((p) => p.set("run", id))} onClose={() => update((p) => p.delete("run"))} /> ); }