feat(web): site center page, add-site wizard, credentials, health and user-data chart (FE-2)

- 资源 → 站点:卡片 / 列表切换(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]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 16:08:29 +08:00
1 parent 81bf010e3e
commit f8362a3f4c
21 files changed
+3083 -1126

No files matched your search

+117 -3
View File
@@ -1,12 +1,29 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { cursorPage, Id, Site, SiteAccount, type SiteCreate, SiteDefinitionView, SiteSnapshot } from "@umt/contracts";
import {
AuditEvent,
cursorPage,
Id,
Site,
SiteAccount,
type SiteCreate,
type SiteCredentialsBody,
SiteCredentialsResult,
SiteDefinitionView,
SiteHealthResult,
SiteListItem,
SiteRefreshLoginAccepted,
SiteSnapshot,
type SiteUpdate,
SiteUserDataPoint,
} from "@umt/contracts";
import { z } from "zod";
import { TTL_SECOND } from "~/lib/api.ts";
import { apiMutate, apiParsed } from "./parse.ts";
// 站点、站点账号、站点快照(W2-B:/sites、/site-accounts、/sites/{id}/snapshot(s))
// 站点中心(W9-A):include=accounts、凭证 PUT、健康检测、刷新登录、用户数据、删除
const SitePage = cursorPage(Site);
const SitePage = cursorPage(SiteListItem);
const AccountPage = cursorPage(SiteAccount);
const SnapshotPage = cursorPage(SiteSnapshot);
@@ -17,12 +34,17 @@ export const siteKeys = {
accounts: (siteId?: string) => ["sites", "accounts", siteId ?? "all"] as const,
snapshots: (siteId: string) => ["sites", "snapshots", siteId] as const,
definitions: () => ["sites", "definitions"] as const,
userData: (siteId: string, accountId?: string, days = 30) =>
["sites", "user-data", siteId, accountId ?? "first", days] as const,
loginHistory: (accountId: string) => ["sites", "login-history", accountId] as const,
};
/** 站点列表(附账号与整体账号状态,站点页一次请求) */
export function useSites() {
return useQuery({
queryKey: siteKeys.list(),
queryFn: ({ signal }) => apiParsed(SitePage, "/api/v1/sites", { params: { limit: 200 }, signal }),
queryFn: ({ signal }) =>
apiParsed(SitePage, "/api/v1/sites", { params: { limit: 200, include: "accounts" }, signal }),
});
}
@@ -108,3 +130,95 @@ export function useCreateSite() {
onSuccess: () => qc.invalidateQueries({ queryKey: siteKeys.all }),
});
}
/** 修改站点(名称、域名、限流、快照、代理) */
export function useUpdateSite() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, body }: { id: string; body: SiteUpdate }) =>
apiMutate("/api/v1/sites/:id", { method: "PATCH", pathParams: { id }, body }).then((d) => Site.parse(d)),
onSuccess: () => qc.invalidateQueries({ queryKey: siteKeys.all }),
});
}
/** 删除站点:资源标 site_disabled 保留,账号与凭证删除 */
export function useDeleteSite() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiMutate("/api/v1/sites/:id", { method: "DELETE", pathParams: { id } }),
onSuccess: () => qc.invalidateQueries({ queryKey: siteKeys.all }),
});
}
/** 写凭证(按 kind 判别);只写不回显,账号回到未验证 */
export function usePutCredentials() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ siteId, body }: { siteId: string; body: SiteCredentialsBody }) =>
apiMutate("/api/v1/sites/:id/credentials", { method: "PUT", pathParams: { id: siteId }, body }).then((d) =>
SiteCredentialsResult.parse(d),
),
onSuccess: () => qc.invalidateQueries({ queryKey: siteKeys.all }),
});
}
/** 删除站点账号及其凭证 */
export function useDeleteSiteAccount() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) => apiMutate("/api/v1/site-accounts/:id", { method: "DELETE", pathParams: { id } }),
onSuccess: () => qc.invalidateQueries({ queryKey: siteKeys.all }),
});
}
/** 检测健康(同步,≤20 s) */
export function useHealthCheck() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) =>
apiMutate("/api/v1/sites/:id/health-check", { pathParams: { id }, body: {} }).then((d) =>
SiteHealthResult.parse(d),
),
onSettled: () => qc.invalidateQueries({ queryKey: siteKeys.all }),
});
}
/** 刷新登录;界面上用 wait=true 同步拿结果(≤30 s) */
export function useRefreshLogin() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, accountId }: { id: string; accountId?: string }) =>
apiMutate("/api/v1/sites/:id/refresh-login", {
pathParams: { id },
body: { wait: true, ...(accountId ? { accountId } : {}) },
}).then((d) => SiteRefreshLoginAccepted.parse(d)),
onSettled: () => qc.invalidateQueries({ queryKey: siteKeys.all }),
});
}
/** 用户数据按天历史 */
export function useSiteUserData(siteId: string | undefined, accountId?: string, days = 30) {
return useQuery({
queryKey: siteKeys.userData(siteId ?? "", accountId, days),
enabled: !!siteId,
queryFn: ({ signal }) =>
apiParsed(z.object({ items: z.array(SiteUserDataPoint) }), "/api/v1/sites/:id/user-data", {
pathParams: { id: siteId ?? "" },
params: { days, accountId },
signal,
}),
});
}
/** 登录历史:审计里该账号的凭证与登录事件 */
export function useLoginHistory(accountId: string | undefined) {
return useQuery({
queryKey: siteKeys.loginHistory(accountId ?? ""),
enabled: !!accountId,
queryFn: ({ signal }) =>
apiParsed(cursorPage(AuditEvent), "/api/v1/audit-events", {
params: { targetType: "site_account", targetId: accountId, limit: 20 },
signal,
}),
});
}