Files
UHD_Media_Tool/apps/web/app/components/downloads/plan-row.tsx
T
rootandClaude Opus 5.5 fa76d7563c feat(控制台): 去掉待处理中心,问题回到发生处
- 侧栏与命令面板没有「待处理」;/console/inbox?kind= 按问题类型跳到整理记录「需要处理」、下载面板、运行状态或订阅
- 整理记录加「需要处理」筛选,行内写原因与推荐动作(指定作品 / 重新整理 / 让智能体识别 / 替换·保留两者·跳过 / 忽略),
  详情页同样可处理;处理按钮从原待处理页抽成 components/decisions/decision-actions.tsx 复用
- 铃铛只放等你批准的智能体操作与系统健康;首页「要我处理」与运行状态「需要关注」放汇总计数,运行状态加智能体用量
- 设置 → 智能体:预算卡改为「用量与限制」(只剩单次工具调用与时长,附花费统计),自动调查卡加「整理识别不到作品时叫智能体识别」
- 集数待确认的原因显示在作品页集数映射(可确认建议规则 / 智能体建议)与订阅详情;未识别资源在媒体库「未识别」里直接选候选或指定
- 下载、站点、任务里指向待处理的链接改到对应页面

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 16:47:25 +08:00

177 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { DownloadBoardPlanItem } from "@umt/contracts";
import { Check, X } from "lucide-react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useDecideApproval, useIsAdminSession } from "~/api/agent.ts";
import { useCancelPlan, usePlanWhy } from "~/api/downloads.ts";
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
import { DetailSection } from "~/components/console/detail-sheet.tsx";
import { JsonView } from "~/components/console/json-view.tsx";
import { KvList } from "~/components/console/kv-list.tsx";
import { consolePath } from "~/components/console/nav.ts";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { TechInfo } from "~/components/console/tech-info.tsx";
import { Button } from "~/components/ui/button.tsx";
import { errorMessage } from "~/lib/api.ts";
import { reasonCode } from "~/lib/reasons.ts";
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
import { PLAN_WAIT, PURPOSE_LABEL, planWaitHint, whyText } from "./plan-format.ts";
// DL-3:还没提交到下载器的下载(等批准 / 等磁盘空间 / 等开启自动化 / 准备提交)在下载面板里的状态、操作与详情;
// 已提交的下载在详情里显示「为什么下这个」。
export function PlanWaitBadge({ p, hint = false }: { p: DownloadBoardPlanItem; hint?: boolean }) {
const w = PLAN_WAIT[p.wait];
const h = hint ? planWaitHint(p) : null;
return (
<span className="flex max-w-56 flex-col items-start gap-0.5">
<StatusBadge tone={w.tone}>{w.label}</StatusBadge>
{h ? <span className="line-clamp-2 text-micro text-muted-foreground">{h}</span> : null}
</span>
);
}
/** 批准 / 取消:等批准的走审批(拒绝后服务端取消这次下载);其余直接取消。都要确认 */
export function PlanActions({ p, size = "sm" }: { p: DownloadBoardPlanItem; size?: "sm" | "xs" }) {
const decide = useDecideApproval();
const cancel = useCancelPlan();
const admin = useIsAdminSession();
const approval = p.approval;
const stop = (e: { stopPropagation: () => void }) => e.stopPropagation();
const decideApproval = (status: "approved" | "rejected") =>
decide
.mutateAsync({
id: approval?.id ?? "",
status,
payloadHash: approval?.payloadHash ?? "",
...(status === "rejected" ? { reason: "在下载面板取消" } : {}),
})
.then(
() => toast.success(status === "approved" ? "已批准,开始提交下载" : "已取消这次下载"),
(err) => {
toast.error(errorMessage(err));
throw err;
},
);
return (
// biome-ignore lint/a11y/noStaticElementInteractions: 只拦截行点击
<span className="inline-flex justify-end gap-1.5 whitespace-nowrap" onClick={stop} onKeyDown={stop}>
{approval ? (
<ConfirmDialog
trigger={
<Button size={size} disabled={!admin} title={admin ? undefined : "只能在控制台管理员登录时批准"}>
<Check />
批准
</Button>
}
title={`批准下载「${p.name}」?`}
description={`${p.sizeBytes !== null ? `${formatBytes(p.sizeBytes, 1)},` : ""}批准后立即提交到下载器。`}
confirmLabel="批准"
onConfirm={() => decideApproval("approved")}
/>
) : null}
<ConfirmDialog
trigger={
<Button
variant="outline"
size={size}
disabled={!!approval && !admin}
title={approval && !admin ? "只能在控制台管理员登录时处理" : undefined}
>
<X />
取消
</Button>
}
title={`取消下载「${p.name}」?`}
description={
approval ? "不批准这次下载,智能体会收到结果。" : "释放为它预留的空间和额度,收录版本之后会重新找资源。"
}
confirmLabel="取消下载"
cancelLabel="返回"
destructive
onConfirm={() =>
approval
? decideApproval("rejected")
: cancel.mutateAsync(p.id).then(
() => toast.success("已取消这次下载"),
(err) => {
toast.error(errorMessage(err));
throw err;
},
)
}
/>
</span>
);
}
/** 「为什么下这个」:来源 · 目的,下一行选中理由要点;原始评估进技术信息 */
export function WhySection({
planId,
extraTech,
}: {
planId: string;
extraTech?: { label: string; value: string | null }[];
}) {
const why = usePlanWhy(planId);
if (why.isPending || !why.data) return null;
const t = whyText(why.data);
return (
<DetailSection title="为什么下这个">
<p className="text-sm text-foreground">{t.head}</p>
{t.points ? <p className="mt-0.5 text-caption text-muted-foreground">{t.points}</p> : null}
<TechInfo
className="mt-2"
items={[
{ label: "计划 ID", value: planId, wide: true },
why.data.source.kind === "agent" ? { label: "会话 ID", value: why.data.source.runId, wide: true } : null,
why.data.replaces ? { label: "被替换的文件 ID", value: why.data.replaces.assetId, wide: true } : null,
...(extraTech ?? []).map((x) => ({ ...x, wide: true })),
]}
>
<JsonView label="评估依据" value={why.data.evaluation} defaultOpen={false} />
</TechInfo>
</DetailSection>
);
}
/** 还没提交的下载的详情(抽屉内容) */
export function PlanDetail({ p }: { p: DownloadBoardPlanItem }) {
const hint = planWaitHint(p);
return (
<div className="space-y-5">
<div className="flex flex-wrap items-center gap-2">
<StatusBadge tone={PLAN_WAIT[p.wait].tone}>{PLAN_WAIT[p.wait].label}</StatusBadge>
<div className="ml-auto">
<PlanActions p={p} />
</div>
</div>
{p.wait === "approval" ? (
<p className="text-caption text-muted-foreground">这次下载要你批准后才提交,铃铛里也有提醒。</p>
) : hint ? (
<p className="text-caption text-muted-foreground">{hint}。</p>
) : null}
<KvList
items={[
{
label: "作品",
value: p.media ? (
<Link to={consolePath("library", p.media.id)} className="hover:underline">
{p.media.title}
{p.media.year ? ` (${p.media.year})` : ""}
</Link>
) : null,
},
{ label: "大小", value: p.sizeBytes === null ? "未知" : formatBytes(p.sizeBytes, 2) },
{ label: "用途", value: PURPOSE_LABEL[p.purpose] },
{ label: "加入时间", value: formatDateTime(p.createdAt) },
]}
/>
<WhySection
planId={p.id}
extraTech={[{ label: "等待原因码", value: reasonCode(p.waitReason) ?? p.waitReason }]}
/>
</div>
);
}