文案(整理向导):按 human-copy 重写整理对话框、预览与作品选择
- 「交给待处理」改「标为需要处理」,「转移后的路径」改「目标目录」,「刷新作品资料」改「刷新元数据」(术语表) - 「来源配置」统一为「整理来源」;按钮与队列入口统一叫「队列」 - 去掉「请…」「本次 / 本批」等套话;只列非零计数;预览原因的括号与分隔符改全角 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
a15d81360b
commit
804f40cd36
8 files changed
+70
-70
No files matched your search
@@ -177,7 +177,7 @@ export function EntityPicker({
|
||||
) : null}
|
||||
{remote.length ? (
|
||||
<>
|
||||
<SectionTitle>TMDB 候选 · 选中后执行时新建作品</SectionTitle>
|
||||
<SectionTitle>TMDB 候选 · 选中后整理时新建作品</SectionTitle>
|
||||
<ul className="space-y-0.5">
|
||||
{remote.map(({ c, e }) => (
|
||||
<Option
|
||||
@@ -269,7 +269,7 @@ export function EntityPicker({
|
||||
</ul>
|
||||
{!hits.local.length && !hits.tmdb.length && !search.isFetching ? (
|
||||
<p className="px-2 py-1.5 text-meta text-muted-foreground">
|
||||
没有匹配的作品{hits.tmdbConfigured ? "" : ";TMDB 未配置,只搜了本地"}
|
||||
没有匹配的作品{hits.tmdbConfigured ? "" : "。TMDB 未配置,只搜了本地作品"}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -130,7 +130,7 @@ describe("识别结果 → 行", () => {
|
||||
|
||||
it("只允许单集或连续集号,不让人工覆写被后端忽略", () => {
|
||||
const r = { ...(rowsFromIdentify([item("show/a.mkv")], new Set())[0] as WizardRow), episodeTouched: true };
|
||||
expect(rowProblem({ ...r, episodes: "3,5" })).toBe("集号需要连续,如 3-5");
|
||||
expect(rowProblem({ ...r, episodes: "3,5" })).toBe("集号必须连续,例如 3-5");
|
||||
expect(wizardStats([{ ...r, episodes: "3,5" }]).pending).toBe(1);
|
||||
expect(rowProblem({ ...r, episodes: "4,3,4" })).toBeNull();
|
||||
expect(rowProblem({ ...r, episodes: "3-5" })).toBeNull();
|
||||
|
||||
@@ -63,12 +63,12 @@ export function rowProblem(r: WizardRow): string | null {
|
||||
if (r.excluded) return null;
|
||||
if (!r.entity) return "未选择作品";
|
||||
if (isMovieKind(r.entity.kind)) return null;
|
||||
if (!r.episodeTouched) return episodeUnresolved(r) ? "集数待确认,请填写季集或排除" : null;
|
||||
if (!r.episodeTouched) return episodeUnresolved(r) ? "集数待确认,填写季集或排除" : null;
|
||||
const eps = parseEpisodes(r.episodes);
|
||||
if (eps === null) return "集号格式不对,请填写单集或连续区间,如 3、3-5";
|
||||
if (eps === null) return "集号格式不对,填单集或连续区间,例如 3 或 3-5";
|
||||
const unique = [...new Set(eps)].sort((a, b) => a - b);
|
||||
if (eps.length > 50) return "单个文件最多指定 50 个集号";
|
||||
if (unique.some((n, i) => i > 0 && n !== unique[i - 1]! + 1)) return "集号需要连续,如 3-5";
|
||||
if (unique.some((n, i) => i > 0 && n !== unique[i - 1]! + 1)) return "集号必须连续,例如 3-5";
|
||||
if (r.season.trim() && !/^\d{1,4}$/.test(r.season.trim())) return "季号必须是数字";
|
||||
if (!r.season.trim()) return "剧集需要季号";
|
||||
return null;
|
||||
@@ -439,12 +439,12 @@ export const EXCLUDE_REASON: Record<string, string> = {
|
||||
extra_content: "额外内容",
|
||||
no_target_dir: "没有目标目录",
|
||||
naming_failed: "命名失败",
|
||||
title_mismatch: "标题不是本作品",
|
||||
title_mismatch: "标题与作品不符",
|
||||
};
|
||||
|
||||
export const SKIP_REASON: Record<string, string> = {
|
||||
successful_record: "已有成功整理记录",
|
||||
already_in_library: "已在库中",
|
||||
already_in_library: "已在媒体库",
|
||||
target_exists: "目标已存在",
|
||||
main_not_transferred: "主文件未整理",
|
||||
duplicate_target: "改名后与其他文件同名",
|
||||
|
||||
@@ -139,7 +139,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
const remote = useRef({ waiting: new Set<string>(), early: new Map<string, IdentifyCompletedPayload>() });
|
||||
const applyRemote = useCallback((p: IdentifyCompletedPayload) => {
|
||||
if (p.status === "succeeded") setRows((rs) => mergeIdentified(rs, p.items));
|
||||
else toast.error(`远端候选查询失败${p.error ? `:${p.error.slice(0, 120)}` : ""}`);
|
||||
else toast.error(`TMDB 查询失败,只显示本地结果${p.error ? `:${p.error.slice(0, 120)}` : ""}`);
|
||||
}, []);
|
||||
const expectRemote = useCallback(
|
||||
(taskId: string | null | undefined) => {
|
||||
@@ -251,7 +251,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
setRows((rs) => mergeIdentified(rs, r.items));
|
||||
expectRemote(r.remoteTaskId);
|
||||
},
|
||||
onError: (e) => toast.error(filesErrorText(e, "补充识别失败")),
|
||||
onError: (e) => toast.error(filesErrorText(e, "重新识别失败")),
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -282,9 +282,9 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
year: m.year,
|
||||
kind: m.kind,
|
||||
});
|
||||
toast.success(`已定位 ${m.title}`);
|
||||
toast.success(`已找到「${m.title}」`);
|
||||
} catch (e) {
|
||||
if (mounted.current && sequence === lookupSequence.current) toast.error(filesErrorText(e, "TMDB 定位失败"));
|
||||
if (mounted.current && sequence === lookupSequence.current) toast.error(filesErrorText(e, "按 TMDB ID 查找失败"));
|
||||
} finally {
|
||||
if (mounted.current && sequence === lookupSequence.current) setLookupPending(false);
|
||||
}
|
||||
@@ -323,7 +323,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
{
|
||||
onSuccess: (r) => {
|
||||
if (signatureRef.current !== snapshot) {
|
||||
setNotice("选项或识别结果已变化,请重新生成预览。");
|
||||
setNotice("选项或识别结果有变化,重新生成预览");
|
||||
return;
|
||||
}
|
||||
setPreview({ groups: r.groups, rows: effectiveRows, key, signature: snapshot });
|
||||
@@ -355,9 +355,9 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
onSuccess: (r) => {
|
||||
const failed = r.groups.filter((g) => !!g.error);
|
||||
const accepted = r.groups.filter((g) => !!g.recordId && !g.error).length;
|
||||
if (accepted) toast.success(`已加入整理队列:${accepted} 组`);
|
||||
if (accepted) toast.success(`${accepted} 组已加入队列`);
|
||||
if (failed.length) {
|
||||
setNotice(`${accepted} 组已受理,${failed.length} 组未加入队列;请检查下方原因,修改后重新预览。`);
|
||||
setNotice(`${accepted} 组已加入队列,${failed.length} 组没有加入。修改后重新生成预览`);
|
||||
const acceptedSources = new Set(
|
||||
r.groups.filter((g) => !!g.recordId && !g.error).flatMap((g) => g.files.map((f) => f.source)),
|
||||
);
|
||||
@@ -368,7 +368,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
}),
|
||||
);
|
||||
setPreview(null);
|
||||
toast.error(failed[0]?.error?.message ?? "部分任务未能加入队列");
|
||||
toast.error(failed[0]?.error?.message ?? `${failed.length} 组没有加入队列`);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
@@ -376,7 +376,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
onError: (e) => {
|
||||
if (filesErrorReason(e) === "plan_stale") {
|
||||
setPreview(null);
|
||||
setNotice("源文件已变化,请重新生成预览。");
|
||||
setNotice("源文件有变化,重新生成预览");
|
||||
}
|
||||
toast.error(filesErrorText(e, "加入队列失败"));
|
||||
},
|
||||
@@ -392,14 +392,14 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
const columns: DataColumn<WizardRow>[] = [
|
||||
{
|
||||
key: "file",
|
||||
header: "本次文件",
|
||||
header: "文件",
|
||||
cell: (r) => (
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-mono text-caption" title={r.path}>
|
||||
{baseName(r.path)}
|
||||
</p>
|
||||
<p className="truncate text-meta text-muted-foreground">
|
||||
{r.entity ? entityLabel(r.entity) : "待识别 / 手动指定作品"}
|
||||
{r.entity ? entityLabel(r.entity) : "未识别"}
|
||||
</p>
|
||||
{rowProblem(r) ? <p className="text-meta text-warning">{rowProblem(r)}</p> : null}
|
||||
</div>
|
||||
@@ -448,7 +448,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
header: "范围",
|
||||
cell: (r) =>
|
||||
r.excluded === "successful_record" ? (
|
||||
<StatusBadge>成功历史 · 跳过</StatusBadge>
|
||||
<StatusBadge>整理过 · 跳过</StatusBadge>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -475,7 +475,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
>
|
||||
<DialogHeader className="shrink-0 border-b border-border px-5 py-4 pr-14">
|
||||
<DialogTitle>整理</DialogTitle>
|
||||
<DialogDescription className="break-all">{src.label} · 本次设置作用于整个所选范围</DialogDescription>
|
||||
<DialogDescription className="break-all">{src.label} · 设置对所选的全部文件生效</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -488,12 +488,12 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
<p className="text-meta text-muted-foreground">{source.scanSummary}</p>
|
||||
) : null}
|
||||
<FormSelect
|
||||
label={inPlace ? "整理位置" : "来源配置"}
|
||||
label={inPlace ? "整理位置" : "整理来源"}
|
||||
value={inPlace ? "none" : (profile?.id ?? (ambiguous ? "choose" : "none"))}
|
||||
onChange={pickProfile}
|
||||
choices={[
|
||||
...(ambiguous ? [{ value: "choose", label: "请选择匹配的来源配置", disabled: true }] : []),
|
||||
{ value: "none", label: inPlace ? "原地 · 使用文件所在媒体库" : "手动指定 · 使用全局默认" },
|
||||
...(ambiguous ? [{ value: "choose", label: "选择整理来源", disabled: true }] : []),
|
||||
{ value: "none", label: inPlace ? "原地整理 · 文件所在的媒体库目录" : "不用整理来源 · 按全局设置" },
|
||||
...matches.map((p) => ({
|
||||
value: p.id,
|
||||
label: `${p.name} · ${p.action === "copy" ? "复制" : "移动"} · ${p.category ?? "自动分类"}`,
|
||||
@@ -512,7 +512,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
) : null}
|
||||
{profiles.error ? (
|
||||
<p className="text-meta text-warning">
|
||||
来源配置读取失败,可手动指定选项。
|
||||
读取整理来源失败,可以手动设置。
|
||||
<button type="button" className="ml-2 underline" onClick={() => void profiles.refetch()}>
|
||||
重试
|
||||
</button>
|
||||
@@ -524,7 +524,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
to="/console/settings#library/sources"
|
||||
onClick={onClose}
|
||||
>
|
||||
去设置新增整理来源
|
||||
新增整理来源
|
||||
</Link>
|
||||
) : null}
|
||||
<fieldset disabled={exec.isPending} className="space-y-4">
|
||||
@@ -551,7 +551,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
setRows((rs) => mergeIdentified(rs, r.items));
|
||||
expectRemote(r.remoteTaskId);
|
||||
},
|
||||
onError: (e) => toast.error(filesErrorText(e, "按媒体类型识别失败,可手动指定作品")),
|
||||
onError: (e) => toast.error(filesErrorText(e, "按媒体类型重新识别失败。可以手动指定作品")),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -572,7 +572,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
cancelLookup();
|
||||
pickEntity(e);
|
||||
}}
|
||||
placeholder="自动识别 · 或搜索 TMDB 选择作品"
|
||||
placeholder="自动识别,或搜索作品"
|
||||
className="h-9"
|
||||
/>
|
||||
</div>
|
||||
@@ -590,14 +590,14 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
setTmdbId(e.target.value);
|
||||
}}
|
||||
inputMode="numeric"
|
||||
placeholder={mediaType === "auto" ? "先选择电影或剧集" : "输入 TMDB ID"}
|
||||
placeholder={mediaType === "auto" ? "先选择电影或剧集" : "例如:693134"}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={mediaType === "auto" || !/^\d{1,12}$/.test(tmdbId.trim()) || lookupPending}
|
||||
onClick={() => void lookup()}
|
||||
>
|
||||
{lookupPending ? <LoaderCircle className="animate-spin" /> : <Search />}定位
|
||||
{lookupPending ? <LoaderCircle className="animate-spin" /> : <Search />}查找
|
||||
</Button>
|
||||
</div>
|
||||
</label>
|
||||
@@ -610,14 +610,14 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
value={season}
|
||||
disabled={mediaType === "movie"}
|
||||
onChange={(e) => setSeason(e.target.value)}
|
||||
placeholder="自动解析"
|
||||
placeholder="自动"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{manual ? (
|
||||
<p className="text-meta text-muted-foreground">
|
||||
本批统一使用:{entityLabel(manual)}
|
||||
{manual.external ? ` · TMDB ${manual.external.externalId}` : ""}。清除作品选择可恢复逐文件自动识别。
|
||||
全部文件按 {entityLabel(manual)}
|
||||
{manual.external ? ` · TMDB ${manual.external.externalId}` : ""} 整理。清除作品后恢复逐个识别。
|
||||
</p>
|
||||
) : null}
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
@@ -638,7 +638,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
/>
|
||||
{!inPlace ? (
|
||||
<FormSelect
|
||||
label="转移后的路径"
|
||||
label="目标目录"
|
||||
value={targetValue}
|
||||
onChange={(v) => {
|
||||
if (v === "dir") setTargetPicker(true);
|
||||
@@ -647,12 +647,12 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
choices={[
|
||||
{ value: "auto", label: profile ? `自动 · ${profile.name}` : "自动 · 按作品类型与已有目录" },
|
||||
...libRoots.map((r) => ({ value: r.id, label: r.name })),
|
||||
{ value: "dir", label: "手动选择目标目录…" },
|
||||
{ value: "dir", label: "选择目录…" },
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<p className="self-end rounded-xl border border-border p-3 text-meta text-muted-foreground">
|
||||
在文件所在媒体库登记,不搬运、不改名;可选在原目录写入 NFO 和图片。
|
||||
文件留在原位置,不搬运、不改名,只登记到媒体库。可以在原目录写 NFO 和图片。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -667,7 +667,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
</Button>
|
||||
) : null}
|
||||
{hnrCount && !inPlace ? (
|
||||
<p className="text-meta text-warning">{hnrCount} 个文件仍有做种义务,本次使用复制。</p>
|
||||
<p className="text-meta text-warning">{hnrCount} 个文件还要做种,只能复制。</p>
|
||||
) : null}
|
||||
{!inPlace ? (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
@@ -676,8 +676,8 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
value={options.onExisting}
|
||||
onChange={(v) => set({ onExisting: v as WizardOptions["onExisting"] })}
|
||||
choices={[
|
||||
{ value: "conflict", label: "交给待处理" },
|
||||
{ value: "skip", label: "跳过已存在的文件" },
|
||||
{ value: "conflict", label: "标为需要处理" },
|
||||
{ value: "skip", label: "跳过" },
|
||||
{ value: "replace_with_verify", label: "核验新文件后替换" },
|
||||
]}
|
||||
/>
|
||||
@@ -712,7 +712,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
<span className="text-caption">
|
||||
整理后刮削
|
||||
<span className="block text-meta text-muted-foreground">
|
||||
成功整理的文件进入刮削任务,生成 NFO 和封面
|
||||
整理完成后写 NFO 和图片
|
||||
</span>
|
||||
</span>
|
||||
<Switch
|
||||
@@ -725,15 +725,15 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
{!isHistory && successfulCount > 0 ? (
|
||||
<div className="flex items-center justify-between gap-3 rounded-xl border border-border px-3 py-2">
|
||||
<span className="text-caption">
|
||||
跳过成功记录
|
||||
跳过整理过的文件
|
||||
<span className="block text-meta text-muted-foreground">
|
||||
{successfulCount} 个文件有成功历史,仅对本次生效
|
||||
{successfulCount} 个文件以前整理成功过。只对这次生效
|
||||
</span>
|
||||
</span>
|
||||
<Switch
|
||||
checked={options.skipSuccessfulRecords}
|
||||
onCheckedChange={(v) => set({ skipSuccessfulRecords: v })}
|
||||
aria-label="跳过成功记录"
|
||||
aria-label="跳过整理过的文件"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -755,19 +755,19 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
</label>
|
||||
{!minSizeValid ? (
|
||||
<p role="alert" className="text-meta text-destructive">
|
||||
最小体积需要填写有效的非负数。
|
||||
填一个不小于 0 的数
|
||||
</p>
|
||||
) : null}
|
||||
<div className="flex items-center justify-between gap-3 text-caption">
|
||||
刷新作品资料
|
||||
刷新元数据
|
||||
<Switch
|
||||
checked={options.refreshMetadata}
|
||||
onCheckedChange={(v) => set({ refreshMetadata: v })}
|
||||
aria-label="刷新作品资料"
|
||||
aria-label="刷新元数据"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-meta text-muted-foreground">
|
||||
同名字幕、外挂音轨随视频整理;NFO 与图片由刮削生成。
|
||||
同名字幕和外挂音轨随视频一起整理。NFO 和图片由刮削生成。
|
||||
<Link to="/console/settings#library/naming" className="underline" onClick={onClose}>
|
||||
修改命名模板
|
||||
</Link>
|
||||
@@ -778,13 +778,13 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
{loading ? (
|
||||
<p role="status" className="flex items-center gap-2 text-caption text-muted-foreground">
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
定位源文件并识别作品…
|
||||
读取文件并识别作品…
|
||||
</p>
|
||||
) : src.error || (!src.items.length && !src.pending) ? (
|
||||
<p role="alert" className="text-caption text-destructive">
|
||||
{src.error
|
||||
? filesErrorText(src.error, "读取来源失败")
|
||||
: "源文件不存在或不在登记目录中,请在文件管理器重新选择。"}
|
||||
: "源文件不存在或不在登记目录里。回到文件管理重新选择"}
|
||||
</p>
|
||||
) : identify.error ? (
|
||||
<div>
|
||||
@@ -797,19 +797,19 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<p className="text-caption">
|
||||
本次整理 {active.length} 个视频 · 跳过 {stats.excluded} 个
|
||||
整理 {active.length} 个视频{stats.excluded ? ` · 跳过 ${stats.excluded} 个` : ""}
|
||||
{stats.pending ? ` · ${stats.pending} 个待确认` : ""}
|
||||
</p>
|
||||
{!rows.length && started ? (
|
||||
<p className="text-meta text-muted-foreground">所选范围没有视频文件。</p>
|
||||
) : null}
|
||||
<Button variant="ghost" size="sm" onClick={() => setFilesOpen((v) => !v)}>
|
||||
{filesOpen ? "收起文件明细" : "查看文件明细与季集"}
|
||||
{filesOpen ? "收起文件列表" : "查看文件和季集"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{truncated ? (
|
||||
<p className="text-meta text-warning">本批达到 200 个视频上限,请进入子目录分批整理。</p>
|
||||
<p className="text-meta text-warning">一次最多 200 个视频,超出的没有列出。进入子目录分批整理。</p>
|
||||
) : null}
|
||||
{src.unresolved.length ? (
|
||||
<p className="break-all text-meta text-warning">以下路径不可用:{src.unresolved.join("、")}</p>
|
||||
@@ -835,7 +835,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
{dry.isPending ? (
|
||||
<p role="status" className="flex gap-2 text-caption text-muted-foreground">
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
生成整理计划…
|
||||
生成预览…
|
||||
</p>
|
||||
) : preview ? (
|
||||
<>
|
||||
@@ -848,7 +848,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
</>
|
||||
) : (
|
||||
<p className="text-caption text-muted-foreground">
|
||||
{dry.error ? filesErrorText(dry.error, "预览失败") : "请生成预览,查看最终路径和处理结果。"}
|
||||
{dry.error ? filesErrorText(dry.error, "预览失败") : "生成预览后在这里查看目标路径和处理方式。"}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
@@ -865,7 +865,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
}}
|
||||
>
|
||||
<FileStack />
|
||||
整理队列
|
||||
队列
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" onClick={runPreview} disabled={!canPreview || dry.isPending || exec.isPending}>
|
||||
@@ -884,7 +884,7 @@ export function OrganizeWizard({ source, onClose }: { source: OrganizeSource; on
|
||||
}
|
||||
>
|
||||
{exec.isPending ? <LoaderCircle className="animate-spin" /> : null}
|
||||
{exec.isPending ? "提交中…" : "加入整理队列"}
|
||||
{exec.isPending ? "提交中…" : "加入队列"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -145,7 +145,7 @@ function RowMarks({ row }: { row: WizardRow }) {
|
||||
<span className="flex flex-wrap gap-1">
|
||||
{it.existing.assetId ? (
|
||||
<StatusBadge tone="neutral" className="whitespace-nowrap">
|
||||
已登记
|
||||
已整理
|
||||
</StatusBadge>
|
||||
) : null}
|
||||
{it.records?.count ? (
|
||||
|
||||
@@ -109,7 +109,7 @@ export function StepOptions({
|
||||
onSelect={() => set({ action: "move" })}
|
||||
disabled={hnrCount ? `${hnrCount} 项做种中且 H&R 未满足,不能移动` : null}
|
||||
title="移动"
|
||||
hint="整理后删源;跨卷时按复制 → 核验 → 删源"
|
||||
hint="整理后删除源文件;跨卷移动会先复制、核验,再删除源文件"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
@@ -119,8 +119,8 @@ export function StepOptions({
|
||||
<Choice
|
||||
checked={options.onExisting === "conflict"}
|
||||
onSelect={() => set({ onExisting: "conflict" })}
|
||||
title="交给待处理"
|
||||
hint="该组不执行,冲突进待处理"
|
||||
title="标为需要处理"
|
||||
hint="这组不执行,在整理记录里标为需要处理"
|
||||
/>
|
||||
<Choice
|
||||
checked={options.onExisting === "skip"}
|
||||
|
||||
@@ -28,7 +28,7 @@ import { PathSegments } from "./path-segments.tsx";
|
||||
export type GroupTitle = (g: FilesOrganizeGroup) => string;
|
||||
|
||||
const ERROR_TEXT: Record<string, string> = {
|
||||
plan_blocked: "目标已存在,按「交给待处理」不执行",
|
||||
plan_blocked: "目标已存在,这组不执行",
|
||||
plan_stale: "源文件在预览后有变化",
|
||||
approval_required: "移动需要审批或持续授权",
|
||||
download_active: "做种中且 H&R 未满足,不能移动",
|
||||
@@ -38,14 +38,14 @@ const ERROR_TEXT: Record<string, string> = {
|
||||
const REASON_PREFIX: Record<string, string> = {
|
||||
library_root_offline: "目标媒体库目录离线",
|
||||
root_unavailable: "目录离线",
|
||||
not_writable: "目标不可写",
|
||||
path_escape: "路径越界",
|
||||
not_writable: "目标目录不可写",
|
||||
path_escape: "路径指向登记目录之外",
|
||||
source_missing: "源文件不存在",
|
||||
};
|
||||
|
||||
function errorText(e: { code: string; message: string }): string {
|
||||
const m = /^([a-z_]+):\s*(.*)$/.exec(e.message ?? "");
|
||||
if (m && REASON_PREFIX[m[1] as string]) return `${REASON_PREFIX[m[1] as string]}(${m[2] || m[1]})`;
|
||||
if (m && REASON_PREFIX[m[1] as string]) return `${REASON_PREFIX[m[1] as string]}:${m[2] || m[1]}`;
|
||||
return e.message || ERROR_TEXT[e.code] || e.code;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ function FileLine({ f, roots, nested }: { f: GroupFile; roots: FileRoot[]; neste
|
||||
) : f.action === "replace" ? (
|
||||
<StatusBadge tone="warning">替换</StatusBadge>
|
||||
) : f.target === f.source ? (
|
||||
<StatusBadge>原地登记</StatusBadge>
|
||||
<StatusBadge>原地整理</StatusBadge>
|
||||
) : !f.target ? (
|
||||
<StatusBadge>不整理</StatusBadge>
|
||||
) : null;
|
||||
@@ -173,7 +173,7 @@ export function PreviewGroupCard({
|
||||
);
|
||||
} else {
|
||||
status = groupExecutable(g) ? (
|
||||
<StatusBadge tone="success">计划就绪</StatusBadge>
|
||||
<StatusBadge tone="success">可执行</StatusBadge>
|
||||
) : groupAllSkipped(g) && !g.error ? (
|
||||
<StatusBadge>全部跳过</StatusBadge>
|
||||
) : (
|
||||
@@ -228,7 +228,7 @@ export function PreviewGroupCard({
|
||||
</p>
|
||||
))}
|
||||
{!result ? <TargetLine g={g} roots={roots} /> : null}
|
||||
{g.truncated ? <p className="text-caption text-warning">文件过多,预览只列出前一部分。</p> : null}
|
||||
{g.truncated ? <p className="text-caption text-warning">文件太多,只列出前一部分。</p> : null}
|
||||
{!result && nested.length ? (
|
||||
<ul className="divide-y divide-border/50">
|
||||
{nested.map((n) => (
|
||||
@@ -248,8 +248,8 @@ export function PreviewGroupCard({
|
||||
排除 {g.excluded.length} 个:
|
||||
{g.excluded
|
||||
.slice(0, 6)
|
||||
.map((e) => `${baseName(e.source)}(${EXCLUDE_REASON[e.reason] ?? e.reason})`)
|
||||
.join(",")}
|
||||
.map((e) => `${baseName(e.source)}(${EXCLUDE_REASON[e.reason] ?? e.reason})`)
|
||||
.join("、")}
|
||||
{g.excluded.length > 6 ? "…" : ""}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -64,7 +64,7 @@ export function StructurePicker({
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{list.error ? <p className="text-meta text-warning">剧集组列表暂时取不到,只能跟随作品。</p> : null}
|
||||
{list.error ? <p className="text-meta text-warning">读取剧集组失败,只能跟随作品</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user