import { useEffect, useState, useCallback } from 'react'; import { Card, Col, Row, Statistic, Table, Tag, Spin, Empty } from 'antd'; import { CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined, ThunderboltOutlined, StopOutlined, BarChartOutlined, } from '@ant-design/icons'; import { PageContainer } from '@ant-design/pro-layout'; import { useOutletContext } from '@umijs/max'; import { Area, Gauge } from '@ant-design/plots'; import intl from 'react-intl-universal'; import { SharedContext } from '@/layouts'; import { request } from '@/utils/http'; import config from '@/utils/config'; import CronLogModal from '../crontab/logModal'; import TaskResultModal from './taskResultModal'; interface Overview { total: number; enabled: number; disabled: number; todayRuns: number; todaySuccess: number; todayFail: number; successRate: string; avgTime: number; } interface TrendItem { date: string; total: number; success: number; fail: number; } interface TopItem { rank: number; name: string; avgTime?: number; maxTime?: number; runCount?: number; successRate?: string; } interface Runtime { runningCount: number; queuedCount: number; running: Array<{ instanceId: number; id: number; name: string; pid: number; elapsed: number; logPath: string }>; idleTasks: Array<{ id: number; name: string; lastRun: string }>; } interface SystemInfo { platform: string; uptime: number; memTotal: number; memFree: number; memUsagePercent: string; heapUsed: number; heapTotal: number; loadAvg: number[]; cpus: number; } const formatBytes = (bytes: number) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`; }; const formatSeconds = (s: number) => { if (s < 60) return `${s}s`; if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`; const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); return `${h}h ${m}m`; }; const REFRESH_INTERVAL = 30000; const Dashboard = () => { const { headerStyle, theme } = useOutletContext(); const isDark = theme === 'vs-dark'; const [overview, setOverview] = useState(null); const [trend, setTrend] = useState([]); const [topTime, setTopTime] = useState([]); const [topCount, setTopCount] = useState([]); const [runtime, setRuntime] = useState(null); const [system, setSystem] = useState(null); const [labels, setLabels] = useState([]); const [logCron, setLogCron] = useState(null); const [loading, setLoading] = useState(true); const [result, setResult] = useState<'success' | 'failure' | null>(null); const fetchData = useCallback(async () => { try { const [overviewRes, trendRes, topTimeRes, topCountRes, runtimeRes, systemRes, labelsRes] = await Promise.allSettled([ request.get(`${config.apiPrefix}dashboard/overview`), request.get(`${config.apiPrefix}dashboard/trend`), request.get(`${config.apiPrefix}dashboard/top-time`), request.get(`${config.apiPrefix}dashboard/top-count`), request.get(`${config.apiPrefix}dashboard/runtime`), request.get(`${config.apiPrefix}dashboard/system`), request.get(`${config.apiPrefix}dashboard/labels`), ]); if (overviewRes.status === 'fulfilled' && overviewRes.value.code === 200) setOverview(overviewRes.value.data); if (trendRes.status === 'fulfilled' && trendRes.value.code === 200) setTrend(trendRes.value.data); if (topTimeRes.status === 'fulfilled' && topTimeRes.value.code === 200) setTopTime(topTimeRes.value.data); if (topCountRes.status === 'fulfilled' && topCountRes.value.code === 200) setTopCount(topCountRes.value.data); if (runtimeRes.status === 'fulfilled' && runtimeRes.value.code === 200) setRuntime(runtimeRes.value.data); if (systemRes.status === 'fulfilled' && systemRes.value.code === 200) setSystem(systemRes.value.data); if (labelsRes.status === 'fulfilled' && labelsRes.value.code === 200) setLabels(labelsRes.value.data); } catch (e) { console.error('[dashboard] fetch error', e); } finally { setLoading(false); } }, []); useEffect(() => { let timer: ReturnType; const poll = async () => { await fetchData(); timer = setTimeout(poll, REFRESH_INTERVAL); }; fetchData(); timer = setTimeout(poll, REFRESH_INTERVAL); return () => clearTimeout(timer); }, [fetchData]); const trendConfig = { data: trend.flatMap((d) => [ { date: d.date, value: d.total, type: intl.get('总执行') }, { date: d.date, value: d.success, type: intl.get('成功') }, { date: d.date, value: d.fail, type: intl.get('失败') }, ]), xField: 'date', yField: 'value', seriesField: 'type', smooth: true, height: 260, color: ['#1677ff', '#52c41a', '#ff4d4f'], legend: { position: 'top' as const }, theme: isDark ? 'dark' : 'light', xAxis: { label: { style: { fill: isDark ? '#aaa' : '#333' } }, }, yAxis: { label: { style: { fill: isDark ? '#aaa' : '#333' } }, grid: { line: { style: { stroke: isDark ? '#333' : '#eee' } } }, }, }; const runtimePagination = { pageSize: 5, showSizeChanger: false, showTotal: (total: number) => `共 ${total} 个`, }; if (loading) return ; return ( } /> } /> } /> setResult('success')} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setResult('success'); } }} >} /> setResult('failure')} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setResult('failure'); } }} >} /> } /> } /> {trend.length > 0 ? : } v ? `${(v / 1000).toFixed(1)}s` : '-' }, { title: intl.get('最长单次'), dataIndex: 'maxTime', width: 100, render: (v: number) => v ? `${(v / 1000).toFixed(1)}s` : '-' }, ]} />
v ? `${(v / 1000).toFixed(1)}s` : '-' }, { title: intl.get('成功率'), dataIndex: 'successRate', width: 80, render: (v: string) => `${v}%` }, ]} /> {labels.length > 0 && (
{v} }, { title: intl.get('任务数'), dataIndex: 'count', width: 80 }, { title: intl.get('今日执行'), dataIndex: 'todayRuns', width: 100 }, { title: intl.get('成功率'), dataIndex: 'successRate', width: 100, render: (v: string) => `${v}%` }, { title: intl.get('平均耗时'), dataIndex: 'avgTime', width: 120, render: (v: number) => v ? `${(v / 1000).toFixed(1)}s` : '-' }, ]} /> )} {intl.get('运行中')} {runtime?.runningCount || 0} {intl.get('排队中')} {runtime?.queuedCount || 0} } >
5 ? runtimePagination : false} size="small" locale={{ emptyText: }} columns={[ { title: intl.get('定时任务'), dataIndex: 'name', ellipsis: true, render: (name: string, record) => { const sameTaskCount = (runtime?.running || []).filter(r => r.id === record.id).length; return sameTaskCount > 1 ? {name} ×{sameTaskCount} : name; } }, { title: 'PID', dataIndex: 'pid', width: 80 }, { title: intl.get('已运行'), dataIndex: 'elapsed', width: 100, render: (v: number) => v ? formatSeconds(v) : '-' }, { title: intl.get('日志'), dataIndex: 'id', width: 60, render: (id, record) => { localStorage.setItem('logCron', String(id)); setLogCron({ id, name: record.name }); }}>{intl.get('查看')} }, ]} /> {runtime?.idleTasks && runtime.idleTasks.length > 0 && ( <>
{intl.get('24小时未运行')} ({runtime.idleTasks.length})
{v} }, ]} /> )} {system && ( { return `${intl.get('内存')}:${target}%`; }, }} />
{intl.get('负载 1m')}: {system.loadAvg?.[0] || '-'} | CPU: {system.cpus} {intl.get('核心')} | {system.platform}
)} {result && setResult(null)} />} {logCron && ( setLogCron(null)} /> )} ); }; export default Dashboard;