200 lines
5.9 KiB
TypeScript
200 lines
5.9 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Card, Row, Col, Statistic, Typography, Button, Tag, Spin, Space } from 'antd'
|
|
import {
|
|
DatabaseOutlined,
|
|
BarChartOutlined,
|
|
AlertOutlined,
|
|
WifiOutlined,
|
|
DisconnectOutlined,
|
|
ReloadOutlined,
|
|
} from '@ant-design/icons'
|
|
import { useAuthStore } from '../../stores/auth'
|
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
|
|
|
const { Title, Text } = Typography
|
|
|
|
interface Stats {
|
|
total_datasources: number
|
|
active_datasources: number
|
|
tasks_today: number
|
|
success_rate: number
|
|
last_updated: string
|
|
alerts: {
|
|
critical: number
|
|
warning: number
|
|
info: number
|
|
}
|
|
}
|
|
|
|
function Dashboard() {
|
|
const { token, clearAuth } = useAuthStore()
|
|
const [stats, setStats] = useState<Stats | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [wsConnected, setWsConnected] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
if (!token) return
|
|
|
|
const fetchStats = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const res = await fetch('/api/v1/dashboard/stats', {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
})
|
|
if (res.status === 401) {
|
|
clearAuth()
|
|
window.location.href = '/'
|
|
return
|
|
}
|
|
const data = await res.json()
|
|
setStats(data)
|
|
setError(null)
|
|
} catch (err) {
|
|
setError('获取数据失败')
|
|
console.error(err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
fetchStats()
|
|
}, [token, clearAuth])
|
|
|
|
useEffect(() => {
|
|
if (!token) return
|
|
|
|
let ws: WebSocket | null = null
|
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
const connectWs = () => {
|
|
try {
|
|
ws = new WebSocket(`ws://localhost:8000/ws?token=${token}`)
|
|
|
|
ws.onopen = () => {
|
|
setWsConnected(true)
|
|
ws?.send(JSON.stringify({ type: 'subscribe', data: { channels: ['dashboard'] } }))
|
|
}
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const msg = JSON.parse(event.data)
|
|
if (msg.type === 'data_frame' && msg.channel === 'dashboard') {
|
|
setStats(msg.payload?.stats as Stats)
|
|
}
|
|
} catch (e) {
|
|
console.error('Parse WS message error:', e)
|
|
}
|
|
}
|
|
|
|
ws.onclose = () => {
|
|
setWsConnected(false)
|
|
reconnectTimer = setTimeout(connectWs, 3000)
|
|
}
|
|
|
|
ws.onerror = () => {
|
|
setWsConnected(false)
|
|
}
|
|
} catch (e) {
|
|
console.error('WS connect error:', e)
|
|
}
|
|
}
|
|
|
|
connectWs()
|
|
|
|
return () => {
|
|
ws?.close()
|
|
if (reconnectTimer) clearTimeout(reconnectTimer)
|
|
}
|
|
}, [token])
|
|
|
|
const handleRetry = () => {
|
|
window.location.reload()
|
|
}
|
|
|
|
if (loading && !stats) {
|
|
return (
|
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
|
<Spin size="large" tip="加载中..." />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<AppLayout>
|
|
<div className="dashboard-page">
|
|
<div className="dashboard-page__header">
|
|
<div>
|
|
<Title level={4} style={{ margin: 0 }}>仪表盘</Title>
|
|
<Text type="secondary">系统总览与实时态势</Text>
|
|
</div>
|
|
<Space wrap className="dashboard-page__actions">
|
|
{wsConnected ? (
|
|
<Tag className="dashboard-status-tag" icon={<WifiOutlined />} color="success">实时连接</Tag>
|
|
) : (
|
|
<Tag className="dashboard-status-tag" icon={<DisconnectOutlined />} color="default">离线</Tag>
|
|
)}
|
|
<Button className="dashboard-refresh-button" icon={<ReloadOutlined />} onClick={handleRetry}>刷新</Button>
|
|
</Space>
|
|
</div>
|
|
|
|
{error && (
|
|
<Card style={{ borderColor: '#ff4d4f' }}>
|
|
<Text style={{ color: '#ff4d4f' }}>{error}</Text>
|
|
</Card>
|
|
)}
|
|
|
|
<Row gutter={[16, 16]}>
|
|
<Col xs={24} sm={12} xl={6}>
|
|
<Card>
|
|
<Statistic title="数据源总数" value={stats?.total_datasources || 0} prefix={<DatabaseOutlined />} />
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={12} xl={6}>
|
|
<Card>
|
|
<Statistic title="活跃数据源" value={stats?.active_datasources || 0} valueStyle={{ color: '#52c41a' }} />
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={12} xl={6}>
|
|
<Card>
|
|
<Statistic title="今日任务" value={stats?.tasks_today || 0} prefix={<BarChartOutlined />} />
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} sm={12} xl={6}>
|
|
<Card>
|
|
<Statistic title="成功率" value={stats?.success_rate || 0} suffix="%" valueStyle={{ color: '#1890ff' }} />
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={[16, 16]}>
|
|
<Col xs={24} md={8}>
|
|
<Card>
|
|
<Statistic title="严重告警" value={stats?.alerts?.critical || 0} valueStyle={{ color: '#ff4d4f' }} prefix={<AlertOutlined />} />
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} md={8}>
|
|
<Card>
|
|
<Statistic title="警告" value={stats?.alerts?.warning || 0} valueStyle={{ color: '#faad14' }} prefix={<AlertOutlined />} />
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} md={8}>
|
|
<Card>
|
|
<Statistic title="提示" value={stats?.alerts?.info || 0} valueStyle={{ color: '#1890ff' }} prefix={<AlertOutlined />} />
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
{stats?.last_updated && (
|
|
<div style={{ textAlign: 'center', color: '#8c8c8c' }}>
|
|
最后更新: {new Date(stats.last_updated).toLocaleString('zh-CN')}
|
|
{wsConnected && <Tag className="dashboard-status-tag" color="green" style={{ marginLeft: 8 }}>实时同步中</Tag>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</AppLayout>
|
|
)
|
|
}
|
|
|
|
export default Dashboard
|