import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Modal, Steps, Button, Space, Spin, Card, Tag, Tooltip, message, Typography, Divider, Badge, Empty, Input, } from 'antd'; import { SwapOutlined, CloudServerOutlined, DatabaseOutlined, SettingOutlined, CheckCircleOutlined, ArrowRightOutlined, PlusOutlined, SearchOutlined, InfoCircleOutlined, } from '@ant-design/icons'; import { motion, AnimatePresence } from 'framer-motion'; import axios from 'axios'; import CloseButton from './CloseButton'; import PortPanel from './PortPanel'; import FilterableDeviceSelect from './FilterableDeviceSelect'; import { designTokens } from '../config/theme'; const { Title, Text } = Typography; const CABLE_TYPES = [ { value: 'ethernet', label: '以太网线', desc: 'Cat6 1G/10G', color: '#52c41a', icon: '🌐' }, { value: 'fiber', label: '光纤', desc: 'SMF/MMF 长距离', color: '#1890ff', icon: '🔦' }, { value: 'copper', label: '铜缆', desc: '电源/特殊连接', color: '#faad14', icon: '🔌' }, ]; const CABLE_LENGTHS = [1, 2, 3, 5, 7, 10, 15, 20, 30, 50]; const getStatusTag = (status) => { const config = { running: { color: 'success', text: '运行中' }, normal: { color: 'success', text: '正常' }, warning: { color: 'warning', text: '警告' }, error: { color: 'error', text: '故障' }, fault: { color: 'error', text: '故障' }, offline: { color: 'default', text: '离线' }, maintenance: { color: 'processing', text: '维护中' }, }; const { color, text } = config[status] || { color: 'default', text: status }; return {text}; }; const CableWizardModal = ({ visible, onClose, onSuccess, initialSourceDevice, editingCable }) => { const [currentStep, setCurrentStep] = useState(0); const [loading, setLoading] = useState(false); const [devices, setDevices] = useState([]); const [sourceDevice, setSourceDevice] = useState(null); const [targetDevice, setTargetDevice] = useState(null); const [sourcePort, setSourcePort] = useState(null); const [targetPort, setTargetPort] = useState(null); const [selectedCableType, setSelectedCableType] = useState('ethernet'); const [selectedCableLength, setSelectedCableLength] = useState(3); const [cableLabel, setCableLabel] = useState(''); const [cableDescription, setCableDescription] = useState(''); const [fetchingDevices, setFetchingDevices] = useState(false); const [sourcePorts, setSourcePorts] = useState([]); const [targetPorts, setTargetPorts] = useState([]); const [conflicts, setConflicts] = useState([]); const [cablesData, setCablesData] = useState([]); const [compatibilityWarning, setCompatibilityWarning] = useState(null); // 初始化编辑模式的数据 useEffect(() => { if (editingCable && visible) { setSourceDevice({ deviceId: editingCable.sourceDeviceId, name: editingCable.sourceDeviceId }); setTargetDevice({ deviceId: editingCable.targetDeviceId, name: editingCable.targetDeviceId }); setSourcePort({ portName: editingCable.sourcePort }); setTargetPort({ portName: editingCable.targetPort }); setSelectedCableType(editingCable.cableType || 'ethernet'); setSelectedCableLength(editingCable.cableLength || 3); setCableLabel(editingCable.cableLabel || ''); setCableDescription(editingCable.description || ''); setCurrentStep(3); // 直接跳转到预览步骤 } }, [editingCable, visible]); const fetchDevices = useCallback(async (keyword = '', type = '') => { try { setFetchingDevices(true); const params = { pageSize: 100 }; if (keyword && keyword.trim()) { params.keyword = keyword.trim(); } if (type && type.trim()) { params.type = type.trim(); } const response = await axios.get('/api/devices', { params }); const deviceList = response.data.devices || []; setDevices(deviceList); return deviceList; } catch (error) { console.error('获取设备列表失败:', error); message.error('获取设备列表失败'); return []; } finally { setFetchingDevices(false); } }, []); const fetchDevicePorts = useCallback(async (deviceId, type) => { console.log('fetchDevicePorts called for:', deviceId, type); if (!deviceId) return; try { const response = await axios.get(`/api/device-ports/device/${deviceId}`); const ports = response.data || []; console.log('Fetched ports:', ports); if (type === 'source') { setSourcePorts(ports); } else { setTargetPorts(ports); } } catch (error) { console.error(`获取端口列表失败: ${deviceId}`, error); message.error('获取端口列表失败'); } }, []); const fetchCables = useCallback(async () => { try { const response = await axios.get('/api/cables'); setCablesData(response.data.cables || []); } catch (error) { console.error('获取接线数据失败:', error); } }, []); const checkPortConflict = useCallback(async (deviceId, portName, excludeCableId = null) => { try { const response = await axios.post('/api/cables/check-conflict', { sourceDeviceId: deviceId, sourcePort: portName, excludeCableId, }); return response.data; } catch (error) { console.error('检查端口冲突失败:', error); return { hasConflict: false, conflicts: [] }; } }, []); const checkPortCompatibility = useCallback(async (srcDeviceId, srcPort, tgtDeviceId, tgtPort) => { try { const response = await axios.post('/api/cables/check-compatibility', { sourceDeviceId: srcDeviceId, sourcePort: srcPort, targetDeviceId: tgtDeviceId, targetPort: tgtPort, }); return response.data; } catch (error) { console.error('检查端口兼容性失败:', error); return { compatible: false, reasons: [] }; } }, []); const debounce = (fn, delay) => { let timer = null; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }; const handleDeviceSearch = useCallback( debounce((value, type = '') => { fetchDevices(value, type); }, 300), [fetchDevices] ); useEffect(() => { if (visible) { setCurrentStep(0); setTargetDevice(null); setSourcePort(null); setTargetPort(null); setSelectedCableType('ethernet'); setSelectedCableLength(3); setCableLabel(''); setCableDescription(''); setTargetPorts([]); setConflicts([]); setCompatibilityWarning(null); setCablesData([]); if (initialSourceDevice?.deviceId) { const deviceWithId = { deviceId: initialSourceDevice.deviceId, name: initialSourceDevice.name || initialSourceDevice.deviceId, type: initialSourceDevice.type || 'switch', model: initialSourceDevice.model, status: initialSourceDevice.status, Rack: initialSourceDevice.Rack, ipAddress: initialSourceDevice.ipAddress, position: initialSourceDevice.position, }; setSourceDevice(deviceWithId); fetchDevicePorts(deviceWithId.deviceId, 'source'); fetchDevices('', 'switch'); } else { setSourceDevice(null); setSourcePorts([]); fetchDevices('', 'switch'); } fetchCables(); } }, [visible, initialSourceDevice, fetchDevices, fetchDevicePorts, fetchCables]); const handleSourceDeviceSelect = useCallback(async device => { setSourceDevice(device); setSourcePort(null); await fetchDevicePorts(device.deviceId, 'source'); }, [fetchDevicePorts]); const handleTargetDeviceSelect = useCallback(async device => { setTargetDevice(device); setTargetPort(null); setConflicts([]); await fetchDevicePorts(device.deviceId, 'target'); }, [fetchDevicePorts]); const handleSourcePortSelect = useCallback(async port => { console.log('handleSourcePortSelect called with port:', port); const isAlreadySelected = sourcePort?.portId === port.portId || sourcePort?.portName === port.portName; if (isAlreadySelected) { console.log('Deselecting source port:', port); setSourcePort(null); setConflicts([]); setCompatibilityWarning(null); } else { if (sourceDevice?.deviceId === targetDevice?.deviceId) { message.warning('源设备和目标设备不能相同'); return; } const conflictResult = await checkPortConflict(sourceDevice.deviceId, port.portName); if (conflictResult.hasConflict) { const conflict = conflictResult.conflicts[0]; message.error(`源端口 ${port.portName} 已被占用,无法选择`); return; } setSourcePort(port); console.log('Source port set to:', port); if (targetDevice) { await checkPortConflict(targetDevice.deviceId, port.portName); } } }, [sourceDevice, targetDevice, sourcePort, checkPortConflict]); const handleTargetPortSelect = useCallback(async port => { console.log('handleTargetPortSelect called with port:', port); const isAlreadySelected = targetPort?.portId === port.portId || targetPort?.portName === port.portName; if (isAlreadySelected) { console.log('Deselecting target port:', port); setTargetPort(null); setConflicts([]); setCompatibilityWarning(null); } else { if (sourceDevice?.deviceId === targetDevice?.deviceId) { message.warning('源设备和目标设备不能相同'); return; } const conflictResult = await checkPortConflict(sourceDevice.deviceId, port.portName); if (conflictResult.hasConflict) { const conflict = conflictResult.conflicts[0]; message.error(`目标端口 ${port.portName} 已被占用,无法选择`); return; } setTargetPort(port); console.log('Target port set to:', port); if (sourcePort) { const compatResult = await checkPortCompatibility( sourceDevice.deviceId, sourcePort.portName, targetDevice.deviceId, port.portName ); if (!compatResult.compatible) { const errorReasons = compatResult.reasons.filter(r => r.severity === 'error'); if (errorReasons.length > 0) { setCompatibilityWarning({ type: 'error', message: errorReasons[0].message, details: compatResult.reasons, }); } } else { const warningReasons = compatResult.reasons.filter(r => r.severity === 'warning'); if (warningReasons.length > 0) { setCompatibilityWarning({ type: 'warning', message: warningReasons[0].message, details: compatResult.reasons, }); } else { setCompatibilityWarning(null); } } } } }, [sourceDevice, targetDevice, targetPort, sourcePort, checkPortConflict, checkPortCompatibility]); const handleNextStep = useCallback(async () => { if (currentStep === 0) { if (!sourceDevice) { message.warning('请先选择源设备'); return; } if (!sourcePort) { message.warning('请先选择源设备端口'); return; } if (sourcePorts.length === 0) { message.warning('源设备没有可用端口'); return; } } else if (currentStep === 1) { if (!targetDevice) { message.warning('请先选择目标设备'); return; } if (!targetPort) { message.warning('请先选择目标设备端口'); return; } if (targetPorts.length === 0) { message.warning('目标设备没有可用端口'); return; } if (compatibilityWarning?.type === 'error') { message.error('端口类型不兼容,无法创建接线'); return; } } else if (currentStep === 2) { if (!sourcePort || !targetPort) { message.warning('请先选择源端口和目标端口'); return; } if (sourcePort.portName === targetPort.portName && sourceDevice?.deviceId === targetDevice?.deviceId) { message.warning('源端口和目标端口不能相同'); return; } if (compatibilityWarning?.type === 'error') { message.error('端口类型不兼容,无法创建接线'); return; } } setCurrentStep(prev => prev + 1); }, [currentStep, sourceDevice, targetDevice, sourcePort, targetPort, sourcePorts, targetPorts, compatibilityWarning]); const handlePrevStep = useCallback(() => { setCurrentStep(prev => Math.max(0, prev - 1)); }, []); const handleSubmit = useCallback(async () => { try { setLoading(true); const payload = { cableId: cableLabel || `CABLE-${Date.now()}`, sourceDeviceId: sourceDevice.deviceId, sourcePort: sourcePort.portName, targetDeviceId: targetDevice.deviceId, targetPort: targetPort.portName, cableType: selectedCableType, cableLength: selectedCableLength, description: cableDescription, }; // 如果是编辑模式,使用 PUT 请求 if (editingCable) { await axios.put(`/api/cables/${editingCable.cableId}`, payload); message.success('接线更新成功'); } else { await axios.post('/api/cables', payload); message.success('接线创建成功'); } onSuccess?.(); onClose(); } catch (error) { console.error('操作接线失败:', error); if (error.response?.data?.conflict) { message.error('端口已被占用,请选择其他端口'); } else { message.error(editingCable ? '接线更新失败' : '接线创建失败'); } } finally { setLoading(false); } }, [ sourceDevice, sourcePort, targetDevice, targetPort, selectedCableType, selectedCableLength, cableLabel, cableDescription, editingCable, onSuccess, onClose, ]); const getAvailablePorts = useCallback( (ports, type) => { return ports.filter(port => { if (port.status !== 'free') return false; if (type === 'source') { return !conflicts.some(c => c.type === 'target' && c.port === port.portName); } else { return !conflicts.some(c => c.type === 'source' && c.port === port.portName); } }); }, [conflicts] ); const getRecommendedPorts = useCallback( (availablePorts, type) => { if (availablePorts.length === 0) return []; const sortedPorts = [...availablePorts].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); }); return sortedPorts.slice(0, 5); }, [] ); const renderStepContent = () => { switch (currentStep) { case 0: return ( ); case 1: return ( ); case 2: return ( ); case 3: return ( c.value === selectedCableType)} cableLength={selectedCableLength} cableLabel={cableLabel} cableDescription={cableDescription} compatibilityWarning={compatibilityWarning} /> ); default: return null; } }; const steps = [ { title: '源设备', icon: , description: '选择连接起点', }, { title: '目标设备', icon: , description: '选择连接终点', }, { title: '线缆配置', icon: , description: '设置线缆参数', }, { title: '预览确认', icon: , description: '确认创建接线', }, ]; return (
新增接线 向导式创建流程
} open={visible} closeIcon={} onCancel={onClose} width={1000} maskClosable={false} footer={null} >
{renderStepContent()}
{currentStep > 0 && ( )} {currentStep < steps.length - 1 ? ( ) : ( )}
); }; const Step1SourceDevice = ({ devices, fetchingDevices, onDeviceSearch, onDeviceSelect, sourceDevice, sourcePorts, sourcePort, onPortSelect, onDevicesChange, }) => { const isSourcePreSelected = !!sourceDevice; return (
步骤 1: 选择源设备端口 } size="small" style={{ marginBottom: '20px' }} > {!isSourcePreSelected && ( <>
选择接线的起点设备
{fetchingDevices ? (
) : ( devices.map(device => ( onDeviceSelect(device)} hoverable style={{ border: sourceDevice?.deviceId === device.deviceId ? `2px solid ${designTokens.colors.primary}` : '1px solid #d9d9d9', background: sourceDevice?.deviceId === device.deviceId ? 'rgba(24,144,255,0.08)' : '#fff', }} >
{device.type === 'server' ? '🖥️' : device.type === 'switch' ? '📡' : device.type === 'router' ? '🔀' : device.type === 'storage' ? '💾' : '📦'}
{device.name || device.deviceId}
{device.status && getStatusTag(device.status)}
ID: {device.deviceId} 类型: {device.type === 'server' ? '服务器' : device.type === 'switch' ? '交换机' : device.type === 'router' ? '路由器' : device.type === 'storage' ? '存储设备' : device.type}
{device.Rack?.Room?.name && ( 机房: {device.Rack.Room.name} )} {device.Rack?.name && ( 机柜: {device.Rack.name} )} {device.position && ( U位: U{device.position} )} {device.ipAddress && ( IP: {device.ipAddress} )}
{device.model && (
型号: {device.model}
)}
{sourceDevice?.deviceId === device.deviceId && ( )}
)) )}
)} {sourceDevice && (
源设备: {sourceDevice.name} {isSourcePreSelected && ( 已选择 )} {sourcePorts.filter(p => p.status === 'free').length} 个空闲端口
{sourcePorts.length === 0 ? ( ) : ( <>
端口总数: {sourcePorts.length} | 空闲端口: {sourcePorts.filter(p => p.status === 'free').length}
{sourcePort && (
✓ 已选择端口: {sourcePort.portName}
{sourcePort.portType && (
端口类型: {sourcePort.portType}
)} {sourcePort.portSpeed && (
端口速率: {sourcePort.portSpeed}
)}
)} )}
)}
); }; const Step2TargetDevice = ({ devices, fetchingDevices, onDeviceSearch, onDeviceSelect, targetDevice, targetPorts, targetPort, onPortSelect, conflicts, compatibilityWarning, onDevicesChange, }) => { return (
步骤 2: 选择目标设备 } size="small" style={{ marginBottom: '20px' }} >
选择接线的终点设备
{fetchingDevices ? (
) : ( devices.map(device => ( onDeviceSelect(device)} hoverable style={{ border: targetDevice?.deviceId === device.deviceId ? `2px solid ${designTokens.colors.primary}` : '1px solid #d9d9d9', background: targetDevice?.deviceId === device.deviceId ? 'rgba(24,144,255,0.08)' : '#fff', }} >
{device.type === 'server' ? '🖥️' : device.type === 'switch' ? '📡' : device.type === 'router' ? '🔀' : device.type === 'storage' ? '💾' : '📦'}
{device.name || device.deviceId}
{device.status && getStatusTag(device.status)}
ID: {device.deviceId} 类型: {device.type === 'server' ? '服务器' : device.type === 'switch' ? '交换机' : device.type === 'router' ? '路由器' : device.type === 'storage' ? '存储设备' : device.type}
{device.Rack?.Room?.name && ( 机房: {device.Rack.Room.name} )} {device.Rack?.name && ( 机柜: {device.Rack.name} )} {device.position && ( U位: U{device.position} )} {device.ipAddress && ( IP: {device.ipAddress} )}
{device.model && (
型号: {device.model}
)}
{targetDevice?.deviceId === device.deviceId && ( )}
)) )}
{targetDevice && (
目标设备已选择: {targetDevice.name}
端口选择 {targetPorts.filter(p => p.status === 'free').length} 个空闲端口
{targetPorts.length === 0 ? ( ) : ( <>
端口总数: {targetPorts.length} | 空闲端口: {targetPorts.filter(p => p.status === 'free').length}
端口状态分布: {targetPorts.map(p => p.status).filter((v, i, a) => a.indexOf(v) === i).join(', ')}
{targetPort && (
✓ 已选择端口: {targetPort.portName}
{targetPort.portType && (
端口类型: {targetPort.portType}
)} {targetPort.portSpeed && (
端口速率: {targetPort.portSpeed}
)}
)} )} {conflicts.length > 0 && (
⚠️ 端口冲突检测
{conflicts.map((conflict, index) => (
{conflict.type === 'source' ? '源端口' : '目标端口'} {conflict.port} 已被 {conflict.existingCable?.sourceDevice?.name} →{' '} {conflict.existingCable?.targetDevice?.name} 占用
))}
)} {compatibilityWarning && (
{compatibilityWarning.type === 'error' ? '❌ 端口类型不兼容' : '⚠️ 端口速率不匹配'}
{compatibilityWarning.message}
{compatibilityWarning.type === 'error' && (
请选择相同类型的端口(如 RJ45 电口只能连接 RJ45 电口)
)}
)}
)}
); }; const Step3CableConfig = ({ sourceDevice, sourcePort, targetDevice, targetPort, cableTypes, cableLengths, selectedCableType, selectedCableLength, onCableTypeChange, onCableLengthChange, cableLabel, cableDescription, setCableLabel, setCableDescription, }) => { const estimatedLength = useMemo(() => { if (!sourceDevice?.rackId || !targetDevice?.rackId) return 3; const distance = Math.abs(sourceDevice.rackId - targetDevice.rackId) * 0.5; return Math.max(3, Math.min(50, Math.round(distance + 3))); }, [sourceDevice, targetDevice]); return (
步骤 3: 线缆配置 } size="small" style={{ marginBottom: '20px' }} >
源设备
{sourceDevice?.name}
{sourcePort?.portName}
建议长度: {estimatedLength}m
目标设备
{targetDevice?.name}
{targetPort?.portName}
线缆类型
{cableTypes.map(type => (
onCableTypeChange(type.value)} style={{ padding: '12px 16px', borderRadius: '8px', border: `2px solid ${selectedCableType === type.value ? type.color : '#e8e8e8'}`, background: selectedCableType === type.value ? `${type.color}10` : '#fafafa', cursor: 'pointer', transition: 'all 0.2s ease', display: 'flex', alignItems: 'center', gap: '8px', }} > {type.icon}
{type.label}
{type.desc}
))}
线缆长度
{cableLengths.map(length => (
onCableLengthChange(length)} style={{ padding: '10px 20px', borderRadius: '20px', border: `2px solid ${selectedCableLength === length ? '#1890ff' : '#e8e8e8'}`, background: selectedCableLength === length ? '#e6f7ff' : '#fafafa', cursor: 'pointer', transition: 'all 0.2s ease', fontWeight: selectedCableLength === length ? 600 : 400, color: selectedCableLength === length ? '#1890ff' : '#595959', }} > {length}m
))}
线缆属性
线缆标签 setCableLabel(e.target.value)} style={{ width: '100%' }} />
备注说明 setCableDescription(e.target.value)} rows={3} style={{ width: '100%' }} />
选择建议
以太网线适用于1G/10G短距离连接 · 光纤适用于长距离或高带宽需求 · 铜缆适用于电源或特殊设备
); }; const Step4Preview = ({ sourceDevice, sourcePort, targetDevice, targetPort, cableType, cableLength, cableLabel, cableDescription, compatibilityWarning, }) => { return (
步骤 4: 预览确认 } size="small" style={{ marginBottom: '20px' }} >
{sourceDevice?.type === 'server' ? '🖥️' : sourceDevice?.type === 'switch' ? '📡' : '💾'}
{cableType?.icon}
{cableType?.label} {cableLength}m
{cableLabel || '自动生成'}
{targetDevice?.type === 'server' ? '🖥️' : targetDevice?.type === 'switch' ? '📡' : '💾'}
源端口
{sourcePort?.portName}
{sourceDevice?.name}
目标端口
{targetPort?.portName}
{targetDevice?.name}
{cableDescription && (
备注说明
{cableDescription}
)}
{compatibilityWarning?.type === 'error' ? ( <> ❌ 端口类型不兼容,无法创建接线 ) : compatibilityWarning?.type === 'warning' ? ( <> ⚠️ {compatibilityWarning.message} ) : ( <> ✅ 所有信息已确认,可以创建接线 )}
); }; export default CableWizardModal;