import { ArrowLeftOutlined, BarChartOutlined, CheckCircleOutlined, LeftOutlined, LineChartOutlined, ReloadOutlined, RightOutlined, SettingOutlined, TeamOutlined, UserOutlined, WarningOutlined, } from "@ant-design/icons"; import { useCallback, useEffect, useMemo, useState } from "react"; import type { WebEnterpriseUsageMember, WebEnterpriseUsageRecord, WebEnterpriseUsageSummary, WebImageWorkbenchTool, WebUsageSummary, WebUserSession, WebViewKey, } from "../../types"; interface TokenUsagePageProps { session: WebUserSession | null; usage: WebUsageSummary; loadEnterpriseUsage?: () => Promise; loadPersonalUsage?: () => Promise; onOpenMore?: () => void; onOpenImageTool?: (tool: WebImageWorkbenchTool) => void; onSelectView?: (view: WebViewKey) => void; } function formatCredits(cents: number): string { const value = Math.max(0, cents) / 100; return value >= 1000 ? `${Math.round(value).toLocaleString()} 积分` : `${value.toFixed(2).replace(/\.00$/, "")} 积分`; } function formatDateTime(value?: string | null): string { if (!value) return "-"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); } function getInitials(value: string): string { const normalized = value.trim(); if (!normalized) return "U"; return normalized.slice(0, 2).toUpperCase(); } function getEnterpriseBalanceCents( session: WebUserSession | null, usage: WebUsageSummary, enterpriseUsage: WebEnterpriseUsageSummary | null, ) { if (enterpriseUsage) return enterpriseUsage.balanceCents; if (typeof usage.enterpriseBalanceCents === "number") return usage.enterpriseBalanceCents; return session?.user.enterpriseBalanceCents ?? usage.balanceCents; } function memberDisplayName(member: WebEnterpriseUsageMember): string { return member.displayName || member.username || String(member.userId); } function recordDuration(record: WebEnterpriseUsageRecord): string { return record.durationSeconds ? `${record.durationSeconds}s` : record.resolution || "-"; } const LOW_BALANCE_THRESHOLD_CENTS = 2000; function formatDayLabel(value: string): string { const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return `${date.getMonth() + 1}/${date.getDate()}`; } type TrendPoint = { date: string; usedCents: number; taskCount: number }; function UsageTrendChart({ data }: { data: TrendPoint[] }) { const W = 680; const H = 200; const padX = 32; const padY = 24; const maxCents = Math.max(1, ...data.map((d) => d.usedCents)); const stepX = data.length > 1 ? (W - padX * 2) / (data.length - 1) : 0; const yOf = (cents: number) => H - padY - (cents / maxCents) * (H - padY * 2); const xOf = (i: number) => padX + i * stepX; const points = data.map((d, i) => ({ x: xOf(i), y: yOf(d.usedCents), ...d })); const linePath = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" "); const areaPath = points.length ? `${linePath} L${points[points.length - 1].x.toFixed(1)},${H - padY} L${points[0].x.toFixed(1)},${H - padY} Z` : ""; const totalCents = data.reduce((sum, d) => sum + d.usedCents, 0); const peak = points.reduce((best, p) => (p.usedCents > best.usedCents ? p : best), points[0] || { usedCents: 0, x: 0, y: 0 }); return (
{[0.25, 0.5, 0.75, 1].map((f) => ( ))} {areaPath ? : null} {linePath ? : null} {points.map((p) => ( {`${formatDayLabel(p.date)} · ${formatCredits(p.usedCents)} · ${p.taskCount} 次`} ))} {points.map((p, i) => ( {formatDayLabel(p.date)} ))}
近 7 天共 {formatCredits(totalCents)} {peak.usedCents > 0 ? 峰值 {formatDayLabel(peak.date)} · {formatCredits(peak.usedCents)} : null}
); } function TokenUsagePage({ session, usage, loadEnterpriseUsage, loadPersonalUsage, onOpenMore, onOpenImageTool, onSelectView, }: TokenUsagePageProps) { const [enterpriseUsage, setEnterpriseUsage] = useState(null); const [enterpriseUsageLoading, setEnterpriseUsageLoading] = useState(false); const [enterpriseUsageError, setEnterpriseUsageError] = useState(null); const isEnterpriseAdmin = session?.user.enterpriseRole === "admin"; const isEnterpriseAccount = Boolean(session?.user.enterpriseId || session?.user.accountType === "enterprise"); const refreshEnterpriseUsage = useCallback(async () => { const loader = isEnterpriseAdmin ? loadEnterpriseUsage : loadPersonalUsage; if (!loader) { setEnterpriseUsage(null); setEnterpriseUsageError(null); return; } setEnterpriseUsageLoading(true); setEnterpriseUsageError(null); try { setEnterpriseUsage(await loader()); } catch (error) { setEnterpriseUsage(null); setEnterpriseUsageError(error instanceof Error ? error.message : "用量数据暂时不可用"); } finally { setEnterpriseUsageLoading(false); } }, [isEnterpriseAdmin, loadEnterpriseUsage, loadPersonalUsage]); useEffect(() => { void refreshEnterpriseUsage(); }, [refreshEnterpriseUsage]); const enterpriseBalanceCents = getEnterpriseBalanceCents(session, usage, enterpriseUsage); const totalCalls = enterpriseUsage ? enterpriseUsage.members.reduce((sum, member) => sum + member.taskCount, 0) : usage.imageUsed + usage.videoUsed + usage.textUsed; const totalUsedCents = enterpriseUsage?.totalUsedCents ?? 0; const modelBreakdown = enterpriseUsage?.modelBreakdown ?? []; const dailyTrend = enterpriseUsage?.dailyTrend ?? []; const availableBalanceCents = isEnterpriseAccount ? enterpriseBalanceCents : usage.balanceCents; const isLowBalance = !usage.betaUnlimited && availableBalanceCents < LOW_BALANCE_THRESHOLD_CENTS; const maxModelCents = Math.max(1, ...modelBreakdown.map((m) => m.usedCents)); const records = enterpriseUsage?.records ?? []; const [recordPage, setRecordPage] = useState(0); const pageSize = 10; const totalPages = Math.max(1, Math.ceil(records.length / pageSize)); const pagedRecords = records.slice(recordPage * pageSize, (recordPage + 1) * pageSize); const members = useMemo(() => { if (enterpriseUsage && enterpriseUsage.members.length) return enterpriseUsage.members; if (!session) return []; return [ { userId: session.user.id, username: session.user.username, displayName: session.user.displayName, role: session.user.enterpriseRole || session.user.role || "personal", usedCents: 0, taskCount: totalCalls, lastUsedAt: null, }, ]; }, [enterpriseUsage, session, totalCalls]); const metricCards = [ { key: "balance", label: "可用余额", value: usage.betaUnlimited ? "不限量" : formatCredits(availableBalanceCents), hint: isLowBalance ? "余额偏低,建议充值" : isEnterpriseAccount ? "企业共享额度" : "个人账户额度", tone: isLowBalance ? "warn" : "accent", }, { key: "used", label: "已用积分", value: formatCredits(totalUsedCents), hint: enterpriseUsage ? `${enterpriseUsage.enterpriseName || "企业"}累计` : "累计消耗", tone: "neutral", }, { key: "calls", label: "调用次数", value: `${totalCalls}`, hint: `${modelBreakdown.length || 0} 个模型`, tone: "neutral", }, ]; const systemStatus = [ { label: "账户类型", value: isEnterpriseAccount ? "企业账户" : "个人账户", tone: "good" }, { label: "企业空间", value: enterpriseUsage?.enterpriseName || session?.user.enterpriseName || "-" }, ]; return (
管理中心
{enterpriseUsageLoading ? "正在同步企业用量" : enterpriseUsageError || "服务器已连接"}
{isLowBalance ? (
当前余额 {formatCredits(availableBalanceCents)},可能不足以完成下一次生成,请及时充值。
) : null}
{metricCards.map((card) => (
{card.label} {card.value} {card.hint}
))}

模型消耗分布

{modelBreakdown.length ? `${modelBreakdown.length} 个模型` : "LIVE"}
{modelBreakdown.length ? (
{modelBreakdown.map((item) => (
{item.model} {formatCredits(item.usedCents)}
{item.taskCount} 次调用 · {Math.round((item.usedCents / Math.max(1, totalUsedCents)) * 100)}%
))}
) : (
暂无模型用量数据
)}

系统状态

{systemStatus.map((item) => (
{item.label}
{item.tone === "good" ? : null} {item.value}
))}
近 7 天趋势 {dailyTrend.some((d) => d.usedCents > 0) ? ( ) : ( 暂无数据 )}

团队成员 ({members.length})

{members.map((member) => (
{getInitials(memberDisplayName(member))}
{memberDisplayName(member)} uid: {member.userId}
{member.role === "admin" ? "管理员" : member.role === "employee" ? "员工" : member.role} {formatCredits(member.usedCents)} {member.taskCount} 调用 {formatDateTime(member.lastUsedAt)}
))}

调用记录

{records.length} 条记录
时间 用户 模型 提示词 状态 耗时 积分
{pagedRecords.length ? ( pagedRecords.map((record) => (
{formatDateTime(record.createdAt)} {record.username} {record.model} {record.prompt || "-"} {record.status === "completed" ? "成功" : record.status === "failed" ? "失败" : record.status} {recordDuration(record)} {record.amountCents > 0 ? formatCredits(record.amountCents) : "-"}
)) ) : (
暂无调用记录
)}
{records.length > pageSize && (
{recordPage + 1} / {totalPages}
)}
); } export default TokenUsagePage;