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:
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}
|
||||
|
||||
Reference in new issue
Block a user