import { SITE_KINDS, type SiteKind } from "@umt/contracts"; import { Globe, LayoutGrid, List, PackageSearch, Plus, RefreshCw } from "lucide-react"; import { useEffect, useState } from "react"; import { useSearchParams } from "react-router"; import { useSites } from "~/api/sites.ts"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { FilterSelect } from "~/components/console/filter-bar.tsx"; import { PageHeader } from "~/components/console/page-header.tsx"; import { useRecordSheet } from "~/components/console/record-sheet.tsx"; import { SearchDefaultDialog } from "~/components/sites/search-default-dialog.tsx"; import { SiteAddDialog } from "~/components/sites/site-add-dialog.tsx"; import { SiteCard, SiteTable } from "~/components/sites/site-card.tsx"; import { type SiteDialog, SiteDialogHost, useSiteChecks } from "~/components/sites/site-dialogs.tsx"; import { needsAttention, type SortKey, sortSites } from "~/components/sites/site-fields.ts"; import { SiteSheet } from "~/components/sites/site-sheet.tsx"; import { Button } from "~/components/ui/button.tsx"; import { SegmentedControl } from "~/components/ui/segmented.tsx"; // 获取 → 站点:站点、凭证、健康、账号数据、RSS 的唯一入口(docs/design/site-center.md §4); // 按类型分组显示:PT 站 / 公开 BT 站(旧的自定义 RSS 源有的话单列);?new=<类型> 直接打开添加。 // 站点详情是本页的宽抽屉(?open=<站点 id>),旧地址 /console/sites/:id 跳到这里。 const VIEW_KEY = "umt.sites.view"; type View = "cards" | "list"; type Filter = "all" | "enabled" | "attention"; /** 分组顺序;公开 BT 站的表只列它用得到的列 */ const GROUPS: { kind: SiteKind; title: string; variant: "pt" | "public" | "subtitle" }[] = [ { kind: "pt", title: "PT 站", variant: "pt" }, { kind: "public_bt", title: "公开 BT 站", variant: "public" }, { kind: "rss_feed", title: "自定义 RSS 源", variant: "public" }, { kind: "torznab", title: "Torznab 索引器", variant: "public" }, { kind: "subtitle", title: "字幕站", variant: "subtitle" }, ]; const FILTERS: { value: Filter; label: string }[] = [ { value: "all", label: "全部" }, { value: "enabled", label: "已启用" }, { value: "attention", label: "需要处理" }, ]; const SORTS: { value: SortKey; label: string }[] = [ { value: "name", label: "名称" }, { value: "ratio", label: "分享率" }, { value: "upload", label: "上传" }, { value: "lastLogin", label: "上次登录" }, ]; function readStoredView(): View | null { try { const v = localStorage.getItem(VIEW_KEY); return v === "cards" || v === "list" ? v : null; } catch { return null; } } export const meta = () => [{ title: "站点 · UHD 媒体自动化控制台" }]; export default function SitesPage() { const sites = useSites(); const [params, setParams] = useSearchParams(); // 记录抽屉(console-ia.md §9):?open=<站点 id>,抽屉里的页签 ?tab=、RSS ?feed= 随抽屉走 const sheet = useRecordSheet({ scoped: ["tab", "feed"] }); const newParam = params.get("new"); const [adding, setAdding] = useState(null); const [dialog, setDialog] = useState(null); const checks = useSiteChecks(setDialog); const urlView = params.get("view"); const [storedView, setStoredView] = useState("cards"); useEffect(() => { const v = readStoredView(); if (v) setStoredView(v); }, []); const view: View = urlView === "list" || urlView === "cards" ? urlView : storedView; const filter = (FILTERS.find((f) => f.value === params.get("filter"))?.value ?? "all") as Filter; const sort = (SORTS.find((s) => s.value === params.get("sort"))?.value ?? "name") as SortKey; const setParam = (k: string, v: string | null) => setParams( (p) => { if (v) p.set(k, v); else p.delete(k); return p; }, { replace: true }, ); const setView = (v: View) => { setStoredView(v); try { localStorage.setItem(VIEW_KEY, v); } catch { // 隐私模式等:只在本次会话记住 } setParam("view", v); }; // 命令面板 / 旧 RSS 页的 ?new=1、?new=rss_feed:打开添加(带类型时跳过选类型) // biome-ignore lint/correctness/useExhaustiveDependencies: 只在参数出现时处理一次 useEffect(() => { if (!newParam) return; setAdding(newParam in SITE_KINDS ? (newParam as SiteKind) : "any"); setParam("new", null); }, [newParam]); // 待处理"改用 Cookie"深链:?credentials= const credFor = params.get("credentials"); // biome-ignore lint/correctness/useExhaustiveDependencies: 只在数据到达时处理一次 useEffect(() => { if (!credFor || !sites.data) return; const s = sites.data.items.find((x) => x.id === credFor); if (s) setDialog({ kind: "credentials", site: s, loginKind: "cookie" }); setParam("credentials", null); }, [credFor, sites.data]); const all = sites.data?.items ?? []; const [searchDefaults, setSearchDefaults] = useState(false); const attention = all.filter(needsAttention).length; const shown = sortSites( all.filter((s) => (filter === "enabled" ? s.enabled : filter === "attention" ? needsAttention(s) : true)), sort, ); const groups = GROUPS.map((g) => ({ ...g, sites: shown.filter((s) => s.kind === g.kind) })).filter( (g) => g.sites.length, ); return ( <> } /> {all.length ? (
setParam("filter", v === "all" ? null : v)} options={FILTERS.map((f) => ({ ...f, count: f.value === "attention" && attention ? attention : undefined, }))} /> setParam("sort", v || null)} allLabel="名称" options={SORTS.filter((x) => x.value !== "name")} />
setView(v as View)} options={[ { value: "cards", label: "卡片", icon: }, { value: "list", label: "列表", icon: }, ]} />
) : null} {sites.isPending ? (
) : sites.error && !sites.data ? ( void sites.refetch()} /> ) : !all.length ? ( setAdding("any")}> 添加站点 } /> ) : !shown.length ? ( ) : (
{groups.map((g) => (

{g.title} {g.sites.length}

{view === "list" ? ( ) : (
{g.sites.map((s) => ( ))}
)}
))}
)} {adding ? setAdding(null)} /> : null} setDialog(null)} /> ); }