- 资源 → 站点:卡片 / 列表切换(URL ?view= + localStorage 记住),筛选全部 / 已启用 / 需处理, 按分享率 / 上传 / 上次登录排序;卡片显示账号状态、凭证类型、上次登录、上传 / 下载 / 分享率、 等级 / VIP、做种、健康检测结果与时间、快照、限流 - 操作:检测健康、刷新登录(wait:true 同步结果)、更新凭证、编辑(限流 / 快照 / 代理)、启停、 删除(确认框列出影响) - 添加站点向导:按适配器分组选 23 个定义(未实测标注)→ 名称 / 域名覆盖 / 凭证三选一 (馒头默认 API Key)→ 保存并测试(刷新登录 + 健康检测);验证码 / 二次验证提示改用 Cookie 并链接待处理 - 站点详情:概览(用户数据磁贴 + 30 天上传 / 下载与分享率折线,recharts,无彩)/ 资源 / 快照与 Feed / 账号(凭证管理、Passkey、登录历史) - 设置"站点与账号"改为"站点默认"(限流、快照周期、UA、代理、做种页数) - 侧栏改名"站点";smoke 补站点路由;依赖新增 recharts Co-Authored-By: Claude Opus 5.5 <[email protected]>
146 lines
5.3 KiB
TypeScript
146 lines
5.3 KiB
TypeScript
import type { FeedHealth, Site } from "@umt/contracts";
|
|
import { Globe, Plus, RefreshCw } from "lucide-react";
|
|
import { useState } from "react";
|
|
import { Link } from "react-router";
|
|
import { useFeeds } from "~/api/feeds.ts";
|
|
import { snapshotStale, useSiteAccounts, useSites } from "~/api/sites.ts";
|
|
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
|
|
import { DetailSection } from "~/components/console/detail-sheet.tsx";
|
|
import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx";
|
|
import { consolePath } from "~/components/console/nav.ts";
|
|
import { PageHeader } from "~/components/console/page-header.tsx";
|
|
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
|
import { useQueryState } from "~/components/console/use-query-state.ts";
|
|
import { Button } from "~/components/ui/button.tsx";
|
|
import { FEED_HEALTH, FeedTable } from "~/components/workbench/feed-table.tsx";
|
|
import { accountSummary, circuitLabel, relTime, SITE_HEALTH } from "~/components/workbench/release-fields.ts";
|
|
import { legacyRedirectLoader } from "./redirects.ts";
|
|
|
|
// 来源健康:跨站点的 Feed 列表与站点健康总表;单个站点的快照、Feed、账号在站点详情。
|
|
|
|
export const meta = () => [{ title: "来源健康 · UHD 媒体自动化控制台" }];
|
|
|
|
// 旧的 ?tab=snapshots&siteId= 跳到站点详情的"快照与 Feed"
|
|
export const clientLoader = legacyRedirectLoader;
|
|
|
|
export default function SourcesPage() {
|
|
const f = useQueryState(["health", "siteId"] as const);
|
|
const [creating, setCreating] = useState(false);
|
|
const sites = useSites();
|
|
const accounts = useSiteAccounts();
|
|
const feeds = useFeeds({
|
|
health: (f.values.health || undefined) as FeedHealth | undefined,
|
|
siteId: f.values.siteId || undefined,
|
|
});
|
|
|
|
const siteColumns: DataColumn<Site>[] = [
|
|
{
|
|
key: "name",
|
|
header: "站点",
|
|
cell: (s) => (
|
|
<Link
|
|
to={`${consolePath("sites", s.id)}?tab=snapshots`}
|
|
className="font-medium text-foreground hover:underline"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{s.name}
|
|
</Link>
|
|
),
|
|
},
|
|
{
|
|
key: "health",
|
|
header: "健康",
|
|
cell: (s) => (
|
|
<div className="flex flex-wrap gap-1">
|
|
<StatusBadge tone={SITE_HEALTH[s.healthStatus].tone}>{SITE_HEALTH[s.healthStatus].label}</StatusBadge>
|
|
{circuitLabel(s) ? <StatusBadge tone="warning">{circuitLabel(s)}</StatusBadge> : null}
|
|
{!s.enabled && s.healthStatus !== "disabled" ? <StatusBadge>已停用</StatusBadge> : null}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: "account",
|
|
header: "账号",
|
|
hideBelow: "md",
|
|
cell: (s) => {
|
|
const a = accountSummary((accounts.data?.items ?? []).filter((x) => x.siteId === s.id));
|
|
return <StatusBadge tone={a.tone}>{a.label}</StatusBadge>;
|
|
},
|
|
},
|
|
{
|
|
key: "snapshot",
|
|
header: "最近快照",
|
|
hideBelow: "sm",
|
|
cell: (s) => (
|
|
<span className="flex flex-wrap items-center gap-1.5 whitespace-nowrap text-muted-foreground">
|
|
{s.snapshotEnabled ? relTime(s.lastSnapshotAt) : "未开启"}
|
|
{s.snapshotEnabled && snapshotStale(s) ? <StatusBadge tone="warning">已过期</StatusBadge> : null}
|
|
</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<PageHeader
|
|
title="来源健康"
|
|
description="单个站点的快照、Feed 和账号在站点页管理。"
|
|
actions={
|
|
<Button size="sm" onClick={() => setCreating(true)}>
|
|
<Plus />
|
|
新建 Feed
|
|
</Button>
|
|
}
|
|
/>
|
|
<div className="space-y-8">
|
|
<DetailSection title="站点">
|
|
<DataTable
|
|
columns={siteColumns}
|
|
rows={sites.data?.items}
|
|
rowKey={(s) => s.id}
|
|
loading={sites.isPending}
|
|
error={sites.error}
|
|
onRetry={() => void sites.refetch()}
|
|
empty={{
|
|
icon: Globe,
|
|
title: "还没有登记站点",
|
|
description: "在站点页添加。",
|
|
}}
|
|
/>
|
|
</DetailSection>
|
|
<DetailSection title="Feed">
|
|
<FilterBar
|
|
onReset={f.active ? f.reset : undefined}
|
|
actions={
|
|
<Button variant="ghost" size="sm" onClick={() => void feeds.refetch()} disabled={feeds.isFetching}>
|
|
<RefreshCw className={feeds.isFetching ? "animate-spin" : undefined} />
|
|
刷新
|
|
</Button>
|
|
}
|
|
>
|
|
<FilterSelect
|
|
label="健康"
|
|
value={f.values.health}
|
|
onChange={(v) => f.set("health", v)}
|
|
options={Object.entries(FEED_HEALTH).map(([value, h]) => ({ value, label: h.label }))}
|
|
/>
|
|
<FilterSelect
|
|
label="站点"
|
|
value={f.values.siteId}
|
|
onChange={(v) => f.set("siteId", v)}
|
|
options={(sites.data?.items ?? []).map((s) => ({ value: s.id, label: s.name }))}
|
|
/>
|
|
</FilterBar>
|
|
<FeedTable
|
|
health={(f.values.health || undefined) as FeedHealth | undefined}
|
|
siteId={f.values.siteId || undefined}
|
|
filtered={f.active}
|
|
creating={creating}
|
|
onCreatingChange={setCreating}
|
|
/>
|
|
</DetailSection>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|