refactor: 移除调试日志并优化代码结构
feat(components): 新增设备管理相关组件和仪表盘组件 feat(hooks): 添加自定义hooks用于API调用和数据管理 style: 优化滚动条样式和模态框布局 chore: 清理无用脚本和调试文件 docs: 更新组件导出文件
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
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>;
|
||||
};
|
||||
|
||||
export default React.memo(AnimatedCounter);
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { designTokens } from '../../config/theme';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CircularProgress);
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { designTokens } from '../../config/theme';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(DeviceTrendChart);
|
||||
@@ -0,0 +1,139 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
CloudServerOutlined,
|
||||
DatabaseOutlined,
|
||||
WarningOutlined,
|
||||
BarChartOutlined,
|
||||
AppstoreOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { designTokens } from '../../config/theme';
|
||||
|
||||
const NAV_BUTTONS_DATA = [
|
||||
{
|
||||
key: 'devices',
|
||||
icon: CloudServerOutlined,
|
||||
text: '设备管理',
|
||||
path: '/devices',
|
||||
color: designTokens.colors.primary.main,
|
||||
},
|
||||
{
|
||||
key: 'racks',
|
||||
icon: DatabaseOutlined,
|
||||
text: '资源规划',
|
||||
path: '/racks',
|
||||
color: '#722ed1',
|
||||
},
|
||||
{
|
||||
key: 'faults',
|
||||
icon: WarningOutlined,
|
||||
text: '故障监控',
|
||||
path: '/faults',
|
||||
color: designTokens.colors.warning.main,
|
||||
},
|
||||
{
|
||||
key: 'tickets',
|
||||
icon: BarChartOutlined,
|
||||
text: '工单管理',
|
||||
path: '/tickets',
|
||||
color: '#13c2c2',
|
||||
},
|
||||
{
|
||||
key: 'consumables',
|
||||
icon: AppstoreOutlined,
|
||||
text: '耗材管理',
|
||||
path: '/consumables',
|
||||
color: '#fa8c16',
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
icon: SettingOutlined,
|
||||
text: '系统配置',
|
||||
path: '/settings',
|
||||
color: designTokens.colors.success.main,
|
||||
},
|
||||
];
|
||||
|
||||
const createNavButtonStyle = (color, isHovered) => ({
|
||||
height: 'auto',
|
||||
padding: 'clamp(16px, 4vw, 24px) clamp(12px, 3vw, 20px)',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
border: `2px solid ${isHovered ? color : '#f0f0f0'}`,
|
||||
background: '#fff',
|
||||
transition: `all ${designTokens.transitions.normal}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 'clamp(8px, 2vw, 12px)',
|
||||
cursor: 'pointer',
|
||||
boxShadow: isHovered ? designTokens.shadows.large : designTokens.shadows.small,
|
||||
transform: isHovered ? 'translateY(-4px)' : 'none',
|
||||
minWidth: 0,
|
||||
});
|
||||
|
||||
const createNavIconContainer = (color) => ({
|
||||
width: 'clamp(44px, 10vw, 60px)',
|
||||
height: 'clamp(44px, 10vw, 60px)',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`,
|
||||
fontSize: 'clamp(20px, 5vw, 28px)',
|
||||
transition: `all ${designTokens.transitions.normal}`,
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
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 NavigationGrid = ({ hoveredCard, onHover }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="nav-grid"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(100px, 1fr))',
|
||||
gap: 'clamp(8px, 2vw, 16px)',
|
||||
marginBottom: '24px',
|
||||
}}
|
||||
>
|
||||
{NAV_BUTTONS_DATA.map(({ key, icon: Icon, text, color }) => {
|
||||
const isHovered = hoveredCard === `nav-${key}`;
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="nav-button"
|
||||
style={{
|
||||
...createNavButtonStyle(color, isHovered),
|
||||
animationDelay: `${NAV_BUTTONS_DATA.findIndex((b) => b.key === key) * 0.1}s`,
|
||||
}}
|
||||
onMouseEnter={() => onHover(`nav-${key}`)}
|
||||
onMouseLeave={() => onHover(null)}
|
||||
onClick={() => navigate(`/${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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NavigationGrid);
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { designTokens } from '../../config/theme';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(PowerGauge);
|
||||
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import { Card, Typography } from 'antd';
|
||||
import { LineChartOutlined, SafetyOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||
import { designTokens } from '../../config/theme';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
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 QuickStats = ({ onlineRate, powerUsage }) => {
|
||||
const quickStats = [
|
||||
{
|
||||
icon: LineChartOutlined,
|
||||
label: '在线率',
|
||||
value: `${onlineRate}%`,
|
||||
color: designTokens.colors.success.main,
|
||||
},
|
||||
{
|
||||
icon: SafetyOutlined,
|
||||
label: '安全等级',
|
||||
value: 'A级',
|
||||
color: designTokens.colors.primary.main,
|
||||
},
|
||||
{
|
||||
icon: ThunderboltOutlined,
|
||||
label: '功率使用',
|
||||
value: `${powerUsage}W`,
|
||||
color: designTokens.colors.warning.main,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
|
||||
gap: '16px',
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(QuickStats);
|
||||
@@ -0,0 +1,176 @@
|
||||
import React from 'react';
|
||||
import { Card, Col, Tag, Spin } from 'antd';
|
||||
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
|
||||
import { designTokens } from '../../config/theme';
|
||||
import AnimatedCounter from './AnimatedCounter';
|
||||
|
||||
const createStatCardStyle = (color) => ({
|
||||
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 ${color}`,
|
||||
});
|
||||
|
||||
const createStatIconContainer = (color) => ({
|
||||
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,
|
||||
background: `linear-gradient(135deg, ${color}20 0%, ${color}10 100%)`,
|
||||
});
|
||||
|
||||
const StatCard = ({
|
||||
config,
|
||||
stats,
|
||||
loading,
|
||||
animatedKey,
|
||||
hoveredCard,
|
||||
onHover,
|
||||
}) => {
|
||||
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.xl }
|
||||
: {}),
|
||||
animationDelay: `${delay * 0.1}s`,
|
||||
};
|
||||
|
||||
return (
|
||||
<Col key={statKey} {...colProps}>
|
||||
<Card
|
||||
style={cardStyle}
|
||||
onMouseEnter={() => onHover(statKey)}
|
||||
onMouseLeave={() => onHover(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 ? (
|
||||
<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>{statKey === 'totalRacks' ? '正常运行中' : '全部在线'}</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(StatCard);
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { designTokens } from '../../config/theme';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(StatusLegend);
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import { designTokens } from '../../config/theme';
|
||||
|
||||
const systemInfoStyle = {
|
||||
background: 'linear-gradient(135deg, #f0f7ff 0%, #e6f7ff 100%)',
|
||||
borderRadius: designTokens.borderRadius.medium,
|
||||
padding: '20px',
|
||||
border: '1px solid #91d5ff',
|
||||
};
|
||||
|
||||
const SystemInfo = ({ onRefresh, isRefreshing }) => {
|
||||
return (
|
||||
<div style={{ animation: 'fadeInUp 0.6s ease-out 0.5s backwards' }}>
|
||||
<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={onRefresh}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SystemInfo);
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as AnimatedCounter } from './AnimatedCounter';
|
||||
export { default as CircularProgress } from './CircularProgress';
|
||||
export { default as PowerGauge } from './PowerGauge';
|
||||
export { default as DeviceTrendChart } from './DeviceTrendChart';
|
||||
export { default as StatusLegend } from './StatusLegend';
|
||||
export { default as StatCard } from './StatCard';
|
||||
export { default as NavigationGrid } from './NavigationGrid';
|
||||
export { default as QuickStats } from './QuickStats';
|
||||
export { default as SystemInfo } from './SystemInfo';
|
||||
Reference in New Issue
Block a user