Found in v0.2 visual QA (docs/plan/09-visual-qa.md): - actor strings (admin_session:<uuid>, user:<uuid>, system) -> 管理员/系统/API Key in policy revisions, subscriptions, inbox, trash, business tasks, organize detail - cache volume without key shows the downloader name instead of instance:<uuid> - download timeline/plan: task and organize statuses, plan purpose, priority and status reason localized; audit gets labels for capability/files/site/feed actions - organize record reason uses conflict detail (没有可入库的主视频) instead of blocked:no_main_video; slot/candidate reasons drop inflight:<uuid> - identity decision summary no longer shows no_viable_candidate - upstream test: 'Connection error.' classified as network, with a Chinese hint - new policy draft prefills key default (placeholder looked like a value) - dialog/sheet close sr-only text in Chinese Co-Authored-By: Claude Opus 5.5 <[email protected]>
81 lines
3.5 KiB
TypeScript
81 lines
3.5 KiB
TypeScript
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<string, InventoryEntry>(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<string, { label: string; tone: OverlayFlag["tone"] }> = {
|
|
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 }),
|
|
});
|
|
}
|