import type { IdentityPoolItem, PoolCandidate } from "@umt/contracts"; import { ExternalLink, LoaderCircle, RotateCw } from "lucide-react"; import { type FormEvent, useState } from "react"; import { Link } from "react-router"; import { toast } from "sonner"; import { z } from "zod"; import { useAssignPool, useDismissPool, useRetryPool } from "~/api/identity.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx"; import { FormField, zodFieldErrors } from "~/components/console/form-field.tsx"; import { JsonView } from "~/components/console/json-view.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { consolePath } from "~/components/console/nav.ts"; 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 { formatDateTime } from "~/lib/utils.ts"; import { EntityPicker, type PickedEntity } from "./identity-entity-picker.tsx"; import { EVIDENCE_LEVEL, POOL_REASON, POOL_STATUS } from "./labels.ts"; import { Chip, Tone } from "./parts.tsx"; // 待识别条目详情:候选证据 + 三种人工指定方式(候选 / 手动 TMDB id / 本地作品)+ 忽略 / 重试。指定后默认加人工锁。 const TmdbForm = z.object({ entityType: z.enum(["movie", "tv"]), externalId: z.string().trim().regex(/^\d+$/, "TMDB ID 只能是数字"), }); /** query 摘要:标题 / 年份 / 季集 */ export function querySummary(q: Record): string { const parts: string[] = []; if (q.title) parts.push(String(q.title)); if (q.year) parts.push(String(q.year)); if (q.season !== undefined && q.season !== null) parts.push(`S${String(q.season).padStart(2, "0")}`); if (q.episode !== undefined && q.episode !== null) parts.push(`E${String(q.episode).padStart(2, "0")}`); if (q.segment) parts.push(String(q.segment)); if (q.cut) parts.push(String(q.cut)); return parts.join(" · ") || "—"; } function CandidateRow({ c, index, onPick, disabled, }: { c: PoolCandidate; index: number; onPick: () => void; disabled: boolean; }) { return (
  • {index + 1}. {c.title} {c.year ? ` (${c.year})` : ""}

    {c.provider}:{c.entityType}:{c.externalId}

    {EVIDENCE_LEVEL[c.level] ?? c.level} {c.localEntityId ? ( 本地作品 ) : ( 本地未建档,指定后按 TMDB 信息建档 )}
    {c.evidence.length ? ( ) : null}
  • ); } export function PoolSheet({ item, onClose }: { item: IdentityPoolItem | null; onClose: () => void }) { const assign = useAssignPool(); const dismiss = useDismissPool(); const retry = useRetryPool(); const [tmdb, setTmdb] = useState({ entityType: "movie" as "movie" | "tv", externalId: "" }); const [tmdbErr, setTmdbErr] = useState>({}); const [entity, setEntity] = useState(null); const [note, setNote] = useState(""); if (!item) return null; const pending = item.status === "pending"; const busy = assign.isPending || dismiss.isPending || retry.isPending; const doAssign = async (body: Parameters[0], ok: string) => { try { await assign.mutateAsync(body); toast.success(ok); onClose(); } catch (err) { toast.error(errorMessage(err)); } }; const submitTmdb = (e: FormEvent) => { e.preventDefault(); const r = TmdbForm.safeParse(tmdb); if (!r.success) { setTmdbErr(zodFieldErrors(r.error.issues)); return; } setTmdbErr({}); void doAssign( { id: item.id, external: { provider: "tmdb", ...r.data }, lock: true }, `已指定 TMDB ${r.data.entityType}:${r.data.externalId} 并加人工锁`, ); }; return ( !o && onClose()} title={item.release.title} description={item.release.subtitle ?? undefined} footer={ <> 忽略 } title="忽略这条待识别资源?" description="忽略后不再自动复查,仍可按原始标题搜到。" confirmLabel="忽略" onConfirm={async () => { try { await dismiss.mutateAsync({ id: item.id, note: note.trim() || undefined }); toast.success("已忽略"); onClose(); } catch (err) { toast.error(errorMessage(err)); throw err; } }} >