first commit
This commit is contained in:
224
frontend/src/pages/Dashboard/Dashboard.tsx
Normal file
224
frontend/src/pages/Dashboard/Dashboard.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Layout, Menu, Card, Row, Col, Statistic, Typography, Button, Tag, Spin } from 'antd'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
DatabaseOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
BarChartOutlined,
|
||||
AlertOutlined,
|
||||
WifiOutlined,
|
||||
DisconnectOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
|
||||
const { Header, Sider, Content } = Layout
|
||||
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 { user, logout, 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])
|
||||
|
||||
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 handleLogout = () => {
|
||||
logout()
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
const handleClearAuth = () => {
|
||||
clearAuth()
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
const handleRetry = () => {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{ key: '/', icon: <DashboardOutlined />, label: <Link to="/">仪表盘</Link> },
|
||||
{ key: '/datasources', icon: <DatabaseOutlined />, label: <Link to="/datasources">数据源</Link> },
|
||||
{ key: '/data', icon: <BarChartOutlined />, label: <Link to="/data">采集数据</Link> },
|
||||
{ key: '/users', icon: <UserOutlined />, label: <Link to="/users">用户管理</Link> },
|
||||
{ key: '/settings', icon: <SettingOutlined />, label: '系统配置' },
|
||||
]
|
||||
|
||||
if (loading && !stats) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
<Spin size="large" tip="加载中..." />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout className="dashboard-layout">
|
||||
<Sider width={240} className="dashboard-sider">
|
||||
<div style={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Title level={4} style={{ color: 'white', margin: 0 }}>智能星球</Title>
|
||||
</div>
|
||||
<Menu theme="dark" mode="inline" defaultSelectedKeys={['/']} items={menuItems} />
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header className="dashboard-header" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Text strong>欢迎, {user?.username}</Text>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
{wsConnected ? (
|
||||
<Tag icon={<WifiOutlined />} color="success">实时连接</Tag>
|
||||
) : (
|
||||
<Tag icon={<DisconnectOutlined />} color="default">离线</Tag>
|
||||
)}
|
||||
<Button type="link" danger onClick={handleLogout}>退出登录</Button>
|
||||
<Button type="link" onClick={handleClearAuth}>清除认证</Button>
|
||||
<Button type="link" icon={<ReloadOutlined />} onClick={handleRetry}>刷新</Button>
|
||||
</div>
|
||||
</Header>
|
||||
<Content className="dashboard-content">
|
||||
{error && (
|
||||
<Card style={{ marginBottom: 16, borderColor: '#ff4d4f' }}>
|
||||
<Text style={{ color: '#ff4d4f' }}>{error}</Text>
|
||||
</Card>
|
||||
)}
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="数据源总数" value={stats?.total_datasources || 0} prefix={<DatabaseOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="活跃数据源" value={stats?.active_datasources || 0} valueStyle={{ color: '#52c41a' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="今日任务" value={stats?.tasks_today || 0} prefix={<BarChartOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="成功率" value={stats?.success_rate || 0} suffix="%" valueStyle={{ color: '#1890ff' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic title="严重告警" value={stats?.alerts?.critical || 0} valueStyle={{ color: '#ff4d4f' }} prefix={<AlertOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic title="警告" value={stats?.alerts?.warning || 0} valueStyle={{ color: '#faad14' }} prefix={<AlertOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic title="提示" value={stats?.alerts?.info || 0} valueStyle={{ color: '#1890ff' }} prefix={<AlertOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
{stats?.last_updated && (
|
||||
<div style={{ marginTop: 16, textAlign: 'center', color: '#8c8c8c' }}>
|
||||
最后更新: {new Date(stats.last_updated).toLocaleString('zh-CN')}
|
||||
{wsConnected && <Tag color="green" style={{ marginLeft: 8 }}>实时同步中</Tag>}
|
||||
</div>
|
||||
)}
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
Reference in New Issue
Block a user