import type { AcquisitionSlot } from "@umt/contracts"; import { Pencil, Plus, Target } from "lucide-react"; import { useId, useState } from "react"; import { toast } from "sonner"; import { z } from "zod"; import { useCreateSlot, useSlots, useUpdateSlot } from "~/api/slots.ts"; import { DetailSheet } from "~/components/console/detail-sheet.tsx"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { FormField } from "~/components/console/form-field.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Input } from "~/components/ui/input.tsx"; import { Switch } from "~/components/ui/switch.tsx"; import { ApiRequestError, errorMessage } from "~/lib/api.ts"; import { formatBytes } from "~/lib/utils.ts"; import { SlotCard } from "./slot-card.tsx"; import { draftFromJson, draftToJson, emptyDraft, type SlotRulesDraft, SlotRulesEditor } from "./slot-rules.tsx"; // 订阅页的 Slot 编辑:列出作品的收录目标,新建 / 编辑规则(乐观并发 expectedRevision)。 const SlotMeta = z.object({ name: z .string() .trim() .min(1, "填写名称") .max(64, "最多 64 个字符") .regex(/^[\w\-.+ ]+$/u, "只用字母、数字、空格和 - _ . +"), distinctGroup: z.string().trim().max(64, "最多 64 个字符"), }); /** 409:别人(或状态机)已改过这个 Slot */ export function conflictToast(err: unknown, onRefresh: () => void) { if (err instanceof ApiRequestError && (err.code === "conflict" || err.code === "precondition_failed")) { toast.error("规则已被修改,已刷新。在最新版本上重新编辑。", { duration: 4000 }); onRefresh(); return; } toast.error(errorMessage(err)); } export function SlotsPanel({ mediaEntityId, subscriptionId, }: { mediaEntityId: string; subscriptionId?: string | null; }) { const q = useSlots({ mediaEntityId }); const [editing, setEditing] = useState(null); const slots = q.data?.items ?? []; return (

每个版本目标单独判定和升级。这里只改规则。

{q.isPending ? ( ) : q.isError ? ( q.refetch()} /> ) : slots.length === 0 ? ( ) : (
{slots.map((s) => ( {[s.currentAsset.resolution, s.currentAsset.sourceType, s.currentAsset.releaseGroup] .filter(Boolean) .join(" · ") || "交付特征未知"} {s.currentAsset.sizeBytes ? ` · ${formatBytes(s.currentAsset.sizeBytes, 1)}` : ""} {s.currentAsset.status !== "active" ? ` · ${s.currentAsset.status}` : ""} ) : s.currentAssetId ? ( {s.currentAssetId} ) : undefined } actions={ } /> ))}
)} {editing ? ( setEditing(null)} onConflict={() => q.refetch()} /> ) : null}
); } function SlotEditorSheet({ slot, mediaEntityId, subscriptionId, onClose, onConflict, }: { slot: AcquisitionSlot | null; mediaEntityId: string; subscriptionId: string | null; onClose: () => void; onConflict: () => void; }) { const id = useId(); const create = useCreateSlot(); const update = useUpdateSlot(); const [name, setName] = useState(slot?.name ?? ""); const [distinctGroup, setDistinctGroup] = useState(slot?.distinctGroup ?? ""); const [requireDistinct, setRequireDistinct] = useState(slot?.requireDistinctAsset ?? false); const [draft, setDraft] = useState(() => (slot ? draftFromJson(slot) : emptyDraft())); const [errors, setErrors] = useState>({}); const pending = create.isPending || update.isPending; const submit = () => { const meta = SlotMeta.safeParse({ name, distinctGroup }); const rules = draftToJson(draft); const errs: Record = {}; 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 (requireDistinct && !distinctGroup.trim()) errs.distinctGroup = "要求不同物理资产时须填写分组"; setErrors(errs); if (Object.keys(errs).length || !meta.success || !rules.ok) return; const common = { ...rules.value, distinctGroup: meta.data.distinctGroup || null, requireDistinctAsset: requireDistinct, }; if (slot) { update.mutate( { id: slot.id, expectedRevision: slot.revision, ...common }, { onSuccess: (s) => { toast.success(`已保存 ${s.name}(r${s.revision})`); onClose(); }, onError: (e) => conflictToast(e, onConflict), }, ); } else { create.mutate( { mediaEntityId, subscriptionId, name: meta.data.name, ...common }, { onSuccess: (s) => { toast.success(`已新建 ${s.name}`); onClose(); }, onError: (e) => toast.error(errorMessage(e)), }, ); } }; return ( !o && !pending && onClose()} width="lg" title={slot ? `编辑版本目标 ${slot.name}` : "新建版本目标"} description={ slot ? `当前 r${slot.revision}。保存时若已被修改,会提示刷新。` : "不满足硬条件的资源不收;软偏好决定先收哪个。" } footer={ <> } >
setName(e.target.value)} /> setDistinctGroup(e.target.value)} />

要求不同物理资产

同一个文件不能同时满足两个目标

); }