文案(整理记录与队列):按 human-copy 重写队列面板、删除确认、整理来源选择
- 队列面板与按钮统一叫「队列」,阶段文字去掉「登记媒体库」「NFO / 图片」等重复说明 - 「选择整理策略」改「选择整理来源」(策略是收录策略的词),「来源配置」统一为「整理来源」 - 扫描存量结果只列非零项;失败 Toast 写清是哪个动作失败 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
32bf49a148
commit
a15d81360b
8 files changed
+74
-64
No files matched your search
@@ -86,7 +86,7 @@ export function DeleteRecordsDialog({
|
|||||||
<input type="checkbox" className="mt-1" checked={dst} onChange={(e) => setDst(e.target.checked)} />
|
<input type="checkbox" className="mt-1" checked={dst} onChange={(e) => setDst(e.target.checked)} />
|
||||||
<span>
|
<span>
|
||||||
同时删除媒体库里的文件
|
同时删除媒体库里的文件
|
||||||
<span className="block text-meta text-muted-foreground">整理后放进媒体库的文件,会从媒体库移除。</span>
|
<span className="block text-meta text-muted-foreground">整理到媒体库目录里的文件。</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,8 +127,8 @@ function Summary({
|
|||||||
src: boolean;
|
src: boolean;
|
||||||
withFiles: boolean;
|
withFiles: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (error) return <p className="text-destructive">{errorMessage(error, "核对失败")}</p>;
|
if (error) return <p className="text-destructive">{errorMessage(error, "检查失败")}</p>;
|
||||||
if (!plan) return <p className="text-muted-foreground">{pending ? "正在核对要删的文件…" : null}</p>;
|
if (!plan) return <p className="text-muted-foreground">{pending ? "检查要删的文件…" : null}</p>;
|
||||||
const files = [
|
const files = [
|
||||||
plan.sourceFiles ? `${plan.sourceFiles} 个源文件` : null,
|
plan.sourceFiles ? `${plan.sourceFiles} 个源文件` : null,
|
||||||
plan.targetFiles ? `${plan.targetFiles} 个媒体库文件` : null,
|
plan.targetFiles ? `${plan.targetFiles} 个媒体库文件` : null,
|
||||||
@@ -137,15 +137,15 @@ function Summary({
|
|||||||
<div className="space-y-1 text-muted-foreground">
|
<div className="space-y-1 text-muted-foreground">
|
||||||
{files.length ? (
|
{files.length ? (
|
||||||
<p>
|
<p>
|
||||||
{files.join("、")}({formatBytes(plan.bytes, 1)})会移入回收站,可以在回收站还原。
|
{files.join("、")}({formatBytes(plan.bytes, 1)})会移入回收站,可以还原。
|
||||||
{plan.assets ? `其中 ${plan.assets} 个是已整理文件,会从媒体库移除。` : ""}
|
{plan.assets ? `其中 ${plan.assets} 个是已整理文件,会从媒体库移除。` : ""}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p>{withFiles ? "没有找到还在的文件,只删记录。" : "只删记录,文件不动。"}</p>
|
<p>{withFiles ? "没有找到还在的文件,只删记录。" : "只删记录,文件不动。"}</p>
|
||||||
)}
|
)}
|
||||||
{src && plan.sourceFiles ? <p>下载器里的任务不会删除。</p> : null}
|
{src && plan.sourceFiles ? <p>下载器里的任务不受影响。</p> : null}
|
||||||
{plan.autoRetry ? (
|
{plan.autoRetry ? (
|
||||||
<p className="text-foreground">{plan.autoRetry} 个下载任务没有别的整理记录了,几分钟后会重新自动整理。</p>
|
<p className="text-foreground">{plan.autoRetry} 个下载任务没有别的整理记录了,几分钟后会自动重新整理。</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function FileSourceSelection({
|
|||||||
const profile = matches[0];
|
const profile = matches[0];
|
||||||
onSelect(
|
onSelect(
|
||||||
profile
|
profile
|
||||||
? { ...source, sourceConfigId: profile.id, defaultAction: profile.action, label: `来源 · ${profile.name}` }
|
? { ...source, sourceConfigId: profile.id, defaultAction: profile.action, label: `整理来源 · ${profile.name}` }
|
||||||
: source,
|
: source,
|
||||||
);
|
);
|
||||||
}, [profiles.isSuccess, matches, source, onSelect]);
|
}, [profiles.isSuccess, matches, source, onSelect]);
|
||||||
@@ -32,14 +32,14 @@ export function FileSourceSelection({
|
|||||||
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{profiles.isPending ? "读取整理来源…" : "选择整理策略"}</DialogTitle>
|
<DialogTitle>{profiles.isPending ? "读取整理来源…" : "选择整理来源"}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
匹配来源配置后,使用其整理方式、目标目录与分类规则。媒体类型将在整理预览时核对。
|
整理时沿用所选整理来源的整理方式、目标目录和分类。
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{profiles.isPending ? (
|
{profiles.isPending ? (
|
||||||
<p role="status" className="text-caption text-muted-foreground">
|
<p role="status" className="text-caption text-muted-foreground">
|
||||||
正在检查所选文件对应的来源配置…
|
匹配整理来源…
|
||||||
</p>
|
</p>
|
||||||
) : profiles.error ? (
|
) : profiles.error ? (
|
||||||
<>
|
<>
|
||||||
@@ -62,19 +62,19 @@ export function FileSourceSelection({
|
|||||||
...source,
|
...source,
|
||||||
sourceConfigId: profile.id,
|
sourceConfigId: profile.id,
|
||||||
defaultAction: profile.action,
|
defaultAction: profile.action,
|
||||||
label: `来源 · ${profile.name}`,
|
label: `整理来源 · ${profile.name}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{profile.name} · {profile.action === "copy" ? "复制" : "移动"} · {profile.category ?? "自动分类"}
|
{profile.name} · {profile.action === "copy" ? "复制" : "移动"} · {profile.category ?? "自动分类"}
|
||||||
{!profile.enabled ? " · 自动监控已停用" : ""}
|
{!profile.enabled ? " · 已停用" : ""}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!profiles.isPending && (profiles.error || matches.length > 1) ? (
|
{!profiles.isPending && (profiles.error || matches.length > 1) ? (
|
||||||
<Button variant="outline" onClick={() => onSelect(source)}>
|
<Button variant="outline" onClick={() => onSelect(source)}>
|
||||||
不使用来源配置,手动指定选项
|
不用整理来源,手动设置
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -30,15 +30,15 @@ function ScrapeQueueItem({ run }: { run: EngineRun }) {
|
|||||||
return (
|
return (
|
||||||
<li className="pg-slab space-y-2 rounded-xl p-4">
|
<li className="pg-slab space-y-2 rounded-xl p-4">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<strong className="min-w-0 flex-1 break-words text-body">{record?.match?.title ?? "媒体库刮削"}</strong>
|
<strong className="min-w-0 flex-1 break-words text-body">{record?.match?.title ?? "刮削"}</strong>
|
||||||
<StatusBadge>刮削</StatusBadge>
|
<StatusBadge>刮削</StatusBadge>
|
||||||
<StatusBadge tone={run.status === "sleeping" ? "warning" : "neutral"}>{status}</StatusBadge>
|
<StatusBadge tone={run.status === "sleeping" ? "warning" : "neutral"}>{status}</StatusBadge>
|
||||||
<span className="text-meta text-muted-foreground">{relTime(run.createdAt)}</span>
|
<span className="text-meta text-muted-foreground">{relTime(run.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 text-caption">
|
<div className="min-w-0 text-caption">
|
||||||
<span className="text-muted-foreground">目标目录 / 文件</span>
|
<span className="text-muted-foreground">位置</span>
|
||||||
<p className="break-all font-mono text-meta">
|
<p className="break-all font-mono text-meta">
|
||||||
{record ? `${record.root} / ${record.path || "(根目录)"}` : "读取刮削目标中…"}
|
{record ? `${record.root} / ${record.path || "(登记目录本身)"}` : "加载中…"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-meta text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-meta text-muted-foreground">
|
||||||
@@ -59,12 +59,12 @@ function ScrapeQueueItem({ run }: { run: EngineRun }) {
|
|||||||
className="hover:text-foreground hover:underline"
|
className="hover:text-foreground hover:underline"
|
||||||
to={`/console/organize/${record.organizationRecordId}`}
|
to={`/console/organize/${record.organizationRecordId}`}
|
||||||
>
|
>
|
||||||
关联整理记录
|
整理记录
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{detail.error || scrape.error ? (
|
{detail.error || scrape.error ? (
|
||||||
<p className="text-meta text-warning">刮削详细信息暂时无法读取,任务状态仍会自动刷新。</p>
|
<p className="text-meta text-warning">刮削详情读取失败,状态仍会自动刷新。</p>
|
||||||
) : null}
|
) : null}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -83,7 +83,7 @@ function QueueItem({ run }: { run: EngineRun }) {
|
|||||||
const download = useDownloadTask(downloadId);
|
const download = useDownloadTask(downloadId);
|
||||||
const record = recordQuery.data ?? matchingRecord;
|
const record = recordQuery.data ?? matchingRecord;
|
||||||
const progress = record ? recordFileProgress(record) : null;
|
const progress = record ? recordFileProgress(record) : null;
|
||||||
const title = record ? recordTitle(record) : (download.data?.name ?? run.business?.title ?? "核验整理任务");
|
const title = record ? recordTitle(record) : (download.data?.name ?? run.business?.title ?? "整理任务");
|
||||||
const source = record?.source.path ?? download.data?.contentPath ?? download.data?.savePath;
|
const source = record?.source.path ?? download.data?.contentPath ?? download.data?.savePath;
|
||||||
const status = run.status === "pending" ? "排队" : run.status === "sleeping" ? "等待" : "执行中";
|
const status = run.status === "pending" ? "排队" : run.status === "sleeping" ? "等待" : "执行中";
|
||||||
return (
|
return (
|
||||||
@@ -99,13 +99,13 @@ function QueueItem({ run }: { run: EngineRun }) {
|
|||||||
<p className="break-all font-mono text-meta">
|
<p className="break-all font-mono text-meta">
|
||||||
{source ??
|
{source ??
|
||||||
(detail.isFetching || download.isFetching || recordQuery.isFetching
|
(detail.isFetching || download.isFetching || recordQuery.isFetching
|
||||||
? "读取来源信息中…"
|
? "加载中…"
|
||||||
: "来源信息暂不可用")}
|
: "读取不到来源")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<span className="text-muted-foreground">目标</span>
|
<span className="text-muted-foreground">目标</span>
|
||||||
<p className="break-all font-mono text-meta">{record?.target.path ?? "核验与生成整理计划后确定"}</p>
|
<p className="break-all font-mono text-meta">{record?.target.path ?? "生成整理计划后确定"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-meta text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-meta text-muted-foreground">
|
||||||
@@ -131,7 +131,7 @@ function QueueItem({ run }: { run: EngineRun }) {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{detail.error || recordQuery.error || related.error || download.error ? (
|
{detail.error || recordQuery.error || related.error || download.error ? (
|
||||||
<p className="text-meta text-warning">部分详细信息暂时无法读取,任务状态仍会自动刷新。</p>
|
<p className="text-meta text-warning">部分详情读取失败,状态仍会自动刷新。</p>
|
||||||
) : null}
|
) : null}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -159,7 +159,7 @@ export function OrganizeQueue() {
|
|||||||
resetKey: "organize-queue",
|
resetKey: "organize-queue",
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<section aria-label="排队与执行中的整理任务" className="space-y-3">
|
<section aria-label="排队和执行中的任务" className="space-y-3">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button variant="outline" size="sm" disabled={runs.isFetching} onClick={() => void runs.refetch()}>
|
<Button variant="outline" size="sm" disabled={runs.isFetching} onClick={() => void runs.refetch()}>
|
||||||
<RefreshCw />
|
<RefreshCw />
|
||||||
@@ -171,7 +171,7 @@ export function OrganizeQueue() {
|
|||||||
) : runs.error && !rows ? (
|
) : runs.error && !rows ? (
|
||||||
<ErrorState error={runs.error} onRetry={() => runs.refetch()} />
|
<ErrorState error={runs.error} onRetry={() => runs.refetch()} />
|
||||||
) : !rows?.length ? (
|
) : !rows?.length ? (
|
||||||
<EmptyState icon={FileStack} title="没有排队或执行中的整理任务" />
|
<EmptyState icon={FileStack} title="没有排队或执行中的任务" />
|
||||||
) : (
|
) : (
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{paged.items?.map((run) =>
|
{paged.items?.map((run) =>
|
||||||
@@ -207,7 +207,7 @@ export function OrganizeQueueButton({ open, onOpenChange }: { open: boolean; onO
|
|||||||
size="sm"
|
size="sm"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-label={count ? `整理队列:${count} 个任务` : "整理队列"}
|
aria-label={count ? `队列:${count} 个任务` : "队列"}
|
||||||
onClick={() => onOpenChange(true)}
|
onClick={() => onOpenChange(true)}
|
||||||
>
|
>
|
||||||
{running ? <Loader2 className="animate-spin" /> : <ListOrdered />}
|
{running ? <Loader2 className="animate-spin" /> : <ListOrdered />}
|
||||||
@@ -217,9 +217,9 @@ export function OrganizeQueueButton({ open, onOpenChange }: { open: boolean; onO
|
|||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent side="right" className="w-full gap-0 sm:max-w-xl">
|
<SheetContent side="right" className="w-full gap-0 sm:max-w-xl">
|
||||||
<SheetHeader className="pr-12">
|
<SheetHeader className="pr-12">
|
||||||
<SheetTitle>整理队列</SheetTitle>
|
<SheetTitle>队列</SheetTitle>
|
||||||
<SheetDescription className="text-meta">
|
<SheetDescription className="text-meta">
|
||||||
下载核验、手动与自动整理、整理后的 NFO / 图片刮削在这里排队。整理完成后进入整理记录,刮削显示到任务结束。
|
排队和执行中的整理、刮削任务。整理完成后移到整理记录。
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-4">{open ? <OrganizeQueue /> : null}</div>
|
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-4">{open ? <OrganizeQueue /> : null}</div>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function OrganizeScrapeHistory({ recordId }: { recordId: string }) {
|
|||||||
) : query.error ? (
|
) : query.error ? (
|
||||||
<ErrorState error={query.error} onRetry={() => query.refetch()} />
|
<ErrorState error={query.error} onRetry={() => query.refetch()} />
|
||||||
) : query.data?.records.length ? (
|
) : query.data?.records.length ? (
|
||||||
<PaginatedList rows={query.data.records} label="关联刮削记录" resetKey={recordId} as="ul" className="space-y-3">
|
<PaginatedList rows={query.data.records} label="刮削记录" resetKey={recordId} as="ul" className="space-y-3">
|
||||||
{(record) => (
|
{(record) => (
|
||||||
<li key={record.id} className="space-y-1.5 rounded-xl border border-border p-3">
|
<li key={record.id} className="space-y-1.5 rounded-xl border border-border p-3">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
@@ -44,7 +44,7 @@ export function OrganizeScrapeHistory({ recordId }: { recordId: string }) {
|
|||||||
{record.match?.title ?? record.path} · {record.path || "/"}
|
{record.match?.title ?? record.path} · {record.path || "/"}
|
||||||
</p>
|
</p>
|
||||||
{record.status === "skipped" ? null : (
|
{record.status === "skipped" ? null : (
|
||||||
<p className="text-meta text-muted-foreground">{scrapeCounts(record) || "等待写入 NFO 与图片"}</p>
|
<p className="text-meta text-muted-foreground">{scrapeCounts(record) || "等待写入"}</p>
|
||||||
)}
|
)}
|
||||||
{record.reason ? (
|
{record.reason ? (
|
||||||
<p
|
<p
|
||||||
@@ -57,7 +57,7 @@ export function OrganizeScrapeHistory({ recordId }: { recordId: string }) {
|
|||||||
) : null}
|
) : null}
|
||||||
{unfinished(record.files).length ? (
|
{unfinished(record.files).length ? (
|
||||||
<details className="text-meta">
|
<details className="text-meta">
|
||||||
<summary className="cursor-pointer text-muted-foreground">失败或没写的文件</summary>
|
<summary className="cursor-pointer text-muted-foreground">失败或跳过的文件</summary>
|
||||||
<PaginatedList
|
<PaginatedList
|
||||||
rows={unfinished(record.files)}
|
rows={unfinished(record.files)}
|
||||||
label="刮削失败或跳过的文件"
|
label="刮削失败或跳过的文件"
|
||||||
@@ -90,7 +90,7 @@ export function OrganizeScrapeHistory({ recordId }: { recordId: string }) {
|
|||||||
)}
|
)}
|
||||||
</PaginatedList>
|
</PaginatedList>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-caption text-muted-foreground">没有关联的刮削记录。</p>
|
<p className="text-caption text-muted-foreground">没有刮削记录。</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,24 +8,22 @@ describe("organize queue", () => {
|
|||||||
recordId: undefined,
|
recordId: undefined,
|
||||||
});
|
});
|
||||||
expect(organizeQueueStage({ status: "pending" })).toBe("等待执行");
|
expect(organizeQueueStage({ status: "pending" })).toBe("等待执行");
|
||||||
expect(organizeQueueStage({ status: "running" })).toBe("核验文件与生成整理计划");
|
expect(organizeQueueStage({ status: "running" })).toBe("核验文件,生成整理计划");
|
||||||
});
|
});
|
||||||
it("distinguishes file transfer, plan waiting, and workflow waiting", () => {
|
it("distinguishes file transfer, plan waiting, and workflow waiting", () => {
|
||||||
expect(organizeRunInput({ recordId: "record" }).recordId).toBe("record");
|
expect(organizeRunInput({ recordId: "record" }).recordId).toBe("record");
|
||||||
expect(organizeQueueStage({ status: "running" }, { status: "queued" })).toContain("等待搬运");
|
expect(organizeQueueStage({ status: "running" }, { status: "queued" })).toContain("等待搬运");
|
||||||
expect(organizeQueueStage({ status: "running" }, { status: "running" })).toContain("搬运文件");
|
expect(organizeQueueStage({ status: "running" }, { status: "running" })).toContain("搬运文件");
|
||||||
expect(organizeQueueStage({ status: "sleeping" }, { status: "running" })).toBe("等待继续执行");
|
expect(organizeQueueStage({ status: "sleeping" }, { status: "running" })).toBe("等待继续");
|
||||||
});
|
});
|
||||||
it("keeps the independent scrape stage distinct after importing finishes", () => {
|
it("keeps the independent scrape stage distinct after importing finishes", () => {
|
||||||
expect(organizeRunInput({ input: { scrapeRecordId: "scrape" } }).scrapeRecordId).toBe("scrape");
|
expect(organizeRunInput({ input: { scrapeRecordId: "scrape" } }).scrapeRecordId).toBe("scrape");
|
||||||
expect(scrapeQueueStage({ status: "running" })).toBe("刮削 NFO / 图片");
|
expect(scrapeQueueStage({ status: "running" })).toBe("刮削中");
|
||||||
expect(scrapeQueueStage({ status: "pending" })).toContain("等待刮削");
|
expect(scrapeQueueStage({ status: "pending" })).toContain("等待刮削");
|
||||||
});
|
});
|
||||||
it("原地入库显示核验与登记,不显示搬运", () => {
|
it("原地整理不显示搬运", () => {
|
||||||
expect(organizeQueueStage({ status: "running" }, { status: "running", action: "in_place" })).toBe(
|
expect(organizeQueueStage({ status: "running" }, { status: "running", action: "in_place" })).toBe("原地整理中");
|
||||||
"核验原文件与登记媒体库",
|
expect(organizeQueueStage({ status: "running" }, { status: "queued", action: "in_place" })).toBe("等待原地整理");
|
||||||
);
|
|
||||||
expect(organizeQueueStage({ status: "running" }, { status: "queued", action: "in_place" })).toBe("等待原地登记");
|
|
||||||
});
|
});
|
||||||
it("does not describe a finished business record as transferring", () => {
|
it("does not describe a finished business record as transferring", () => {
|
||||||
expect(organizeQueueStage({ status: "running" }, { status: "succeeded" })).toContain("收尾");
|
expect(organizeQueueStage({ status: "running" }, { status: "succeeded" })).toContain("收尾");
|
||||||
|
|||||||
@@ -20,18 +20,18 @@ export function organizeQueueStage(
|
|||||||
run: Pick<EngineRun, "status">,
|
run: Pick<EngineRun, "status">,
|
||||||
record?: Pick<PublicOrganizationRecord, "status"> & Partial<Pick<PublicOrganizationRecord, "action">>,
|
record?: Pick<PublicOrganizationRecord, "status"> & Partial<Pick<PublicOrganizationRecord, "action">>,
|
||||||
) {
|
) {
|
||||||
if (run.status === "sleeping") return "等待继续执行";
|
if (run.status === "sleeping") return "等待继续";
|
||||||
if (run.status === "pending") return "等待执行";
|
if (run.status === "pending") return "等待执行";
|
||||||
if (!record) return "核验文件与生成整理计划";
|
if (!record) return "核验文件,生成整理计划";
|
||||||
if (record.action === "in_place" && (record.status === "queued" || record.status === "running"))
|
if (record.action === "in_place" && (record.status === "queued" || record.status === "running"))
|
||||||
return record.status === "queued" ? "等待原地登记" : "核验原文件与登记媒体库";
|
return record.status === "queued" ? "等待原地整理" : "原地整理中";
|
||||||
if (record.status === "queued") return "整理计划已生成,等待搬运";
|
if (record.status === "queued") return "等待搬运文件";
|
||||||
if (record.status === "running") return "搬运文件与登记媒体库";
|
if (record.status === "running") return "搬运文件中";
|
||||||
return "整理已结束,完成任务收尾";
|
return "收尾中";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function scrapeQueueStage(run: Pick<EngineRun, "status">) {
|
export function scrapeQueueStage(run: Pick<EngineRun, "status">) {
|
||||||
if (run.status === "pending") return "等待刮削 NFO / 图片";
|
if (run.status === "pending") return "等待刮削";
|
||||||
if (run.status === "sleeping") return "等待继续刮削 NFO / 图片";
|
if (run.status === "sleeping") return "等待继续刮削";
|
||||||
return "刮削 NFO / 图片";
|
return "刮削中";
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ type Rec = Pick<PublicOrganizationRecord, "id" | "action" | "status" | "attentio
|
|||||||
export function attentionLine(r: Rec): string | null {
|
export function attentionLine(r: Rec): string | null {
|
||||||
const a = r.attention;
|
const a = r.attention;
|
||||||
if (!a) return null;
|
if (!a) return null;
|
||||||
if (a.decision?.status === "investigating") return "智能体正在识别作品,认出来会自动重新整理";
|
if (a.decision?.status === "investigating") return "智能体正在识别作品,识别出来后自动重新整理";
|
||||||
if (a.topic === "identity") return "没识别出作品。指定作品,或让智能体识别";
|
if (a.topic === "identity") return "没识别出作品。指定作品,或让智能体识别";
|
||||||
return a.decision?.summary ? humanizeReason(a.decision.summary) : null;
|
return a.decision?.summary ? humanizeReason(a.decision.summary) : null;
|
||||||
}
|
}
|
||||||
@@ -96,8 +96,8 @@ export function RecordAttentionActions({
|
|||||||
disabled={identify.isPending}
|
disabled={identify.isPending}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
identify.mutateAsync(r.id).then(
|
identify.mutateAsync(r.id).then(
|
||||||
(x) => toast.success(x.queued ? "已让智能体识别,认出来会自动重新整理" : "智能体已经在识别了"),
|
(x) => toast.success(x.queued ? "已交给智能体识别" : "智能体正在识别"),
|
||||||
(err) => toast.error(errorMessage(err, "操作失败")),
|
(err) => toast.error(errorMessage(err, "交给智能体识别失败")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -112,14 +112,14 @@ export function RecordAttentionActions({
|
|||||||
忽略
|
忽略
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title="忽略这条记录的问题?"
|
title="忽略这个问题?"
|
||||||
description="不再算需要处理,文件和记录都不动;之后可以随时重新整理。"
|
description="这条记录不再算需要处理,文件和记录不变。之后仍可重新整理。"
|
||||||
confirmLabel="忽略"
|
confirmLabel="忽略"
|
||||||
onConfirm={() =>
|
onConfirm={() =>
|
||||||
dismiss.mutateAsync(r.id).then(
|
dismiss.mutateAsync(r.id).then(
|
||||||
() => toast.success("已忽略"),
|
() => toast.success("已忽略"),
|
||||||
(err) => {
|
(err) => {
|
||||||
toast.error(errorMessage(err, "操作失败"));
|
toast.error(errorMessage(err, "忽略失败"));
|
||||||
throw err;
|
throw err;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,9 +17,21 @@ export function stockWizardSource(profile: OrganizeSourceView, stock: OrganizeSt
|
|||||||
sourceConfigId: profile.id,
|
sourceConfigId: profile.id,
|
||||||
defaultAction: profile.action,
|
defaultAction: profile.action,
|
||||||
label: `整理存量 · ${profile.name}`,
|
label: `整理存量 · ${profile.name}`,
|
||||||
scanSummary: `找到 ${stock.items.length} 个文件,跳过 ${stock.skipped} 个,不可读取 ${stock.unavailable} 个${stock.truncated ? ";扫描已达到上限,仅显示本批结果" : ""}。本次扫描不改变自动整理基线,请先预览再执行。`,
|
scanSummary: `找到 ${stock.items.length} 个文件${stockNotes(stock)}。`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
/** 扫描结果的补充说明:只写非零项 */
|
||||||
|
function stockNotes(r: Pick<OrganizeStockResult, "skipped" | "unavailable" | "truncated">): string {
|
||||||
|
return [
|
||||||
|
r.skipped ? `跳过 ${r.skipped} 个` : "",
|
||||||
|
r.unavailable ? `${r.unavailable} 个读取失败` : "",
|
||||||
|
r.truncated ? "达到扫描上限,只列出这一批" : "",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((x) => `,${x}`)
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
export function SourceSelection({
|
export function SourceSelection({
|
||||||
onClose,
|
onClose,
|
||||||
onSelect,
|
onSelect,
|
||||||
@@ -36,7 +48,7 @@ export function SourceSelection({
|
|||||||
mode="source"
|
mode="source"
|
||||||
allowRoot={false}
|
allowRoot={false}
|
||||||
onClose={() => setManual(false)}
|
onClose={() => setManual(false)}
|
||||||
onSelect={(ref) => onSelect({ kind: "files", items: [ref], label: "手动选择来源目录" })}
|
onSelect={(ref) => onSelect({ kind: "files", items: [ref], label: "手动选择的目录" })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
@@ -44,19 +56,19 @@ export function SourceSelection({
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>选择整理来源</DialogTitle>
|
<DialogTitle>选择整理来源</DialogTitle>
|
||||||
<DialogDescription>扫描来源存量后,进入文件识别、预览与执行。扫描不会改变自动整理基线。</DialogDescription>
|
<DialogDescription>扫描来源目录里已有的文件,预览后再整理。扫描不改变自动整理的基线。</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="max-h-72 space-y-2 overflow-auto">
|
<div className="max-h-72 space-y-2 overflow-auto">
|
||||||
{sources.isPending ? <p>读取来源中…</p> : null}
|
{sources.isPending ? <p className="text-caption text-muted-foreground">加载中…</p> : null}
|
||||||
{sources.error ? (
|
{sources.error ? (
|
||||||
<p role="alert" className="text-destructive">
|
<p role="alert" className="text-destructive">
|
||||||
来源配置读取失败,仍可手动选择目录。
|
读取整理来源失败。可以手动选择目录
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{sources.data?.items.length ? (
|
{sources.data?.items.length ? (
|
||||||
<PaginatedList
|
<PaginatedList
|
||||||
rows={sources.data.items}
|
rows={sources.data.items}
|
||||||
label="整理来源配置"
|
label="整理来源"
|
||||||
resetKey="organize-source-selection"
|
resetKey="organize-source-selection"
|
||||||
className="space-y-2"
|
className="space-y-2"
|
||||||
>
|
>
|
||||||
@@ -74,7 +86,7 @@ export function SourceSelection({
|
|||||||
r.items.length
|
r.items.length
|
||||||
? onSelect(stockWizardSource(p, r))
|
? onSelect(stockWizardSource(p, r))
|
||||||
: toast.info(
|
: toast.info(
|
||||||
`没有可整理文件;跳过 ${r.skipped},不可读取 ${r.unavailable}${r.truncated ? ",扫描已达到上限" : ""}`,
|
`没有可整理的文件${stockNotes(r)}`,
|
||||||
),
|
),
|
||||||
onError: (e) => toast.error(filesErrorText(e, "扫描存量失败")),
|
onError: (e) => toast.error(filesErrorText(e, "扫描存量失败")),
|
||||||
},
|
},
|
||||||
@@ -89,7 +101,7 @@ export function SourceSelection({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" disabled={stock.isPending} onClick={() => setManual(true)}>
|
<Button variant="outline" disabled={stock.isPending} onClick={() => setManual(true)}>
|
||||||
手动选择其他来源目录
|
手动选择目录
|
||||||
</Button>
|
</Button>
|
||||||
{sources.isSuccess && !sources.data.items.length ? (
|
{sources.isSuccess && !sources.data.items.length ? (
|
||||||
<Link
|
<Link
|
||||||
@@ -97,7 +109,7 @@ export function SourceSelection({
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-caption text-muted-foreground underline"
|
className="text-caption text-muted-foreground underline"
|
||||||
>
|
>
|
||||||
去设置新增整理来源
|
新增整理来源
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
Reference in new issue
Block a user