import type { SiteAccount, SiteListItem } from "@umt/contracts"; import { KeyRound, LoaderCircle, RefreshCw, Trash2, UserRound } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { useDeleteSiteAccount, useLoginHistory, usePutCredentials } from "~/api/sites.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { DetailSection } from "~/components/console/detail-sheet.tsx"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { FormField } from "~/components/console/form-field.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { SecretInput } from "~/components/console/secret-input.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { TechInfo } from "~/components/console/tech-info.tsx"; import { Button } from "~/components/ui/button.tsx"; import { relTime } from "~/components/workbench/release-fields.ts"; import { errorMessage } from "~/lib/api.ts"; import { formatDateTime } from "~/lib/utils.ts"; import type { SiteDialog, useSiteChecks } from "./site-dialogs.tsx"; import { ACCOUNT_OVERALL, AUTH_KIND_LABEL, loginFailureText, REASON_SHORT } from "./site-fields.ts"; // 站点详情"账号":凭证管理(更新 / 刷新登录 / 删除 / Passkey)+ 登录历史 type Checks = ReturnType; export function SiteAccountsTab({ site, checks, onDialog, }: { site: SiteListItem; checks: Checks; onDialog: (d: SiteDialog) => void; }) { const accounts = site.accounts ?? []; if (!accounts.length) return ( onDialog({ kind: "credentials", site })}> 填写凭证 } /> ); return (
{accounts.map((a) => ( ))} a.hasPasskey)} />
); } function AccountBlock({ site, account: a, checks, onDialog, }: { site: SiteListItem; account: SiteAccount; checks: Checks; onDialog: (d: SiteDialog) => void; }) { const del = useDeleteSiteAccount(); const st = ACCOUNT_OVERALL[a.enabled ? a.status : "unconfigured"]; const busy = checks.isBusy(site.id, "login"); const loginKind = a.authKind === "passkey" ? undefined : a.authKind; return (

账号 · {a.label}

{a.enabled ? st.label : "已停用"} {AUTH_KIND_LABEL[a.authKind]} {a.hasPasskey && a.authKind !== "passkey" ? ( Passkey ) : null}
{a.authKind !== "passkey" ? ( ) : null} 删除账号 } title={`删除账号「${a.label}」?`} description="账号、凭证和用户数据历史一起删除,不可恢复。站点和资源保留。" confirmLabel="删除账号" destructive onConfirm={async () => { try { await del.mutateAsync(a.id); toast.success(`已删除账号「${a.label}」`); } catch (err) { toast.error(errorMessage(err, "删除失败")); throw err; } }} />
{loginFailureText(a.lastError.reason, a.lastError.message, a.authKind).text} ), wide: true, }, ] : []), ]} />
); } /** 站点请求 UA 的来源(BR-4);原始 UA 在技术信息里 */ const UA_SOURCE_TEXT: Record = { browser: "跟随浏览器", manual: "手动填写", global: "使用设置里的全局 UA", pending: "待切换:下次刷新登录,或在原站视图里用过后改为跟随浏览器", }; const ACTION_LABEL: Record = { "site_account.login_refreshed": "刷新登录", "site_account.login_refresh_requested": "请求刷新登录", "site_account.credentials_set": "更新凭证", "site_account.created": "添加账号", "site_account.updated": "修改账号", }; function LoginHistory({ accountId }: { accountId: string }) { const q = useLoginHistory(accountId); const items = (q.data?.items ?? []).filter((e) => e.action in ACTION_LABEL); return ( {q.isPending ? ( ) : q.error ? ( void q.refetch()} /> ) : !items.length ? (

还没有记录。刷新登录或更新凭证后出现在这里

) : (
    {items.map((e) => { const ok = e.payload.ok; const reason = typeof e.payload.reason === "string" ? e.payload.reason : null; return (
  1. {ACTION_LABEL[e.action]} {ok === true ? ( 成功 ) : ok === false ? ( {(reason && REASON_SHORT[reason as keyof typeof REASON_SHORT]) || "失败"} ) : null}
  2. ); })}
)}
); } function PasskeyForm({ site, label, has }: { site: SiteListItem; label: string; has: boolean }) { const put = usePutCredentials(); const [v, setV] = useState(""); const [err, setErr] = useState(null); return (
{ e.preventDefault(); if (!v.trim()) { setErr(has ? "填写新的 Passkey 再保存" : "填写 Passkey"); return; } setErr(null); try { await put.mutateAsync({ siteId: site.id, body: { kind: "passkey", label, passkey: v.trim() } }); setV(""); toast.success("Passkey 已保存"); } catch (error) { toast.error(errorMessage(error, "保存失败")); } }} >

站点 Passkey

用于 RSS 和下载链接,不参与登录。{has ? "填写新值保存会替换。" : "还没有保存。"}

); }