import React, { useState, useEffect, useCallback } from 'react'; import { Table, Button, Modal, Form, Input, Select, message, Card, Space, Popconfirm, Tag, Tooltip, InputNumber, Collapse, Empty, Spin, Upload, Progress, Checkbox } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ReloadOutlined, ExportOutlined, ImportOutlined, DownloadOutlined, UploadOutlined as UploadIcon } from '@ant-design/icons'; import axios from 'axios'; import * as XLSX from 'xlsx'; import Papa from 'papaparse'; 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 [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); const fetchPorts = useCallback(async () => { try { setLoading(true); const params = {}; 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: 1000 } }); setDevices(response.data.devices || response.data || []); } catch (error) { console.error('获取设备列表失败:', error); } }, []); useEffect(() => { fetchPorts(); fetchDevices(); }, [fetchPorts, fetchDevices]); 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); }); 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 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); } }; const handleSubmit = async () => { try { const values = await form.validateFields(); if (editingPort) { await axios.put(`/api/device-ports/${editingPort.portId}`, values); message.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 ? (
) : ( {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}
} > ); })} )} { 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)} 秒 )}
)} ); } export default PortManagement;