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 15:45:12 +08:00
1 parent 265340751c
commit 7d5ba35280
9 files changed
+827 -138

No files matched your search

+54
View File
@@ -1,11 +1,14 @@
import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
EpisodeGroupList,
EpisodeGroupSetResult,
EpisodeMappingPreview,
type EpisodeMappingPreviewInput,
type EpisodeMappingRuleCreate,
EpisodeMappingRuleList,
type EpisodeMappingRuleUpdate,
EpisodeMappingRuleWriteResult,
EpisodeStructureSeasons,
} from "@umt/contracts";
import { api } from "~/lib/api.ts";
import { decisionKeys } from "./decisions.ts";
@@ -18,6 +21,9 @@ export const episodeMappingKeys = {
rules: (entityId: string) => ["episode-mapping", "rules", entityId] as const,
preview: (entityId: string, input: EpisodeMappingPreviewInput) =>
["episode-mapping", "preview", entityId, input] as const,
groups: (entityId: string) => ["episode-mapping", "groups", entityId] as const,
structure: (entityId: string, groupId: string | null) =>
["episode-mapping", "structure", entityId, groupId ?? "default"] as const,
};
export function useEpisodeMappingRules(entityId: string | undefined) {
@@ -73,3 +79,51 @@ export function useEpisodeMappingWrite(entityId: string) {
},
});
}
// ── 剧集组(季集结构) ──
/** 作品可选的 TMDB 剧集组与当前选择 */
export function useEpisodeGroups(entityId: string | undefined, enabled = true) {
return useQuery({
queryKey: episodeMappingKeys.groups(entityId ?? ""),
enabled: !!entityId && enabled,
staleTime: 5 * 60_000,
queryFn: ({ signal }) =>
getParsed(EpisodeGroupList, `/media-entities/${entityId}/episode-groups`, undefined, signal),
});
}
/** 某套季集结构(null = TMDB 默认季)的季卡片与每集,带本地已入库 */
export function useEpisodeStructure(entityId: string | undefined, groupId: string | null, enabled = true) {
return useQuery({
queryKey: episodeMappingKeys.structure(entityId ?? "", groupId),
enabled: !!entityId && enabled,
placeholderData: keepPreviousData,
queryFn: ({ signal }) =>
getParsed(
EpisodeStructureSeasons,
`/media-entities/${entityId}/episode-structure`,
groupId ? { groupId } : undefined,
signal,
),
});
}
/** 切换作品的季集结构:成功后刷新剧集组、预览、规则、待处理与作品详情(季集都变了) */
export function useSetEpisodeGroup(entityId: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: async (groupId: string | null) =>
parseOrThrow(
EpisodeGroupSetResult,
await api.put<unknown>(`/media-entities/${entityId}/episode-group`, { groupId }),
"/media-entities/{id}/episode-group",
),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: episodeMappingKeys.all });
void qc.invalidateQueries({ queryKey: decisionKeys.all });
void qc.invalidateQueries({ queryKey: libraryKeys.all });
void qc.invalidateQueries({ queryKey: ["subscriptions"] });
},
});
}