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 { designTokens } from '../config/theme'; const { Title, Text } = Typography; const CABLE_TYPES = [ { value: 'ethernet', label: '以太网线', color: '#52c41a', icon: '🌐' }, { value: 'fiber', label: '光纤', color: '#1890ff', icon: '🔦' }, { value: 'copper', label: '铜缆', 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([]); // 初始化编辑模式的数据 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 debounce = (fn, delay) => { let timer = null; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }; const handleDeviceSearch = useCallback( debounce((value) => { fetchDevices(value, 'switch'); }, 300), [fetchDevices] ); useEffect(() => { if (visible) { setCurrentStep(0); setSourceDevice(null); setTargetDevice(null); setSourcePort(null); setTargetPort(null); setSelectedCableType('ethernet'); setSelectedCableLength(3); setCableLabel(''); setCableDescription(''); setSourcePorts([]); setTargetPorts([]); setConflicts([]); setDevices([]); setCablesData([]); fetchDevices('', 'switch').then(deviceList => { if (initialSourceDevice?.deviceId) { const device = deviceList.find(d => d.deviceId === initialSourceDevice.deviceId); if (device) { setSourceDevice(device); fetchDevicePorts(device.deviceId, 'source'); } } }); 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([]); } else { // 只有在选择新端口时才进行设备相同性检查 if (sourceDevice?.deviceId === targetDevice?.deviceId) { message.warning('源设备和目标设备不能相同'); 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([]); } else { // 只有在选择新端口时才进行设备相同性检查 if (sourceDevice?.deviceId === targetDevice?.deviceId) { message.warning('源设备和目标设备不能相同'); return; } setTargetPort(port); console.log('Target port set to:', port); await checkPortConflict(sourceDevice.deviceId, port.portName); } }, [sourceDevice, targetDevice, targetPort, checkPortConflict]); 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; } } else if (currentStep === 2) { if (!sourcePort || !targetPort) { message.warning('请先选择源端口和目标端口'); return; } if (sourcePort.portName === targetPort.portName && sourceDevice?.deviceId === targetDevice?.deviceId) { message.warning('源端口和目标端口不能相同'); return; } } setCurrentStep(prev => prev + 1); }, [currentStep, sourceDevice, targetDevice, sourcePort, targetPort, sourcePorts, targetPorts]); 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} /> ); 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, }) => { const [searchKeyword, setSearchKeyword] = useState(''); const handleSearch = (e) => { const value = e.target.value; setSearchKeyword(value); onDeviceSearch?.(value); }; return (
步骤 1: 选择源设备 } size="small" style={{ marginBottom: '20px' }} >
选择接线的起点设备
} value={searchKeyword} onChange={handleSearch} style={{ width: '100%' }} />
{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}
端口选择 {sourcePorts.filter(p => p.status === 'free').length} 个空闲端口
{sourcePorts.length === 0 ? ( ) : ( <>
端口总数: {sourcePorts.length} | 空闲端口: {sourcePorts.filter(p => p.status === 'free').length}
端口状态分布: {sourcePorts.map(p => p.status).filter((v, i, a) => a.indexOf(v) === i).join(', ')}
{sourcePort && (
✓ 已选择端口: {sourcePort.portName}
{sourcePort.portType && (
端口类型: {sourcePort.portType}
)} {sourcePort.portSpeed && (
端口速率: {sourcePort.portSpeed}
)}
)} )}
)}
); }; const Step2TargetDevice = ({ devices, fetchingDevices, onDeviceSearch, onDeviceSelect, targetDevice, targetPorts, targetPort, onPortSelect, conflicts, }) => { const [searchKeyword, setSearchKeyword] = useState(''); const handleSearch = (e) => { const value = e.target.value; setSearchKeyword(value); onDeviceSearch?.(value); }; return (
步骤 2: 选择目标设备 } size="small" style={{ marginBottom: '20px' }} >
选择接线的终点设备
} value={searchKeyword} onChange={handleSearch} style={{ width: '100%' }} />
{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} 占用
))}
)}
)}
); }; 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}
目标设备
{targetDevice?.name}
端口: {targetPort?.portName}
线缆类型
{cableTypes.map(type => ( onCableTypeChange(type.value)} > {type.icon} {type.label} ))}
线缆长度
{cableLengths.map(length => ( onCableLengthChange(length)} > {length}m ))}
建议长度: {estimatedLength}m
线缆属性
线缆标签 setCableLabel(e.target.value)} style={{ width: '100%' }} />
示例: CABLE-{sourceDevice?.deviceId.slice(-4)}-{targetDevice?.deviceId.slice(-4)}-{Date.now().toString().slice(-4)}
备注说明 setCableDescription(e.target.value)} rows={6} style={{ width: '100%' }} />
💡 提示
• 以太网线(Cat6): 适用于1G/10G短距离连接
• 光纤(SMF/MMF): 适用于长距离或高带宽需求
• 铜缆: 适用于电源或特殊设备连接
); }; const Step4Preview = ({ sourceDevice, sourcePort, targetDevice, targetPort, cableType, cableLength, cableLabel, cableDescription, }) => { 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}
)}
✅ 所有信息已确认,可以创建接线
); }; export default CableWizardModal;