import type { Subscription, SubscriptionCoverage, SubscriptionOverviewItem } from "@umt/contracts"; import { Check, Wand2 } from "lucide-react"; import { useEffect, useId, useMemo, useState } from "react"; import { toast } from "sonner"; import { useFillSlot, useSetSlotOrder, useUpdateSubscription } from "~/api/subscriptions.ts"; import { Panel } from "~/components/library/parts.tsx"; import { Button } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; import { AUTOFILL_SOURCE, type DraftErrors, draftFromSubscription, draftToParams, type SubscriptionDraft, } from "./model.ts"; import { SlotOrderList } from "./slot-order.tsx"; import { AutoFillFields } from "./subscription-form.tsx"; // 对比视图的"收录版本"区(设计 §2.2):拖动排序(PUT slot-order)、行尾计数、补全此版本、自动补全开关与预算。 type SlotRow = SubscriptionCoverage["rows"][number] & { id: string }; export function SlotPanel({ sub, cov, item, canFill, }: { sub: Subscription; cov: SubscriptionCoverage | undefined; item: SubscriptionOverviewItem | null | undefined; canFill: boolean; }) { const id = useId(); const setOrder = useSetSlotOrder(); const fill = useFillSlot(); const update = useUpdateSubscription(); const serverRows = useMemo( () => (cov?.rows ?? []).filter((r) => r.slotId !== null).map((r) => ({ ...r, id: r.slotId! })), [cov], ); const [rows, setRows] = useState(serverRows); useEffect(() => setRows(serverRows), [serverRows]); const dirty = rows.map((r) => r.id).join() !== serverRows.map((r) => r.id).join(); const base = useMemo(() => draftFromSubscription(sub, "off"), [sub]); const [af, setAf] = useState>(base); useEffect(() => setAf(base), [base]); const [errs, setErrs] = useState({}); const afDirty = af.autoFill !== base.autoFill || af.unitsPerDay !== base.unitsPerDay || af.gbPerDay !== base.gbPerDay; const saveOrder = () => setOrder.mutate( { id: sub.id, slotIds: rows.map((r) => r.id) }, { onSuccess: () => toast.success(`主版本:${rows[0]?.name ?? ""}`), onError: (e) => { toast.error(errorMessage(e)); setRows(serverRows); }, }, ); const fillSlot = (r: SlotRow) => fill.mutate( { id: sub.id, slotId: r.id }, { onSuccess: (x) => toast.success(x.deduplicated ? "补全任务已在排队" : `已开始补全「${r.name}」`), onError: (e) => toast.error(errorMessage(e, "补全失败")), }, ); const saveAutoFill = () => { const { params, errors } = draftToParams({ ...base, ...af }, sub.params, { upgradeChanged: false, totalChanged: false, }); setErrs(errors); if (Object.keys(errors).length) return; update.mutate( { id: sub.id, params }, { onSuccess: () => toast.success("自动补全设置已保存"), onError: (e) => toast.error(errorMessage(e)) }, ); }; return ( ) : null } > {rows.length === 0 ? (

{sub.mediaEntityId ? "还没有收录版本。在下方规则区新建。" : "作品加入媒体库后才有收录版本。"}

) : ( ( 已整理 {r.summary.have} 缺 {r.summary.missing} 下载中 {r.summary.inflight} {i === 0 ? null : {r.autoFill ? "自动补全" : "只登记缺失"}} )} /> )}

自动补全其他版本

{item ? ( 当前{item.autoFill.enabled ? "开启" : "关闭"} · 来自{AUTOFILL_SOURCE[item.autoFill.source]} ) : null}
setAf((x) => ({ ...x, ...p }))} errors={errs} idPrefix={id} /> {afDirty ? (
) : null}
); }