Files
yunrui_asset/frontend/src/components/CableWizardModal.jsx
T

1499 lines
52 KiB
React
Raw Normal View History

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 <Tag color={color} size="small">{text}</Tag>;
};
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 (
<Step1SourceDevice
devices={devices}
fetchingDevices={fetchingDevices}
onDeviceSearch={handleDeviceSearch}
onDeviceSelect={handleSourceDeviceSelect}
sourceDevice={sourceDevice}
sourcePorts={sourcePorts}
sourcePort={sourcePort}
onPortSelect={handleSourcePortSelect}
/>
);
case 1:
return (
<Step2TargetDevice
devices={devices}
fetchingDevices={fetchingDevices}
onDeviceSearch={handleDeviceSearch}
onDeviceSelect={handleTargetDeviceSelect}
targetDevice={targetDevice}
targetPorts={targetPorts}
targetPort={targetPort}
onPortSelect={handleTargetPortSelect}
conflicts={conflicts}
/>
);
case 2:
return (
<Step3CableConfig
sourceDevice={sourceDevice}
sourcePort={sourcePort}
targetDevice={targetDevice}
targetPort={targetPort}
cableTypes={CABLE_TYPES}
cableLengths={CABLE_LENGTHS}
selectedCableType={selectedCableType}
selectedCableLength={selectedCableLength}
onCableTypeChange={setSelectedCableType}
onCableLengthChange={setSelectedCableLength}
cableLabel={cableLabel}
cableDescription={cableDescription}
setCableLabel={setCableLabel}
setCableDescription={setCableDescription}
/>
);
case 3:
return (
<Step4Preview
sourceDevice={sourceDevice}
sourcePort={sourcePort}
targetDevice={targetDevice}
targetPort={targetPort}
cableType={CABLE_TYPES.find(c => c.value === selectedCableType)}
cableLength={selectedCableLength}
cableLabel={cableLabel}
cableDescription={cableDescription}
/>
);
default:
return null;
}
};
const steps = [
{
title: '源设备',
icon: <CloudServerOutlined />,
description: '选择连接起点',
},
{
title: '目标设备',
icon: <DatabaseOutlined />,
description: '选择连接终点',
},
{
title: '线缆配置',
icon: <SettingOutlined />,
description: '设置线缆参数',
},
{
title: '预览确认',
icon: <CheckCircleOutlined />,
description: '确认创建接线',
},
];
return (
<Modal
title={
<Space>
<SwapOutlined style={{ color: '#1890ff', fontSize: '24px' }} />
<div>
<Title level={3} style={{ margin: 0, lineHeight: 1.2 }}>
新增接线
</Title>
<Text type="secondary" style={{ fontSize: '12px' }}>
向导式创建流程
</Text>
</div>
</Space>
}
open={visible}
closeIcon={<CloseButton />}
onCancel={onClose}
width={1000}
maskClosable={false}
footer={null}
>
<div style={{ padding: '20px 0' }}>
<Steps
current={currentStep}
items={steps}
size="small"
responsive
onChange={setCurrentStep}
/>
</div>
<div style={{ minHeight: 400 }}>
<AnimatePresence mode="wait">
<motion.div
key={currentStep}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3, ease: [0.25, 0.46, 0.45, 0.94] }}
>
{renderStepContent()}
</motion.div>
</AnimatePresence>
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: '24px',
paddingTop: '20px',
borderTop: `1px solid ${designTokens.colors.border}`,
}}
>
<Button
onClick={onClose}
disabled={currentStep === 0}
style={{ width: 100 }}
>
取消
</Button>
<Space>
{currentStep > 0 && (
<Button onClick={handlePrevStep} style={{ width: 100 }}>
上一步
</Button>
)}
{currentStep < steps.length - 1 ? (
<Button
type="primary"
onClick={handleNextStep}
style={{
width: 100,
background: designTokens.colors.primary.gradient,
border: 'none',
}}
>
下一步
</Button>
) : (
<Button
type="primary"
onClick={handleSubmit}
loading={loading}
style={{
width: 120,
background: designTokens.colors.primary.gradient,
border: 'none',
}}
>
创建接线
</Button>
)}
</Space>
</div>
</Modal>
);
};
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 (
<div style={{ padding: '20px 0' }}>
<Card
title={
<Space>
<CloudServerOutlined style={{ color: '#1890ff' }} />
<span>步骤 1: 选择源设备</span>
</Space>
}
size="small"
style={{ marginBottom: '20px' }}
>
<div style={{ marginBottom: '16px' }}>
<Typography.Text type="secondary" style={{ fontSize: '13px' }}>
选择接线的起点设备
</Typography.Text>
</div>
<div style={{ marginBottom: '16px' }}>
<Input
placeholder="搜索设备名称或ID..."
prefix={<SearchOutlined />}
value={searchKeyword}
onChange={handleSearch}
style={{ width: '100%' }}
/>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: '12px',
maxHeight: 300,
overflowY: 'auto',
padding: '8px',
background: designTokens.colors.background,
borderRadius: '8px',
}}
>
{fetchingDevices ? (
<div style={{ textAlign: 'center', padding: '40px', gridColumn: '1 / -1' }}>
<Spin size="large" />
</div>
) : (
devices.map(device => (
<motion.div
key={device.deviceId}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Card
onClick={() => 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',
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
<div
style={{
width: '48px',
height: '48px',
borderRadius: '10px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px',
flexShrink: 0,
}}
>
{device.type === 'server' ? '🖥️' : device.type === 'switch' ? '📡' : device.type === 'router' ? '🔀' : device.type === 'storage' ? '💾' : '📦'}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
<div style={{ fontWeight: 600, color: '#262626', fontSize: '15px' }}>
{device.name || device.deviceId}
</div>
{device.status && getStatusTag(device.status)}
</div>
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
<span style={{ marginRight: '12px' }}>
<span style={{ color: '#bfbfbf', marginRight: '4px' }}>ID:</span>
{device.deviceId}
</span>
<span>
<span style={{ color: '#bfbfbf', marginRight: '4px' }}>类型:</span>
{device.type === 'server' ? '服务器' : device.type === 'switch' ? '交换机' : device.type === 'router' ? '路由器' : device.type === 'storage' ? '存储设备' : device.type}
</span>
</div>
<div style={{ fontSize: '11px', color: '#8c8c8c', lineHeight: '1.5' }}>
{device.Rack?.Room?.name && (
<span style={{ marginRight: '12px' }}>
<span style={{ color: '#bfbfbf', marginRight: '4px' }}>机房:</span>
{device.Rack.Room.name}
</span>
)}
{device.Rack?.name && (
<span style={{ marginRight: '12px' }}>
<span style={{ color: '#bfbfbf', marginRight: '4px' }}>机柜:</span>
{device.Rack.name}
</span>
)}
{device.position && (
<span style={{ marginRight: '12px' }}>
<span style={{ color: '#bfbfbf', marginRight: '4px' }}>U位:</span>
U{device.position}
</span>
)}
{device.ipAddress && (
<span>
<span style={{ color: '#bfbfbf', marginRight: '4px' }}>IP:</span>
{device.ipAddress}
</span>
)}
</div>
{device.model && (
<div style={{ fontSize: '11px', color: '#bfbfbf', marginTop: '4px' }}>
<span style={{ color: '#8c8c8c', marginRight: '4px' }}>型号:</span>
{device.model}
</div>
)}
</div>
{sourceDevice?.deviceId === device.deviceId && (
<CheckCircleOutlined style={{ color: designTokens.colors.primary, fontSize: '22px', flexShrink: 0 }} />
)}
</div>
</Card>
</motion.div>
))
)}
</div>
{sourceDevice && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
style={{ marginTop: '20px' }}
>
<div style={{ marginBottom: '12px' }}>
<Typography.Text style={{ fontWeight: 600, color: '#262626' }}>
源设备已选择: {sourceDevice.name}
</Typography.Text>
</div>
<div
style={{
background: '#f5f5f5',
borderRadius: '12px',
padding: '16px',
border: '1px solid #d9d9d9',
}}
>
<div style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<DatabaseOutlined style={{ color: '#1890ff' }} />
<Typography.Text style={{ fontWeight: 600 }}>端口选择</Typography.Text>
<Tag color="blue" style={{ marginLeft: 'auto' }}>
{sourcePorts.filter(p => p.status === 'free').length} 个空闲端口
</Tag>
</div>
{sourcePorts.length === 0 ? (
<Empty description="该设备暂无端口数据" />
) : (
<>
<div style={{ marginBottom: '8px', fontSize: '12px', color: '#8c8c8c' }}>
端口总数: {sourcePorts.length} | 空闲端口: {sourcePorts.filter(p => p.status === 'free').length}
</div>
<div style={{ marginBottom: '8px', fontSize: '11px', color: '#bfbfbf' }}>
端口状态分布: {sourcePorts.map(p => p.status).filter((v, i, a) => a.indexOf(v) === i).join(', ')}
</div>
{sourcePort && (
<div
style={{
marginBottom: '16px',
padding: '16px',
background: 'linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%)',
borderRadius: '12px',
border: '2px solid #1890ff',
display: 'flex',
alignItems: 'center',
gap: '12px',
boxShadow: '0 4px 12px rgba(24,144,255,0.15)',
}}
>
<CheckCircleOutlined
style={{
color: '#1890ff',
fontSize: '24px',
}}
/>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '15px', fontWeight: 700, color: '#096dd9', marginBottom: '4px' }}>
已选择端口: {sourcePort.portName}
</div>
{sourcePort.portType && (
<div style={{ fontSize: '12px', color: '#1890ff', fontWeight: 500 }}>
端口类型: {sourcePort.portType}
</div>
)}
{sourcePort.portSpeed && (
<div style={{ fontSize: '11px', color: '#40a9ff', marginTop: '2px' }}>
端口速率: {sourcePort.portSpeed}
</div>
)}
</div>
</div>
)}
<PortPanel
ports={sourcePorts}
deviceName={sourceDevice.name}
deviceId={sourceDevice.deviceId}
onPortClick={onPortSelect}
selectedPort={sourcePort}
compact
/>
</>
)}
</div>
</motion.div>
)}
</Card>
</div>
);
};
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 (
<div style={{ padding: '20px 0' }}>
<Card
title={
<Space>
<DatabaseOutlined style={{ color: '#1890ff' }} />
<span>步骤 2: 选择目标设备</span>
</Space>
}
size="small"
style={{ marginBottom: '20px' }}
>
<div style={{ marginBottom: '16px' }}>
<Typography.Text type="secondary" style={{ fontSize: '13px' }}>
选择接线的终点设备
</Typography.Text>
</div>
<div style={{ marginBottom: '16px' }}>
<Input
placeholder="搜索设备名称或ID..."
prefix={<SearchOutlined />}
value={searchKeyword}
onChange={handleSearch}
style={{ width: '100%' }}
/>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
gap: '12px',
maxHeight: 300,
overflowY: 'auto',
padding: '8px',
background: designTokens.colors.background,
borderRadius: '8px',
}}
>
{fetchingDevices ? (
<div style={{ textAlign: 'center', padding: '40px', gridColumn: '1 / -1' }}>
<Spin size="large" />
</div>
) : (
devices.map(device => (
<motion.div
key={device.deviceId}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Card
onClick={() => 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',
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
<div
style={{
width: '48px',
height: '48px',
borderRadius: '10px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px',
flexShrink: 0,
}}
>
{device.type === 'server' ? '🖥️' : device.type === 'switch' ? '📡' : device.type === 'router' ? '🔀' : device.type === 'storage' ? '💾' : '📦'}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
<div style={{ fontWeight: 600, color: '#262626', fontSize: '15px' }}>
{device.name || device.deviceId}
</div>
{device.status && getStatusTag(device.status)}
</div>
<div style={{ fontSize: '12px', color: '#8c8c8c', marginBottom: '4px' }}>
<span style={{ marginRight: '12px' }}>
<span style={{ color: '#bfbfbf', marginRight: '4px' }}>ID:</span>
{device.deviceId}
</span>
<span>
<span style={{ color: '#bfbfbf', marginRight: '4px' }}>类型:</span>
{device.type === 'server' ? '服务器' : device.type === 'switch' ? '交换机' : device.type === 'router' ? '路由器' : device.type === 'storage' ? '存储设备' : device.type}
</span>
</div>
<div style={{ fontSize: '11px', color: '#8c8c8c', lineHeight: '1.5' }}>
{device.Rack?.Room?.name && (
<span style={{ marginRight: '12px' }}>
<span style={{ color: '#bfbfbf', marginRight: '4px' }}>机房:</span>
{device.Rack.Room.name}
</span>
)}
{device.Rack?.name && (
<span style={{ marginRight: '12px' }}>
<span style={{ color: '#bfbfbf', marginRight: '4px' }}>机柜:</span>
{device.Rack.name}
</span>
)}
{device.position && (
<span style={{ marginRight: '12px' }}>
<span style={{ color: '#bfbfbf', marginRight: '4px' }}>U位:</span>
U{device.position}
</span>
)}
{device.ipAddress && (
<span>
<span style={{ color: '#bfbfbf', marginRight: '4px' }}>IP:</span>
{device.ipAddress}
</span>
)}
</div>
{device.model && (
<div style={{ fontSize: '11px', color: '#bfbfbf', marginTop: '4px' }}>
<span style={{ color: '#8c8c8c', marginRight: '4px' }}>型号:</span>
{device.model}
</div>
)}
</div>
{targetDevice?.deviceId === device.deviceId && (
<CheckCircleOutlined style={{ color: designTokens.colors.primary, fontSize: '22px', flexShrink: 0 }} />
)}
</div>
</Card>
</motion.div>
))
)}
</div>
{targetDevice && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
style={{ marginTop: '20px' }}
>
<div style={{ marginBottom: '12px' }}>
<Typography.Text style={{ fontWeight: 600, color: '#262626' }}>
目标设备已选择: {targetDevice.name}
</Typography.Text>
</div>
<div
style={{
background: '#f5f5f5',
borderRadius: '12px',
padding: '16px',
border: '1px solid #d9d9d9',
}}
>
<div style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '8px' }}>
<DatabaseOutlined style={{ color: '#1890ff' }} />
<Typography.Text style={{ fontWeight: 600 }}>端口选择</Typography.Text>
<Tag color="blue" style={{ marginLeft: 'auto' }}>
{targetPorts.filter(p => p.status === 'free').length} 个空闲端口
</Tag>
</div>
{targetPorts.length === 0 ? (
<Empty description="该设备暂无端口数据" />
) : (
<>
<div style={{ marginBottom: '8px', fontSize: '12px', color: '#8c8c8c' }}>
端口总数: {targetPorts.length} | 空闲端口: {targetPorts.filter(p => p.status === 'free').length}
</div>
<div style={{ marginBottom: '8px', fontSize: '11px', color: '#bfbfbf' }}>
端口状态分布: {targetPorts.map(p => p.status).filter((v, i, a) => a.indexOf(v) === i).join(', ')}
</div>
{targetPort && (
<div
style={{
marginBottom: '16px',
padding: '16px',
background: 'linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%)',
borderRadius: '12px',
border: '2px solid #1890ff',
display: 'flex',
alignItems: 'center',
gap: '12px',
boxShadow: '0 4px 12px rgba(24,144,255,0.15)',
}}
>
<CheckCircleOutlined
style={{
color: '#1890ff',
fontSize: '24px',
}}
/>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '15px', fontWeight: 700, color: '#096dd9', marginBottom: '4px' }}>
已选择端口: {targetPort.portName}
</div>
{targetPort.portType && (
<div style={{ fontSize: '12px', color: '#1890ff', fontWeight: 500 }}>
端口类型: {targetPort.portType}
</div>
)}
{targetPort.portSpeed && (
<div style={{ fontSize: '11px', color: '#40a9ff', marginTop: '2px' }}>
端口速率: {targetPort.portSpeed}
</div>
)}
</div>
</div>
)}
<PortPanel
ports={targetPorts}
deviceName={targetDevice.name}
deviceId={targetDevice.deviceId}
onPortClick={onPortSelect}
selectedPort={targetPort}
compact
/>
</>
)}
{conflicts.length > 0 && (
<div
style={{
marginTop: '16px',
padding: '12px',
background: 'rgba(239,68,68,0.1)',
borderRadius: '8px',
border: '1px solid rgba(239,68,68,0.3)',
}}
>
<Typography.Text type="danger" style={{ fontWeight: 600 }}>
⚠️ 端口冲突检测
</Typography.Text>
<div style={{ marginTop: '8px', fontSize: '12px' }}>
{conflicts.map((conflict, index) => (
<div key={index} style={{ marginBottom: '4px' }}>
<Tag color="error" style={{ marginRight: '8px' }}>
{conflict.type === 'source' ? '源端口' : '目标端口'}
</Tag>
<Text code>{conflict.port}</Text>
<Text type="secondary" style={{ marginLeft: '8px' }}>
已被 {conflict.existingCable?.sourceDevice?.name} {' '}
{conflict.existingCable?.targetDevice?.name} 占用
</Text>
</div>
))}
</div>
</div>
)}
</div>
</motion.div>
)}
</Card>
</div>
);
};
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 (
<div style={{ padding: '20px 0' }}>
<Card
title={
<Space>
<SettingOutlined style={{ color: '#1890ff' }} />
<span>步骤 3: 线缆配置</span>
</Space>
}
size="small"
style={{ marginBottom: '20px' }}
>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '24px',
}}
>
<div>
<Typography.Text style={{ fontWeight: 600, marginBottom: '12px', display: 'block' }}>
连接信息
</Typography.Text>
<div
style={{
background: '#f5f5f5',
borderRadius: '12px',
padding: '16px',
marginBottom: '16px',
border: '1px solid #d9d9d9',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '12px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>源设备</div>
<div style={{ fontWeight: 600, color: '#262626' }}>{sourceDevice?.name}</div>
<div style={{ fontSize: '11px', color: '#bfbfbf' }}>
端口: {sourcePort?.portName}
</div>
</div>
<ArrowRightOutlined style={{ color: '#1890ff' }} />
<div style={{ flex: 1 }}>
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>目标设备</div>
<div style={{ fontWeight: 600, color: '#262626' }}>{targetDevice?.name}</div>
<div style={{ fontSize: '11px', color: '#bfbfbf' }}>
端口: {targetPort?.portName}
</div>
</div>
</div>
<Divider style={{ margin: '8px 0' }} />
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>线缆类型</div>
<div style={{ marginTop: '8px' }}>
{cableTypes.map(type => (
<Tag
key={type.value}
color={type.color}
style={{
cursor: 'pointer',
marginRight: '8px',
marginBottom: '8px',
border: selectedCableType === type.value
? `2px solid ${type.color}`
: '1px solid #d9d9d9',
}}
onClick={() => onCableTypeChange(type.value)}
>
{type.icon} {type.label}
</Tag>
))}
</div>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>线缆长度</div>
<div style={{ marginTop: '8px' }}>
{cableLengths.map(length => (
<Tag
key={length}
color={selectedCableLength === length ? 'blue' : 'default'}
style={{
cursor: 'pointer',
marginRight: '8px',
marginBottom: '8px',
border: selectedCableLength === length
? '2px solid #1890ff'
: '1px solid #d9d9d9',
}}
onClick={() => onCableLengthChange(length)}
>
{length}m
</Tag>
))}
</div>
</div>
</div>
</div>
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>
<InfoCircleOutlined style={{ marginRight: '4px' }} />
建议长度: <Text code>{estimatedLength}m</Text>
</div>
</div>
<div>
<Typography.Text style={{ fontWeight: 600, marginBottom: '12px', display: 'block' }}>
线缆属性
</Typography.Text>
<div
style={{
background: '#f5f5f5',
borderRadius: '12px',
padding: '16px',
border: '1px solid #d9d9d9',
}}
>
<div style={{ marginBottom: '16px' }}>
<Typography.Text style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}>
线缆标签
</Typography.Text>
<Input
placeholder="自动生成或手动输入标签"
value={cableLabel}
onChange={e => setCableLabel(e.target.value)}
style={{ width: '100%' }}
/>
<div style={{ fontSize: '11px', color: '#bfbfbf', marginTop: '4px' }}>
示例: CABLE-{sourceDevice?.deviceId.slice(-4)}-{targetDevice?.deviceId.slice(-4)}-{Date.now().toString().slice(-4)}
</div>
</div>
<div>
<Typography.Text style={{ fontSize: '13px', display: 'block', marginBottom: '8px' }}>
备注说明
</Typography.Text>
<Input.TextArea
placeholder="添加接线说明..."
value={cableDescription}
onChange={e => setCableDescription(e.target.value)}
rows={6}
style={{ width: '100%' }}
/>
</div>
</div>
<div
style={{
marginTop: '16px',
padding: '12px',
background: 'rgba(24,144,255,0.1)',
borderRadius: '8px',
border: '1px solid rgba(24,144,255,0.3)',
}}
>
<Typography.Text style={{ fontWeight: 600, color: '#1890ff' }}>
💡 提示
</Typography.Text>
<div style={{ fontSize: '12px', marginTop: '8px', lineHeight: '1.6' }}>
以太网线Cat6: 适用于1G/10G短距离连接
<br />
光纤SMF/MMF: 适用于长距离或高带宽需求
<br />
铜缆: 适用于电源或特殊设备连接
</div>
</div>
</div>
</div>
</Card>
</div>
);
};
const Step4Preview = ({
sourceDevice,
sourcePort,
targetDevice,
targetPort,
cableType,
cableLength,
cableLabel,
cableDescription,
}) => {
return (
<div style={{ padding: '20px 0' }}>
<Card
title={
<Space>
<CheckCircleOutlined style={{ color: '#52c41a' }} />
<span>步骤 4: 预览确认</span>
</Space>
}
size="small"
style={{ marginBottom: '20px' }}
>
<div
style={{
background: '#f5f5f5',
borderRadius: '16px',
padding: '24px',
border: '1px solid #d9d9d9',
}}
>
<div style={{ textAlign: 'center', marginBottom: '24px' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '24px',
marginBottom: '24px',
}}
>
<div
style={{
width: '80px',
height: '80px',
borderRadius: '16px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '32px',
boxShadow: '0 10px 30px rgba(0,0,0,0.3)',
}}
>
{sourceDevice?.type === 'server' ? '🖥️' : sourceDevice?.type === 'switch' ? '📡' : '💾'}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div
style={{
width: '40px',
height: '40px',
borderRadius: '50%',
background: cableType?.color + '30',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
}}
>
{cableType?.icon}
</div>
<div>
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>
{cableType?.label} {cableLength}m
</div>
<div style={{ fontSize: '11px', color: '#bfbfbf' }}>
{cableLabel || '自动生成'}
</div>
</div>
</div>
<div
style={{
width: '80px',
height: '80px',
borderRadius: '16px',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '32px',
boxShadow: '0 10px 30px rgba(0,0,0,0.3)',
}}
>
{targetDevice?.type === 'server' ? '🖥️' : targetDevice?.type === 'switch' ? '📡' : '💾'}
</div>
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '16px',
}}
>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>源端口</div>
<div style={{ fontWeight: 600, color: '#52c41a' }}>{sourcePort?.portName}</div>
<div style={{ fontSize: '11px', color: '#bfbfbf' }}>
{sourceDevice?.name}
</div>
</div>
<ArrowRightOutlined style={{ color: '#1890ff' }} />
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>目标端口</div>
<div style={{ fontWeight: 600, color: '#1890ff' }}>{targetPort?.portName}</div>
<div style={{ fontSize: '11px', color: '#bfbfbf' }}>
{targetDevice?.name}
</div>
</div>
</div>
</div>
{cableDescription && (
<div
style={{
padding: '16px',
background: '#fff',
borderRadius: '8px',
marginBottom: '16px',
border: '1px solid #d9d9d9',
}}
>
<Typography.Text style={{ fontSize: '13px', display: 'block', marginBottom: '8px', color: '#262626' }}>
备注说明
</Typography.Text>
<div style={{ fontSize: '14px', color: '#595959' }}>
{cableDescription}
</div>
</div>
)}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '12px',
background: 'rgba(82,196,26,0.1)',
borderRadius: '8px',
border: '1px solid rgba(82,196,26,0.3)',
}}
>
<CheckCircleOutlined style={{ color: '#52c41a' }} />
<Typography.Text style={{ fontWeight: 600, color: '#52c41a' }}>
所有信息已确认可以创建接线
</Typography.Text>
</div>
</div>
</Card>
</div>
);
};
export default CableWizardModal;