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:
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")}>
|
||||
|
||||
Reference in new issue
Block a user