Files
UHD_Media_Tool/apps/web/app/components/sites/site-overview.tsx
T
rootandClaude Opus 5.5 147c6b1947 fix(sites): 不再单独显示 VIP 标签
等级里已经体现 VIP,各站 vip 判定也不一致(同为 VIP 等级,有的站是 true、有的是 null)。
卡片、列表、登录结果、站点概况去掉 VIP 显示,数据字段保留。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 18:20:52 +08:00

225 lines
8.7 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 { SiteListItem } from "@umt/contracts";
import { Link } from "react-router";
import { useFeeds } from "~/api/feeds.ts";
import { accessSummary, useSiteRequestStats, useSiteUserData } from "~/api/sites.ts";
import { DetailSection } from "~/components/console/detail-sheet.tsx";
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
import { KvList } from "~/components/console/kv-list.tsx";
import { consolePath } from "~/components/console/nav.ts";
import { StatTile } from "~/components/console/stat-tile.tsx";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { TechInfo } from "~/components/console/tech-info.tsx";
import { relTime } from "~/components/workbench/release-fields.ts";
import {
ACCOUNT_OVERALL,
AUTH_KIND_LABEL,
bytes,
cloudflareLine,
cloudflareTechItems,
dataAccount,
hasLogin,
healthLine,
healthMessage,
healthTechItems,
kindLabel,
overallStatus,
primaryAccount,
rateLimitText,
ratioText,
siteCan,
} from "./site-fields.ts";
import { UserDataChart } from "./user-data-chart.tsx";
/** Cloudflare 状态行(CF-2);需要你验证时给原站视图入口 */
function cloudflareRow(site: SiteListItem) {
const cf = cloudflareLine(site.config);
if (!cf) return [];
const text = cf.text.replace(/^Cloudflare:/, "");
return [
{
label: "Cloudflare",
value: cf.takeover ? (
<span>
<span className="text-warning">{text}</span>
<Link className="ml-2 text-meta underline" to={`${consolePath("sites", site.id)}?tab=origin`}>
去原站视图
</Link>
</span>
) : (
text
),
},
];
}
const CAP_LABEL = [
["search", "搜索"],
["list", "列表抓取"],
["rss", "RSS"],
["detail", "详情页"],
["login", "登录"],
["userinfo", "账号数据"],
] as const;
/** 可用的获取方式:"搜索 · 列表抓取 · RSS" */
export function capabilityText(site: Pick<SiteListItem, "capabilities">): string {
const on = CAP_LABEL.filter(([c]) => siteCan(site, c)).map(([, l]) => l);
return on.length ? on.join(" · ") : "—";
}
/** 站点详情"概览":账号数据磁贴 + 30 天趋势 + 状态;没有登录的站点(公开 BT、自定义 RSS 源)只看 RSS */
export function SiteOverview({ site }: { site: SiteListItem }) {
return hasLogin(site) ? <AccountSiteOverview site={site} /> : <FeedSiteOverview site={site} />;
}
function FeedSiteOverview({ site }: { site: SiteListItem }) {
const feeds = useFeeds({ siteId: site.id });
const stats = useSiteRequestStats(site.id);
const items = feeds.data?.items ?? [];
const enabled = items.filter((f) => f.enabled);
const failing = enabled.filter((f) => f.healthStatus === "failing" || f.healthStatus === "circuit_open").length;
const lastOk = items
.map((f) => f.lastSuccessAt)
.filter((x): x is string => !!x)
.sort()
.at(-1);
return (
<div className="space-y-6 pt-2">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<StatTile
label="RSS"
value={feeds.data ? items.length : "—"}
hint={items.length !== enabled.length ? `${enabled.length} 个启用` : undefined}
/>
<StatTile label="抓取失败" value={feeds.data ? failing || "—" : "—"} />
<StatTile label="上次成功" value={relTime(lastOk)} />
<StatTile label="近 24 小时请求" value={stats.data ? stats.data.last24h : "—"} />
</div>
<DetailSection title="站点">
<KvList
columns={1}
items={[
{ label: "类型", value: kindLabel(site.kind) },
{ label: "获取方式", value: capabilityText(site) },
{ label: "限流", value: rateLimitText(site.rateLimit) },
{ label: "代理", value: site.proxy ? "使用" : "不使用" },
]}
/>
</DetailSection>
</div>
);
}
function AccountSiteOverview({ site }: { site: SiteListItem }) {
const dAcc = dataAccount(site.accounts);
const acc = primaryAccount(site.accounts);
const u = dAcc?.userData ?? null;
const history = useSiteUserData(site.id, dAcc?.id, 30);
const stats = useSiteRequestStats(site.id);
const st = ACCOUNT_OVERALL[overallStatus(site)];
const h = healthLine(site.lastHealth, site.lastHealthAt);
return (
<div className="space-y-6 pt-2">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
<StatTile label="上传" value={bytes(u?.upload)} />
<StatTile label="下载" value={bytes(u?.download)} />
<StatTile label="分享率" value={ratioText(u)} />
<StatTile label="等级" value={u?.userLevel ?? "—"} />
<StatTile label="做种" value={u ? u.seeding : "—"} hint={u?.seedingSize ? bytes(u.seedingSize) : undefined} />
<StatTile
label={u?.bonus !== null && u?.bonus !== undefined ? "魔力" : "未读消息"}
value={
u
? u.bonus !== null
? u.bonus.toLocaleString("zh-CN", { maximumFractionDigits: 1 })
: u.messageUnread || "—"
: "—"
}
hint={u && u.bonus !== null && u.messageUnread ? `${u.messageUnread} 条未读` : undefined}
/>
</div>
<div className="grid gap-6 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
<section className="pg-slab min-w-0 rounded-xl p-4">
<div className="mb-3 flex flex-wrap items-baseline justify-between gap-2">
<h2 className="text-lead font-semibold text-foreground">近 30 天</h2>
<span className="text-meta text-muted-foreground">
{dAcc ? `账号 ${dAcc.label} · 数据 ${relTime(dAcc.userDataAt)}` : "没有账号数据"}
</span>
</div>
{history.isPending && dAcc ? (
<LoadingState rows={4} />
) : history.error ? (
<ErrorState plain title="读取历史失败" error={history.error} onRetry={() => void history.refetch()} />
) : (
<UserDataChart points={history.data?.items ?? []} />
)}
</section>
<div className="space-y-6">
<DetailSection title="账号">
<KvList
columns={1}
items={[
{ label: "状态", value: <StatusBadge tone={st.tone}>{st.label}</StatusBadge> },
{ label: "凭证", value: acc ? AUTH_KIND_LABEL[acc.authKind] : "未配置" },
{ label: "用户名", value: u?.username ?? acc?.loginName },
{ label: "上次登录", value: relTime(acc?.lastLoginAt) },
...(acc?.lastError && acc.status !== "active"
? [{ label: "最近错误", value: <span className="text-warning">{acc.lastError.message}</span> }]
: []),
]}
/>
</DetailSection>
<DetailSection title="站点">
<KvList
columns={1}
items={[
{
label: "健康检测",
value: (
<span>
{h.text}
{healthMessage(site.lastHealth) ? (
<span className="block text-meta text-muted-foreground">{healthMessage(site.lastHealth)}</span>
) : null}
</span>
),
},
{
label: "抓取",
value: site.snapshotEnabled
? `${site.lastSnapshotAt ? relTime(site.lastSnapshotAt) : "还没抓取过"} · ${site.snapshotIntervalSec ? `每 ${Math.round(site.snapshotIntervalSec / 60)} 分钟` : "默认周期"}`
: "未开启",
},
{ label: "限流", value: rateLimitText(site.rateLimit) },
{
label: "近 24 小时请求",
value: stats.data ? (
<span>
{stats.data.last24h} 次
<span className="block text-meta text-muted-foreground">{accessSummary(stats.data)}</span>
</span>
) : (
"—"
),
},
{ label: "代理", value: site.proxy ? "使用" : "不使用" },
...cloudflareRow(site),
...(site.kind && site.kind !== "pt" ? [{ label: "类型", value: kindLabel(site.kind) }] : []),
{ label: "获取方式", value: capabilityText(site) },
]}
/>
<TechInfo
className="mt-2"
columns={1}
items={[...healthTechItems(site.lastHealth), ...cloudflareTechItems(site.config)]}
/>
</DetailSection>
</div>
</div>
</div>
);
}