import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { adminAPI } from '@/api'; const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 20 }) => ( {children} ); interface DashboardStats { total_users: number; active_users: number; total_models: number; enabled_models: number; total_conversations: number; active_conversations: number; total_knowledge_bases: number; total_documents: number; today_orders: number; today_tokens: number; week_tokens: number; total_tokens: number; today_conversations: number; server_time: string; } interface DailyStat { date: string; tokens: number; } const quickEntries = [ { path: '/admin/models', label: '大模型库', desc: '管理AI模型与供应商', icon: }, { path: '/admin/settings', label: '网站设置', desc: '品牌、邮件、短信配置', icon: }, { path: '/admin/payment', label: '支付配置', desc: '微信、支付宝渠道配置', icon: }, { path: '/admin/services', label: '增值服务', desc: '服务管理与定制订单', icon: }, { path: '/admin/membership', label: '会员体系', desc: '订阅计划与自动化规则', icon: }, { path: '/admin/market', label: '应用市场', desc: '应用审核与优惠券管理', icon: }, { path: '/admin/logs', label: '日志风控', desc: '操作/调用/付费日志与风控', icon: }, ]; // SVG Bar Chart Component const BarChart: React.FC<{ data: DailyStat[] }> = ({ data }) => { if (!data || data.length === 0) return
暂无数据
; const maxVal = Math.max(...data.map(d => d.tokens), 1); const w = 600, h = 200, padLeft = 55, padRight = 10, padTop = 10, padBottom = 25; const barAreaW = w - padLeft - padRight; const barAreaH = h - padTop - padBottom; const barGap = 2; const barW = Math.max(2, (barAreaW / data.length) - barGap); // Y axis ticks const yTicks = [0, Math.round(maxVal * 0.25), Math.round(maxVal * 0.5), Math.round(maxVal * 0.75), maxVal]; const formatK = (n: number) => n >= 10000 ? (n / 1000).toFixed(0) + 'k' : String(n); const formatDate = (d: string) => { const parts = d.split('-'); return parts.length >= 2 ? `${parts[1]}/${parts[2]}` : d; }; return ( {/* Grid lines */} {yTicks.map((tick, i) => { const y = padTop + barAreaH - (tick / maxVal) * barAreaH; return ( {formatK(tick)} ); })} {/* Bars */} {data.map((d, i) => { const barH = (d.tokens / maxVal) * barAreaH; const x = padLeft + i * (barAreaW / data.length); const y = padTop + barAreaH - barH; return ( {`${d.date}: ${d.tokens.toLocaleString()} tokens`} {/* X label - every 7th bar */} {i % Math.max(1, Math.floor(data.length / 8)) === 0 && ( {formatDate(d.date)} )} ); })} ); }; // Mini Donut for stat cards const MiniDonut: React.FC<{ value: number; max: number; color: string }> = ({ value, max, color }) => { const pct = max > 0 ? Math.min(value / max, 1) : 0; const r = 16, c = 20, circumference = 2 * Math.PI * r; const dash = circumference * pct; return ( {Math.round(pct * 100)}% ); }; const Dashboard: React.FC = () => { const navigate = useNavigate(); const [stats, setStats] = useState({ total_users: 0, active_users: 0, total_models: 0, enabled_models: 0, total_conversations: 0, active_conversations: 0, total_knowledge_bases: 0, total_documents: 0, today_orders: 0, today_tokens: 0, week_tokens: 0, total_tokens: 0, today_conversations: 0, server_time: '', }); const [dailyStats, setDailyStats] = useState([]); const [operationLogs, setOperationLogs] = useState([]); const [auditLogs, setAuditLogs] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchData = async () => { setLoading(true); try { const [statsRes, dailyRes, logsRes, auditRes] = await Promise.all([ adminAPI.getStats() as any, (adminAPI as any).getDailyStats?.(30) || Promise.resolve({ data: [] }), adminAPI.getRecentLogs() as any, adminAPI.getRecentAudit() as any, ]); // Stats: backend returns raw dict (no {code,data} wrapper) — use .data or the response itself const statsData = statsRes?.data || statsRes; if (statsData && typeof statsData === 'object') setStats(statsData); // Daily stats: backend returns {code, data: {days, data: [...]}} — access nested .data.data const dailyData = dailyRes?.data?.data || (Array.isArray(dailyRes?.data) ? dailyRes.data : []) || []; setDailyStats(Array.isArray(dailyData) ? dailyData : []); // Recent logs: backend returns raw list (no wrapper) — use the response directly if it's an array const logsData = Array.isArray(logsRes) ? logsRes : (Array.isArray(logsRes?.data) ? logsRes.data : []); setOperationLogs(logsData); // Recent audit: same as logs const auditData = Array.isArray(auditRes) ? auditRes : (Array.isArray(auditRes?.data) ? auditRes.data : []); setAuditLogs(auditData); } catch (err) { console.error('Dashboard数据加载失败:', err); } finally { setLoading(false); } }; fetchData(); }, []); const formatDate = (d: string) => { if (!d) return '-'; const dt = new Date(d); if (isNaN(dt.getTime())) return d.slice(0, 19) || '-'; return dt.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; const formatTokens = (n: number): string => { if (n >= 100000000) return (n / 100000000).toFixed(2) + '亿'; if (n >= 10000) return (n / 10000).toFixed(1) + '万'; return n.toLocaleString('en-US'); }; const statCardDefs = [ { label: '总用户', value: stats.total_users, sub: `活跃 ${stats.active_users}`, color: '#6C63FF', bg: 'rgba(108,99,255,0.12)', subBg: 'rgba(108,99,255,0.08)', icon: }, { label: 'AI模型', value: stats.total_models, sub: `启用 ${stats.enabled_models}`, color: '#219B5E', bg: 'rgba(33,155,94,0.12)', subBg: 'rgba(33,155,94,0.08)', icon: }, { label: '对话数', value: stats.total_conversations, sub: `活跃 ${stats.active_conversations}`, color: '#E8922C', bg: 'rgba(232,146,44,0.12)', subBg: 'rgba(232,146,44,0.08)', icon: }, { label: '知识库', value: stats.total_knowledge_bases, sub: `文档 ${stats.total_documents}`, color: '#3B82F6', bg: 'rgba(59,130,246,0.12)', subBg: 'rgba(59,130,246,0.08)', icon: }, { label: '今日对话', value: stats.today_conversations, sub: `订单 ${stats.today_orders}`, color: '#8B5CF6', bg: 'rgba(139,92,246,0.12)', subBg: 'rgba(139,92,246,0.08)', icon: }, { label: '今日Token', value: stats.today_tokens, sub: `本周 ${formatTokens(stats.week_tokens)}`, color: '#EC4899', bg: 'rgba(236,72,153,0.12)', subBg: 'rgba(236,72,153,0.08)', format: true, icon: }, ]; if (loading) { return (
加载仪表盘数据...
); } return (

平台驾驶舱

{/* Stats Cards Row */}
{statCardDefs.map((s, i) => (
{s.icon}
{s.label}
{s.format ? formatTokens(s.value as number) : (s.value as number).toLocaleString()}
{s.sub}
))}
{/* Charts Row */}
{/* Token Trend Chart */}

{' '}Token消耗趋势(近30天)

{/* System Health */}

{' '}平台概览

{[ { label: '累计Token消耗', value: formatTokens(stats.total_tokens), color: '#6C63FF', pct: 100 }, { label: '模型利用率', value: `${stats.enabled_models}/${stats.total_models}`, color: '#219B5E', pct: stats.total_models > 0 ? stats.enabled_models / stats.total_models : 0 }, { label: '用户活跃率', value: `${stats.active_users}/${stats.total_users}`, color: '#E8922C', pct: stats.total_users > 0 ? stats.active_users / stats.total_users : 0 }, { label: '对话活跃率', value: `${stats.active_conversations}/${stats.total_conversations}`, color: '#3B82F6', pct: stats.total_conversations > 0 ? stats.active_conversations / stats.total_conversations : 0 }, ].map((item, i) => (
{item.label}
{item.value}
))}
服务器时间: {stats.server_time ? new Date(stats.server_time).toLocaleString('zh-CN') : '-'}
{/* Quick Actions */}

快捷入口

{quickEntries.map((entry) => (
navigate(entry.path)} style={{ background: '#FFFFFF', borderRadius: 'var(--radius-lg)', border: '1px solid rgba(0,0,0,0.08)', padding: 'var(--space-md) var(--space-lg)', cursor: 'pointer', transition: 'all 0.2s', display: 'flex', alignItems: 'center', gap: 'var(--space-md)', boxShadow: '0 1px 3px rgba(0,0,0,0.04)', }} onMouseEnter={(e) => { e.currentTarget.style.borderColor = '#6C63FF'; e.currentTarget.style.boxShadow = '0 2px 8px rgba(108,99,255,0.15)'; e.currentTarget.style.transform = 'translateY(-1px)'; }} onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--color-border)'; e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.transform = 'none'; }} >
{entry.icon}
{entry.label}
{entry.desc}
))}
{/* Recent Logs */}

最近操作日志

最近10条
{operationLogs.length > 0 ? operationLogs.map(log => ( )) : ( )}
用户 操作 目标 详情 IP 时间
{log.username} {log.action} {log.target} {log.detail} {log.ip} {formatDate(log.created_at)}
暂无操作日志
{/* Recent Audit Logs */}

最近审计日志

最近10条
{auditLogs.length > 0 ? auditLogs.map(log => ( )) : ( )}
操作人 操作类型 目标类型 详情 时间
{log.admin_name} {log.action} {log.target_type} {log.detail} {formatDate(log.created_at)}
暂无审计日志
); }; export default Dashboard;