import type { RuleAtom, RuleAtomCreate, RuleDefaults, RuleGroup, RuleGroupKind } from "@umt/contracts"; import { ArrowDown, ArrowUp, Ban, FlaskConical, Plus, Save, Trash2, X } from "lucide-react"; import { useEffect, useId, useMemo, useState } from "react"; import { toast } from "sonner"; import { useCreateRuleAtom, useDeleteRuleAtom, useLiftRuleSuppression, useRuleAtoms, useRuleDefaults, useRuleGroups, useRuleKeywords, useRuleSuppressions, useSaveRuleDefaults, useUpdateRuleAtom, } from "~/api/rules.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { type DataColumn, DataTable } from "~/components/console/data-table.tsx"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { FilterSelect } from "~/components/console/filter-bar.tsx"; import { FormField } from "~/components/console/form-field.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { Chip, ChipToggleGroup, Panel } from "~/components/library/parts.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog.tsx"; import { Input } from "~/components/ui/input.tsx"; import { Switch } from "~/components/ui/switch.tsx"; import { Textarea } from "~/components/ui/textarea.tsx"; import { errorMessage } from "~/lib/api.ts"; import { formatDateTime } from "~/lib/utils.ts"; import { DEFAULT_CHAIN_LABEL, RULE_KIND, suppressReason } from "./labels.ts"; // 规则页的其余 Tab:自定义条件、默认值、抑制记录 // ─── 自定义条件 ─── const MATCH_FIELD = { title: "标题", subtitle: "副标题", labels: "标签" } as const; type MatchField = keyof typeof MATCH_FIELD; type AtomForm = { key: string; name: string; description: string; include: string; exclude: string; matchFields: MatchField[]; sizeMin: string; sizeMax: string; minSeeders: string; delayMin: string; delayMax: string; free: boolean; }; const lines = (s: string) => s .split("\n") .map((x) => x.trim()) .filter(Boolean); const num = (s: string) => (s.trim() === "" ? undefined : Number(s)); const range = (min: string, max: string) => num(min) === undefined && num(max) === undefined ? null : { min: num(min), max: num(max) }; function atomForm(a: RuleAtom | null): AtomForm { return { key: a?.key ?? "", name: a?.name ?? "", description: a?.description ?? "", include: (a?.include ?? []).join("\n"), exclude: (a?.exclude ?? []).join("\n"), matchFields: a?.matchFields ?? [], sizeMin: a?.unitSizeMb?.min?.toString() ?? "", sizeMax: a?.unitSizeMb?.max?.toString() ?? "", minSeeders: a?.minSeeders?.toString() ?? "", delayMin: a?.publishDelayMinutes?.min?.toString() ?? "", delayMax: a?.publishDelayMinutes?.max?.toString() ?? "", free: a?.free ?? false, }; } function AtomDialog({ open, atom, onOpenChange, }: { open: boolean; atom: RuleAtom | null; onOpenChange: (o: boolean) => void; }) { const uid = useId(); const create = useCreateRuleAtom(); const update = useUpdateRuleAtom(); const [f, setF] = useState(() => atomForm(atom)); const [error, setError] = useState(null); // biome-ignore lint/correctness/useExhaustiveDependencies: 打开时按当前条件重置 useEffect(() => { if (open) { setF(atomForm(atom)); setError(null); } }, [open, atom?.id]); const set = (k: K, v: AtomForm[K]) => setF((x) => ({ ...x, [k]: v })); const submit = async () => { const body: RuleAtomCreate = { key: f.key.trim().toUpperCase(), name: f.name.trim() || null, description: f.description.trim() || null, include: lines(f.include), exclude: lines(f.exclude), matchFields: f.matchFields, unitSizeMb: range(f.sizeMin, f.sizeMax), minSeeders: num(f.minSeeders) ?? null, publishDelayMinutes: range(f.delayMin, f.delayMax), free: f.free, }; try { if (atom) { const { key: _k, ...rest } = body; await update.mutateAsync({ id: atom.id, ...rest }); } else await create.mutateAsync(body); toast.success(atom ? "已保存" : `已新建条件 ${body.key}`); onOpenChange(false); } catch (err) { setError(errorMessage(err, "保存失败")); } }; const pending = create.isPending || update.isPending; return ( {atom ? `编辑条件 ${atom.key}` : "新建自定义条件"} 字段同 MoviePilot 自定义规则。正则不区分大小写,多条之间是「或」。
set("key", e.target.value.toUpperCase())} className="font-mono" /> set("name", e.target.value)} /> set("description", e.target.value)} />