Files
UHD_Media_Tool/apps/web/app/routes/console/tasks.tsx
T
rootandClaude Opus 5.5 260f3ab27f feat(web): 首页只回答三件事、计划页巡检置顶与分组折叠、任务列表去噪(DEAI-2)
首页:顶部一行状态(自动化开关 + 只在异常时出现的服务 / 下载盘 / 站点账号 / 下载器问题),
下面只有「在忙什么」「要我处理」「最近入库」三块;去掉运行状态、计划、缓存、来源健康、
满足率与 0 值铺陈;0 订阅时写「还没有订阅。去搜索添加」。只读对账移出首页,设置里放进
自动化 →「高级」(与存储预算、流水保留、缓存有效期一起默认收起)。

计划页:「智能体巡检」移到最上面——巡检数量少、由用户自己建、每次会花钱,是这页最需要
看的;自动任务任务多、多数不用管,放在下面。自动任务按分类分组、每组可折叠:有失败或
运行中的组默认展开,其余收起为「N 个任务 · 下次 xx」一行,折叠状态记在 localStorage;
维护类整组叫「数据清理」,未接入调度的备份不列。巡检卡只留名称、开关、间隔、上次结果与
费用,目标 / 能力 / 预算 / 模型 / 最近会话收进「详情」。去掉顶部 6 个计数卡。定时任务说明
去掉表名、字段名与实现备注。

任务页:默认隐藏系统任务(无业务对象的引擎步骤、定时任务的成功运行),可一键显示;
标题不再用 ID;耗时移入技术信息;< 1 秒写「< 1 秒」;费用两位小数,不足一分写「< $0.01」。
下载盘相关文案改用「暂停线 / 恢复线」。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-29 20:48:35 +08:00

152 lines
4.7 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", "system"];
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="任务"
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"))}
/>
</>
);
}