import { useEffect, useState, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { ClipboardList, Clock, CheckCircle, AlertTriangle, Database, ChevronRight, X, Bell, Eye, Activity, TrendingUp, Zap, Server, Search, FileText, ArrowUpRight, ArrowDownRight, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { statusMap, statusColorMap, dbTypeColorMap } from '@/lib/constants'; import { useTheme } from '@/hooks/useTheme'; import { useAuth } from '@/hooks/useAuth'; import { dashboardApi } from '@/api/dashboard'; import type { DashboardStats } from '@/types'; /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ interface QuickAction { label: string; icon: any; onClick: () => void; color: string; bg: string; } /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ function calcHealthScore(inst: any): number { if (inst.status === 'offline') return 0; if (inst.healthScore != null) return inst.healthScore; let score = 100; const cpu = inst.cpu ?? 0, memory = inst.memory ?? 0, latency = inst.latency ?? 0, disk = inst.disk ?? 0; if (cpu > 80) score -= 25; else if (cpu > 60) score -= 10; if (memory > 85) score -= 20; else if (memory > 70) score -= 10; if (latency > 10) score -= 20; else if (latency > 5) score -= 10; if (disk > 80) score -= 15; return Math.max(0, score); } function healthColor(score: number): string { if (score >= 90) return 'text-green-400'; if (score >= 70) return 'text-yellow-400'; if (score >= 50) return 'text-orange-400'; return 'text-red-400'; } function healthBg(score: number): string { if (score >= 90) return 'bg-green-500/15'; if (score >= 70) return 'bg-yellow-500/15'; if (score >= 50) return 'bg-orange-500/15'; return 'bg-red-500/15'; } /* ------------------------------------------------------------------ */ /* Skeleton Components */ /* ------------------------------------------------------------------ */ function StatCardSkeleton() { return (
); } /* ------------------------------------------------------------------ */ /* Main Dashboard */ /* ------------------------------------------------------------------ */ export default function Dashboard() { const theme = useTheme(); const { user } = useAuth(); const navigate = useNavigate(); const [dismissedIds, setDismissedIds] = useState>(new Set()); const [expandedId, setExpandedId] = useState(null); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [announcements, setAnnouncements] = useState([]); const [dbInstances, setDbInstances] = useState([]); const [tickets, setTickets] = useState([]); const [queryStats, setQueryStats] = useState(null); useEffect(() => { import('@/api/request').then(m => m.default.get('/announcements')).then(res => { const list = Array.isArray(res) ? res : []; setAnnouncements(list.map((a: any) => ({ id: String(a.id), title: a.title || '', content: a.content || '', priority: 'normal' as const, pinned: false, enabled: a.is_published ?? true, publishTime: a.published_at || a.created_at || '', targetRoles: ['admin', 'dba', 'developer'], }))); }).catch(() => setAnnouncements([])); }, []); /* ---- Data Fetch ---- */ useEffect(() => { dashboardApi.getStats() .then(data => { setStats(data); // 关联查询统计 if (data.query_stats) setQueryStats(data.query_stats); }) .catch(() => setStats(null)) .finally(() => setLoading(false)); }, []); /* ---- Instances & Tickets ---- */ useEffect(() => { import('@/api/request').then(m => { m.default.get('/instances').then((res: any) => { const list = Array.isArray(res) ? res : (res?.items || res?.data || []); setDbInstances(list.map((i: any) => ({ id: i.id, name: i.name, dbType: i.db_type || i.dbType, status: i.status || 'online', healthScore: i.health_score ?? i.healthScore ?? 100, host: i.host || '', port: i.port || '', qps: i.qps ?? 0, connections: i.connections ?? 0, }))); }).catch(() => setDbInstances([])); m.default.get('/dashboard/my-tickets').then((res: any) => { const list = Array.isArray(res) ? res : []; setTickets(list); }).catch(() => setTickets([])); }); }, []); /* ---- Announcements ---- */ const visibleAnnouncements = useMemo(() => { return announcements .filter(a => a.enabled && !dismissedIds.has(a.id)) .sort((a, b) => new Date(b.publishTime).getTime() - new Date(a.publishTime).getTime()); }, [dismissedIds, announcements]); const dismiss = useCallback((id: string) => setDismissedIds(prev => new Set(prev).add(id)), []); const toggleExpand = useCallback((id: string) => setExpandedId(prev => prev === id ? null : id), []); /* ---- Computed Data ---- */ const onlineInstances = dbInstances.filter(i => i.status === 'online').length; const warningInstances = dbInstances.filter(i => i.status === 'warning').length; const offlineInstances = dbInstances.filter(i => i.status === 'offline').length; const pendingTickets = tickets; const statsCards = useMemo(() => { if (!stats) return []; const queryTotal = stats.query_stats?.total ?? 0; return [ { title: '工单总数', value: stats.ticket_total.toLocaleString(), icon: ClipboardList, color: 'text-blue-400', bg: 'bg-blue-400/10', subtitle: `${stats.ticket_pending} 个待审批 · ${stats.ticket_executing} 个执行中`, }, { title: '查询总数', value: queryTotal.toLocaleString(), icon: Search, color: 'text-green-400', bg: 'bg-green-400/10', subtitle: `今日 ${stats.queries_today} 次 · 平均 ${stats.query_stats?.avg_time_ms ?? 0}ms`, }, { title: '数据源总数', value: stats.instance_total, icon: Database, color: 'text-purple-400', bg: 'bg-purple-400/10', subtitle: `${stats.instance_online} 个在线 · ${stats.user_total} 个用户`, }, ]; }, [stats]); const quickActions = [ { label: '新建工单', icon: FileText, onClick: () => navigate('/tickets/create'), color: 'text-blue-400', bg: 'bg-blue-400/10', border: 'border-blue-400/20' }, { label: '新建查询', icon: Search, onClick: () => navigate('/queries'), color: 'text-green-400', bg: 'bg-green-400/10', border: 'border-green-400/20' }, { label: '添加实例', icon: Database, onClick: () => navigate('/instances'), color: 'text-purple-400', bg: 'bg-purple-400/10', border: 'border-purple-400/20' }, ]; /* ================================================================ */ /* RENDER */ /* ================================================================ */ return (
{/* ---- Quick Actions ---- */}
{quickActions.map(action => ( ))}
{/* ---- Announcements ---- */} {visibleAnnouncements.length > 0 && (
{visibleAnnouncements.slice(0, 3).map(ann => { const isExpanded = expandedId === ann.id; return (
{ann.title} {ann.publishTime.slice(0, 10)}
{ann.content}
{ann.content.includes('\n') && ( )}
); })}
)} {/* ---- Stats Cards ---- */}
{loading ? ( Array.from({ length: 3 }).map((_, i) => ) ) : ( statsCards.map((s, i) => (
{s.title}
{s.value}
{s.subtitle}
)) )}
{/* ---- My Pending Tickets + Instance Status ---- */}
{/* My Pending Tickets */}
我的待处理 ({pendingTickets.length}) 查看全部
{pendingTickets.length === 0 ? (
暂无待处理工单
) : (
{pendingTickets.map((ticket: any) => ( ))}
工单号 标题 数据库 状态 待处理 关联 时间 操作
TK-{ticket.id} {ticket.title} {(ticket.db_type || '').toUpperCase()} {statusMap[ticket.status] || ticket.status} {ticket.pending_info || '-'} {ticket.relation} {ticket.create_time}
)}
{/* Instance Status */}
实例状态 {onlineInstances}/{dbInstances.length} 在线
{/* Status Overview */}
{onlineInstances}
在线
{warningInstances}
告警
{offlineInstances}
离线
{/* Instance List */} {dbInstances.slice(0, 5).map(inst => { const health = calcHealthScore(inst); return (
{inst.name} {health}
{inst.host}{inst.port ? `:${inst.port}` : ''} {(inst.qps ?? 0).toLocaleString()} QPS {inst.connections ?? 0} 连接
); })}
{/* ---- DataSource Health + Query Stats ---- */}
{/* DataSource Health */}
数据源健康概览 {onlineInstances}/{dbInstances.length} 在线
{/* By Type */}
{(['mysql', 'postgres', 'redis', 'mongo'] as const).map(type => { const typeInstances = dbInstances.filter(i => i.dbType === type); const typeOnline = typeInstances.filter(i => i.status === 'online').length; const iconMap: Record = { mysql: '/db-logos/mysql.svg', postgres: '/db-logos/postgres.svg', redis: '/db-logos/redis.svg', mongo: '/db-logos/mongo.svg' }; const labelMap: Record = { mysql: 'MySQL', postgres: 'PostgreSQL', redis: 'Redis', mongo: 'MongoDB' }; const allHealthy = typeOnline === typeInstances.length && typeInstances.length > 0; const someHealthy = typeOnline > 0; return (
{typeInstances.length}
{labelMap[type]}
{typeOnline}/{typeInstances.length}
); })}
{/* Attention List */} {dbInstances.filter(i => i.status === 'warning' || i.status === 'offline').length > 0 && (
需要关注
{dbInstances.filter(i => i.status === 'warning' || i.status === 'offline').slice(0, 3).map(inst => { const health = calcHealthScore(inst); return (
{inst.name} {inst.host}:{inst.port} {health}分 {inst.status === 'offline' ? '离线' : '告警'}
); })}
)}
{/* Query Stats */}
查询统计
{(['今日', '本周', '本月'] as const).map((p, i) => ( ))}
总查询数
{(queryStats?.total ?? 0).toLocaleString()}
12% vs 昨日
平均耗时
{queryStats?.avg_time ?? 0}ms
8% vs 昨日
慢查询
{queryStats?.slow_count ?? 0}
3 vs 昨日
查询类型分布
{(queryStats?.type_distribution ?? [ { label: 'MySQL', count: 0, color: 'bg-[#00758f]' }, { label: 'PostgreSQL', count: 0, color: 'bg-[#336791]' }, { label: 'Redis', count: 0, color: 'bg-[#dc382d]' }, { label: 'MongoDB', count: 0, color: 'bg-[#47a248]' }, ]).map((item: any) => { const total = queryStats?.total || 1; return (
{item.label}
{item.count}
); })}
); }