'use client' import { useEffect, useMemo, useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/navigation' import { BarChart3, RefreshCcw } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { AdminUnauthorizedError, adminFetchJson } from '@/lib/admin-api' interface Stats { date: string request_count: number input_tokens: number output_tokens: number cached_count: number error_count: number hourly: { hour: number request_count: number input_tokens: number output_tokens: number cached_count: number error_count: number }[] } interface Realtime { rpm: number tpm: number } export default function StatsPage() { const router = useRouter() const [providers, setProviders] = useState<{ id: number; name: string; model_id: string; is_default: boolean }[]>([]) const [providerId, setProviderId] = useState('') const [date, setDate] = useState(() => new Date().toISOString().slice(0, 10)) const [stats, setStats] = useState(null) const [realtime, setRealtime] = useState({ rpm: 0, tpm: 0 }) const [error, setError] = useState('') const [isLoading, setIsLoading] = useState(false) const totalTokens = useMemo(() => { if (!stats) return 0 return stats.input_tokens + stats.output_tokens }, [stats]) const cacheHitRate = useMemo(() => { if (!stats || stats.request_count <= 0) return 0 return stats.cached_count / stats.request_count }, [stats]) const errorRate = useMemo(() => { if (!stats || stats.request_count <= 0) return 0 return stats.error_count / stats.request_count }, [stats]) const maxHourlyRequest = useMemo(() => { if (!stats) return 0 return Math.max(0, ...stats.hourly.map(h => h.request_count)) }, [stats]) const fetchProviders = async () => { setError('') try { const data = await adminFetchJson<{ id: number name: string model_id: string is_default: boolean }[]>('/api/v1/admin/providers') setProviders(data) const defaultOne = data.find(p => p.is_default) || data[0] || null if (defaultOne) setProviderId(String(defaultOne.id)) } catch (err: any) { if (err instanceof AdminUnauthorizedError) { router.replace('/login') return } setError(err?.message || '加载 Provider 失败') } } const fetchRealtime = async () => { if (!providerId) return try { const data = await adminFetchJson(`/api/v1/admin/stats/realtime/${providerId}`) setRealtime(data) } catch (err: any) { if (err instanceof AdminUnauthorizedError) { router.replace('/login') return } } } const fetchStats = async () => { if (!providerId) return setIsLoading(true) setError('') try { const data = await adminFetchJson(`/api/v1/admin/stats/daily/${providerId}?date=${date}`) setStats(data) } catch (err: any) { if (err instanceof AdminUnauthorizedError) { router.replace('/login') return } setError(err?.message || '加载统计失败') } finally { setIsLoading(false) } } useEffect(() => { fetchProviders() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { if (!providerId) return fetchStats() fetchRealtime() const interval = setInterval(fetchRealtime, 5000) return () => clearInterval(interval) // eslint-disable-next-line react-hooks/exhaustive-deps }, [providerId, date]) return (

使用统计

实时 RPM/TPM + 指定日期的明细(按小时)

{error && (
{error}
)}
筛选 选择 Provider 与日期
setDate(e.target.value)} />
RPM {realtime.rpm} TPM {realtime.tpm} 缓存命中率 {stats ? `${(cacheHitRate * 100).toFixed(1)}%` : '--'} {stats ? `${stats.cached_count} / ${stats.request_count}` : ''} 错误率 {stats ? `${(errorRate * 100).toFixed(1)}%` : '--'} {stats ? `${stats.error_count} / ${stats.request_count}` : ''}
{stats && (
总请求 {stats.request_count} 总 Token {totalTokens} 输入 Token {stats.input_tokens} 输出 Token {stats.output_tokens}
)}
按小时趋势 以请求数为主(0-23 点) {!stats ? (
暂无数据
) : ( <>
{stats.hourly.map((h) => { const pct = maxHourlyRequest ? h.request_count / maxHourlyRequest : 0 return (
{h.hour}
) })}
小时 请求 Token 缓存 错误 状态 {stats.hourly.map((h) => ( {String(h.hour).padStart(2, '0')}:00 {h.request_count} {h.input_tokens + h.output_tokens} {h.cached_count} {h.error_count} {h.error_count > 0 ? ( 注意 ) : h.request_count > 0 ? ( 正常 ) : ( 空闲 )} ))}
)}
) }