import { keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ManualAcquisitionResult, ReleaseSearchPage, type ReleaseSearchSort, type SearchReleasePlanRequest, SearchResponse, type SearchScope, } from "@umt/contracts"; import { api } from "~/lib/api.ts"; import { getParsed, parseOrThrow } from "./library.ts"; // 全局搜索(W9-G):命令面板查 media/actions(快);资源结果页走 /search/releases(筛选/排序/游标,与 /search 共用 30s 缓存)。 export const searchKeys = { all: ["search"] as const, aggregate: (q: string, scope: SearchScope, limit: number) => ["search", "aggregate", q, scope, limit] as const, releases: (f: ReleaseSearchFilters) => ["search", "releases", f] as const, }; /** 聚合搜索;q 为空不发请求 */ export function useSearch(q: string, scope: SearchScope, limit = 8, enabled = true) { const term = q.trim(); return useQuery({ queryKey: searchKeys.aggregate(term, scope, limit), enabled: enabled && term.length > 0, queryFn: ({ signal }) => getParsed(SearchResponse, "/search", { q: term, scope, limit }, signal), placeholderData: keepPreviousData, staleTime: 15_000, }); } export type ReleaseSearchFilters = { q: string; /** 逗号分隔 */ sites?: string; resolution?: string; source?: string; /** 字节 */ minSize?: number; maxSize?: number; free?: "true" | "false"; hr?: "true" | "false"; sort?: ReleaseSearchSort; order?: "asc" | "desc"; }; /** 资源结果页:游标翻页;refresh() 带 refresh=true 跳过缓存重查第一页 */ export function useReleaseSearch(filters: ReleaseSearchFilters, enabled = true) { return useInfiniteQuery({ queryKey: searchKeys.releases(filters), enabled: enabled && filters.q.trim().length > 0, initialPageParam: undefined as string | undefined, queryFn: ({ pageParam, signal }) => getParsed(ReleaseSearchPage, "/search/releases", { ...filters, cursor: pageParam, limit: 50 }, signal), getNextPageParam: (last) => last.nextCursor ?? undefined, placeholderData: keepPreviousData, staleTime: 30_000, }); } /** 跳过缓存重查站点(仍参与并发合并),结果写回当前筛选的第一页 */ export function useRefreshReleaseSearch() { const qc = useQueryClient(); return useMutation({ mutationFn: (filters: ReleaseSearchFilters) => getParsed(ReleaseSearchPage, "/search/releases", { ...filters, refresh: "true", limit: 50 }), onSuccess: (page, filters) => { qc.setQueryData(searchKeys.releases(filters), { pages: [page], pageParams: [undefined] }); void qc.invalidateQueries({ queryKey: ["search", "aggregate"] }); }, }); } /** 从搜索结果下载:与 POST /acquisition-plans 同一流程 */ export function usePlanFromSearch() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ siteId, siteReleaseId, ...body }: SearchReleasePlanRequest & { siteId: string; siteReleaseId: string }) => api .post(`/search/releases/${encodeURIComponent(siteId)}/${encodeURIComponent(siteReleaseId)}/plan`, body) .then((d) => parseOrThrow(ManualAcquisitionResult, d, "/search/releases/plan")), onSuccess: () => { void qc.invalidateQueries({ queryKey: ["plans"] }); void qc.invalidateQueries({ queryKey: ["slots"] }); void qc.invalidateQueries({ queryKey: searchKeys.all }); }, }); }