2026-01-20 11:28:53 +08:00
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
2026-02-10 14:17:26 +08:00
|
|
|
|
import { useDebounce } from '../hooks/useDebounce';
|
2026-01-20 11:28:53 +08:00
|
|
|
|
import {
|
2026-02-10 11:04:01 +08:00
|
|
|
|
Table,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
message,
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
InputNumber,
|
|
|
|
|
|
Progress,
|
|
|
|
|
|
Drawer,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Empty,
|
|
|
|
|
|
Badge,
|
|
|
|
|
|
Checkbox,
|
|
|
|
|
|
Dropdown,
|
|
|
|
|
|
Statistic,
|
|
|
|
|
|
Upload,
|
|
|
|
|
|
Spin,
|
2026-02-10 15:39:29 +08:00
|
|
|
|
Pagination,
|
2026-01-20 11:28:53 +08:00
|
|
|
|
} from 'antd';
|
|
|
|
|
|
import {
|
2026-02-10 11:04:01 +08:00
|
|
|
|
PlusOutlined,
|
|
|
|
|
|
EditOutlined,
|
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
|
UploadOutlined,
|
|
|
|
|
|
DownloadOutlined,
|
|
|
|
|
|
SearchOutlined,
|
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
|
EyeOutlined,
|
|
|
|
|
|
DatabaseOutlined,
|
|
|
|
|
|
HomeOutlined,
|
|
|
|
|
|
ThunderboltOutlined,
|
|
|
|
|
|
ExpandOutlined,
|
|
|
|
|
|
CompressOutlined,
|
|
|
|
|
|
MoreOutlined,
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
WarningOutlined,
|
|
|
|
|
|
SyncOutlined,
|
|
|
|
|
|
LineChartOutlined,
|
|
|
|
|
|
FilterOutlined,
|
|
|
|
|
|
DeleteFilled,
|
2026-01-20 11:28:53 +08:00
|
|
|
|
} from '@ant-design/icons';
|
2025-12-18 10:45:13 +08:00
|
|
|
|
import axios from 'axios';
|
2026-03-10 15:49:02 +08:00
|
|
|
|
import { designTokens } from '../config/theme';
|
2026-03-12 12:40:12 +08:00
|
|
|
|
import CloseButton from '../components/CloseButton';
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
|
|
|
|
|
const { Option } = Select;
|
2026-01-20 11:28:53 +08:00
|
|
|
|
const { Title, Text } = Typography;
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
2026-01-20 11:28:53 +08:00
|
|
|
|
const containerStyle = {
|
|
|
|
|
|
minHeight: '100vh',
|
|
|
|
|
|
background: 'linear-gradient(180deg, #f5f7fa 0%, #e8ecf1 100%)',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
padding: '24px',
|
2026-01-20 11:28:53 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const headerStyle = {
|
|
|
|
|
|
marginBottom: '24px',
|
|
|
|
|
|
padding: '24px',
|
|
|
|
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
|
|
|
borderRadius: '20px',
|
|
|
|
|
|
color: '#fff',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
boxShadow: '0 8px 32px rgba(102, 126, 234, 0.3)',
|
2026-01-20 11:28:53 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-20 14:31:20 +08:00
|
|
|
|
const statCardStyle = {
|
2026-01-20 11:28:53 +08:00
|
|
|
|
background: 'rgba(255, 255, 255, 0.15)',
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
padding: '16px',
|
|
|
|
|
|
border: '1px solid rgba(255, 255, 255, 0.2)',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
backdropFilter: 'blur(10px)',
|
2026-01-20 14:31:20 +08:00
|
|
|
|
};
|
2026-01-20 11:28:53 +08:00
|
|
|
|
|
|
|
|
|
|
const cardStyle = {
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.large,
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
boxShadow: designTokens.shadows.medium,
|
|
|
|
|
|
background: '#fff',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
overflow: 'hidden',
|
2026-01-20 11:28:53 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const cardHeadStyle = {
|
|
|
|
|
|
borderBottom: '1px solid #f0f0f0',
|
|
|
|
|
|
padding: '16px 24px',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
background: 'linear-gradient(135deg, #f8f9ff 0%, #ffffff 100%)',
|
2026-01-20 11:28:53 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const primaryButtonStyle = {
|
|
|
|
|
|
height: '42px',
|
|
|
|
|
|
borderRadius: '10px',
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
boxShadow: '0 4px 16px rgba(102, 126, 234, 0.35)',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
fontWeight: '500',
|
2026-01-20 11:28:53 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const actionButtonStyle = {
|
|
|
|
|
|
height: '36px',
|
|
|
|
|
|
borderRadius: '8px',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
border: '1px solid #e8e8e8',
|
2026-01-20 11:28:53 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const searchInputStyle = {
|
|
|
|
|
|
borderRadius: '10px',
|
|
|
|
|
|
height: '42px',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
border: '1px solid #e8e8e8',
|
2026-01-20 11:28:53 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const statusConfig = {
|
|
|
|
|
|
active: { text: '在用', color: 'success', icon: <CheckCircleOutlined /> },
|
|
|
|
|
|
maintenance: { text: '维护中', color: 'warning', icon: <SyncOutlined spin /> },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
inactive: { text: '停用', color: 'default', icon: <WarningOutlined /> },
|
2026-01-20 11:28:53 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const PowerGauge = ({ current, max }) => {
|
|
|
|
|
|
const percentage = Math.min((current / max) * 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={{ width: '100%' }}>
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
|
|
|
|
|
|
<Text style={{ fontSize: '13px', color: designTokens.colors.text.secondary }}>
|
|
|
|
|
|
{current} / {max} W
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Text style={{ fontSize: '13px', fontWeight: '600', color: getColor() }}>
|
|
|
|
|
|
{percentage.toFixed(1)}%
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Progress
|
|
|
|
|
|
percent={percentage}
|
|
|
|
|
|
showInfo={false}
|
|
|
|
|
|
strokeColor={getColor()}
|
|
|
|
|
|
trailColor="#f0f0f0"
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const RackCard = ({ rack, onEdit, onDelete, onView, selected, onSelect }) => {
|
2026-03-24 14:54:39 +08:00
|
|
|
|
const usedU = rack.Devices?.reduce((sum, d) => sum + (d.height || 1), 0) || 0;
|
2026-01-20 11:28:53 +08:00
|
|
|
|
const powerUsage = (rack.currentPower / rack.maxPower) * 100;
|
|
|
|
|
|
const statusInfo = statusConfig[rack.status];
|
2026-03-24 14:54:39 +08:00
|
|
|
|
const availableU = rack.height - usedU;
|
2026-01-20 11:28:53 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card
|
|
|
|
|
|
hoverable
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
border: selected ? `2px solid ${designTokens.colors.primary.main}` : '1px solid #f0f0f0',
|
|
|
|
|
|
boxShadow: designTokens.shadows.small,
|
|
|
|
|
|
transition: `all ${designTokens.transitions.normal}`,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
cursor: 'pointer',
|
2026-01-20 11:28:53 +08:00
|
|
|
|
}}
|
|
|
|
|
|
onClick={() => onSelect(rack.rackId)}
|
|
|
|
|
|
onDoubleClick={() => onView(rack)}
|
2026-01-30 14:01:23 +08:00
|
|
|
|
styles={{ body: { padding: '20px' } }}
|
2026-01-20 11:28:53 +08:00
|
|
|
|
>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<DatabaseOutlined
|
|
|
|
|
|
style={{ fontSize: '20px', color: designTokens.colors.purple.main }}
|
|
|
|
|
|
/>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Text strong style={{ fontSize: '16px', color: designTokens.colors.text.primary }}>
|
|
|
|
|
|
{rack.name}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '13px' }}>
|
|
|
|
|
|
{rack.rackId}
|
|
|
|
|
|
</Text>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<Tag color={statusInfo.color} icon={statusInfo.icon} style={{ borderRadius: '20px' }}>
|
|
|
|
|
|
{statusInfo.text}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
|
|
|
|
|
|
<HomeOutlined style={{ color: designTokens.colors.text.tertiary }} />
|
|
|
|
|
|
<Text style={{ fontSize: '13px', color: designTokens.colors.text.secondary }}>
|
|
|
|
|
|
{rack.Room?.name || '未分配'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gridTemplateColumns: '1fr 1fr',
|
|
|
|
|
|
gap: '12px',
|
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<div style={{ padding: '12px', background: '#fafafa', borderRadius: '8px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '11px' }}>
|
|
|
|
|
|
高度/已用U位
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{ fontSize: '14px', fontWeight: '600', color: designTokens.colors.text.primary }}
|
|
|
|
|
|
>
|
2026-03-24 14:54:39 +08:00
|
|
|
|
{rack.height}U / {usedU}
|
2026-01-20 11:28:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ padding: '12px', background: '#fafafa', borderRadius: '8px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '11px' }}>
|
|
|
|
|
|
功率使用
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
color:
|
|
|
|
|
|
powerUsage >= 80
|
|
|
|
|
|
? designTokens.colors.error.main
|
|
|
|
|
|
: designTokens.colors.text.primary,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
{powerUsage.toFixed(1)}%
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ marginBottom: '16px' }}>
|
|
|
|
|
|
<PowerGauge current={rack.currentPower} max={rack.maxPower} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
|
|
|
|
最大功率: {rack.maxPower}W
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Tooltip title="查看详情">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={<EyeOutlined />}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onClick={e => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
onView(rack);
|
|
|
|
|
|
}}
|
2026-01-20 11:28:53 +08:00
|
|
|
|
style={{ color: designTokens.colors.text.secondary }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip title="编辑">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={<EditOutlined />}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onClick={e => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
onEdit(rack);
|
|
|
|
|
|
}}
|
2026-01-20 11:28:53 +08:00
|
|
|
|
style={{ color: designTokens.colors.primary.main }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip title="删除">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
|
danger
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onClick={e => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
onDelete(rack.rackId);
|
|
|
|
|
|
}}
|
2026-01-20 11:28:53 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
2025-12-26 11:03:47 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-18 10:45:13 +08:00
|
|
|
|
function RackManagement() {
|
|
|
|
|
|
const [racks, setRacks] = useState([]);
|
|
|
|
|
|
const [rooms, setRooms] = useState([]);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
const [modalVisible, setModalVisible] = useState(false);
|
2026-01-20 11:28:53 +08:00
|
|
|
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
|
|
|
|
|
const [importModalVisible, setImportModalVisible] = useState(false);
|
2025-12-18 10:45:13 +08:00
|
|
|
|
const [editingRack, setEditingRack] = useState(null);
|
2026-01-20 11:28:53 +08:00
|
|
|
|
const [viewingRack, setViewingRack] = useState(null);
|
|
|
|
|
|
const [selectedRackIds, setSelectedRackIds] = useState([]);
|
|
|
|
|
|
const [viewMode, setViewMode] = useState('table');
|
|
|
|
|
|
const [searchKeyword, setSearchKeyword] = useState('');
|
2026-02-10 14:17:26 +08:00
|
|
|
|
const debouncedSearchKeyword = useDebounce(searchKeyword, 300);
|
2026-01-20 11:28:53 +08:00
|
|
|
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
|
|
|
|
const [roomFilter, setRoomFilter] = useState('all');
|
|
|
|
|
|
const [pagination, setPagination] = useState({
|
|
|
|
|
|
current: 1,
|
|
|
|
|
|
pageSize: 10,
|
2025-12-22 08:59:44 +08:00
|
|
|
|
total: 0,
|
|
|
|
|
|
pageSizeOptions: ['10', '20', '30', '50', '100'],
|
|
|
|
|
|
showSizeChanger: true,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
showTotal: total => `共 ${total} 条记录`,
|
2025-12-22 08:59:44 +08:00
|
|
|
|
});
|
2026-01-20 11:28:53 +08:00
|
|
|
|
const [form] = Form.useForm();
|
2025-12-22 08:59:44 +08:00
|
|
|
|
const [importProgress, setImportProgress] = useState(0);
|
2025-12-29 12:08:05 +08:00
|
|
|
|
const [importPhase, setImportPhase] = useState('');
|
2025-12-22 08:59:44 +08:00
|
|
|
|
const [isImporting, setIsImporting] = useState(false);
|
|
|
|
|
|
const [importResult, setImportResult] = useState(null);
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const fetchRacks = useCallback(
|
|
|
|
|
|
async (page = 1, pageSize = 10) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
const response = await axios.get('/api/racks', {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
page,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
roomId: roomFilter,
|
|
|
|
|
|
status: statusFilter,
|
2026-02-10 14:17:26 +08:00
|
|
|
|
keyword: debouncedSearchKeyword || undefined,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
const { racks: data, total } = response.data;
|
|
|
|
|
|
setRacks(data);
|
|
|
|
|
|
setPagination(prev => ({ ...prev, current: page, pageSize, total }));
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('获取机柜列表失败');
|
|
|
|
|
|
console.error('获取机柜列表失败:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-10 14:17:26 +08:00
|
|
|
|
[roomFilter, statusFilter, debouncedSearchKeyword]
|
2026-02-10 11:04:01 +08:00
|
|
|
|
);
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
2025-12-26 11:03:47 +08:00
|
|
|
|
const fetchRooms = useCallback(async () => {
|
2025-12-18 10:45:13 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await axios.get('/api/rooms');
|
2026-03-23 09:16:18 +08:00
|
|
|
|
setRooms(response.data.rooms || []);
|
2025-12-18 10:45:13 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('获取机房列表失败');
|
|
|
|
|
|
console.error('获取机房列表失败:', error);
|
|
|
|
|
|
}
|
2025-12-26 11:03:47 +08:00
|
|
|
|
}, []);
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-12-22 08:59:44 +08:00
|
|
|
|
fetchRacks(pagination.current, pagination.pageSize);
|
2025-12-18 10:45:13 +08:00
|
|
|
|
fetchRooms();
|
2025-12-26 11:03:47 +08:00
|
|
|
|
}, [fetchRacks, fetchRooms]);
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
2026-02-09 16:55:07 +08:00
|
|
|
|
// 当筛选条件变化时,重置到第一页并重新加载
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setPagination(prev => ({ ...prev, current: 1 }));
|
|
|
|
|
|
fetchRacks(1, pagination.pageSize);
|
2026-02-10 14:17:26 +08:00
|
|
|
|
}, [roomFilter, statusFilter, debouncedSearchKeyword]);
|
2026-02-09 16:55:07 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleTableChange = useCallback(
|
|
|
|
|
|
pagination => {
|
|
|
|
|
|
fetchRacks(pagination.current, pagination.pageSize);
|
|
|
|
|
|
},
|
|
|
|
|
|
[fetchRacks]
|
|
|
|
|
|
);
|
2026-01-20 11:28:53 +08:00
|
|
|
|
|
|
|
|
|
|
const showModal = (rack = null) => {
|
2025-12-18 10:45:13 +08:00
|
|
|
|
setEditingRack(rack);
|
|
|
|
|
|
if (rack) {
|
|
|
|
|
|
form.setFieldsValue(rack);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
}
|
|
|
|
|
|
setModalVisible(true);
|
2026-01-20 11:28:53 +08:00
|
|
|
|
};
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
2026-01-20 11:28:53 +08:00
|
|
|
|
const handleCancel = () => {
|
2025-12-18 10:45:13 +08:00
|
|
|
|
setModalVisible(false);
|
|
|
|
|
|
setEditingRack(null);
|
2026-01-20 11:28:53 +08:00
|
|
|
|
};
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleSubmit = async values => {
|
2025-12-18 10:45:13 +08:00
|
|
|
|
try {
|
|
|
|
|
|
if (editingRack) {
|
|
|
|
|
|
await axios.put(`/api/racks/${editingRack.rackId}`, values);
|
|
|
|
|
|
message.success('机柜更新成功');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await axios.post('/api/racks', values);
|
|
|
|
|
|
message.success('机柜创建成功');
|
|
|
|
|
|
}
|
|
|
|
|
|
setModalVisible(false);
|
|
|
|
|
|
fetchRacks();
|
|
|
|
|
|
setEditingRack(null);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error(editingRack ? '机柜更新失败' : '机柜创建失败');
|
|
|
|
|
|
console.error(editingRack ? '机柜更新失败:' : '机柜创建失败:', error);
|
|
|
|
|
|
}
|
2026-01-20 11:28:53 +08:00
|
|
|
|
};
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleDelete = async rackId => {
|
2025-12-18 10:45:13 +08:00
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确认删除',
|
2026-01-20 11:28:53 +08:00
|
|
|
|
content: '确定要删除这个机柜吗?删除后无法恢复。',
|
2025-12-18 10:45:13 +08:00
|
|
|
|
okText: '删除',
|
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await axios.delete(`/api/racks/${rackId}`);
|
|
|
|
|
|
message.success('机柜删除成功');
|
|
|
|
|
|
fetchRacks();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('机柜删除失败');
|
|
|
|
|
|
console.error('机柜删除失败:', error);
|
|
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
},
|
2025-12-18 10:45:13 +08:00
|
|
|
|
});
|
2026-01-20 11:28:53 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleBatchDelete = async () => {
|
|
|
|
|
|
if (selectedRackIds.length === 0) {
|
|
|
|
|
|
message.warning('请先选择要删除的机柜');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '批量删除',
|
|
|
|
|
|
content: `确定要删除选中的 ${selectedRackIds.length} 个机柜吗?`,
|
|
|
|
|
|
okText: '删除',
|
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await Promise.all(selectedRackIds.map(id => axios.delete(`/api/racks/${id}`)));
|
|
|
|
|
|
message.success(`成功删除 ${selectedRackIds.length} 个机柜`);
|
|
|
|
|
|
setSelectedRackIds([]);
|
|
|
|
|
|
fetchRacks();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('批量删除失败');
|
|
|
|
|
|
console.error('批量删除失败:', error);
|
|
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
},
|
2026-01-20 11:28:53 +08:00
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleView = rack => {
|
2026-01-20 11:28:53 +08:00
|
|
|
|
setViewingRack(rack);
|
|
|
|
|
|
setDrawerVisible(true);
|
|
|
|
|
|
};
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
2025-12-26 11:03:47 +08:00
|
|
|
|
const handleDownloadTemplate = useCallback(() => {
|
2025-12-22 08:59:44 +08:00
|
|
|
|
window.open('/api/racks/import-template', '_blank');
|
|
|
|
|
|
message.success('模板下载成功');
|
2025-12-26 11:03:47 +08:00
|
|
|
|
}, []);
|
2025-12-18 19:11:56 +08:00
|
|
|
|
|
2026-02-06 16:23:56 +08:00
|
|
|
|
// 导出租机柜数据
|
|
|
|
|
|
const handleExport = useCallback(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
message.loading('正在导出租机柜数据...', 0);
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-02-06 16:23:56 +08:00
|
|
|
|
const response = await axios.get('/api/racks/export', {
|
2026-02-10 11:04:01 +08:00
|
|
|
|
responseType: 'blob',
|
2026-02-06 16:23:56 +08:00
|
|
|
|
});
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-02-06 16:23:56 +08:00
|
|
|
|
// 创建下载链接
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const blob = new Blob([response.data], {
|
|
|
|
|
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
2026-02-06 16:23:56 +08:00
|
|
|
|
});
|
|
|
|
|
|
const url = window.URL.createObjectURL(blob);
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
|
link.href = url;
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-02-06 16:23:56 +08:00
|
|
|
|
// 从响应头获取文件名,或使用默认文件名
|
|
|
|
|
|
const contentDisposition = response.headers['content-disposition'];
|
|
|
|
|
|
let fileName = '机柜导出.xlsx';
|
|
|
|
|
|
if (contentDisposition) {
|
|
|
|
|
|
const match = contentDisposition.match(/filename\*?=(?:UTF-8'')?([^;]+)/);
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
fileName = decodeURIComponent(match[1].replace(/['"]/g, ''));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-02-06 16:23:56 +08:00
|
|
|
|
link.setAttribute('download', fileName);
|
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
|
link.click();
|
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
|
window.URL.revokeObjectURL(url);
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-02-06 16:23:56 +08:00
|
|
|
|
message.destroy();
|
|
|
|
|
|
message.success('机柜导出成功');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.destroy();
|
|
|
|
|
|
message.error('机柜导出失败');
|
|
|
|
|
|
console.error('机柜导出失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleImport = useCallback(
|
|
|
|
|
|
async file => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setIsImporting(true);
|
|
|
|
|
|
setImportProgress(0);
|
|
|
|
|
|
setImportPhase('正在上传文件...');
|
|
|
|
|
|
setImportResult(null);
|
2026-01-20 11:28:53 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const formData = new FormData();
|
|
|
|
|
|
formData.append('file', file);
|
2026-01-20 11:28:53 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const response = await axios.post('/api/racks/import', formData, {
|
|
|
|
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
|
|
|
|
});
|
2026-01-20 11:28:53 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
setImportProgress(100);
|
|
|
|
|
|
setImportPhase('导入完成');
|
|
|
|
|
|
setImportResult(response.data);
|
|
|
|
|
|
setIsImporting(false);
|
2026-01-20 11:28:53 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
if (response.data.success) {
|
|
|
|
|
|
message.success('机柜导入成功');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.warning(response.data.message || '部分记录导入失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fetchRacks();
|
|
|
|
|
|
return false;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setIsImporting(false);
|
|
|
|
|
|
setImportProgress(0);
|
|
|
|
|
|
message.error('机柜导入失败');
|
|
|
|
|
|
console.error('机柜导入失败:', error);
|
|
|
|
|
|
return false;
|
2025-12-22 08:59:44 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
[fetchRacks]
|
|
|
|
|
|
);
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
2026-02-09 16:55:07 +08:00
|
|
|
|
// 后端已过滤,直接使用 racks 数据
|
|
|
|
|
|
const filteredRacks = racks;
|
2026-01-20 11:28:53 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const stats = useMemo(
|
|
|
|
|
|
() => ({
|
|
|
|
|
|
total: racks.length,
|
|
|
|
|
|
active: racks.filter(r => r.status === 'active').length,
|
|
|
|
|
|
maintenance: racks.filter(r => r.status === 'maintenance').length,
|
|
|
|
|
|
totalPower: racks.reduce((sum, r) => sum + (r.currentPower || 0), 0),
|
|
|
|
|
|
totalDevices: racks.reduce((sum, r) => sum + (r.Devices?.length || 0), 0),
|
|
|
|
|
|
}),
|
|
|
|
|
|
[racks]
|
|
|
|
|
|
);
|
2026-01-20 11:28:53 +08:00
|
|
|
|
|
|
|
|
|
|
const tableColumns = [
|
2025-12-18 10:45:13 +08:00
|
|
|
|
{
|
2026-01-20 11:28:53 +08:00
|
|
|
|
title: '机柜信息',
|
|
|
|
|
|
key: 'rackInfo',
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '44px',
|
|
|
|
|
|
height: '44px',
|
|
|
|
|
|
borderRadius: '10px',
|
|
|
|
|
|
background: 'linear-gradient(135deg, #722ed115 0%, #722ed108 100%)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<DatabaseOutlined
|
|
|
|
|
|
style={{ fontSize: '22px', color: designTokens.colors.purple.main }}
|
|
|
|
|
|
/>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ fontWeight: '600', color: designTokens.colors.text.primary }}>
|
|
|
|
|
|
{record.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ fontSize: '12px', color: designTokens.colors.text.tertiary }}>
|
|
|
|
|
|
{record.rackId}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
),
|
2025-12-18 10:45:13 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '所属机房',
|
|
|
|
|
|
dataIndex: ['Room', 'name'],
|
|
|
|
|
|
key: 'room',
|
2026-01-20 11:28:53 +08:00
|
|
|
|
render: (name, record) => (
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
|
|
|
|
<HomeOutlined style={{ color: designTokens.colors.text.tertiary }} />
|
|
|
|
|
|
<span>{name || '未分配'}</span>
|
|
|
|
|
|
</div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
),
|
2025-12-18 10:45:13 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-01-20 11:28:53 +08:00
|
|
|
|
title: '高度/已用U位',
|
|
|
|
|
|
key: 'heightUsage',
|
|
|
|
|
|
render: (_, record) => {
|
2026-03-24 14:54:39 +08:00
|
|
|
|
const used = record.Devices?.reduce((sum, d) => sum + (d.height || 1), 0) || 0;
|
2026-01-20 11:28:53 +08:00
|
|
|
|
return (
|
2026-03-24 14:54:39 +08:00
|
|
|
|
<Text style={{ fontSize: '13px' }}>
|
|
|
|
|
|
{record.height}U / 已用 {used} U位
|
|
|
|
|
|
</Text>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
);
|
|
|
|
|
|
},
|
2026-03-24 14:54:39 +08:00
|
|
|
|
sorter: (a, b) => (a.Devices?.reduce((sum, d) => sum + (d.height || 1), 0) || 0) - (b.Devices?.reduce((sum, d) => sum + (d.height || 1), 0) || 0),
|
2025-12-18 10:45:13 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-01-20 11:28:53 +08:00
|
|
|
|
title: '功率使用',
|
|
|
|
|
|
key: 'powerUsage',
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<div style={{ minWidth: '120px' }}>
|
|
|
|
|
|
<PowerGauge current={record.currentPower} max={record.maxPower} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
2026-02-10 11:04:01 +08:00
|
|
|
|
sorter: (a, b) => (a.currentPower || 0) - (b.currentPower || 0),
|
2025-12-18 10:45:13 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
dataIndex: 'status',
|
|
|
|
|
|
key: 'status',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
render: status => {
|
2026-01-20 11:28:53 +08:00
|
|
|
|
const config = statusConfig[status];
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Tag color={config.color} icon={config.icon} style={{ borderRadius: '20px' }}>
|
|
|
|
|
|
{config.text}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
filters: [
|
|
|
|
|
|
{ text: '在用', value: 'active' },
|
|
|
|
|
|
{ text: '维护中', value: 'maintenance' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
{ text: '停用', value: 'inactive' },
|
2026-01-20 11:28:53 +08:00
|
|
|
|
],
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onFilter: (value, record) => record.status === value,
|
2025-12-18 10:45:13 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
2026-01-20 11:28:53 +08:00
|
|
|
|
title: '设备数',
|
2025-12-18 10:45:13 +08:00
|
|
|
|
key: 'deviceCount',
|
2026-01-20 11:28:53 +08:00
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Badge count={record.Devices?.length || 0} style={{ backgroundColor: '#52c41a' }} />
|
|
|
|
|
|
),
|
2026-02-10 11:04:01 +08:00
|
|
|
|
sorter: (a, b) => (a.Devices?.length || 0) - (b.Devices?.length || 0),
|
2025-12-18 10:45:13 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '创建时间',
|
|
|
|
|
|
dataIndex: 'createdAt',
|
|
|
|
|
|
key: 'createdAt',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
render: date => (date ? new Date(date).toLocaleString() : '-'),
|
|
|
|
|
|
sorter: (a, b) => new Date(a.createdAt || 0) - new Date(b.createdAt || 0),
|
2025-12-18 10:45:13 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
2026-01-20 11:28:53 +08:00
|
|
|
|
fixed: 'right',
|
|
|
|
|
|
width: 160,
|
2025-12-18 10:45:13 +08:00
|
|
|
|
render: (_, record) => (
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Space>
|
|
|
|
|
|
<Tooltip title="查看">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={<EyeOutlined />}
|
|
|
|
|
|
onClick={() => handleView(record)}
|
|
|
|
|
|
style={{ color: designTokens.colors.text.secondary }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip title="编辑">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={<EditOutlined />}
|
|
|
|
|
|
onClick={() => showModal(record)}
|
|
|
|
|
|
style={{ color: designTokens.colors.primary.main }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip title="删除">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
|
danger
|
|
|
|
|
|
onClick={() => handleDelete(record.rackId)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
2025-12-18 10:45:13 +08:00
|
|
|
|
</Space>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
),
|
|
|
|
|
|
},
|
2026-01-20 11:28:53 +08:00
|
|
|
|
];
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
2026-01-20 11:28:53 +08:00
|
|
|
|
const rowSelection = {
|
|
|
|
|
|
selectedRowKeys: selectedRackIds,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onChange: selectedRowKeys => {
|
2026-01-20 11:28:53 +08:00
|
|
|
|
setSelectedRackIds(selectedRowKeys);
|
2026-02-10 11:04:01 +08:00
|
|
|
|
},
|
2025-12-29 11:15:55 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-18 10:45:13 +08:00
|
|
|
|
return (
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<div style={containerStyle}>
|
2026-03-06 14:39:52 +08:00
|
|
|
|
<style>{`
|
|
|
|
|
|
.ant-modal-close {
|
|
|
|
|
|
top: 16px !important;
|
|
|
|
|
|
right: 16px !important;
|
|
|
|
|
|
width: 32px !important;
|
|
|
|
|
|
height: 32px !important;
|
|
|
|
|
|
line-height: 32px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
.ant-modal-close-x {
|
|
|
|
|
|
width: 32px !important;
|
|
|
|
|
|
height: 32px !important;
|
|
|
|
|
|
line-height: 32px !important;
|
|
|
|
|
|
display: flex !important;
|
|
|
|
|
|
align-items: center !important;
|
|
|
|
|
|
justify-content: center !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
`}</style>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<div style={headerStyle}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
gap: '16px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<h1
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '24px',
|
|
|
|
|
|
fontWeight: '700',
|
|
|
|
|
|
margin: '0 0 4px 0',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '12px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<DatabaseOutlined />
|
|
|
|
|
|
机柜管理
|
|
|
|
|
|
</h1>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<p style={{ margin: 0, opacity: 0.9, fontSize: '14px' }}>管理和监控所有机柜设备</p>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '12px' }}>
|
2026-01-20 14:31:20 +08:00
|
|
|
|
<div style={statCardStyle}>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>总机柜</Text>
|
|
|
|
|
|
<div style={{ fontSize: '24px', fontWeight: '700' }}>{stats.total}</div>
|
|
|
|
|
|
</div>
|
2026-01-20 14:31:20 +08:00
|
|
|
|
<div style={statCardStyle}>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>在用机柜</Text>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div style={{ fontSize: '24px', fontWeight: '700', color: '#52c41a' }}>
|
|
|
|
|
|
{stats.active}
|
|
|
|
|
|
</div>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
</div>
|
2026-01-20 14:31:20 +08:00
|
|
|
|
<div style={statCardStyle}>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>设备总数</Text>
|
|
|
|
|
|
<div style={{ fontSize: '24px', fontWeight: '700' }}>{stats.totalDevices}</div>
|
|
|
|
|
|
</div>
|
2026-01-20 14:31:20 +08:00
|
|
|
|
<div style={statCardStyle}>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Text style={{ color: 'rgba(255,255,255,0.8)', fontSize: '12px' }}>总功率</Text>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div style={{ fontSize: '24px', fontWeight: '700' }}>
|
|
|
|
|
|
{(stats.totalPower / 1000).toFixed(1)}kW
|
|
|
|
|
|
</div>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-29 13:33:26 +08:00
|
|
|
|
<Card style={cardStyle} styles={{ header: cardHeadStyle, body: { padding: '20px 24px' } }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginBottom: '20px',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
gap: '16px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<div style={{ display: 'flex', gap: '12px', flex: 1, maxWidth: '800px' }}>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="搜索机柜名称、ID..."
|
|
|
|
|
|
prefix={<SearchOutlined style={{ color: '#bfbfbf' }} />}
|
|
|
|
|
|
value={searchKeyword}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onChange={e => setSearchKeyword(e.target.value)}
|
2026-01-20 11:28:53 +08:00
|
|
|
|
style={searchInputStyle}
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={statusFilter}
|
|
|
|
|
|
onChange={setStatusFilter}
|
|
|
|
|
|
style={{ width: '120px', height: '42px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Option value="all">全部状态</Option>
|
|
|
|
|
|
<Option value="active">在用</Option>
|
|
|
|
|
|
<Option value="maintenance">维护中</Option>
|
|
|
|
|
|
<Option value="inactive">停用</Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={roomFilter}
|
|
|
|
|
|
onChange={setRoomFilter}
|
|
|
|
|
|
style={{ width: '160px', height: '42px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Option value="all">全部机房</Option>
|
|
|
|
|
|
{rooms.map(room => (
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Option key={room.roomId} value={room.roomId}>
|
|
|
|
|
|
{room.name}
|
|
|
|
|
|
</Option>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '12px' }}>
|
|
|
|
|
|
{selectedRackIds.length > 0 && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
danger
|
|
|
|
|
|
icon={<DeleteFilled />}
|
|
|
|
|
|
onClick={handleBatchDelete}
|
|
|
|
|
|
style={{ borderRadius: '10px', height: '42px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
批量删除 ({selectedRackIds.length})
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
|
onClick={() => fetchRacks(pagination.current, pagination.pageSize)}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
style={actionButtonStyle}
|
|
|
|
|
|
>
|
|
|
|
|
|
刷新
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<UploadOutlined />}
|
|
|
|
|
|
onClick={() => setImportModalVisible(true)}
|
|
|
|
|
|
style={actionButtonStyle}
|
|
|
|
|
|
>
|
|
|
|
|
|
导入
|
|
|
|
|
|
</Button>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button icon={<DownloadOutlined />} onClick={handleExport} style={actionButtonStyle}>
|
2026-02-06 16:23:56 +08:00
|
|
|
|
导出
|
|
|
|
|
|
</Button>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
|
onClick={() => showModal()}
|
|
|
|
|
|
style={primaryButtonStyle}
|
|
|
|
|
|
>
|
|
|
|
|
|
添加机柜
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type={viewMode === 'table' ? 'primary' : 'text'}
|
|
|
|
|
|
icon={<ExpandOutlined />}
|
|
|
|
|
|
onClick={() => setViewMode('table')}
|
|
|
|
|
|
style={viewMode === 'table' ? primaryButtonStyle : actionButtonStyle}
|
|
|
|
|
|
>
|
|
|
|
|
|
表格视图
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type={viewMode === 'card' ? 'primary' : 'text'}
|
|
|
|
|
|
icon={<CompressOutlined />}
|
|
|
|
|
|
onClick={() => setViewMode('card')}
|
|
|
|
|
|
style={viewMode === 'card' ? primaryButtonStyle : actionButtonStyle}
|
|
|
|
|
|
>
|
|
|
|
|
|
卡片视图
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{viewMode === 'table' ? (
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={tableColumns}
|
|
|
|
|
|
dataSource={filteredRacks}
|
|
|
|
|
|
rowKey="rackId"
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
rowSelection={rowSelection}
|
|
|
|
|
|
pagination={pagination}
|
|
|
|
|
|
onChange={handleTableChange}
|
|
|
|
|
|
scroll={{ x: 1200 }}
|
|
|
|
|
|
rowClassName={() => 'table-row'}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Row gutter={[16, 16]}>
|
|
|
|
|
|
{filteredRacks.length > 0 ? (
|
|
|
|
|
|
filteredRacks.map(rack => (
|
|
|
|
|
|
<Col xs={24} sm={12} lg={8} xl={6} key={rack.rackId}>
|
|
|
|
|
|
<RackCard
|
|
|
|
|
|
rack={rack}
|
|
|
|
|
|
onEdit={showModal}
|
|
|
|
|
|
onDelete={handleDelete}
|
|
|
|
|
|
onView={handleView}
|
|
|
|
|
|
selected={selectedRackIds.includes(rack.rackId)}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
onSelect={id => {
|
2026-01-20 11:28:53 +08:00
|
|
|
|
if (selectedRackIds.includes(id)) {
|
|
|
|
|
|
setSelectedRackIds(selectedRackIds.filter(rid => rid !== id));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedRackIds([...selectedRackIds, id]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
))
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Col span={24}>
|
|
|
|
|
|
<Empty description="暂无机柜数据" style={{ padding: '60px 0' }} />
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
)}
|
2026-02-10 15:39:29 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 卡片视图分页 - 始终显示当总数据大于0时 */}
|
|
|
|
|
|
{viewMode === 'card' && (
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '24px' }}>
|
|
|
|
|
|
<Pagination
|
|
|
|
|
|
current={pagination.current}
|
|
|
|
|
|
pageSize={pagination.pageSize}
|
|
|
|
|
|
total={pagination.total}
|
|
|
|
|
|
pageSizeOptions={pagination.pageSizeOptions}
|
|
|
|
|
|
showSizeChanger={pagination.showSizeChanger}
|
|
|
|
|
|
showTotal={pagination.showTotal}
|
|
|
|
|
|
onChange={(page, pageSize) => {
|
|
|
|
|
|
handleTableChange({ current: page, pageSize });
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-12-18 10:45:13 +08:00
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Modal
|
2025-12-29 11:15:55 +08:00
|
|
|
|
title={
|
2026-03-06 14:39:52 +08:00
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', paddingRight: '32px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '4px',
|
|
|
|
|
|
height: '20px',
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
|
|
|
|
|
borderRadius: '2px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
{editingRack ? '编辑机柜' : '添加机柜'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2025-12-18 10:45:13 +08:00
|
|
|
|
open={modalVisible}
|
2026-03-12 12:40:12 +08:00
|
|
|
|
closeIcon={<CloseButton />}
|
2025-12-18 10:45:13 +08:00
|
|
|
|
onCancel={handleCancel}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={600}
|
2025-12-29 13:33:26 +08:00
|
|
|
|
destroyOnHidden
|
2025-12-29 11:15:55 +08:00
|
|
|
|
styles={{
|
|
|
|
|
|
body: { padding: '24px' },
|
2026-03-06 14:39:52 +08:00
|
|
|
|
header: {
|
|
|
|
|
|
borderBottom: '1px solid #f0f0f0',
|
|
|
|
|
|
padding: '16px 24px',
|
|
|
|
|
|
position: 'relative',
|
|
|
|
|
|
},
|
2025-12-29 11:15:55 +08:00
|
|
|
|
}}
|
2026-01-20 11:28:53 +08:00
|
|
|
|
style={{ borderRadius: '16px' }}
|
2026-03-06 14:39:52 +08:00
|
|
|
|
classNames={{
|
|
|
|
|
|
header: 'modal-header-fix',
|
|
|
|
|
|
}}
|
2025-12-18 10:45:13 +08:00
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={12}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Form.Item name="rackId" label="机柜ID(留空自动生成)">
|
2026-02-06 13:33:45 +08:00
|
|
|
|
<Input placeholder="如:RACK001,留空则自动生成" style={{ borderRadius: '8px' }} />
|
2026-01-20 11:28:53 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="name"
|
|
|
|
|
|
label="机柜名称"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入机柜名称' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input placeholder="请输入机柜名称" style={{ borderRadius: '8px' }} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="roomId"
|
|
|
|
|
|
label="所属机房"
|
|
|
|
|
|
rules={[{ required: true, message: '请选择机房' }]}
|
|
|
|
|
|
>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Select placeholder="请选择机房" style={{ borderRadius: '8px' }}>
|
2025-12-18 10:45:13 +08:00
|
|
|
|
{rooms.map(room => (
|
|
|
|
|
|
<Option key={room.roomId} value={room.roomId}>
|
|
|
|
|
|
{room.name} ({room.roomId})
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="height"
|
|
|
|
|
|
label="高度(U)"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入机柜高度' }]}
|
|
|
|
|
|
>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<InputNumber
|
|
|
|
|
|
placeholder="请输入机柜高度"
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
max={50}
|
|
|
|
|
|
style={{ width: '100%', borderRadius: '8px' }}
|
|
|
|
|
|
/>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="maxPower"
|
|
|
|
|
|
label="最大功率(W)"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入最大功率' }]}
|
|
|
|
|
|
>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<InputNumber
|
|
|
|
|
|
placeholder="请输入最大功率"
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
style={{ width: '100%', borderRadius: '8px' }}
|
|
|
|
|
|
/>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
2025-12-18 10:45:13 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Form.Item name="status" label="状态" rules={[{ required: true, message: '请选择状态' }]}>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<Select placeholder="请选择状态" style={{ borderRadius: '8px' }}>
|
2025-12-18 10:45:13 +08:00
|
|
|
|
<Option value="active">在用</Option>
|
|
|
|
|
|
<Option value="maintenance">维护中</Option>
|
|
|
|
|
|
<Option value="inactive">停用</Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{ display: 'flex', justifyContent: 'flex-end', gap: '12px', marginTop: '24px' }}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Button onClick={handleCancel} style={{ borderRadius: '8px', height: '42px' }}>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="primary" htmlType="submit" style={primaryButtonStyle}>
|
|
|
|
|
|
确定
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2025-12-18 10:45:13 +08:00
|
|
|
|
</Form>
|
|
|
|
|
|
</Modal>
|
2025-12-18 19:11:56 +08:00
|
|
|
|
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Drawer
|
|
|
|
|
|
title={
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<DatabaseOutlined
|
|
|
|
|
|
style={{ fontSize: '20px', color: designTokens.colors.purple.main }}
|
|
|
|
|
|
/>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
机柜详情 - {viewingRack?.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
open={drawerVisible}
|
|
|
|
|
|
onClose={() => setDrawerVisible(false)}
|
|
|
|
|
|
width={520}
|
|
|
|
|
|
styles={{
|
|
|
|
|
|
header: { borderBottom: '1px solid #f0f0f0' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
body: { padding: '24px' },
|
2026-01-20 11:28:53 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{viewingRack && (
|
|
|
|
|
|
<div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '20px',
|
|
|
|
|
|
background: 'linear-gradient(135deg, #722ed115 0%, #722ed108 100%)',
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.medium,
|
|
|
|
|
|
marginBottom: '20px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<DatabaseOutlined
|
|
|
|
|
|
style={{ fontSize: '32px', color: designTokens.colors.purple.main }}
|
|
|
|
|
|
/>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '18px',
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
color: designTokens.colors.text.primary,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
{viewingRack.name}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ fontSize: '13px', color: designTokens.colors.text.secondary }}>
|
|
|
|
|
|
{viewingRack.rackId}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Tag color={statusConfig[viewingRack.status].color} style={{ borderRadius: '20px' }}>
|
|
|
|
|
|
{statusConfig[viewingRack.status].text}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Row gutter={16} style={{ marginBottom: '20px' }}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<div style={{ padding: '16px', background: '#fafafa', borderRadius: '10px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
|
|
|
|
所属机房
|
|
|
|
|
|
</Text>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<div style={{ fontSize: '15px', fontWeight: '500', marginTop: '4px' }}>
|
|
|
|
|
|
{viewingRack.Room?.name || '未分配'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<div style={{ padding: '16px', background: '#fafafa', borderRadius: '10px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
|
|
|
|
设备数量
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '20px',
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
color: designTokens.colors.primary.main,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
{viewingRack.Devices?.length || 0}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
<Row gutter={16} style={{ marginBottom: '20px' }}>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<div style={{ padding: '16px', background: '#fafafa', borderRadius: '10px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
|
|
|
|
机柜高度
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '20px',
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
color: designTokens.colors.text.primary,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
{viewingRack.height}U
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
|
<div style={{ padding: '16px', background: '#fafafa', borderRadius: '10px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
|
|
|
|
可用U位
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '20px',
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
color: designTokens.colors.success.main,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
{viewingRack.height - (viewingRack.Devices?.length || 0)}U
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ marginBottom: '20px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '13px' }}>
|
|
|
|
|
|
功率使用情况
|
|
|
|
|
|
</Text>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<div style={{ marginTop: '12px' }}>
|
|
|
|
|
|
<PowerGauge current={viewingRack.currentPower} max={viewingRack.maxPower} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '12px', marginTop: '24px' }}>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<EditOutlined />}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setDrawerVisible(false);
|
|
|
|
|
|
showModal(viewingRack);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={primaryButtonStyle}
|
|
|
|
|
|
>
|
|
|
|
|
|
编辑机柜
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
danger
|
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setDrawerVisible(false);
|
|
|
|
|
|
handleDelete(viewingRack.rackId);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{ borderRadius: '8px', height: '42px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
删除机柜
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Drawer>
|
|
|
|
|
|
|
2025-12-22 08:59:44 +08:00
|
|
|
|
<Modal
|
2025-12-29 11:15:55 +08:00
|
|
|
|
title={
|
2026-03-06 14:39:52 +08:00
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', paddingRight: '32px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '4px',
|
|
|
|
|
|
height: '20px',
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
|
|
|
|
|
borderRadius: '2px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
导入机柜
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2025-12-22 08:59:44 +08:00
|
|
|
|
open={importModalVisible}
|
2026-03-12 12:40:12 +08:00
|
|
|
|
closeIcon={<CloseButton />}
|
2025-12-22 08:59:44 +08:00
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setImportModalVisible(false);
|
|
|
|
|
|
setImportProgress(0);
|
2025-12-29 12:08:05 +08:00
|
|
|
|
setImportPhase('');
|
2025-12-22 08:59:44 +08:00
|
|
|
|
setImportResult(null);
|
|
|
|
|
|
setIsImporting(false);
|
|
|
|
|
|
}}
|
|
|
|
|
|
footer={null}
|
2025-12-29 12:08:05 +08:00
|
|
|
|
width={650}
|
2025-12-29 13:33:26 +08:00
|
|
|
|
destroyOnHidden
|
2025-12-29 11:15:55 +08:00
|
|
|
|
styles={{
|
|
|
|
|
|
body: { padding: '24px' },
|
2026-03-06 14:39:52 +08:00
|
|
|
|
header: {
|
|
|
|
|
|
borderBottom: '1px solid #f0f0f0',
|
|
|
|
|
|
padding: '16px 24px',
|
|
|
|
|
|
position: 'relative',
|
|
|
|
|
|
},
|
2025-12-29 11:15:55 +08:00
|
|
|
|
}}
|
2026-01-20 11:28:53 +08:00
|
|
|
|
style={{ borderRadius: '16px' }}
|
2025-12-22 08:59:44 +08:00
|
|
|
|
>
|
|
|
|
|
|
{!isImporting && !importResult ? (
|
|
|
|
|
|
<div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '16px',
|
|
|
|
|
|
background: designTokens.colors.primary.bgGradient,
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
marginBottom: '20px',
|
|
|
|
|
|
border: '1px solid #e8e8e8',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<p style={{ margin: '0 0 12px 0', fontWeight: '600', color: '#262626' }}>
|
|
|
|
|
|
请上传XLSX格式的机柜数据文件
|
|
|
|
|
|
</p>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<p style={{ margin: 0, color: '#8c8c8c', fontSize: '12px' }}>支持的编码格式:UTF-8</p>
|
2025-12-29 11:15:55 +08:00
|
|
|
|
</div>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
|
2025-12-29 11:15:55 +08:00
|
|
|
|
<div style={{ marginBottom: '24px' }}>
|
|
|
|
|
|
<p style={{ fontWeight: '600', marginBottom: '12px', color: '#262626' }}>
|
|
|
|
|
|
Excel文件格式要求:
|
|
|
|
|
|
</p>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<ul
|
|
|
|
|
|
style={{
|
|
|
|
|
|
paddingLeft: '20px',
|
|
|
|
|
|
marginBottom: '10px',
|
|
|
|
|
|
color: '#595959',
|
|
|
|
|
|
lineHeight: '1.8',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-12-22 08:59:44 +08:00
|
|
|
|
<li>必填字段:机柜ID、机柜名称、所属机房名称、高度(U)、最大功率(W)、状态</li>
|
|
|
|
|
|
<li>状态值:active(在用)、maintenance(维护中)、inactive(停用)</li>
|
|
|
|
|
|
<li>高度和功率必须是数字格式</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '16px',
|
|
|
|
|
|
background: '#f6f6f6',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
marginBottom: '20px',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '12px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
icon={<DownloadOutlined />}
|
2025-12-29 11:15:55 +08:00
|
|
|
|
onClick={handleDownloadTemplate}
|
|
|
|
|
|
style={actionButtonStyle}
|
|
|
|
|
|
>
|
2025-12-22 08:59:44 +08:00
|
|
|
|
下载导入模板
|
|
|
|
|
|
</Button>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<span style={{ color: '#8c8c8c', fontSize: '12px' }}>包含示例数据的XLSX模板文件</span>
|
2025-12-22 08:59:44 +08:00
|
|
|
|
</div>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
|
2025-12-22 08:59:44 +08:00
|
|
|
|
<Upload
|
|
|
|
|
|
name="file"
|
|
|
|
|
|
accept=".xlsx, .xls"
|
|
|
|
|
|
showUploadList={false}
|
|
|
|
|
|
beforeUpload={handleImport}
|
|
|
|
|
|
maxCount={1}
|
|
|
|
|
|
>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<UploadOutlined />}
|
2025-12-29 11:15:55 +08:00
|
|
|
|
block
|
|
|
|
|
|
style={{
|
|
|
|
|
|
...primaryButtonStyle,
|
|
|
|
|
|
height: '48px',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
fontSize: '16px',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-12-22 08:59:44 +08:00
|
|
|
|
选择Excel文件
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Upload>
|
|
|
|
|
|
</div>
|
2025-12-29 12:08:05 +08:00
|
|
|
|
) : isImporting ? (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '16px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '48px',
|
|
|
|
|
|
height: '48px',
|
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
marginRight: '16px',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontSize: '20px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-12-29 12:08:05 +08:00
|
|
|
|
<UploadOutlined spin />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<p
|
|
|
|
|
|
style={{
|
|
|
|
|
|
margin: '0 0 4px 0',
|
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
|
color: '#333',
|
|
|
|
|
|
fontSize: '16px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
正在导入机柜数据
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p style={{ margin: 0, color: designTokens.colors.primary.main, fontSize: '14px' }}>
|
|
|
|
|
|
{importPhase}
|
|
|
|
|
|
</p>
|
2025-12-29 12:08:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-20 11:28:53 +08:00
|
|
|
|
<Progress
|
|
|
|
|
|
percent={importProgress}
|
2025-12-29 12:08:05 +08:00
|
|
|
|
status="active"
|
2025-12-29 11:15:55 +08:00
|
|
|
|
strokeColor={{
|
|
|
|
|
|
'0%': '#667eea',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
'100%': '#764ba2',
|
2025-12-29 11:15:55 +08:00
|
|
|
|
}}
|
2025-12-29 12:08:05 +08:00
|
|
|
|
format={() => `${importProgress}%`}
|
2025-12-29 11:15:55 +08:00
|
|
|
|
/>
|
2025-12-29 12:08:05 +08:00
|
|
|
|
</div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
importResult && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '20px',
|
|
|
|
|
|
background: '#f6f6f6',
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
marginBottom: '20px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<p style={{ marginBottom: '12px', fontWeight: '600' }}>导入结果:</p>
|
|
|
|
|
|
<p style={{ margin: '8px 0', color: '#52c41a' }}>
|
|
|
|
|
|
✓ 总记录数:{importResult.total || 0}
|
2025-12-29 11:15:55 +08:00
|
|
|
|
</p>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<p style={{ margin: '8px 0', color: '#52c41a' }}>
|
|
|
|
|
|
✓ 成功导入:{importResult.successCount || 0}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{importResult.failedCount > 0 && (
|
|
|
|
|
|
<p style={{ margin: '8px 0', color: '#ff4d4f' }}>
|
|
|
|
|
|
✗ 导入失败:{importResult.failedCount}
|
2026-01-20 11:28:53 +08:00
|
|
|
|
</p>
|
2025-12-22 08:59:44 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
{importResult.errors && importResult.errors.length > 0 && (
|
|
|
|
|
|
<div style={{ marginBottom: '20px' }}>
|
|
|
|
|
|
<p style={{ fontWeight: '600', marginBottom: '8px' }}>错误详情:</p>
|
|
|
|
|
|
{importResult.errors.slice(0, 5).map((err, idx) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={idx}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '8px 12px',
|
|
|
|
|
|
background: '#fff2f0',
|
|
|
|
|
|
borderRadius: '6px',
|
|
|
|
|
|
marginBottom: '8px',
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
color: '#cf1322',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
第{err.row || idx + 1}行:{err.error}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{importResult.errors.length > 5 && (
|
|
|
|
|
|
<p style={{ color: '#8c8c8c', fontSize: '12px' }}>
|
|
|
|
|
|
还有 {importResult.errors.length - 5} 处错误...
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setImportModalVisible(false);
|
|
|
|
|
|
setImportResult(null);
|
|
|
|
|
|
fetchRacks();
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={primaryButtonStyle}
|
|
|
|
|
|
>
|
|
|
|
|
|
完成
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
2026-01-20 11:28:53 +08:00
|
|
|
|
)}
|
2025-12-22 08:59:44 +08:00
|
|
|
|
</Modal>
|
2025-12-18 10:45:13 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-20 11:28:53 +08:00
|
|
|
|
export default React.memo(RackManagement);
|