import { useEffect, useState } from "react"; import { App, Button, Descriptions, Drawer, Progress, Skeleton, Tabs } from "antd"; import { IconButton } from "@/components/ui/base/buttons"; import { EmptyState } from "@/components/ui/product/empty-state"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { PaginationBar } from "@/components/layout/workspace-page"; import { formatCredits } from "@/constant/credits"; import { AdminDataTable, AdminStatusBadge, AdminTableEmpty, type AdminStatusTone } from "./admin-ui"; import { getAdminUserDetail, listAdminUserAuditEvents, listAdminUserLedger, listAdminUserTasks, type AdminAuditEvent, type AdminUserDetail, type AdminUserTask } from "@/services/api/auth"; import type { CreditLedgerEntry } from "@/services/api/wallet"; export function AdminUserDetailDrawer({ userId, onClose, previousUserId, nextUserId, onNavigate }: { userId: string | null; onClose: () => void; previousUserId?: string; nextUserId?: string; onNavigate?: (userId: string) => void }) { const { message } = App.useApp(); const [detail, setDetail] = useState(null); const [ledger, setLedger] = useState([]); const [tasks, setTasks] = useState([]); const [events, setEvents] = useState([]); const [loading, setLoading] = useState(false); const [ledgerPage, setLedgerPage] = useState(1); const [ledgerTotal, setLedgerTotal] = useState(0); const [taskPage, setTaskPage] = useState(1); const [taskTotal, setTaskTotal] = useState(0); const [auditPage, setAuditPage] = useState(1); const [auditTotal, setAuditTotal] = useState(0); useEffect(() => { if (!userId) return; let active = true; setLoading(true); setDetail(null); setLedgerPage(1); setTaskPage(1); setAuditPage(1); void getAdminUserDetail(userId) .then((nextDetail) => { if (active) setDetail(nextDetail); }) .catch((error) => active && message.error(error instanceof Error ? error.message : "读取用户详情失败")) .finally(() => active && setLoading(false)); return () => { active = false; }; }, [message, userId]); useEffect(() => { if (!userId) return; let active = true; void listAdminUserLedger(userId, { page: ledgerPage, limit: 20 }) .then((result) => { if (active) { setLedger(result.entries); setLedgerTotal(result.total); } }) .catch((error) => active && message.error(error instanceof Error ? error.message : "读取积分流水失败")); return () => { active = false; }; }, [ledgerPage, message, userId]); useEffect(() => { if (!userId) return; let active = true; void listAdminUserTasks(userId, { page: taskPage, limit: 20 }) .then((result) => { if (active) { setTasks(result.tasks); setTaskTotal(result.total); } }) .catch((error) => active && message.error(error instanceof Error ? error.message : "读取任务记录失败")); return () => { active = false; }; }, [message, taskPage, userId]); useEffect(() => { if (!userId) return; let active = true; void listAdminUserAuditEvents(userId, { page: auditPage, limit: 20 }) .then((result) => { if (active) { setEvents(result.events); setAuditTotal(result.total); } }) .catch((error) => active && message.error(error instanceof Error ? error.message : "读取管理操作失败")); return () => { active = false; }; }, [auditPage, message, userId]); return ( previousUserId && onNavigate(previousUserId)} /> nextUserId && onNavigate(nextUserId)} /> ) : null} > {loading && !detail ? ( ) : detail ? ( }, { key: "available", label: "可用积分", children: formatCredits(detail.account.availableMicrocredits) }, { key: "reserved", label: "冻结积分", children: formatCredits(detail.account.reservedMicrocredits) }, { key: "created", label: "注册时间", children: formatTime(detail.user.createdAt) }, { key: "login", label: "最后登录", children: formatTime(detail.user.lastLoginAt) }, ]} />
{Object.entries({ 积分流水: detail.counts.ledgerEntries, 生成任务: detail.counts.tasks, 上游请求: detail.counts.apiCalls, 管理操作: detail.counts.auditEvents }).map(([label, value]) => (
{label}
{value}
))}
资源与配额占用
{quotaUsageItems(detail).map((item) => (
{item.label} {item.display}
0 ? Math.round(item.value / item.limit * 100) : 0)} size="small" showInfo={false} status={item.value >= item.limit ? "exception" : "normal"} />
))}
), }, { key: "ledger", label: `积分流水 ${detail.counts.ledgerEntries}`, children: ( formatCredits(value) }, { title: "说明", dataIndex: "note", ellipsis: true }, ], scroll: { x: 720 }, }} empty={} footer={ setLedgerPage(page)} pageSizeOptions={[20]} />} /> ), }, { key: "tasks", label: `生成任务 ${detail.counts.tasks}`, children: ( }, { title: "阶段", dataIndex: "stage", ellipsis: true }, ], scroll: { x: 820 }, }} empty={} footer={ setTaskPage(page)} pageSizeOptions={[20]} />} /> ), }, { key: "audit", label: `管理操作 ${detail.counts.auditEvents}`, children: ( } footer={ setAuditPage(page)} pageSizeOptions={[20]} />} /> ), }, ]} /> ) : ( )}
); } function formatTime(value?: string) { return value ? new Date(value).toLocaleString("zh-CN", { hour12: false }) : "--"; } function taskStatusTone(value?: string): AdminStatusTone { const normalized = String(value || "").toLowerCase(); if (["completed", "succeeded", "success", "done"].includes(normalized)) return "success"; if (["failed", "error", "cancelled", "canceled"].includes(normalized)) return "error"; if (["running", "processing", "pending", "queued"].includes(normalized)) return "warning"; return "neutral"; } function quotaUsageItems(detail: AdminUserDetail) { const structuredBytes = detail.storageUsage.assetBytes + detail.storageUsage.canvasBytes + detail.storageUsage.sessionBytes; const bytes = (value: number) => value >= 1024 ** 3 ? `${(value / 1024 ** 3).toFixed(2)} GB` : `${(value / 1024 ** 2).toFixed(1)} MB`; const number = (value: number) => new Intl.NumberFormat("zh-CN").format(value); return [ { label: "资源与附件", value: detail.storedFileBytes, limit: detail.quota.storedFileGB * 1024 ** 3, display: `${bytes(detail.storedFileBytes)} / ${detail.quota.storedFileGB} GB` }, { label: "今日上传(UTC)", value: detail.dailyUploadBytes, limit: detail.quota.dailyUploadMB * 1024 ** 2, display: `${bytes(detail.dailyUploadBytes)} / ${detail.quota.dailyUploadMB} MB` }, { label: "画布、素材与会话数据", value: structuredBytes, limit: detail.quota.structuredDataMB * 1024 ** 2, display: `${bytes(structuredBytes)} / ${detail.quota.structuredDataMB} MB` }, { label: "任务与请求日志数据", value: detail.storageUsage.taskBytes, limit: detail.quota.taskDataGB * 1024 ** 3, display: `${bytes(detail.storageUsage.taskBytes)} / ${detail.quota.taskDataGB} GB` }, { label: "素材数量", value: detail.storageUsage.assetCount, limit: detail.quota.assetCount, display: `${number(detail.storageUsage.assetCount)} / ${number(detail.quota.assetCount)}` }, { label: "画布数量", value: detail.storageUsage.canvasCount, limit: detail.quota.canvasCount, display: `${number(detail.storageUsage.canvasCount)} / ${number(detail.quota.canvasCount)}` }, { label: "Agent 会话数量", value: detail.storageUsage.sessionCount, limit: detail.quota.sessionCount, display: `${number(detail.storageUsage.sessionCount)} / ${number(detail.quota.sessionCount)}` }, { label: "任务历史数量", value: detail.storageUsage.taskCount, limit: detail.quota.taskCount, display: `${number(detail.storageUsage.taskCount)} / ${number(detail.quota.taskCount)}` }, { label: "上游请求日志数量", value: detail.storageUsage.apiCallCount, limit: detail.quota.apiCallLogCount, display: `${number(detail.storageUsage.apiCallCount)} / ${number(detail.quota.apiCallLogCount)}` }, ]; }