import type { EpisodeMappingPreview, EpisodeMappingPreviewInput, EpisodeMappingRuleRow, EpisodeMappingRuleSpec, EpisodeSpecialKind, EpisodeSuggestion, } from "@umt/contracts"; import { Pause, PencilLine, Play, Plus, Trash2, Wand2 } from "lucide-react"; import { useEffect, useId, useMemo, useRef, useState } from "react"; import { useLocation, useSearchParams } from "react-router"; import { toast } from "sonner"; import { useEpisodeMappingPreview, useEpisodeMappingRules, useEpisodeMappingWrite } from "~/api/episode-mapping.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { FormField } from "~/components/console/form-field.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { Chip, Panel } from "~/components/library/parts.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Input } from "~/components/ui/input.tsx"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx"; import { Textarea } from "~/components/ui/textarea.tsx"; import { errorMessage } from "~/lib/api.ts"; import { actorText } from "~/lib/labels.ts"; import { agentRuleInfo, disabledReasonText, draftFromSearch, draftOfRule, EMPTY_DRAFT, groupMethodsText, MAPPING_ANCHOR, RULE_KIND_LABEL, type RuleDraft, reresolveToast, ruleText, SPECIAL_LABEL, scopeText, specOfDraft, } from "./model.ts"; import { PreviewTable } from "./preview-table.tsx"; // 作品页「集数映射」分区(批次 4):季表摘要、规则列表(增改停删)、草稿规则边改边预览、应用后提示重算了多少条。 /** 草稿改动后等这么久再预览 */ const PREVIEW_DEBOUNCE_MS = 400; function useDebounced(value: T, ms: number): T { const [v, setV] = useState(value); useEffect(() => { const t = setTimeout(() => setV(value), ms); return () => clearTimeout(t); }, [value, ms]); return v; } function SeasonSummary({ p }: { p: EpisodeMappingPreview["seasons"] }) { const main = p.seasons.filter((s) => s.season >= 1); if (!main.length) return

还没有季表,刷新元数据后出现。

; return (
{main.map((s) => ( 第 {s.season} 季 · {s.total != null ? `${s.total} 集` : "总集数未知"} {s.season === p.latestSeason ? " · 最新" : ""} ))} {p.ended ? "已完结" : "未完结"} {p.structure ? ` · 按剧集组「${p.structure.name}」分季` : ""}
); } function RuleRow({ rule, editing, onEdit, write, }: { rule: EpisodeMappingRuleRow; editing: boolean; onEdit: () => void; write: ReturnType; }) { const reason = disabledReasonText(rule); const agent = agentRuleInfo(rule); const run = (vars: Parameters[0], verb: string) => write.mutateAsync(vars).then( (r) => toast.success(reresolveToast(r.reresolve, verb)), (e) => { toast.error(errorMessage(e)); throw e; }, ); return (
  • {scopeText(rule.season, rule.releaseGroup)} {rule.status === "active" ? "启用" : "停用"} {agent ? {agent.label} : null} {editing ? 修改中 : null}
    {RULE_KIND_LABEL[rule.kind]}:{ruleText(rule)} {agent ? null : ( <> {" · "} {actorText(rule.createdById ? `${rule.createdByType}:${rule.createdById}` : rule.createdByType)} )}
    {agent?.reasons.length ? (
    依据:{agent.reasons.join(";")}
    ) : null} {reason ?
    {reason}
    : null}
    {rule.status === "active" ? ( ) : ( )} } title="删除这条规则?" description={`删除后按作品重算:「${scopeText(rule.season, rule.releaseGroup)}」的资源回到自动判定。不能恢复。`} confirmLabel="删除" destructive onConfirm={() => run({ op: "delete", id: rule.id }, "已删除")} />
  • ); } function DraftEditor({ draft, onChange, error, groups, }: { draft: RuleDraft; onChange: (d: RuleDraft) => void; error: string | null; groups: string[]; }) { const id = useId(); const f = (k: string) => `${id}-${k}`; const set = (patch: Partial) => onChange({ ...draft, ...patch }); return (
    set({ season: e.target.value })} /> set({ releaseGroup: e.target.value })} /> {groups.map((g) => ( {draft.kind === "expression" ? ( set({ targetSeason: e.target.value })} /> ) : draft.kind === "special" ? ( ) : (
    )} {draft.kind === "expression" ? ( set({ expression: e.target.value })} aria-invalid={!!error} /> ) : draft.kind === "table" ? (