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

1401 lines
40 KiB
React
Raw Normal View History

import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { Card, Row, Col, Statistic, message, Button, Tag, Typography, Progress, Spin } from 'antd';
import {
DatabaseOutlined,
CloudServerOutlined,
WarningOutlined,
HomeOutlined,
SettingOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DashboardOutlined,
ReloadOutlined,
EnvironmentOutlined,
LineChartOutlined,
SafetyOutlined,
TeamOutlined,
ThunderboltOutlined,
AppstoreOutlined,
BarChartOutlined,
} from '@ant-design/icons';
import api from '../api';
2025-12-18 10:45:13 +08:00
const { Title, Text } = Typography;
const designTokens = {
colors: {
primary: {
main: '#1890ff',
light: '#40a9ff',
dark: '#096dd9',
gradient: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
bgGradient: 'linear-gradient(135deg, #1890ff15 0%, #096dd908 100%)',
},
success: {
main: '#52c41a',
light: '#73d13d',
dark: '#389e0d',
gradient: 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)',
bgGradient: 'linear-gradient(135deg, #52c41a15 0%, #389e0d08 100%)',
},
warning: {
main: '#faad14',
light: '#ffc53d',
dark: '#d48806',
gradient: 'linear-gradient(135deg, #faad14 0%, #d48806 100%)',
bgGradient: 'linear-gradient(135deg, #faad1415 0%, #d4880608 100%)',
},
error: {
main: '#ff4d4f',
light: '#ff7875',
dark: '#cf1322',
gradient: 'linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%)',
bgGradient: 'linear-gradient(135deg, #ff4d4f15 0%, #cf132208 100%)',
},
purple: {
main: '#722ed1',
light: '#9254de',
dark: '#531dab',
gradient: 'linear-gradient(135deg, #722ed1 0%, #531dab 100%)',
bgGradient: 'linear-gradient(135deg, #722ed115 0%, #531dab08 100%)',
},
cyan: {
main: '#13c2c2',
light: '#36cfc9',
dark: '#08979c',
gradient: 'linear-gradient(135deg, #13c2c2 0%, #08979c 100%)',
bgGradient: 'linear-gradient(135deg, #13c2c215 0%, #08979c08 100%)',
},
text: {
primary: '#262626',
secondary: '#8c8c8c',
tertiary: '#bfbfbf',
},
},
shadows: {
small: '0 2px 8px rgba(0, 0, 0, 0.06)',
medium: '0 4px 16px rgba(0, 0, 0, 0.08)',
large: '0 8px 24px rgba(0, 0, 0, 0.12)',
hover: '0 12px 32px rgba(0, 0, 0, 0.15)',
},
borderRadius: {
small: '8px',
medium: '12px',
large: '16px',
xl: '20px',
},
transitions: {
fast: '0.15s ease',
normal: '0.3s cubic-bezier(0.4, 0, 0.2, 1)',
slow: '0.5s cubic-bezier(0.4, 0, 0.2, 1)',
},
};
const responsiveConfig = {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 8 },
lg: { span: 6 },
xl: { span: 5 },
xxl: { span: 4 },
};
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 trendItemStyle = {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 0',
borderBottom: '1px solid #f0f0f0',
};
const STAT_CARD_BASE_STYLE = {
borderRadius: designTokens.borderRadius.large,
border: 'none',
boxShadow: designTokens.shadows.medium,
background: '#fff',
transition: `all ${designTokens.transitions.normal}`,
overflow: 'hidden',
cursor: 'pointer',
height: '100%',
animation: 'fadeInUp 0.6s ease-out backwards',
borderLeft: '4px solid transparent',
};
const STAT_ICON_CONTAINER_BASE = {
width: 'clamp(40px, 8vw, 64px)',
height: 'clamp(40px, 8vw, 64px)',
borderRadius: designTokens.borderRadius.medium,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 'clamp(20px, 4vw, 32px)',
transition: `all ${designTokens.transitions.normal}`,
flexShrink: 0,
};
const NAV_BUTTON_BASE = {
height: 'auto',
padding: 'clamp(16px, 4vw, 24px) clamp(12px, 3vw, 20px)',
borderRadius: designTokens.borderRadius.medium,
border: '2px solid #f0f0f0',
background: '#fff',
transition: `all ${designTokens.transitions.normal}`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 'clamp(8px, 2vw, 12px)',
cursor: 'pointer',
boxShadow: designTokens.shadows.small,
minWidth: '0',
};
const NAV_ICON_CONTAINER_BASE = {
width: 'clamp(44px, 10vw, 60px)',
height: 'clamp(44px, 10vw, 60px)',
borderRadius: designTokens.borderRadius.medium,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 'clamp(20px, 5vw, 28px)',
transition: `all ${designTokens.transitions.normal}`,
flexShrink: 0,
};
const createStatCardStyle = color => ({
...STAT_CARD_BASE_STYLE,
borderLeftColor: color,
});
const createStatIconContainer = color => ({
...STAT_ICON_CONTAINER_BASE,
background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`,
});
const createNavButtonStyle = color => ({
...NAV_BUTTON_BASE,
borderColor: color,
});
const createNavIconContainer = color => ({
...NAV_ICON_CONTAINER_BASE,
background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`,
});
const overviewCardStyle = {
borderRadius: designTokens.borderRadius.large,
border: 'none',
boxShadow: designTokens.shadows.medium,
background: '#fff',
animation: 'fadeInUp 0.6s ease-out 0.2s backwards',
};
const navigationGridStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))',
gap: 'clamp(8px, 2vw, 16px)',
marginBottom: '24px',
};
const navButtonStyle = color => ({
height: 'auto',
padding: '24px 20px',
borderRadius: designTokens.borderRadius.medium,
border: '2px solid #f0f0f0',
background: '#fff',
transition: `all ${designTokens.transitions.normal}`,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '12px',
cursor: 'pointer',
boxShadow: designTokens.shadows.small,
});
const navIconContainer = color => ({
width: '60px',
height: '60px',
borderRadius: designTokens.borderRadius.medium,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`,
fontSize: '28px',
transition: `all ${designTokens.transitions.normal}`,
});
const navTextStyle = {
fontSize: 'clamp(0.75rem, 2vw, 0.9rem)',
fontWeight: '600',
color: designTokens.colors.text.primary,
textAlign: 'center',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%',
};
const systemInfoStyle = {
background: 'linear-gradient(135deg, #f0f7ff 0%, #e6f7ff 100%)',
borderRadius: designTokens.borderRadius.medium,
padding: '20px',
border: '1px solid #91d5ff',
};
const quickStatsStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
gap: '16px',
marginBottom: '24px',
};
const quickStatItemStyle = {
background: 'linear-gradient(135deg, #fff 0%, #fafafa 100%)',
borderRadius: designTokens.borderRadius.medium,
padding: '20px',
display: 'flex',
alignItems: 'center',
gap: '16px',
border: '1px solid #f0f0f0',
boxShadow: designTokens.shadows.small,
};
const AnimatedCounter = ({ value, duration = 1500 }) => {
const [displayValue, setDisplayValue] = useState(0);
const animationRef = useRef(null);
const startTimeRef = useRef(null);
useEffect(() => {
const animate = currentTime => {
if (!startTimeRef.current) {
startTimeRef.current = currentTime;
}
const elapsed = currentTime - startTimeRef.current;
const progress = Math.min(elapsed / duration, 1);
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const currentValue = Math.floor(easeOutQuart * value);
setDisplayValue(currentValue);
if (progress < 1) {
animationRef.current = requestAnimationFrame(animate);
}
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [value, duration]);
return <span>{displayValue}</span>;
};
const CircularProgress = ({ percentage, size = 120, strokeWidth = 10, color, label }) => {
const circumference = 2 * Math.PI * ((size - strokeWidth) / 2);
const offset = circumference - (percentage / 100) * circumference;
return (
<div style={{ position: 'relative', width: size, height: size }}>
<svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}>
<circle
cx={size / 2}
cy={size / 2}
r={(size - strokeWidth) / 2}
fill="none"
stroke="#f0f0f0"
strokeWidth={strokeWidth}
/>
<circle
cx={size / 2}
cy={size / 2}
r={(size - strokeWidth) / 2}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
style={{
transition: 'stroke-dashoffset 1s ease-out',
filter: `drop-shadow(0 0 6px ${color}40)`,
}}
/>
</svg>
<div
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center',
}}
>
<div
style={{ fontSize: '1.5rem', fontWeight: '700', color: designTokens.colors.text.primary }}
>
{percentage}%
</div>
<div style={{ fontSize: '0.75rem', color: designTokens.colors.text.secondary }}>
{label}
</div>
</div>
</div>
);
};
const PowerGauge = ({ value, maxValue }) => {
const percentage = Math.min((value / maxValue) * 100, 100);
const getColor = () => {
if (percentage >= 80) return designTokens.colors.error.main;
if (percentage >= 60) return designTokens.colors.warning.main;
return designTokens.colors.success.main;
};
return (
<div style={{ marginTop: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<Text style={{ fontSize: '0.85rem', color: designTokens.colors.text.secondary }}>
功率使用率
</Text>
<Text style={{ fontSize: '0.85rem', fontWeight: '600', color: getColor() }}>
{percentage.toFixed(1)}%
</Text>
</div>
<div
style={{
height: '8px',
borderRadius: '4px',
background: '#f0f0f0',
overflow: 'hidden',
}}
>
<div
style={{
height: '100%',
borderRadius: '4px',
background: `linear-gradient(90deg, ${getColor()}, ${getColor()}80)`,
width: `${percentage}%`,
transition: 'width 0.8s ease-out',
boxShadow: `0 0 8px ${getColor()}40`,
}}
/>
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: '4px',
fontSize: '0.75rem',
color: designTokens.colors.text.tertiary,
}}
>
<span>{value}W</span>
<span>{maxValue}W</span>
</div>
</div>
);
};
const DeviceTrendChart = ({ data }) => {
const maxValue = Math.max(...data.map(d => d.value));
const chartHeight = 120;
return (
<div style={{ marginTop: '16px' }}>
<div
style={{
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'space-between',
height: chartHeight,
gap: '8px',
padding: '0 8px',
}}
>
{data.map((item, index) => (
<div
key={index}
style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }}
>
<div
style={{
width: '100%',
maxWidth: '40px',
height: `${(item.value / maxValue) * chartHeight}px`,
borderRadius: '4px 4px 0 0',
background: `linear-gradient(180deg, ${item.color} 0%, ${item.color}80 100%)`,
transition: `height ${designTokens.transitions.slow}`,
boxShadow: `0 -2px 8px ${item.color}30`,
}}
/>
<span
style={{
fontSize: '0.7rem',
color: designTokens.colors.text.tertiary,
marginTop: '4px',
whiteSpace: 'nowrap',
}}
>
{item.label}
</span>
</div>
))}
</div>
</div>
);
};
const StatusLegend = () => {
const legends = [
{ color: designTokens.colors.success.main, label: '运行中', percent: 60 },
{ color: designTokens.colors.warning.main, label: '维护中', percent: 20 },
{ color: designTokens.colors.error.main, label: '故障', percent: 10 },
{ color: designTokens.colors.primary.main, label: '离线', percent: 10 },
];
return (
<div style={{ marginTop: '16px' }}>
{legends.map((item, index) => (
<div
key={index}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '8px 0',
borderBottom: index < legends.length - 1 ? '1px solid #f5f5f5' : 'none',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div
style={{
width: '12px',
height: '12px',
borderRadius: '3px',
background: item.color,
boxShadow: `0 0 6px ${item.color}40`,
}}
/>
<Text style={{ fontSize: '0.85rem', color: designTokens.colors.text.secondary }}>
{item.label}
</Text>
</div>
<Text
style={{
fontSize: '0.85rem',
fontWeight: '600',
color: designTokens.colors.text.primary,
}}
>
{item.percent}%
</Text>
</div>
))}
</div>
);
};
const navButtonsData = [
{
key: 'devices',
icon: CloudServerOutlined,
text: '设备管理',
path: '/devices',
color: designTokens.colors.primary.main,
},
{
key: 'racks',
icon: DatabaseOutlined,
text: '资源规划',
path: '/racks',
color: designTokens.colors.purple.main,
},
{
key: 'faults',
icon: WarningOutlined,
text: '故障监控',
path: '/faults',
color: designTokens.colors.warning.main,
},
{
key: 'tickets',
icon: BarChartOutlined,
text: '工单管理',
path: '/tickets',
color: designTokens.colors.cyan.main,
},
{
key: 'consumables',
icon: AppstoreOutlined,
text: '耗材管理',
path: '/consumables',
color: '#fa8c16',
},
{
key: 'settings',
icon: SettingOutlined,
text: '系统配置',
path: '/settings',
color: designTokens.colors.success.main,
},
];
2025-12-18 10:45:13 +08:00
function Dashboard() {
const [stats, setStats] = useState({
totalDevices: 0,
totalRacks: 0,
totalRooms: 0,
faultDevices: 0,
deviceGrowth: 2.5,
faultTrend: -12.3,
onlineRate: 98.5,
powerUsage: 0,
totalUsers: 0,
activeTickets: 0,
});
2025-12-18 10:45:13 +08:00
const [loading, setLoading] = useState(true);
const [hoveredCard, setHoveredCard] = useState(null);
const [isRefreshing, setIsRefreshing] = useState(false);
const [animatedKey, setAnimatedKey] = useState(0);
2025-12-18 10:45:13 +08:00
const fetchStats = useCallback(async () => {
try {
setIsRefreshing(true);
const [devicesRes, racksRes, roomsRes, usersRes, ticketsRes] = await Promise.all([
api.get('/devices', { params: { pageSize: 1 } }),
api.get('/racks', { params: { pageSize: 1 } }),
api.get('/rooms'),
api.get('/users', { params: { pageSize: 1 } }),
api.get('/tickets', { params: { pageSize: 1, status: 'open' } }),
]);
const totalDevices = devicesRes.total || 0;
const totalRacks = racksRes.total || 0;
const rooms = roomsRes || [];
const totalRooms = rooms.length;
const totalUsers = usersRes.total || 0;
const activeTickets = ticketsRes.total || 0;
let faultDevices = 0;
if (totalDevices > 0) {
try {
const faultRes = await api.get('/devices', {
params: { status: 'fault', pageSize: 1 },
});
faultDevices = faultRes.total || 0;
} catch (error) {
message.warning('获取故障设备数失败,使用默认值');
console.error('获取故障设备数失败:', error);
faultDevices = 0;
}
2025-12-18 10:45:13 +08:00
}
setStats({
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,
});
setAnimatedKey(prev => prev + 1);
} catch (error) {
message.error(`获取统计数据失败: ${error}`);
console.error('获取统计数据失败:', error);
} finally {
setLoading(false);
setIsRefreshing(false);
}
2025-12-18 10:45:13 +08:00
}, []);
useEffect(() => {
fetchStats();
}, [fetchStats]);
const handleNavHover = (e, isEnter, buttonKey) => {
if (isEnter) {
setHoveredCard(buttonKey);
} else {
setHoveredCard(null);
}
};
const handleRefresh = useCallback(() => {
fetchStats();
}, [fetchStats]);
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: designTokens.colors.purple.main,
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: designTokens.colors.cyan.main,
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 renderStatCard = useCallback(
config => {
const {
icon: Icon,
color,
statKey,
title,
trend,
tagColor,
customStatus,
xs,
sm,
lg,
xl,
delay,
} = config;
const colProps = { xs, sm, lg, xl };
const cardStyle = {
...createStatCardStyle(color),
...(hoveredCard === statKey
? { transform: 'translateY(-6px)', boxShadow: designTokens.shadows.hover }
: {}),
animationDelay: `${delay * 0.1}s`,
};
return (
<Col key={statKey} {...colProps}>
<Card
style={cardStyle}
onMouseEnter={() => setHoveredCard(statKey)}
onMouseLeave={() => setHoveredCard(null)}
styles={{ body: { padding: 'clamp(16px, 3vw, 24px)' } }}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '8px',
}}
>
<span
style={{
fontSize: 'clamp(0.75rem, 2vw, 0.9rem)',
fontWeight: '600',
color: designTokens.colors.text.secondary,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
flex: 1,
}}
>
{title}
</span>
<div style={createStatIconContainer(color)}>
<Icon style={{ color }} />
</div>
</div>
<div
className="stat-value"
style={{
fontSize: 'clamp(1.6rem, 4vw, 2.2rem)',
fontWeight: '700',
color: color,
lineHeight: 1,
whiteSpace: 'nowrap',
}}
>
{loading ? (
<Spin size="small" />
) : (
<AnimatedCounter key={`${animatedKey}-${statKey}`} value={stats[statKey]} />
)}
</div>
{customStatus ? (
statKey === 'totalRacks' ? (
<div
style={{
display: 'flex',
alignItems: 'center',
fontSize: 'clamp(0.7rem, 1.8vw, 0.85rem)',
color: designTokens.colors.success.main,
whiteSpace: 'nowrap',
}}
>
<span
style={{
width: '6px',
height: '6px',
background: designTokens.colors.success.main,
borderRadius: '50%',
marginRight: '6px',
boxShadow: '0 0 6px rgba(82, 196, 26, 0.5)',
flexShrink: 0,
}}
/>
<span>正常运行中</span>
</div>
) : (
<div
style={{
display: 'flex',
alignItems: 'center',
fontSize: 'clamp(0.7rem, 1.8vw, 0.85rem)',
color: designTokens.colors.success.main,
whiteSpace: 'nowrap',
}}
>
<span
style={{
width: '6px',
height: '6px',
background: designTokens.colors.success.main,
borderRadius: '50%',
marginRight: '6px',
boxShadow: '0 0 6px rgba(82, 196, 26, 0.5)',
flexShrink: 0,
}}
/>
<span>全部在线</span>
</div>
)
) : (
<div
style={{
display: 'flex',
alignItems: 'center',
fontSize: 'clamp(0.7rem, 1.8vw, 0.875rem)',
fontWeight: '500',
color:
trend > 0 ? designTokens.colors.success.main : designTokens.colors.error.main,
flexWrap: 'wrap',
gap: '4px',
}}
>
{trend > 0 ? (
<ArrowUpOutlined style={{ fontSize: '0.75rem' }} />
) : (
<ArrowDownOutlined style={{ fontSize: '0.75rem' }} />
)}
<span>{Math.abs(trend)}%</span>
<Tag
color={tagColor}
style={{
fontSize: 'clamp(0.6rem, 1.5vw, 0.75rem)',
borderRadius: '4px',
margin: 0,
padding: '0 4px',
lineHeight: '1.4',
}}
>
环比
</Tag>
</div>
)}
</div>
</Card>
</Col>
);
},
[stats, loading, hoveredCard, animatedKey]
);
const navButtons = useMemo(
() =>
navButtonsData.map(({ key, icon: Icon, text, color }) => (
<div
key={key}
className="nav-button"
style={{
...createNavButtonStyle(color),
...(hoveredCard === `nav-${key}`
? {
transform: 'translateY(-4px)',
boxShadow: designTokens.shadows.large,
borderColor: color,
}
: {}),
animationDelay: `${navButtonsData.findIndex(b => b.key === key) * 0.1}s`,
}}
onMouseEnter={e => handleNavHover(e, true, `nav-${key}`)}
onMouseLeave={e => handleNavHover(e, false, `nav-${key}`)}
>
<div className="nav-icon" style={createNavIconContainer(color)}>
<Icon style={{ color, fontSize: 'clamp(20px, 5vw, 28px)' }} />
</div>
<span className="nav-text" style={navTextStyle}>
{text}
</span>
</div>
)),
[hoveredCard]
);
const quickStats = useMemo(
() => [
{
icon: LineChartOutlined,
label: '在线率',
value: `${stats.onlineRate}%`,
color: designTokens.colors.success.main,
},
{
icon: SafetyOutlined,
label: '安全等级',
value: 'A级',
color: designTokens.colors.primary.main,
},
{
icon: ThunderboltOutlined,
label: '功率使用',
value: `${stats.powerUsage}W`,
color: designTokens.colors.warning.main,
},
],
[stats.onlineRate, stats.powerUsage]
);
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 systemInfo = useMemo(
() => (
<div style={systemInfoStyle}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: '16px',
}}
>
<div>
<p
style={{
margin: '0',
fontSize: '0.9rem',
color: designTokens.colors.text.primary,
fontWeight: '600',
}}
>
<strong>系统版本</strong> v1.0.0
</p>
<p
style={{
margin: '4px 0 0 0',
fontSize: '0.85rem',
color: designTokens.colors.text.secondary,
}}
>
<strong>最后更新</strong>
{new Date().toLocaleDateString()}
</p>
</div>
<Button
type="primary"
icon={<ReloadOutlined spin={isRefreshing} />}
size="small"
onClick={handleRefresh}
loading={isRefreshing}
style={{
background: designTokens.colors.primary.gradient,
border: 'none',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(24, 144, 255, 0.3)',
}}
>
刷新数据
</Button>
</div>
</div>
),
[handleRefresh, isRefreshing]
);
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(renderStatCard)}
</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',
}}
>
<Text style={{ fontSize: '0.75rem', color: designTokens.colors.text.tertiary }}>
周一 周日 设备变化趋势
</Text>
</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>
<div
style={{
...quickStatsStyle,
marginBottom: '0',
animation: 'fadeInUp 0.6s ease-out 0.5s backwards',
}}
>
{quickStats.map((stat, index) => (
<div key={index} style={quickStatItemStyle}>
<div
style={{
width: '48px',
height: '48px',
borderRadius: designTokens.borderRadius.medium,
background: `linear-gradient(135deg, ${stat.color}20 0%, ${stat.color}10 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px',
color: stat.color,
boxShadow: `0 4px 12px ${stat.color}20`,
}}
>
<stat.icon />
</div>
<div>
<Text
style={{
color: designTokens.colors.text.secondary,
fontSize: '0.85rem',
}}
>
{stat.label}
</Text>
<div
style={{
fontSize: '1.2rem',
fontWeight: '700',
color: designTokens.colors.text.primary,
}}
>
{stat.value}
</div>
</div>
</div>
))}
</div>
</Col>
<Col xs={24} md={8}>
<div style={{ animation: 'fadeInUp 0.6s ease-out 0.5s backwards' }}>
{systemInfo}
</div>
</Col>
</Row>
<div
className="nav-grid"
style={{
...navigationGridStyle,
animation: 'fadeInUp 0.6s ease-out 0.6s backwards',
}}
>
{navButtons}
</div>
</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);