Files
yunrui_asset/frontend/src/pages/Dashboard.jsx
T

523 lines
15 KiB
React
Raw Normal View History

import React, { useState, useCallback, useMemo } from 'react';
import { Card, Row, Col, Typography, message } from 'antd';
import { DatabaseOutlined, CloudServerOutlined, WarningOutlined, HomeOutlined, TeamOutlined, BarChartOutlined, DashboardOutlined } from '@ant-design/icons';
import api from '../api';
import { designTokens } from '../config/theme';
import { useFetch } from '../hooks/useSWR';
import {
AnimatedCounter,
CircularProgress,
PowerGauge,
DeviceTrendChart,
StatusLegend,
StatCard,
NavigationGrid,
QuickStats,
SystemInfo,
} from '../components/dashboard';
2025-12-18 10:45:13 +08:00
const { Title } = Typography;
const containerStyle = {
minHeight: '100vh',
background: 'linear-gradient(180deg, #f5f7fa 0%, #e8ecf1 100%)',
padding: 'clamp(12px, 3vw, 20px)',
};
const headerStyle = {
textAlign: 'center',
marginBottom: '32px',
padding: 'clamp(20px, 5vw, 32px) clamp(16px, 4vw, 48px)',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: 'clamp(12px, 3vw, 20px)',
color: '#fff',
boxShadow: '0 8px 32px rgba(102, 126, 234, 0.3)',
animation: 'fadeInDown 0.6s ease-out',
};
const titleStyle = {
fontSize: 'clamp(1.4rem, 4vw, 2.2rem)',
fontWeight: '700',
color: '#fff',
margin: '0 0 8px 0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 'clamp(8px, 2vw, 16px)',
flexWrap: 'wrap',
textAlign: 'center',
};
const subtitleStyle = {
fontSize: 'clamp(0.85rem, 2.5vw, 1.1rem)',
color: 'rgba(255, 255, 255, 0.85)',
margin: '0',
textAlign: 'center',
};
const progressCardStyle = {
borderRadius: designTokens.borderRadius.large,
border: 'none',
boxShadow: designTokens.shadows.medium,
background: '#fff',
height: '100%',
animation: 'fadeInUp 0.6s ease-out 0.3s backwards',
};
const chartContainerStyle = {
padding: '20px',
borderRadius: designTokens.borderRadius.medium,
background: 'linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%)',
border: '1px solid #f0f0f0',
};
const PIE_CHART_COLORS = {
success: designTokens.colors.success.main,
warning: designTokens.colors.warning.main,
error: designTokens.colors.error.main,
primary: designTokens.colors.primary.main,
};
const pieChartStyle = {
width: '180px',
height: '180px',
borderRadius: '50%',
background: `conic-gradient(
${PIE_CHART_COLORS.success} 0deg 216deg,
${PIE_CHART_COLORS.warning} 216deg 288deg,
${PIE_CHART_COLORS.error} 288deg 324deg,
${PIE_CHART_COLORS.primary} 324deg 360deg
)`,
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
};
const pieChartInner = {
width: '120px',
height: '120px',
borderRadius: '50%',
background: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
};
const overviewCardStyle = {
borderRadius: designTokens.borderRadius.large,
border: 'none',
boxShadow: designTokens.shadows.medium,
background: '#fff',
animation: 'fadeInUp 0.6s ease-out 0.2s backwards',
};
2025-12-18 10:45:13 +08:00
function Dashboard() {
const [hoveredCard, setHoveredCard] = useState(null);
const [animatedKey, setAnimatedKey] = useState(0);
2025-12-18 10:45:13 +08:00
const { data: devicesData, isLoading: devicesLoading, mutate: mutateDevices } = useFetch(
'/devices?pageSize=1'
);
const { data: racksData, isLoading: racksLoading } = useFetch('/racks?pageSize=1');
const { data: roomsData, isLoading: roomsLoading } = useFetch('/rooms');
const { data: usersData, isLoading: usersLoading } = useFetch('/users?pageSize=1');
const { data: ticketsData, isLoading: ticketsLoading } = useFetch('/tickets?pageSize=1&status=open');
const { data: faultData, isLoading: faultLoading } = useFetch('/devices?status=fault&pageSize=1');
const loading = devicesLoading || racksLoading || roomsLoading || usersLoading || ticketsLoading;
const isRefreshing = devicesLoading && devicesData;
const stats = useMemo(() => {
const totalDevices = devicesData?.total || 0;
const totalRacks = racksData?.total || 0;
const rooms = roomsData || [];
const totalRooms = Array.isArray(rooms) ? rooms.length : 0;
const totalUsers = usersData?.total || 0;
const activeTickets = ticketsData?.total || 0;
const faultDevices = faultData?.total || 0;
return {
totalDevices,
totalRacks,
totalRooms,
totalUsers,
activeTickets,
faultDevices,
deviceGrowth: 2.5,
faultTrend: -12.3,
onlineRate:
totalDevices > 0
? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1)
: 100,
powerUsage: Math.floor(Math.random() * 5000) + 2000,
};
}, [devicesData, racksData, roomsData, usersData, ticketsData, faultData]);
const handleRefresh = useCallback(async () => {
setAnimatedKey((prev) => prev + 1);
await Promise.all([
mutateDevices(),
]);
}, [mutateDevices]);
const handleHover = useCallback((key) => {
setHoveredCard(key);
2025-12-18 10:45:13 +08:00
}, []);
const statCards = useMemo(
() => [
{
key: 'devices',
xs: 12,
sm: 12,
md: 8,
lg: 6,
xl: 4,
icon: CloudServerOutlined,
color: designTokens.colors.primary.main,
statKey: 'totalDevices',
title: '总设备数',
trend: stats.deviceGrowth,
tagColor: 'blue',
delay: 0,
},
{
key: 'racks',
xs: 12,
sm: 12,
md: 8,
lg: 6,
xl: 4,
icon: DatabaseOutlined,
color: '#722ed1',
statKey: 'totalRacks',
title: '总机柜数',
trend: 0,
tagColor: 'green',
customStatus: true,
delay: 1,
},
{
key: 'rooms',
xs: 12,
sm: 12,
md: 8,
lg: 6,
xl: 4,
icon: HomeOutlined,
color: designTokens.colors.success.main,
statKey: 'totalRooms',
title: '总机房数',
trend: 0,
tagColor: 'green',
customStatus: true,
delay: 2,
},
{
key: 'faults',
xs: 12,
sm: 12,
md: 8,
lg: 6,
xl: 4,
icon: WarningOutlined,
color: designTokens.colors.error.main,
statKey: 'faultDevices',
title: '故障设备',
trend: stats.faultTrend,
tagColor: 'red',
delay: 3,
},
{
key: 'users',
xs: 12,
sm: 12,
md: 8,
lg: 6,
xl: 4,
icon: TeamOutlined,
color: '#13c2c2',
statKey: 'totalUsers',
title: '用户总数',
trend: 5.2,
tagColor: 'cyan',
delay: 4,
},
{
key: 'tickets',
xs: 12,
sm: 12,
md: 8,
lg: 6,
xl: 4,
icon: BarChartOutlined,
color: '#fa8c16',
statKey: 'activeTickets',
title: '待处理工单',
trend: -8.5,
tagColor: 'orange',
delay: 5,
},
],
[stats.deviceGrowth, stats.faultTrend]
);
const deviceTrendData = useMemo(
() => [
{ label: '周一', value: 45, color: designTokens.colors.primary.main },
{ label: '周二', value: 52, color: designTokens.colors.primary.main },
{ label: '周三', value: 48, color: designTokens.colors.primary.main },
{ label: '周四', value: 60, color: designTokens.colors.success.main },
{ label: '周五', value: 55, color: designTokens.colors.success.main },
{ label: '周六', value: 42, color: designTokens.colors.warning.main },
{ label: '周日', value: 58, color: designTokens.colors.primary.main },
],
[]
);
const styles = `
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`;
2025-12-18 10:45:13 +08:00
return (
<>
<style>{styles}</style>
<div style={containerStyle} className="dashboard-container">
<div style={headerStyle} className="dashboard-header">
<h1 style={titleStyle} className="dashboard-title">
<DashboardOutlined />
IDC设备管理系统
</h1>
<p style={subtitleStyle} className="dashboard-subtitle">
实时监控 · 智能管理 · 高效运维
</p>
</div>
<Row gutter={[24, 24]} style={{ marginBottom: '32px' }}>
{statCards.map((config) => (
<StatCard
key={config.statKey}
config={config}
stats={stats}
loading={loading}
animatedKey={animatedKey}
hoveredCard={hoveredCard}
onHover={handleHover}
/>
))}
</Row>
<Row gutter={[24, 24]} style={{ marginBottom: '24px' }}>
<Col xs={24} md={8}>
<Card style={progressCardStyle}>
<div style={{ padding: '20px' }}>
<Title
level={5}
style={{ margin: '0 0 20px 0', color: designTokens.colors.text.primary }}
>
设备状态分布
</Title>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={pieChartStyle}>
<div style={pieChartInner}>
<span
style={{
fontSize: '1.5rem',
fontWeight: '700',
color: designTokens.colors.text.primary,
}}
>
{stats.totalDevices}
</span>
<span
style={{ fontSize: '0.75rem', color: designTokens.colors.text.secondary }}
>
设备总数
</span>
</div>
</div>
<StatusLegend />
</div>
</div>
</Card>
</Col>
<Col xs={24} md={8}>
<Card style={progressCardStyle}>
<div style={{ padding: '20px' }}>
<Title
level={5}
style={{ margin: '0 0 20px 0', color: designTokens.colors.text.primary }}
>
系统健康指标
</Title>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '24px',
}}
>
<CircularProgress
percentage={parseFloat(stats.onlineRate)}
size={140}
strokeWidth={12}
color={designTokens.colors.success.main}
label="在线率"
/>
<PowerGauge value={stats.powerUsage} maxValue={10000} />
</div>
</div>
</Card>
</Col>
<Col xs={24} md={8}>
<Card style={progressCardStyle}>
<div style={{ padding: '20px' }}>
<Title
level={5}
style={{ margin: '0 0 20px 0', color: designTokens.colors.text.primary }}
>
周设备趋势
</Title>
<div style={chartContainerStyle}>
<DeviceTrendChart data={deviceTrendData} />
</div>
<div
style={{
marginTop: '16px',
display: 'flex',
justifyContent: 'center',
gap: '16px',
}}
>
<span style={{ fontSize: '0.75rem', color: designTokens.colors.text.tertiary }}>
周一 周日 设备变化趋势
</span>
</div>
</div>
</Card>
</Col>
</Row>
<div>
<Card style={overviewCardStyle}>
<div style={{ padding: '24px' }}>
<Row gutter={[24, 24]}>
<Col xs={24} md={16}>
<div
className="welcome-banner"
style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
borderRadius: designTokens.borderRadius.medium,
padding: '28px',
color: '#fff',
marginBottom: '24px',
animation: 'fadeInUp 0.6s ease-out 0.4s backwards',
}}
>
<h2
style={{
fontSize: '1.4rem',
fontWeight: '600',
margin: '0 0 8px 0',
color: '#fff',
}}
>
欢迎使用IDC设备管理系统
</h2>
<p
style={{
fontSize: '1rem',
margin: '0',
opacity: '0.9',
color: '#fff',
}}
>
专业的机房设备管理解决方案提供全方位的设备监控和管理能力
</p>
</div>
<QuickStats onlineRate={stats.onlineRate} powerUsage={stats.powerUsage} />
</Col>
<Col xs={24} md={8}>
<SystemInfo onRefresh={handleRefresh} isRefreshing={isRefreshing} />
</Col>
</Row>
<NavigationGrid hoveredCard={hoveredCard} onHover={handleHover} />
</div>
</Card>
</div>
<style>{`
@media (max-width: 576px) {
.dashboard-container {
padding: 12px !important;
}
.dashboard-header {
padding: 20px 16px !important;
border-radius: 12px !important;
}
.dashboard-title {
font-size: 1.4rem !important;
gap: 8px !important;
}
.dashboard-subtitle {
font-size: 0.85rem !important;
}
.stat-value {
font-size: 1.6rem !important;
}
.nav-grid {
grid-template-columns: repeat(3, 1fr) !important;
gap: 8px !important;
}
.nav-button {
padding: 12px 8px !important;
}
.nav-icon {
width: 40px !important;
height: 40px !important;
font-size: 20px !important;
}
.nav-text {
font-size: 0.7rem !important;
}
}
@media (max-width: 375px) {
.nav-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
}
`}</style>
</div>
</>
2025-12-18 10:45:13 +08:00
);
}
export default React.memo(Dashboard);