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
@@ -0,0 +1,171 @@
|
||||
import type { SearchSiteMode, Site } from "@umt/contracts";
|
||||
import { PackageSearch } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { useSiteOptions } from "~/api/instances.ts";
|
||||
import { useSettings } from "~/api/settings.ts";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import { ChipToggleGroup } from "~/components/library/parts.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "~/components/ui/dialog.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
|
||||
// 搜资源前选站点:默认(站点页勾了「默认参与搜索」的)/ 全部启用站点 / 指定站点。
|
||||
// 初始值 = 本机上次的选择,没有就用 settings.search.defaultSiteMode。
|
||||
|
||||
export type SitePick = { mode: SearchSiteMode; siteIds: string[] };
|
||||
|
||||
const STORE_KEY = "umt.search.sitePick";
|
||||
|
||||
export function loadSitePick(): SitePick | null {
|
||||
try {
|
||||
const raw = globalThis.localStorage?.getItem(STORE_KEY);
|
||||
if (!raw) return null;
|
||||
const v = JSON.parse(raw) as Partial<SitePick>;
|
||||
if (v.mode !== "default" && v.mode !== "all" && v.mode !== "custom") return null;
|
||||
return { mode: v.mode, siteIds: Array.isArray(v.siteIds) ? v.siteIds.filter((x) => typeof x === "string") : [] };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveSitePick(p: SitePick) {
|
||||
try {
|
||||
globalThis.localStorage?.setItem(STORE_KEY, JSON.stringify(p));
|
||||
} catch {
|
||||
// 隐私模式等存不了:只是下次不记得
|
||||
}
|
||||
}
|
||||
|
||||
/** 按模式解析出站点 ID(只含启用站点) */
|
||||
export function resolvePick(pick: SitePick, sites: Pick<Site, "id" | "enabled" | "searchDefault">[]): string[] {
|
||||
const on = sites.filter((s) => s.enabled);
|
||||
if (pick.mode === "all") return on.map((s) => s.id);
|
||||
if (pick.mode === "custom") {
|
||||
const set = new Set(pick.siteIds);
|
||||
return on.filter((s) => set.has(s.id)).map((s) => s.id);
|
||||
}
|
||||
return on.filter((s) => s.searchDefault !== false).map((s) => s.id);
|
||||
}
|
||||
|
||||
const MODES: { value: SearchSiteMode; label: string }[] = [
|
||||
{ value: "default", label: "默认站点" },
|
||||
{ value: "all", label: "全部站点" },
|
||||
{ value: "custom", label: "指定站点" },
|
||||
];
|
||||
|
||||
export function SitePickerDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
onConfirm,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (o: boolean) => void;
|
||||
/** 作品名,显示在说明里 */
|
||||
title: string;
|
||||
onConfirm: (siteIds: string[]) => void;
|
||||
}) {
|
||||
const sitesQ = useSiteOptions();
|
||||
const settings = useSettings();
|
||||
const sites = useMemo(() => (sitesQ.data?.items ?? []).filter((s) => s.enabled), [sitesQ.data]);
|
||||
const [pick, setPick] = useState<SitePick | null>(null);
|
||||
// 打开时取初始值:本机上次 > 全局设置 > 默认
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const cfg = settings.data?.search;
|
||||
setPick(
|
||||
loadSitePick() ?? (cfg ? { mode: cfg.defaultSiteMode, siteIds: cfg.siteIds } : { mode: "default", siteIds: [] }),
|
||||
);
|
||||
}, [open, settings.data]);
|
||||
|
||||
const cur = pick ?? { mode: "default" as const, siteIds: [] };
|
||||
const ids = resolvePick(cur, sites);
|
||||
const count = (m: SearchSiteMode) => resolvePick({ mode: m, siteIds: cur.siteIds }, sites).length;
|
||||
|
||||
const confirm = () => {
|
||||
saveSitePick(cur);
|
||||
onConfirm(ids);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>搜索资源</DialogTitle>
|
||||
<DialogDescription>在所选站点搜「{title}」。每个站点按作品原名和译名各查一次。</DialogDescription>
|
||||
</DialogHeader>
|
||||
<fieldset aria-label="站点范围" className="m-0 grid grid-cols-3 gap-1 rounded-lg border border-border/70 p-1">
|
||||
{MODES.map((m) => (
|
||||
<button
|
||||
key={m.value}
|
||||
type="button"
|
||||
aria-pressed={cur.mode === m.value}
|
||||
onClick={() =>
|
||||
setPick({
|
||||
mode: m.value,
|
||||
// 切到「指定」时从当前集合起步
|
||||
siteIds: m.value === "custom" && cur.mode !== "custom" ? ids : cur.siteIds,
|
||||
})
|
||||
}
|
||||
className={cn(
|
||||
"cursor-pointer rounded-md px-2 py-1.5 text-caption transition-colors duration-micro",
|
||||
cur.mode === m.value
|
||||
? "bg-foreground/[0.08] font-medium text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{m.label}
|
||||
{m.value !== "custom" && sitesQ.data ? (
|
||||
<span className="ml-1 text-meta text-muted-foreground tabular-nums">{count(m.value)}</span>
|
||||
) : null}
|
||||
</button>
|
||||
))}
|
||||
</fieldset>
|
||||
{sitesQ.isPending ? (
|
||||
<p className="text-caption text-muted-foreground">加载站点…</p>
|
||||
) : sites.length === 0 ? (
|
||||
<p className="text-caption text-muted-foreground">
|
||||
还没有启用的站点。
|
||||
<Link to={consolePath("sites")} className="ml-1 underline underline-offset-4">
|
||||
去站点页
|
||||
</Link>
|
||||
</p>
|
||||
) : cur.mode === "custom" ? (
|
||||
<ChipToggleGroup
|
||||
label="站点"
|
||||
options={sites.map((s) => ({ value: s.id, label: s.name }))}
|
||||
value={cur.siteIds.filter((id) => sites.some((s) => s.id === id))}
|
||||
onChange={(v) => setPick({ mode: "custom", siteIds: v })}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-caption break-words text-muted-foreground">
|
||||
{ids.length
|
||||
? sites
|
||||
.filter((s) => ids.includes(s.id))
|
||||
.map((s) => s.name)
|
||||
.join("、")
|
||||
: "没有站点勾选「默认参与搜索」。在站点页打开,或改选全部站点。"}
|
||||
</p>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={confirm} disabled={ids.length === 0}>
|
||||
<PackageSearch />
|
||||
搜索 {ids.length} 个站点
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user