Files
UHD_Media_Tool/apps/web/app/components/sites/search-default-dialog.tsx
T
rootandClaude Opus 5.5 614262378e feat(web): nav regroup, site RSS tab and site types (IA-2)
- sidebar: 概览 / 获取 / 整理 / 系统. Tasks moves to 系统; sites,
  subscriptions and rules form the 获取 group (ACQUIRE_GROUP_TITLE in
  nav.ts, the single place to rename it); standalone RSS nav item removed
- site detail gets an RSS tab with everything the RSS page had (create,
  edit, enable, fetch now, fetch state) plus recent items; tabs and
  header actions follow the site's capabilities
- old /console/sources links redirect to the site's RSS tab (?feed= looks
  up the owning site) or the site list; task links to RSS point at sites
- site list shows and filters by type; add-site wizard starts with a type
  step (PT still picks from the definition library; public BT / custom
  RSS source only need name, URL and RSS URL)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 00:29:56 +08:00

111 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { SearchSiteMode, SiteListItem } from "@umt/contracts";
import { LoaderCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { useSettings, useUpdateSettings } from "~/api/settings.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 { errorMessage } from "~/lib/api.ts";
import { cn } from "~/lib/utils.ts";
// 站点页「搜索站点」:settings.search.defaultSiteMode —— 搜资源时站点选择的初始值,也是 API 不带 sites 时的站点范围。
const MODES: { value: SearchSiteMode; label: string; hint: string }[] = [
{ value: "default", label: "默认站点", hint: "站点卡上打开了「默认参与搜索」的站点" },
{ value: "all", label: "全部站点", hint: "全部已启用站点" },
{ value: "custom", label: "指定站点", hint: "只查下面选中的站点" },
];
export function SearchDefaultDialog({
open,
onOpenChange,
sites,
}: {
open: boolean;
onOpenChange: (o: boolean) => void;
sites: SiteListItem[];
}) {
const settings = useSettings();
const update = useUpdateSettings();
const [mode, setMode] = useState<SearchSiteMode>("default");
const [ids, setIds] = useState<string[]>([]);
useEffect(() => {
if (!open || !settings.data) return;
setMode(settings.data.search?.defaultSiteMode ?? "default");
setIds(settings.data.search?.siteIds ?? []);
}, [open, settings.data]);
const enabled = sites.filter((s) => s.enabled && s.capabilities.includes("search"));
const save = () =>
update.mutate(
{ search: { defaultSiteMode: mode, siteIds: mode === "custom" ? ids : [] } },
{
onSuccess: () => {
toast.success("搜索站点已保存");
onOpenChange(false);
},
onError: (e) => toast.error(errorMessage(e, "保存失败")),
},
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>搜索站点</DialogTitle>
<DialogDescription>搜资源时默认查哪些站点。每次搜索前仍可改选,本机会记住上次的选择。</DialogDescription>
</DialogHeader>
<fieldset aria-label="默认范围" className="m-0 space-y-1.5 border-0 p-0">
{MODES.map((m) => (
<button
key={m.value}
type="button"
aria-pressed={mode === m.value}
onClick={() => setMode(m.value)}
className={cn(
"block w-full cursor-pointer rounded-lg border px-3 py-2 text-left transition-colors duration-micro",
mode === m.value ? "pill-selected" : "border-border hover:bg-foreground/[0.04]",
)}
>
<span className="block text-sm font-medium text-foreground">{m.label}</span>
<span className="block text-meta text-muted-foreground">
{m.hint}
{m.value === "default"
? `(${enabled.filter((s) => s.searchDefault !== false).length} 个)`
: m.value === "all"
? `(${enabled.length} 个)`
: ""}
</span>
</button>
))}
</fieldset>
{mode === "custom" ? (
<ChipToggleGroup
label="站点"
options={enabled.map((s) => ({ value: s.id, label: s.name }))}
value={ids.filter((id) => enabled.some((s) => s.id === id))}
onChange={setIds}
/>
) : null}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={update.isPending}>
取消
</Button>
<Button onClick={save} disabled={update.isPending || (mode === "custom" && ids.length === 0)}>
{update.isPending ? <LoaderCircle className="animate-spin" /> : null}
保存
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}