// ============================================================ // 用量统计 + 转化漏斗 // ============================================================ import { useState } from 'react' import { useQuery } from '@tanstack/react-query' import { Card, Col, Row, Select, Statistic } from 'antd' import { ThunderboltOutlined, ColumnWidthOutlined, UserOutlined, TeamOutlined } from '@ant-design/icons' import type { ProColumns } from '@ant-design/pro-components' import { ProTable } from '@ant-design/pro-components' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, FunnelChart, Funnel, LabelList, } from 'recharts' import { telemetryService } from '@/services/telemetry' import { statsService } from '@/services/stats' import { PageHeader } from '@/components/PageHeader' import { ErrorState } from '@/components/ErrorState' import type { DailyUsageStat, ModelUsageStat } from '@/types' // ─── Conversion Funnel Data ─── interface FunnelStep { name: string value: number fill: string } function buildFunnelData( totalAccounts: number, activeAccounts: number, dailyData?: DailyUsageStat[], modelData?: ModelUsageStat[], ): FunnelStep[] { const activeDevicesToday = dailyData?.length ? dailyData.reduce((s, d) => s + d.unique_devices, 0) : 0 const activeModels = modelData?.filter((m) => m.request_count > 0).length ?? 0 return [ { name: '注册用户', value: totalAccounts, fill: '#8c8c8c' }, { name: '活跃用户', value: activeAccounts, fill: '#863bff' }, { name: '今日使用', value: Math.max(activeDevicesToday, 0), fill: '#47bfff' }, { name: '使用多模型', value: activeModels, fill: '#10b981' }, ] } // ─── Daily Trend Bar Data ─── interface DailyTrend { day: string requests: number inputTokens: number outputTokens: number } function buildDailyTrend(data?: DailyUsageStat[]): DailyTrend[] { if (!data) return [] return data.map((d) => ({ day: d.day.slice(5), // MM-DD requests: d.request_count, inputTokens: Math.round(d.input_tokens / 1000), // K tokens outputTokens: Math.round(d.output_tokens / 1000), })) } // ─── Main Component ─── export default function Usage() { const [days, setDays] = useState(30) const { data: dailyData, isLoading: dailyLoading, error: dailyError, refetch, } = useQuery({ queryKey: ['usage-daily', days], queryFn: ({ signal }) => telemetryService.dailyStats({ days }, signal), }) const { data: modelData, isLoading: modelLoading } = useQuery({ queryKey: ['usage-model', days], queryFn: ({ signal }) => telemetryService.modelStats({}, signal), }) const { data: dashboardStats } = useQuery({ queryKey: ['stats-dashboard'], queryFn: ({ signal }) => statsService.dashboard(signal), }) if (dailyError) { return ( <> refetch()} /> ) } const totalRequests = dailyData?.reduce((s, d) => s + d.request_count, 0) ?? 0 const totalTokens = dailyData?.reduce((s, d) => s + d.input_tokens + d.output_tokens, 0) ?? 0 const totalAccounts = dashboardStats?.total_accounts ?? 0 const activeAccounts = dashboardStats?.active_accounts ?? 0 const funnelData = buildFunnelData(totalAccounts, activeAccounts, dailyData, modelData) const trendData = buildDailyTrend(dailyData) const dailyColumns: ProColumns[] = [ { title: '日期', dataIndex: 'day', width: 120 }, { title: '请求数', dataIndex: 'request_count', width: 100, render: (_, r) => r.request_count.toLocaleString(), }, { title: '输入 Token', dataIndex: 'input_tokens', width: 120, render: (_, r) => r.input_tokens.toLocaleString(), }, { title: '输出 Token', dataIndex: 'output_tokens', width: 120, render: (_, r) => r.output_tokens.toLocaleString(), }, { title: '设备数', dataIndex: 'unique_devices', width: 80 }, ] const modelColumns: ProColumns[] = [ { title: '模型', dataIndex: 'model_id', width: 200 }, { title: '请求数', dataIndex: 'request_count', width: 100, render: (_, r) => r.request_count.toLocaleString(), }, { title: '输入 Token', dataIndex: 'input_tokens', width: 120, render: (_, r) => r.input_tokens.toLocaleString(), }, { title: '输出 Token', dataIndex: 'output_tokens', width: 120, render: (_, r) => r.output_tokens.toLocaleString(), }, { title: '平均延迟', dataIndex: 'avg_latency_ms', width: 100, render: (_, r) => (r.avg_latency_ms ? `${Math.round(r.avg_latency_ms)}ms` : '-'), }, { title: '成功率', dataIndex: 'success_rate', width: 100, render: (_, r) => `${(r.success_rate * 100).toFixed(1)}%`, }, ] return (
} /> {/* Summary Cards */} 总请求数 } value={totalRequests} prefix={} valueStyle={{ fontWeight: 600, color: '#863bff' }} /> 总 Token 数 } value={totalTokens} prefix={} valueStyle={{ fontWeight: 600, color: '#47bfff' }} /> 注册用户 } value={totalAccounts} prefix={} valueStyle={{ fontWeight: 600, color: '#10b981' }} /> 活跃用户 } value={activeAccounts} prefix={} valueStyle={{ fontWeight: 600, color: '#f59e0b' }} /> {/* Conversion Funnel + Daily Trend */} 用户转化漏斗 } size="small" > [value.toLocaleString(), '数量']} /> 每日趋势 } size="small" > { const labels: Record = { requests: '请求数', inputTokens: '输入 Token(K)', outputTokens: '输出 Token(K)', } return [value.toLocaleString(), labels[name] ?? name] }} /> {/* Daily Stats */} 每日统计 } className="mb-6" size="small" styles={{ body: { padding: 0 } }} > columns={dailyColumns} dataSource={dailyData ?? []} loading={dailyLoading} rowKey="day" search={false} toolBarRender={false} pagination={false} size="small" /> {/* Model Stats */} 按模型统计 } size="small" styles={{ body: { padding: 0 } }} > columns={modelColumns} dataSource={modelData ?? []} loading={modelLoading} rowKey="model_id" search={false} toolBarRender={false} pagination={false} size="small" />
) }