- Waiting plans (approval / disk space / automation off / queued) are
active-segment rows on /download-tasks/board, merged by created_at,
counted, with approve / cancel actions; ?plan= deep links and legacy
/downloads/plans routes redirect to the panel
- Download detail shows "why this download" (source, purpose, replaced
version, reasons); raw evaluation in tech info
- Failed / cancelled / expired plans appear in the entity download
history; rejected / revoked / expired approvals close draft plans
- New GET /acquisition-plans/{id}/why, POST /acquisition-plans/{id}/cancel
- User-facing copy no longer says 下载计划; glossary updated
Co-Authored-By: Claude Opus 5.5 <[email protected]>
68 lines
2.5 KiB
TypeScript
68 lines
2.5 KiB
TypeScript
import { ExternalLink } from "lucide-react";
|
|
import { Link } from "react-router";
|
|
import type { TimelineEvent, TimelineStage } from "~/api/tasks.ts";
|
|
import { LoadingState } from "~/components/console/empty-state.tsx";
|
|
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
|
import { Timeline } from "~/components/console/timeline.tsx";
|
|
|
|
// 业务时间线(参考 MovieClaw task-activity):把观察 → 计划 → 下载 → 整理 → Agent 串成一个业务过程,
|
|
// 阶段计数与首页/侧栏同口径;每条事件都有文字说明,不只靠颜色。
|
|
|
|
export const STAGE_LABEL: Record<TimelineStage, string> = {
|
|
observation: "发现",
|
|
plan: "准备",
|
|
download: "下载",
|
|
organization: "整理",
|
|
agent: "智能体",
|
|
engine: "任务",
|
|
};
|
|
|
|
const ORDER: TimelineStage[] = ["observation", "plan", "download", "organization", "agent", "engine"];
|
|
|
|
export function BusinessTimeline({ events, loading }: { events: TimelineEvent[]; loading?: boolean }) {
|
|
const counts = new Map<TimelineStage, number>();
|
|
for (const e of events) counts.set(e.stage, (counts.get(e.stage) ?? 0) + 1);
|
|
const ongoing = events.filter((e) => e.ongoing);
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{ORDER.filter((s) => counts.get(s)).map((s) => {
|
|
const bad = events.some((e) => e.stage === s && e.tone === "destructive");
|
|
return (
|
|
<StatusBadge key={s} tone={bad ? "destructive" : "neutral"}>
|
|
{STAGE_LABEL[s]} · {counts.get(s)}
|
|
</StatusBadge>
|
|
);
|
|
})}
|
|
</div>
|
|
{ongoing.length ? (
|
|
<p className="text-caption text-muted-foreground">当前:{ongoing.map((e) => e.title).join(";")}</p>
|
|
) : null}
|
|
{loading && !events.length ? (
|
|
<LoadingState rows={3} />
|
|
) : (
|
|
<Timeline
|
|
items={events.map((e) => ({
|
|
id: e.key,
|
|
at: e.at,
|
|
tone: e.tone,
|
|
meta: STAGE_LABEL[e.stage],
|
|
title: e.href ? (
|
|
<Link to={e.href} className="inline-flex items-center gap-1 underline-offset-4 hover:underline">
|
|
{e.title}
|
|
<ExternalLink className="h-3 w-3" />
|
|
</Link>
|
|
) : (
|
|
e.title
|
|
),
|
|
description: e.detail ?? undefined,
|
|
}))}
|
|
/>
|
|
)}
|
|
{loading && events.length ? (
|
|
<p className="text-meta text-muted-foreground">正在加载关联的整理记录与引擎运行…</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|