import type { ExternalIdentity, IdentityCandidateReason, IdentityCandidateStatus, IdentityPoolItem, IdentityStatus, } from "@umt/contracts"; import { Fingerprint, Lock, SearchX } from "lucide-react"; import { useEffect, useState } from "react"; import { useExternalIdentities, useIdentityPool } from "~/api/identity.ts"; import { type DataColumn, DataTable } from "~/components/console/data-table.tsx"; import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx"; import { ExternalIdentitySheet, identityRef } from "~/components/library/identity-external-sheet.tsx"; import { PoolSheet, querySummary } from "~/components/library/identity-pool-sheet.tsx"; import { EVIDENCE_LEVEL, IDENTITY_STATUS, opts, POOL_REASON, POOL_STATUS, VERIFY_STATUS, } from "~/components/library/labels.ts"; import { relTime, Tone } from "~/components/library/parts.tsx"; // 媒体概览的身份视图(原"身份与元数据"页,IA-3 并入 /console/library):待识别资源、外部身份。 // 未识别资源:点开详情选候选、手动指定或忽略(2026-10-06 起待处理中心取消,不再转去待处理)。 function useDebounced(v: string, ms = 250) { const [d, setD] = useState(v); useEffect(() => { const t = setTimeout(() => setD(v), ms); return () => clearTimeout(t); }, [v, ms]); return d; } export function PoolTab() { const [q, setQ] = useState(""); const [status, setStatus] = useState("pending"); const [reason, setReason] = useState(""); const dq = useDebounced(q.trim()); const pool = useIdentityPool({ q: dq || undefined, status: (status || undefined) as IdentityCandidateStatus | undefined, reason: (reason || undefined) as IdentityCandidateReason | undefined, }); const [openId, setOpenId] = useState(null); const rows = pool.data?.pages.flatMap((p) => p.items); const open = rows?.find((r) => r.id === openId) ?? null; const columns: DataColumn[] = [ { key: "title", header: "原始标题", className: "max-w-0 w-full", cell: (r) => (

{r.release.title}

{r.release.subtitle ? `${r.release.subtitle} · ` : ""}解析:{querySummary(r.query)}

), }, { key: "reason", header: "原因", cell: (r) => (
), }, { key: "cands", header: "候选", hideBelow: "sm", align: "right", cell: (r) => {r.candidates.length}, }, { key: "attempts", header: "尝试", hideBelow: "md", cell: (r) => ( {r.attempts} 次 · {relTime(r.lastAttemptAt)} ), }, { key: "next", header: "下次复查", hideBelow: "lg", cell: (r) => ( {r.status === "pending" ? ( r.nextReviewAt ? ( relTime(r.nextReviewAt) ) : ( "不自动复查" ) ) : ( )} ), }, ]; const filtered = !!q || status !== "pending" || !!reason; return ( <> { setQ(""); setStatus("pending"); setReason(""); } : undefined } > r.id} loading={pool.isPending} error={pool.error} onRetry={() => pool.refetch()} onRowClick={(r) => setOpenId(r.id)} selectedKey={openId} empty={{ icon: SearchX, title: filtered ? "没有符合条件的未识别资源" : "没有未识别资源", description: "未命中订阅、证据不足或冲突的资源会出现在这里;新增相近别名时自动重试,也可手动指定。", }} hasMore={pool.hasNextPage} onLoadMore={() => pool.fetchNextPage()} loadingMore={pool.isFetchingNextPage} /> setOpenId(null)} /> ); } export function ExternalTab() { const [provider, setProvider] = useState(""); const [status, setStatus] = useState(""); const list = useExternalIdentities({ provider: provider || undefined, status: (status || undefined) as IdentityStatus | undefined, }); const [openId, setOpenId] = useState(null); const rows = list.data?.pages.flatMap((p) => p.items); const open = rows?.find((r) => r.id === openId) ?? null; const columns: DataColumn[] = [ { key: "ref", header: "外部身份", cell: (r) => ( {r.locked ? : null} {identityRef(r)} ), }, { key: "entity", header: "作品", hideBelow: "sm", cell: (r) => ( {r.mediaEntityTitle ?? 未关联} ), }, { key: "status", header: "状态", cell: (r) => }, { key: "level", header: "证据", hideBelow: "sm", cell: (r) => {EVIDENCE_LEVEL[r.evidenceLevel]}, }, { key: "verify", header: "外部校验", hideBelow: "md", cell: (r) => }, { key: "rev", header: "修订", hideBelow: "lg", align: "right", cell: (r) => ( r{r.revision} · {relTime(r.updatedAt)} ), }, ]; const filtered = !!provider || !!status; return ( <> { setProvider(""); setStatus(""); } : undefined } > r.id} loading={list.isPending} error={list.error} onRetry={() => list.refetch()} onRowClick={(r) => setOpenId(r.id)} selectedKey={openId} empty={{ icon: Fingerprint, title: "没有外部身份", description: "没有外部编号的作品也能整理。", }} hasMore={list.hasNextPage} onLoadMore={() => list.fetchNextPage()} loadingMore={list.isFetchingNextPage} /> setOpenId(null)} /> ); }