From d7eb6a45d0cb54a5ed7f4d4ebb151f204e27a4b0 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 28 Sep 2026 15:22:15 +0800 Subject: [PATCH] fix(web): organize dialog blocks execute on unusable plan; feed table fits phones - manual organize: Execute is disabled when the preview has no organize plan or has blocking conflicts (it used to create a failed record); plan preview shows Chinese labels for operation/file action/kind and known failure reasons - feed table: name column narrower below sm so health and actions fit 390px Co-Authored-By: Claude Opus 5.5 --- .../components/downloads/organize-dialog.tsx | 31 +++++++++++++++---- .../app/components/workbench/feed-table.tsx | 2 +- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/apps/web/app/components/downloads/organize-dialog.tsx b/apps/web/app/components/downloads/organize-dialog.tsx index e42a623a..0f64aee4 100644 --- a/apps/web/app/components/downloads/organize-dialog.tsx +++ b/apps/web/app/components/downloads/organize-dialog.tsx @@ -21,10 +21,14 @@ import { Input } from "~/components/ui/input.tsx"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx"; import { Textarea } from "~/components/ui/textarea.tsx"; import { errorMessage } from "~/lib/api.ts"; +import { organizationActionLabel } from "~/lib/labels.ts"; import { cn, formatBytes } from "~/lib/utils.ts"; const AUTO = "__auto__"; +const FILE_ACTION: Record = { transfer: "入库", skip: "跳过", replace: "替换", hold: "暂缓" }; +const FILE_KIND: Record = { video: "视频", subtitle: "字幕", audio: "音轨", nfo: "NFO", other: "其它" }; + const EXCLUDE_REASON: Record = { sample: "样片", too_small: "体积过小", @@ -37,6 +41,20 @@ const EXCLUDE_REASON: Record = { naming_failed: "命名失败", }; +/** 预览里的计划能否执行:没生成整理计划或有阻塞冲突时不能执行(执行只会得到一条失败记录) */ +function usePlanExecutable(planId: string | null) { + const art = useArtifact(planId); + const parsed = art.data ? OrganizePlanPreview.safeParse(art.data.content) : null; + const org = parsed?.success ? parsed.data.plan?.organize : undefined; + return !!org && !org.conflicts.some((c) => c.blocking); +} + +const PLAN_FAIL_TEXT: Record = { + source_size_mismatch: "源文件大小与下载器记录不一致,可能还在写入;稍后重新预览", + source_missing: "源文件不存在,检查路径映射与挂载", + source_path_unknown: "找不到下载任务的保存路径或路径映射", +}; + function PlanPreview({ planId }: { planId: string }) { const art = useArtifact(planId); if (art.isPending) return ; @@ -55,14 +73,14 @@ function PlanPreview({ planId }: { planId: string }) { if (!org) return (

- 无法生成计划:{p.reason ?? p.status ?? "未知原因"} + 无法生成计划:{(p.reason && PLAN_FAIL_TEXT[p.reason]) ?? p.reason ?? p.status ?? "未知原因"}

); const blocking = org.conflicts.filter((c) => c.blocking); return (
- {org.operation} + {organizationActionLabel(org.operation)} {org.crossDevice ? 跨卷:复制 → 核验 → 删源 : null} {org.requiresApproval ? 需要审批 : null} {blocking.length ? 阻塞冲突 {blocking.length} : null} @@ -72,9 +90,9 @@ function PlanPreview({ planId }: { planId: string }) {
  • - {f.action} + {FILE_ACTION[f.action] ?? f.action} - {f.kind} + {FILE_KIND[f.kind] ?? f.kind} {formatBytes(f.size, 1)} {f.skipReason ? ({f.skipReason}) : null}
    @@ -131,6 +149,7 @@ export function OrganizeDialog({ const [errors, setErrors] = useState>({}); const [planId, setPlanId] = useState(null); const search = useEntitySearch(entity ? "" : entityQ); + const executable = usePlanExecutable(planId); const body = (dryRun: boolean) => { const sourcePaths = paths @@ -318,8 +337,8 @@ export function OrganizeDialog({