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, Tabs, Badge, List, Typography, } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, ExportOutlined, ImportOutlined, DownloadOutlined, UploadOutlined as UploadIcon, AppstoreOutlined, UnorderedListOutlined, FilterOutlined, EyeOutlined, CompressOutlined, CloudServerOutlined, } from '@ant-design/icons'; import axios from 'axios'; import * as XLSX from 'xlsx'; import Papa from 'papaparse'; import PortPanel from '../components/PortPanel'; import VirtualDeviceList from '../components/VirtualDeviceList'; import NetworkCardPanel from '../components/NetworkCardPanel'; import NetworkCardCreateModal from '../components/NetworkCardCreateModal'; import PortCreateModal from '../components/PortCreateModal'; const { Option } = Select; const { Panel } = Collapse; const designTokens = { colors: { primary: { main: '#667eea', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', light: '#8b9ff0', dark: '#4f5db8', }, success: { main: '#10b981', gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', light: '#34d399', dark: '#047857', }, warning: { main: '#f59e0b', gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', light: '#fbbf24', dark: '#b45309', }, error: { main: '#ef4444', gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)', light: '#f87171', dark: '#b91c1c', }, }, borderRadius: { small: '6px', medium: '10px', large: '16px', }, shadows: { medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)', }, }; function PortManagement() { const [ports, setPorts] = useState([]); const [devices, setDevices] = useState([]); const [cables, setCables] = useState([]); const [groupedPorts, setGroupedPorts] = useState({}); const [loading, setLoading] = useState(false); const [filters, setFilters] = useState({ deviceId: '', status: 'all', portType: 'all', portSpeed: 'all', }); const [modalVisible, setModalVisible] = useState(false); const [editingPort, setEditingPort] = useState(null); const [form] = Form.useForm(); const [importModalVisible, setImportModalVisible] = useState(false); const [importFileList, setImportFileList] = useState([]); const [importPreview, setImportPreview] = 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 [panelFilters, setPanelFilters] = useState({ deviceType: 'all', searchText: '', showOnlyOccupied: false, }); const [visibleDeviceCount, setVisibleDeviceCount] = useState(10); const [expandedDevices, setExpandedDevices] = useState({}); // 网卡管理相关状态 const [networkCardModalVisible, setNetworkCardModalVisible] = useState(false); const [portCreateModalVisible, setPortCreateModalVisible] = useState(false); const [selectedDeviceForNic, setSelectedDeviceForNic] = useState(null); const [refreshTrigger, setRefreshTrigger] = useState(0); const fetchPorts = useCallback(async () => { try { setLoading(true); const params = { pageSize: 1000, // 获取所有端口,不分页 }; if (filters.deviceId) params.deviceId = filters.deviceId; if (filters.status !== 'all') params.status = filters.status; if (filters.portType !== 'all') params.portType = filters.portType; if (filters.portSpeed !== 'all') params.portSpeed = filters.portSpeed; const response = await axios.get('/api/device-ports', { params }); setPorts(response.data.ports || response.data || []); } catch (error) { message.error('获取端口列表失败'); console.error('获取端口列表失败:', error); } finally { setLoading(false); } }, [filters]); const fetchDevices = useCallback(async () => { try { const response = await axios.get('/api/devices', { params: { pageSize: 100 } }); setDevices(response.data.devices || response.data || []); } catch (error) { message.error('获取设备列表失败'); console.error('获取设备列表失败:', error); } }, []); const fetchCables = useCallback(async () => { try { const response = await axios.get('/api/cables'); setCables(response.data.cables || response.data || []); } catch (error) { console.error('获取接线列表失败:', error); } }, []); useEffect(() => { fetchPorts(); fetchDevices(); fetchCables(); }, [fetchPorts, fetchDevices, fetchCables]); useEffect(() => { const grouped = {}; ports.forEach(port => { const deviceId = port.deviceId; if (!grouped[deviceId]) { grouped[deviceId] = { device: devices.find(d => d.deviceId === deviceId), ports: [], }; } grouped[deviceId].ports.push(port); }); // 对每个设备的端口按名称升序排序 Object.keys(grouped).forEach(deviceId => { grouped[deviceId].ports.sort((a, b) => { const extractNumbers = str => { 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); }); }); setGroupedPorts(grouped); }, [ports, devices]); const handleSearch = () => { fetchPorts(); }; const handleReset = () => { setFilters({ deviceId: '', status: 'all', portType: 'all', portSpeed: 'all', }); }; const handleAdd = () => { setEditingPort(null); form.resetFields(); setModalVisible(true); }; const handleAddPortForDevice = device => { setEditingPort(null); form.resetFields(); // 自动选中当前设备 form.setFieldsValue({ deviceId: device.deviceId, }); setModalVisible(true); }; // 打开网卡管理模态框 const handleManageNetworkCards = device => { setSelectedDeviceForNic(device); setNetworkCardModalVisible(true); }; // 打开添加网卡模态框 const handleAddNetworkCard = device => { setSelectedDeviceForNic(device); setPortCreateModalVisible(true); }; // 网卡/端口创建成功回调 const handleNicSuccess = () => { message.success('操作成功'); setRefreshTrigger(prev => prev + 1); fetchPorts(); }; const handlePortSuccess = () => { message.success('端口添加成功'); setRefreshTrigger(prev => prev + 1); fetchPorts(); }; 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 => { try { await axios.delete(`/api/device-ports/${portId}`); message.success('删除成功'); fetchPorts(); } catch (error) { message.error('删除失败'); console.error('删除失败:', error); } }; // 解析端口名称范围,例如 "1/0/1-1/0/48" -> ["1/0/1", "1/0/2", ..., "1/0/48"] const parsePortRange = portName => { const rangeMatch = portName.match(/^(.*?)\/(\d+)-\1\/(\d+)$/); if (rangeMatch) { const prefix = rangeMatch[1]; const start = parseInt(rangeMatch[2]); const end = parseInt(rangeMatch[3]); if (start <= end && end - start < 100) { // 限制最多100个端口 return Array.from({ length: end - start + 1 }, (_, i) => `${prefix}/${start + i}`); } } return [portName]; // 如果不是范围格式,返回原名称 }; const handleSubmit = async () => { try { const values = await form.validateFields(); if (editingPort) { await axios.put(`/api/device-ports/${editingPort.portId}`, values); message.success('更新成功'); } else { // 解析端口名称范围 const portNames = parsePortRange(values.portName); if (portNames.length > 1) { // 批量创建端口 const portsData = portNames.map((name, index) => ({ portId: `PORT-${Date.now()}-${index}`, deviceId: values.deviceId, portName: name, portType: values.portType, portSpeed: values.portSpeed, status: values.status, vlanId: values.vlanId, description: values.description, })); const response = await axios.post('/api/device-ports/batch', { ports: portsData }); const { success, failed } = response.data; if (failed > 0) { message.warning(`批量创建完成!成功 ${success} 个,失败 ${failed} 个`); } else { message.success(`成功创建 ${success} 个端口`); } } else { // 单个创建 await axios.post('/api/device-ports', values); message.success('创建成功'); } } setModalVisible(false); form.resetFields(); fetchPorts(); } catch (error) { message.error(editingPort ? '更新失败' : '创建失败'); console.error('提交失败:', error); } }; const handleImport = () => { setImportModalVisible(true); setImportPreview([]); setImportProgress({ current: 0, total: 0 }); }; const handleFileUpload = info => { const { file } = info; setImportFileList([file]); const reader = new FileReader(); reader.onload = async e => { try { const data = e.target.result; let parsedData = []; if (file.name.endsWith('.xlsx') || file.name.endsWith('.xls')) { const workbook = XLSX.read(data, { type: 'binary' }); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; parsedData = XLSX.utils.sheet_to_json(worksheet); } else if (file.name.endsWith('.csv')) { Papa.parse(data, { header: true, skipEmptyLines: true, complete: results => { parsedData = results.data; }, }); } else { message.error('不支持的文件格式,请上传 .xlsx 或 .csv 文件'); return; } const validatedData = await validateImportData(parsedData); setImportPreview(validatedData); setImportProgress({ current: 0, total: validatedData.length }); } catch (error) { message.error('文件解析失败'); console.error('文件解析失败:', error); } }; reader.readAsBinaryString(file); }; const validateImportData = async data => { const validatedData = []; const errors = []; for (let i = 0; i < data.length; i++) { const row = data[i]; const error = await validatePortRow(row, i); if (error) { errors.push(error); } else { validatedData.push(row); } } if (errors.length > 0) { message.warning(`发现 ${errors.length} 条数据错误,已跳过`); console.log('导入错误:', errors); } return validatedData; }; const validatePortRow = async (row, index) => { const errors = []; if (!row['设备ID'] || !row['端口名称']) { return { valid: false, error: `第 ${index + 1} 行:缺少必填字段(设备ID或端口名称)` }; } const device = devices.find(d => d.deviceId === row['设备ID']); if (!device) { return { valid: false, error: `第 ${index + 1} 行:设备不存在` }; } const validPortTypes = ['RJ45', 'SFP', 'SFP+', 'SFP28', 'QSFP', 'QSFP28']; if (!validPortTypes.includes(row['端口类型'])) { return { valid: false, error: `第 ${index + 1} 行:无效的端口类型` }; } const validPortSpeeds = ['100M', '1G', '10G', '25G', '40G', '100G']; if (!validPortSpeeds.includes(row['端口速率'])) { return { valid: false, error: `第 ${index + 1} 行:无效的端口速率` }; } const validStatuses = ['空闲', '占用', '故障']; if (!validStatuses.includes(row['状态'])) { return { valid: false, error: `第 ${index + 1} 行:无效的状态` }; } if (errors.length > 0) { return { valid: false, error: errors.join('; ') }; } return { valid: true }; }; const handleBatchImport = async () => { if (importPreview.length === 0) { message.warning('请先选择要导入的数据'); return; } setImporting(true); setImportProgress({ current: 0, total: importPreview.length }); try { const statusMap = { 空闲: 'free', 占用: 'occupied', 故障: 'fault', }; const portsData = importPreview.map((row, index) => ({ portId: `PORT-${Date.now()}-${index}`, deviceId: row['设备ID'], portName: row['端口名称'], portType: row['端口类型'], portSpeed: row['端口速率'], status: statusMap[row['状态']] || 'free', vlanId: row['VLAN ID'], description: row['描述'], })); const response = await axios.post('/api/device-ports/batch', { ports: portsData }); const { total, success, failed, errors } = response.data; setImportProgress({ current: total, total: total }); if (failed > 0) { console.error('导入错误:', errors); message.warning(`导入完成!成功 ${success} 条,失败 ${failed} 条`); } else { message.success(`导入完成!成功 ${success} 条`); } fetchPorts(); setImportModalVisible(false); setImportPreview([]); } catch (error) { console.error('批量导入失败:', error); message.error('批量导入失败,请检查数据格式'); } finally { setImporting(false); } }; const handleDownloadTemplate = () => { const templateData = [ { 设备ID: 'DEV001', 端口名称: 'eth0/1', 端口类型: 'RJ45', 端口速率: '1G', 状态: '空闲', 'VLAN ID': '100', 描述: '示例端口', }, ]; const worksheet = XLSX.utils.json_to_sheet(templateData); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, '端口数据'); XLSX.writeFile(workbook, '端口导入模板.xlsx'); }; const getStatusTag = status => { const statusMap = { free: { color: 'success', text: '空闲' }, occupied: { color: 'processing', text: '占用' }, fault: { color: 'error', text: '故障' }, 空闲: { color: 'success', text: '空闲' }, 占用: { color: 'processing', text: '占用' }, 故障: { color: 'error', text: '故障' }, }; const config = statusMap[status] || { color: 'default', text: status }; 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 = [ { title: '端口名称', dataIndex: 'portName', key: 'portName', width: 120, }, { title: '端口类型', dataIndex: 'portType', key: 'portType', width: 100, render: type => getPortTypeTag(type), }, { title: '端口速率', dataIndex: 'portSpeed', key: 'portSpeed', width: 100, }, { 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: 150, fixed: 'right', render: (_, record) => ( handleDelete(record.portId)} okText="确定" cancelText="取消" > ), }, ]; return (
{loading ? (
) : Object.keys(groupedPorts).length === 0 ? (
) : viewMode === 'panel' ? ( // 面板视图 - 使用虚拟滚动优化 g.device) .filter(Boolean)} groupedPorts={groupedPorts} cables={cables} allDevices={devices} onPortClick={port => handleEdit(port)} onAddPort={device => handleAddPortForDevice(device)} onManageNetworkCards={device => handleManageNetworkCards(device)} initialVisibleCount={5} loadMoreCount={5} /> ) : ( // 列表视图 {Object.entries(groupedPorts).map(([deviceId, data]) => { 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; return (
{device?.type?.toLowerCase()?.includes('server') ? '🖥️' : device?.type?.toLowerCase()?.includes('switch') ? '🔀' : device?.type?.toLowerCase()?.includes('router') ? '🌐' : '📦'}
{device?.name || '未知设备'}
{device?.deviceId || '-'}
空闲: {freeCount} 占用: {occupiedCount} 故障: {faultCount} 总计: {devicePorts.length} {/* 网卡管理按钮 - 只有服务器显示 */} {device?.type?.toLowerCase()?.includes('server') && ( )}
} > `共 ${total} 个端口`, pageSizeOptions: ['10', '20', '50', '100'], }} size="small" scroll={{ x: 1000 }} /> ); })} )} { setModalVisible(false); form.resetFields(); }} width={600} okText="确定" cancelText="取消" >
{ setImportModalVisible(false); setImportPreview([]); setImportProgress({ current: 0, total: 0 }); }} width={900} footer={[ , , , ]} >
false} customRequest={({ file, onSuccess }) => { handleFileUpload({ file, onSuccess }); }} >

点击或拖拽文件到此处上传

支持 .xlsx, .xls, .csv 格式

setSkipExisting(e.target.checked)}> 跳过已存在的端口 setUpdateExisting(e.target.checked)}> 更新已存在的端口
{importPreview.length > 0 && ( <>
数据预览(前10条)
getPortTypeTag(type), }, { title: '端口速率', dataIndex: '端口速率', key: 'portSpeed', width: 100, }, { title: '状态', dataIndex: '状态', key: 'status', width: 100, render: status => getStatusTag(status), }, { title: 'VLAN ID', dataIndex: 'VLAN ID', key: 'vlanId', width: 100, render: vlanId => vlanId || '-', }, { title: '描述', dataIndex: '描述', key: 'description', ellipsis: true, render: text => ( {text || '-'} ), }, ]} dataSource={importPreview.slice(0, 10)} rowKey={(record, index) => index} pagination={false} size="small" scroll={{ x: 1000 }} /> {importPreview.length > 10 && (
仅显示前10条数据,共 {importPreview.length} 条
)} )} {importing && (
正在导入 {importProgress.current} / {importProgress.total} 条数据... {importProgress.current > 0 && ( 预计剩余时间:{Math.ceil((importProgress.total - importProgress.current) / 5)}{' '} 秒 )}
)} {/* 网卡管理模态框 */} 网卡管理 - {selectedDeviceForNic?.name} } open={networkCardModalVisible} onCancel={() => { setNetworkCardModalVisible(false); setSelectedDeviceForNic(null); }} footer={null} width={800} destroyOnClose > {selectedDeviceForNic && ( )} {/* 创建网卡模态框 */} { setPortCreateModalVisible(false); setSelectedDeviceForNic(null); }} onSuccess={handleNicSuccess} /> ); } export default PortManagement;