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:
1 parent
81bf010e3e
commit
f8362a3f4c
21 files changed
+3083
-1126
No files matched your search
+117
-3
@@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user