import { useState, useEffect, useCallback } from 'react'; import { adminAPI } from '@/api'; interface Overview { total_users: number; active_users: number; new_users: number; api_calls: number; avg_response_time: number; error_rate: number; tokens_used: number; total_cost: number; total_chats: number; total_messages: number; revenue: number; } interface TrendItem { date: string; calls?: number; errors?: number; tokens?: number; new_users?: number; cumulative?: number; revenue?: number; consumption?: number; } interface ModelUsage { model_name: string; call_count: number; total_tokens: number; total_cost: number; avg_tokens: number; } const Analytics: React.FC = () => { const [days, setDays] = useState(7); const [loading, setLoading] = useState(false); const [overview, setOverview] = useState(null); const [apiTrend, setApiTrend] = useState([]); const [tokenTrend, setTokenTrend] = useState<{ daily: any[]; model_distribution: Record }>({ daily: [], model_distribution: {} }); const [userTrend, setUserTrend] = useState([]); const [revenueTrend, setRevenueTrend] = useState([]); const [modelUsage, setModelUsage] = useState([]); const fetchAll = useCallback(async () => { setLoading(true); try { const [ov, api, tok, usr, rev, mu] = await Promise.all([ adminAPI.getAnalyticsOverview(days), adminAPI.getAnalyticsApiTrends(days), adminAPI.getAnalyticsTokenTrends(days), adminAPI.getAnalyticsUserTrends(days), adminAPI.getAnalyticsRevenueTrends(days), adminAPI.getAnalyticsModelUsage(days), ]); setOverview((ov as any)?.data || ov); setApiTrend((api as any)?.data || []); const td = (tok as any)?.data || { daily: [], model_distribution: {} }; setTokenTrend(td); setUserTrend((usr as any)?.data || []); setRevenueTrend((rev as any)?.data || []); setModelUsage((mu as any)?.data || []); } catch (e) { console.error('fetchAnalytics:', e); } setLoading(false); }, [days]); useEffect(() => { fetchAll(); }, [fetchAll]); const renderBar = (data: TrendItem[], key: string, color: string, label: string) => { if (!data.length) return
暂无数据
; const values = data.map((d: any) => d[key] || 0); const max = Math.max(...values, 1); return (
{data.map((d: any, i) => { const v = d[key] || 0; const h = (v / max) * 140; return (
{v > 9999 ? `${(v/1000).toFixed(0)}k` : v}
{d.date?.substring(5)}
); })}
); }; const renderLine = (data: TrendItem[], key: string, color: string) => { if (!data.length) return
暂无数据
; const values = data.map((d: any) => d[key] || 0); const max = Math.max(...values, 1); const w = 100 / (data.length - 1 || 1); const points = data.map((d: any, i) => `${i * w},${100 - ((d[key] || 0) / max) * 90}`).join(' '); return ( {data.map((d: any, i) => ( ))} ); }; const statCards = overview ? [ { label: '总用户数', value: overview.total_users, color: '#6C63FF', icon: 'U' }, { label: '活跃用户', value: overview.active_users, color: '#10b981', icon: 'A' }, { label: '新增用户', value: overview.new_users, color: '#3b82f6', icon: '+' }, { label: 'API调用数', value: overview.api_calls, color: '#f59e0b', icon: 'API' }, { label: 'Token用量', value: overview.tokens_used, color: '#ec4899', icon: 'T' }, { label: '总费用(元)', value: overview.total_cost?.toFixed(2), color: '#ef4444', icon: '¥' }, { label: '对话总数', value: overview.total_chats, color: '#8b5cf6', icon: 'C' }, { label: '消息总数', value: overview.total_messages, color: '#06b6d4', icon: 'M' }, ] : []; return (

数据分析面板

{[7, 14, 30, 90].map(d => ( ))}
{loading ?
加载中...
: ( <> {/* Stats grid */}
{statCards.map((s, i) => (
{s.icon}
{typeof s.value === 'number' ? s.value.toLocaleString() : s.value}
{s.label}
))}
{/* Charts grid */}

API调用趋势 ({days}天)

{renderBar(apiTrend, 'calls', '#f59e0b', '调用次数')}

Token使用趋势 ({days}天)

{renderLine(tokenTrend.daily, 'total_tokens', '#ec4899')}

用户增长 ({days}天)

{renderBar(userTrend, 'new_users', '#3b82f6', '新增用户')}

收入趋势 ({days}天)

{renderBar(revenueTrend, 'revenue', '#10b981', '收入')}
{/* Model usage */}

模型使用分析 ({days}天)

{modelUsage.length > 0 ? ( {['模型', '调用次数', '总Tokens', '平均Tokens', '费用(元)'].map(h => ( ))} {modelUsage.map((m, i) => ( ))}
{h}
{m.model_name} {m.call_count.toLocaleString()} {m.total_tokens.toLocaleString()} {m.avg_tokens.toLocaleString()} ¥{m.total_cost.toFixed(4)}
) :
暂无模型使用数据
}
{/* Model distribution pie (CSS) */} {Object.keys(tokenTrend.model_distribution || {}).length > 0 && (

模型使用分布

{(() => { const entries = Object.entries(tokenTrend.model_distribution); const total = entries.reduce((s, [, v]) => s + v, 1); const colors = ['#6C63FF', '#10b981', '#f59e0b', '#ec4899', '#3b82f6', '#8b5cf6', '#06b6d4', '#ef4444']; return entries.map(([model, val], i) => { const pct = (val / total) * 100; return
; }); })()}
{Object.entries(tokenTrend.model_distribution).map(([model, val], i) => { const colors = ['#6C63FF', '#10b981', '#f59e0b', '#ec4899', '#3b82f6', '#8b5cf6', '#06b6d4', '#ef4444']; const total = Object.values(tokenTrend.model_distribution).reduce((s, v) => s + v, 1); return (
{model} {((val / total) * 100).toFixed(1)}%
); })}
)} )}
); }; export default Analytics;