import { useEffect, useState, useCallback } from 'react' import { useNavigate } from 'react-router-dom' import Skeleton from '../components/ui/Skeleton' import { getDashboardSummary, getHealthDistribution, getGatewayTrend, getKanbanSummary, getCronRecent, type SummaryData, type HealthDistribution, type GatewayTrendItem, type KanbanSummaryItem, type CronRecentItem, } from '../api/dashboard' /* ---------- helpers ---------- */ function timeAgo(iso: string): string { const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 1000) if (diff < 5) return '刚刚' if (diff < 60) return diff + ' 秒前' const m = Math.floor(diff / 60) if (m < 60) return m + ' 分钟前' return Math.floor(m / 60) + ' 小时前' } const VAL_COLORS: Record = { green: 'var(--green)', red: 'var(--red)', yellow: 'var(--yellow)', blue: 'var(--blue)', } interface RecentEvent { time: string profile: string type: string typeClass: string summary: string status: string statusClass: string } const MOCK_EVENTS: RecentEvent[] = [ { time: '21:13', profile: 'hindsight', type: 'Cron 失败', typeClass: 'badge-offline', summary: 'Hindsight index 更新超时', status: '待处理', statusClass: 'badge-warn' }, { time: '18:45', profile: 'monitor', type: '心跳超时', typeClass: 'badge-warn', summary: 'Gateway 心跳 3 次无响应', status: '待处理', statusClass: 'badge-warn' }, { time: '14:20', profile: 'feishu-gateway', type: '飞书断连', typeClass: 'badge-warn', summary: 'WebSocket 断开 8 分钟后恢复', status: '已恢复', statusClass: 'badge-a' }, ] /* ---------- Stat card ---------- */ function StatCard({ label, value, valueColor, change, changeDirection, }: { label: string; value: string | number; valueColor?: string change?: string; changeDirection?: 'up' | 'down' }) { const dirIcon = changeDirection === 'up' ? '↑' : '↓' const dirColor = changeDirection === 'up' ? 'var(--green)' : 'var(--red)' return (
{label}
{value}
{change && (
{changeDirection && {dirIcon} } {change}
)}
) } /* ---------- Health vertical bar chart ---------- */ 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(function(meta) { var val = dist[meta.key]; var pct = (val / maxVal) * 100; return (
{val} {meta.key}级
) })}
) } /* ---------- Gateway 48-bar timeline ---------- */ function barColor(item: GatewayTrendItem): string { if (item.heartbeat_timeouts > 0) return 'var(--red)' if (item.feishu_disconnects > 0) return 'var(--orange)' return 'var(--green)' } function GatewayTimeline({ trend }: { trend: GatewayTrendItem[] }) { var bars = []; for (var i = 0; i < trend.length; i++) { var item = trend[i]; var col = barColor(item); var details = []; if (item.heartbeat_timeouts > 0) details.push('心跳超时 ' + item.heartbeat_timeouts); if (item.feishu_disconnects > 0) details.push('飞书断连 ' + item.feishu_disconnects); if (item.restarts > 0) details.push('重启 ' + item.restarts); var tip = details.length > 0 ? details.join(' / ') : '正常运行'; bars.push({ color: col, tooltip: item.hour + ': ' + tip }); bars.push({ color: col, tooltip: item.hour + ': ' + tip }); } return (
{bars.map(function(b, i) { return (
) })}
) } /* ---------- Badge helpers for events table ---------- */ function BadgeSpan({ cls, children }: { cls: string; children: React.ReactNode }) { var styleMap: Record = { 'badge-offline': { background: 'rgba(255,61,61,.15)', color: 'var(--red)' }, 'badge-warn': { background: 'rgba(255,171,0,.15)', color: 'var(--yellow)' }, 'badge-a': { background: 'rgba(0,200,83,.15)', color: 'var(--green)' }, } var baseStyle: React.CSSProperties = styleMap[cls] || { background: 'var(--bg-hover)', color: 'var(--text-dim)' } return ( {children} ) } /* ---------- main page ---------- */ export default function DashboardPage() { var navigate = useNavigate() var _a = useState(null), summary = _a[0], setSummary = _a[1] var _b = useState(null), health = _b[0], setHealth = _b[1] var _c = useState([]), trend = _c[0], setTrend = _c[1] var _d = useState([]), kanban = _d[0], setKanban = _d[1] var _e = useState([]), cron = _e[0], setCron = _e[1] var _f = useState(true), loading = _f[0], setLoading = _f[1] var _g = useState(null), error = _g[0], setError = _g[1] var fetchAll = useCallback(async function() { setLoading(true) setError(null) try { var res = await Promise.all([ getDashboardSummary(), getHealthDistribution(), getGatewayTrend(), getKanbanSummary(), getCronRecent(), ]) setSummary(res[0].data) setHealth(res[1].data) setTrend(res[2].data) setKanban(res[3].data) setCron(res[4].data) } catch (e) { setError('加载仪表盘数据失败') } finally { setLoading(false) } }, []) useEffect(function() { fetchAll() }, [fetchAll]) /* loading */ if (loading) { return (
蜂群总览
) } /* error */ if (error) { return (
蜂群总览

{error}

) } /* empty */ if (!summary) { return (
蜂群总览

暂无数据

) } /* subtitle */ var subtitle = summary.total_profiles + ' 个 Profile' + (summary.updated_at ? ' · 最后更新 ' + timeAgo(summary.updated_at) : '') return (
{/* page header */}
蜂群总览
{subtitle}
{/* stat cards */}
{/* health + trend */}
{/* health distribution */}
蜂群健康分布 查看全部 →
{health ? (
⚠ {health.d_profiles.length} 个 D 级 Profile: {health.d_profiles.join(' · ')}
) : null}
) : (

暂无数据

)}
{/* gateway trend */}
Gateway 异常趋势 (24h)
{trend.length > 0 ? :

暂无数据

}
● 正常运行 ● 心跳超时 ● 飞书断连
{/* kanban + cron */}
{/* kanban overview */}
Kanban 任务概览
看板 RUNNING READY
{kanban.length > 0 ? kanban.map(function(item) { return (
{item.board} {item.running || '—'} {item.ready || '—'}
) }) :

暂无数据

}
{/* cron recent */}
Cron 最近执行
Job 状态 时间
{cron.length > 0 ? cron.map(function(item) { return (
{item.job} {item.status === 'success' ? '✅' : item.status === 'failure' ? '❌' : item.status === 'paused' ? '⏸' : '❓'} {item.time || '—'}
) }) :

暂无数据

}
{/* recent events table */}
最近异常事件
{MOCK_EVENTS.map(function(evt, i) { return ( ) })}
时间 Profile 类型 摘要 状态
{evt.time} {evt.profile} {evt.type} {evt.summary} {evt.status}
) }