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 { 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 { Button } from "~/components/ui/button.tsx"; import { cn } from "~/lib/utils.ts"; // 资源 → 站点:站点、凭证、健康、账号数据的唯一入口(docs/design/site-center.md §4) const VIEW_KEY = "umt.sites.view"; type View = "cards" | "list"; type Filter = "all" | "enabled" | "attention"; 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(); const [adding, setAdding] = useState(false); 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); }; // 待处理"改用 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, ); return ( <> } /> {all.length ? (
setParam("filter", v === "all" ? null : v)} options={FILTERS.map((f) => ({ ...f, label: f.value === "attention" && attention ? `${f.label} ${attention}` : f.label, }))} /> 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(true)}> 添加站点 } /> ) : !shown.length ? ( ) : view === "list" ? ( ) : (
{shown.map((s) => ( ))}
)} {adding ? setAdding(false)} /> : null} setDialog(null)} /> ); } function Segmented({ label, value, onChange, options, }: { label: string; value: string; onChange: (v: string) => void; options: { value: string; label: string; icon?: React.ReactNode }[]; }) { return (
{options.map((o) => ( // biome-ignore lint/a11y/useSemanticElements: 分段按钮 ))}
); }