2026-03-26 14:35:08 +08:00
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
2026-02-10 11:04:01 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Table,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
message,
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Popconfirm,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
InputNumber,
|
|
|
|
|
|
Collapse,
|
|
|
|
|
|
Empty,
|
|
|
|
|
|
Spin,
|
|
|
|
|
|
Upload,
|
|
|
|
|
|
Progress,
|
|
|
|
|
|
Checkbox,
|
|
|
|
|
|
Badge,
|
2026-02-12 16:29:19 +08:00
|
|
|
|
Row,
|
|
|
|
|
|
Col,
|
|
|
|
|
|
Skeleton,
|
|
|
|
|
|
Alert,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
Typography,
|
2026-03-16 17:07:20 +08:00
|
|
|
|
Divider,
|
2026-03-27 17:52:35 +08:00
|
|
|
|
Pagination,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
} from 'antd';
|
|
|
|
|
|
import {
|
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
|
EditOutlined,
|
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
|
SearchOutlined,
|
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
|
ExportOutlined,
|
|
|
|
|
|
ImportOutlined,
|
|
|
|
|
|
DownloadOutlined,
|
|
|
|
|
|
UploadOutlined as UploadIcon,
|
|
|
|
|
|
AppstoreOutlined,
|
|
|
|
|
|
FilterOutlined,
|
2026-02-12 16:29:19 +08:00
|
|
|
|
ClearOutlined,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
CloudServerOutlined,
|
2026-02-12 16:29:19 +08:00
|
|
|
|
ApiOutlined,
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
ExclamationCircleOutlined,
|
|
|
|
|
|
DisconnectOutlined,
|
2026-03-16 17:07:20 +08:00
|
|
|
|
UpOutlined,
|
|
|
|
|
|
DownOutlined,
|
2026-03-26 17:05:20 +08:00
|
|
|
|
TagOutlined,
|
|
|
|
|
|
ThunderboltOutlined,
|
|
|
|
|
|
FileTextOutlined,
|
|
|
|
|
|
InfoCircleOutlined,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
} from '@ant-design/icons';
|
2026-03-16 17:07:20 +08:00
|
|
|
|
import api from '../api';
|
2026-03-27 17:52:35 +08:00
|
|
|
|
import { roomAPI, rackAPI } from '../api/cache';
|
2026-01-23 08:55:16 +08:00
|
|
|
|
import * as XLSX from 'xlsx';
|
|
|
|
|
|
import Papa from 'papaparse';
|
2026-02-12 16:29:19 +08:00
|
|
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
2026-01-30 17:53:05 +08:00
|
|
|
|
import NetworkCardPanel from '../components/NetworkCardPanel';
|
|
|
|
|
|
import NetworkCardCreateModal from '../components/NetworkCardCreateModal';
|
2026-03-27 17:52:35 +08:00
|
|
|
|
import NetworkCardImportModal from '../components/NetworkCardImportModal';
|
|
|
|
|
|
import BatchImportModal from '../components/BatchImportModal';
|
|
|
|
|
|
import PortAddGuideModal from '../components/PortAddGuideModal';
|
|
|
|
|
|
import ServerNicCard from '../components/ServerNicCard';
|
|
|
|
|
|
import PortExportModal from '../components/PortExportModal';
|
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';
|
2026-03-10 15:49:02 +08:00
|
|
|
|
import { debounce } from '../utils/common';
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
|
|
|
|
|
const { Option } = Select;
|
|
|
|
|
|
const { Panel } = Collapse;
|
2026-02-12 16:29:19 +08:00
|
|
|
|
const { Text, Title } = Typography;
|
|
|
|
|
|
const { TextArea } = Input;
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
2026-02-12 16:29:19 +08:00
|
|
|
|
// 动画配置
|
|
|
|
|
|
const animations = {
|
|
|
|
|
|
container: {
|
|
|
|
|
|
hidden: { opacity: 0 },
|
|
|
|
|
|
visible: {
|
|
|
|
|
|
opacity: 1,
|
|
|
|
|
|
transition: {
|
|
|
|
|
|
staggerChildren: 0.05,
|
|
|
|
|
|
delayChildren: 0.1,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
item: {
|
|
|
|
|
|
hidden: { opacity: 0, y: 20 },
|
|
|
|
|
|
visible: {
|
|
|
|
|
|
opacity: 1,
|
|
|
|
|
|
y: 0,
|
|
|
|
|
|
transition: {
|
|
|
|
|
|
duration: 0.3,
|
|
|
|
|
|
ease: [0.25, 0.46, 0.45, 0.94],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-02-10 11:04:01 +08:00
|
|
|
|
},
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function PortManagement() {
|
|
|
|
|
|
const [ports, setPorts] = useState([]);
|
|
|
|
|
|
const [devices, setDevices] = useState([]);
|
2026-03-03 10:51:51 +08:00
|
|
|
|
const [deviceSearching, setDeviceSearching] = useState(false);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const [groupedPorts, setGroupedPorts] = useState({});
|
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
const [filters, setFilters] = useState({
|
|
|
|
|
|
deviceId: '',
|
|
|
|
|
|
});
|
|
|
|
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|
|
|
|
|
const [editingPort, setEditingPort] = useState(null);
|
|
|
|
|
|
const [form] = Form.useForm();
|
2026-01-30 17:53:05 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const [importModalVisible, setImportModalVisible] = useState(false);
|
|
|
|
|
|
const [importPreview, setImportPreview] = useState([]);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const [importErrors, setImportErrors] = useState([]);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const [importProgress, setImportProgress] = useState({ current: 0, total: 0 });
|
|
|
|
|
|
const [importing, setImporting] = useState(false);
|
|
|
|
|
|
const [skipExisting, setSkipExisting] = useState(false);
|
|
|
|
|
|
const [updateExisting, setUpdateExisting] = useState(false);
|
|
|
|
|
|
|
2026-01-30 17:53:05 +08:00
|
|
|
|
// 视图模式:list 或 panel
|
2026-03-26 17:05:20 +08:00
|
|
|
|
// const [viewMode, setViewMode] = useState('list');
|
2026-01-30 17:53:05 +08:00
|
|
|
|
|
|
|
|
|
|
// 网卡管理相关状态
|
|
|
|
|
|
const [networkCardModalVisible, setNetworkCardModalVisible] = useState(false);
|
2026-03-26 17:05:20 +08:00
|
|
|
|
const [serverNicListVisible, setServerNicListVisible] = useState(false);
|
|
|
|
|
|
const [serverNicList, setServerNicList] = useState([]);
|
|
|
|
|
|
const [serverNicLoading, setServerNicLoading] = useState(false);
|
2026-01-30 17:53:05 +08:00
|
|
|
|
const [portCreateModalVisible, setPortCreateModalVisible] = useState(false);
|
|
|
|
|
|
const [selectedDeviceForNic, setSelectedDeviceForNic] = useState(null);
|
|
|
|
|
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const [networkCardImportModalVisible, setNetworkCardImportModalVisible] = useState(false);
|
|
|
|
|
|
const [batchImportModalVisible, setBatchImportModalVisible] = useState(false);
|
|
|
|
|
|
const [portAddGuideModalVisible, setPortAddGuideModalVisible] = useState(false);
|
|
|
|
|
|
const [portExportModalVisible, setPortExportModalVisible] = useState(false);
|
2026-03-29 10:06:32 +08:00
|
|
|
|
const [importDeviceType, setImportDeviceType] = useState(null);
|
2026-01-30 17:53:05 +08:00
|
|
|
|
|
2026-02-12 16:29:19 +08:00
|
|
|
|
// 展开的设备
|
|
|
|
|
|
const [expandedKeys, setExpandedKeys] = useState([]);
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const [selectDeviceModalVisible, setSelectDeviceModalVisible] = useState(false);
|
|
|
|
|
|
const [selectedDeviceForPort, setSelectedDeviceForPort] = useState(null);
|
|
|
|
|
|
const [nicList, setNicList] = useState([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 批量选择相关状态
|
|
|
|
|
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 设备选择弹窗 Tab 状态
|
|
|
|
|
|
const [deviceFilterType, setDeviceFilterType] = useState('all');
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const [guidedDeviceType, setGuidedDeviceType] = useState(null);
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const [devicePage, setDevicePage] = useState(1);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 机房机柜筛选状态
|
|
|
|
|
|
const [roomList, setRoomList] = useState([]);
|
|
|
|
|
|
const [rackList, setRackList] = useState([]);
|
|
|
|
|
|
const [selectedRoomId, setSelectedRoomId] = useState(null);
|
|
|
|
|
|
const [selectedRackId, setSelectedRackId] = useState(null);
|
|
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const loadMoreRef = useRef(null);
|
|
|
|
|
|
const hasMoreRef = useRef(true);
|
|
|
|
|
|
const isLoadingRef = useRef(false);
|
2026-02-12 16:29:19 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
// 设备卡片分页状态
|
|
|
|
|
|
const [deviceCardPage, setDeviceCardPage] = useState(1);
|
|
|
|
|
|
const deviceCardPageSize = 10;
|
|
|
|
|
|
const [deviceCardTotal, setDeviceCardTotal] = useState(0);
|
|
|
|
|
|
|
|
|
|
|
|
// 分页和懒加载状态
|
|
|
|
|
|
const [portPage, setPortPage] = useState(1);
|
|
|
|
|
|
const [portPageSize, setPortPageSize] = useState(50);
|
|
|
|
|
|
const [portTotal, setPortTotal] = useState(0);
|
|
|
|
|
|
const [portLoading, setPortLoading] = useState(false);
|
|
|
|
|
|
const [portLoadingMore, setPortLoadingMore] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 服务器网卡卡片列表状态
|
|
|
|
|
|
const [serverNicSearchText, setServerNicSearchText] = useState('');
|
|
|
|
|
|
const [serverNicRoomFilter, setServerNicRoomFilter] = useState('all');
|
|
|
|
|
|
const [serverNicRackFilter, setServerNicRackFilter] = useState('all');
|
|
|
|
|
|
const [serverNicCardPage, setServerNicCardPage] = useState(1);
|
|
|
|
|
|
const serverNicCardPageSize = 12;
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const fetchPorts = useCallback(
|
|
|
|
|
|
async (page = 1, append = false) => {
|
|
|
|
|
|
const validPage = Number.isInteger(page) && page > 0 ? page : 1;
|
|
|
|
|
|
const validPageSize = Number.isInteger(portPageSize) && portPageSize > 0 ? portPageSize : 50;
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (append) {
|
|
|
|
|
|
setPortLoadingMore(true);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const params = {
|
|
|
|
|
|
page: validPage,
|
|
|
|
|
|
pageSize: validPageSize,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (filters.deviceId) params.deviceId = filters.deviceId;
|
|
|
|
|
|
|
|
|
|
|
|
const response = await api.get('/device-ports', { params });
|
|
|
|
|
|
const portsData = response.ports || [];
|
|
|
|
|
|
const total = response.total || 0;
|
|
|
|
|
|
const filteredPorts = portsData;
|
|
|
|
|
|
|
|
|
|
|
|
if (append) {
|
|
|
|
|
|
setPorts(prev => [...prev, ...filteredPorts]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setPorts(filteredPorts);
|
|
|
|
|
|
}
|
|
|
|
|
|
setPortTotal(total);
|
|
|
|
|
|
setPortPage(page);
|
|
|
|
|
|
hasMoreRef.current = page * portPageSize < total;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('获取端口列表失败');
|
|
|
|
|
|
console.error('获取端口列表失败:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
setPortLoadingMore(false);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
[filters, portPageSize]
|
|
|
|
|
|
);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
2026-03-03 10:51:51 +08:00
|
|
|
|
const fetchDevices = useCallback(async (keyword = '') => {
|
2026-01-23 08:55:16 +08:00
|
|
|
|
try {
|
2026-03-03 10:51:51 +08:00
|
|
|
|
setDeviceSearching(true);
|
|
|
|
|
|
if (keyword && keyword.trim()) {
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const params = { keyword: keyword.trim() };
|
|
|
|
|
|
const response = await api.get('/devices/all', { params });
|
|
|
|
|
|
setDevices(response.devices || response || []);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const response = await api.get('/devices/all');
|
|
|
|
|
|
setDevices(response.devices || response || []);
|
2026-03-03 10:51:51 +08:00
|
|
|
|
}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
} catch (error) {
|
2026-01-30 17:53:05 +08:00
|
|
|
|
message.error('获取设备列表失败');
|
2026-01-23 08:55:16 +08:00
|
|
|
|
console.error('获取设备列表失败:', error);
|
2026-03-03 10:51:51 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setDeviceSearching(false);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-03-03 10:51:51 +08:00
|
|
|
|
const handleDeviceSearch = useCallback(
|
|
|
|
|
|
debounce(value => {
|
|
|
|
|
|
fetchDevices(value);
|
|
|
|
|
|
}, 300),
|
|
|
|
|
|
[fetchDevices]
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
useEffect(() => {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
fetchPorts(1, false);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
fetchDevices();
|
2026-03-26 17:05:20 +08:00
|
|
|
|
}, [fetchPorts, fetchDevices]);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const handleLoadMore = useCallback(() => {
|
|
|
|
|
|
if (!hasMoreRef.current || isLoadingRef.current || portLoadingMore) return;
|
|
|
|
|
|
isLoadingRef.current = true;
|
|
|
|
|
|
const nextPage = portPage + 1;
|
|
|
|
|
|
fetchPorts(nextPage, true).then(() => {
|
|
|
|
|
|
isLoadingRef.current = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
}, [portPage, portLoadingMore, fetchPorts]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!loadMoreRef.current) return;
|
|
|
|
|
|
|
|
|
|
|
|
const observer = new IntersectionObserver(
|
|
|
|
|
|
entries => {
|
|
|
|
|
|
if (entries[0].isIntersecting && hasMoreRef.current && !portLoadingMore) {
|
|
|
|
|
|
handleLoadMore();
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ threshold: 0.1, rootMargin: '100px' }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
observer.observe(loadMoreRef.current);
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
observer.disconnect();
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [handleLoadMore, portLoadingMore]);
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const handlePageSizeChange = useCallback(
|
|
|
|
|
|
(newPage, newPageSize) => {
|
|
|
|
|
|
setPortPageSize(newPageSize);
|
|
|
|
|
|
setPortPage(1);
|
|
|
|
|
|
fetchPorts(1, false);
|
|
|
|
|
|
},
|
|
|
|
|
|
[fetchPorts]
|
|
|
|
|
|
);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const grouped = {};
|
|
|
|
|
|
ports.forEach(port => {
|
|
|
|
|
|
const deviceId = port.deviceId;
|
|
|
|
|
|
if (!grouped[deviceId]) {
|
|
|
|
|
|
grouped[deviceId] = {
|
2026-03-15 12:19:13 +08:00
|
|
|
|
device: port.device || devices.find(d => d.deviceId === deviceId),
|
2026-02-10 11:04:01 +08:00
|
|
|
|
ports: [],
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
grouped[deviceId].ports.push(port);
|
|
|
|
|
|
});
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-30 17:53:05 +08:00
|
|
|
|
// 对每个设备的端口按名称升序排序
|
|
|
|
|
|
Object.keys(grouped).forEach(deviceId => {
|
|
|
|
|
|
grouped[deviceId].ports.sort((a, b) => {
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const extractNumbers = str => {
|
2026-01-30 17:53:05 +08:00
|
|
|
|
const matches = str.match(/\d+/g);
|
|
|
|
|
|
return matches ? matches.map(Number) : [];
|
|
|
|
|
|
};
|
|
|
|
|
|
const numsA = extractNumbers(a.portName);
|
|
|
|
|
|
const numsB = extractNumbers(b.portName);
|
|
|
|
|
|
for (let i = 0; i < Math.min(numsA.length, numsB.length); i++) {
|
|
|
|
|
|
if (numsA[i] !== numsB[i]) {
|
|
|
|
|
|
return numsA[i] - numsB[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return a.portName.localeCompare(b.portName);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
setGroupedPorts(grouped);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
|
2026-02-12 16:29:19 +08:00
|
|
|
|
const deviceIds = Object.keys(grouped);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const newTotal = deviceIds.length;
|
|
|
|
|
|
setDeviceCardTotal(newTotal);
|
|
|
|
|
|
if (deviceCardPage > Math.ceil(newTotal / deviceCardPageSize)) {
|
|
|
|
|
|
setDeviceCardPage(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
setExpandedKeys([]);
|
|
|
|
|
|
}, [ports, devices, deviceCardPage, deviceCardPageSize]);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const handleSearch = useCallback(() => {
|
|
|
|
|
|
setPortPage(1);
|
|
|
|
|
|
setDeviceCardPage(1);
|
|
|
|
|
|
hasMoreRef.current = true;
|
|
|
|
|
|
fetchPorts(1, false);
|
|
|
|
|
|
}, [fetchPorts]);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const handleReset = useCallback(() => {
|
2026-01-23 08:55:16 +08:00
|
|
|
|
setFilters({
|
|
|
|
|
|
deviceId: '',
|
|
|
|
|
|
});
|
2026-03-27 17:52:35 +08:00
|
|
|
|
setPortPage(1);
|
|
|
|
|
|
setDeviceCardPage(1);
|
|
|
|
|
|
hasMoreRef.current = true;
|
|
|
|
|
|
fetchPorts(1, false);
|
|
|
|
|
|
}, [fetchPorts]);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
|
|
|
|
|
const handleAdd = () => {
|
|
|
|
|
|
setEditingPort(null);
|
|
|
|
|
|
form.resetFields();
|
2026-03-27 17:52:35 +08:00
|
|
|
|
setPortAddGuideModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const handleGuideSelectType = type => {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
setPortAddGuideModalVisible(false);
|
|
|
|
|
|
setGuidedDeviceType(type);
|
2026-03-26 14:35:08 +08:00
|
|
|
|
fetchDevices();
|
2026-03-27 17:52:35 +08:00
|
|
|
|
fetchRoomsAndRacks();
|
|
|
|
|
|
if (type === 'switch') {
|
|
|
|
|
|
setDeviceFilterType('switch');
|
|
|
|
|
|
} else if (type === 'server') {
|
|
|
|
|
|
setDeviceFilterType('server');
|
|
|
|
|
|
}
|
2026-03-26 14:35:08 +08:00
|
|
|
|
setSelectDeviceModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const fetchRoomsAndRacks = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [roomsData, racksData] = await Promise.all([
|
|
|
|
|
|
roomAPI.list(),
|
|
|
|
|
|
api.get('/racks', { params: { pageSize: 1000 } }),
|
|
|
|
|
|
]);
|
|
|
|
|
|
setRoomList(roomsData.rooms || roomsData || []);
|
|
|
|
|
|
setRackList(racksData.racks || racksData || []);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取机房机柜数据失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 17:05:20 +08:00
|
|
|
|
const handleManageServerNics = async () => {
|
|
|
|
|
|
setServerNicListVisible(true);
|
|
|
|
|
|
setServerNicLoading(true);
|
|
|
|
|
|
try {
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const [devicesResponse, portsResponse, nicsResponse, roomsData, racksData] =
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
api.get('/devices/all', { params: { type: 'server' } }),
|
|
|
|
|
|
api.get('/device-ports', { params: { pageSize: 10000 } }),
|
|
|
|
|
|
api.get('/network-cards'),
|
|
|
|
|
|
roomAPI.list(),
|
|
|
|
|
|
api.get('/racks', { params: { pageSize: 1000 } }),
|
|
|
|
|
|
]);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
setRoomList(roomsData.rooms || roomsData || []);
|
|
|
|
|
|
setRackList(racksData.racks || racksData || []);
|
2026-03-26 17:05:20 +08:00
|
|
|
|
|
|
|
|
|
|
const servers = devicesResponse.devices || devicesResponse || [];
|
|
|
|
|
|
const ports = portsResponse.ports || portsResponse || [];
|
2026-03-27 17:52:35 +08:00
|
|
|
|
let allNics = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(nicsResponse)) {
|
|
|
|
|
|
allNics = nicsResponse;
|
|
|
|
|
|
} else if (nicsResponse && Array.isArray(nicsResponse.data)) {
|
|
|
|
|
|
allNics = nicsResponse.data;
|
|
|
|
|
|
} else if (nicsResponse && Array.isArray(nicsResponse.networkCards)) {
|
|
|
|
|
|
allNics = nicsResponse.networkCards;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[DEBUG] handleManageServerNics:', {
|
|
|
|
|
|
serversCount: servers.length,
|
|
|
|
|
|
portsCount: ports.length,
|
|
|
|
|
|
nicsCount: allNics.length,
|
|
|
|
|
|
sampleServer: servers[0],
|
2026-03-27 19:12:16 +08:00
|
|
|
|
sampleNic: allNics[0],
|
2026-03-27 17:52:35 +08:00
|
|
|
|
});
|
2026-03-26 17:05:20 +08:00
|
|
|
|
|
|
|
|
|
|
const serversWithPorts = new Set(ports.map(p => p.deviceId));
|
|
|
|
|
|
const serversWithNics = servers.filter(server => {
|
|
|
|
|
|
const serverNics = allNics.filter(nic => nic.deviceId === server.deviceId);
|
|
|
|
|
|
return serverNics.length > 0;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const serversNeedingAttention = serversWithNics
|
|
|
|
|
|
.filter(server => !serversWithPorts.has(server.deviceId))
|
|
|
|
|
|
.map(server => {
|
|
|
|
|
|
const serverNics = allNics.filter(nic => nic.deviceId === server.deviceId);
|
|
|
|
|
|
return { ...server, nics: serverNics, nicCount: serverNics.length };
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
console.log(
|
|
|
|
|
|
'[DEBUG] serversWithNics:',
|
|
|
|
|
|
serversWithNics.length,
|
|
|
|
|
|
'serversNeedingAttention:',
|
|
|
|
|
|
serversNeedingAttention.length
|
|
|
|
|
|
);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
|
2026-03-26 17:05:20 +08:00
|
|
|
|
setServerNicList(serversNeedingAttention);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取服务器列表失败:', error);
|
|
|
|
|
|
message.error('获取服务器列表失败: ' + (error.message || error));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setServerNicLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const handleSelectDeviceForPort = device => {
|
|
|
|
|
|
setSelectDeviceModalVisible(false);
|
|
|
|
|
|
if (!device) return;
|
|
|
|
|
|
|
|
|
|
|
|
const deviceType = getDeviceType(device);
|
|
|
|
|
|
|
|
|
|
|
|
if (deviceType === 'server') {
|
2026-03-27 19:12:16 +08:00
|
|
|
|
api
|
|
|
|
|
|
.get(`/network-cards/device/${device.deviceId}`)
|
2026-03-26 14:35:08 +08:00
|
|
|
|
.then(nicList => {
|
2026-03-26 17:05:20 +08:00
|
|
|
|
const validNicList = Array.isArray(nicList) ? nicList : [];
|
2026-03-26 14:35:08 +08:00
|
|
|
|
if (validNicList.length === 0) {
|
|
|
|
|
|
message.warning({
|
|
|
|
|
|
content: '该服务器尚未添加网卡,请先在网卡管理中添加网卡',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
icon: (
|
|
|
|
|
|
<ExclamationCircleOutlined style={{ color: designTokens.colors.warning.main }} />
|
|
|
|
|
|
),
|
2026-03-26 14:35:08 +08:00
|
|
|
|
duration: 3,
|
|
|
|
|
|
});
|
|
|
|
|
|
handleManageNetworkCards(device);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setNicList(validNicList);
|
|
|
|
|
|
setSelectedDeviceForPort(device);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
form.setFieldsValue({ deviceId: device.deviceId });
|
|
|
|
|
|
setModalVisible(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
message.warning({
|
|
|
|
|
|
content: '该服务器尚未添加网卡,请先在网卡管理中添加网卡',
|
|
|
|
|
|
icon: <ExclamationCircleOutlined style={{ color: designTokens.colors.warning.main }} />,
|
|
|
|
|
|
duration: 3,
|
|
|
|
|
|
});
|
|
|
|
|
|
handleManageNetworkCards(device);
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedDeviceForPort(device);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
form.setFieldsValue({ deviceId: device.deviceId });
|
|
|
|
|
|
setModalVisible(true);
|
|
|
|
|
|
}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleAddPortForDevice = device => {
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const deviceType = getDeviceType(device);
|
|
|
|
|
|
|
|
|
|
|
|
if (deviceType === 'server') {
|
2026-03-27 19:12:16 +08:00
|
|
|
|
api
|
|
|
|
|
|
.get(`/network-cards/device/${device.deviceId}`)
|
2026-03-26 17:05:20 +08:00
|
|
|
|
.then(nicList => {
|
|
|
|
|
|
const validNicList = Array.isArray(nicList) ? nicList : [];
|
|
|
|
|
|
if (validNicList.length === 0) {
|
2026-03-26 14:35:08 +08:00
|
|
|
|
message.warning({
|
|
|
|
|
|
content: '该服务器尚未添加网卡,请先在网卡管理中添加网卡',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
icon: (
|
|
|
|
|
|
<ExclamationCircleOutlined style={{ color: designTokens.colors.warning.main }} />
|
|
|
|
|
|
),
|
2026-03-26 14:35:08 +08:00
|
|
|
|
duration: 3,
|
|
|
|
|
|
});
|
|
|
|
|
|
handleManageNetworkCards(device);
|
|
|
|
|
|
} else {
|
2026-03-26 17:05:20 +08:00
|
|
|
|
setNicList(validNicList);
|
2026-03-26 14:35:08 +08:00
|
|
|
|
setSelectedDeviceForPort(device);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
form.setFieldsValue({ deviceId: device.deviceId });
|
|
|
|
|
|
setModalVisible(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
message.warning({
|
|
|
|
|
|
content: '该服务器尚未添加网卡,请先在网卡管理中添加网卡',
|
|
|
|
|
|
icon: <ExclamationCircleOutlined style={{ color: designTokens.colors.warning.main }} />,
|
|
|
|
|
|
duration: 3,
|
|
|
|
|
|
});
|
|
|
|
|
|
handleManageNetworkCards(device);
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedDeviceForPort(device);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
form.setFieldsValue({ deviceId: device.deviceId });
|
|
|
|
|
|
setModalVisible(true);
|
|
|
|
|
|
}
|
2026-01-30 17:53:05 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleManageNetworkCards = device => {
|
2026-01-30 17:53:05 +08:00
|
|
|
|
setSelectedDeviceForNic(device);
|
|
|
|
|
|
setNetworkCardModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleNicSuccess = () => {
|
2026-02-12 16:29:19 +08:00
|
|
|
|
message.success({
|
|
|
|
|
|
content: '操作成功',
|
|
|
|
|
|
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
|
|
|
|
|
});
|
2026-01-30 17:53:05 +08:00
|
|
|
|
setRefreshTrigger(prev => prev + 1);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
fetchPorts(1, false);
|
2026-01-30 17:53:05 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleEdit = port => {
|
2026-01-23 08:55:16 +08:00
|
|
|
|
setEditingPort(port);
|
|
|
|
|
|
form.setFieldsValue({
|
|
|
|
|
|
portId: port.portId,
|
|
|
|
|
|
deviceId: port.deviceId,
|
|
|
|
|
|
portName: port.portName,
|
|
|
|
|
|
portType: port.portType,
|
|
|
|
|
|
portSpeed: port.portSpeed,
|
|
|
|
|
|
status: port.status,
|
|
|
|
|
|
vlanId: port.vlanId,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
description: port.description,
|
2026-01-23 08:55:16 +08:00
|
|
|
|
});
|
|
|
|
|
|
setModalVisible(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const handleDelete = async portId => {
|
2026-03-20 17:15:14 +08:00
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确认删除',
|
|
|
|
|
|
content: '确定要删除这个端口吗?此操作不可恢复!',
|
|
|
|
|
|
okText: '删除',
|
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await api.delete(`/device-ports/${portId}`);
|
|
|
|
|
|
message.success({
|
|
|
|
|
|
content: '删除成功',
|
|
|
|
|
|
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
|
|
|
|
|
});
|
2026-03-27 17:52:35 +08:00
|
|
|
|
fetchPorts(1, false);
|
2026-03-20 17:15:14 +08:00
|
|
|
|
} catch (error) {
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const errorMsg = error.response?.data?.error || '';
|
|
|
|
|
|
if (errorMsg.includes('关联的接线记录')) {
|
|
|
|
|
|
message.error('该端口存在关联的接线记录,请先删除关联的接线');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error('删除失败');
|
|
|
|
|
|
}
|
2026-03-20 17:15:14 +08:00
|
|
|
|
console.error('删除失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const handleBatchDelete = () => {
|
|
|
|
|
|
if (selectedRowKeys.length === 0) {
|
|
|
|
|
|
message.warning('请先选择要删除的端口');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
|
title: '确认批量删除',
|
|
|
|
|
|
content: `确定要删除选中的 ${selectedRowKeys.length} 个端口吗?此操作不可恢复!`,
|
|
|
|
|
|
okText: '删除',
|
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
|
onOk: async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await api.post('/device-ports/batch-delete', {
|
|
|
|
|
|
portIds: selectedRowKeys,
|
|
|
|
|
|
});
|
|
|
|
|
|
message.success({
|
|
|
|
|
|
content: `成功删除 ${selectedRowKeys.length} 个端口`,
|
|
|
|
|
|
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
|
|
|
|
|
});
|
|
|
|
|
|
setSelectedRowKeys([]);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
fetchPorts(1, false);
|
2026-03-26 14:35:08 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
const errorMsg = error.response?.data?.error || '';
|
|
|
|
|
|
if (errorMsg.includes('关联的接线记录')) {
|
|
|
|
|
|
message.error('部分端口存在关联的接线记录,请先删除关联的接线');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error('批量删除失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
console.error('批量删除失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const parsePortRange = portName => {
|
2026-01-30 17:53:05 +08:00
|
|
|
|
const rangeMatch = portName.match(/^(.*?)\/(\d+)-\1\/(\d+)$/);
|
|
|
|
|
|
if (rangeMatch) {
|
|
|
|
|
|
const prefix = rangeMatch[1];
|
|
|
|
|
|
const start = parseInt(rangeMatch[2]);
|
|
|
|
|
|
const end = parseInt(rangeMatch[3]);
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
|
|
|
|
|
if (start <= end && end - start < 100) {
|
2026-01-30 17:53:05 +08:00
|
|
|
|
return Array.from({ length: end - start + 1 }, (_, i) => `${prefix}/${start + i}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-12 16:29:19 +08:00
|
|
|
|
return [portName];
|
2026-01-30 17:53:05 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const generateUniquePortId = () => {
|
|
|
|
|
|
return `PORT-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const values = await form.validateFields();
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
if (editingPort) {
|
2026-03-16 17:07:20 +08:00
|
|
|
|
await api.put(`/device-ports/${editingPort.portId}`, values);
|
2026-02-12 16:29:19 +08:00
|
|
|
|
message.success({
|
|
|
|
|
|
content: '更新成功',
|
|
|
|
|
|
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
|
|
|
|
|
});
|
2026-01-23 08:55:16 +08:00
|
|
|
|
} else {
|
2026-01-30 17:53:05 +08:00
|
|
|
|
const portNames = parsePortRange(values.portName);
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const targetDeviceId = selectedDeviceForPort
|
|
|
|
|
|
? selectedDeviceForPort.deviceId
|
|
|
|
|
|
: values.deviceId;
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-30 17:53:05 +08:00
|
|
|
|
if (portNames.length > 1) {
|
|
|
|
|
|
const portsData = portNames.map((name, index) => ({
|
2026-03-26 14:35:08 +08:00
|
|
|
|
portId: generateUniquePortId(),
|
|
|
|
|
|
deviceId: targetDeviceId,
|
2026-01-30 17:53:05 +08:00
|
|
|
|
portName: name,
|
|
|
|
|
|
portType: values.portType,
|
2026-03-26 17:05:20 +08:00
|
|
|
|
status: 'free',
|
2026-01-30 17:53:05 +08:00
|
|
|
|
vlanId: values.vlanId,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
description: values.description,
|
2026-03-26 14:35:08 +08:00
|
|
|
|
nicId: values.nicId || null,
|
2026-01-30 17:53:05 +08:00
|
|
|
|
}));
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const result = await api.post('/device-ports/batch', { ports: portsData });
|
|
|
|
|
|
const success = result.success ?? 0;
|
|
|
|
|
|
const failed = result.failed ?? 0;
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-30 17:53:05 +08:00
|
|
|
|
if (failed > 0) {
|
|
|
|
|
|
message.warning(`批量创建完成!成功 ${success} 个,失败 ${failed} 个`);
|
|
|
|
|
|
} else {
|
2026-02-12 16:29:19 +08:00
|
|
|
|
message.success({
|
|
|
|
|
|
content: `成功创建 ${success} 个端口`,
|
|
|
|
|
|
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
|
|
|
|
|
});
|
2026-01-30 17:53:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const result = await api.post('/device-ports', {
|
|
|
|
|
|
deviceId: targetDeviceId,
|
|
|
|
|
|
portName: portNames[0],
|
|
|
|
|
|
portType: values.portType,
|
2026-03-26 17:05:20 +08:00
|
|
|
|
status: 'free',
|
2026-03-26 14:35:08 +08:00
|
|
|
|
vlanId: values.vlanId,
|
|
|
|
|
|
description: values.description,
|
|
|
|
|
|
nicId: values.nicId || null,
|
|
|
|
|
|
});
|
2026-02-12 16:29:19 +08:00
|
|
|
|
message.success({
|
|
|
|
|
|
content: '创建成功',
|
|
|
|
|
|
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
|
|
|
|
|
});
|
2026-01-30 17:53:05 +08:00
|
|
|
|
}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
setModalVisible(false);
|
|
|
|
|
|
form.resetFields();
|
2026-03-26 14:35:08 +08:00
|
|
|
|
setSelectedDeviceForPort(null);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
fetchPorts(1, false);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
} catch (error) {
|
2026-03-26 14:35:08 +08:00
|
|
|
|
message.error(error.response?.data?.error || editingPort ? '更新失败' : '创建失败');
|
2026-01-23 08:55:16 +08:00
|
|
|
|
console.error('提交失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-29 10:06:32 +08:00
|
|
|
|
const handleImport = (deviceType = 'all') => {
|
2026-01-23 08:55:16 +08:00
|
|
|
|
setImportModalVisible(true);
|
|
|
|
|
|
setImportPreview([]);
|
|
|
|
|
|
setImportProgress({ current: 0, total: 0 });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const handleFileUpload = (file, onSuccess) => {
|
|
|
|
|
|
console.log('handleFileUpload 被调用', file.name, file.size);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const reader = new FileReader();
|
2026-02-10 11:04:01 +08:00
|
|
|
|
reader.onload = async e => {
|
2026-01-23 08:55:16 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const data = e.target.result;
|
|
|
|
|
|
let parsedData = [];
|
2026-03-29 10:06:32 +08:00
|
|
|
|
let networkCardData = [];
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) {
|
|
|
|
|
|
const workbook = XLSX.read(data, { type: 'binary' });
|
2026-03-29 10:06:32 +08:00
|
|
|
|
|
|
|
|
|
|
if (importDeviceType === 'server' && workbook.SheetNames.length >= 2) {
|
|
|
|
|
|
// 服务器设备:解析两张表格
|
|
|
|
|
|
const networkCardSheet = workbook.Sheets[workbook.SheetNames[0]];
|
|
|
|
|
|
const portSheet = workbook.Sheets[workbook.SheetNames[1]];
|
|
|
|
|
|
|
|
|
|
|
|
networkCardData = XLSX.utils.sheet_to_json(networkCardSheet);
|
|
|
|
|
|
parsedData = XLSX.utils.sheet_to_json(portSheet);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 其他设备:只解析第一张表格
|
|
|
|
|
|
const sheetName = workbook.SheetNames[0];
|
|
|
|
|
|
const worksheet = workbook.Sheets[sheetName];
|
|
|
|
|
|
parsedData = XLSX.utils.sheet_to_json(worksheet);
|
|
|
|
|
|
}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
} else if (file.name.endsWith('.csv')) {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
parsedData = Papa.parse(data, {
|
2026-01-23 08:55:16 +08:00
|
|
|
|
header: true,
|
|
|
|
|
|
skipEmptyLines: true,
|
2026-03-27 17:52:35 +08:00
|
|
|
|
}).data;
|
2026-01-23 08:55:16 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
message.error('不支持的文件格式,请上传 .xlsx 或 .csv 文件');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (parsedData.length === 0) {
|
|
|
|
|
|
message.warning('文件内容为空,请检查文件内容');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-29 10:06:32 +08:00
|
|
|
|
// 保存网卡数据,供后续导入使用
|
|
|
|
|
|
if (networkCardData.length > 0) {
|
|
|
|
|
|
// 验证网卡数据
|
|
|
|
|
|
const validatedNicData = [];
|
|
|
|
|
|
const nicErrors = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < networkCardData.length; i++) {
|
|
|
|
|
|
const row = networkCardData[i];
|
|
|
|
|
|
const rowNum = i + 2;
|
|
|
|
|
|
const errors = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (!row['设备SN']) {
|
|
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
|
|
|
|
|
field: '设备SN',
|
|
|
|
|
|
value: row['设备SN'] || '(空)',
|
|
|
|
|
|
error: '缺少必填字段',
|
|
|
|
|
|
suggestion: '请填写设备SN,格式如:SNAA0001',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!row['网卡名称']) {
|
|
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
|
|
|
|
|
field: '网卡名称',
|
|
|
|
|
|
value: row['网卡名称'] || '(空)',
|
|
|
|
|
|
error: '缺少必填字段',
|
|
|
|
|
|
suggestion: '请填写网卡名称,格式如:网卡1、eth0',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (row['插槽编号']) {
|
|
|
|
|
|
const validSlotValues = ['LOM', 'OCP', '1', '2', '3', '4'];
|
|
|
|
|
|
const slotValue = row['插槽编号'].toString().trim();
|
|
|
|
|
|
if (!validSlotValues.includes(slotValue) && !/^\d+$/.test(slotValue)) {
|
|
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
|
|
|
|
|
field: '插槽编号',
|
|
|
|
|
|
value: row['插槽编号'],
|
|
|
|
|
|
error: '插槽编号格式错误',
|
|
|
|
|
|
suggestion: '插槽编号必须为数字或预选项(LOM、OCP、1-4)',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
|
|
nicErrors.push(...errors.map(err => ({ ...err, originalRow: row })));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
validatedNicData.push(row);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (nicErrors.length > 0) {
|
|
|
|
|
|
message.warning({
|
|
|
|
|
|
content: `发现 ${nicErrors.length} 个网卡数据错误,请查看错误详情并修正后重新导入`,
|
|
|
|
|
|
duration: 5,
|
|
|
|
|
|
});
|
|
|
|
|
|
setImportErrors(nicErrors);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sessionStorage.setItem('networkCardImportData', JSON.stringify(validatedNicData));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证并处理数据
|
|
|
|
|
|
// 对于服务器设备,跳过网卡存在性验证,因为网卡还没有被导入
|
|
|
|
|
|
const validatedResult = await validateImportData(parsedData, importDeviceType === 'server');
|
2026-03-27 17:52:35 +08:00
|
|
|
|
console.log('文件解析结果:', {
|
|
|
|
|
|
总行数: parsedData.length,
|
|
|
|
|
|
有效数据: validatedResult.validData.length,
|
|
|
|
|
|
错误数据: validatedResult.errors.length,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
第一行数据: parsedData[0],
|
2026-03-29 10:06:32 +08:00
|
|
|
|
网卡数据行数: networkCardData.length,
|
2026-03-27 17:52:35 +08:00
|
|
|
|
});
|
2026-03-29 10:06:32 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
setImportPreview(validatedResult.validData);
|
|
|
|
|
|
setImportErrors(validatedResult.errors);
|
|
|
|
|
|
setImportProgress({ current: 0, total: validatedResult.validData.length });
|
|
|
|
|
|
if (onSuccess) onSuccess();
|
2026-01-23 08:55:16 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('文件解析失败');
|
|
|
|
|
|
console.error('文件解析失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (file.name.endsWith('.csv')) {
|
|
|
|
|
|
reader.readAsText(file);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
reader.readAsBinaryString(file);
|
|
|
|
|
|
}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-29 10:06:32 +08:00
|
|
|
|
const validateImportData = async (data, skipNicValidation = false) => {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const validData = [];
|
|
|
|
|
|
const allErrors = [];
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
|
|
|
|
const row = data[i];
|
2026-03-29 10:06:32 +08:00
|
|
|
|
const result = await validatePortRow(row, i, skipNicValidation);
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (result.valid) {
|
|
|
|
|
|
validData.push(row);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
} else {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
allErrors.push(...result.errors.map(err => ({ ...err, originalRow: row })));
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (allErrors.length > 0) {
|
|
|
|
|
|
message.warning({
|
|
|
|
|
|
content: `发现 ${allErrors.length} 个数据错误,请查看错误详情并修正后重新导入`,
|
|
|
|
|
|
duration: 5,
|
|
|
|
|
|
});
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
return { validData, errors: allErrors };
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-29 10:06:32 +08:00
|
|
|
|
const validatePortRow = async (row, index, skipNicValidation = false) => {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const rowNum = index + 2;
|
|
|
|
|
|
const errors = [];
|
|
|
|
|
|
|
2026-03-29 10:06:32 +08:00
|
|
|
|
if (!row['设备SN']) {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
2026-03-29 10:06:32 +08:00
|
|
|
|
field: '设备SN',
|
|
|
|
|
|
value: row['设备SN'] || '(空)',
|
2026-03-27 17:52:35 +08:00
|
|
|
|
error: '缺少必填字段',
|
2026-03-29 10:06:32 +08:00
|
|
|
|
suggestion: '请填写设备SN,格式如:SNAA0001',
|
2026-03-27 17:52:35 +08:00
|
|
|
|
});
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (!row['端口名称']) {
|
|
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
|
|
|
|
|
field: '端口名称',
|
|
|
|
|
|
value: row['端口名称'] || '(空)',
|
|
|
|
|
|
error: '缺少必填字段',
|
|
|
|
|
|
suggestion: '请填写端口名称,格式如:eth0/1 或 GigabitEthernet0/0/1',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-29 10:06:32 +08:00
|
|
|
|
if (row['设备SN']) {
|
|
|
|
|
|
const device = devices.find(d => d.serialNumber === row['设备SN']);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (!device) {
|
|
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
2026-03-29 10:06:32 +08:00
|
|
|
|
field: '设备SN',
|
|
|
|
|
|
value: row['设备SN'],
|
2026-03-27 17:52:35 +08:00
|
|
|
|
error: '设备不存在',
|
2026-03-29 10:06:32 +08:00
|
|
|
|
suggestion: `设备SN "${row['设备SN']}" 未在系统中注册,请先在设备管理中添加该设备`,
|
2026-03-27 17:52:35 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const validPortTypes = ['RJ45', 'SFP', 'SFP+', 'SFP28', 'QSFP', 'QSFP28'];
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (row['端口类型'] && !validPortTypes.includes(row['端口类型'])) {
|
|
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
|
|
|
|
|
field: '端口类型',
|
|
|
|
|
|
value: row['端口类型'],
|
|
|
|
|
|
error: '无效的端口类型',
|
|
|
|
|
|
suggestion: `端口类型 "${row['端口类型']}" 不支持。可选值:${validPortTypes.join('、')}`,
|
|
|
|
|
|
});
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const validPortSpeeds = ['100M', '1G', '10G', '25G', '40G', '100G'];
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (row['端口速率'] && !validPortSpeeds.includes(row['端口速率'])) {
|
|
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
|
|
|
|
|
field: '端口速率',
|
|
|
|
|
|
value: row['端口速率'],
|
|
|
|
|
|
error: '无效的端口速率',
|
|
|
|
|
|
suggestion: `端口速率 "${row['端口速率']}" 不支持。可选值:${validPortSpeeds.join('、')}`,
|
|
|
|
|
|
});
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const validStatuses = ['空闲', '占用', '故障'];
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (row['状态'] && !validStatuses.includes(row['状态'])) {
|
|
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
|
|
|
|
|
field: '状态',
|
|
|
|
|
|
value: row['状态'],
|
|
|
|
|
|
error: '无效的状态值',
|
|
|
|
|
|
suggestion: `状态 "${row['状态']}" 不支持。可选值:${validStatuses.join('、')}`,
|
|
|
|
|
|
});
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (row['VLAN ID']) {
|
|
|
|
|
|
const vlanPattern = /^\d+$/;
|
|
|
|
|
|
if (!vlanPattern.test(row['VLAN ID'])) {
|
|
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
|
|
|
|
|
field: 'VLAN ID',
|
|
|
|
|
|
value: row['VLAN ID'],
|
|
|
|
|
|
error: 'VLAN ID 格式错误',
|
|
|
|
|
|
suggestion: 'VLAN ID 必须为数字,如:100、200',
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const vlanNum = parseInt(row['VLAN ID']);
|
|
|
|
|
|
if (vlanNum < 1 || vlanNum > 4094) {
|
|
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
|
|
|
|
|
field: 'VLAN ID',
|
|
|
|
|
|
value: row['VLAN ID'],
|
|
|
|
|
|
error: 'VLAN ID 范围超限',
|
|
|
|
|
|
suggestion: 'VLAN ID 必须介于 1-4094 之间',
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-29 10:06:32 +08:00
|
|
|
|
if (!skipNicValidation && row['网卡名称'] && row['设备SN']) {
|
2026-03-26 14:35:08 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const nicResponse = await api.get('/network-cards/find', {
|
2026-03-29 10:06:32 +08:00
|
|
|
|
params: { deviceSn: row['设备SN'], name: row['网卡名称'] },
|
2026-03-26 14:35:08 +08:00
|
|
|
|
});
|
2026-03-27 17:52:35 +08:00
|
|
|
|
row._nicId = nicResponse.nicId || null;
|
2026-03-26 14:35:08 +08:00
|
|
|
|
} catch (error) {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
row._nicId = null;
|
2026-03-26 14:35:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-29 10:06:32 +08:00
|
|
|
|
if (row['设备SN']) {
|
|
|
|
|
|
const device = devices.find(d => d.serialNumber === row['设备SN']) || devices.find(d => d.sn === row['设备SN']);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (device) {
|
|
|
|
|
|
const isServer = device.type && device.type.toLowerCase().includes('server');
|
|
|
|
|
|
if (isServer) {
|
|
|
|
|
|
if (!row['网卡名称'] && !row._nicId) {
|
|
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
|
|
|
|
|
field: '网卡名称',
|
|
|
|
|
|
value: row['网卡名称'] || '(空)',
|
|
|
|
|
|
error: '服务器端口必须关联网卡',
|
|
|
|
|
|
suggestion: `服务器 "${device.name}" 的端口必须关联到网卡,请先在网卡管理中添加网卡,或在导入模板中填写网卡名称`,
|
|
|
|
|
|
});
|
2026-03-29 10:06:32 +08:00
|
|
|
|
} else if (!skipNicValidation && row['网卡名称'] && !row._nicId) {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
errors.push({
|
|
|
|
|
|
row: rowNum,
|
|
|
|
|
|
field: '网卡名称',
|
|
|
|
|
|
value: row['网卡名称'],
|
|
|
|
|
|
error: '网卡不存在',
|
|
|
|
|
|
suggestion: `服务器 "${device.name}" 的网卡"${row['网卡名称']}"不存在,请先在网卡管理中添加该网卡`,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
|
|
return { valid: false, errors };
|
|
|
|
|
|
}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
return { valid: true };
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleBatchImport = async () => {
|
|
|
|
|
|
if (importPreview.length === 0) {
|
|
|
|
|
|
message.warning('请先选择要导入的数据');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
setImporting(true);
|
|
|
|
|
|
setImportProgress({ current: 0, total: importPreview.length });
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
let progressInterval;
|
|
|
|
|
|
let currentProgress = 0;
|
|
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
try {
|
2026-03-29 10:06:32 +08:00
|
|
|
|
// 检查是否有网卡数据需要导入
|
|
|
|
|
|
let networkCardImportResult = null;
|
|
|
|
|
|
const networkCardDataStr = sessionStorage.getItem('networkCardImportData');
|
|
|
|
|
|
|
|
|
|
|
|
if (networkCardDataStr) {
|
|
|
|
|
|
const networkCardData = JSON.parse(networkCardDataStr);
|
|
|
|
|
|
if (networkCardData.length > 0) {
|
|
|
|
|
|
// 先导入网卡数据
|
|
|
|
|
|
console.log('开始导入网卡数据:', networkCardData.length, '条');
|
|
|
|
|
|
|
|
|
|
|
|
// 生成网卡ID
|
|
|
|
|
|
const networkCardsData = networkCardData.map((card, index) => ({
|
|
|
|
|
|
nicId: `NIC-${Date.now()}-${index}`,
|
|
|
|
|
|
deviceSn: card['设备SN'],
|
|
|
|
|
|
name: card['网卡名称'],
|
|
|
|
|
|
slotNumber: card['插槽编号'] ? parseInt(card['插槽编号']) : null,
|
|
|
|
|
|
model: card['网卡型号'] || null,
|
|
|
|
|
|
manufacturer: card['制造商'] || null,
|
|
|
|
|
|
description: card['描述'] || null,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
const nicResponse = await api.post('/network-cards/batch', {
|
|
|
|
|
|
networkCards: networkCardsData,
|
|
|
|
|
|
skipExisting: true,
|
|
|
|
|
|
updateExisting: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
networkCardImportResult = nicResponse;
|
|
|
|
|
|
console.log('网卡导入结果:', nicResponse);
|
|
|
|
|
|
|
|
|
|
|
|
// 重新获取设备列表,确保包含最新数据
|
|
|
|
|
|
await fetchDevices();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清除保存的网卡数据
|
|
|
|
|
|
sessionStorage.removeItem('networkCardImportData');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const statusMap = {
|
2026-02-10 11:04:01 +08:00
|
|
|
|
空闲: 'free',
|
|
|
|
|
|
占用: 'occupied',
|
|
|
|
|
|
故障: 'fault',
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const portsData = importPreview.map((row, index) => ({
|
2026-03-26 14:35:08 +08:00
|
|
|
|
portId: generateUniquePortId(),
|
2026-03-29 10:06:32 +08:00
|
|
|
|
deviceSn: row['设备SN'],
|
2026-03-26 14:35:08 +08:00
|
|
|
|
nicId: row._nicId || null,
|
2026-03-29 10:06:32 +08:00
|
|
|
|
'网卡名称': row['网卡名称'],
|
2026-01-23 08:55:16 +08:00
|
|
|
|
portName: row['端口名称'],
|
|
|
|
|
|
portType: row['端口类型'],
|
|
|
|
|
|
portSpeed: row['端口速率'],
|
|
|
|
|
|
status: statusMap[row['状态']] || 'free',
|
|
|
|
|
|
vlanId: row['VLAN ID'],
|
2026-02-10 11:04:01 +08:00
|
|
|
|
description: row['描述'],
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}));
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
progressInterval = setInterval(() => {
|
|
|
|
|
|
currentProgress = Math.min(currentProgress + Math.random() * 15, 85);
|
|
|
|
|
|
setImportProgress(prev => ({
|
|
|
|
|
|
...prev,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
current: Math.floor((currentProgress / 100) * importPreview.length),
|
2026-03-26 14:35:08 +08:00
|
|
|
|
}));
|
|
|
|
|
|
}, 200);
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const response = await api.post('/device-ports/batch', {
|
|
|
|
|
|
ports: portsData,
|
|
|
|
|
|
skipExisting,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
updateExisting,
|
2026-03-26 14:35:08 +08:00
|
|
|
|
});
|
|
|
|
|
|
const { total, success, failed, skipped = 0, updated = 0, errors } = response;
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
clearInterval(progressInterval);
|
|
|
|
|
|
setImportProgress({ current: importPreview.length, total: importPreview.length });
|
|
|
|
|
|
|
|
|
|
|
|
let msgContent = '';
|
2026-03-29 10:06:32 +08:00
|
|
|
|
if (networkCardImportResult) {
|
|
|
|
|
|
const { success: nicSuccess, failed: nicFailed } = networkCardImportResult;
|
|
|
|
|
|
if (nicSuccess > 0) {
|
|
|
|
|
|
msgContent += `网卡导入成功 ${nicSuccess} 个,`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (nicFailed > 0) {
|
|
|
|
|
|
msgContent += `网卡导入失败 ${nicFailed} 个,`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-26 14:35:08 +08:00
|
|
|
|
if (updated > 0) {
|
2026-03-29 10:06:32 +08:00
|
|
|
|
msgContent += `端口更新 ${updated} 个,`;
|
2026-03-26 14:35:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (skipped > 0) {
|
2026-03-29 10:06:32 +08:00
|
|
|
|
msgContent += `端口跳过 ${skipped} 个,`;
|
2026-03-26 14:35:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (success > 0) {
|
2026-03-29 10:06:32 +08:00
|
|
|
|
msgContent += `端口新增 ${success - updated} 个,`;
|
2026-03-26 14:35:08 +08:00
|
|
|
|
}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
if (failed > 0) {
|
2026-03-29 10:06:32 +08:00
|
|
|
|
msgContent += `端口失败 ${failed} 个`;
|
2026-01-23 08:55:16 +08:00
|
|
|
|
console.error('导入错误:', errors);
|
2026-03-26 14:35:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (failed > 0 && success === 0 && skipped === 0 && updated === 0) {
|
|
|
|
|
|
message.error(`导入失败!${msgContent}`);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
} else {
|
2026-02-12 16:29:19 +08:00
|
|
|
|
message.success({
|
2026-03-26 14:35:08 +08:00
|
|
|
|
content: `导入完成!${msgContent}`,
|
2026-02-12 16:29:19 +08:00
|
|
|
|
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
|
|
|
|
|
});
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
fetchPorts(1, false);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
setImportModalVisible(false);
|
|
|
|
|
|
setImportPreview([]);
|
|
|
|
|
|
} catch (error) {
|
2026-03-26 14:35:08 +08:00
|
|
|
|
clearInterval(progressInterval);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
console.error('批量导入失败:', error);
|
|
|
|
|
|
message.error('批量导入失败,请检查数据格式');
|
|
|
|
|
|
} finally {
|
2026-03-26 14:35:08 +08:00
|
|
|
|
clearInterval(progressInterval);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
setImporting(false);
|
2026-03-29 10:06:32 +08:00
|
|
|
|
// 确保清除保存的网卡数据
|
|
|
|
|
|
sessionStorage.removeItem('networkCardImportData');
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleDownloadTemplate = () => {
|
2026-03-29 10:06:32 +08:00
|
|
|
|
let fileName = '端口导入模板.xlsx';
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const workbook = XLSX.utils.book_new();
|
2026-03-29 10:06:32 +08:00
|
|
|
|
|
|
|
|
|
|
if (importDeviceType === 'server') {
|
|
|
|
|
|
// 网卡数据表格
|
|
|
|
|
|
const networkCardTemplateData = [
|
|
|
|
|
|
{
|
|
|
|
|
|
'设备SN': 'SN-20230001',
|
|
|
|
|
|
'网卡名称': '网卡1',
|
|
|
|
|
|
'插槽编号': 'LOM',
|
|
|
|
|
|
'网卡型号': 'Intel X710',
|
|
|
|
|
|
'制造商': 'Intel',
|
|
|
|
|
|
'描述': '板载网卡',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
'设备SN': 'SN-20230001',
|
|
|
|
|
|
'网卡名称': '网卡2',
|
|
|
|
|
|
'插槽编号': 'OCP',
|
|
|
|
|
|
'网卡型号': 'Intel X710',
|
|
|
|
|
|
'制造商': 'Intel',
|
|
|
|
|
|
'描述': 'OCP网卡',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
'设备SN': 'SN-20230001',
|
|
|
|
|
|
'网卡名称': '网卡3',
|
|
|
|
|
|
'插槽编号': '1',
|
|
|
|
|
|
'网卡型号': 'Intel X710',
|
|
|
|
|
|
'制造商': 'Intel',
|
|
|
|
|
|
'描述': 'PCIe插槽1',
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 端口数据表格
|
|
|
|
|
|
const portTemplateData = [
|
|
|
|
|
|
{
|
|
|
|
|
|
'设备SN': 'SN-20230001',
|
|
|
|
|
|
'网卡名称': '网卡1',
|
|
|
|
|
|
'端口名称': 'eth0/1',
|
|
|
|
|
|
'端口类型': 'RJ45',
|
|
|
|
|
|
'端口速率': '1G',
|
|
|
|
|
|
'状态': '空闲',
|
|
|
|
|
|
'VLAN ID': '100',
|
|
|
|
|
|
'描述': '服务器端口',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
'设备SN': 'SN-20230001',
|
|
|
|
|
|
'网卡名称': '网卡2',
|
|
|
|
|
|
'端口名称': 'eth0/2',
|
|
|
|
|
|
'端口类型': 'RJ45',
|
|
|
|
|
|
'端口速率': '1G',
|
|
|
|
|
|
'状态': '空闲',
|
|
|
|
|
|
'VLAN ID': '200',
|
|
|
|
|
|
'描述': '服务器端口',
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const networkCardWorksheet = XLSX.utils.json_to_sheet(networkCardTemplateData);
|
|
|
|
|
|
const portWorksheet = XLSX.utils.json_to_sheet(portTemplateData);
|
|
|
|
|
|
|
|
|
|
|
|
XLSX.utils.book_append_sheet(workbook, networkCardWorksheet, '网卡数据');
|
|
|
|
|
|
XLSX.utils.book_append_sheet(workbook, portWorksheet, '端口数据');
|
|
|
|
|
|
fileName = '服务器设备导入模板.xlsx';
|
|
|
|
|
|
} else if (importDeviceType === 'switch') {
|
|
|
|
|
|
const templateData = [
|
|
|
|
|
|
{
|
|
|
|
|
|
'设备SN': 'SW-20230001',
|
|
|
|
|
|
'端口名称': 'GigabitEthernet0/0/1',
|
|
|
|
|
|
'端口类型': 'RJ45',
|
|
|
|
|
|
'端口速率': '1G',
|
|
|
|
|
|
'状态': '空闲',
|
|
|
|
|
|
'VLAN ID': '100',
|
|
|
|
|
|
'描述': '交换机端口',
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
const worksheet = XLSX.utils.json_to_sheet(templateData);
|
|
|
|
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, '端口数据');
|
|
|
|
|
|
fileName = '交换机端口导入模板.xlsx';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const templateData = [
|
|
|
|
|
|
{
|
|
|
|
|
|
'设备SN': 'DEV-20230001',
|
|
|
|
|
|
'端口名称': 'eth0/1',
|
|
|
|
|
|
'网卡名称': '',
|
|
|
|
|
|
'端口类型': 'RJ45',
|
|
|
|
|
|
'端口速率': '1G',
|
|
|
|
|
|
'状态': '空闲',
|
|
|
|
|
|
'VLAN ID': '100',
|
|
|
|
|
|
'描述': '示例端口',
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
const worksheet = XLSX.utils.json_to_sheet(templateData);
|
|
|
|
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, '端口数据');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
XLSX.writeFile(workbook, fileName);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const handleExport = async ({ scope, format }) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
let exportData = [];
|
|
|
|
|
|
let totalCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (scope === 'all') {
|
|
|
|
|
|
const params = {
|
|
|
|
|
|
pageSize: 50000,
|
|
|
|
|
|
};
|
|
|
|
|
|
const response = await api.get('/device-ports/export/all', { params });
|
|
|
|
|
|
exportData = response.ports || [];
|
|
|
|
|
|
totalCount = response.total || exportData.length;
|
|
|
|
|
|
} else if (scope === 'filtered') {
|
|
|
|
|
|
const params = {
|
|
|
|
|
|
deviceId: filters.deviceId || undefined,
|
|
|
|
|
|
pageSize: 50000,
|
|
|
|
|
|
};
|
|
|
|
|
|
const response = await api.get('/device-ports/export/all', { params });
|
|
|
|
|
|
exportData = response.ports || [];
|
|
|
|
|
|
totalCount = response.total || exportData.length;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
exportData = ports.map(port => {
|
|
|
|
|
|
const device = devices.find(d => d.deviceId === port.deviceId);
|
|
|
|
|
|
const statusMap = { free: '空闲', occupied: '占用', fault: '故障' };
|
|
|
|
|
|
return {
|
|
|
|
|
|
端口ID: port.portId,
|
|
|
|
|
|
设备ID: port.deviceId,
|
|
|
|
|
|
设备名称: device?.name || '-',
|
|
|
|
|
|
设备类型: device?.type || '-',
|
|
|
|
|
|
机房: '-',
|
|
|
|
|
|
机架: '-',
|
|
|
|
|
|
网卡名称: port.nicId || '-',
|
|
|
|
|
|
端口名称: port.portName,
|
|
|
|
|
|
端口类型: port.portType,
|
|
|
|
|
|
端口速率: port.portSpeed,
|
|
|
|
|
|
状态: statusMap[port.status] || port.status,
|
|
|
|
|
|
VLAN_ID: port.vlanId || '-',
|
|
|
|
|
|
描述: port.description || '-',
|
|
|
|
|
|
创建时间: port.createdAt ? new Date(port.createdAt).toLocaleString('zh-CN') : '-',
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
totalCount = exportData.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (exportData.length === 0) {
|
|
|
|
|
|
message.warning('没有可导出的端口数据');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (format === 'csv') {
|
|
|
|
|
|
const csvContent = Papa.unparse(exportData);
|
|
|
|
|
|
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8' });
|
|
|
|
|
|
const url = window.URL.createObjectURL(blob);
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
|
link.href = url;
|
|
|
|
|
|
link.download = `端口导出_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.csv`;
|
|
|
|
|
|
document.body.appendChild(link);
|
|
|
|
|
|
link.click();
|
|
|
|
|
|
document.body.removeChild(link);
|
|
|
|
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const worksheet = XLSX.utils.json_to_sheet(exportData);
|
|
|
|
|
|
const workbook = XLSX.utils.book_new();
|
|
|
|
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, '端口数据');
|
|
|
|
|
|
|
|
|
|
|
|
const colWidths = [
|
|
|
|
|
|
{ wch: 20 },
|
|
|
|
|
|
{ wch: 15 },
|
|
|
|
|
|
{ wch: 20 },
|
|
|
|
|
|
{ wch: 12 },
|
|
|
|
|
|
{ wch: 10 },
|
|
|
|
|
|
{ wch: 12 },
|
|
|
|
|
|
{ wch: 15 },
|
|
|
|
|
|
{ wch: 15 },
|
|
|
|
|
|
{ wch: 10 },
|
|
|
|
|
|
{ wch: 8 },
|
|
|
|
|
|
{ wch: 10 },
|
|
|
|
|
|
{ wch: 10 },
|
|
|
|
|
|
{ wch: 25 },
|
|
|
|
|
|
{ wch: 20 },
|
|
|
|
|
|
];
|
|
|
|
|
|
worksheet['!cols'] = colWidths;
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
XLSX.writeFile(
|
|
|
|
|
|
workbook,
|
|
|
|
|
|
`端口导出_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.xlsx`
|
|
|
|
|
|
);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message.success({
|
|
|
|
|
|
content: `成功导出 ${totalCount} 个端口`,
|
|
|
|
|
|
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('导出失败:', error);
|
|
|
|
|
|
message.error('导出失败,请重试');
|
2026-03-26 14:35:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const getStatusTag = status => {
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const statusMap = {
|
2026-02-12 16:29:19 +08:00
|
|
|
|
free: { color: 'success', text: '空闲', icon: <CheckCircleOutlined /> },
|
|
|
|
|
|
occupied: { color: 'processing', text: '占用', icon: <AppstoreOutlined /> },
|
|
|
|
|
|
fault: { color: 'error', text: '故障', icon: <ExclamationCircleOutlined /> },
|
|
|
|
|
|
空闲: { color: 'success', text: '空闲', icon: <CheckCircleOutlined /> },
|
|
|
|
|
|
占用: { color: 'processing', text: '占用', icon: <AppstoreOutlined /> },
|
|
|
|
|
|
故障: { color: 'error', text: '故障', icon: <ExclamationCircleOutlined /> },
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
2026-02-12 16:29:19 +08:00
|
|
|
|
const config = statusMap[status] || { color: 'default', text: status, icon: null };
|
|
|
|
|
|
return (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Tag
|
2026-02-12 16:29:19 +08:00
|
|
|
|
color={config.color}
|
|
|
|
|
|
icon={config.icon}
|
|
|
|
|
|
style={{ borderRadius: '4px', padding: '2px 8px', fontSize: '12px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{config.text}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const getPortTypeTag = type => {
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const typeMap = {
|
2026-02-10 11:04:01 +08:00
|
|
|
|
RJ45: { color: 'blue', text: 'RJ45' },
|
|
|
|
|
|
SFP: { color: 'green', text: 'SFP' },
|
2026-01-23 08:55:16 +08:00
|
|
|
|
'SFP+': { color: 'cyan', text: 'SFP+' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
SFP28: { color: 'purple', text: 'SFP28' },
|
|
|
|
|
|
QSFP: { color: 'orange', text: 'QSFP' },
|
|
|
|
|
|
QSFP28: { color: 'red', text: 'QSFP28' },
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
const config = typeMap[type] || { color: 'default', text: type };
|
2026-02-12 16:29:19 +08:00
|
|
|
|
return (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Tag
|
|
|
|
|
|
color={config.color}
|
|
|
|
|
|
style={{ borderRadius: '4px', padding: '2px 8px', fontSize: '12px' }}
|
|
|
|
|
|
>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
{config.text}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const portColumns = useMemo(
|
|
|
|
|
|
() => [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '端口名称',
|
|
|
|
|
|
dataIndex: 'portName',
|
|
|
|
|
|
key: 'portName',
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
render: text => (
|
|
|
|
|
|
<span style={{ fontWeight: 500, color: designTokens.colors.neutral[800] }}>{text}</span>
|
|
|
|
|
|
),
|
2026-03-26 14:35:08 +08:00
|
|
|
|
},
|
2026-03-27 19:12:16 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '所属网卡',
|
|
|
|
|
|
dataIndex: 'networkCard',
|
|
|
|
|
|
key: 'networkCard',
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
render: (nic, record) => {
|
|
|
|
|
|
const isServer = record.device?.type?.toLowerCase()?.includes('server');
|
|
|
|
|
|
if (!isServer) return <Text type="secondary">-</Text>;
|
|
|
|
|
|
if (!nic)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Text type="secondary" style={{ color: '#ff4d4f' }}>
|
|
|
|
|
|
未关联
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
);
|
|
|
|
|
|
return <Text style={{ color: '#667eea' }}>{nic.name}</Text>;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '端口类型',
|
|
|
|
|
|
dataIndex: 'portType',
|
|
|
|
|
|
key: 'portType',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: type => getPortTypeTag(type),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '端口速率',
|
|
|
|
|
|
dataIndex: 'portSpeed',
|
|
|
|
|
|
key: 'portSpeed',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: text => <Text type="secondary">{text}</Text>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
dataIndex: 'status',
|
|
|
|
|
|
key: 'status',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: status => getStatusTag(status),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'VLAN ID',
|
|
|
|
|
|
dataIndex: 'vlanId',
|
|
|
|
|
|
key: 'vlanId',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: vlanId => vlanId || <Text type="secondary">-</Text>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '描述',
|
|
|
|
|
|
dataIndex: 'description',
|
|
|
|
|
|
key: 'description',
|
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
|
render: text => text || <Text type="secondary">-</Text>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space size="small">
|
|
|
|
|
|
<Tooltip title="编辑">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon={<EditOutlined />}
|
|
|
|
|
|
onClick={() => handleEdit(record)}
|
|
|
|
|
|
style={{ color: designTokens.colors.primary.main }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip title="删除">
|
|
|
|
|
|
<Popconfirm
|
|
|
|
|
|
title="确定要删除这个端口吗?"
|
|
|
|
|
|
onConfirm={() => handleDelete(record.portId)}
|
|
|
|
|
|
okText="确定"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
|
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
[]
|
|
|
|
|
|
);
|
2026-02-12 16:29:19 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取设备图标
|
|
|
|
|
|
const getDeviceIcon = device => {
|
|
|
|
|
|
if (!device?.type) return <AppstoreOutlined />;
|
|
|
|
|
|
const type = device.type.toLowerCase();
|
|
|
|
|
|
if (type.includes('server')) return <CloudServerOutlined />;
|
|
|
|
|
|
if (type.includes('switch')) return <AppstoreOutlined />;
|
|
|
|
|
|
if (type.includes('router')) return <ApiOutlined />;
|
|
|
|
|
|
return <AppstoreOutlined />;
|
|
|
|
|
|
};
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const getDeviceType = device => {
|
|
|
|
|
|
if (!device?.type) return 'unknown';
|
|
|
|
|
|
const type = device.type.toLowerCase();
|
|
|
|
|
|
if (type.includes('switch')) return 'switch';
|
|
|
|
|
|
if (type.includes('server')) return 'server';
|
|
|
|
|
|
return 'other';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const isSwitchDevice = device => getDeviceType(device) === 'switch';
|
|
|
|
|
|
const isServerDevice = device => getDeviceType(device) === 'server';
|
|
|
|
|
|
|
|
|
|
|
|
// 过滤后的设备列表(带分页)
|
|
|
|
|
|
const paginatedDevices = useMemo(() => {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const rackRoomMap = {};
|
|
|
|
|
|
rackList.forEach(rack => {
|
|
|
|
|
|
rackRoomMap[rack.rackId] = rack.roomId;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const filtered = devices.filter(d => {
|
|
|
|
|
|
const type = getDeviceType(d);
|
|
|
|
|
|
if (type !== 'switch' && type !== 'server') return false;
|
|
|
|
|
|
if (deviceFilterType !== 'all' && type !== deviceFilterType) return false;
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (selectedRackId && d.rackId !== selectedRackId) return false;
|
|
|
|
|
|
if (selectedRoomId && d.rackId && rackRoomMap[d.rackId] !== selectedRoomId) return false;
|
2026-03-26 14:35:08 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
});
|
|
|
|
|
|
const pageSize = 100;
|
|
|
|
|
|
const end = devicePage * pageSize;
|
|
|
|
|
|
const hasMore = end < filtered.length;
|
|
|
|
|
|
hasMoreRef.current = hasMore;
|
|
|
|
|
|
return {
|
|
|
|
|
|
list: filtered.slice(0, end),
|
|
|
|
|
|
total: filtered.length,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
hasMore,
|
2026-03-26 14:35:08 +08:00
|
|
|
|
};
|
2026-03-27 17:52:35 +08:00
|
|
|
|
}, [devices, deviceFilterType, devicePage, rackList, selectedRoomId, selectedRackId]);
|
2026-03-26 14:35:08 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!loadMoreRef.current) return;
|
|
|
|
|
|
|
|
|
|
|
|
const observer = new IntersectionObserver(
|
|
|
|
|
|
entries => {
|
|
|
|
|
|
if (entries[0].isIntersecting && hasMoreRef.current && !isLoadingRef.current) {
|
|
|
|
|
|
isLoadingRef.current = true;
|
|
|
|
|
|
setDevicePage(p => p + 1);
|
2026-03-27 19:12:16 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
isLoadingRef.current = false;
|
|
|
|
|
|
}, 200);
|
2026-03-26 14:35:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ threshold: 0.1 }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
observer.observe(loadMoreRef.current);
|
|
|
|
|
|
|
|
|
|
|
|
return () => observer.disconnect();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
return (
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<motion.div
|
|
|
|
|
|
variants={animations.container}
|
|
|
|
|
|
initial="hidden"
|
|
|
|
|
|
animate="visible"
|
2026-03-27 19:12:16 +08:00
|
|
|
|
style={{
|
|
|
|
|
|
padding: '24px',
|
|
|
|
|
|
background: designTokens.colors.neutral[50],
|
2026-02-12 16:29:19 +08:00
|
|
|
|
minHeight: '100vh',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 页面标题 */}
|
|
|
|
|
|
<motion.div variants={animations.item} style={{ marginBottom: '24px' }}>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '40px',
|
|
|
|
|
|
height: '40px',
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.md,
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontSize: '20px',
|
|
|
|
|
|
}}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<ApiOutlined />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Title level={4} style={{ margin: 0, color: designTokens.colors.neutral[800] }}>
|
|
|
|
|
|
端口管理
|
|
|
|
|
|
</Title>
|
|
|
|
|
|
<Text type="secondary" style={{ fontSize: '13px' }}>
|
|
|
|
|
|
管理设备的网络端口信息
|
|
|
|
|
|
</Text>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</motion.div>
|
2026-01-30 17:53:05 +08:00
|
|
|
|
|
2026-02-12 16:29:19 +08:00
|
|
|
|
{/* 主内容区 */}
|
|
|
|
|
|
<motion.div variants={animations.item}>
|
|
|
|
|
|
<Card
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.lg,
|
|
|
|
|
|
boxShadow: designTokens.shadows.md,
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
}}
|
|
|
|
|
|
bodyStyle={{ padding: '24px' }}
|
|
|
|
|
|
>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
{/* 功能区:过滤 + 操作按钮 */}
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<div style={{ marginBottom: '24px' }}>
|
|
|
|
|
|
<Card
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: designTokens.colors.neutral[50],
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.md,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
bodyStyle={{ padding: '16px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Row gutter={[16, 16]} align="middle">
|
2026-03-27 17:52:35 +08:00
|
|
|
|
<Col xs={24} sm={24} md={10} lg={8}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginBottom: '6px',
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
color: designTokens.colors.neutral[600],
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
设备筛选
|
2026-02-12 16:29:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<Select
|
2026-03-03 10:51:51 +08:00
|
|
|
|
placeholder="输入关键词搜索设备"
|
2026-02-12 16:29:19 +08:00
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
|
value={filters.deviceId || undefined}
|
|
|
|
|
|
onChange={value => setFilters(prev => ({ ...prev, deviceId: value }))}
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
showSearch
|
2026-03-03 10:51:51 +08:00
|
|
|
|
loading={deviceSearching}
|
|
|
|
|
|
filterOption={false}
|
|
|
|
|
|
onSearch={handleDeviceSearch}
|
|
|
|
|
|
onDropdownVisibleChange={open => {
|
|
|
|
|
|
if (open && devices.length === 0) {
|
|
|
|
|
|
fetchDevices();
|
|
|
|
|
|
}
|
2026-02-12 16:29:19 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{devices.map(device => (
|
|
|
|
|
|
<Option key={device.deviceId} value={device.deviceId}>
|
|
|
|
|
|
{device.name} ({device.deviceId})
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
<Col xs={24} sm={24} md={14} lg={16}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginBottom: '6px',
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
color: designTokens.colors.neutral[600],
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
操作
|
2026-02-12 16:29:19 +08:00
|
|
|
|
</div>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
<Space size="small" wrap>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
2026-03-27 17:52:35 +08:00
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
|
onClick={handleAdd}
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
2026-02-12 16:29:19 +08:00
|
|
|
|
border: 'none',
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.sm,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
新增端口
|
2026-02-12 16:29:19 +08:00
|
|
|
|
</Button>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
icon={<CloudServerOutlined />}
|
|
|
|
|
|
onClick={handleManageServerNics}
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
style={{ borderRadius: designTokens.borderRadius.sm }}
|
|
|
|
|
|
>
|
|
|
|
|
|
管理网卡
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<ImportOutlined />}
|
|
|
|
|
|
onClick={() => setBatchImportModalVisible(true)}
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
style={{ borderRadius: designTokens.borderRadius.sm }}
|
|
|
|
|
|
>
|
|
|
|
|
|
批量导入
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<ExportOutlined />}
|
|
|
|
|
|
onClick={() => setPortExportModalVisible(true)}
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
style={{ borderRadius: designTokens.borderRadius.sm }}
|
|
|
|
|
|
>
|
|
|
|
|
|
导出
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Tooltip title="刷新数据">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<ReloadOutlined />}
|
|
|
|
|
|
onClick={fetchPorts}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
size="middle"
|
2026-02-12 16:29:19 +08:00
|
|
|
|
style={{ borderRadius: designTokens.borderRadius.sm }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 数据展示区域 */}
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
|
<div style={{ padding: '40px' }}>
|
|
|
|
|
|
<Skeleton active paragraph={{ rows: 6 }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : Object.keys(groupedPorts).length === 0 ? (
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ duration: 0.3 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Empty
|
|
|
|
|
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
|
|
|
|
description={
|
|
|
|
|
|
<div style={{ textAlign: 'center' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '16px',
|
|
|
|
|
|
color: designTokens.colors.neutral[600],
|
|
|
|
|
|
marginBottom: '8px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
暂无端口数据
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ fontSize: '13px', color: designTokens.colors.neutral[400] }}>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
点击下方按钮添加端口
|
2026-02-12 16:29:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
|
onClick={handleAdd}
|
2026-03-26 17:05:20 +08:00
|
|
|
|
style={{
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
2026-02-12 16:29:19 +08:00
|
|
|
|
border: 'none',
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.sm,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
新增端口
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Empty>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
) : (
|
2026-03-16 17:07:20 +08:00
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
{Object.entries(groupedPorts)
|
|
|
|
|
|
.slice(
|
|
|
|
|
|
(deviceCardPage - 1) * deviceCardPageSize,
|
|
|
|
|
|
deviceCardPage * deviceCardPageSize
|
|
|
|
|
|
)
|
|
|
|
|
|
.map(([deviceId, data], index) => {
|
|
|
|
|
|
const device = data.device;
|
|
|
|
|
|
const devicePorts = data.ports || [];
|
|
|
|
|
|
const freeCount = devicePorts.filter(p => p.status === 'free').length;
|
|
|
|
|
|
const occupiedCount = devicePorts.filter(p => p.status === 'occupied').length;
|
|
|
|
|
|
const faultCount = devicePorts.filter(p => p.status === 'fault').length;
|
|
|
|
|
|
const isExpanded = expandedKeys.includes(deviceId);
|
2026-02-12 16:29:19 +08:00
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<Card
|
|
|
|
|
|
key={deviceId}
|
2026-03-16 17:07:20 +08:00
|
|
|
|
style={{
|
2026-03-27 19:12:16 +08:00
|
|
|
|
borderRadius: designTokens.borderRadius.lg,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
|
|
|
|
|
overflow: 'hidden',
|
2026-03-16 17:07:20 +08:00
|
|
|
|
}}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
bodyStyle={{ padding: 0 }}
|
2026-03-16 17:07:20 +08:00
|
|
|
|
>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
{/* 设备头部 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
padding: '16px 20px',
|
|
|
|
|
|
background: isExpanded ? designTokens.colors.primary.light : '#fff',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
transition: 'background 0.2s',
|
|
|
|
|
|
}}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
if (isExpanded) {
|
|
|
|
|
|
setExpandedKeys(prev => prev.filter(key => key !== deviceId));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setExpandedKeys(prev => [...prev, deviceId]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '48px',
|
|
|
|
|
|
height: '48px',
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.md,
|
|
|
|
|
|
background: isServerDevice(device)
|
|
|
|
|
|
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
|
|
|
|
|
: isSwitchDevice(device)
|
|
|
|
|
|
? 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)'
|
|
|
|
|
|
: designTokens.colors.primary.gradient,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontSize: '24px',
|
|
|
|
|
|
boxShadow: designTokens.shadows.md,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{getDeviceIcon(device)}
|
2026-03-16 17:07:20 +08:00
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
fontSize: '16px',
|
|
|
|
|
|
color: designTokens.colors.neutral[800],
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{device?.name || '未知设备'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<Tag
|
|
|
|
|
|
color={
|
|
|
|
|
|
isServerDevice(device)
|
|
|
|
|
|
? 'blue'
|
|
|
|
|
|
: isSwitchDevice(device)
|
|
|
|
|
|
? 'green'
|
|
|
|
|
|
: 'default'
|
|
|
|
|
|
}
|
|
|
|
|
|
style={{ marginLeft: '4px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isServerDevice(device)
|
|
|
|
|
|
? '服务器'
|
|
|
|
|
|
: isSwitchDevice(device)
|
|
|
|
|
|
? '交换机'
|
|
|
|
|
|
: '设备'}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
color: designTokens.colors.neutral[500],
|
|
|
|
|
|
marginTop: '2px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{device?.deviceId || '-'} · {device?.model || device?.type || '设备'}
|
|
|
|
|
|
</div>
|
2026-03-16 17:07:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
|
<Space size="small">
|
|
|
|
|
|
<Tooltip title="空闲">
|
|
|
|
|
|
<Tag
|
|
|
|
|
|
color="success"
|
|
|
|
|
|
style={{ borderRadius: '4px', padding: '4px 12px' }}
|
|
|
|
|
|
icon={<CheckCircleOutlined />}
|
|
|
|
|
|
>
|
|
|
|
|
|
{freeCount}
|
2026-03-16 17:07:20 +08:00
|
|
|
|
</Tag>
|
|
|
|
|
|
</Tooltip>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Tooltip title="占用">
|
|
|
|
|
|
<Tag
|
|
|
|
|
|
color="processing"
|
|
|
|
|
|
style={{ borderRadius: '4px', padding: '4px 12px' }}
|
|
|
|
|
|
icon={<AppstoreOutlined />}
|
|
|
|
|
|
>
|
|
|
|
|
|
{occupiedCount}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
{faultCount > 0 && (
|
|
|
|
|
|
<Tooltip title="故障">
|
|
|
|
|
|
<Tag
|
|
|
|
|
|
color="error"
|
|
|
|
|
|
style={{ borderRadius: '4px', padding: '4px 12px' }}
|
|
|
|
|
|
icon={<ExclamationCircleOutlined />}
|
|
|
|
|
|
>
|
|
|
|
|
|
{faultCount}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Tag
|
|
|
|
|
|
color="blue"
|
|
|
|
|
|
style={{ borderRadius: '4px', padding: '4px 12px', fontWeight: 500 }}
|
|
|
|
|
|
>
|
|
|
|
|
|
总计: {devicePorts.length}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
<Divider type="vertical" style={{ height: '24px', margin: '0 8px' }} />
|
|
|
|
|
|
<Space size="small">
|
|
|
|
|
|
<Tooltip
|
|
|
|
|
|
title={
|
|
|
|
|
|
isServerDevice(device)
|
|
|
|
|
|
? '添加端口'
|
|
|
|
|
|
: '添加端口(交换机端口无需关联网卡)'
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
2026-03-27 19:12:16 +08:00
|
|
|
|
icon={<PlusOutlined />}
|
2026-03-16 17:07:20 +08:00
|
|
|
|
onClick={e => {
|
|
|
|
|
|
e.stopPropagation();
|
2026-03-27 19:12:16 +08:00
|
|
|
|
handleAddPortForDevice(device);
|
2026-03-16 17:07:20 +08:00
|
|
|
|
}}
|
2026-02-12 16:29:19 +08:00
|
|
|
|
style={{ color: designTokens.colors.primary.main }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
{isServerDevice(device) && (
|
|
|
|
|
|
<Tooltip title="网卡管理">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
icon={<CloudServerOutlined />}
|
|
|
|
|
|
onClick={e => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
handleManageNetworkCards(device);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{ color: designTokens.colors.primary.main }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Tooltip>
|
2026-03-26 14:35:08 +08:00
|
|
|
|
)}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon={isExpanded ? <UpOutlined /> : <DownOutlined />}
|
|
|
|
|
|
style={{ color: designTokens.colors.neutral[600], minWidth: '70px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isExpanded ? '收起' : '展开'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
2026-03-16 17:07:20 +08:00
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 端口列表 */}
|
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '16px 20px',
|
|
|
|
|
|
borderTop: `1px solid ${designTokens.colors.neutral[200]}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{devicePorts.length > 0 ? (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{selectedRowKeys.length > 0 && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginBottom: '12px',
|
|
|
|
|
|
padding: '8px 12px',
|
|
|
|
|
|
background: designTokens.colors.error.bg,
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.sm,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Text type="secondary">
|
|
|
|
|
|
已选择 {selectedRowKeys.length} 个端口
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
danger
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
|
onClick={handleBatchDelete}
|
|
|
|
|
|
>
|
|
|
|
|
|
批量删除
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={portColumns}
|
|
|
|
|
|
dataSource={devicePorts}
|
|
|
|
|
|
rowKey="portId"
|
|
|
|
|
|
rowSelection={{
|
|
|
|
|
|
selectedRowKeys,
|
|
|
|
|
|
onChange: setSelectedRowKeys,
|
|
|
|
|
|
}}
|
|
|
|
|
|
pagination={{
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
showTotal: total => `共 ${total} 个端口`,
|
|
|
|
|
|
pageSizeOptions: ['10', '20', '50', '100'],
|
|
|
|
|
|
}}
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
scroll={{ x: 1000 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Empty description="暂无端口数据" style={{ padding: '24px 0' }} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2026-03-27 17:52:35 +08:00
|
|
|
|
{deviceCardTotal > deviceCardPageSize && (
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '16px 0' }}>
|
|
|
|
|
|
<Pagination
|
|
|
|
|
|
current={deviceCardPage}
|
|
|
|
|
|
pageSize={deviceCardPageSize}
|
|
|
|
|
|
total={deviceCardTotal}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
onChange={page => {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
setDeviceCardPage(page);
|
|
|
|
|
|
setExpandedKeys([]);
|
|
|
|
|
|
}}
|
|
|
|
|
|
showSizeChanger={false}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
showTotal={(total, range) =>
|
|
|
|
|
|
`第 ${range[0]}-${range[1]} 个,共 ${total} 个设备`
|
|
|
|
|
|
}
|
2026-03-27 17:52:35 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{hasMoreRef.current && (
|
|
|
|
|
|
<div ref={loadMoreRef} style={{ textAlign: 'center', padding: '20px' }}>
|
|
|
|
|
|
{portLoadingMore && <Spin tip="加载更多端口..." />}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{!hasMoreRef.current && ports.length > 0 && (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '16px', color: '#999' }}>
|
|
|
|
|
|
已加载全部 {portTotal} 个端口
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-03-16 17:07:20 +08:00
|
|
|
|
</div>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</motion.div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-02-12 16:29:19 +08:00
|
|
|
|
{/* 新增/编辑端口弹窗 */}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
<Modal
|
2026-03-26 17:05:20 +08:00
|
|
|
|
open={modalVisible}
|
|
|
|
|
|
closeIcon={<CloseButton />}
|
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setModalVisible(false);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
setSelectedDeviceForPort(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={600}
|
|
|
|
|
|
zIndex={1050}
|
|
|
|
|
|
style={{ borderRadius: '16px' }}
|
|
|
|
|
|
styles={{ body: { padding: 0 } }}
|
|
|
|
|
|
>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: `linear-gradient(135deg, ${designTokens.colors.primary.main} 0%, ${designTokens.colors.primary.dark} 100%)`,
|
|
|
|
|
|
padding: '20px 24px',
|
|
|
|
|
|
borderRadius: '16px 16px 0 0',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '36px',
|
|
|
|
|
|
height: '36px',
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.md,
|
2026-03-26 17:05:20 +08:00
|
|
|
|
background: 'rgba(255,255,255,0.2)',
|
2026-02-12 16:29:19 +08:00
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ApiOutlined />
|
|
|
|
|
|
</div>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
<span style={{ fontSize: '18px', fontWeight: 600, color: '#fff' }}>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
{editingPort ? '编辑端口' : '新增端口'}
|
|
|
|
|
|
</span>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
{selectedDeviceForPort && (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Tag
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'rgba(255,255,255,0.2)',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
{selectedDeviceForPort.name}
|
|
|
|
|
|
</Tag>
|
|
|
|
|
|
)}
|
2026-02-12 16:29:19 +08:00
|
|
|
|
</div>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ padding: '24px', background: '#fff' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
2026-03-26 17:05:20 +08:00
|
|
|
|
display: 'flex',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
marginBottom: '24px',
|
2026-03-26 17:05:20 +08:00
|
|
|
|
gap: '8px',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
2026-03-26 17:05:20 +08:00
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
gap: '8px',
|
|
|
|
|
|
padding: '8px 20px',
|
|
|
|
|
|
borderRadius: '20px',
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
background: designTokens.colors.primary.bg,
|
|
|
|
|
|
color: designTokens.colors.primary.main,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '24px',
|
|
|
|
|
|
height: '24px',
|
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
|
background: designTokens.colors.primary.main,
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
1
|
|
|
|
|
|
</div>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
<span>基本信息</span>
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '40px',
|
|
|
|
|
|
height: '2px',
|
|
|
|
|
|
background: designTokens.colors.primary.main,
|
|
|
|
|
|
alignSelf: 'center',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
2026-03-26 17:05:20 +08:00
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
gap: '8px',
|
|
|
|
|
|
padding: '8px 20px',
|
|
|
|
|
|
borderRadius: '20px',
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
background: designTokens.colors.neutral[100],
|
|
|
|
|
|
color: designTokens.colors.neutral[500],
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '24px',
|
|
|
|
|
|
height: '24px',
|
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
|
background: 'transparent',
|
|
|
|
|
|
color: designTokens.colors.neutral[500],
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.neutral[400]}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
2
|
|
|
|
|
|
</div>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
<span>高级配置</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Form form={form} layout="vertical">
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message="格式说明"
|
|
|
|
|
|
description={
|
|
|
|
|
|
<div style={{ fontSize: '12px', lineHeight: '1.8' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>单个端口:</strong>eth0/1、gigabitethernet1/0/1
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>端口范围:</strong>1/0/1-1/0/48(创建 1/0/1 到 1/0/48 共48个端口)
|
|
|
|
|
|
</div>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
background: designTokens.colors.info.bg,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.info.light}40`,
|
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: designTokens.colors.neutral[50],
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
padding: '20px',
|
2026-03-26 17:05:20 +08:00
|
|
|
|
marginBottom: '16px',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
color: designTokens.colors.neutral[800],
|
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '8px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
<TagOutlined style={{ color: designTokens.colors.primary.main }} />
|
|
|
|
|
|
端口标识
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{selectedDeviceForPort ? (
|
|
|
|
|
|
<>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Form.Item
|
|
|
|
|
|
label={<span style={{ fontSize: '13px', fontWeight: 500 }}>设备</span>}
|
|
|
|
|
|
>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
value={selectedDeviceForPort.name}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
addonAfter={
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
color: isServerDevice(selectedDeviceForPort)
|
|
|
|
|
|
? designTokens.colors.warning.main
|
|
|
|
|
|
: designTokens.colors.success.main,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isServerDevice(selectedDeviceForPort)
|
|
|
|
|
|
? '服务器'
|
|
|
|
|
|
: isSwitchDevice(selectedDeviceForPort)
|
|
|
|
|
|
? '交换机'
|
|
|
|
|
|
: '设备'}
|
2026-03-26 17:05:20 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
style={{ borderRadius: '8px' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gridTemplateColumns: isServerDevice(selectedDeviceForPort)
|
|
|
|
|
|
? '1fr 1fr'
|
|
|
|
|
|
: '1fr',
|
|
|
|
|
|
gap: '16px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
{isServerDevice(selectedDeviceForPort) && (
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="nicId"
|
|
|
|
|
|
label={<span style={{ fontSize: '13px', fontWeight: 500 }}>所属网卡</span>}
|
|
|
|
|
|
rules={[{ required: true, message: '请选择网卡' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
placeholder="请选择网卡"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
style={{ borderRadius: '8px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{nicList.map(nic => (
|
|
|
|
|
|
<Option key={nic.nicId} value={nic.nicId}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '8px',
|
|
|
|
|
|
padding: '4px 0',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ fontWeight: 500 }}>{nic.name}</div>
|
|
|
|
|
|
{nic.speed && (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
|
color: designTokens.colors.neutral[500],
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
{nic.speed}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="portName"
|
|
|
|
|
|
label={<span style={{ fontSize: '13px', fontWeight: 500 }}>端口名称</span>}
|
|
|
|
|
|
rules={[{ required: true, message: '请输入端口名称' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="例如: eth0/1 或 1/0/1-1/0/48"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
prefix={<TagOutlined style={{ color: designTokens.colors.neutral[400] }} />}
|
|
|
|
|
|
suffix={
|
|
|
|
|
|
<Tooltip title="支持批量添加,例如: 1/0/1-1/0/48 将创建 48 个端口">
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<InfoCircleOutlined
|
|
|
|
|
|
style={{ color: designTokens.colors.neutral[400] }}
|
|
|
|
|
|
/>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
</Tooltip>
|
|
|
|
|
|
}
|
|
|
|
|
|
style={{ borderRadius: '8px' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
2026-03-26 14:35:08 +08:00
|
|
|
|
<Form.Item
|
2026-03-26 17:05:20 +08:00
|
|
|
|
name="deviceId"
|
|
|
|
|
|
label={<span style={{ fontSize: '13px', fontWeight: 500 }}>设备</span>}
|
|
|
|
|
|
rules={[{ required: true, message: '请选择设备' }]}
|
2026-03-26 14:35:08 +08:00
|
|
|
|
>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
<Select
|
|
|
|
|
|
placeholder="输入关键词搜索设备"
|
|
|
|
|
|
showSearch
|
|
|
|
|
|
loading={deviceSearching}
|
|
|
|
|
|
filterOption={false}
|
|
|
|
|
|
onSearch={handleDeviceSearch}
|
|
|
|
|
|
onDropdownVisibleChange={open => {
|
|
|
|
|
|
if (open && devices.length === 0) {
|
|
|
|
|
|
fetchDevices();
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
disabled={!!editingPort}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
style={{ borderRadius: '8px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{devices.map(device => (
|
|
|
|
|
|
<Option key={device.deviceId} value={device.deviceId}>
|
|
|
|
|
|
{device.name} ({device.deviceId})
|
2026-03-26 14:35:08 +08:00
|
|
|
|
</Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
)}
|
2026-03-26 17:05:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: designTokens.colors.neutral[50],
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
padding: '20px',
|
2026-03-26 17:05:20 +08:00
|
|
|
|
marginBottom: '16px',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
color: designTokens.colors.neutral[800],
|
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '8px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
<ThunderboltOutlined style={{ color: designTokens.colors.primary.main }} />
|
|
|
|
|
|
端口属性
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="portType"
|
|
|
|
|
|
label={<span style={{ fontSize: '13px', fontWeight: 500 }}>端口类型</span>}
|
|
|
|
|
|
rules={[{ required: true, message: '请选择端口类型' }]}
|
|
|
|
|
|
initialValue="RJ45"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select size="large" style={{ borderRadius: '8px' }}>
|
|
|
|
|
|
<Option value="RJ45">
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '8px',
|
|
|
|
|
|
height: '8px',
|
|
|
|
|
|
borderRadius: '2px',
|
|
|
|
|
|
background: designTokens.colors.device.server,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
RJ45
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
<Option value="SFP">
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '8px',
|
|
|
|
|
|
height: '8px',
|
|
|
|
|
|
borderRadius: '2px',
|
|
|
|
|
|
background: designTokens.colors.device.switch,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
SFP
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
<Option value="SFP+">
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '8px',
|
|
|
|
|
|
height: '8px',
|
|
|
|
|
|
borderRadius: '2px',
|
|
|
|
|
|
background: designTokens.colors.purple.main,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
SFP+
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
<Option value="SFP28">
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '8px',
|
|
|
|
|
|
height: '8px',
|
|
|
|
|
|
borderRadius: '2px',
|
|
|
|
|
|
background: designTokens.colors.info.main,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
SFP28
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
<Option value="QSFP">
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '8px',
|
|
|
|
|
|
height: '8px',
|
|
|
|
|
|
borderRadius: '2px',
|
|
|
|
|
|
background: designTokens.colors.warning.main,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
QSFP
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
<Option value="QSFP28">
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '8px',
|
|
|
|
|
|
height: '8px',
|
|
|
|
|
|
borderRadius: '2px',
|
|
|
|
|
|
background: designTokens.colors.secondary.main,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
QSFP28
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="vlanId"
|
|
|
|
|
|
label={<span style={{ fontSize: '13px', fontWeight: 500 }}>VLAN ID</span>}
|
|
|
|
|
|
>
|
|
|
|
|
|
<InputNumber
|
|
|
|
|
|
placeholder="1-4094"
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
max={4094}
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
style={{ width: '100%', borderRadius: '8px' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{!editingPort && (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: designTokens.colors.success.bg,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.success.light}`,
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
padding: '12px 16px',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '10px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<CheckCircleOutlined
|
|
|
|
|
|
style={{ color: designTokens.colors.success.main, fontSize: '18px' }}
|
|
|
|
|
|
/>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
<div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
color: designTokens.colors.success.dark,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
新建端口默认状态为空闲
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ fontSize: '12px', color: designTokens.colors.success.main }}>
|
|
|
|
|
|
接线后状态将自动更新为占用
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{editingPort && (
|
|
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="status"
|
|
|
|
|
|
label={<span style={{ fontSize: '13px', fontWeight: 500 }}>状态</span>}
|
|
|
|
|
|
rules={[{ required: true, message: '请选择状态' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select size="large" style={{ borderRadius: '8px' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Option value="free">
|
|
|
|
|
|
<Tag color="success">空闲</Tag>
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
<Option value="occupied">
|
|
|
|
|
|
<Tag color="warning">占用</Tag>
|
|
|
|
|
|
</Option>
|
|
|
|
|
|
<Option value="fault">
|
|
|
|
|
|
<Tag color="error">故障</Tag>
|
|
|
|
|
|
</Option>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: designTokens.colors.neutral[50],
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
padding: '20px',
|
2026-03-26 17:05:20 +08:00
|
|
|
|
marginBottom: '16px',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
color: designTokens.colors.neutral[800],
|
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '8px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
<FileTextOutlined style={{ color: designTokens.colors.primary.main }} />
|
|
|
|
|
|
描述信息
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Form.Item name="description" style={{ marginBottom: 0 }}>
|
|
|
|
|
|
<TextArea
|
|
|
|
|
|
rows={3}
|
|
|
|
|
|
placeholder="请输入描述信息(可选)"
|
|
|
|
|
|
style={{ borderRadius: '8px', resize: 'none' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '16px 24px',
|
|
|
|
|
|
background: designTokens.colors.neutral[50],
|
|
|
|
|
|
borderTop: `1px solid ${designTokens.colors.neutral[200]}`,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
borderRadius: '0 0 16px 16px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
<div style={{ fontSize: '12px', color: designTokens.colors.neutral[500] }}>
|
|
|
|
|
|
{editingPort ? '编辑模式下仅修改单个端口' : '支持批量创建端口'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setModalVisible(false);
|
|
|
|
|
|
form.resetFields();
|
|
|
|
|
|
setSelectedDeviceForPort(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{ borderRadius: '8px', minWidth: '80px' }}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
取消
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
onClick={handleSubmit}
|
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: `linear-gradient(135deg, ${designTokens.colors.primary.main} 0%, ${designTokens.colors.primary.dark} 100%)`,
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
boxShadow: `0 4px 12px ${designTokens.colors.primary.main}40`,
|
|
|
|
|
|
minWidth: '120px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{editingPort ? '保存' : '创建'}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</Modal>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-02-12 16:29:19 +08:00
|
|
|
|
{/* 批量导入弹窗 */}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
<Modal
|
2026-02-12 16:29:19 +08:00
|
|
|
|
title={
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '36px',
|
|
|
|
|
|
height: '36px',
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.md,
|
|
|
|
|
|
background: designTokens.colors.info.main,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ImportOutlined />
|
|
|
|
|
|
</div>
|
2026-03-29 10:06:32 +08:00
|
|
|
|
<span style={{ fontSize: '18px', fontWeight: 600 }}>
|
|
|
|
|
|
{importDeviceType === 'server' ? '服务器批量导入' : '交换机批量导入'}
|
|
|
|
|
|
</span>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
open={importModalVisible}
|
2026-03-12 12:40:12 +08:00
|
|
|
|
closeIcon={<CloseButton />}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setImportModalVisible(false);
|
|
|
|
|
|
setImportPreview([]);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
setImportErrors([]);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
setImportProgress({ current: 0, total: 0 });
|
|
|
|
|
|
}}
|
|
|
|
|
|
width={900}
|
|
|
|
|
|
footer={[
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
key="cancel"
|
2026-02-12 16:29:19 +08:00
|
|
|
|
onClick={() => setImportModalVisible(false)}
|
|
|
|
|
|
style={{ borderRadius: designTokens.borderRadius.sm }}
|
|
|
|
|
|
>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
取消
|
|
|
|
|
|
</Button>,
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
key="download"
|
|
|
|
|
|
icon={<DownloadOutlined />}
|
2026-02-12 16:29:19 +08:00
|
|
|
|
onClick={handleDownloadTemplate}
|
|
|
|
|
|
style={{ borderRadius: designTokens.borderRadius.sm }}
|
|
|
|
|
|
>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
下载模板
|
|
|
|
|
|
</Button>,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
key="import"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<ImportOutlined />}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
onClick={handleBatchImport}
|
|
|
|
|
|
loading={importing}
|
|
|
|
|
|
disabled={importPreview.length === 0}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
style={{
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
2026-02-12 16:29:19 +08:00
|
|
|
|
border: 'none',
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.sm,
|
|
|
|
|
|
}}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
>
|
|
|
|
|
|
开始导入
|
2026-02-10 11:04:01 +08:00
|
|
|
|
</Button>,
|
2026-01-23 08:55:16 +08:00
|
|
|
|
]}
|
|
|
|
|
|
>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<div style={{ padding: '16px 0' }}>
|
2026-03-27 18:08:26 +08:00
|
|
|
|
<Alert
|
2026-03-29 10:06:32 +08:00
|
|
|
|
message="导入说明"
|
2026-03-27 18:08:26 +08:00
|
|
|
|
description={
|
|
|
|
|
|
<div style={{ fontSize: '12px', lineHeight: '1.8' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div style={{ marginTop: '8px' }}>
|
|
|
|
|
|
<strong>操作步骤:</strong>
|
2026-03-27 18:08:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
2026-03-29 10:06:32 +08:00
|
|
|
|
<div>1. 点击「下载模板」获取{importDeviceType === 'server' ? '服务器批量导入' : '交换机批量导入'}模板</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div>
|
2026-03-29 10:06:32 +08:00
|
|
|
|
2. 按模板格式填写数据:
|
2026-03-27 19:12:16 +08:00
|
|
|
|
</div>
|
2026-03-29 10:06:32 +08:00
|
|
|
|
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
|
|
|
|
|
{importDeviceType === 'server' ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
- <strong>第一张表格(网卡数据):</strong>填写服务器的网卡信息
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
- <strong>第二张表格(端口数据):</strong>填写网卡对应的端口信息
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
- <strong>端口数据:</strong>填写交换机的端口信息
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
</div>
|
2026-03-29 10:06:32 +08:00
|
|
|
|
<div>3. 点击上传区域选择文件,或直接拖拽文件到上传区域</div>
|
|
|
|
|
|
<div>4. 系统自动校验数据,可预览前10条数据及错误详情</div>
|
|
|
|
|
|
<div>5. 选择导入策略(跳过/更新已存在),点击「开始导入」</div>
|
2026-03-27 18:08:26 +08:00
|
|
|
|
</div>
|
2026-03-29 10:06:32 +08:00
|
|
|
|
{importDeviceType === 'server' && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div style={{ marginTop: '8px' }}>
|
|
|
|
|
|
<strong>网卡数据字段说明:</strong>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>设备SN</strong>(必填):服务器的唯一序列号,如SN-20230001
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>网卡名称</strong>(必填):网卡的名称或标识,如网卡1、eth0
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>插槽编号</strong>(选填):网卡所在的插槽位置,可选择预选项(LOM、OCP、1-4)或填写其他数字
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>网卡型号</strong>(选填):如Intel X710、BCM57414
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>制造商</strong>(选填):如Intel、Mellanox
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>描述</strong>(选填):备注信息
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div style={{ marginTop: '8px' }}>
|
2026-03-29 10:06:32 +08:00
|
|
|
|
<strong>端口数据字段说明:</strong>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
</div>
|
2026-03-27 18:08:26 +08:00
|
|
|
|
<div style={{ paddingLeft: '12px', marginTop: '4px' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div>
|
2026-03-29 10:06:32 +08:00
|
|
|
|
• <strong>设备SN</strong>(必填):{importDeviceType === 'server' ? '服务器' : '交换机'}的唯一序列号,如{importDeviceType === 'server' ? 'SN-20230001' : 'SW-20230001'}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
</div>
|
2026-03-29 10:06:32 +08:00
|
|
|
|
{importDeviceType === 'server' && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>网卡名称</strong>(必填):端口所属的网卡名称,必须与网卡数据中的网卡名称对应
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div>
|
2026-03-29 10:06:32 +08:00
|
|
|
|
• <strong>端口名称</strong>(必填):端口的名称或标识,如{importDeviceType === 'server' ? 'eth0/1' : 'GigabitEthernet0/0/1'}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>端口类型</strong>(选填):如RJ45、LC、SC、FC、SFP+、QSFP28等
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>端口速率</strong>(选填):如1G、10G、25G、100G、40G等
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>状态</strong>(选填):空闲/占用/故障,默认为空闲
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>VLAN ID</strong>(选填):端口所属的VLAN编号,如100
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
• <strong>描述</strong>(选填):备注信息
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ marginTop: '8px', color: '#faad14' }}>
|
|
|
|
|
|
<strong>注意事项:</strong>
|
2026-03-27 18:08:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ paddingLeft: '12px', marginTop: '4px', color: '#faad14' }}>
|
2026-03-29 10:06:32 +08:00
|
|
|
|
{importDeviceType === 'server' && (
|
|
|
|
|
|
<div>• 同一设备下网卡名称不可重复</div>
|
|
|
|
|
|
)}
|
2026-03-27 18:08:26 +08:00
|
|
|
|
<div>• 同一设备下端口名称不可重复</div>
|
|
|
|
|
|
<div>• 批量导入支持最多50000条记录</div>
|
|
|
|
|
|
<div>• 状态字段请使用:空闲/占用/故障,勿使用其他值</div>
|
2026-03-29 10:06:32 +08:00
|
|
|
|
{importDeviceType === 'server' && (
|
|
|
|
|
|
<div>• 端口数据的网卡名称必须与网卡数据中的网卡名称对应</div>
|
|
|
|
|
|
)}
|
2026-03-27 18:08:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.md,
|
|
|
|
|
|
background: designTokens.colors.info.bg,
|
|
|
|
|
|
border: `1px solid ${designTokens.colors.info.light}40`,
|
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<Upload.Dragger
|
|
|
|
|
|
name="file"
|
|
|
|
|
|
accept=".xlsx,.xls,.csv"
|
2026-03-27 17:52:35 +08:00
|
|
|
|
showUploadList={false}
|
|
|
|
|
|
beforeUpload={file => {
|
|
|
|
|
|
handleFileUpload(file, null);
|
|
|
|
|
|
return false;
|
2026-02-12 16:29:19 +08:00
|
|
|
|
}}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.lg,
|
|
|
|
|
|
border: `2px dashed ${designTokens.colors.primary.light}`,
|
|
|
|
|
|
background: designTokens.colors.primary.bg,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<p className="ant-upload-drag-icon">
|
|
|
|
|
|
<UploadIcon style={{ fontSize: '48px', color: designTokens.colors.primary.main }} />
|
|
|
|
|
|
</p>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<p
|
|
|
|
|
|
className="ant-upload-text"
|
|
|
|
|
|
style={{ fontSize: '16px', color: designTokens.colors.neutral[700] }}
|
|
|
|
|
|
>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
点击或拖拽文件到此处上传
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="ant-upload-hint" style={{ color: designTokens.colors.neutral[500] }}>
|
|
|
|
|
|
支持 .xlsx, .xls, .csv 格式文件
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</Upload.Dragger>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
gap: '24px',
|
|
|
|
|
|
marginTop: '16px',
|
|
|
|
|
|
padding: '16px',
|
|
|
|
|
|
background: designTokens.colors.neutral[50],
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.md,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Checkbox checked={skipExisting} onChange={e => setSkipExisting(e.target.checked)}>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
跳过已存在的端口
|
|
|
|
|
|
</Checkbox>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Checkbox checked={updateExisting} onChange={e => setUpdateExisting(e.target.checked)}>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
更新已存在的端口
|
|
|
|
|
|
</Checkbox>
|
|
|
|
|
|
</div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
{importErrors.length > 0 && (
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 10 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ duration: 0.3 }}
|
|
|
|
|
|
style={{ marginTop: '16px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Alert
|
|
|
|
|
|
message={`发现 ${importErrors.length} 个错误`}
|
|
|
|
|
|
description={
|
|
|
|
|
|
<div style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={[
|
2026-03-27 19:12:16 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '行号',
|
|
|
|
|
|
dataIndex: 'row',
|
|
|
|
|
|
key: 'row',
|
|
|
|
|
|
width: 70,
|
|
|
|
|
|
render: row => <Tag color="red">{row}</Tag>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '字段',
|
|
|
|
|
|
dataIndex: 'field',
|
|
|
|
|
|
key: 'field',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: field => <Text strong>{field}</Text>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '错误值',
|
|
|
|
|
|
dataIndex: 'value',
|
|
|
|
|
|
key: 'value',
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
render: val => <Text code>{val}</Text>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '错误原因',
|
|
|
|
|
|
dataIndex: 'error',
|
|
|
|
|
|
key: 'error',
|
|
|
|
|
|
render: err => <Text type="danger">{err}</Text>,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '修正建议',
|
|
|
|
|
|
dataIndex: 'suggestion',
|
|
|
|
|
|
key: 'suggestion',
|
|
|
|
|
|
render: sug => <Text type="secondary">{sug}</Text>,
|
|
|
|
|
|
},
|
2026-03-27 17:52:35 +08:00
|
|
|
|
]}
|
|
|
|
|
|
dataSource={importErrors}
|
|
|
|
|
|
rowKey={(record, index) => `error-${index}`}
|
|
|
|
|
|
pagination={{
|
|
|
|
|
|
pageSize: 5,
|
|
|
|
|
|
size: 'small',
|
|
|
|
|
|
showSizeChanger: false,
|
|
|
|
|
|
showTotal: total => `共 ${total} 条错误`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
scroll={{ x: 600 }}
|
|
|
|
|
|
style={{ marginTop: '8px' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
type="error"
|
|
|
|
|
|
showIcon
|
|
|
|
|
|
style={{ borderRadius: designTokens.borderRadius.md }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
{importPreview.length > 0 && (
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0, y: 10 }}
|
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
|
transition={{ duration: 0.3 }}
|
|
|
|
|
|
style={{ marginTop: '24px' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Alert
|
2026-03-27 17:52:35 +08:00
|
|
|
|
message={
|
|
|
|
|
|
<span>
|
|
|
|
|
|
成功解析 {importPreview.length} 条有效数据
|
2026-03-27 19:12:16 +08:00
|
|
|
|
{importErrors.length > 0 && (
|
|
|
|
|
|
<span style={{ color: '#ff4d4f', marginLeft: 8 }}>
|
|
|
|
|
|
({importErrors.length} 条错误)
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
2026-03-27 17:52:35 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
type={importErrors.length > 0 ? 'warning' : 'success'}
|
2026-02-12 16:29:19 +08:00
|
|
|
|
showIcon
|
|
|
|
|
|
style={{ marginBottom: '16px', borderRadius: designTokens.borderRadius.md }}
|
|
|
|
|
|
/>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginBottom: '8px',
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
color: designTokens.colors.neutral[700],
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
数据预览(前10条)
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</div>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<Table
|
|
|
|
|
|
columns={[
|
2026-03-29 10:06:32 +08:00
|
|
|
|
{ title: '设备SN', dataIndex: '设备SN', key: 'deviceSn', width: 120 },
|
2026-02-12 16:29:19 +08:00
|
|
|
|
{ title: '端口名称', dataIndex: '端口名称', key: 'portName', width: 120 },
|
2026-03-27 19:12:16 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '端口类型',
|
|
|
|
|
|
dataIndex: '端口类型',
|
|
|
|
|
|
key: 'portType',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: type => getPortTypeTag(type),
|
|
|
|
|
|
},
|
2026-02-12 16:29:19 +08:00
|
|
|
|
{ title: '端口速率', dataIndex: '端口速率', key: 'portSpeed', width: 100 },
|
2026-03-27 19:12:16 +08:00
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
dataIndex: '状态',
|
|
|
|
|
|
key: 'status',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: status => getStatusTag(status),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: 'VLAN ID',
|
|
|
|
|
|
dataIndex: 'VLAN ID',
|
|
|
|
|
|
key: 'vlanId',
|
|
|
|
|
|
width: 100,
|
|
|
|
|
|
render: vlanId => vlanId || '-',
|
|
|
|
|
|
},
|
2026-02-12 16:29:19 +08:00
|
|
|
|
{ title: '描述', dataIndex: '描述', key: 'description', ellipsis: true },
|
|
|
|
|
|
]}
|
|
|
|
|
|
dataSource={importPreview.slice(0, 10)}
|
2026-03-26 14:35:08 +08:00
|
|
|
|
rowKey={(record, index) => `import-row-${index}`}
|
2026-02-12 16:29:19 +08:00
|
|
|
|
pagination={false}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
scroll={{ x: 800 }}
|
|
|
|
|
|
style={{ borderRadius: designTokens.borderRadius.md }}
|
|
|
|
|
|
/>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
{importPreview.length > 10 && (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
marginTop: '12px',
|
|
|
|
|
|
color: designTokens.colors.neutral[500],
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
仅显示前10条数据,共 {importPreview.length} 条
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-02-12 16:29:19 +08:00
|
|
|
|
</motion.div>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
)}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
{importing && (
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
|
animate={{ opacity: 1 }}
|
|
|
|
|
|
style={{ textAlign: 'center', padding: '40px 24px' }}
|
|
|
|
|
|
>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
<Spin size="large" tip="导入中..." />
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<div style={{ marginTop: '24px' }}>
|
2026-01-30 17:53:05 +08:00
|
|
|
|
<Progress
|
|
|
|
|
|
percent={Math.round((importProgress.current / importProgress.total) * 100)}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
status="active"
|
|
|
|
|
|
strokeColor={{
|
|
|
|
|
|
'0%': designTokens.colors.primary.main,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
'100%': designTokens.colors.success.main,
|
2026-01-23 08:55:16 +08:00
|
|
|
|
}}
|
2026-02-12 16:29:19 +08:00
|
|
|
|
style={{ borderRadius: designTokens.borderRadius.sm }}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
/>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<div style={{ marginTop: '16px', color: designTokens.colors.neutral[600] }}>
|
|
|
|
|
|
正在导入 {importProgress.current} / {importProgress.total} 条数据...
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
</motion.div>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Modal>
|
2026-01-30 17:53:05 +08:00
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
{/* 设备选择弹窗 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title={
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '4px 0' }}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '40px',
|
|
|
|
|
|
height: '40px',
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ApiOutlined style={{ fontSize: '20px' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ fontSize: '18px', fontWeight: 600, color: '#1a1a2e', lineHeight: 1.3 }}>
|
|
|
|
|
|
选择设备
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ fontSize: '12px', color: '#666', marginTop: '2px' }}>
|
|
|
|
|
|
为端口选择所属设备
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
open={selectDeviceModalVisible}
|
|
|
|
|
|
closeIcon={<CloseButton />}
|
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setSelectDeviceModalVisible(false);
|
|
|
|
|
|
setDeviceFilterType('all');
|
|
|
|
|
|
setDevicePage(1);
|
|
|
|
|
|
setSelectedRowKeys([]);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
setGuidedDeviceType(null);
|
|
|
|
|
|
setSelectedRoomId(null);
|
|
|
|
|
|
setSelectedRackId(null);
|
2026-03-26 14:35:08 +08:00
|
|
|
|
}}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={620}
|
|
|
|
|
|
style={{ top: 100 }}
|
|
|
|
|
|
bodyStyle={{ padding: '0 24px 24px' }}
|
|
|
|
|
|
>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
{guidedDeviceType && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
margin: '0 -24px 16px',
|
|
|
|
|
|
padding: '12px 24px',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
background:
|
|
|
|
|
|
guidedDeviceType === 'switch'
|
|
|
|
|
|
? 'linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 100%)'
|
|
|
|
|
|
: 'linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%)',
|
2026-03-27 17:52:35 +08:00
|
|
|
|
borderBottom: `1px solid ${guidedDeviceType === 'switch' ? '#bbf7d0' : '#c7d2fe'}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 14:35:08 +08:00
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
{guidedDeviceType === 'switch' ? (
|
|
|
|
|
|
<ApiOutlined style={{ fontSize: '16px', color: '#059669' }} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<CloudServerOutlined style={{ fontSize: '16px', color: '#6366f1' }} />
|
|
|
|
|
|
)}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
color: guidedDeviceType === 'switch' ? '#047857' : '#4f46e5',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
{guidedDeviceType === 'switch' ? '添加交换机端口' : '添加服务器端口'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span style={{ fontSize: '12px', color: '#666' }}>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
{guidedDeviceType === 'switch'
|
|
|
|
|
|
? '(交换机端口可直接创建,无需关联网卡)'
|
|
|
|
|
|
: '(服务器端口需关联网卡)'}
|
2026-03-27 17:52:35 +08:00
|
|
|
|
</span>
|
2026-03-26 14:35:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
)}
|
2026-03-26 14:35:08 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
<div style={{ display: 'flex', gap: '12px', marginBottom: '16px', alignItems: 'center' }}>
|
2026-03-26 14:35:08 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="搜索设备名称、IP或位置..."
|
|
|
|
|
|
prefix={<SearchOutlined style={{ color: '#999' }} />}
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
onChange={e => handleDeviceSearch(e.target.value)}
|
|
|
|
|
|
style={{
|
2026-03-27 17:52:35 +08:00
|
|
|
|
flex: 1,
|
2026-03-26 14:35:08 +08:00
|
|
|
|
borderRadius: '10px',
|
|
|
|
|
|
border: '1px solid #e8ecf4',
|
2026-03-27 17:52:35 +08:00
|
|
|
|
height: '40px',
|
2026-03-26 14:35:08 +08:00
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
<Select
|
|
|
|
|
|
placeholder="按机房筛选"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
value={selectedRoomId}
|
|
|
|
|
|
onChange={value => {
|
|
|
|
|
|
setSelectedRoomId(value);
|
|
|
|
|
|
setSelectedRackId(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{ width: 140 }}
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
>
|
|
|
|
|
|
{roomList.map(room => (
|
|
|
|
|
|
<Select.Option key={room.roomId} value={room.roomId}>
|
|
|
|
|
|
{room.name}
|
|
|
|
|
|
</Select.Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
placeholder="按机柜筛选"
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
value={selectedRackId}
|
|
|
|
|
|
onChange={value => setSelectedRackId(value)}
|
|
|
|
|
|
style={{ width: 140 }}
|
|
|
|
|
|
size="middle"
|
|
|
|
|
|
disabled={!selectedRoomId}
|
|
|
|
|
|
>
|
|
|
|
|
|
{rackList
|
|
|
|
|
|
.filter(rack => !selectedRoomId || rack.roomId === selectedRoomId)
|
|
|
|
|
|
.map(rack => (
|
|
|
|
|
|
<Select.Option key={rack.rackId} value={rack.rackId}>
|
|
|
|
|
|
{rack.name}
|
|
|
|
|
|
</Select.Option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
2026-03-26 14:35:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{(() => {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const rackRoomMap = {};
|
|
|
|
|
|
rackList.forEach(rack => {
|
|
|
|
|
|
rackRoomMap[rack.rackId] = rack.roomId;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-26 14:35:08 +08:00
|
|
|
|
const allDevices = devices.filter(d => {
|
|
|
|
|
|
const type = getDeviceType(d);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
if (type !== 'switch' && type !== 'server') return false;
|
|
|
|
|
|
if (selectedRackId && d.rackId !== selectedRackId) return false;
|
2026-03-27 19:12:16 +08:00
|
|
|
|
if (selectedRoomId && d.rackId && rackRoomMap[d.rackId] !== selectedRoomId)
|
|
|
|
|
|
return false;
|
2026-03-27 17:52:35 +08:00
|
|
|
|
return true;
|
2026-03-26 14:35:08 +08:00
|
|
|
|
});
|
|
|
|
|
|
const switchCount = allDevices.filter(d => getDeviceType(d) === 'switch').length;
|
|
|
|
|
|
const serverCount = allDevices.filter(d => getDeviceType(d) === 'server').length;
|
2026-03-27 17:52:35 +08:00
|
|
|
|
const showSwitchTab = guidedDeviceType === null || guidedDeviceType === 'switch';
|
|
|
|
|
|
const showServerTab = guidedDeviceType === null || guidedDeviceType === 'server';
|
2026-03-26 14:35:08 +08:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
gap: '8px',
|
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
|
background: '#f5f7fa',
|
|
|
|
|
|
padding: '4px',
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
{guidedDeviceType === null && (
|
|
|
|
|
|
<button
|
2026-03-27 19:12:16 +08:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setDeviceFilterType('all');
|
|
|
|
|
|
setDevicePage(1);
|
|
|
|
|
|
}}
|
2026-03-27 17:52:35 +08:00
|
|
|
|
style={{
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
padding: '10px 16px',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
transition: 'all 0.2s',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
background:
|
|
|
|
|
|
deviceFilterType === 'all'
|
|
|
|
|
|
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
|
|
|
|
|
: 'transparent',
|
2026-03-27 17:52:35 +08:00
|
|
|
|
color: deviceFilterType === 'all' ? '#fff' : '#666',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
boxShadow:
|
|
|
|
|
|
deviceFilterType === 'all' ? '0 2px 8px rgba(102, 126, 234, 0.3)' : 'none',
|
2026-03-27 17:52:35 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
全部 ({allDevices.length})
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{showSwitchTab && (
|
|
|
|
|
|
<button
|
2026-03-27 19:12:16 +08:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setDeviceFilterType('switch');
|
|
|
|
|
|
setDevicePage(1);
|
|
|
|
|
|
}}
|
2026-03-27 17:52:35 +08:00
|
|
|
|
style={{
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
padding: '10px 16px',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
transition: 'all 0.2s',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
background:
|
|
|
|
|
|
deviceFilterType === 'switch'
|
|
|
|
|
|
? 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)'
|
|
|
|
|
|
: 'transparent',
|
2026-03-27 17:52:35 +08:00
|
|
|
|
color: deviceFilterType === 'switch' ? '#fff' : '#666',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
boxShadow:
|
|
|
|
|
|
deviceFilterType === 'switch' ? '0 2px 8px rgba(17, 153, 142, 0.3)' : 'none',
|
2026-03-27 17:52:35 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
交换机 ({switchCount})
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{showServerTab && (
|
|
|
|
|
|
<button
|
2026-03-27 19:12:16 +08:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setDeviceFilterType('server');
|
|
|
|
|
|
setDevicePage(1);
|
|
|
|
|
|
}}
|
2026-03-27 17:52:35 +08:00
|
|
|
|
style={{
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
padding: '10px 16px',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
transition: 'all 0.2s',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
background:
|
|
|
|
|
|
deviceFilterType === 'server'
|
|
|
|
|
|
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
|
|
|
|
|
: 'transparent',
|
2026-03-27 17:52:35 +08:00
|
|
|
|
color: deviceFilterType === 'server' ? '#fff' : '#666',
|
2026-03-27 19:12:16 +08:00
|
|
|
|
boxShadow:
|
|
|
|
|
|
deviceFilterType === 'server' ? '0 2px 8px rgba(102, 126, 234, 0.3)' : 'none',
|
2026-03-27 17:52:35 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
服务器 ({serverCount})
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
2026-03-26 14:35:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})()}
|
|
|
|
|
|
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{ maxHeight: '480px', overflowY: 'auto', margin: '0 -24px', padding: '0 16px' }}
|
|
|
|
|
|
>
|
2026-03-26 14:35:08 +08:00
|
|
|
|
{deviceSearching ? (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '60px 0' }}>
|
|
|
|
|
|
<Spin size="large" />
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div style={{ marginTop: '16px', color: '#999', fontSize: '13px' }}>
|
|
|
|
|
|
加载设备中...
|
|
|
|
|
|
</div>
|
2026-03-26 14:35:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
) : paginatedDevices.list.length === 0 ? (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '60px 0' }}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '80px',
|
|
|
|
|
|
height: '80px',
|
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
|
background: 'linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
margin: '0 auto 16px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SearchOutlined style={{ fontSize: '32px', color: '#ccc' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ fontSize: '15px', color: '#666', marginBottom: '4px' }}>未找到设备</div>
|
|
|
|
|
|
<div style={{ fontSize: '13px', color: '#999' }}>
|
|
|
|
|
|
{deviceFilterType === 'all'
|
|
|
|
|
|
? '暂无可添加端口的设备'
|
|
|
|
|
|
: deviceFilterType === 'switch'
|
2026-03-27 19:12:16 +08:00
|
|
|
|
? '暂无可添加端口的交换机'
|
|
|
|
|
|
: '暂无可添加端口的服务器'}
|
2026-03-26 14:35:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
|
color: '#999',
|
|
|
|
|
|
marginBottom: '12px',
|
|
|
|
|
|
paddingLeft: '4px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-26 14:35:08 +08:00
|
|
|
|
共 {paginatedDevices.total} 个设备
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{paginatedDevices.list.map(device => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={device.deviceId}
|
|
|
|
|
|
onClick={() => handleSelectDeviceForPort(device)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '14px 16px',
|
|
|
|
|
|
marginBottom: '10px',
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
border: '1px solid #e8ecf4',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
transition: 'all 0.2s',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseEnter={e => {
|
|
|
|
|
|
e.currentTarget.style.borderColor = '#667eea';
|
|
|
|
|
|
e.currentTarget.style.background = '#f8f9ff';
|
|
|
|
|
|
e.currentTarget.style.transform = 'translateY(-1px)';
|
|
|
|
|
|
e.currentTarget.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.15)';
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseLeave={e => {
|
|
|
|
|
|
e.currentTarget.style.borderColor = '#e8ecf4';
|
|
|
|
|
|
e.currentTarget.style.background = '#fff';
|
|
|
|
|
|
e.currentTarget.style.transform = 'none';
|
|
|
|
|
|
e.currentTarget.style.boxShadow = 'none';
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '14px' }}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '48px',
|
|
|
|
|
|
height: '48px',
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
background: isServerDevice(device)
|
|
|
|
|
|
? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
|
|
|
|
|
: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
fontSize: '22px',
|
|
|
|
|
|
boxShadow: isServerDevice(device)
|
|
|
|
|
|
? '0 4px 12px rgba(102, 126, 234, 0.3)'
|
|
|
|
|
|
: '0 4px 12px rgba(17, 153, 142, 0.3)',
|
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{getDeviceIcon(device)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
fontSize: '15px',
|
|
|
|
|
|
color: '#1a1a2e',
|
|
|
|
|
|
marginBottom: '4px',
|
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{device.name || '未命名设备'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
|
color: '#999',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
gap: '12px',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{device.Rack?.Room?.name && (
|
|
|
|
|
|
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
|
|
|
|
<span>📍</span>
|
|
|
|
|
|
{device.Rack.Room.name}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{device.Rack?.name && (
|
|
|
|
|
|
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
|
|
|
|
<span>🗄️</span>
|
|
|
|
|
|
{device.Rack.name}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{device.ipAddress && (
|
|
|
|
|
|
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
|
|
|
|
<span>🌐</span>
|
|
|
|
|
|
{device.ipAddress}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '4px 10px',
|
|
|
|
|
|
borderRadius: '6px',
|
|
|
|
|
|
fontSize: '11px',
|
|
|
|
|
|
fontWeight: 500,
|
|
|
|
|
|
background: isServerDevice(device)
|
|
|
|
|
|
? 'rgba(102, 126, 234, 0.1)'
|
|
|
|
|
|
: 'rgba(17, 153, 142, 0.1)',
|
|
|
|
|
|
color: isServerDevice(device) ? '#667eea' : '#11998e',
|
|
|
|
|
|
flexShrink: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isServerDevice(device) ? '服务器' : '交换机'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
|
|
|
|
|
|
{paginatedDevices.hasMore && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref={loadMoreRef}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
padding: '20px 0',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
}}
|
|
|
|
|
|
onClick={() => setDevicePage(p => p + 1)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'inline-flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
gap: '8px',
|
|
|
|
|
|
padding: '10px 20px',
|
|
|
|
|
|
borderRadius: '20px',
|
|
|
|
|
|
background: '#f5f7fa',
|
|
|
|
|
|
color: '#666',
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
transition: 'all 0.2s',
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseEnter={e => {
|
|
|
|
|
|
e.currentTarget.style.background = '#667eea';
|
|
|
|
|
|
e.currentTarget.style.color = '#fff';
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseLeave={e => {
|
|
|
|
|
|
e.currentTarget.style.background = '#f5f7fa';
|
|
|
|
|
|
e.currentTarget.style.color = '#666';
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>点击加载更多</span>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<span style={{ fontSize: '11px' }}>
|
|
|
|
|
|
({Math.min(devicePage * 100, paginatedDevices.total)} /{' '}
|
|
|
|
|
|
{paginatedDevices.total})
|
|
|
|
|
|
</span>
|
2026-03-26 14:35:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{!paginatedDevices.hasMore && paginatedDevices.total > 0 && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
padding: '20px 0',
|
|
|
|
|
|
color: '#999',
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{ marginBottom: '4px', color: '#667eea', fontWeight: 500 }}>
|
|
|
|
|
|
已加载全部
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>共 {paginatedDevices.total} 个设备</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
2026-01-30 17:53:05 +08:00
|
|
|
|
{/* 网卡管理模态框 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title={
|
2026-02-12 16:29:19 +08:00
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '36px',
|
|
|
|
|
|
height: '36px',
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.md,
|
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<CloudServerOutlined />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span style={{ fontSize: '18px', fontWeight: 600 }}>
|
|
|
|
|
|
网卡管理 - {selectedDeviceForNic?.name}
|
|
|
|
|
|
</span>
|
2026-01-30 17:53:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
open={networkCardModalVisible}
|
2026-03-12 12:40:12 +08:00
|
|
|
|
closeIcon={<CloseButton />}
|
2026-01-30 17:53:05 +08:00
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setNetworkCardModalVisible(false);
|
|
|
|
|
|
setSelectedDeviceForNic(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
footer={null}
|
|
|
|
|
|
width={800}
|
|
|
|
|
|
destroyOnClose
|
|
|
|
|
|
>
|
|
|
|
|
|
{selectedDeviceForNic && (
|
|
|
|
|
|
<NetworkCardPanel
|
|
|
|
|
|
deviceId={selectedDeviceForNic.deviceId}
|
|
|
|
|
|
deviceName={selectedDeviceForNic.name}
|
|
|
|
|
|
onRefresh={fetchPorts}
|
|
|
|
|
|
refreshTrigger={refreshTrigger}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 创建网卡模态框 */}
|
|
|
|
|
|
<NetworkCardCreateModal
|
|
|
|
|
|
device={selectedDeviceForNic}
|
|
|
|
|
|
visible={portCreateModalVisible}
|
|
|
|
|
|
onClose={() => {
|
|
|
|
|
|
setPortCreateModalVisible(false);
|
|
|
|
|
|
setSelectedDeviceForNic(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
onSuccess={handleNicSuccess}
|
|
|
|
|
|
/>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 服务器网卡列表弹窗 */}
|
|
|
|
|
|
<Modal
|
|
|
|
|
|
title={
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '36px',
|
|
|
|
|
|
height: '36px',
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.md,
|
2026-03-27 17:52:35 +08:00
|
|
|
|
background: designTokens.colors.primary.gradient,
|
2026-03-26 17:05:20 +08:00
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<CloudServerOutlined />
|
|
|
|
|
|
</div>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<span style={{ fontSize: '18px', fontWeight: 600 }}>服务器网卡管理</span>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
open={serverNicListVisible}
|
|
|
|
|
|
closeIcon={<CloseButton />}
|
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
setServerNicListVisible(false);
|
|
|
|
|
|
setServerNicList([]);
|
2026-03-27 17:52:35 +08:00
|
|
|
|
setServerNicSearchText('');
|
|
|
|
|
|
setServerNicRoomFilter('all');
|
|
|
|
|
|
setServerNicRackFilter('all');
|
|
|
|
|
|
setServerNicCardPage(1);
|
2026-03-26 17:05:20 +08:00
|
|
|
|
}}
|
|
|
|
|
|
footer={null}
|
2026-03-27 17:52:35 +08:00
|
|
|
|
width={900}
|
2026-03-26 17:05:20 +08:00
|
|
|
|
destroyOnClose
|
|
|
|
|
|
>
|
|
|
|
|
|
{serverNicLoading ? (
|
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '60px 0' }}>
|
|
|
|
|
|
<Spin size="large" />
|
|
|
|
|
|
<div style={{ marginTop: '16px', color: '#999' }}>加载中...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
2026-03-27 17:52:35 +08:00
|
|
|
|
<>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
gap: '12px',
|
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="搜索服务器名称或ID..."
|
|
|
|
|
|
prefix={<SearchOutlined style={{ color: designTokens.colors.primary.main }} />}
|
|
|
|
|
|
value={serverNicSearchText}
|
|
|
|
|
|
onChange={e => {
|
|
|
|
|
|
setServerNicSearchText(e.target.value);
|
|
|
|
|
|
setServerNicCardPage(1);
|
2026-03-26 17:05:20 +08:00
|
|
|
|
}}
|
2026-03-27 17:52:35 +08:00
|
|
|
|
allowClear
|
|
|
|
|
|
style={{
|
|
|
|
|
|
width: '260px',
|
|
|
|
|
|
borderRadius: designTokens.borderRadius.md,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={serverNicRoomFilter}
|
|
|
|
|
|
onChange={value => {
|
|
|
|
|
|
setServerNicRoomFilter(value);
|
|
|
|
|
|
setServerNicRackFilter('all');
|
|
|
|
|
|
setServerNicCardPage(1);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{ width: '140px' }}
|
|
|
|
|
|
placeholder="选择机房"
|
2026-03-26 17:05:20 +08:00
|
|
|
|
>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
<Option value="all">全部机房</Option>
|
|
|
|
|
|
{roomList.map(room => (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Option key={room.roomId} value={room.roomId}>
|
|
|
|
|
|
{room.name}
|
|
|
|
|
|
</Option>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={serverNicRackFilter}
|
|
|
|
|
|
onChange={value => {
|
|
|
|
|
|
setServerNicRackFilter(value);
|
|
|
|
|
|
setServerNicCardPage(1);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{ width: '140px' }}
|
|
|
|
|
|
placeholder="选择机柜"
|
|
|
|
|
|
disabled={serverNicRoomFilter === 'all'}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Option value="all">全部机柜</Option>
|
|
|
|
|
|
{rackList
|
2026-03-27 19:12:16 +08:00
|
|
|
|
.filter(
|
|
|
|
|
|
rack => serverNicRoomFilter === 'all' || rack.roomId === serverNicRoomFilter
|
|
|
|
|
|
)
|
2026-03-27 17:52:35 +08:00
|
|
|
|
.map(rack => (
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<Option key={rack.rackId} value={rack.rackId}>
|
|
|
|
|
|
{rack.name}
|
|
|
|
|
|
</Option>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
))}
|
|
|
|
|
|
</Select>
|
2026-03-27 19:12:16 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginLeft: 'auto',
|
|
|
|
|
|
color: designTokens.colors.text.secondary,
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
alignSelf: 'center',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
共 {serverNicList.length} 台设备
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
|
2026-03-27 17:52:35 +08:00
|
|
|
|
{serverNicList.length === 0 ? (
|
|
|
|
|
|
<Empty description="暂无服务器数据" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div
|
2026-03-26 17:05:20 +08:00
|
|
|
|
style={{
|
2026-03-27 17:52:35 +08:00
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
|
|
|
|
|
gap: '16px',
|
|
|
|
|
|
maxHeight: '520px',
|
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
|
padding: '4px',
|
2026-03-26 17:05:20 +08:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
{serverNicList
|
|
|
|
|
|
.filter(server => {
|
|
|
|
|
|
const searchMatch =
|
|
|
|
|
|
!serverNicSearchText ||
|
|
|
|
|
|
server.name?.toLowerCase().includes(serverNicSearchText.toLowerCase()) ||
|
|
|
|
|
|
server.deviceId?.toLowerCase().includes(serverNicSearchText.toLowerCase());
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const roomMatch =
|
|
|
|
|
|
serverNicRoomFilter === 'all' || server.roomId === serverNicRoomFilter;
|
|
|
|
|
|
const rackMatch =
|
|
|
|
|
|
serverNicRackFilter === 'all' || server.rackId === serverNicRackFilter;
|
2026-03-27 17:52:35 +08:00
|
|
|
|
return searchMatch && roomMatch && rackMatch;
|
|
|
|
|
|
})
|
|
|
|
|
|
.slice(
|
|
|
|
|
|
(serverNicCardPage - 1) * serverNicCardPageSize,
|
|
|
|
|
|
serverNicCardPage * serverNicCardPageSize
|
|
|
|
|
|
)
|
|
|
|
|
|
.map(server => (
|
|
|
|
|
|
<ServerNicCard
|
|
|
|
|
|
key={server.deviceId}
|
|
|
|
|
|
server={server}
|
|
|
|
|
|
onManage={() => {
|
|
|
|
|
|
setSelectedDeviceForNic(server);
|
|
|
|
|
|
setServerNicListVisible(false);
|
|
|
|
|
|
setNetworkCardModalVisible(true);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{serverNicList.filter(server => {
|
|
|
|
|
|
const searchMatch =
|
|
|
|
|
|
!serverNicSearchText ||
|
|
|
|
|
|
server.name?.toLowerCase().includes(serverNicSearchText.toLowerCase()) ||
|
|
|
|
|
|
server.deviceId?.toLowerCase().includes(serverNicSearchText.toLowerCase());
|
2026-03-27 19:12:16 +08:00
|
|
|
|
const roomMatch =
|
|
|
|
|
|
serverNicRoomFilter === 'all' || server.roomId === serverNicRoomFilter;
|
|
|
|
|
|
const rackMatch =
|
|
|
|
|
|
serverNicRackFilter === 'all' || server.rackId === serverNicRackFilter;
|
2026-03-27 17:52:35 +08:00
|
|
|
|
return searchMatch && roomMatch && rackMatch;
|
|
|
|
|
|
}).length > serverNicCardPageSize && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
marginTop: '16px',
|
|
|
|
|
|
paddingTop: '12px',
|
|
|
|
|
|
borderTop: `1px solid ${designTokens.colors.border.light}`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Pagination
|
|
|
|
|
|
current={serverNicCardPage}
|
|
|
|
|
|
pageSize={serverNicCardPageSize}
|
2026-03-27 19:12:16 +08:00
|
|
|
|
total={
|
|
|
|
|
|
serverNicList.filter(server => {
|
|
|
|
|
|
const searchMatch =
|
|
|
|
|
|
!serverNicSearchText ||
|
|
|
|
|
|
server.name
|
|
|
|
|
|
?.toLowerCase()
|
|
|
|
|
|
.includes(serverNicSearchText.toLowerCase()) ||
|
|
|
|
|
|
server.deviceId
|
|
|
|
|
|
?.toLowerCase()
|
|
|
|
|
|
.includes(serverNicSearchText.toLowerCase());
|
|
|
|
|
|
const typeMatch =
|
|
|
|
|
|
serverNicTypeFilter === 'all' || server.type === serverNicTypeFilter;
|
|
|
|
|
|
return searchMatch && typeMatch;
|
|
|
|
|
|
}).length
|
|
|
|
|
|
}
|
2026-03-27 17:52:35 +08:00
|
|
|
|
onChange={page => setServerNicCardPage(page)}
|
|
|
|
|
|
showSizeChanger={false}
|
|
|
|
|
|
showQuickJumper
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
2026-03-26 17:05:20 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</Modal>
|
2026-03-27 17:52:35 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 批量导入选择弹窗 */}
|
|
|
|
|
|
<BatchImportModal
|
|
|
|
|
|
visible={batchImportModalVisible}
|
|
|
|
|
|
onClose={() => setBatchImportModalVisible(false)}
|
2026-03-29 10:06:32 +08:00
|
|
|
|
onImportNetworkCard={() => {
|
|
|
|
|
|
setImportDeviceType('server');
|
|
|
|
|
|
handleImport();
|
|
|
|
|
|
}}
|
2026-03-27 17:52:35 +08:00
|
|
|
|
onImportPort={() => {
|
2026-03-29 10:06:32 +08:00
|
|
|
|
setImportDeviceType('switch');
|
2026-03-27 17:52:35 +08:00
|
|
|
|
handleImport();
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 网卡批量导入弹窗 */}
|
|
|
|
|
|
<NetworkCardImportModal
|
|
|
|
|
|
visible={networkCardImportModalVisible}
|
|
|
|
|
|
onClose={() => setNetworkCardImportModalVisible(false)}
|
2026-03-29 10:06:32 +08:00
|
|
|
|
onSuccess={async () => {
|
2026-03-27 17:52:35 +08:00
|
|
|
|
message.success({
|
|
|
|
|
|
content: '网卡导入成功',
|
|
|
|
|
|
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
|
|
|
|
|
});
|
|
|
|
|
|
setRefreshTrigger(prev => prev + 1);
|
2026-03-29 10:06:32 +08:00
|
|
|
|
// 重新获取设备列表,确保包含最新数据
|
|
|
|
|
|
await fetchDevices();
|
|
|
|
|
|
// 服务器设备导入网卡后自动进入端口导入
|
|
|
|
|
|
if (importDeviceType === 'server') {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setNetworkCardImportModalVisible(false);
|
|
|
|
|
|
handleImport();
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
}
|
2026-03-27 17:52:35 +08:00
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 端口新增引导弹窗 */}
|
|
|
|
|
|
<PortAddGuideModal
|
|
|
|
|
|
visible={portAddGuideModalVisible}
|
|
|
|
|
|
onClose={() => setPortAddGuideModalVisible(false)}
|
|
|
|
|
|
onSelectType={handleGuideSelectType}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 端口导出弹窗 */}
|
|
|
|
|
|
<PortExportModal
|
|
|
|
|
|
visible={portExportModalVisible}
|
|
|
|
|
|
filters={filters}
|
|
|
|
|
|
totalCount={portTotal}
|
|
|
|
|
|
currentPageCount={ports.length}
|
|
|
|
|
|
selectedCount={selectedRowKeys.length}
|
|
|
|
|
|
onExport={handleExport}
|
|
|
|
|
|
onCancel={() => setPortExportModalVisible(false)}
|
|
|
|
|
|
/>
|
2026-02-12 16:29:19 +08:00
|
|
|
|
</motion.div>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default PortManagement;
|