From 80780add667c961d080dcedcee0f04d1b9a61400 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 27 Sep 2026 23:34:13 +0800 Subject: [PATCH] feat(web): W5-C subscription progress, manual search and episode total MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - subscription sheet: 立即搜索, active-search times, 追更进度 (per-season total with source/lock, per-episode state chips, in-flight/deferred counts) and set/lock season episode total - labels for subscription pending and candidate decision deferred Co-Authored-By: Claude Opus 5.5 --- apps/web/app/api/subscriptions.ts | 30 ++++ apps/web/app/components/library/labels.ts | 2 + .../workbench/release-detail-sheet.tsx | 1 + apps/web/app/routes/console/subscriptions.tsx | 143 +++++++++++++++++- 4 files changed, 175 insertions(+), 1 deletion(-) diff --git a/apps/web/app/api/subscriptions.ts b/apps/web/app/api/subscriptions.ts index 2c39e93b..cb90630f 100644 --- a/apps/web/app/api/subscriptions.ts +++ b/apps/web/app/api/subscriptions.ts @@ -1,10 +1,12 @@ import { keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { cursorPage, + type EpisodeTotalSet, MediaEntity, Subscription, type SubscriptionCreate, type SubscriptionMediaKind, + SubscriptionProgress, type SubscriptionStatus, type SubscriptionUpdate, } from "@umt/contracts"; @@ -70,3 +72,31 @@ export function useEntityTitle(id: string | null | undefined) { queryFn: ({ signal }) => getParsed(MediaEntity, `/media-entities/${id}`, undefined, signal), }); } + +/** 订阅进度:季总集数账本 + 每集状态(已覆盖/在途/缺失/疑似缺集)+ Slot 在途与延后(W5-C) */ +export function useSubscriptionProgress(id: string | null | undefined, enabled = true) { + return useQuery({ + queryKey: ["subscriptions", "progress", id ?? ""], + enabled: !!id && enabled, + queryFn: ({ signal }) => getParsed(SubscriptionProgress, `/subscriptions/${id}/progress`, undefined, signal), + }); +} + +/** 手动主动搜索:按标题/别名在已开启站点搜索,结果走同一识别评估链 */ +export function useSearchSubscription() { + return useMutation({ + mutationFn: (id: string) => api.post<{ runId: string; deduplicated: boolean }>(`/subscriptions/${id}/search`), + }); +} + +/** 手动设置季总集数(不锁定时只升;锁定后元数据刷新不再改写) */ +export function useSetEpisodeTotal() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: ({ id, ...body }: EpisodeTotalSet & { id: string }) => + api + .post(`/subscriptions/${id}/episode-total`, body) + .then((d) => parseOrThrow(SubscriptionProgress, d, "/subscriptions/episode-total")), + onSuccess: (_d, v) => void qc.invalidateQueries({ queryKey: ["subscriptions", "progress", v.id] }), + }); +} diff --git a/apps/web/app/components/library/labels.ts b/apps/web/app/components/library/labels.ts index 4bd55fd4..bada7306 100644 --- a/apps/web/app/components/library/labels.ts +++ b/apps/web/app/components/library/labels.ts @@ -128,6 +128,7 @@ export const CANDIDATE_DECISION: Record = { reject: { label: "拒绝", tone: "destructive" }, unknown_field: { label: "字段未知", tone: "warning" }, skipped: { label: "跳过", tone: "neutral" }, + deferred: { label: "延后(单元在途)", tone: "warning" }, }; export const DECISION_KIND: Record = { @@ -212,6 +213,7 @@ export const PRIORITY: Record = { export const SUB_STATUS: Record = { active: { label: "进行中", tone: "success" }, paused: { label: "已暂停", tone: "warning" }, + pending: { label: "待定(元数据未就绪)", tone: "warning" }, completed: { label: "已完成", tone: "neutral" }, cancelled: { label: "已取消", tone: "neutral" }, }; diff --git a/apps/web/app/components/workbench/release-detail-sheet.tsx b/apps/web/app/components/workbench/release-detail-sheet.tsx index 28991bf4..55ffce6d 100644 --- a/apps/web/app/components/workbench/release-detail-sheet.tsx +++ b/apps/web/app/components/workbench/release-detail-sheet.tsx @@ -24,6 +24,7 @@ const DECISION_LABEL = { reject: { label: "不收录", tone: "destructive" }, unknown_field: { label: "缺字段,待复核", tone: "warning" }, skipped: { label: "被跳过", tone: "neutral" }, + deferred: { label: "延后(单元在途)", tone: "warning" }, } as const; /** 本地库存 + 候选理由(来自作品详情:Slot / CandidateEvaluation),不从标题猜 */ diff --git a/apps/web/app/routes/console/subscriptions.tsx b/apps/web/app/routes/console/subscriptions.tsx index abb78eec..a125572a 100644 --- a/apps/web/app/routes/console/subscriptions.tsx +++ b/apps/web/app/routes/console/subscriptions.tsx @@ -6,6 +6,7 @@ import { SubscriptionCreate, SubscriptionMediaKind, SubscriptionMode, + type SubscriptionProgress, type SubscriptionStatus, } from "@umt/contracts"; import { CalendarClock, Check, FilePlus2, Pause, Play, Plus, Search, XCircle } from "lucide-react"; @@ -17,6 +18,9 @@ import { type SubscriptionFilters, useCreateSubscription, useEntityTitle, + useSearchSubscription, + useSetEpisodeTotal, + useSubscriptionProgress, useSubscriptions, useUpdateSubscription, } from "~/api/subscriptions.ts"; @@ -248,6 +252,13 @@ function SubscriptionsTab() { function SubscriptionSheet({ sub, onClose }: { sub: Subscription | null; onClose: () => void }) { const update = useUpdateSubscription(); + const search = useSearchSubscription(); + const runSearch = () => + sub && + search.mutate(sub.id, { + onSuccess: () => toast.success("已开始在已开启站点搜索,结果会按收录目标自动评估"), + onError: (e) => toast.error(errorMessage(e)), + }); const title = useEntityTitle(sub?.mediaEntityId); const setStatus = (status: SubscriptionStatus, done: string) => sub @@ -270,7 +281,13 @@ function SubscriptionSheet({ sub, onClose }: { sub: Subscription | null; onClose footer={ sub && sub.status !== "cancelled" && sub.status !== "completed" ? ( <> - {sub.status === "active" ? ( + {sub.mediaEntityId ? ( + + ) : null} + {sub.status === "active" || sub.status === "pending" ? ( + + )} + + ); +} + function SubscriptionEditForm({ sub }: { sub: Subscription }) { const id = useId(); const update = useUpdateSubscription();