- 资源 → 站点:卡片 / 列表切换(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]>
116 lines
4.8 KiB
TypeScript
116 lines
4.8 KiB
TypeScript
import type { SiteListItem } from "@umt/contracts";
|
|
import { 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 { StatTile } from "~/components/console/stat-tile.tsx";
|
|
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
|
import { relTime } from "~/components/workbench/release-fields.ts";
|
|
import {
|
|
ACCOUNT_OVERALL,
|
|
AUTH_KIND_LABEL,
|
|
bytes,
|
|
dataAccount,
|
|
healthLine,
|
|
healthMessage,
|
|
overallStatus,
|
|
primaryAccount,
|
|
rateLimitText,
|
|
ratioText,
|
|
} from "./site-fields.ts";
|
|
import { UserDataChart } from "./user-data-chart.tsx";
|
|
|
|
/** 站点详情"概览":账号数据磁贴 + 30 天趋势 + 状态 */
|
|
export function SiteOverview({ 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 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 ?? "—"} hint={u?.vip ? "VIP" : undefined} />
|
|
<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: "代理", value: site.proxy ? "使用" : "不使用" },
|
|
]}
|
|
/>
|
|
</DetailSection>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|