import { useQuery } from "@tanstack/react-query"; import { type InventoryEntry, InventoryResponse, LibraryDetail } from "@umt/contracts"; import { apiParsed } from "./parse.ts"; // 站点工作台库存叠加(W2-C:/inventory?entityIds=,最多 200 个) export function useInventory(entityIds: string[]) { const ids = [...new Set(entityIds)].sort().slice(0, 200); return useQuery({ queryKey: ["inventory", ids], enabled: ids.length > 0, queryFn: ({ signal }) => apiParsed(InventoryResponse, "/api/v1/inventory", { params: { entityIds: ids.join(",") }, signal }), select: (d) => new Map(d.items.map((i) => [i.entityId, i])), }); } /** 叠加标记:只来自本地身份/Slot/任务,不从网页标题猜确定事实 */ export type OverlayFlag = { key: string; label: string; tone: "success" | "warning" | "neutral"; title?: string }; const DECISION_FLAG: Record = { accept: { label: "已选中", tone: "success" }, upgrade: { label: "可升级", tone: "success" }, reject: { label: "未选", tone: "warning" }, unknown_field: { label: "字段未知", tone: "warning" }, skipped: { label: "被跳过", tone: "neutral" }, deferred: { label: "延后", tone: "neutral" }, }; /** 该资源在各 Slot 上的最近一次评估(服务端每个 (Slot, 资源) 只保留一条) */ export function releaseCandidates(entry: InventoryEntry | undefined, releaseId: string) { return entry?.candidateReasons.filter((c) => c.sourceReleaseId === releaseId) ?? []; } export function inventoryFlags( entry: InventoryEntry | undefined, releaseResolution?: string | null, releaseId?: string, ): OverlayFlag[] { if (!entry) return []; const flags: OverlayFlag[] = []; if (entry.subscribed) flags.push({ key: "sub", label: "已订阅", tone: "neutral" }); const available = entry.versions.filter((v) => v.available); if (available.length) { const res = [...new Set(available.map((v) => v.resolution ?? "未知分辨率"))].join(" / "); flags.push({ key: "have", label: `已有 ${res}`, tone: "success" }); } const has4k = available.some((v) => /2160|4k|uhd/i.test(v.resolution ?? "")); const wants4k = entry.gaps.some((g) => /2160|4k|uhd/i.test(g.name)); if (wants4k && !has4k) flags.push({ key: "no4k", label: "缺 4K", tone: "warning" }); for (const g of entry.gaps.filter((g) => !/2160|4k|uhd/i.test(g.name) || has4k)) { flags.push({ key: `gap-${g.slotId}`, label: `缺口:${g.name}`, tone: "warning" }); } if (entry.downloading.length) { flags.push({ key: "dl", label: `下载中 ${entry.downloading.length}`, tone: "neutral" }); } for (const c of releaseId ? releaseCandidates(entry, releaseId) : []) { const d = DECISION_FLAG[c.decision] ?? { label: c.decision, tone: "neutral" as const }; flags.push({ key: `cand-${c.slotId}`, label: `${c.slotName}:${d.label}`, tone: d.tone, title: c.reasons.join(";"), }); } if (releaseResolution && releaseResolution === "resolved" && flags.length === 0) { flags.push({ key: "tracked", label: "已识别,无收录版本", tone: "neutral" }); } return flags; } /** 作品详情(候选理由、Slot、下载历史);工作台详情抽屉用它回答"为什么选/不选" */ export function useEntityDetail(entityId: string | null | undefined) { return useQuery({ queryKey: ["library", "detail", entityId ?? ""], enabled: !!entityId, queryFn: ({ signal }) => apiParsed(LibraryDetail, "/api/v1/library/:id", { pathParams: { id: entityId ?? "" }, signal }), }); }