feat(web): global search — top bar entry, Ctrl/Cmd+K palette and /console/search results (FE-3)
- palette queries media + actions: local works open, TMDB candidates view/subscribe, page jumps
- results page: works / releases / actions; release filters, facets, sort, cursor, partial-site warning
- release rows: download via /search/releases/{siteId}/{siteReleaseId}/plan, delegate to Agent
- smoke: rules tabs, preferences redirect, search page
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2c6a89f309
commit
393a4cadcc
12 files changed
+1399
-1
No files matched your search
@@ -0,0 +1,93 @@
|
||||
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<unknown>(`/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 });
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user