merge: copy/c3
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
commit
f5f322754b
35 files changed
+119
-122
No files matched your search
@@ -271,7 +271,7 @@ function ExploreResults({ s }: { s: ExploreState }) {
|
|||||||
if (!items.length)
|
if (!items.length)
|
||||||
return (
|
return (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title={s.list === "search" ? "没有搜到作品" : "没有条目"}
|
title={s.list === "search" ? "没有搜到作品" : "没有作品"}
|
||||||
description={s.list === "search" ? "换个关键词,或换个来源再搜。" : "换个列表或放宽筛选条件。"}
|
description={s.list === "search" ? "换个关键词,或换个来源再搜。" : "换个列表或放宽筛选条件。"}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ function Section({ section, actions }: { section: RecommendSection; actions: Ret
|
|||||||
</div>
|
</div>
|
||||||
) : q.isSuccess && items.length === 0 ? (
|
) : q.isSuccess && items.length === 0 ? (
|
||||||
<p className="rounded-xl border border-dashed border-border px-4 py-6 text-caption text-muted-foreground">
|
<p className="rounded-xl border border-dashed border-border px-4 py-6 text-caption text-muted-foreground">
|
||||||
这里暂时没有条目。
|
这一栏暂时没有作品。
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="scroll-bleed flex gap-3 overflow-x-auto">
|
<ul className="scroll-bleed flex gap-3 overflow-x-auto">
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ function IgnoreButton({ d, view }: { d: Decision; view: EpisodeDecisionView }) {
|
|||||||
.then(
|
.then(
|
||||||
() => toast.success(withRule ? "已忽略,并建了忽略规则" : "已忽略"),
|
() => toast.success(withRule ? "已忽略,并建了忽略规则" : "已忽略"),
|
||||||
(err) => {
|
(err) => {
|
||||||
toast.error(errorMessage(err, "操作失败"));
|
toast.error(errorMessage(err, "忽略失败"));
|
||||||
throw err;
|
throw err;
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -269,12 +269,12 @@ export function EpisodeDecision({ d, open }: { d: Decision; open: boolean }) {
|
|||||||
trigger={
|
trigger={
|
||||||
<Button size="sm">
|
<Button size="sm">
|
||||||
<Bot />
|
<Bot />
|
||||||
确认智能体建议
|
采用智能体建议
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title="按智能体建议的规则处理?"
|
title="按智能体建议建规则?"
|
||||||
description={`${view.confirmAgent.description ?? ""}。规则在作品页的集数映射里可以再改。`}
|
description={`${view.confirmAgent.description ?? ""}。规则在作品页的集数映射里可以再改。`}
|
||||||
confirmLabel="确认"
|
confirmLabel="建规则并重算"
|
||||||
onConfirm={() =>
|
onConfirm={() =>
|
||||||
resolve.mutateAsync({ id: d.id, optionKey: view.confirmAgent!.key }).then(
|
resolve.mutateAsync({ id: d.id, optionKey: view.confirmAgent!.key }).then(
|
||||||
() => toast.success("已建规则并重算"),
|
() => toast.success("已建规则并重算"),
|
||||||
@@ -291,12 +291,12 @@ export function EpisodeDecision({ d, open }: { d: Decision; open: boolean }) {
|
|||||||
trigger={
|
trigger={
|
||||||
<Button size="sm" variant={view.confirmAgent && view.agentSuggestion ? "outline" : "default"}>
|
<Button size="sm" variant={view.confirmAgent && view.agentSuggestion ? "outline" : "default"}>
|
||||||
<Check />
|
<Check />
|
||||||
确认
|
采用建议规则
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title="按建议规则处理?"
|
title="按建议建规则?"
|
||||||
description={`${view.confirm.description ?? ""}。规则在作品页的集数映射里可以再改。`}
|
description={`${view.confirm.description ?? ""}。规则在作品页的集数映射里可以再改。`}
|
||||||
confirmLabel="确认"
|
confirmLabel="建规则并重算"
|
||||||
onConfirm={() =>
|
onConfirm={() =>
|
||||||
resolve.mutateAsync({ id: d.id, optionKey: view.confirm!.key }).then(
|
resolve.mutateAsync({ id: d.id, optionKey: view.confirm!.key }).then(
|
||||||
() => toast.success("已建规则并重算"),
|
() => toast.success("已建规则并重算"),
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function SubscriptionMappingSummary({
|
|||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
title="集数映射"
|
title="集数映射"
|
||||||
description={`${season != null ? `第 ${season} 季` : "全部季"}最近资源按发布组的识别方式`}
|
description={`按发布组列出${season != null ? `第 ${season} 季` : "全部季"}最近资源的集数识别方式`}
|
||||||
actions={
|
actions={
|
||||||
<Link
|
<Link
|
||||||
to={mappingHref(consolePath("library", entityId))}
|
to={mappingHref(consolePath("library", entityId))}
|
||||||
|
|||||||
@@ -101,14 +101,12 @@ export function IdentityDialog({
|
|||||||
<DialogContent className="max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>修改外部身份</DialogTitle>
|
<DialogTitle>修改外部身份</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>旧的 TMDB 身份停用,修订记录保留。加人工锁后,自动匹配和刷新不会再改。</DialogDescription>
|
||||||
同类型的旧 TMDB 身份会退役,保留修订记录。加人工锁后,自动匹配和刷新不再改动。
|
|
||||||
</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 +225,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;
|
||||||
|
|||||||
@@ -63,5 +63,5 @@ export function originText(
|
|||||||
|
|
||||||
/** 切换确认的说明:改的是作品设置,影响季表、集数映射、订阅与刮削 */
|
/** 切换确认的说明:改的是作品设置,影响季表、集数映射、订阅与刮削 */
|
||||||
export function switchDescription(target: string): string {
|
export function switchDescription(target: string): string {
|
||||||
return `作品改按「${target}」分季:季与集按 TMDB 原季集迁到新位置,已整理文件跟着走;集数映射、订阅和刮削都按新季集,已绑定的资源会重新换算。之后可以再切回来。`;
|
return `改按「${target}」分季后,已整理文件随季集一起调整;集数映射、订阅和刮削都按新季集,已绑定的资源会重新换算。之后可以再切回来。`;
|
||||||
}
|
}
|
||||||
@@ -149,8 +149,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>,
|
||||||
@@ -139,7 +139,7 @@ export function PoolTab() {
|
|||||||
empty={{
|
empty={{
|
||||||
icon: SearchX,
|
icon: SearchX,
|
||||||
title: filtered ? "没有符合条件的未识别资源" : "没有未识别资源",
|
title: filtered ? "没有符合条件的未识别资源" : "没有未识别资源",
|
||||||
description: "未命中订阅、证据不足或冲突的资源会出现在这里;新增相近别名时自动重试,也可手动指定。",
|
description: "未命中订阅、证据不足或有冲突的资源。新增相近别名时会自动重试,也可以手动指定。",
|
||||||
}}
|
}}
|
||||||
hasMore={pool.hasNextPage}
|
hasMore={pool.hasNextPage}
|
||||||
onLoadMore={() => pool.fetchNextPage()}
|
onLoadMore={() => pool.fetchNextPage()}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -307,8 +307,7 @@ function PolicyGroup({
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<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 +448,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 +457,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}?`}
|
||||||
@@ -537,9 +536,7 @@ const scopeText = (scope: JsonObject) => {
|
|||||||
function PreviewBlock({ value }: { value: JsonObject | null }) {
|
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);
|
||||||
if (!p.success) return <JsonView value={value} />;
|
if (!p.success) return <JsonView value={value} />;
|
||||||
@@ -565,7 +562,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 +583,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 +656,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,9 @@ 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 +197,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 +215,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 +228,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>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function useSubscriptionOps() {
|
|||||||
reset: (id: string) =>
|
reset: (id: string) =>
|
||||||
reset.mutateAsync(id).then((r) => {
|
reset.mutateAsync(id).then((r) => {
|
||||||
const kept = r.keptPlans.length ? `,${r.keptPlans.length} 个仍在下载的保留` : "";
|
const kept = r.keptPlans.length ? `,${r.keptPlans.length} 个仍在下载的保留` : "";
|
||||||
toast.success(`已重置:释放 ${r.releasedPlans.length} 个计划${kept}`);
|
toast.success(`已重置:取消 ${r.releasedPlans.length} 个下载${kept}`);
|
||||||
}),
|
}),
|
||||||
remove: (ids: string[]) =>
|
remove: (ids: string[]) =>
|
||||||
batch.mutateAsync({ ids, action: "delete" }).then((r) => {
|
batch.mutateAsync({ ids, action: "delete" }).then((r) => {
|
||||||
@@ -63,7 +63,7 @@ export function useSubscriptionOps() {
|
|||||||
const ok = r.results.filter((x) => x.ok).length;
|
const ok = r.results.filter((x) => x.ok).length;
|
||||||
const label = { search: "开始搜索", pause: "暂停", resume: "恢复" }[a];
|
const label = { search: "开始搜索", pause: "暂停", resume: "恢复" }[a];
|
||||||
if (ok === ids.length) toast.success(`${ok} 个订阅已${label}`);
|
if (ok === ids.length) toast.success(`${ok} 个订阅已${label}`);
|
||||||
else toast.warning(`${ok}/${ids.length} 个已${label};${r.results.find((x) => !x.ok)?.error ?? ""}`);
|
else toast.warning(`${ok}/${ids.length} 个已${label};${r.results.find((x) => !x.ok)?.error ?? ""}`);
|
||||||
},
|
},
|
||||||
onError: (e) => toast.error(errorMessage(e)),
|
onError: (e) => toast.error(errorMessage(e)),
|
||||||
},
|
},
|
||||||
@@ -186,7 +186,7 @@ export function SubscriptionMenu({
|
|||||||
open={confirmReset}
|
open={confirmReset}
|
||||||
onOpenChange={setConfirmReset}
|
onOpenChange={setConfirmReset}
|
||||||
title="重置这个订阅的进度?"
|
title="重置这个订阅的进度?"
|
||||||
description="取消还没提交、或下载任务已不在的计划,清掉延后记录,然后重新评估。仍在下载的任务、已整理文件和总集数保留。"
|
description="取消还没提交或下载任务已不在的下载,清掉延后记录,再重新评估。仍在下载的任务、已整理文件和总集数保留。"
|
||||||
confirmLabel="重置"
|
confirmLabel="重置"
|
||||||
onConfirm={() =>
|
onConfirm={() =>
|
||||||
ops.reset(sub.id).catch((e) => {
|
ops.reset(sub.id).catch((e) => {
|
||||||
|
|||||||
@@ -302,17 +302,17 @@ export function CellSheet({
|
|||||||
{
|
{
|
||||||
label: "下载",
|
label: "下载",
|
||||||
value: c.planId
|
value: c.planId
|
||||||
? `${PLAN_STAGE[c.planStage ?? ""] ?? c.planStage ?? "在途"} · ${c.planId.slice(0, 8)}`
|
? `${PLAN_STAGE[c.planStage ?? ""] ?? c.planStage ?? "下载中"} · ${c.planId.slice(0, 8)}`
|
||||||
: null,
|
: null,
|
||||||
},
|
},
|
||||||
{ label: "原因", value: c.reason ? (SKIP_REASON[c.reason] ?? c.reason) : null },
|
{ label: "原因", value: c.reason ? (SKIP_REASON[c.reason] ?? c.reason) : null },
|
||||||
{ label: "延后", value: c.deferred ? "有资源在等待重评(预算或自动补全关闭)" : null },
|
{ label: "延后", value: c.deferred ? "有资源等待重新评估(预算用完或自动补全已关)" : null },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</DetailSection>
|
</DetailSection>
|
||||||
{fillable ? (
|
{fillable ? (
|
||||||
<p className="text-meta text-muted-foreground">
|
<p className="text-meta text-muted-foreground">
|
||||||
手动补全不受自动补全开关和补全预算限制,仍受全局每日预算与缓存卷阈值约束。
|
手动补全不受自动补全开关和补全预算限制,但仍受每日总预算和下载盘阈值限制。
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
{sel.row.slotId === null && c.state === "missing" ? (
|
{sel.row.slotId === null && c.state === "missing" ? (
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ describe("订阅总览文案", () => {
|
|||||||
expect(seasonLabel({ mediaKind: "series", seasonNumber: 1, seasonScope: "current_season" })).toBe("S1");
|
expect(seasonLabel({ mediaKind: "series", seasonNumber: 1, seasonScope: "current_season" })).toBe("S1");
|
||||||
expect(seasonLabel({ mediaKind: "movie", seasonNumber: null, seasonScope: "current_season" })).toBeNull();
|
expect(seasonLabel({ mediaKind: "movie", seasonNumber: null, seasonScope: "current_season" })).toBeNull();
|
||||||
});
|
});
|
||||||
it("版本徽标按 洗版 → 在途 → 缺 → 已齐 判定", () => {
|
it("版本徽标按 洗版 → 下载中 → 缺 → 已齐 判定", () => {
|
||||||
expect(slotBadge(slot({ upgrading: true, minPri: 97, targetPri: 100, missing: 2 })).label).toBe("洗版中 97→100");
|
expect(slotBadge(slot({ upgrading: true, minPri: 97, targetPri: 100, missing: 2 })).label).toBe("洗版中 97→100");
|
||||||
expect(slotBadge(slot({ inflight: 2, missing: 3 }))).toEqual({ label: "在途 2", tone: "neutral" });
|
expect(slotBadge(slot({ inflight: 2, missing: 3 }))).toEqual({ label: "下载中 2", tone: "neutral" });
|
||||||
expect(slotBadge(slot({ missing: 3 })).tone).toBe("destructive");
|
expect(slotBadge(slot({ missing: 3 })).tone).toBe("destructive");
|
||||||
expect(slotBadge(slot({ have: 10, total: 10 }))).toEqual({ label: "已齐", tone: "success" });
|
expect(slotBadge(slot({ have: 10, total: 10 }))).toEqual({ label: "已齐", tone: "success" });
|
||||||
expect(slotBadge(slot({})).label).toBe("未开始");
|
expect(slotBadge(slot({})).label).toBe("未开始");
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const GROUP_LABEL: Record<SubscriptionGroup, string> = {
|
|||||||
following: "追更中",
|
following: "追更中",
|
||||||
waiting: "待搜索",
|
waiting: "待搜索",
|
||||||
completed: "已完成",
|
completed: "已完成",
|
||||||
paused: "暂停",
|
paused: "已暂停",
|
||||||
pending: "待定",
|
pending: "待定",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export function slotBadge(s: SubscriptionSlotSummary): ToneLabel {
|
|||||||
label: s.minPri !== null && s.targetPri !== null ? `洗版中 ${s.minPri}→${s.targetPri}` : "洗版中",
|
label: s.minPri !== null && s.targetPri !== null ? `洗版中 ${s.minPri}→${s.targetPri}` : "洗版中",
|
||||||
tone: "warning",
|
tone: "warning",
|
||||||
};
|
};
|
||||||
if (s.inflight > 0) return { label: `在途 ${s.inflight}`, tone: "neutral" };
|
if (s.inflight > 0) return { label: `下载中 ${s.inflight}`, tone: "neutral" };
|
||||||
if (s.missing > 0) return { label: `缺 ${s.missing}`, tone: "destructive" };
|
if (s.missing > 0) return { label: `缺 ${s.missing}`, tone: "destructive" };
|
||||||
if (s.total !== null && s.total > 0 && s.have >= s.total) return { label: "已齐", tone: "success" };
|
if (s.total !== null && s.total > 0 && s.have >= s.total) return { label: "已齐", tone: "success" };
|
||||||
return { label: s.have > 0 ? `已整理 ${s.have}` : "未开始", tone: "neutral" };
|
return { label: s.have > 0 ? `已整理 ${s.have}` : "未开始", tone: "neutral" };
|
||||||
@@ -79,11 +79,11 @@ export const AUTOFILL_LABEL: Record<AutoFillMode, string> = { inherit: "跟随
|
|||||||
export const AUTOFILL_SOURCE: Record<"subscription" | "rule_group" | "default", string> = {
|
export const AUTOFILL_SOURCE: Record<"subscription" | "rule_group" | "default", string> = {
|
||||||
subscription: "订阅设置",
|
subscription: "订阅设置",
|
||||||
rule_group: "规则组",
|
rule_group: "规则组",
|
||||||
default: "系统缺省",
|
default: "系统默认",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function autoFillText(a: SubscriptionOverviewItem["autoFill"]): string {
|
export function autoFillText(a: SubscriptionOverviewItem["autoFill"]): string {
|
||||||
return `自动补全${a.enabled ? "开" : "关"}${a.mode === "inherit" ? `(${AUTOFILL_SOURCE[a.source]})` : ""}`;
|
return `自动补全${a.enabled ? "开" : "关"}${a.mode === "inherit" ? `(${AUTOFILL_SOURCE[a.source]})` : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 覆盖矩阵 ──
|
// ── 覆盖矩阵 ──
|
||||||
@@ -123,7 +123,7 @@ export function cellKind(c: CoverageCell): CellKind {
|
|||||||
export const CELL_LABEL: Record<CellKind, string> = {
|
export const CELL_LABEL: Record<CellKind, string> = {
|
||||||
have: "已整理",
|
have: "已整理",
|
||||||
upgradable: "可升级",
|
upgradable: "可升级",
|
||||||
inflight: "在途",
|
inflight: "下载中",
|
||||||
deferred: "延后",
|
deferred: "延后",
|
||||||
missing: "缺失",
|
missing: "缺失",
|
||||||
suspected: "疑似缺失",
|
suspected: "疑似缺失",
|
||||||
@@ -131,12 +131,12 @@ export const CELL_LABEL: Record<CellKind, string> = {
|
|||||||
skipped: "不需要",
|
skipped: "不需要",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PLAN_STAGE: Record<string, string> = { reserved: "已预留", downloading: "下载中", verifying: "核验中" };
|
export const PLAN_STAGE: Record<string, string> = { reserved: "准备下载", downloading: "下载中", verifying: "核验中" };
|
||||||
|
|
||||||
export const SKIP_REASON: Record<string, string> = {
|
export const SKIP_REASON: Record<string, string> = {
|
||||||
out_of_range: "集范围外",
|
out_of_range: "集范围外",
|
||||||
not_aired: "未播出",
|
not_aired: "未播出",
|
||||||
not_required: "该收录版本不要求这个内容版本",
|
not_required: "收录版本不要求这个内容版本",
|
||||||
suspected: "总集数未知,按已见最高集推断",
|
suspected: "总集数未知,按已见最高集推断",
|
||||||
deferred: "有延后的资源",
|
deferred: "有延后的资源",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const itemTitle = (x: Item) => x.mediaEntityTitle ?? x.titleQuery ?? "未
|
|||||||
function Flags({ x }: { x: Item }) {
|
function Flags({ x }: { x: Item }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{x.group === "paused" ? <StatusBadge tone="neutral">暂停</StatusBadge> : null}
|
{x.group === "paused" ? <StatusBadge tone="neutral">已暂停</StatusBadge> : null}
|
||||||
{x.group === "pending" ? (
|
{x.group === "pending" ? (
|
||||||
<StatusBadge tone="warning">{x.mediaEntityId ? "待定" : "不在媒体库"}</StatusBadge>
|
<StatusBadge tone="warning">{x.mediaEntityId ? "待定" : "不在媒体库"}</StatusBadge>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -45,7 +45,7 @@ export function ProgressBlock({ x, className }: { x: Pick<Item, "progress" | "mo
|
|||||||
<span className="font-medium text-foreground tabular-nums">
|
<span className="font-medium text-foreground tabular-nums">
|
||||||
{progressText(x.progress)}
|
{progressText(x.progress)}
|
||||||
{x.progress.inflight > 0 ? (
|
{x.progress.inflight > 0 ? (
|
||||||
<span className="font-normal text-muted-foreground"> · 在途 {x.progress.inflight}</span>
|
<span className="font-normal text-muted-foreground"> · 下载中 {x.progress.inflight}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
{unknown ? <span className="text-muted-foreground">持续追踪</span> : null}
|
{unknown ? <span className="text-muted-foreground">持续追踪</span> : null}
|
||||||
@@ -57,7 +57,7 @@ export function ProgressBlock({ x, className }: { x: Pick<Item, "progress" | "mo
|
|||||||
"bg-[repeating-linear-gradient(90deg,var(--color-foreground)_0_4px,transparent_4px_8px)] opacity-20",
|
"bg-[repeating-linear-gradient(90deg,var(--color-foreground)_0_4px,transparent_4px_8px)] opacity-20",
|
||||||
)}
|
)}
|
||||||
role="img"
|
role="img"
|
||||||
aria-label={`已整理 ${x.progress.have},在途 ${x.progress.inflight},总 ${x.progress.total ?? "未知"}`}
|
aria-label={`已整理 ${x.progress.have},下载中 ${x.progress.inflight},总 ${x.progress.total ?? "未知"}`}
|
||||||
>
|
>
|
||||||
{unknown ? null : (
|
{unknown ? null : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export function SlotPanel({
|
|||||||
<span className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-1 text-meta text-muted-foreground tabular-nums max-sm:w-full max-sm:pl-9">
|
<span className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-1 text-meta text-muted-foreground tabular-nums max-sm:w-full max-sm:pl-9">
|
||||||
<span>已整理 {r.summary.have}</span>
|
<span>已整理 {r.summary.have}</span>
|
||||||
<span className={r.summary.missing ? "text-destructive" : undefined}>缺 {r.summary.missing}</span>
|
<span className={r.summary.missing ? "text-destructive" : undefined}>缺 {r.summary.missing}</span>
|
||||||
<span>在途 {r.summary.inflight}</span>
|
<span>下载中 {r.summary.inflight}</span>
|
||||||
{i === 0 ? null : <span>{r.autoFill ? "自动补全" : "只登记缺失"}</span>}
|
{i === 0 ? null : <span>{r.autoFill ? "自动补全" : "只登记缺失"}</span>}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -461,7 +461,11 @@ export function SubscriptionFormDialog({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{editing ? "编辑订阅" : "新建订阅"}</DialogTitle>
|
<DialogTitle>{editing ? "编辑订阅" : "新建订阅"}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{editing ? titleText : initialTarget ? titleText : "从媒体库或 TMDB 选作品;选 TMDB 条目会先加入媒体库。"}
|
{editing
|
||||||
|
? titleText
|
||||||
|
: initialTarget
|
||||||
|
? titleText
|
||||||
|
: "从媒体库或 TMDB 选作品;选 TMDB 上的作品会先加入媒体库。"}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{dup ? (
|
{dup ? (
|
||||||
@@ -483,7 +487,7 @@ export function SubscriptionFormDialog({
|
|||||||
<Tabs value={tab} onValueChange={setTab} className="min-w-0">
|
<Tabs value={tab} onValueChange={setTab} className="min-w-0">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="basic">基础</TabsTrigger>
|
<TabsTrigger value="basic">基础</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">进阶</TabsTrigger>
|
<TabsTrigger value="advanced">高级</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="basic" className="mt-4 min-w-0 space-y-5">
|
<TabsContent value="basic" className="mt-4 min-w-0 space-y-5">
|
||||||
@@ -504,7 +508,7 @@ export function SubscriptionFormDialog({
|
|||||||
{target.kind === "entity"
|
{target.kind === "entity"
|
||||||
? "媒体库作品"
|
? "媒体库作品"
|
||||||
: target.kind === "tmdb"
|
: target.kind === "tmdb"
|
||||||
? "TMDB 条目,保存时加入媒体库"
|
? "TMDB 作品,保存时加入媒体库"
|
||||||
: (target.note ?? "按标题追踪,不在媒体库")}
|
: (target.note ?? "按标题追踪,不在媒体库")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -871,7 +875,7 @@ export function SubscriptionFormDialog({
|
|||||||
id={f("tpl")}
|
id={f("tpl")}
|
||||||
value={d.policyRevisionId}
|
value={d.policyRevisionId}
|
||||||
onChange={(v) => patch({ policyRevisionId: v })}
|
onChange={(v) => patch({ policyRevisionId: v })}
|
||||||
placeholder={defaultTpl ? `按已发布收录策略(${defaultTpl.title})` : "按已发布收录策略"}
|
placeholder={defaultTpl ? `按已发布收录策略(${defaultTpl.title})` : "按已发布收录策略"}
|
||||||
options={templates.map((p) => ({
|
options={templates.map((p) => ({
|
||||||
value: p.id,
|
value: p.id,
|
||||||
label: `${p.title} r${p.revision}:${tplNames(p.id).join(" → ") || "无模板"}`,
|
label: `${p.title} r${p.revision}:${tplNames(p.id).join(" → ") || "无模板"}`,
|
||||||
@@ -930,7 +934,7 @@ export function AutoFillFields({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
<FormField label="开关" htmlFor={`${idPrefix}-af`}>
|
<FormField label="自动补全" htmlFor={`${idPrefix}-af`}>
|
||||||
<fieldset aria-label="自动补全" className="m-0 flex gap-1 border-0 p-0" id={`${idPrefix}-af`}>
|
<fieldset aria-label="自动补全" className="m-0 flex gap-1 border-0 p-0" id={`${idPrefix}-af`}>
|
||||||
{(["inherit", "on", "off"] as AutoFillMode[]).map((m) => (
|
{(["inherit", "on", "off"] as AutoFillMode[]).map((m) => (
|
||||||
<button
|
<button
|
||||||
@@ -972,7 +976,7 @@ export function AutoFillFields({
|
|||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-meta text-muted-foreground">
|
<p className="text-meta text-muted-foreground">
|
||||||
预算只算自动补全的计划,超出的延到次日;同时受全局每日预算限制。
|
预算只算自动补全发起的下载,超出的延到第二天;也受每日总预算限制。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function DiscoverPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="发现" description="TMDB、豆瓣、Bangumi 的热门与榜单;点开作品可订阅或搜索资源" />
|
<PageHeader title="发现" description="TMDB、豆瓣、Bangumi 的热门与榜单" />
|
||||||
<Tabs
|
<Tabs
|
||||||
value={view}
|
value={view}
|
||||||
onValueChange={(v) =>
|
onValueChange={(v) =>
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export default function FilesTrashPage() {
|
|||||||
if (!snap?.title) return <span className="text-muted-foreground">—</span>;
|
if (!snap?.title) return <span className="text-muted-foreground">—</span>;
|
||||||
const label = `${snap.title}${snap.contentUnit ? ` ${snap.contentUnit}` : ""}`;
|
const label = `${snap.title}${snap.contentUnit ? ` ${snap.contentUnit}` : ""}`;
|
||||||
return (
|
return (
|
||||||
<span className="block max-w-48 text-meta" title="删除时已从媒体库移除;还原不会自动恢复">
|
<span className="block max-w-48 text-meta" title="还原后不会回到媒体库,需要时重新整理">
|
||||||
{snap.mediaEntityId ? (
|
{snap.mediaEntityId ? (
|
||||||
<Link className="underline underline-offset-2" to={`/console/library/${snap.mediaEntityId}`}>
|
<Link className="underline underline-offset-2" to={`/console/library/${snap.mediaEntityId}`}>
|
||||||
{label}
|
{label}
|
||||||
@@ -227,7 +227,7 @@ export default function FilesTrashPage() {
|
|||||||
) : (
|
) : (
|
||||||
label
|
label
|
||||||
)}
|
)}
|
||||||
<span className="block text-muted-foreground">已退役</span>
|
<span className="block text-muted-foreground">已移出媒体库</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -419,7 +419,7 @@ function RestoreToDialog({
|
|||||||
<DialogContent className="sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>还原到…</DialogTitle>
|
<DialogTitle>还原到…</DialogTitle>
|
||||||
<DialogDescription>{ids.length} 项 · 选一个可写媒体库目录下的目录。同名项已存在时不覆盖。</DialogDescription>
|
<DialogDescription>{ids.length} 项 · 选一个可写的媒体库目录。同名项已存在时不覆盖。</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DirPicker roots={roots.data ?? []} value={target} onChange={setTarget} className="max-h-80" />
|
<DirPicker roots={roots.data ?? []} value={target} onChange={setTarget} className="max-h-80" />
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ import { cn, formatBytes } from "~/lib/utils.ts";
|
|||||||
|
|
||||||
const HEALTH_REASON: Record<string, string> = {
|
const HEALTH_REASON: Record<string, string> = {
|
||||||
root_missing: "挂载点不存在",
|
root_missing: "挂载点不存在",
|
||||||
sentinel_missing: "目录里没有标记文件,挂载可能没生效",
|
sentinel_missing: "目录不可访问或未挂载",
|
||||||
timeout: "探测超时",
|
timeout: "探测超时",
|
||||||
permission_denied: "没有读取权限",
|
permission_denied: "没有读取权限",
|
||||||
};
|
};
|
||||||
@@ -721,7 +721,7 @@ export default function FilesPage() {
|
|||||||
key: "scrape",
|
key: "scrape",
|
||||||
label: "刷新元数据",
|
label: "刷新元数据",
|
||||||
icon: RefreshCw,
|
icon: RefreshCw,
|
||||||
disabled: selected.every((e) => e.asset) ? null : "只对已整理的文件",
|
disabled: selected.every((e) => e.asset) ? null : "只能刷新已整理的文件",
|
||||||
onSelect: () => act("scrape", selected),
|
onSelect: () => act("scrape", selected),
|
||||||
});
|
});
|
||||||
items.push({
|
items.push({
|
||||||
@@ -854,7 +854,7 @@ export default function FilesPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
aria-label={order === "asc" ? "升序,点此改为降序" : "降序,点此改为升序"}
|
aria-label={order === "asc" ? "升序,切换为降序" : "降序,切换为升序"}
|
||||||
title={order === "asc" ? "升序" : "降序"}
|
title={order === "asc" ? "升序" : "降序"}
|
||||||
onClick={() => patch({ order: order === "asc" ? "desc" : null })}
|
onClick={() => patch({ order: order === "asc" ? "desc" : null })}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export default function ConsoleLayout() {
|
|||||||
if (session.isError) {
|
if (session.isError) {
|
||||||
return (
|
return (
|
||||||
<div className="grid min-h-screen place-items-center bg-background px-4">
|
<div className="grid min-h-screen place-items-center bg-background px-4">
|
||||||
<ErrorState title="无法获取登录状态" error={session.error} onRetry={() => session.refetch()} />
|
<ErrorState title="读取登录状态失败" error={session.error} onRetry={() => session.refetch()} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const meta = () => [{ title: "媒体库 · UHD 媒体自动化控制台"
|
|||||||
|
|
||||||
const FILTERS: { value: NonNullable<LibraryListQuery["filter"]>; label: string }[] = [
|
const FILTERS: { value: NonNullable<LibraryListQuery["filter"]>; label: string }[] = [
|
||||||
{ value: "gap", label: "有缺口" },
|
{ value: "gap", label: "有缺口" },
|
||||||
{ value: "attention", label: "需处理" },
|
{ value: "attention", label: "需要处理" },
|
||||||
{ value: "downloading", label: "下载中" },
|
{ value: "downloading", label: "下载中" },
|
||||||
{ value: "satisfied", label: "已满足" },
|
{ value: "satisfied", label: "已满足" },
|
||||||
];
|
];
|
||||||
@@ -27,7 +27,7 @@ const FILTERS: { value: NonNullable<LibraryListQuery["filter"]>; label: string }
|
|||||||
/** 各 Slot 状态计数:只列非零项,文字 + 语气 */
|
/** 各 Slot 状态计数:只列非零项,文字 + 语气 */
|
||||||
function SlotSummary({ counts }: { counts: LibraryItem["slotCounts"] }) {
|
function SlotSummary({ counts }: { counts: LibraryItem["slotCounts"] }) {
|
||||||
const parts = SLOT_STATUS_ORDER.filter((s) => counts[s] > 0);
|
const parts = SLOT_STATUS_ORDER.filter((s) => counts[s] > 0);
|
||||||
if (parts.length === 0) return <span className="text-meta text-muted-foreground">未设目标</span>;
|
if (parts.length === 0) return <span className="text-meta text-muted-foreground">没有收录版本</span>;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{parts.map((s: SlotStatus) => (
|
{parts.map((s: SlotStatus) => (
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ import { reasonCode, reasonLine } from "~/lib/reasons.ts";
|
|||||||
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
|
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||||
|
|
||||||
const FILE_STATUS: Record<PublicOrganizationFile["status"], { label: string; tone: StatusTone }> = {
|
const FILE_STATUS: Record<PublicOrganizationFile["status"], { label: string; tone: StatusTone }> = {
|
||||||
queued: { label: "排队", tone: "neutral" },
|
queued: { label: "排队中", tone: "neutral" },
|
||||||
running: { label: "执行中", tone: "neutral" },
|
running: { label: "整理中", tone: "neutral" },
|
||||||
succeeded: { label: "成功", tone: "success" },
|
succeeded: { label: "成功", tone: "success" },
|
||||||
failed: { label: "失败", tone: "destructive" },
|
failed: { label: "失败", tone: "destructive" },
|
||||||
skipped: { label: "跳过", tone: "neutral" },
|
skipped: { label: "已跳过", tone: "neutral" },
|
||||||
cancelled: { label: "已取消", tone: "neutral" },
|
cancelled: { label: "已取消", tone: "neutral" },
|
||||||
};
|
};
|
||||||
const ROLE_LABEL = {
|
const ROLE_LABEL = {
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="设置" description="按接入对象和业务环节分组。密钥保存后不再显示。" />
|
<PageHeader title="设置" description="密钥保存后不再显示。" />
|
||||||
<div className="grid gap-6 lg:grid-cols-[14rem_minmax(0,1fr)] lg:gap-8">
|
<div className="grid gap-6 lg:grid-cols-[14rem_minmax(0,1fr)] lg:gap-8">
|
||||||
{/* 窄屏:搜索 + 分区下拉 + 节横排 */}
|
{/* 窄屏:搜索 + 分区下拉 + 节横排 */}
|
||||||
<div className="grid gap-3 lg:hidden">
|
<div className="grid gap-3 lg:hidden">
|
||||||
@@ -114,7 +114,7 @@ export default function SettingsPage() {
|
|||||||
</Select>
|
</Select>
|
||||||
<p className="text-caption text-muted-foreground">{section.description}</p>
|
<p className="text-caption text-muted-foreground">{section.description}</p>
|
||||||
{section.anchors.length > 1 ? (
|
{section.anchors.length > 1 ? (
|
||||||
<nav aria-label="本分区的节" className="scroll-bleed -mx-1 overflow-x-auto px-1 pb-1">
|
<nav aria-label="本分区目录" className="scroll-bleed -mx-1 overflow-x-auto px-1 pb-1">
|
||||||
<ul className="flex w-max gap-1.5">
|
<ul className="flex w-max gap-1.5">
|
||||||
{section.anchors.map((a) => (
|
{section.anchors.map((a) => (
|
||||||
<li key={a.id}>
|
<li key={a.id}>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import { errorMessage } from "~/lib/api.ts";
|
|||||||
const RESOLUTION: { value: ResolutionStatus; label: string }[] = [
|
const RESOLUTION: { value: ResolutionStatus; label: string }[] = [
|
||||||
{ value: "resolved", label: "已识别" },
|
{ value: "resolved", label: "已识别" },
|
||||||
{ value: "unresolved", label: "未识别" },
|
{ value: "unresolved", label: "未识别" },
|
||||||
{ value: "ambiguous", label: "有歧义" },
|
{ value: "ambiguous", label: "匹配到多部作品" },
|
||||||
{ value: "unidentifiable", label: "无法识别" },
|
{ value: "unidentifiable", label: "无法识别" },
|
||||||
];
|
];
|
||||||
const AGE = [
|
const AGE = [
|
||||||
@@ -207,7 +207,6 @@ export default function SiteWorkbenchPage() {
|
|||||||
<EmptyState
|
<EmptyState
|
||||||
icon={Globe}
|
icon={Globe}
|
||||||
title="站点不存在或已删除"
|
title="站点不存在或已删除"
|
||||||
description="回到站点页查看现有站点"
|
|
||||||
action={
|
action={
|
||||||
<Button asChild size="sm" variant="outline">
|
<Button asChild size="sm" variant="outline">
|
||||||
<Link to={consolePath("sites")}>
|
<Link to={consolePath("sites")}>
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ type Filter = "all" | "enabled" | "attention";
|
|||||||
const GROUPS: { kind: SiteKind; title: string; variant: "pt" | "public" | "subtitle" }[] = [
|
const GROUPS: { kind: SiteKind; title: string; variant: "pt" | "public" | "subtitle" }[] = [
|
||||||
{ kind: "pt", title: "PT 站", variant: "pt" },
|
{ kind: "pt", title: "PT 站", variant: "pt" },
|
||||||
{ kind: "public_bt", title: "公开 BT 站", variant: "public" },
|
{ kind: "public_bt", title: "公开 BT 站", variant: "public" },
|
||||||
{ kind: "rss_feed", title: "RSS 源", variant: "public" },
|
{ kind: "rss_feed", title: "自定义 RSS 源", variant: "public" },
|
||||||
{ kind: "torznab", title: "索引器", variant: "public" },
|
{ kind: "torznab", title: "Torznab 索引器", variant: "public" },
|
||||||
{ kind: "subtitle", title: "字幕站", variant: "subtitle" },
|
{ kind: "subtitle", title: "字幕站", variant: "subtitle" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const FILTERS: { value: Filter; label: string }[] = [
|
const FILTERS: { value: Filter; label: string }[] = [
|
||||||
{ value: "all", label: "全部" },
|
{ value: "all", label: "全部" },
|
||||||
{ value: "enabled", label: "已启用" },
|
{ value: "enabled", label: "已启用" },
|
||||||
{ value: "attention", label: "需处理" },
|
{ value: "attention", label: "需要处理" },
|
||||||
];
|
];
|
||||||
const SORTS: { value: SortKey; label: string }[] = [
|
const SORTS: { value: SortKey; label: string }[] = [
|
||||||
{ value: "name", label: "名称" },
|
{ value: "name", label: "名称" },
|
||||||
@@ -193,7 +193,7 @@ export default function SitesPage() {
|
|||||||
) : !shown.length ? (
|
) : !shown.length ? (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={Globe}
|
icon={Globe}
|
||||||
title={filter === "attention" ? "没有需处理的站点" : "没有已启用的站点"}
|
title={filter === "attention" ? "没有需要处理的站点" : "没有已启用的站点"}
|
||||||
description="切换到「全部」查看其余站点"
|
description="切换到「全部」查看其余站点"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ function problems(d: SystemHealth): string[] {
|
|||||||
const disks = d.disks.filter((x) => x.level === "warn" || x.level === "critical");
|
const disks = d.disks.filter((x) => x.level === "warn" || x.level === "critical");
|
||||||
if (disks.length) out.push(`${disks.length} 个磁盘用量超过阈值`);
|
if (disks.length) out.push(`${disks.length} 个磁盘用量超过阈值`);
|
||||||
const mounts = d.mounts.filter((m) => m.status !== "online");
|
const mounts = d.mounts.filter((m) => m.status !== "online");
|
||||||
if (mounts.length) out.push(`${mounts.length} 个挂载根不可用`);
|
if (mounts.length) out.push(`${mounts.length} 个挂载点不可用`);
|
||||||
if (d.engine.level === "warn" || d.engine.level === "critical") out.push("任务积压或失败率偏高");
|
if (d.engine.level === "warn" || d.engine.level === "critical") out.push("任务积压或失败率偏高");
|
||||||
if (d.schedules.failing.length) out.push(`${d.schedules.failing.length} 个定时任务上次运行失败`);
|
if (d.schedules.failing.length) out.push(`${d.schedules.failing.length} 个定时任务上次运行失败`);
|
||||||
if (d.storage.level === "warn" || d.storage.level === "critical") out.push("存储用量超过告警线或预算,见「存储」");
|
if (d.storage.level === "warn" || d.storage.level === "critical") out.push("存储用量超过告警线或预算,见「存储」");
|
||||||
@@ -471,7 +471,7 @@ function worst(levels: HealthLevel[]): HealthLevel {
|
|||||||
function Mounts({ d }: { d: SystemHealth }) {
|
function Mounts({ d }: { d: SystemHealth }) {
|
||||||
const bad = d.mounts.filter((m) => m.status !== "online").length;
|
const bad = d.mounts.filter((m) => m.status !== "online").length;
|
||||||
return (
|
return (
|
||||||
<Card title="挂载根" icon={HardDrive} level={d.mounts.length ? (bad ? "critical" : "ok") : undefined}>
|
<Card title="挂载点" icon={HardDrive} level={d.mounts.length ? (bad ? "critical" : "ok") : undefined}>
|
||||||
{d.mounts.length === 0 ? (
|
{d.mounts.length === 0 ? (
|
||||||
<p className="text-caption text-muted-foreground">没有启用的媒体库目录、下载目录映射或 Alist 存储。</p>
|
<p className="text-caption text-muted-foreground">没有启用的媒体库目录、下载目录映射或 Alist 存储。</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ export const meta = () => [{ title: "订阅详情 · UHD 媒体自动化控制
|
|||||||
|
|
||||||
const TOTAL_SOURCE: Record<string, string> = {
|
const TOTAL_SOURCE: Record<string, string> = {
|
||||||
tmdb: "TMDB",
|
tmdb: "TMDB",
|
||||||
injected: "注入",
|
injected: "订阅里填写",
|
||||||
manual: "手动",
|
manual: "手动",
|
||||||
covered_floor: "按已整理",
|
covered_floor: "按已整理",
|
||||||
previous: "沿用",
|
previous: "沿用上次",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SubscriptionDetailPage() {
|
export default function SubscriptionDetailPage() {
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ const OP_TONE: Record<AssetOperation["status"], StatusTone> = {
|
|||||||
compensated: "warning",
|
compensated: "warning",
|
||||||
};
|
};
|
||||||
const OP_STATUS: Record<AssetOperation["status"], string> = {
|
const OP_STATUS: Record<AssetOperation["status"], string> = {
|
||||||
planned: "计划",
|
planned: "待执行",
|
||||||
started: "进行中",
|
started: "进行中",
|
||||||
succeeded: "成功",
|
succeeded: "成功",
|
||||||
failed: "失败",
|
failed: "失败",
|
||||||
skipped: "跳过",
|
skipped: "已跳过",
|
||||||
compensated: "已回滚",
|
compensated: "已回滚",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -255,9 +255,7 @@ export default function LoginPage() {
|
|||||||
<h1 className="mt-3 max-w-xl font-display text-display-sm leading-tight font-semibold text-foreground">
|
<h1 className="mt-3 max-w-xl font-display text-display-sm leading-tight font-semibold text-foreground">
|
||||||
订阅、下载、整理
|
订阅、下载、整理
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-4 max-w-lg text-lead text-muted-foreground">
|
<p className="mt-4 max-w-lg text-lead text-muted-foreground">每一步都有记录。拿不准的会停下来等你决定。</p>
|
||||||
每一步都有记录。拿不准的事项进待处理,等你决定。
|
|
||||||
</p>
|
|
||||||
<ul className="mt-8 space-y-3 text-body text-muted-foreground">
|
<ul className="mt-8 space-y-3 text-body text-muted-foreground">
|
||||||
<li className="flex items-center gap-2.5">
|
<li className="flex items-center gap-2.5">
|
||||||
<ShieldCheck className="h-4 w-4 text-foreground/70" />
|
<ShieldCheck className="h-4 w-4 text-foreground/70" />
|
||||||
@@ -456,7 +454,7 @@ export default function LoginPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
{session.isError ? (
|
{session.isError ? (
|
||||||
<p className="text-center text-meta text-muted-foreground">
|
<p className="text-center text-meta text-muted-foreground">
|
||||||
无法获取登录状态,
|
读取登录状态失败,
|
||||||
<button type="button" className="cursor-pointer underline" onClick={() => session.refetch()}>
|
<button type="button" className="cursor-pointer underline" onClick={() => session.refetch()}>
|
||||||
重试
|
重试
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in new issue
Block a user