feat: 端口管理页面添加网卡功能和服务器背板可视化

- 在端口管理页面添加网卡管理功能,与3D可视化页面同步
- 新增 ServerBackplanePanel 组件,按真实服务器背板布局展示网卡和端口
- 支持板载网卡、管理口、PCIe扩展插槽的可视化展示
- 添加网卡-端口层级展示,点击网卡可查看端口详情
- 更新 VirtualDeviceList 组件,集成服务器背板视图
This commit is contained in:
zhang1106
2026-01-30 17:53:05 +08:00
parent 5e3ac98bac
commit bc2f17890d
8 changed files with 2149 additions and 97 deletions
+211 -40
View File
@@ -73,16 +73,17 @@ function CableManagement() {
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 });
setCables(response.data.cables || []);
const cablesData = response.data.cables || [];
setCables(cablesData);
const grouped = {};
response.data.cables.forEach(cable => {
cablesData.forEach(cable => {
const switchId = cable.sourceDeviceId;
if (!grouped[switchId]) {
grouped[switchId] = {
@@ -93,22 +94,36 @@ function CableManagement() {
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]);
}, [filters, devicePorts]);
const fetchDevices = useCallback(async () => {
try {
const response = await axios.get('/api/devices', { params: { pageSize: 1000 } });
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);
}
}, []);
@@ -189,27 +204,87 @@ function CableManagement() {
}
};
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('更新成功');
} else {
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;
}
}
setModalVisible(false);
form.resetFields();
fetchCables();
} 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([]);
@@ -562,9 +637,12 @@ function CableManagement() {
onChange={(value) => setFilters(prev => ({ ...prev, switchDeviceId: value }))}
allowClear
showSearch
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
filterOption={(input, option) => {
const device = switchDevices.find(d => d.deviceId === option.value);
if (!device) return false;
const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
return searchText.indexOf(input.toLowerCase()) >= 0;
}}
>
{switchDevices.map(device => (
<Option key={device.deviceId} value={device.deviceId}>
@@ -572,7 +650,7 @@ function CableManagement() {
</Option>
))}
</Select>
<Select
placeholder="线缆类型"
style={{ width: 120 }}
@@ -762,12 +840,15 @@ function CableManagement() {
label="源设备"
rules={[{ required: true, message: '请选择源设备' }]}
>
<Select
placeholder="请选择源设备"
<Select
placeholder="请选择源设备"
showSearch
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
filterOption={(input, option) => {
const device = switchDevices.find(d => d.deviceId === option.value);
if (!device) return false;
const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
return searchText.indexOf(input.toLowerCase()) >= 0;
}}
onChange={(value) => {
fetchDevicePorts(value);
form.setFieldsValue({ sourcePort: undefined });
@@ -780,18 +861,22 @@ function CableManagement() {
))}
</Select>
</Form.Item>
<Form.Item
name="sourcePort"
label="源设备端口"
rules={[{ required: true, message: '请选择源设备端口' }]}
>
<Select
placeholder="请先选择源设备"
<Select
placeholder="请先选择源设备"
showSearch
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
filterOption={(input, option) => {
const ports = devicePorts[form.getFieldValue('sourceDeviceId')] || [];
const port = ports.find(p => p.portName === option.value);
if (!port) return false;
const searchText = `${port.portName} ${port.portType} ${port.portSpeed}`.toLowerCase();
return searchText.indexOf(input.toLowerCase()) >= 0;
}}
disabled={!form.getFieldValue('sourceDeviceId')}
>
{(devicePorts[form.getFieldValue('sourceDeviceId')] || []).map(port => (
@@ -801,18 +886,21 @@ function CableManagement() {
))}
</Select>
</Form.Item>
<Form.Item
name="targetDeviceId"
label="目标设备"
rules={[{ required: true, message: '请选择目标设备' }]}
>
<Select
<Select
placeholder="请选择目标设备"
showSearch
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
filterOption={(input, option) => {
const device = devices.find(d => d.deviceId === option.value);
if (!device) return false;
const searchText = `${device.name} ${device.deviceId}`.toLowerCase();
return searchText.indexOf(input.toLowerCase()) >= 0;
}}
onChange={(value) => {
fetchDevicePorts(value);
form.setFieldsValue({ targetPort: undefined });
@@ -825,18 +913,22 @@ function CableManagement() {
))}
</Select>
</Form.Item>
<Form.Item
name="targetPort"
label="目标设备端口"
rules={[{ required: true, message: '请选择目标设备端口' }]}
>
<Select
placeholder="请先选择目标设备"
<Select
placeholder="请先选择目标设备"
showSearch
filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
filterOption={(input, option) => {
const ports = devicePorts[form.getFieldValue('targetDeviceId')] || [];
const port = ports.find(p => p.portName === option.value);
if (!port) return false;
const searchText = `${port.portName} ${port.portType} ${port.portSpeed}`.toLowerCase();
return searchText.indexOf(input.toLowerCase()) >= 0;
}}
disabled={!form.getFieldValue('targetDeviceId')}
>
{(devicePorts[form.getFieldValue('targetDeviceId')] || []).map(port => (
@@ -846,7 +938,7 @@ function CableManagement() {
))}
</Select>
</Form.Item>
<Form.Item
name="cableType"
label="线缆类型"
@@ -1052,6 +1144,85 @@ function CableManagement() {
)}
</div>
</Modal>
{/* 冲突提示弹窗 */}
<Modal
title="端口冲突警告"
open={conflictModalVisible}
onCancel={() => {
setConflictModalVisible(false);
setConflictInfo(null);
setPendingSubmitValues(null);
}}
footer={[
<Button
key="cancel"
onClick={() => {
setConflictModalVisible(false);
setConflictInfo(null);
setPendingSubmitValues(null);
}}
>
取消
</Button>,
<Button
key="force"
type="primary"
danger
onClick={handleForceSubmit}
>
强制接管
</Button>
]}
width={600}
>
{conflictInfo && (
<div>
<div style={{ marginBottom: 16, color: '#ef4444', fontWeight: 500 }}>
<span style={{ fontSize: 20, marginRight: 8 }}></span>
检测到端口冲突以下端口已被占用
</div>
{conflictInfo.map((conflict, index) => (
<Card
key={index}
size="small"
style={{ marginBottom: 12, background: '#fef2f2', border: '1px solid #fecaca' }}
>
<div style={{ marginBottom: 8 }}>
<Tag color="error">
{conflict.type === 'source' ? '源端口' : conflict.type === 'target' ? '目标端口' : '端口'}
</Tag>
<span style={{ fontWeight: 500, marginLeft: 8 }}>{conflict.port}</span>
</div>
{conflict.existingCable && (
<div style={{ fontSize: 13, color: '#666' }}>
<div>当前连接</div>
<div style={{ marginTop: 4, paddingLeft: 12 }}>
<div>
源设备{conflict.existingCable.sourceDevice?.name || conflict.existingCable.sourceDeviceId}
({conflict.existingCable.sourcePort})
</div>
<div style={{ marginTop: 2 }}>
目标设备{conflict.existingCable.targetDevice?.name || conflict.existingCable.targetDeviceId}
({conflict.existingCable.targetPort})
</div>
<div style={{ marginTop: 2 }}>
线缆类型{getCableTypeTag(conflict.existingCable.cableType)}
</div>
</div>
</div>
)}
</Card>
))}
<div style={{ marginTop: 16, padding: 12, background: '#fff7ed', borderRadius: 6, border: '1px solid #fed7aa' }}>
<span style={{ color: '#ea580c' }}>💡</span>
<span style={{ marginLeft: 8, color: '#9a3412' }}>
点击"强制接管"将断开原有连接并创建新接线此操作不可恢复
</span>
</div>
</div>
)}
</Modal>
</div>
);
}