import { useEffect, useState, useCallback } from 'react' import { useNavigate } from 'react-router-dom' import Card from '../components/ui/Card' import Skeleton from '../components/ui/Skeleton' import Timeline, { type TimelineEvent } from '../components/ui/Timeline' import toast from '../components/ui/Toast' import { getDashboardSummary, getHealthDistribution, getGatewayTrend, getKanbanSummary, getCronRecent, type SummaryData, type HealthDistribution, type GatewayTrendItem, type KanbanSummaryItem, type CronRecentItem, } from '../api/dashboard' /* ---------- helpers ---------- */ const hourLabel = (iso: string) => { const m = iso.match(/T(\d{2}):/) return m ? `${m[1]}:00` : iso } const statusIcon = (s: string) => { if (s === 'success') return '✅' if (s === 'failure') return '❌' if (s === 'paused') return '⏸' return '❓' } const trendColor = (item: GatewayTrendItem): string => { if (item.heartbeat_timeouts > 0) return 'var(--red)' if (item.feishu_disconnects > 0) return 'var(--orange)' if (item.restarts > 0) return 'var(--yellow)' return 'var(--green)' } /* ---------- bar-chart sub-component ---------- */ const GRADE_META = [ { key: 'A', color: 'var(--green)' }, { key: 'B', color: 'var(--blue)' }, { key: 'C', color: 'var(--yellow)' }, { key: 'D', color: 'var(--red)' }, ] as const function HealthBarChart({ dist, onBarClick, }: { dist: HealthDistribution onBarClick: (g: string) => void }) { if (!dist || dist.total === 0) { return

暂无数据

} const maxVal = Math.max(dist.A, dist.B, dist.C, dist.D, 1) return (
{GRADE_META.map(({ key, color }) => { const val = dist[key as keyof typeof dist] as number const pct = (val / maxVal) * 100 return (
onBarClick(key)} > {key}
{val}
) })}
) } /* ---------- main page ---------- */ export default function DashboardPage() { const navigate = useNavigate() const [summary, setSummary] = useState(null) const [health, setHealth] = useState(null) const [trend, setTrend] = useState([]) const [kanban, setKanban] = useState([]) const [cron, setCron] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const fetchAll = useCallback(async () => { setLoading(true) setError(null) try { const [s, h, t, k, c] = await Promise.all([ getDashboardSummary(), getHealthDistribution(), getGatewayTrend(), getKanbanSummary(), getCronRecent(), ]) setSummary(s.data) setHealth(h.data) setTrend(t.data) setKanban(k.data) setCron(c.data) } catch { setError('加载仪表盘数据失败') } finally { setLoading(false) } }, []) useEffect(() => { fetchAll() }, [fetchAll]) /* ---------- loading ---------- */ if (loading) { return (

蜂群总览

) } /* ---------- error ---------- */ if (error) { return (

蜂群总览

{error}

) } /* ---------- empty ---------- */ if (!summary) { return (

蜂群总览

暂无数据

) } /* ---------- trend -> timeline events ---------- */ const timelineEvents: TimelineEvent[] = trend.map((item) => ({ time: hourLabel(item.hour), type: item.heartbeat_timeouts > 0 ? 'red' : item.feishu_disconnects > 0 ? 'orange' : item.restarts > 0 ? 'orange' : 'green', label: ( {item.restarts > 0 && R:{item.restarts}} {item.feishu_disconnects > 0 && F:{item.feishu_disconnects}} {item.heartbeat_timeouts > 0 && H:{item.heartbeat_timeouts}} {item.restarts === 0 && item.feishu_disconnects === 0 && item.heartbeat_timeouts === 0 && ( OK )} ), color: trendColor(item), })) return (

蜂群总览

{/* ---- summary cards ---- */}
3 ? 'red' : 'yellow'} change={summary.cron_failures_today > 3 ? '需关注' : undefined} changeDirection={summary.cron_failures_today > 3 ? 'up' : undefined} />
{/* ---- health + trend ---- */}
{/* health distribution */}

蜂群健康分布

共 {health?.total ?? 0} 个 Profile

{health ? ( navigate(`/health?filter=${g}`)} /> ) : (

暂无数据

)}
{/* gateway trend */}

Gateway 异常趋势 (24h)

{trend.length > 0 ? (
) : (

暂无数据

)}
● 正常 ● 心跳超时 ● 飞书断连 ● 重启
{/* ---- kanban + cron ---- */}
{/* kanban overview */}

Kanban 概览

{kanban.length > 0 ? (
{kanban.map((item) => (
navigate('/kanban')} > {item.board} RUNNING: {item.running ?? '-'} READY: {item.ready ?? '-'}
))}
) : (

暂无数据

)}
{/* cron recent */}

Cron 最近执行

{cron.length > 0 ? (
{cron.map((item) => (
navigate('/cron')} > {item.job} {statusIcon(item.status)} {item.time ?? '-'}
))}
) : (

暂无数据

)}
) }