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

+49 -13
View File
@@ -45,6 +45,7 @@ import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty
import { consolePath } from "~/components/console/nav.ts";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { TechInfo } from "~/components/console/tech-info.tsx";
import { DROPPED_STATUS, droppedReason, PURPOSE_LABEL } from "~/components/downloads/plan-format.ts";
import { evidenceLabel, FieldLockDialog, IdentityDialog, MergeDialog } from "~/components/library/entity-dialogs.tsx";
import {
CONTENT_TYPE,
@@ -88,7 +89,7 @@ import { SubscriptionFormDialog } from "~/components/subscriptions/subscription-
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { useUiPrefs } from "~/lib/ui-prefs.ts";
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
// 作品详情(REQ §13.1 / §13.4.3):实际路径、外部身份映射及状态、元数据来源、各 Slot、候选拒绝理由、
// 内容单元、下载历史与校验结果,同一页同时展示。
@@ -871,11 +872,7 @@ function DownloadsPanel({ detail }: { detail: LibraryDetail }) {
{d.downloadTask.name}
</Link>
<p className="text-meta text-muted-foreground">
{d.plan
? `${d.plan.purpose === "upgrade" ? "升级" : d.plan.purpose === "fill" ? "补缺" : "换源"}计划 · `
: d.downloadTask.origin === "adopted"
? "接管 · "
: ""}
{d.plan ? `${PURPOSE_LABEL[d.plan.purpose]} · ` : d.downloadTask.origin === "adopted" ? "接管 · " : ""}
{formatDateTime(d.downloadTask.createdAt)}
</p>
</div>
@@ -935,15 +932,54 @@ function DownloadsPanel({ detail }: { detail: LibraryDetail }) {
// 没有下载任务的整理记录(文件管理按路径手动整理等),下载任务的记录已在上表
const listed = new Set(detail.downloads.map((d) => d.downloadTask.id));
const manual = (detail.organizationRecords ?? []).filter((r) => !r.downloadTaskId || !listed.has(r.downloadTaskId));
// DL-3:没提交到下载器就结束的下载(失败 / 取消 / 过期),原因走原因码表,原码进技术信息
const dropped = detail.droppedPlans ?? [];
return (
<Panel title="下载历史与校验结果" bodyClassName="p-0 sm:p-0">
<DataTable
plain
columns={columns}
rows={detail.downloads}
rowKey={(d) => d.downloadTask.id}
empty={{ title: manual.length ? "没有下载任务,只有手动整理记录" : "还没有下载记录" }}
/>
{detail.downloads.length || !dropped.length ? (
<DataTable
plain
columns={columns}
rows={detail.downloads}
rowKey={(d) => d.downloadTask.id}
empty={{ title: manual.length ? "没有下载任务,只有手动整理记录" : "还没有下载记录" }}
/>
) : null}
{dropped.length ? (
<div className={cn("px-4 py-3 sm:px-5", detail.downloads.length ? "border-t border-border/60" : "")}>
<p className="mb-2 text-meta text-muted-foreground">没提交到下载器</p>
<ul className="space-y-2">
{dropped.map(({ plan, releaseTitle }) => {
const st = DROPPED_STATUS[plan.status as keyof typeof DROPPED_STATUS] ?? DROPPED_STATUS.failed;
const why = droppedReason(plan.statusReason);
return (
<li key={plan.id} className="min-w-0">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<StatusBadge tone={st.tone}>{st.label}</StatusBadge>
<Link
to={`${consolePath("downloads")}?plan=${plan.id}`}
className="min-w-0 max-w-full truncate font-mono text-caption text-foreground hover:underline sm:max-w-md"
title={releaseTitle ?? undefined}
>
{releaseTitle ?? "未知资源"}
</Link>
<span className="text-meta text-muted-foreground tabular-nums">
{PURPOSE_LABEL[plan.purpose]} · {formatDateTime(plan.updatedAt)}
</span>
</div>
{why ? <p className="mt-0.5 text-meta text-muted-foreground">{why}</p> : null}
<TechInfo
items={[
{ label: "计划 ID", value: plan.id, wide: true },
{ label: "原因码", value: plan.statusReason, wide: true },
]}
/>
</li>
);
})}
</ul>
</div>
) : null}
{manual.length ? (
<div className="border-t border-border/60 px-4 py-3 sm:px-5">
<p className="mb-2 text-meta text-muted-foreground">其他整理记录</p>