文案(媒体库组件):状态标签按术语表统一,去掉候选/退役/已预留等内部说法

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:48:33 +08:00
1 parent 542f107566
commit 9c7ce109ea
10 files changed
+48 -48

No files matched your search

@@ -102,13 +102,13 @@ export function IdentityDialog({
<DialogHeader> <DialogHeader>
<DialogTitle>修改外部身份</DialogTitle> <DialogTitle>修改外部身份</DialogTitle>
<DialogDescription> <DialogDescription>
同类型的旧 TMDB 身份会退役,保留修订记录。加人工锁后,自动匹配和刷新不再改动。 旧的 TMDB 身份停用,修订记录保留。加人工锁后,自动匹配和刷新不会再改。
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={submit} className="grid gap-3"> <form onSubmit={submit} className="grid gap-3">
{usable.length ? ( {usable.length ? (
<div className="grid gap-1.5"> <div className="grid gap-1.5">
<p className="text-caption font-medium text-foreground">候选</p> <p className="text-caption font-medium text-foreground">已记录的身份</p>
<ul className="grid gap-1.5"> <ul className="grid gap-1.5">
{usable.map((c) => ( {usable.map((c) => (
<li key={c.id}> <li key={c.id}>
@@ -227,7 +227,7 @@ export function FieldLockDialog({
if (field === "year") { if (field === "year") {
const n = Number(r.data.value); const n = Number(r.data.value);
if (!Number.isInteger(n) || n < 1870 || n > 2200) { if (!Number.isInteger(n) || n < 1870 || n > 2200) {
setErrors({ value: "年份须是 1870–2200 的整数" }); setErrors({ value: "年份填 1870–2200 之间的整数" });
return; return;
} }
v = n; v = n;
@@ -150,7 +150,7 @@ export function EnrichSection({ item }: { item: IdentityPoolItem }) {
{!out ? ( {!out ? (
<p className="text-caption text-muted-foreground"> <p className="text-caption text-muted-foreground">
TMDB TMDB
查不到时,按标题搜索网页:读平台和百科网站,其它网站只用搜索摘要,抽取字段并附出处和置信度。先看建议,确认后才建作品。 查不到时,按标题搜索网页,从平台和百科页面抽取字段,附出处和置信度。确认后才建作品。
</p> </p>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
@@ -160,12 +160,12 @@ function ManualAssign({ item }: { item: IdentityPoolItem }) {
await dismiss.mutateAsync({ id: item.id }); await dismiss.mutateAsync({ id: item.id });
toast.success("已忽略"); toast.success("已忽略");
} catch (err) { } catch (err) {
toast.error(errorMessage(err, "操作失败")); toast.error(errorMessage(err, "忽略失败"));
} }
}} }}
> >
<BellOff /> <BellOff />
忽略此资源 忽略这个资源
</Button> </Button>
</DetailSection> </DetailSection>
); );
@@ -229,7 +229,7 @@ export function PoolSheet({ item, onClose }: { item: IdentityPoolItem | null; on
<EnrichSection key={item.id} item={item} /> <EnrichSection key={item.id} item={item} />
) : null} ) : null}
<DetailSection title={`候选(${item.candidates.length})`}> <DetailSection title={`候选作品(${item.candidates.length})`}>
{item.candidates.length ? ( {item.candidates.length ? (
<ul className="space-y-2"> <ul className="space-y-2">
{item.candidates.map((c, i) => ( {item.candidates.map((c, i) => (
@@ -245,7 +245,7 @@ export function PoolSheet({ item, onClose }: { item: IdentityPoolItem | null; on
<p className="text-caption text-muted-foreground"> <p className="text-caption text-muted-foreground">
{item.reason === "unmatched" {item.reason === "unmatched"
? "没有命中订阅或本地作品,未查询 TMDB。点「重新识别」会查一次 TMDB;新增相近别名时自动重试。" ? "没有命中订阅或本地作品,未查询 TMDB。点「重新识别」会查一次 TMDB;新增相近别名时自动重试。"
: "没有候选。有新证据时自动重试。"} : "没有候选作品。有新证据时自动重试。"}
</p> </p>
)} )}
</DetailSection> </DetailSection>
@@ -73,7 +73,7 @@ export function PoolTab() {
}, },
{ {
key: "cands", key: "cands",
header: "候选", header: "候选作品",
hideBelow: "sm", hideBelow: "sm",
align: "right", align: "right",
cell: (r) => <span className="tabular-nums">{r.candidates.length}</span>, cell: (r) => <span className="tabular-nums">{r.candidates.length}</span>,
+17 -17
View File
@@ -43,12 +43,12 @@ export const ENTITY_STATUS: Record<MediaEntityStatus, ToneLabel> = {
/** Slot 状态(REQ §6.1):每个状态有明确文本,不只靠颜色 */ /** Slot 状态(REQ §6.1):每个状态有明确文本,不只靠颜色 */
export const SLOT_STATUS: Record<SlotStatus, ToneLabel> = { export const SLOT_STATUS: Record<SlotStatus, ToneLabel> = {
empty: { label: "空缺", tone: "neutral" }, empty: { label: "空缺", tone: "neutral" },
reserved: { label: "已预留", tone: "neutral" }, reserved: { label: "准备下载", tone: "neutral" },
downloading: { label: "下载中", tone: "neutral" }, downloading: { label: "下载中", tone: "neutral" },
verifying: { label: "核验中", tone: "neutral" }, verifying: { label: "核验中", tone: "neutral" },
satisfied: { label: "已满足", tone: "success" }, satisfied: { label: "已满足", tone: "success" },
upgrade_pending: { label: "等待升级", tone: "warning" }, upgrade_pending: { label: "等待升级", tone: "warning" },
attention: { label: "需处理", tone: "destructive" }, attention: { label: "需要处理", tone: "destructive" },
}; };
export const SLOT_STATUS_ORDER: SlotStatus[] = [ export const SLOT_STATUS_ORDER: SlotStatus[] = [
"attention", "attention",
@@ -64,7 +64,7 @@ export const IDENTITY_STATUS: Record<IdentityStatus, ToneLabel> = {
accepted: { label: "已确认", tone: "success" }, accepted: { label: "已确认", tone: "success" },
proposed: { label: "待确认", tone: "warning" }, proposed: { label: "待确认", tone: "warning" },
conflicted: { label: "冲突", tone: "destructive" }, conflicted: { label: "冲突", tone: "destructive" },
retired: { label: "已退役", tone: "neutral" }, retired: { label: "已停用", tone: "neutral" },
}; };
export const EVIDENCE_LEVEL: Record<string, string> = { export const EVIDENCE_LEVEL: Record<string, string> = {
@@ -100,7 +100,7 @@ export const LOCATION_KIND: Record<string, string> = {
}; };
export const DOWNLOAD_STATUS: Record<DownloadStatus, ToneLabel> = { export const DOWNLOAD_STATUS: Record<DownloadStatus, ToneLabel> = {
queued: { label: "排队", tone: "neutral" }, queued: { label: "排队中", tone: "neutral" },
submitting: { label: "提交中", tone: "neutral" }, submitting: { label: "提交中", tone: "neutral" },
downloading: { label: "下载中", tone: "neutral" }, downloading: { label: "下载中", tone: "neutral" },
paused: { label: "已暂停", tone: "warning" }, paused: { label: "已暂停", tone: "warning" },
@@ -110,15 +110,15 @@ export const DOWNLOAD_STATUS: Record<DownloadStatus, ToneLabel> = {
imported: { label: "已整理", tone: "success" }, imported: { label: "已整理", tone: "success" },
failed: { label: "失败", tone: "destructive" }, failed: { label: "失败", tone: "destructive" },
removed: { label: "已移除", tone: "neutral" }, removed: { label: "已移除", tone: "neutral" },
missing: { label: "下载器中缺失", tone: "warning" }, missing: { label: "下载器里没有", tone: "warning" },
}; };
export const ORG_STATUS: Record<OrganizationStatus, ToneLabel> = { export const ORG_STATUS: Record<OrganizationStatus, ToneLabel> = {
queued: { label: "排队", tone: "neutral" }, queued: { label: "排队中", tone: "neutral" },
running: { label: "整理中", tone: "neutral" }, running: { label: "整理中", tone: "neutral" },
succeeded: { label: "核验通过", tone: "success" }, succeeded: { label: "已整理", tone: "success" },
partial: { label: "部分成功", tone: "warning" }, partial: { label: "部分整理", tone: "warning" },
failed: { label: "失败", tone: "destructive" }, failed: { label: "整理失败", tone: "destructive" },
cancelled: { label: "已取消", tone: "neutral" }, cancelled: { label: "已取消", tone: "neutral" },
}; };
@@ -127,8 +127,8 @@ export const CANDIDATE_DECISION: Record<string, ToneLabel> = {
upgrade: { label: "可升级", tone: "success" }, upgrade: { label: "可升级", tone: "success" },
reject: { label: "拒绝", tone: "destructive" }, reject: { label: "拒绝", tone: "destructive" },
unknown_field: { label: "字段未知", tone: "warning" }, unknown_field: { label: "字段未知", tone: "warning" },
skipped: { label: "跳过", tone: "neutral" }, skipped: { label: "已跳过", tone: "neutral" },
deferred: { label: "延后:已有计划在途", tone: "warning" }, deferred: { label: "延后:已有下载在进行", tone: "warning" },
}; };
export const DECISION_KIND: Record<DecisionKind, string> = { export const DECISION_KIND: Record<DecisionKind, string> = {
@@ -141,7 +141,7 @@ export const DECISION_KIND: Record<DecisionKind, string> = {
space_insufficient: "空间不足", space_insufficient: "空间不足",
approval_required: "需要审批", approval_required: "需要审批",
metadata_conflict: "元数据冲突", metadata_conflict: "元数据冲突",
source_unreachable: "来源不可达", source_unreachable: "来源连不上",
downloader_offline: "下载器连不上", downloader_offline: "下载器连不上",
cache_volume_unknown: "下载目录读不到容量", cache_volume_unknown: "下载目录读不到容量",
other: "其他", other: "其他",
@@ -163,7 +163,7 @@ export const SEVERITY: Record<Severity, ToneLabel> = {
}; };
export const POOL_REASON: Record<IdentityCandidateReason, ToneLabel> = { export const POOL_REASON: Record<IdentityCandidateReason, ToneLabel> = {
ambiguous: { label: "多个候选", tone: "warning" }, ambiguous: { label: "匹配到多部作品", tone: "warning" },
no_match: { label: "没有匹配", tone: "neutral" }, no_match: { label: "没有匹配", tone: "neutral" },
proposed: { label: "待确认", tone: "warning" }, proposed: { label: "待确认", tone: "warning" },
lookup_failed: { label: "查询失败", tone: "destructive" }, lookup_failed: { label: "查询失败", tone: "destructive" },
@@ -207,7 +207,7 @@ export const POLICY_STATUS: Record<PolicyStatus, ToneLabel> = {
export const SUB_MODE: Record<SubscriptionMode, string> = { once: "单次收录", continuous: "持续追踪" }; export const SUB_MODE: Record<SubscriptionMode, string> = { once: "单次收录", continuous: "持续追踪" };
export const SEASON_SCOPE: Record<SeasonScope, string> = { export const SEASON_SCOPE: Record<SeasonScope, string> = {
current_season: "仅本季", current_season: "仅本季",
following_seasons: "接续后续季", following_seasons: "包括后续季",
}; };
export const PRIORITY: Record<AcquisitionPriority, string> = { export const PRIORITY: Record<AcquisitionPriority, string> = {
immediate: "立即", immediate: "立即",
@@ -336,9 +336,9 @@ export const opts = <K extends string>(rec: Record<K, string | { label: string }
/** 收录版本 / 候选的原因串(如 `blocked:no_main_video`、`deferred:slot_busy;inflight:<id>`)→ 中文;不认识的原样显示 */ /** 收录版本 / 候选的原因串(如 `blocked:no_main_video`、`deferred:slot_busy;inflight:<id>`)→ 中文;不认识的原样显示 */
const SLOT_REASON: Record<string, string> = { const SLOT_REASON: Record<string, string> = {
"blocked:no_main_video": "没有可整理的主视频", "blocked:no_main_video": "没有可整理的主视频",
"deferred:slot_busy": "已有计划在途,等它结束再评估", "deferred:slot_busy": "已有下载在进行,结束后再评估",
verified: "已核验整理", verified: "已整理",
verified_partial: "部分核验整理", verified_partial: "部分整理",
hard_conditions_met: "硬条件全部满足", hard_conditions_met: "硬条件全部满足",
}; };
export function slotReasonText(raw: string): string { export function slotReasonText(raw: string): string {
@@ -108,8 +108,8 @@ export function PreferenceFormDialog({
<DialogTitle>{tag ? "编辑偏好" : "添加偏好"}</DialogTitle> <DialogTitle>{tag ? "编辑偏好" : "添加偏好"}</DialogTitle>
<DialogDescription> <DialogDescription>
{tag {tag
? "修改作用范围或规则会留下旧语义的抑制记录,智能体不会按旧证据加回。" ? "改了作用范围或规则后,智能体不会再按旧证据把它加回来。"
: "手动添加等同对话确认。之前取消过的偏好,重新添加会解除抑制。"} : "手动添加和在对话里确认效果相同。之前取消过的偏好,重新添加后恢复生效。"}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<form onSubmit={submit} className="grid gap-2" noValidate> <form onSubmit={submit} className="grid gap-2" noValidate>
@@ -26,7 +26,7 @@ export const SCRAPE_STATUS: Record<ScrapeRecord["status"], { label: string; tone
const ACTION: Record<ScrapeFileResult["action"], { label: string; tone: StatusTone }> = { const ACTION: Record<ScrapeFileResult["action"], { label: string; tone: StatusTone }> = {
written: { label: "已写入", tone: "success" }, written: { label: "已写入", tone: "success" },
skipped: { label: "跳过", tone: "neutral" }, skipped: { label: "已跳过", tone: "neutral" },
failed: { label: "失败", tone: "destructive" }, failed: { label: "失败", tone: "destructive" },
}; };
@@ -144,7 +144,7 @@ export function ScrapePanel({ detail }: { detail: LibraryDetail }) {
scrape.mutateAsync(mode ? { mode } : {}).then( scrape.mutateAsync(mode ? { mode } : {}).then(
(r) => toast.success(r.deduplicated ? "已在排队" : "已提交刮削"), (r) => toast.success(r.deduplicated ? "已在排队" : "已提交刮削"),
(err) => { (err) => {
toast.error(errorMessage(err, "刮削没有提交")); toast.error(errorMessage(err, "刮削提交失败"));
throw err; throw err;
}, },
); );
@@ -78,7 +78,7 @@ export function SlotCard({
</dl> </dl>
{latest.length ? ( {latest.length ? (
<div className="mt-3 border-t border-border/60 pt-3"> <div className="mt-3 border-t border-border/60 pt-3">
<p className="mb-2 text-meta text-muted-foreground">最近候选及选用理由(每个资源最近一次评估)</p> <p className="mb-2 text-meta text-muted-foreground">最近评估的资源和理由(每个资源只列最近一次)</p>
<ul className="space-y-2"> <ul className="space-y-2">
{latest.map((c) => ( {latest.map((c) => (
<li key={c.sourceReleaseId} className="min-w-0"> <li key={c.sourceReleaseId} className="min-w-0">
@@ -53,7 +53,7 @@ const KIND_OPTS: { value: string; label: string }[] = SubscriptionMediaKind.opti
})); }));
export const PUBLISH_NOTICE = export const PUBLISH_NOTICE =
"发布后,作用范围内的下载、追更和整理按此策略自动执行,不再逐次审批。升级替换、媒体清理和下载缓存清理仍需单独审批。旧的已发布版本被替代,授权随之失效。"; "发布后,作用范围内的下载、追更和整理按这个策略自动执行,不再逐次审批;升级替换、媒体清理和下载缓存清理仍要单独审批。上一个已发布版本的授权随之失效。";
type TemplateDraft = { key: string; name: string; rules: SlotRulesDraft; open: boolean }; type TemplateDraft = { key: string; name: string; rules: SlotRulesDraft; open: boolean };
type PolicyDraft = { type PolicyDraft = {
@@ -164,7 +164,7 @@ function buildPolicy(d: PolicyDraft):
const strict = AcquisitionPolicyRules.strict().safeParse(rules); const strict = AcquisitionPolicyRules.strict().safeParse(rules);
if (!strict.success && !Object.keys(errors).length) { if (!strict.success && !Object.keys(errors).length) {
const i = strict.error.issues[0]; const i = strict.error.issues[0];
errors.rules = `规则结构不合法:${i?.path.join(".")} ${i?.message}`; errors.rules = `规则格式有误:${i?.path.join(".")} ${i?.message}`;
} }
if (Object.keys(errors).length || !meta.success) return { ok: false, errors }; if (Object.keys(errors).length || !meta.success) return { ok: false, errors };
return { return {
@@ -308,7 +308,7 @@ function PolicyGroup({
) : ( ) : (
<Button variant="outline" size="sm" onClick={() => onDraftFrom(head)}> <Button variant="outline" size="sm" onClick={() => onDraftFrom(head)}>
<FilePlus2 /> <FilePlus2 />
基于 r{head.revision} 新建草案 从 r{head.revision} 新建草案
</Button> </Button>
) )
} }
@@ -449,7 +449,7 @@ function PolicyDetailSheet({
> >
{!pv?.success ? ( {!pv?.success ? (
<p className="rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-caption text-warning"> <p className="rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-caption text-warning">
这个草案还没有预览。先预览可以看到影响范围。 还没有预览。预览后能看到影响范围。
</p> </p>
) : null} ) : null}
</ConfirmDialog> </ConfirmDialog>
@@ -458,14 +458,14 @@ function PolicyDetailSheet({
<> <>
<Button variant="outline" onClick={() => onDraftFrom(rev)}> <Button variant="outline" onClick={() => onDraftFrom(rev)}>
<FilePlus2 /> <FilePlus2 />
基于此版本新建草案 从这个版本新建草案
</Button> </Button>
{rev.status === "superseded" || rev.status === "rolled_back" ? ( {rev.status === "superseded" || rev.status === "rolled_back" ? (
<ConfirmDialog <ConfirmDialog
trigger={ trigger={
<Button variant="outline"> <Button variant="outline">
<Undo2 /> <Undo2 />
回滚到此版本 回滚到这个版本
</Button> </Button>
} }
title={`回滚到 r${rev.revision}?`} title={`回滚到 r${rev.revision}?`}
@@ -538,7 +538,7 @@ function PreviewBlock({ value }: { value: JsonObject | null }) {
if (!value) if (!value)
return ( return (
<p className="text-caption text-muted-foreground"> <p className="text-caption text-muted-foreground">
还没有预览。预览用最近已识别的资源评估模板,不会执行任何操作。 还没有预览。预览用最近识别的资源试算模板,不会下载或整理。
</p> </p>
); );
const p = PolicyPreview.safeParse(value); const p = PolicyPreview.safeParse(value);
@@ -565,7 +565,7 @@ function PreviewBlock({ value }: { value: JsonObject | null }) {
))} ))}
</div> </div>
<p className="text-meta text-muted-foreground"> <p className="text-meta text-muted-foreground">
计算于 {formatDateTime(v.evaluatedAt)};接受/排除按"资源 × 模板"计数。 计算于 {formatDateTime(v.evaluatedAt)};接受 / 排除按「资源 × 模板」计数。
</p> </p>
{v.samples.length ? ( {v.samples.length ? (
<ul className="divide-y divide-border/60 rounded-lg border border-border/70"> <ul className="divide-y divide-border/60 rounded-lg border border-border/70">
@@ -586,7 +586,7 @@ function PreviewBlock({ value }: { value: JsonObject | null }) {
))} ))}
</ul> </ul>
) : ( ) : (
<p className="text-caption text-muted-foreground">没有可用于回放的已识别资源。</p> <p className="text-caption text-muted-foreground">没有可回放的已识别资源。</p>
)} )}
</div> </div>
); );
@@ -659,7 +659,7 @@ function PolicyEditorSheet({
open open
onOpenChange={(o) => !o && !pending && onClose()} onOpenChange={(o) => !o && !pending && onClose()}
width="lg" width="lg"
title={editId ? `编辑草案 r${base?.revision}` : base ? `基于 r${base.revision} 新建草案` : "新建收录策略草案"} title={editId ? `编辑草案 r${base?.revision}` : base ? `从 r${base.revision} 新建草案` : "新建收录策略草案"}
description="保存为草案,发布后才生效。" description="保存为草案,发布后才生效。"
footer={ footer={
<> <>
@@ -66,7 +66,7 @@ export function SlotsPanel({
plain plain
icon={Target} icon={Target}
title="还没有收录版本" title="还没有收录版本"
description="没有时按已发布收录策略的默认模板创建,也可以手动新建。" description="订阅时按已发布收录策略的默认模板自动创建,也可以手动新建。"
className="py-8" className="py-8"
/> />
) : ( ) : (
@@ -80,7 +80,7 @@ export function SlotsPanel({
<span> <span>
{[s.currentAsset.resolution, s.currentAsset.sourceType, s.currentAsset.releaseGroup] {[s.currentAsset.resolution, s.currentAsset.sourceType, s.currentAsset.releaseGroup]
.filter(Boolean) .filter(Boolean)
.join(" · ") || "交付特征未知"} .join(" · ") || "规格未知"}
{s.currentAsset.sizeBytes ? ` · ${formatBytes(s.currentAsset.sizeBytes, 1)}` : ""} {s.currentAsset.sizeBytes ? ` · ${formatBytes(s.currentAsset.sizeBytes, 1)}` : ""}
{s.currentAsset.status !== "active" ? ` · ${s.currentAsset.status}` : ""} {s.currentAsset.status !== "active" ? ` · ${s.currentAsset.status}` : ""}
</span> </span>
@@ -140,7 +140,7 @@ function SlotEditorSheet({
const errs: Record<string, string> = {}; const errs: Record<string, string> = {};
if (!meta.success) for (const i of meta.error.issues) errs[String(i.path[0])] ??= i.message; if (!meta.success) for (const i of meta.error.issues) errs[String(i.path[0])] ??= i.message;
if (!rules.ok) Object.assign(errs, rules.errors); if (!rules.ok) Object.assign(errs, rules.errors);
if (requireDistinct && !distinctGroup.trim()) errs.distinctGroup = "要求独立文件时须填写分组"; if (requireDistinct && !distinctGroup.trim()) errs.distinctGroup = "要求独立文件时要填组合分组";
setErrors(errs); setErrors(errs);
if (Object.keys(errs).length || !meta.success || !rules.ok) return; if (Object.keys(errs).length || !meta.success || !rules.ok) return;
const common = { const common = {
@@ -180,7 +180,7 @@ function SlotEditorSheet({
width="lg" width="lg"
title={slot ? `编辑收录版本 ${slot.name}` : "新建收录版本"} title={slot ? `编辑收录版本 ${slot.name}` : "新建收录版本"}
description={ description={
slot ? `当前 r${slot.revision}。保存时若已被修改,会提示刷新。` : "不满足硬条件的资源不收;软偏好决定先收哪个。" slot ? `当前 r${slot.revision}。保存时如果已被别处修改,会提示刷新。` : "不满足硬条件的资源不收;软偏好决定先收哪个。"
} }
footer={ footer={
<> <>
@@ -195,7 +195,7 @@ function SlotEditorSheet({
> >
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<FormField <FormField
label="目标名称" label="名称"
htmlFor={`${id}-name`} htmlFor={`${id}-name`}
required required
error={errors.name} error={errors.name}
@@ -213,7 +213,7 @@ function SlotEditorSheet({
label="组合分组" label="组合分组"
htmlFor={`${id}-grp`} htmlFor={`${id}-grp`}
error={errors.distinctGroup} error={errors.distinctGroup}
hint="同组目标可要求由不同文件满足" hint="同组的收录版本可以要求由不同文件满足"
> >
<Input <Input
id={`${id}-grp`} id={`${id}-grp`}
@@ -226,7 +226,7 @@ function SlotEditorSheet({
<div className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2"> <div className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-caption font-medium text-foreground">要求独立文件</p> <p className="text-caption font-medium text-foreground">要求独立文件</p>
<p className="text-meta text-muted-foreground">同一个文件不能同时满足两个目标</p> <p className="text-meta text-muted-foreground">同一个文件不能同时满足同组的两个收录版本</p>
</div> </div>
<Switch checked={requireDistinct} onCheckedChange={setRequireDistinct} aria-label="要求独立文件" /> <Switch checked={requireDistinct} onCheckedChange={setRequireDistinct} aria-label="要求独立文件" />
</div> </div>