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 (
)
}
/* ---------- 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 ?? '-'}
))}
) : (
暂无数据
)}
)
}