import React, { useState, useEffect, useCallback } from 'react'; import { Table, Button, Modal, Form, Input, Select, message, Card, Space, Popconfirm, Tag, Tooltip, 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 CableManagement() { const [cables, setCables] = useState([]); const [devices, setDevices] = useState([]); const [switchDevices, setSwitchDevices] = useState([]); const [groupedCables, setGroupedCables] = useState({}); const [devicePorts, setDevicePorts] = useState({}); const [loading, setLoading] = useState(false); const [filters, setFilters] = useState({ switchDeviceId: '', status: 'all', cableType: 'all' }); const [modalVisible, setModalVisible] = useState(false); const [editingCable, setEditingCable] = 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 fetchCables = useCallback(async () => { try { setLoading(true); const params = {}; if (filters.switchDeviceId) params.sourceDeviceId = filters.switchDeviceId; if (filters.status !== 'all') params.status = filters.status; if (filters.cableType !== 'all') params.cableType = filters.cableType; const response = await axios.get('/api/cables', { params }); const cablesData = response.data.cables || []; setCables(cablesData); const grouped = {}; cablesData.forEach(cable => { const switchId = cable.sourceDeviceId; if (!grouped[switchId]) { grouped[switchId] = { switch: cable.sourceDevice, cables: [] }; } grouped[switchId].cables.push(cable); }); setGroupedCables(grouped); // 自动为每个交换机加载端口数据 const switchIds = Object.keys(grouped); for (const switchId of switchIds) { if (!devicePorts[switchId]) { try { const portsResponse = await axios.get(`/api/device-ports/device/${switchId}`); setDevicePorts(prev => ({ ...prev, [switchId]: portsResponse.data || [] })); } catch (error) { console.error(`获取交换机 ${switchId} 端口失败:`, error); } } } } catch (error) { message.error('获取接线列表失败'); console.error('获取接线列表失败:', error); } finally { setLoading(false); } }, [filters, devicePorts]); const fetchDevices = useCallback(async () => { try { const response = await axios.get('/api/devices', { params: { pageSize: 100 } }); const allDevices = response.data.devices || []; const switches = allDevices.filter(device => device.type === 'switch'); setDevices(allDevices); setSwitchDevices(switches); } catch (error) { message.error('获取设备列表失败'); console.error('获取设备列表失败:', error); } }, []); const fetchDevicePorts = useCallback(async (deviceId) => { if (!deviceId) { setDevicePorts(prev => ({ ...prev, [deviceId]: [] })); return; } try { const response = await axios.get(`/api/device-ports/device/${deviceId}`); setDevicePorts(prev => ({ ...prev, [deviceId]: response.data || [] })); } catch (error) { console.error('获取设备端口失败:', error); setDevicePorts(prev => ({ ...prev, [deviceId]: [] })); } }, []); useEffect(() => { fetchCables(); fetchDevices(); }, [fetchCables, fetchDevices]); const handleSearch = () => { fetchCables(); }; const handleReset = () => { setFilters({ switchDeviceId: '', status: 'all', cableType: 'all' }); }; const handleAdd = () => { setEditingCable(null); form.resetFields(); setModalVisible(true); }; const handleEdit = (cable) => { setEditingCable(cable); form.setFieldsValue({ sourceDeviceId: cable.sourceDeviceId, sourcePort: cable.sourcePort, targetDeviceId: cable.targetDeviceId, targetPort: cable.targetPort, cableType: cable.cableType, cableLength: cable.cableLength, status: cable.status, description: cable.description }); setModalVisible(true); }; const handleDelete = async (cableId) => { try { await axios.delete(`/api/cables/${cableId}`); message.success('删除成功'); fetchCables(); } catch (error) { message.error('删除失败'); console.error('删除失败:', error); } }; const handleDeleteSwitch = async (switchId) => { try { await axios.delete(`/api/devices/${switchId}`); message.success('删除设备成功'); fetchDevices(); fetchCables(); } catch (error) { message.error('删除设备失败'); console.error('删除设备失败:', error); } }; const [conflictModalVisible, setConflictModalVisible] = useState(false); const [conflictInfo, setConflictInfo] = useState(null); const [pendingSubmitValues, setPendingSubmitValues] = useState(null); const handleSubmit = async () => { try { const values = await form.validateFields(); // 如果是编辑模式,直接提交 if (editingCable) { await axios.put(`/api/cables/${editingCable.cableId}`, values); message.success('更新成功'); setModalVisible(false); form.resetFields(); fetchCables(); return; } // 创建模式:先检查冲突 try { const checkResponse = await axios.post('/api/cables/check-conflict', { sourceDeviceId: values.sourceDeviceId, sourcePort: values.sourcePort, targetDeviceId: values.targetDeviceId, targetPort: values.targetPort }); if (checkResponse.data.hasConflict) { setConflictInfo(checkResponse.data.conflicts); setPendingSubmitValues(values); setConflictModalVisible(true); return; } // 无冲突,直接创建 await axios.post('/api/cables', values); message.success('创建成功'); setModalVisible(false); form.resetFields(); fetchCables(); } catch (error) { if (error.response?.status === 409) { // 冲突错误 setConflictInfo([{ type: 'unknown', existingCable: error.response.data.existingCable }]); setPendingSubmitValues(values); setConflictModalVisible(true); } else { throw error; } } } catch (error) { message.error(editingCable ? '更新失败' : '创建失败'); console.error('提交失败:', error); } }; const handleForceSubmit = async () => { try { if (!pendingSubmitValues) return; await axios.post('/api/cables', { ...pendingSubmitValues, force: true }); message.success('接线已强制接管并创建成功'); setConflictModalVisible(false); setModalVisible(false); form.resetFields(); setPendingSubmitValues(null); setConflictInfo(null); fetchCables(); } catch (error) { message.error('强制接管失败'); 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 validateCableRow(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 validateCableRow = async (row, index) => { const errors = []; if (!row['源设备ID'] || !row['源设备端口']) { return { valid: false, error: `第 ${index + 1} 行:缺少必填字段(源设备ID或源设备端口)` }; } const sourceDevice = devices.find(d => d.deviceId === row['源设备ID']); if (!sourceDevice) { return { valid: false, error: `第 ${index + 1} 行:源设备不存在` }; } const targetDevice = devices.find(d => d.deviceId === row['目标设备ID']); if (!targetDevice) { return { valid: false, error: `第 ${index + 1} 行:目标设备不存在` }; } const validCableTypes = ['网线', '光纤', '铜缆']; if (!validCableTypes.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 cableTypeMap = { '网线': 'ethernet', '光纤': 'fiber', '铜缆': 'copper' }; const statusMap = { '正常': 'normal', '故障': 'fault', '未连接': 'disconnected' }; const cablesData = importPreview.map((row, index) => ({ cableId: `CABLE-${Date.now()}-${index}`, sourceDeviceId: row['源设备ID'], sourcePort: row['源设备端口'], targetDeviceId: row['目标设备ID'], targetPort: row['目标设备端口'], cableType: cableTypeMap[row['线缆类型']] || 'ethernet', cableLength: row['线缆长度(米)'], status: statusMap[row['状态']] || 'normal', description: row['描述'] })); const response = await axios.post('/api/cables/batch', { cables: cablesData }); 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} 条`); } fetchCables(); setImportModalVisible(false); setImportPreview([]); } catch (error) { console.error('批量导入失败:', error); message.error('批量导入失败,请检查数据格式'); } finally { setImporting(false); } }; const handleDownloadTemplate = () => { const templateData = [ { '源设备ID': 'DEV001', '源设备端口': 'eth0/1', '目标设备ID': 'DEV002', '目标设备端口': 'eth0', '线缆类型': '网线', '线缆长度(米)': '5', '状态': '正常', '描述': '示例接线' } ]; 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 = { normal: { color: 'success', text: '正常' }, fault: { color: 'error', text: '故障' }, disconnected: { color: 'default', text: '未连接' } }; const config = statusMap[status] || { color: 'default', text: status }; return {config.text}; }; const getCableTypeTag = (type) => { const typeMap = { '网线': { color: 'blue', text: '网线' }, '光纤': { color: 'green', text: '光纤' }, '铜缆': { color: 'orange', text: '铜缆' } }; const config = typeMap[type] || { color: 'default', text: type }; return {config.text}; }; const getPortConnectionStatus = (portName, switchData) => { const cable = switchData.cables.find(c => c.sourcePort === portName); if (!cable) { return { status: 'disconnected', text: '未连接', color: 'default' }; } return { status: cable.status, text: cable.status === 'normal' ? '已连接' : cable.status === 'fault' ? '故障' : '未连接', color: cable.status === 'normal' ? 'success' : cable.status === 'fault' ? 'error' : 'default' }; }; const portColumns = [ { title: '端口名称', dataIndex: 'portName', key: 'portName', width: 120 }, { title: '端口类型', dataIndex: 'portType', key: 'portType', width: 100, render: (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}; } }, { title: '端口速率', dataIndex: 'portSpeed', key: 'portSpeed', width: 100 }, { title: '连接状态', dataIndex: 'connectionStatus', key: 'connectionStatus', width: 100, render: (_, record) => { const status = getPortConnectionStatus(record.portName, record.switchData); return {status.text}; } }, { title: '目标设备', dataIndex: 'targetDevice', key: 'targetDevice', width: 200, render: (_, record) => { const cable = record.switchData.cables.find(c => c.sourcePort === record.portName); if (!cable) return '-'; return (
{cable.targetDevice?.name || '-'}
{cable.targetPort}
); } }, { title: '线缆类型', dataIndex: 'cableType', key: 'cableType', width: 100, render: (_, record) => { const cable = record.switchData.cables.find(c => c.sourcePort === record.portName); if (!cable) return '-'; return getCableTypeTag(cable.cableType); } }, { title: '长度(米)', dataIndex: 'cableLength', key: 'cableLength', width: 100, render: (_, record) => { const cable = record.switchData.cables.find(c => c.sourcePort === record.portName); if (!cable) return '-'; return cable.cableLength ? `${cable.cableLength}m` : '-'; } }, { title: '操作', key: 'action', width: 150, fixed: 'right', render: (_, record) => { const cable = record.switchData.cables.find(c => c.sourcePort === record.portName); return ( {cable && ( <> handleDelete(cable.cableId)} okText="确定" cancelText="取消" > )} ); } } ]; return (
{loading ? (
) : Object.keys(groupedCables).length === 0 ? (
) : ( {Object.entries(groupedCables).map(([switchId, switchData]) => { const switchDevice = switchData.switch; const switchPorts = devicePorts[switchId] || []; const connectedCount = switchData.cables.filter(c => c.status === 'normal').length; const disconnectedCount = switchData.cables.filter(c => c.status === 'disconnected').length; const faultCount = switchData.cables.filter(c => c.status === 'fault').length; return (
🔀
{switchDevice?.name || '未知设备'}
{switchDevice?.deviceId || '-'}
已连接: {connectedCount} 未连接: {disconnectedCount} {faultCount > 0 && 故障: {faultCount}} 总计: {switchData.cables.length}
} extra={ handleDeleteSwitch(switchId)} okText="确定" cancelText="取消" > } > ({ ...port, switchData: switchData }))} rowKey="portId" pagination={false} size="small" scroll={{ x: 1200 }} /> ); })} )} { setModalVisible(false); form.resetFields(); }} width={600} okText="确定" cancelText="取消" >
{ setImportModalVisible(false); setImportPreview([]); setImportProgress({ current: 0, total: 0 }); }} width={800} footer={[ , , ]} >
false} customRequest={({ file, onSuccess }) => { handleFileUpload({ file, onSuccess }); }} >

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

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

setSkipExisting(e.target.checked)}> 跳过已存在的接线 setUpdateExisting(e.target.checked)}> 更新已存在的接线
{importPreview.length > 0 && ( <>
数据预览(前10条)
getCableTypeTag(type) }, { title: '状态', dataIndex: '状态', key: 'status', width: 100, render: (status) => getStatusTag(status) }, { title: '描述', dataIndex: '描述', key: 'description', ellipsis: true } ]} 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)} 秒 )}
)} {/* 冲突提示弹窗 */} { setConflictModalVisible(false); setConflictInfo(null); setPendingSubmitValues(null); }} footer={[ , ]} width={600} > {conflictInfo && (
⚠️ 检测到端口冲突,以下端口已被占用:
{conflictInfo.map((conflict, index) => (
{conflict.type === 'source' ? '源端口' : conflict.type === 'target' ? '目标端口' : '端口'} {conflict.port}
{conflict.existingCable && (
当前连接:
源设备:{conflict.existingCable.sourceDevice?.name || conflict.existingCable.sourceDeviceId} ({conflict.existingCable.sourcePort})
目标设备:{conflict.existingCable.targetDevice?.name || conflict.existingCable.targetDeviceId} ({conflict.existingCable.targetPort})
线缆类型:{getCableTypeTag(conflict.existingCable.cableType)}
)}
))}
💡 点击"强制接管"将断开原有连接并创建新接线。此操作不可恢复!
)}
); } export default CableManagement;