import type { SiteListItem } from "@umt/contracts"; import { Activity, Camera, Ellipsis, ExternalLink, KeyRound, LoaderCircle, Pencil, Power, RefreshCw, Trash2, } from "lucide-react"; import { type ReactNode, useState } from "react"; import { Link } from "react-router"; import { toast } from "sonner"; import { snapshotStale, useSnapshotSite, useToggleSite, useUpdateSite } from "~/api/sites.ts"; import { DataTable } from "~/components/console/data-table.tsx"; import { consolePath } from "~/components/console/nav.ts"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover.tsx"; import { Switch } from "~/components/ui/switch.tsx"; import { circuitLabel, relTime } from "~/components/workbench/release-fields.ts"; import { errorMessage } from "~/lib/api.ts"; import { cn } from "~/lib/utils.ts"; import type { SiteDialog, useSiteChecks } from "./site-dialogs.tsx"; import { ACCOUNT_OVERALL, AUTH_KIND_LABEL, bytes, dataAccount, healthLine, healthMessage, hostOf, loginFailureText, overallStatus, primaryAccount, rateLimitText, ratioText, } from "./site-fields.ts"; type Checks = ReturnType; type Props = { site: SiteListItem; checks: Checks; onDialog: (d: SiteDialog) => void }; /** 按凭证类型给"更新 xx"文案 */ function updateCredLabel(site: SiteListItem): string { const k = primaryAccount(site.accounts)?.authKind; if (k === "api_key") return "更新 API Key"; if (k === "password") return "更新密码"; if (k === "cookie") return "更新 Cookie"; return "填写凭证"; } function EnableSwitch({ site, withLabel }: { site: SiteListItem; withLabel?: boolean }) { const toggle = useToggleSite(); return ( {withLabel ? : null} toggle.mutate( { id: site.id, enabled: v }, { onSuccess: () => toast.success(v ? `已启用 ${site.name}` : `已停用 ${site.name}`), onError: (e) => toast.error(errorMessage(e)), }, ) } /> ); } /** 默认参与搜索:搜资源时「默认站点」集合 */ function SearchDefaultSwitch({ site }: { site: SiteListItem }) { const update = useUpdateSite(); const on = site.searchDefault !== false; return ( update.mutate( { id: site.id, body: { searchDefault: v } }, { onSuccess: () => toast.success(v ? `${site.name} 默认参与搜索` : `${site.name} 不再默认参与搜索`), onError: (e) => toast.error(errorMessage(e)), }, ) } /> ); } /** "更多"菜单:更新凭证、编辑、快照、启停、删除 */ export function SiteMoreMenu({ site, onDialog }: { site: SiteListItem; onDialog: (d: SiteDialog) => void }) { const [open, setOpen] = useState(false); const snap = useSnapshotSite(); const toggle = useToggleSite(); const item = ( icon: ReactNode, label: string, onClick: () => void, opts: { danger?: boolean; disabled?: boolean } = {}, ) => ( ); return ( {item(, updateCredLabel(site), () => onDialog({ kind: "credentials", site }))} {item(, "编辑站点", () => onDialog({ kind: "edit", site }))} {item( , "抓取快照", () => snap.mutate( { id: site.id }, { onSuccess: () => toast.success("快照已排队"), onError: (e) => toast.error(errorMessage(e)) }, ), { disabled: !site.enabled }, )} {item(, site.enabled ? "停用站点" : "启用站点", () => toggle.mutate( { id: site.id, enabled: !site.enabled }, { onSuccess: () => toast.success(site.enabled ? `已停用 ${site.name}` : `已启用 ${site.name}`), onError: (e) => toast.error(errorMessage(e)), }, ), )}
{item(, "删除站点", () => onDialog({ kind: "delete", site }), { danger: true })} ); } function CheckButtons({ site, checks, compact }: Omit & { compact?: boolean }) { const h = checks.isBusy(site.id, "health"); const l = checks.isBusy(site.id, "login"); const hasAccount = !!site.accounts?.some((a) => a.enabled && a.authKind !== "passkey"); return ( <> ); } function AccountBadges({ site }: { site: SiteListItem }) { const st = ACCOUNT_OVERALL[overallStatus(site)]; const acc = primaryAccount(site.accounts); const hasPasskey = site.accounts?.some((a) => a.hasPasskey || a.authKind === "passkey"); return ( <> 账号{st.label} {acc && acc.authKind !== "passkey" ? ( {AUTH_KIND_LABEL[acc.authKind]} ) : null} {hasPasskey ? ( Passkey ) : null} ); } function Stat({ label, children, className }: { label: string; children: ReactNode; className?: string }) { return (
{label}
{children}
); } export function SiteCard({ site, checks, onDialog }: Props) { const acc = primaryAccount(site.accounts); const dAcc = dataAccount(site.accounts); const u = dAcc?.userData ?? null; const health = healthLine(site.lastHealth, site.lastHealthAt); const hmsg = healthMessage(site.lastHealth); const stale = site.snapshotEnabled && snapshotStale(site); const circuit = circuitLabel(site); const lastErr = acc && acc.status !== "active" ? acc.lastError : null; return (
{u?.vip ? VIP : null} {circuit ? {circuit} : null}
{lastErr ? (

{loginFailureText(lastErr.reason, lastErr.message, acc?.authKind).text}

) : null}
{u ? ( <>
{bytes(u.upload)} {bytes(u.download)} {ratioText(u)} {u.userLevel ?? "—"} {u.seeding} {u.seedingSize ? · {bytes(u.seedingSize)} : null} {u.bonus !== null ? ( {u.bonus.toLocaleString("zh-CN", { maximumFractionDigits: 1 })} ) : ( {u.messageUnread || "—"} )}

{u.username ?? ""} {u.messageUnread > 0 && u.bonus !== null ? `${u.messageUnread} 条未读 · ` : ""}数据{" "} {relTime(dAcc?.userDataAt)}

) : (

{acc ? "还没有用户数据。刷新登录后显示" : "还没有凭证。填写凭证后显示账号数据"}

)}
上次登录
{relTime(acc?.lastLoginAt)}
健康检测
{health.text}
快照
{!site.snapshotEnabled ? "未开启" : site.lastSnapshotAt ? `${relTime(site.lastSnapshotAt)}${stale ? " · 已过期" : ""}` : "还没有快照"}
限流
{rateLimitText(site.rateLimit)}
{acc ? ( ) : ( )}
); } function StatusDot({ tone }: { tone: "success" | "warning" | "destructive" | "neutral" }) { return ( ); } /** 列表视图:同字段,窄屏隐藏次要列 */ export function SiteTable({ sites, checks, onDialog, }: { sites: SiteListItem[]; checks: Checks; onDialog: (d: SiteDialog) => void; }) { return ( r.id} columns={[ { key: "name", header: "站点", cell: (s) => (
{s.name}

{hostOf(s.baseUrl)}

), }, { key: "account", header: "账号", cell: (s) => (
{dataAccount(s.accounts)?.userData?.vip ? VIP : null}
), }, { key: "login", header: "上次登录", hideBelow: "lg", cell: (s) => ( {relTime(primaryAccount(s.accounts)?.lastLoginAt)} ), }, { key: "traffic", header: "上传 / 下载", hideBelow: "md", cell: (s) => { const u = dataAccount(s.accounts)?.userData; return u ? ( {bytes(u.upload)} / {bytes(u.download)} ) : ( "—" ); }, }, { key: "ratio", header: "分享率", hideBelow: "sm", align: "right", cell: (s) => {ratioText(dataAccount(s.accounts)?.userData)}, }, { key: "level", header: "等级 · 做种", hideBelow: "lg", cell: (s) => { const u = dataAccount(s.accounts)?.userData; return u ? `${u.userLevel ?? "—"} · ${u.seeding}` : "—"; }, }, { key: "health", header: "健康检测", hideBelow: "md", cell: (s) => { const h = healthLine(s.lastHealth, s.lastHealthAt); return ( {h.text} ); }, }, { key: "snapshot", header: "快照", hideBelow: "lg", cell: (s) => ( {!s.snapshotEnabled ? "未开启" : s.lastSnapshotAt ? relTime(s.lastSnapshotAt) : "还没有"} ), }, { key: "actions", header: 操作, align: "right", cell: (s) => (
{primaryAccount(s.accounts) ? : null}
), }, ]} /> ); }