feat(web): 发现页改为「推荐」分区 +「探索」筛选,卡片带本地状态与订阅 / 搜索资源

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-06 18:09:50 +08:00
1 parent 9a154bc249
commit 740e2fe67d
9 files changed
+1422 -211

No files matched your search

+44 -15
View File
@@ -1,30 +1,59 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { DiscoverCalendar, type DiscoverItem, DiscoverList, DiscoverMatchResult } from "@umt/contracts";
import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
import { DiscoverCalendar, type DiscoverItem, DiscoverMatchResult, DiscoverPage } from "@umt/contracts";
import type { ApiQuery } from "~/components/discover/discover-state.ts";
import { api } from "~/lib/api.ts";
import { getParsed, parseOrThrow } from "./library.ts";
// 发现:bgm.tv 每日放送、豆瓣热门(服务端缓存 1 小时);点条目时匹配 TMDB 作品。
// 发现:TMDB / 豆瓣 / bgm 列表(服务端走请求缓存);点豆瓣 / bgm 条目时匹配 TMDB 作品。
const STALE = 10 * 60_000;
export const discoverKeys = {
calendar: ["discover", "calendar"] as const,
hot: (kind: "movie" | "tv") => ["discover", "douban-hot", kind] as const,
list: (q: ApiQuery) => ["discover", q] as const,
};
async function fetchCalendar(signal?: AbortSignal) {
return getParsed(DiscoverCalendar, "/discover/bangumi-calendar", {}, signal);
}
/** 一页;每日放送取那一天,不分页 */
export async function fetchDiscoverPage(q: ApiQuery, page: number, signal?: AbortSignal): Promise<DiscoverPage> {
if (q.kind === "calendar") {
const cal = await fetchCalendar(signal);
const items = cal.days.find((d) => String(d.weekday) === q.weekday)?.items ?? [];
return { items, page: 1, hasMore: false };
}
return getParsed(DiscoverPage, q.path, { ...q.params, page }, signal);
}
/** 推荐分区:只取第一页;enabled 由分区是否进入视口决定 */
export function useDiscoverSection(q: ApiQuery, enabled: boolean) {
return useQuery({
queryKey: [...discoverKeys.list(q), 1],
enabled,
queryFn: ({ signal }) => fetchDiscoverPage(q, 1, signal),
staleTime: STALE,
});
}
/** 探索:无限滚动 */
export function useDiscoverInfinite(q: ApiQuery) {
return useInfiniteQuery({
queryKey: [...discoverKeys.list(q), "all"],
initialPageParam: 1,
queryFn: ({ pageParam, signal }) => fetchDiscoverPage(q, pageParam, signal),
getNextPageParam: (last) => (last.hasMore ? last.page + 1 : undefined),
staleTime: STALE,
});
}
export function useBangumiCalendar(enabled = true) {
return useQuery({
queryKey: discoverKeys.calendar,
enabled,
queryFn: ({ signal }) => getParsed(DiscoverCalendar, "/discover/bangumi-calendar", {}, signal),
staleTime: 30 * 60_000,
});
}
export function useDoubanHot(kind: "movie" | "tv", enabled = true) {
return useQuery({
queryKey: discoverKeys.hot(kind),
enabled,
queryFn: ({ signal }) => getParsed(DiscoverList, "/discover/douban-hot", { kind }, signal),
staleTime: 30 * 60_000,
queryFn: ({ signal }) => fetchCalendar(signal),
staleTime: STALE,
});
}