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 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 15:22:15 +08:00
1 parent 0c0907ad4c
commit d7eb6a45d0
2 files changed
+26 -7

No files matched your search

@@ -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<string, string> = { transfer: "入库", skip: "跳过", replace: "替换", hold: "暂缓" };
const FILE_KIND: Record<string, string> = { video: "视频", subtitle: "字幕", audio: "音轨", nfo: "NFO", other: "其它" };
const EXCLUDE_REASON: Record<string, string> = {
sample: "样片",
too_small: "体积过小",
@@ -37,6 +41,20 @@ const EXCLUDE_REASON: Record<string, string> = {
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<string, string> = {
source_size_mismatch: "源文件大小与下载器记录不一致,可能还在写入;稍后重新预览",
source_missing: "源文件不存在,检查路径映射与挂载",
source_path_unknown: "找不到下载任务的保存路径或路径映射",
};
function PlanPreview({ planId }: { planId: string }) {
const art = useArtifact(planId);
if (art.isPending) return <LoadingState rows={3} />;
@@ -55,14 +73,14 @@ function PlanPreview({ planId }: { planId: string }) {
if (!org)
return (
<p className="rounded-lg border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning">
无法生成计划:{p.reason ?? p.status ?? "未知原因"}
无法生成计划:{(p.reason && PLAN_FAIL_TEXT[p.reason]) ?? p.reason ?? p.status ?? "未知原因"}
</p>
);
const blocking = org.conflicts.filter((c) => c.blocking);
return (
<div className="space-y-2">
<div className="flex flex-wrap gap-1.5">
<StatusBadge>{org.operation}</StatusBadge>
<StatusBadge>{organizationActionLabel(org.operation)}</StatusBadge>
{org.crossDevice ? <StatusBadge tone="warning">跨卷:复制 → 核验 → 删源</StatusBadge> : null}
{org.requiresApproval ? <StatusBadge tone="warning">需要审批</StatusBadge> : null}
{blocking.length ? <StatusBadge tone="destructive">阻塞冲突 {blocking.length}</StatusBadge> : null}
@@ -72,9 +90,9 @@ function PlanPreview({ planId }: { planId: string }) {
<li key={f.src} className="rounded-lg border border-border/60 px-2.5 py-1.5">
<div className="flex flex-wrap items-center gap-1.5">
<StatusBadge tone={f.action === "transfer" ? "success" : f.action === "skip" ? "neutral" : "warning"}>
{f.action}
{FILE_ACTION[f.action] ?? f.action}
</StatusBadge>
<span className="text-muted-foreground">{f.kind}</span>
<span className="text-muted-foreground">{FILE_KIND[f.kind] ?? f.kind}</span>
<span className="text-muted-foreground">{formatBytes(f.size, 1)}</span>
{f.skipReason ? <span className="text-muted-foreground">({f.skipReason})</span> : null}
</div>
@@ -131,6 +149,7 @@ export function OrganizeDialog({
const [errors, setErrors] = useState<Record<string, string>>({});
const [planId, setPlanId] = useState<string | null>(null);
const search = useEntitySearch(entity ? "" : entityQ);
const executable = usePlanExecutable(planId);
const body = (dryRun: boolean) => {
const sourcePaths = paths
@@ -318,8 +337,8 @@ export function OrganizeDialog({
</Button>
<Button
onClick={() => run(false)}
disabled={organize.isPending || !planId}
title={planId ? undefined : "先预览计划"}
disabled={organize.isPending || !planId || !executable}
title={!planId ? "先预览计划" : executable ? undefined : "计划不可执行"}
>
<Play />
执行
@@ -112,7 +112,7 @@ export function FeedTable({
key: "name",
header: "名称",
cell: (f) => (
<div className="min-w-0 max-w-xs">
<div className="min-w-0 max-w-32 sm:max-w-xs">
<p className="truncate font-medium text-foreground">{f.name}</p>
<p className="truncate font-mono text-meta text-muted-foreground" title={f.urlDisplay}>
{f.urlDisplay}