import React, { useState, useEffect, useCallback, useMemo } 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, } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, ExportOutlined, ImportOutlined, DownloadOutlined, UploadOutlined as UploadIcon, AppstoreOutlined, UnorderedListOutlined, FilterOutlined, ClearOutlined, CloudServerOutlined, ApiOutlined, CheckCircleOutlined, ExclamationCircleOutlined, DisconnectOutlined, } from '@ant-design/icons'; import axios from 'axios'; import * as XLSX from 'xlsx'; import Papa from 'papaparse'; import { motion, AnimatePresence } from 'framer-motion'; import VirtualDeviceList from '../components/VirtualDeviceList'; import NetworkCardPanel from '../components/NetworkCardPanel'; import NetworkCardCreateModal from '../components/NetworkCardCreateModal'; const { Option } = Select; const { Panel } = Collapse; const { Text, Title } = Typography; const { TextArea } = Input; const debounce = (func, wait) => { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }; // 设计令牌 - 与接线管理页面保持一致 const designTokens = { colors: { primary: { main: '#6366f1', gradient: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', light: '#818cf8', dark: '#4f46e5', bg: '#eef2ff', }, success: { main: '#10b981', gradient: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', light: '#34d399', dark: '#047857', bg: '#ecfdf5', }, warning: { main: '#f59e0b', gradient: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', light: '#fbbf24', dark: '#b45309', bg: '#fffbeb', }, error: { main: '#ef4444', gradient: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)', light: '#f87171', dark: '#b91c1c', bg: '#fef2f2', }, info: { main: '#3b82f6', bg: '#eff6ff', }, neutral: { 50: '#f8fafc', 100: '#f1f5f9', 200: '#e2e8f0', 300: '#cbd5e1', 400: '#94a3b8', 500: '#64748b', 600: '#475569', 700: '#334155', 800: '#1e293b', 900: '#0f172a', }, }, borderRadius: { sm: '6px', md: '10px', lg: '16px', xl: '20px', }, shadows: { sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)', lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)', xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)', }, }; // 动画配置 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], }, }, }, }; function PortManagement() { const [ports, setPorts] = useState([]); const [devices, setDevices] = useState([]); const [deviceSearching, setDeviceSearching] = useState(false); const [cables, setCables] = useState([]); const [groupedPorts, setGroupedPorts] = useState({}); const [loading, setLoading] = useState(false); const [filters, setFilters] = useState({ deviceId: '', status: 'all', portType: 'all', portSpeed: 'all', searchText: '', }); const [modalVisible, setModalVisible] = useState(false); const [editingPort, setEditingPort] = useState(null); const [form] = Form.useForm(); const [importModalVisible, setImportModalVisible] = useState(false); 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 [networkCardModalVisible, setNetworkCardModalVisible] = useState(false); const [portCreateModalVisible, setPortCreateModalVisible] = useState(false); const [selectedDeviceForNic, setSelectedDeviceForNic] = useState(null); const [refreshTrigger, setRefreshTrigger] = useState(0); // 展开的设备 const [expandedKeys, setExpandedKeys] = useState([]); 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 }); const portsData = response.data.ports || response.data || []; // 搜索过滤 let filteredPorts = portsData; if (filters.searchText) { const searchLower = filters.searchText.toLowerCase(); filteredPorts = portsData.filter(port => port.portName?.toLowerCase().includes(searchLower) || port.portType?.toLowerCase().includes(searchLower) || port.description?.toLowerCase().includes(searchLower) ); } setPorts(filteredPorts); } catch (error) { message.error('获取端口列表失败'); console.error('获取端口列表失败:', error); } finally { setLoading(false); } }, [filters]); const fetchDevices = useCallback(async (keyword = '') => { try { setDeviceSearching(true); const params = { pageSize: 50 }; if (keyword && keyword.trim()) { params.keyword = keyword.trim(); } const response = await axios.get('/api/devices', { params }); setDevices(response.data.devices || response.data || []); } catch (error) { message.error('获取设备列表失败'); console.error('获取设备列表失败:', error); } finally { setDeviceSearching(false); } }, []); const handleDeviceSearch = useCallback( debounce(value => { fetchDevices(value); }, 300), [fetchDevices] ); 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); // 自动展开前5个 const deviceIds = Object.keys(grouped); setExpandedKeys(deviceIds.slice(0, 5)); }, [ports, devices]); const handleSearch = () => { fetchPorts(); }; const handleReset = () => { setFilters({ deviceId: '', status: 'all', portType: 'all', portSpeed: 'all', searchText: '', }); }; 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 handleNicSuccess = () => { message.success({ content: '操作成功', icon: , }); 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({ content: '删除成功', icon: , }); fetchPorts(); } catch (error) { message.error('删除失败'); console.error('删除失败:', error); } }; 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) { 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({ content: '更新成功', icon: , }); } 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({ content: `成功创建 ${success} 个端口`, icon: , }); } } else { await axios.post('/api/device-ports', values); message.success({ content: '创建成功', icon: , }); } } 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; 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} 条数据错误,已跳过`); } return validatedData; }; const validatePortRow = async (row, index) => { 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} 行:无效的状态` }; } 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({ content: `导入完成!成功 ${success} 条`, icon: , }); } 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: '空闲', 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: '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) => ( ) : 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], 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; return (
{getDeviceIcon(device)}
{device?.name || '未知设备'}
{device?.deviceId || '-'} · {device?.model || device?.type || '设备'}
} > {freeCount} } > {occupiedCount} {faultCount > 0 && ( } > {faultCount} )} 总计: {devicePorts.length} } extra={ e.stopPropagation()}>