feat(downloads): fold download plans into download panel (DL-3)

- 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]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 01:15:25 +08:00
1 parent c0522f2531
commit a9147e647d
50 files changed
+1535 -348

No files matched your search

@@ -23,13 +23,11 @@ import { OrganizeWizard } from "~/components/organize-wizard/organize-wizard.tsx
import { Button } from "~/components/ui/button.tsx";
import { relTime } from "~/components/workbench/release-fields.ts";
import { errorMessage } from "~/lib/api.ts";
import { planReasonText } from "~/lib/labels.ts";
import { humanizeReason, reasonCode, reasonLine } from "~/lib/reasons.ts";
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
import { liveItems } from "./board-format.ts";
import { BusinessTimeline } from "./business-timeline.tsx";
const PLAN_PURPOSE_LABEL: Record<string, string> = { fill: "补缺", upgrade: "升级", replace_source: "换源" };
import { WhySection } from "./plan-row.tsx";
/** 进度:细线 + 文字;总量未知时不伪造百分比 */
export function ProgressLine({ t }: { t: DownloadTask }) {
@@ -91,7 +89,7 @@ export function PauseResume({ t, size = "sm" }: { t: DownloadTask; size?: "sm" |
);
}
/** 下载任务详情:状态、阈值说明、业务时间线、文件、关联计划 */
/** 下载任务详情:状态、阈值说明、业务时间线、为什么下这个(DL-3)、文件 */
export function DownloadDetail({ task, live }: { task: DownloadTask; live?: DownloadLive | null }) {
const plan = usePlan(task.acquisitionPlanId);
const names = useDownloaderNames();
@@ -157,6 +155,20 @@ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: Down
</p>
) : null}
{task.acquisitionPlanId ? (
<WhySection
planId={task.acquisitionPlanId}
extraTech={[
{
label: "计划状态",
value: plan.data
? `${PLAN_STATUS_LABEL[plan.data.status]}${plan.data.statusReason ? ` · ${plan.data.statusReason}` : ""}`
: null,
},
]}
/>
) : null}
<DetailSection title="进度">
<ProgressLine t={live ? { ...task, progress: live.progress } : task} />
<KvList
@@ -194,28 +206,6 @@ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: Down
<BusinessTimeline events={trace.events} loading={trace.loading} />
</DetailSection>
{plan.data ? (
<DetailSection title="下载计划">
<KvList
items={[
{ label: "状态", value: PLAN_STATUS_LABEL[plan.data.status] },
{ label: "用途", value: PLAN_PURPOSE_LABEL[plan.data.purpose] ?? plan.data.purpose },
{ label: "预留空间", value: formatBytes(plan.data.reservedBytes, 1) },
{ label: "说明", value: planReasonText(plan.data.statusReason), wide: true },
]}
/>
<TechInfo
className="mt-2"
items={[
{ label: "计划 ID", value: plan.data.id, wide: true },
{ label: "说明原码", value: plan.data.statusReason },
]}
>
<JsonView label="评估依据" value={plan.data.evaluation} />
</TechInfo>
</DetailSection>
) : null}
{task.fileList?.length ? (
<DetailSection title={`文件(${task.fileList.length})`}>
<ul className="max-h-60 space-y-1 overflow-auto text-meta">