Files
UHD_Media_Tool/apps/web/app/routes/console/tasks.tsx
T
rootandClaude Opus 5.5 baf2d6ea04 feat(web): split plan page from tasks (FE-6)
- Nav overview group: 首页 / 任务 / 计划 / Agent 会话; tasks page drops the plan tab
- /console/tasks?tab=plan[&job=] redirects to /console/schedules[?job=]
- Plan page: 自动任务 compact grouped list (switch, interval preset/custom, last result,
  countdown, failure reason, run now/skip/run records) and Agent 巡检 cards (goal,
  allowlist, budget, upstream/model, trigger, cost per run, recent sessions); split stats
- Home plan band: two counts linking to #auto / #checks
- Docs: console-ia §7.4a, glossary, schedules runbook; smoke routes

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 17:13:54 +08:00

153 lines
4.8 KiB
TypeScript

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 { 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 / plan 跳到下载面板 / 下载计划 / 任务流 / 计划页
export const clientLoader = legacyRedirectLoader;
type Tab = StreamTab;
const TABS: Tab[] = ["all", "active", "failed"];
const FILTER_KEYS: (keyof StreamFilters)[] = ["status", "kind", "ref", "source", "since", "workflow"];
function Count({ n }: { n: number | undefined }) {
if (!n) return null;
return <span className="text-meta text-muted-foreground tabular-nums">{n > 999 ? "999+" : n}</span>;
}
function RunSheet({
runId,
onOpen,
onClose,
}: {
runId: string | null;
onOpen: (id: string) => void;
onClose: () => void;
}) {
const q = useEngineRun(runId ?? undefined);
const r = q.data;
return (
<DetailSheet
open={!!runId}
onOpenChange={(o) => !o && onClose()}
width="lg"
title={r ? runTitle(r) : "任务"}
description={r ? <TaskStatusLine r={r} /> : undefined}
footer={
r ? (
<>
<Button asChild variant="ghost" size="sm">
<Link to={`/console/tasks/${encodeURIComponent(r.id)}`}>
<ExternalLink />
独立页面
</Link>
</Button>
<RunActions r={r} onOpenRun={onOpen} />
</>
) : null
}
>
{q.isPending ? (
<LoadingState rows={6} />
) : q.isError ? (
<ErrorState plain error={q.error} onRetry={() => q.refetch()} />
) : r ? (
<TaskDetailBody r={r} onOpenRun={onOpen} />
) : null}
</DetailSheet>
);
}
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 (
<>
<PageHeader
title="任务"
description="每次运行都是一条任务,不论定时、事件、手动还是 Agent 触发。"
actions={
<Button variant="outline" size="sm" onClick={() => qc.invalidateQueries({ queryKey: qk.all("engine") })}>
<RotateCw />
刷新
</Button>
}
/>
<Tabs
value={tab}
onValueChange={(v) =>
update((p) => {
p.set("tab", v);
if (v !== "all") p.delete("status");
})
}
>
<TabsList>
<TabsTrigger value="all">全部</TabsTrigger>
<TabsTrigger value="active">
进行中
<Count n={by ? (by.running ?? 0) + (by.pending ?? 0) : undefined} />
</TabsTrigger>
<TabsTrigger value="failed">
失败
<Count n={by?.failed} />
</TabsTrigger>
</TabsList>
</Tabs>
<div className="mt-4">
<TaskStream
tab={tab}
filters={filters}
setFilter={setFilter}
resetFilters={() =>
update((p) => {
for (const k of FILTER_KEYS) p.delete(k);
})
}
onOpen={(id) => update((p) => p.set("run", id))}
selectedId={runId}
/>
</div>
<RunSheet
runId={runId}
onOpen={(id) => update((p) => p.set("run", id))}
onClose={() => update((p) => p.delete("run"))}
/>
</>
);
}