文案(整理向导):按 human-copy 重写整理对话框、预览与作品选择

- 「交给待处理」改「标为需要处理」,「转移后的路径」改「目标目录」,「刷新作品资料」改「刷新元数据」(术语表)
- 「来源配置」统一为「整理来源」;按钮与队列入口统一叫「队列」
- 去掉「请…」「本次 / 本批」等套话;只列非零计数;预览原因的括号与分隔符改全角

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-07 01:51:01 +08:00
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>
);
}