import { Pause, Play, ShieldAlert } from "lucide-react"; import type { ReactNode } from "react"; import { toast } from "sonner"; import { useUpdateSettings } from "~/api/settings.ts"; import { Switch } from "~/components/ui/switch.tsx"; import { errorMessage } from "~/lib/api.ts"; import { cn } from "~/lib/utils.ts"; import { num } from "./resource-section.tsx"; import { SettingsBlock } from "./settings-block.tsx"; import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx"; // 自动化分区:总开关、只读对账、缓存水位、每日下载预算、保留天数 const GiB = 1024 ** 3; const CACHE_CARD: CardSpec = { anchor: "cache", title: "缓存水位", description: "缓存卷使用率到硬阈值时停止新增下载并暂停纳管任务;降到恢复水位以下只恢复因阈值暂停的任务。", fields: [ { name: "cache.hardThresholdPercent", label: "硬阈值(%)", kind: "number" }, { name: "cache.resumePercent", label: "恢复水位(%)", kind: "number", hint: "低于硬阈值" }, { name: "cache.cleanupTargetPercent", label: "清理目标(%)", kind: "number", hint: "不高于恢复水位" }, { name: "cache.checkIntervalSec", label: "检查间隔(秒)", kind: "number", hint: "10–3600" }, ], fromSettings: (s) => ({ "cache.hardThresholdPercent": n(s.cache.hardThresholdPercent), "cache.resumePercent": n(s.cache.resumePercent), "cache.cleanupTargetPercent": n(s.cache.cleanupTargetPercent), "cache.checkIntervalSec": n(s.cache.checkIntervalSec), }), toPatch: (v) => ({ cache: { hardThresholdPercent: num(v["cache.hardThresholdPercent"]), resumePercent: num(v["cache.resumePercent"]), cleanupTargetPercent: num(v["cache.cleanupTargetPercent"]), checkIntervalSec: num(v["cache.checkIntervalSec"]), }, }), }; const BUDGET_CARD: CardSpec = { anchor: "budget", title: "每日预算", description: "任一项到上限后不再建新的下载计划;前两项按自然日计。", fields: [ { name: "budget.maxNewTitlesPerDay", label: "每日新作品数", kind: "number" }, { name: "budget.maxDownloadGiBPerDay", label: "每日下载量(GiB)", kind: "number" }, { name: "budget.maxConcurrentDownloads", label: "最大并发下载", kind: "number", hint: "1–100" }, ], fromSettings: (s) => ({ "budget.maxNewTitlesPerDay": n(s.budget.maxNewTitlesPerDay), "budget.maxDownloadGiBPerDay": n(Math.round((s.budget.maxDownloadBytesPerDay / GiB) * 100) / 100), "budget.maxConcurrentDownloads": n(s.budget.maxConcurrentDownloads), }), toPatch: (v) => { const gib = num(v["budget.maxDownloadGiBPerDay"]); return { budget: { maxNewTitlesPerDay: num(v["budget.maxNewTitlesPerDay"]), maxDownloadBytesPerDay: gib === undefined ? undefined : Math.round(gib * GiB), maxConcurrentDownloads: num(v["budget.maxConcurrentDownloads"]), }, }; }, }; const RETENTION_CARD: CardSpec = { anchor: "retention", title: "保留", fields: [ { name: "retention.seedRetentionDays", label: "源文件保留天数", kind: "number", hint: "复制入库后下载源在缓存里至少留这么久;0–365", }, { name: "retention.logRetentionDays", label: "日志保留天数", kind: "number", hint: "观察与日志类数据" }, ], fromSettings: (s) => ({ "retention.seedRetentionDays": n(s.retention.seedRetentionDays), "retention.logRetentionDays": n(s.retention.logRetentionDays), }), toPatch: (v) => ({ retention: { seedRetentionDays: num(v["retention.seedRetentionDays"]), logRetentionDays: num(v["retention.logRetentionDays"]), }, }), }; function ToggleRow({ id, icon, title, description, checked, onChange, disabled, tone, }: { id: string; icon: ReactNode; title: string; description: string; checked: boolean; onChange: (v: boolean) => void; disabled: boolean; tone: "success" | "warning"; }) { return (
{icon} {description}
); } function Switches() { const update = useUpdateSettings(); const set = async (patch: { enabled?: boolean; readOnlyReconcile?: boolean }, msg: string) => { try { await update.mutateAsync({ automation: patch }); toast.success(msg); } catch (err) { toast.error(errorMessage(err, "切换失败")); } }; return ( {(s) => (
: } tone={s.automation.enabled ? "success" : "warning"} title={`自动化总开关 · ${s.automation.enabled ? "运行中" : "已暂停"}`} description="关闭后自动收录、下载、整理全部停止;查询与手动操作不受影响。" checked={s.automation.enabled} disabled={update.isPending} onChange={(v) => set({ enabled: v }, v ? "自动化已恢复" : "自动化已暂停")} /> } tone={s.automation.readOnlyReconcile ? "warning" : "success"} title={`只读对账 · ${s.automation.readOnlyReconcile ? "开启" : "关闭"}`} description="从备份恢复后先开启:只核对库存与路径,不做自动写入。核对完再关闭。" checked={s.automation.readOnlyReconcile} disabled={update.isPending} onChange={(v) => set({ readOnlyReconcile: v }, v ? "已开启只读对账" : "已关闭只读对账")} />
)}
); } export function AutomationSection() { return (
{(s) => ( <> )}
); }