docs(plan): on-demand site detail page parsing (HHClub details.php)

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-29 15:13:35 +08:00
1 parent 94fd48bff2
commit ac478673bc
39 files changed
+2446 -339

No files matched your search

@@ -66,9 +66,19 @@ import { SlotOrderList } from "./slot-order.tsx";
// 新建 / 编辑订阅(设计 §3):基础 / 进阶两栏,对齐 MP 编辑对话框。作品用全局搜索的 media 段(本地 + TMDB)。
type Target =
/** 订阅的作品:媒体库作品 / TMDB 条目(保存时先建档,SEARCH-2 详情页入口)/ 按标题追踪 */
export type SubscriptionTarget =
| { kind: "entity"; id: string; title: string; year: number | null; mediaKind: SubscriptionMediaKind }
| {
kind: "tmdb";
entityType: "movie" | "tv";
externalId: string;
title: string;
year: number | null;
mediaKind: SubscriptionMediaKind;
}
| { kind: "title"; titleQuery: string; mediaKind: SubscriptionMediaKind; note?: string };
type Target = SubscriptionTarget;
const toSubKind = (k: string): SubscriptionMediaKind =>
k === "movie" ? "movie" : k === "variety" ? "variety" : "series";
@@ -263,11 +273,14 @@ function WorkPicker({ onPick, error }: { onPick: (t: Target) => void; error?: st
export function SubscriptionFormDialog({
sub,
title,
initialTarget,
onClose,
onCreated,
}: {
/** 缺省 = 新建 */
sub?: Subscription | null;
/** 新建时预填作品(作品详情页的「订阅」),不再显示作品搜索 */
initialTarget?: SubscriptionTarget;
/** 编辑时的作品名(单条订阅接口不带标题) */
title?: string;
onClose: () => void;
@@ -278,8 +291,9 @@ export function SubscriptionFormDialog({
const create = useCreateSubscription();
const update = useUpdateSubscription();
const setOrder = useSetSlotOrder();
const [target, setTarget] = useState<Target | null>(null);
const [target, setTarget] = useState<Target | null>(initialTarget ?? null);
const [tab, setTab] = useState("basic");
const entityFromTmdb = useEntityFromCandidate();
const slotsQ = useSlots({ subscriptionId: sub?.id }, editing);
const subSlots = useMemo(
() =>
@@ -289,7 +303,19 @@ export function SubscriptionFormDialog({
[slotsQ.data, sub?.id],
);
const initialUpgrade: UpgradeStrategy = sub ? upgradeOf(subSlots, sub.mediaKind) : "off";
const [d, setD] = useState<SubscriptionDraft>(() => (sub ? draftFromSubscription(sub, "off") : emptyDraft("series")));
const [d, setD] = useState<SubscriptionDraft>(() => {
if (sub) return draftFromSubscription(sub, "off");
const k = initialTarget?.mediaKind ?? "series";
const base = emptyDraft(k);
return initialTarget
? {
...base,
mediaKind: k,
mode: k === "movie" ? "once" : "continuous",
seasonScope: k === "movie" ? "current_season" : "following_seasons",
}
: base;
});
const [order, setOrderState] = useState<{ id: string; name: string }[] | null>(null);
const [upgradeTouched, setUpgradeTouched] = useState(false);
const [totalTouched, setTotalTouched] = useState(false);
@@ -340,7 +366,7 @@ export function SubscriptionFormDialog({
});
};
const pending = create.isPending || update.isPending || setOrder.isPending;
const pending = create.isPending || update.isPending || setOrder.isPending || entityFromTmdb.isPending;
const submit = async () => {
const { params, errors: errs } = draftToParams(d, sub?.params ?? {}, {
@@ -372,10 +398,23 @@ export function SubscriptionFormDialog({
toast.success("订阅已保存");
onClose();
} else if (target) {
// TMDB 条目:订阅时隐式建档(季/集与总集数随之就位),同一 TMDB id 已建档则复用
const tmdbEntity =
target.kind === "tmdb"
? (
await entityFromTmdb.mutateAsync({
provider: "tmdb",
externalId: target.externalId,
kind: target.entityType,
})
).mediaEntityId
: null;
const created = await create.mutateAsync({
...(target.kind === "entity"
? { mediaEntityId: target.id, seasonUnitId: d.seasonUnitId || undefined }
: { titleQuery: target.titleQuery.trim() }),
: tmdbEntity
? { mediaEntityId: tmdbEntity }
: { titleQuery: target.kind === "title" ? target.titleQuery.trim() : target.title }),
mediaKind: d.mediaKind,
mode: d.mode,
seasonScope: isMovie ? "current_season" : d.seasonScope,
@@ -405,7 +444,7 @@ export function SubscriptionFormDialog({
const f = (k: string) => `${id}-${k}`;
const titleText = sub
? (title ?? sub.mediaEntityTitle ?? sub.titleQuery ?? "订阅")
: target?.kind === "entity"
: target?.kind === "entity" || target?.kind === "tmdb"
? `${target.title}${target.year ? ` (${target.year})` : ""}`
: (target?.titleQuery ?? "");
@@ -415,7 +454,7 @@ export function SubscriptionFormDialog({
<DialogHeader>
<DialogTitle>{editing ? "编辑订阅" : "新建订阅"}</DialogTitle>
<DialogDescription>
{editing ? titleText : "从媒体库或 TMDB 选作品;选 TMDB 条目会先建档。"}
{editing ? titleText : initialTarget ? titleText : "从媒体库或 TMDB 选作品;选 TMDB 条目会先建档。"}
</DialogDescription>
</DialogHeader>
{dup ? (
@@ -455,13 +494,19 @@ export function SubscriptionFormDialog({
<p className="truncate text-sm font-medium text-foreground">{titleText}</p>
)}
<p className="mt-0.5 text-meta text-muted-foreground">
{target.kind === "entity" ? "媒体库作品" : (target.note ?? "按标题追踪,尚未建档")}
{target.kind === "entity"
? "媒体库作品"
: target.kind === "tmdb"
? "TMDB 条目,保存时建档"
: (target.note ?? "按标题追踪,尚未建档")}
</p>
</div>
<Button variant="ghost" size="sm" onClick={() => setTarget(null)}>
<X />
更换
</Button>
{initialTarget ? null : (
<Button variant="ghost" size="sm" onClick={() => setTarget(null)}>
<X />
更换
</Button>
)}
</div>
) : (
<WorkPicker onPick={pickTarget} error={errors.target} />
@@ -479,7 +524,7 @@ export function SubscriptionFormDialog({
value: v,
label: SUB_KIND_LABEL[v],
}))}
disabled={editing || target?.kind === "entity"}
disabled={editing || target?.kind === "entity" || target?.kind === "tmdb"}
/>
</FormField>
<FormField label="方式" htmlFor={f("mode")}>