import { type ContentUnit, FinalFieldValue, type UnitMetadataField } from "@umt/contracts"; import { LoaderCircle, Lock, LockOpen } from "lucide-react"; import { type FormEvent, useId, useState } from "react"; import { toast } from "sonner"; import { useLockUnitField, useUnlockUnitField } from "~/api/library.ts"; import { StatusBadge } from "~/components/console/status-badge.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 { Label } from "~/components/ui/label.tsx"; import { Textarea } from "~/components/ui/textarea.tsx"; import { errorMessage } from "~/lib/api.ts"; import { METADATA_SOURCE, UNIT_FIELD_LABEL } from "./labels.ts"; // 季 / 集字段覆盖与锁定(MD-1):改标题 / 简介 / 播出日期即锁定,元数据刷新不再改写;解锁回到来源值。 const EDITABLE: UnitMetadataField[] = ["name", "airDate", "overview"]; const pad = (n: number | null) => String(n ?? 0).padStart(2, "0"); export function finalOf(u: ContentUnit, field: string): FinalFieldValue | null { const p = FinalFieldValue.safeParse((u.metadata as Record)[field]); return p.success ? p.data : null; } export function UnitFieldDialog({ entityId, unit, onOpenChange, }: { entityId: string; /** null 表示关闭 */ unit: ContentUnit | null; onOpenChange: (o: boolean) => void; }) { const id = useId(); const lock = useLockUnitField(entityId); const unlock = useUnlockUnitField(entityId); const [values, setValues] = useState>({}); const [error, setError] = useState(null); const [lastId, setLastId] = useState(null); if ((unit?.id ?? null) !== lastId) { setLastId(unit?.id ?? null); setError(null); setValues( Object.fromEntries( EDITABLE.map((f) => { const v = unit ? finalOf(unit, f)?.value : null; return [f, typeof v === "string" ? v : ""]; }), ), ); } if (!unit) return ; const busy = lock.isPending || unlock.isPending; const code = `S${pad(unit.seasonNumber)}E${pad(unit.episodeNumber)}`; const submit = async (e: FormEvent) => { e.preventDefault(); const changed = EDITABLE.filter((f) => { const cur = finalOf(unit, f)?.value; return (values[f] ?? "").trim() !== (typeof cur === "string" ? cur : ""); }); if (!changed.length) { onOpenChange(false); return; } const date = values.airDate?.trim(); if (changed.includes("airDate") && date && !/^\d{4}-\d{2}-\d{2}$/.test(date)) { setError("播出日期格式是 YYYY-MM-DD"); return; } if (changed.includes("name") && !values.name?.trim()) { setError("标题不能为空"); return; } try { for (const f of changed) { const v = (values[f] ?? "").trim(); await lock.mutateAsync({ unitId: unit.id, field: f, value: v || null }); } toast.success(`已锁定 ${code} 的 ${changed.map((f) => UNIT_FIELD_LABEL[f]).join("、")}`); onOpenChange(false); } catch (err) { toast.error(errorMessage(err, "保存失败")); } }; const locked = EDITABLE.filter((f) => unit.fieldLocks.includes(f)); return ( !busy && onOpenChange(o)}> 修改 {code} 改过的字段会锁定,刷新元数据不再改写。编号和文件位置不变。
{EDITABLE.map((f) => { const fin = finalOf(unit, f); const isLocked = unit.fieldLocks.includes(f); return (
{fin ? ( 来源 {METADATA_SOURCE[fin.source] ?? fin.source} {fin.language ? ` · ${fin.language}` : ""} ) : null} {isLocked ? ( 已锁定 ) : null}
{f === "overview" ? (