import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Table, Button, Modal, Form, Input, Select, message, Card, Space, Popconfirm, Tag, Tooltip, InputNumber, Collapse, Empty, Spin, Upload, Progress, Checkbox, Badge, Row, Col, Skeleton, Alert, Typography, Divider, Pagination, Segmented, } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, ExportOutlined, ImportOutlined, DownloadOutlined, UploadOutlined as UploadIcon, AppstoreOutlined, FilterOutlined, ClearOutlined, CloudServerOutlined, ApiOutlined, CheckCircleOutlined, ExclamationCircleOutlined, DisconnectOutlined, UpOutlined, DownOutlined, TagOutlined, ThunderboltOutlined, FileTextOutlined, InfoCircleOutlined, } from '@ant-design/icons'; import api from '../api'; import { roomAPI, rackAPI } from '../api/cache'; import * as XLSX from 'xlsx'; import Papa from 'papaparse'; import { motion, AnimatePresence } from 'framer-motion'; import NetworkCardPanel from '../components/NetworkCardPanel'; import NetworkCardCreateModal from '../components/NetworkCardCreateModal'; 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'; import { designTokens } from '../config/theme'; import CloseButton from '../components/CloseButton'; import { debounce } from '../utils/common'; const { Option } = Select; const { Panel } = Collapse; const { Text, Title } = Typography; const { TextArea } = Input; // 动画配置 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], }, }, }, }; /** * 按端口名称中的数字段进行自然排序 * @param {Array} ports - 端口数组 * @returns {Array} 排序后的新数组 */ function sortPortsByName(ports) { const extractNumbers = str => { const matches = str ? String(str).match(/\d+/g) : null; return matches ? matches.map(Number) : []; }; return [...ports].sort((a, b) => { 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 String(a.portName).localeCompare(String(b.portName)); }); } function PortManagement() { const [ports, setPorts] = useState([]); const [devices, setDevices] = useState([]); const [deviceSearching, setDeviceSearching] = useState(false); // 按设备分组的端口列表(数组,每项 { device, ports }) 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(); const [importModalVisible, setImportModalVisible] = useState(false); const [importPreview, setImportPreview] = useState([]); const [importErrors, setImportErrors] = useState([]); const [importProgress, setImportProgress] = useState({ current: 0, total: 0 }); const [importing, setImporting] = useState(false); const [skipExisting, setSkipExisting] = useState(false); const [updateExisting, setUpdateExisting] = useState(false); // 视图模式:list 或 panel // const [viewMode, setViewMode] = useState('list'); // 网卡管理相关状态 const [networkCardModalVisible, setNetworkCardModalVisible] = useState(false); const [serverNicListVisible, setServerNicListVisible] = useState(false); const [serverNicList, setServerNicList] = useState([]); const [serverNicLoading, setServerNicLoading] = useState(false); const [portCreateModalVisible, setPortCreateModalVisible] = useState(false); const [selectedDeviceForNic, setSelectedDeviceForNic] = useState(null); const [refreshTrigger, setRefreshTrigger] = useState(0); const [networkCardImportModalVisible, setNetworkCardImportModalVisible] = useState(false); const [batchImportModalVisible, setBatchImportModalVisible] = useState(false); const [portAddGuideModalVisible, setPortAddGuideModalVisible] = useState(false); const [portExportModalVisible, setPortExportModalVisible] = useState(false); const [importDeviceType, setImportDeviceType] = useState(null); // 展开的设备 const [expandedKeys, setExpandedKeys] = useState([]); const [selectDeviceModalVisible, setSelectDeviceModalVisible] = useState(false); const [selectedDeviceForPort, setSelectedDeviceForPort] = useState(null); const [nicList, setNicList] = useState([]); const [portMode, setPortMode] = useState('quick'); // 'range' | 'quick' const [parseError, setParseError] = useState(null); const [parseResult, setParseResult] = useState(null); const [previewPorts, setPreviewPorts] = useState([]); const [showPreview, setShowPreview] = useState(false); // 批量选择相关状态 const [selectedRowKeys, setSelectedRowKeys] = useState([]); // 设备选择弹窗 Tab 状态 const [deviceFilterType, setDeviceFilterType] = useState('all'); const [guidedDeviceType, setGuidedDeviceType] = useState(null); const [devicePage, setDevicePage] = useState(1); // 机房机柜筛选状态 const [roomList, setRoomList] = useState([]); const [rackList, setRackList] = useState([]); const [selectedRoomId, setSelectedRoomId] = useState(null); const [selectedRackId, setSelectedRackId] = useState(null); const loadMoreRef = useRef(null); const hasMoreRef = useRef(true); const isLoadingRef = useRef(false); // 设备卡片分页状态(按设备维度分页) const [deviceCardPage, setDeviceCardPage] = useState(1); const deviceCardPageSize = 10; const [deviceCardTotal, setDeviceCardTotal] = useState(0); // 端口总数(所有设备端口总和,用于统计与导出弹窗) const [portTotal, setPortTotal] = useState(0); // 服务器网卡卡片列表状态 const [serverNicSearchText, setServerNicSearchText] = useState(''); const [serverNicRoomFilter, setServerNicRoomFilter] = useState('all'); const [serverNicRackFilter, setServerNicRackFilter] = useState('all'); const [serverNicCardPage, setServerNicCardPage] = useState(1); const serverNicCardPageSize = 12; // 按设备分组获取端口(设备维度分页,避免端口数过多时部分设备不显示) const fetchPorts = useCallback( async (page = 1) => { const validPage = Number.isInteger(page) && page > 0 ? page : 1; try { setLoading(true); const params = { page: validPage, pageSize: deviceCardPageSize, }; if (filters.deviceId) params.deviceId = filters.deviceId; const response = await api.get('/device-ports/grouped', { params }); const groups = response.groups || []; // 对每个设备的端口按名称自然排序,并展开为扁平数组(供统计、导出当前页使用) const flatPorts = []; const sortedGroups = groups.map(g => { const sortedPorts = sortPortsByName(g.ports || []); sortedPorts.forEach(p => flatPorts.push(p)); return { device: g.device, ports: sortedPorts }; }); setPorts(flatPorts); setGroupedPorts(sortedGroups); setDeviceCardTotal(response.total || 0); setPortTotal(response.totalPorts || 0); setDeviceCardPage(validPage); } catch (error) { message.error('获取端口列表失败'); console.error('获取端口列表失败:', error); } finally { setLoading(false); } }, [filters, deviceCardPageSize] ); const fetchDevices = useCallback(async (keyword = '') => { try { setDeviceSearching(true); if (keyword && keyword.trim()) { 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 || []); } } catch (error) { message.error('获取设备列表失败'); console.error('获取设备列表失败:', error); } finally { setDeviceSearching(false); } }, []); const handleDeviceSearch = useCallback( debounce(value => { fetchDevices(value); }, 300), [fetchDevices] ); useEffect(() => { fetchPorts(1); fetchDevices(); }, [fetchPorts, fetchDevices]); const handleSearch = useCallback(() => { setDeviceCardPage(1); fetchPorts(1); }, [fetchPorts]); const handleReset = useCallback(() => { setFilters({ deviceId: '', }); setDeviceCardPage(1); fetchPorts(1); }, [fetchPorts]); const handleAdd = () => { setEditingPort(null); form.resetFields(); setPortAddGuideModalVisible(true); }; const handleGuideSelectType = type => { setPortAddGuideModalVisible(false); setGuidedDeviceType(type); fetchDevices(); fetchRoomsAndRacks(); if (type === 'switch') { setDeviceFilterType('switch'); } else if (type === 'server') { setDeviceFilterType('server'); } setSelectDeviceModalVisible(true); }; 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); } }; const handleManageServerNics = async () => { setServerNicListVisible(true); setServerNicLoading(true); try { 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 } }), ]); setRoomList(roomsData.rooms || roomsData || []); setRackList(racksData.racks || racksData || []); const servers = devicesResponse.devices || devicesResponse || []; const ports = portsResponse.ports || portsResponse || []; 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], sampleNic: allNics[0], }); 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 }; }); console.log( '[DEBUG] serversWithNics:', serversWithNics.length, 'serversNeedingAttention:', serversNeedingAttention.length ); setServerNicList(serversNeedingAttention); } catch (error) { console.error('获取服务器列表失败:', error); message.error('获取服务器列表失败: ' + (error.message || error)); } finally { setServerNicLoading(false); } }; const handleSelectDeviceForPort = device => { setSelectDeviceModalVisible(false); if (!device) return; const deviceType = getDeviceType(device); if (deviceType === 'server') { api .get(`/network-cards/device/${device.deviceId}`) .then(nicList => { const validNicList = Array.isArray(nicList) ? nicList : []; if (validNicList.length === 0) { message.warning({ content: '该服务器尚未添加网卡,请先在网卡管理中添加网卡', icon: ( ), duration: 3, }); handleManageNetworkCards(device); } else { setNicList(validNicList); setSelectedDeviceForPort(device); form.resetFields(); form.setFieldsValue({ deviceId: device.deviceId }); setModalVisible(true); } }) .catch(() => { message.warning({ content: '该服务器尚未添加网卡,请先在网卡管理中添加网卡', icon: , duration: 3, }); handleManageNetworkCards(device); }); } else { setSelectedDeviceForPort(device); form.resetFields(); form.setFieldsValue({ deviceId: device.deviceId }); setModalVisible(true); } }; const handleAddPortForDevice = device => { const deviceType = getDeviceType(device); if (deviceType === 'server') { api .get(`/network-cards/device/${device.deviceId}`) .then(nicList => { const validNicList = Array.isArray(nicList) ? nicList : []; if (validNicList.length === 0) { message.warning({ content: '该服务器尚未添加网卡,请先在网卡管理中添加网卡', icon: ( ), duration: 3, }); handleManageNetworkCards(device); } else { setNicList(validNicList); setSelectedDeviceForPort(device); form.resetFields(); form.setFieldsValue({ deviceId: device.deviceId }); setModalVisible(true); } }) .catch(() => { message.warning({ content: '该服务器尚未添加网卡,请先在网卡管理中添加网卡', icon: , duration: 3, }); handleManageNetworkCards(device); }); } else { setSelectedDeviceForPort(device); form.resetFields(); form.setFieldsValue({ deviceId: device.deviceId }); setModalVisible(true); } }; const handleManageNetworkCards = device => { setSelectedDeviceForNic(device); setNetworkCardModalVisible(true); }; const handleNicSuccess = () => { message.success({ content: '操作成功', icon: , }); setRefreshTrigger(prev => prev + 1); fetchPorts(1); }; const handleEdit = port => { 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, description: port.description, }); setModalVisible(true); }; const handleDelete = async portId => { Modal.confirm({ title: '确认删除', content: '确定要删除这个端口吗?此操作不可恢复!', okText: '删除', okType: 'danger', cancelText: '取消', onOk: async () => { try { await api.delete(`/device-ports/${portId}`); message.success({ content: '删除成功', icon: , }); fetchPorts(1); } catch (error) { const errorMsg = error.response?.data?.error || ''; if (errorMsg.includes('关联的接线记录')) { message.error('该端口存在关联的接线记录,请先删除关联的接线'); } else { message.error('删除失败'); } console.error('删除失败:', error); } }, }); }; 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: , }); setSelectedRowKeys([]); fetchPorts(1); } catch (error) { const errorMsg = error.response?.data?.error || ''; if (errorMsg.includes('关联的接线记录')) { message.error('部分端口存在关联的接线记录,请先删除关联的接线'); } else { message.error('批量删除失败'); } console.error('批量删除失败:', error); } }, }); }; /** * 从端口名中提取前缀和末尾数字 * @param {string} portName - 端口名称 * @returns {{ prefix: string, num: number } | null} */ const extractPrefixAndNum = portName => { if (!portName || typeof portName !== 'string') return null; const match = portName.match(/^(.*?)(\d+)$/); if (!match) return null; return { prefix: match[1], num: parseInt(match[2], 10) }; }; /** * 快捷模式解析端口范围 * @param {string} portName - 端口范围字符串 * @returns {{ isRange: boolean, ports: string[], portCount: number, error?: string } | null} */ const parsePortRangeQuick = portName => { if (!portName || typeof portName !== 'string') return null; const trimmed = portName.trim(); if (!trimmed.includes('-')) return null; const lastDashIdx = trimmed.lastIndexOf('-'); const startPart = trimmed.substring(0, lastDashIdx).trim(); const endPart = trimmed.substring(lastDashIdx + 1).trim(); if (!startPart || !endPart) { return { isRange: false, ports: [], portCount: 0, error: '格式不完整,- 前后都需要有内容' }; } const startParsed = extractPrefixAndNum(startPart); const endParsed = extractPrefixAndNum(endPart); if (!startParsed) { return { isRange: false, ports: [], portCount: 0, error: `起始端口名 "${startPart}" 末尾无数字` }; } if (!endParsed) { return { isRange: false, ports: [], portCount: 0, error: `结束端口名 "${endPart}" 末尾无数字` }; } let prefix; if (endParsed.prefix === '') { prefix = startParsed.prefix; } else { if (startParsed.prefix !== endParsed.prefix) { return { isRange: false, ports: [], portCount: 0, error: `前后端口名前缀不一致:"${startParsed.prefix}" vs "${endParsed.prefix}"` }; } prefix = startParsed.prefix; } if (startParsed.num >= endParsed.num) { return { isRange: false, ports: [], portCount: 0, error: `起始数字 ${startParsed.num} 必须小于结束数字 ${endParsed.num}` }; } if (endParsed.num - startParsed.num > 1000) { return { isRange: false, ports: [], portCount: 0, error: '单次最多创建1000个端口' }; } const portCount = endParsed.num - startParsed.num + 1; const ports = []; for (let i = 0; i < portCount; i++) { ports.push(`${prefix}${startParsed.num + i}`); } return { isRange: true, ports, portCount }; }; /** * 范围模式解析(双输入框) * @param {string} startPortName - 起始端口名 * @param {string} endPortName - 结束端口名 * @returns {{ isRange: boolean, ports: string[], portCount: number, error?: string }} */ const parsePortRangeMode = (startPortName, endPortName) => { if (!startPortName || !endPortName) { return { isRange: false, ports: [], portCount: 0, error: '请填写起始和结束端口名' }; } const startParsed = extractPrefixAndNum(startPortName.trim()); const endParsed = extractPrefixAndNum(endPortName.trim()); if (!startParsed) { return { isRange: false, ports: [], portCount: 0, error: `起始端口名 "${startPortName}" 末尾无数字` }; } if (!endParsed) { return { isRange: false, ports: [], portCount: 0, error: `结束端口名 "${endPortName}" 末尾无数字` }; } if (startParsed.prefix !== endParsed.prefix) { return { isRange: false, ports: [], portCount: 0, error: `前后端口名前缀不一致:"${startParsed.prefix || '(空)'}" vs "${endParsed.prefix || '(空)'}"` }; } if (startParsed.num >= endParsed.num) { return { isRange: false, ports: [], portCount: 0, error: `起始数字 ${startParsed.num} 必须小于结束数字 ${endParsed.num}` }; } if (endParsed.num - startParsed.num > 1000) { return { isRange: false, ports: [], portCount: 0, error: '单次最多创建1000个端口' }; } const portCount = endParsed.num - startParsed.num + 1; const ports = []; for (let i = 0; i < portCount; i++) { ports.push(`${startParsed.prefix}${startParsed.num + i}`); } return { isRange: true, ports, portCount }; }; /** * 根据模式生成端口名列表 * @param {string} mode - 'range' 或 'quick' * @param {object} params - { portName } 或 { startPortName, endPortName } * @returns {{ ports: string[], error: string | null, parseResult: object | null }} */ const generatePortNames = (mode, params) => { if (mode === 'range') { const { startPortName, endPortName } = params; if (!startPortName && !endPortName) return { ports: [], error: null, parseResult: null }; if (!startPortName || !endPortName) return { ports: [], error: null, parseResult: null }; const result = parsePortRangeMode(startPortName, endPortName); if (result.isRange) return { ports: result.ports, error: null, parseResult: result }; return { ports: [], error: result.error, parseResult: result }; } const { portName } = params; if (!portName) return { ports: [], error: null, parseResult: null }; // 快捷模式(仅单端口,不支持范围) return { ports: [portName.trim()], error: null, parseResult: null }; }; /** 更新端口预览 */ const updatePortPreview = (mode, params) => { const result = generatePortNames(mode, params); setParseError(result.error); setParseResult(result.parseResult); if (result.ports.length > 1) { setPreviewPorts(result.ports.slice(0, 20)); setShowPreview(true); } else { setPreviewPorts([]); setShowPreview(false); } }; /** 模式切换 */ const handleModeChange = newMode => { setPortMode(newMode); setParseError(null); setParseResult(null); setPreviewPorts([]); setShowPreview(false); form.setFieldsValue({ portName: undefined, startPortName: undefined, endPortName: undefined }); }; const generateUniquePortId = () => { return `PORT-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; }; const handleSubmit = async () => { try { const values = await form.validateFields(); if (editingPort) { await api.put(`/device-ports/${editingPort.portId}`, values); message.success({ content: '更新成功', icon: , }); } else { // 根据模式获取端口名列表 const genResult = portMode === 'range' ? generatePortNames('range', { startPortName: values.startPortName, endPortName: values.endPortName }) : generatePortNames('quick', { portName: values.portName }); if (genResult.error) { message.error(genResult.error); return; } const portNames = genResult.ports; if (!portNames || portNames.length === 0) { message.error('请输入端口名称'); return; } const targetDeviceId = selectedDeviceForPort ? selectedDeviceForPort.deviceId : values.deviceId; if (portNames.length > 1) { const portsData = portNames.map((name, index) => ({ portId: generateUniquePortId(), deviceId: targetDeviceId, portName: name, portType: values.portType, status: 'free', vlanId: values.vlanId, description: values.description, nicId: values.nicId || null, })); const result = await api.post('/device-ports/batch', { ports: portsData }); const success = result.success ?? 0; const failed = result.failed ?? 0; if (failed > 0) { message.warning(`批量创建完成!成功 ${success} 个,失败 ${failed} 个`); } else { message.success({ content: `成功创建 ${success} 个端口`, icon: , }); } } else { const result = await api.post('/device-ports', { deviceId: targetDeviceId, portName: portNames[0], portType: values.portType, status: 'free', vlanId: values.vlanId, description: values.description, nicId: values.nicId || null, }); message.success({ content: '创建成功', icon: , }); } } setModalVisible(false); form.resetFields(); setSelectedDeviceForPort(null); setParseError(null); setParseResult(null); setPreviewPorts([]); setShowPreview(false); fetchPorts(1); } catch (error) { message.error(error.response?.data?.error || editingPort ? '更新失败' : '创建失败'); console.error('提交失败:', error); } }; const handleImport = (deviceType = 'all') => { setImportModalVisible(true); setImportPreview([]); setImportProgress({ current: 0, total: 0 }); }; const handleFileUpload = (file, onSuccess) => { console.log('handleFileUpload 被调用', file.name, file.size); const reader = new FileReader(); reader.onload = async e => { try { const data = e.target.result; let parsedData = []; let networkCardData = []; if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) { const workbook = XLSX.read(data, { type: 'binary' }); 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); } } else if (file.name.endsWith('.csv')) { parsedData = Papa.parse(data, { header: true, skipEmptyLines: true, }).data; } else { message.error('不支持的文件格式,请上传 .xlsx 或 .csv 文件'); return; } if (parsedData.length === 0) { message.warning('文件内容为空,请检查文件内容'); return; } // 保存网卡数据,供后续导入使用 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'); console.log('文件解析结果:', { 总行数: parsedData.length, 有效数据: validatedResult.validData.length, 错误数据: validatedResult.errors.length, 第一行数据: parsedData[0], 网卡数据行数: networkCardData.length, }); setImportPreview(validatedResult.validData); setImportErrors(validatedResult.errors); setImportProgress({ current: 0, total: validatedResult.validData.length }); if (onSuccess) onSuccess(); } catch (error) { message.error('文件解析失败'); console.error('文件解析失败:', error); } }; if (file.name.endsWith('.csv')) { reader.readAsText(file); } else { reader.readAsBinaryString(file); } }; const validateImportData = async (data, skipNicValidation = false) => { const validData = []; const allErrors = []; for (let i = 0; i < data.length; i++) { const row = data[i]; const result = await validatePortRow(row, i, skipNicValidation); if (result.valid) { validData.push(row); } else { allErrors.push(...result.errors.map(err => ({ ...err, originalRow: row }))); } } if (allErrors.length > 0) { message.warning({ content: `发现 ${allErrors.length} 个数据错误,请查看错误详情并修正后重新导入`, duration: 5, }); } return { validData, errors: allErrors }; }; const validatePortRow = async (row, index, skipNicValidation = false) => { const rowNum = index + 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: '请填写端口名称,格式如:eth0/1 或 GigabitEthernet0/0/1', }); } if (row['设备SN']) { const device = devices.find(d => d.serialNumber === row['设备SN']); if (!device) { errors.push({ row: rowNum, field: '设备SN', value: row['设备SN'], error: '设备不存在', suggestion: `设备SN "${row['设备SN']}" 未在系统中注册,请先在设备管理中添加该设备`, }); } } const validPortTypes = ['RJ45', 'SFP', 'SFP+', 'SFP28', 'QSFP', 'QSFP28']; if (row['端口类型'] && !validPortTypes.includes(row['端口类型'])) { errors.push({ row: rowNum, field: '端口类型', value: row['端口类型'], error: '无效的端口类型', suggestion: `端口类型 "${row['端口类型']}" 不支持。可选值:${validPortTypes.join('、')}`, }); } const validPortSpeeds = ['100M', '1G', '10G', '25G', '40G', '100G']; if (row['端口速率'] && !validPortSpeeds.includes(row['端口速率'])) { errors.push({ row: rowNum, field: '端口速率', value: row['端口速率'], error: '无效的端口速率', suggestion: `端口速率 "${row['端口速率']}" 不支持。可选值:${validPortSpeeds.join('、')}`, }); } const validStatuses = ['空闲', '占用', '故障']; if (row['状态'] && !validStatuses.includes(row['状态'])) { errors.push({ row: rowNum, field: '状态', value: row['状态'], error: '无效的状态值', suggestion: `状态 "${row['状态']}" 不支持。可选值:${validStatuses.join('、')}`, }); } 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 之间', }); } } } if (!skipNicValidation && row['网卡名称'] && row['设备SN']) { try { const nicResponse = await api.get('/network-cards/find', { params: { deviceSn: row['设备SN'], name: row['网卡名称'] }, }); row._nicId = nicResponse.nicId || null; } catch (error) { row._nicId = null; } } if (row['设备SN']) { const device = devices.find(d => d.serialNumber === row['设备SN']) || devices.find(d => d.sn === row['设备SN']); 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}" 的端口必须关联到网卡,请先在网卡管理中添加网卡,或在导入模板中填写网卡名称`, }); } else if (!skipNicValidation && row['网卡名称'] && !row._nicId) { errors.push({ row: rowNum, field: '网卡名称', value: row['网卡名称'], error: '网卡不存在', suggestion: `服务器 "${device.name}" 的网卡"${row['网卡名称']}"不存在,请先在网卡管理中添加该网卡`, }); } } } } if (errors.length > 0) { return { valid: false, errors }; } return { valid: true }; }; const handleBatchImport = async () => { if (importPreview.length === 0) { message.warning('请先选择要导入的数据'); return; } setImporting(true); setImportProgress({ current: 0, total: importPreview.length }); let progressInterval; let currentProgress = 0; try { // 检查是否有网卡数据需要导入 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'); } const statusMap = { 空闲: 'free', 占用: 'occupied', 故障: 'fault', }; const portsData = importPreview.map((row, index) => ({ portId: generateUniquePortId(), deviceSn: row['设备SN'], nicId: row._nicId || null, '网卡名称': row['网卡名称'], portName: row['端口名称'], portType: row['端口类型'], portSpeed: row['端口速率'], status: statusMap[row['状态']] || 'free', vlanId: row['VLAN ID'], description: row['描述'], })); progressInterval = setInterval(() => { currentProgress = Math.min(currentProgress + Math.random() * 15, 85); setImportProgress(prev => ({ ...prev, current: Math.floor((currentProgress / 100) * importPreview.length), })); }, 200); const response = await api.post('/device-ports/batch', { ports: portsData, skipExisting, updateExisting, }); const { total, success, failed, skipped = 0, updated = 0, errors } = response; clearInterval(progressInterval); setImportProgress({ current: importPreview.length, total: importPreview.length }); let msgContent = ''; if (networkCardImportResult) { const { success: nicSuccess, failed: nicFailed } = networkCardImportResult; if (nicSuccess > 0) { msgContent += `网卡导入成功 ${nicSuccess} 个,`; } if (nicFailed > 0) { msgContent += `网卡导入失败 ${nicFailed} 个,`; } } if (updated > 0) { msgContent += `端口更新 ${updated} 个,`; } if (skipped > 0) { msgContent += `端口跳过 ${skipped} 个,`; } if (success > 0) { msgContent += `端口新增 ${success - updated} 个,`; } if (failed > 0) { msgContent += `端口失败 ${failed} 个`; console.error('导入错误:', errors); } if (failed > 0 && success === 0 && skipped === 0 && updated === 0) { message.error(`导入失败!${msgContent}`); } else { message.success({ content: `导入完成!${msgContent}`, icon: , }); } fetchPorts(1); setImportModalVisible(false); setImportPreview([]); } catch (error) { clearInterval(progressInterval); console.error('批量导入失败:', error); message.error('批量导入失败,请检查数据格式'); } finally { clearInterval(progressInterval); setImporting(false); // 确保清除保存的网卡数据 sessionStorage.removeItem('networkCardImportData'); } }; const handleDownloadTemplate = () => { let fileName = '端口导入模板.xlsx'; const workbook = XLSX.utils.book_new(); 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); }; 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; XLSX.writeFile( workbook, `端口导出_${new Date().toISOString().slice(0, 10).replace(/-/g, '')}.xlsx` ); } message.success({ content: `成功导出 ${totalCount} 个端口`, icon: , }); } catch (error) { console.error('导出失败:', error); message.error('导出失败,请重试'); } }; const getStatusTag = status => { const statusMap = { free: { color: 'success', text: '空闲', icon: }, occupied: { color: 'processing', text: '占用', icon: }, fault: { color: 'error', text: '故障', icon: }, 空闲: { color: 'success', text: '空闲', icon: }, 占用: { color: 'processing', text: '占用', icon: }, 故障: { color: 'error', text: '故障', icon: }, }; const config = statusMap[status] || { color: 'default', text: status, icon: null }; return ( {config.text} ); }; const getPortTypeTag = type => { const typeMap = { RJ45: { color: 'blue', text: 'RJ45' }, SFP: { color: 'green', text: 'SFP' }, 'SFP+': { color: 'cyan', text: 'SFP+' }, SFP28: { color: 'purple', text: 'SFP28' }, QSFP: { color: 'orange', text: 'QSFP' }, QSFP28: { color: 'red', text: 'QSFP28' }, }; const config = typeMap[type] || { color: 'default', text: type }; return ( {config.text} ); }; const portColumns = useMemo( () => [ { title: '端口名称', dataIndex: 'portName', key: 'portName', width: 120, render: text => ( {text} ), }, { title: '所属网卡', dataIndex: 'networkCard', key: 'networkCard', width: 120, render: (nic, record) => { const isServer = record.device?.type?.toLowerCase()?.includes('server'); if (!isServer) return -; if (!nic) return ( 未关联 ); return {nic.name}; }, }, { title: '端口类型', dataIndex: 'portType', key: 'portType', width: 100, render: type => getPortTypeTag(type), }, { title: '端口速率', dataIndex: 'portSpeed', key: 'portSpeed', width: 100, render: 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 || -, }, { title: '描述', dataIndex: 'description', key: 'description', ellipsis: true, render: text => text || -, }, { title: '操作', key: 'action', width: 120, fixed: 'right', render: (_, record) => ( ) : (
{groupedPorts.map((data, index) => { const device = data.device; const deviceId = device?.deviceId; 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 = deviceId ? expandedKeys.includes(deviceId) : false; return ( {/* 设备头部 */}
{ if (isExpanded) { setExpandedKeys(prev => prev.filter(key => key !== deviceId)); } else { setExpandedKeys(prev => [...prev, deviceId]); } }} >
{getDeviceIcon(device)}
{device?.name || '未知设备'} {isServerDevice(device) ? '服务器' : isSwitchDevice(device) ? '交换机' : '设备'}
{device?.deviceId || '-'} · {device?.model || device?.type || '设备'}
} > {freeCount} } > {occupiedCount} {faultCount > 0 && ( } > {faultCount} )} 总计: {devicePorts.length}
{/* 端口列表 */} {isExpanded && (
{devicePorts.length > 0 ? (
{selectedRowKeys.length > 0 && (
已选择 {selectedRowKeys.length} 个端口
)} `共 ${total} 个端口`, pageSizeOptions: ['10', '20', '50', '100'], }} size="middle" scroll={{ x: 1000 }} /> ) : ( )} )} ); })} {deviceCardTotal > deviceCardPageSize && (
{ setExpandedKeys([]); fetchPorts(page); }} showSizeChanger={false} showTotal={(total, range) => `第 ${range[0]}-${range[1]} 个,共 ${total} 个设备` } />
)} {ports.length > 0 && (
共 {deviceCardTotal} 个设备,{portTotal} 个端口
)} )} {/* 新增/编辑端口弹窗 */} } onCancel={() => { setModalVisible(false); form.resetFields(); setSelectedDeviceForPort(null); setParseError(null); setParseResult(null); setPreviewPorts([]); setShowPreview(false); }} footer={null} width={860} zIndex={1050} style={{ borderRadius: '16px' }} styles={{ body: { padding: 0 } }} >
{editingPort ? '编辑端口' : '新增端口'} {selectedDeviceForPort && ( {selectedDeviceForPort.name} )}
1
基本信息
2
高级配置
{/* 格式说明 - 顶部 */} {portMode === 'range' ? ( <>
范围模式:分别输入起始和结束端口名,系统自动生成端口序列
规则:起始和结束端口名的前缀必须一致,且起始数字需小于结束数字
示例:起始 1/0/1,结束 1/0/48 → 创建 1/0/1 至 1/0/48 共 48 个端口
) : ( <>
单端口模式:每次创建一个端口,如需批量创建请切换至范围模式
示例:eth0/1、GigabitEthernet0/0/1、1/0/24
)}
} type="info" showIcon style={{ borderRadius: '8px', background: designTokens.colors.info.bg, border: `1px solid ${designTokens.colors.info.light}40`, marginBottom: '16px', }} /> {/* 端口标识 + 端口属性 左右排列 */} {/* 左侧:端口标识 */}
端口标识 {!editingPort && (
)}
{selectedDeviceForPort ? ( <> 设备} style={{ marginBottom: '12px' }} > {isServerDevice(selectedDeviceForPort) ? '服务器' : isSwitchDevice(selectedDeviceForPort) ? '交换机' : '设备'} } style={{ borderRadius: '8px' }} /> {isServerDevice(selectedDeviceForPort) && ( 所属网卡} rules={[{ required: true, message: '请选择网卡' }]} style={{ marginBottom: '12px' }} > )} {editingPort ? ( 端口名称} rules={[{ required: true, message: '请输入端口名称' }]} style={{ marginBottom: '0' }} > } style={{ borderRadius: '8px' }} /> ) : portMode === 'range' ? ( <> 起始端口名} rules={[{ required: true, message: '请输入起始端口名' }]} style={{ marginBottom: '12px' }} > } style={{ borderRadius: '8px' }} onChange={() => { const startPortName = form.getFieldValue('startPortName'); const endPortName = form.getFieldValue('endPortName'); updatePortPreview('range', { startPortName, endPortName }); }} /> 结束端口名} rules={[{ required: true, message: '请输入结束端口名' }]} style={{ marginBottom: '0' }} > } style={{ borderRadius: '8px' }} onChange={() => { const startPortName = form.getFieldValue('startPortName'); const endPortName = form.getFieldValue('endPortName'); updatePortPreview('range', { startPortName, endPortName }); }} /> ) : ( 端口名称} rules={[{ required: true, message: '请输入端口名称' }]} style={{ marginBottom: '0' }} > } suffix={ } style={{ borderRadius: '8px' }} onChange={e => updatePortPreview('quick', { portName: e.target.value })} /> )} {/* 解析失败提示 */} {parseError && !editingPort && ( )} {/* 端口预览 */} {showPreview && parseResult && !editingPort && (
将创建 {parseResult.portCount} 个端口
{previewPorts.map((port, index) => ( {port} ))} {parseResult.portCount > previewPorts.length && ( ...等 {parseResult.portCount} 个 )}
)} ) : ( 设备} rules={[{ required: true, message: '请选择设备' }]} style={{ marginBottom: '0' }} > )}
{/* 右侧:端口属性 */}
端口属性
端口类型} rules={[{ required: true, message: '请选择端口类型' }]} initialValue="RJ45" style={{ marginBottom: '12px' }} > 端口速率} initialValue="1G" style={{ marginBottom: '12px' }} > VLAN ID} style={{ marginBottom: '0' }} > {editingPort && ( 状态} rules={[{ required: true, message: '请选择状态' }]} style={{ marginTop: '12px', marginBottom: '0' }} > )} {!editingPort && (
新建端口默认状态为空闲
接线后状态将自动更新为占用
)}
{/* 描述信息 - 底部 */}
描述信息