2026-01-23 08:55:16 +08:00
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
2026-02-10 11:04:01 +08:00
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
Button,
|
|
|
|
|
Space,
|
|
|
|
|
Tag,
|
|
|
|
|
Tooltip,
|
|
|
|
|
Popconfirm,
|
|
|
|
|
Empty,
|
|
|
|
|
Spin,
|
|
|
|
|
Badge,
|
|
|
|
|
Collapse,
|
|
|
|
|
Card,
|
|
|
|
|
} from 'antd';
|
|
|
|
|
import {
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
ApiOutlined,
|
|
|
|
|
CloudServerOutlined,
|
|
|
|
|
FolderOutlined,
|
|
|
|
|
} from '@ant-design/icons';
|
2026-03-16 17:07:20 +08:00
|
|
|
import api from '../api';
|
2026-01-23 08:55:16 +08:00
|
|
|
import PortCreateModal from './PortCreateModal';
|
|
|
|
|
import NetworkCardCreateModal from './NetworkCardCreateModal';
|
2026-03-10 15:49:02 +08:00
|
|
|
import { designTokens } from '../config/theme';
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
|
|
|
const { Panel } = Collapse;
|
|
|
|
|
|
2026-01-26 17:21:20 +08:00
|
|
|
function NetworkCardPanel({ deviceId, deviceName, onRefresh, refreshTrigger }) {
|
2026-01-23 08:55:16 +08:00
|
|
|
const [cards, setCards] = useState([]);
|
|
|
|
|
const [networkCards, setNetworkCards] = useState([]);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [createPortModalVisible, setCreatePortModalVisible] = useState(false);
|
|
|
|
|
const [createCardModalVisible, setCreateCardModalVisible] = useState(false);
|
|
|
|
|
const [selectedCard, setSelectedCard] = useState(null);
|
|
|
|
|
const [expandedCards, setExpandedCards] = useState([]);
|
|
|
|
|
|
|
|
|
|
const fetchData = useCallback(async () => {
|
|
|
|
|
if (!deviceId) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
const [cardsResponse, networkCardsResponse] = await Promise.all([
|
2026-03-16 17:07:20 +08:00
|
|
|
api.get(`/network-cards/device/${deviceId}/with-ports`),
|
|
|
|
|
api.get(`/network-cards/device/${deviceId}`),
|
2026-01-23 08:55:16 +08:00
|
|
|
]);
|
|
|
|
|
|
2026-03-16 17:07:20 +08:00
|
|
|
const cardsData = cardsResponse.data || cardsResponse || [];
|
2026-01-23 08:55:16 +08:00
|
|
|
setCards(cardsData);
|
2026-03-16 17:07:20 +08:00
|
|
|
setNetworkCards(networkCardsResponse.data || networkCardsResponse || []);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
|
|
|
const initialExpanded = cardsData
|
|
|
|
|
.filter(card => card.ports && card.ports.length > 0)
|
|
|
|
|
.map(card => card.nicId);
|
|
|
|
|
setExpandedCards(initialExpanded);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取网卡数据失败:', error);
|
|
|
|
|
setCards([]);
|
|
|
|
|
setNetworkCards([]);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, [deviceId]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchData();
|
2026-01-26 17:21:20 +08:00
|
|
|
}, [fetchData, refreshTrigger]);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
const handleDeleteCard = useCallback(
|
|
|
|
|
async card => {
|
|
|
|
|
try {
|
2026-03-16 17:07:20 +08:00
|
|
|
await api.delete(`/network-cards/${card.nicId}`);
|
2026-02-10 11:04:01 +08:00
|
|
|
import('antd').then(({ message }) => message.success('网卡删除成功'));
|
|
|
|
|
fetchData();
|
|
|
|
|
onRefresh?.();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
import('antd').then(({ message }) =>
|
|
|
|
|
message.error(error.response?.data?.error || '网卡删除失败')
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[fetchData, onRefresh]
|
|
|
|
|
);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
const handleDeletePort = useCallback(
|
|
|
|
|
async port => {
|
|
|
|
|
try {
|
2026-03-16 17:07:20 +08:00
|
|
|
await api.delete(`/device-ports/${port.portId}`);
|
2026-02-10 11:04:01 +08:00
|
|
|
import('antd').then(({ message }) => message.success('端口删除成功'));
|
|
|
|
|
fetchData();
|
|
|
|
|
onRefresh?.();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
import('antd').then(({ message }) => message.error('端口删除失败'));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[fetchData, onRefresh]
|
|
|
|
|
);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
|
|
|
const handleCreateCardSuccess = useCallback(() => {
|
|
|
|
|
fetchData();
|
|
|
|
|
onRefresh?.();
|
|
|
|
|
}, [fetchData, onRefresh]);
|
|
|
|
|
|
|
|
|
|
const handleCreatePortSuccess = useCallback(() => {
|
|
|
|
|
fetchData();
|
|
|
|
|
onRefresh?.();
|
|
|
|
|
}, [fetchData, onRefresh]);
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
const handleExpand = nicId => {
|
2026-01-23 08:55:16 +08:00
|
|
|
setExpandedCards(prev => {
|
|
|
|
|
if (prev.includes(nicId)) {
|
|
|
|
|
return prev.filter(id => id !== nicId);
|
|
|
|
|
}
|
|
|
|
|
return [...prev, nicId];
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
const getStatusTag = status => {
|
2026-01-23 08:55:16 +08:00
|
|
|
const config = {
|
|
|
|
|
free: { color: 'success', text: '空闲' },
|
|
|
|
|
occupied: { color: 'processing', text: '占用' },
|
|
|
|
|
fault: { color: 'error', text: '故障' },
|
|
|
|
|
normal: { color: 'success', text: '正常' },
|
|
|
|
|
warning: { color: 'warning', text: '警告' },
|
2026-02-10 11:04:01 +08:00
|
|
|
offline: { color: 'default', text: '离线' },
|
2026-01-23 08:55:16 +08:00
|
|
|
};
|
|
|
|
|
const { color, text } = config[status] || { color: 'default', text: status };
|
|
|
|
|
return <Tag color={color}>{text}</Tag>;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
const getTypeTag = type => {
|
2026-01-23 08:55:16 +08:00
|
|
|
const config = {
|
2026-02-10 11:04:01 +08:00
|
|
|
RJ45: { color: 'blue', text: 'RJ45' },
|
|
|
|
|
SFP: { color: 'green', text: 'SFP' },
|
2026-01-23 08:55:16 +08:00
|
|
|
'SFP+': { color: 'cyan', text: 'SFP+' },
|
2026-02-10 11:04:01 +08:00
|
|
|
SFP28: { color: 'purple', text: 'SFP28' },
|
|
|
|
|
QSFP: { color: 'orange', text: 'QSFP' },
|
|
|
|
|
QSFP28: { color: 'red', text: 'QSFP28' },
|
2026-01-23 08:55:16 +08:00
|
|
|
};
|
|
|
|
|
const { color, text } = config[type] || { color: 'default', text: type };
|
|
|
|
|
return <Tag color={color}>{text}</Tag>;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderPortTable = (ports, nicId) => {
|
|
|
|
|
const columns = [
|
|
|
|
|
{
|
|
|
|
|
title: '端口名称',
|
|
|
|
|
dataIndex: 'portName',
|
|
|
|
|
key: 'portName',
|
|
|
|
|
width: 120,
|
2026-02-10 11:04:01 +08:00
|
|
|
render: text => <span style={{ fontWeight: 500 }}>{text}</span>,
|
2026-01-23 08:55:16 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '类型',
|
|
|
|
|
dataIndex: 'portType',
|
|
|
|
|
key: 'portType',
|
|
|
|
|
width: 80,
|
2026-02-10 11:04:01 +08:00
|
|
|
render: type => getTypeTag(type),
|
2026-01-23 08:55:16 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '速率',
|
|
|
|
|
dataIndex: 'portSpeed',
|
|
|
|
|
key: 'portSpeed',
|
2026-02-10 11:04:01 +08:00
|
|
|
width: 70,
|
2026-01-23 08:55:16 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '状态',
|
|
|
|
|
dataIndex: 'status',
|
|
|
|
|
key: 'status',
|
|
|
|
|
width: 70,
|
2026-02-10 11:04:01 +08:00
|
|
|
render: status => getStatusTag(status),
|
2026-01-23 08:55:16 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'VLAN',
|
|
|
|
|
dataIndex: 'vlanId',
|
|
|
|
|
key: 'vlanId',
|
|
|
|
|
width: 60,
|
2026-02-10 11:04:01 +08:00
|
|
|
render: vlanId => vlanId || '-',
|
2026-01-23 08:55:16 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '操作',
|
|
|
|
|
key: 'action',
|
|
|
|
|
width: 80,
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
<Space size="small">
|
|
|
|
|
<Popconfirm
|
|
|
|
|
title="确定要删除此端口吗?"
|
|
|
|
|
onConfirm={() => handleDeletePort(record)}
|
|
|
|
|
okText="确定"
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
>
|
|
|
|
|
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
|
|
|
|
删除
|
|
|
|
|
</Button>
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
</Space>
|
2026-02-10 11:04:01 +08:00
|
|
|
),
|
|
|
|
|
},
|
2026-01-23 08:55:16 +08:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Table
|
|
|
|
|
columns={columns}
|
|
|
|
|
dataSource={ports}
|
|
|
|
|
rowKey="portId"
|
|
|
|
|
pagination={false}
|
|
|
|
|
size="small"
|
|
|
|
|
scroll={{ x: 500 }}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
const renderCardHeader = card => {
|
2026-01-23 08:55:16 +08:00
|
|
|
const stats = card.stats || { free: 0, occupied: 0, fault: 0, total: 0 };
|
2026-02-10 11:04:01 +08:00
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
return (
|
2026-02-10 11:04:01 +08:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
width: '100%',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-01-23 08:55:16 +08:00
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
width: '36px',
|
|
|
|
|
height: '36px',
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
background: card.isUngrouped
|
|
|
|
|
? 'linear-gradient(135deg, #94a3b8 0%, #64748b 100%)'
|
|
|
|
|
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
color: '#fff',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-01-23 08:55:16 +08:00
|
|
|
{card.isUngrouped ? <FolderOutlined /> : <CloudServerOutlined />}
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div style={{ fontWeight: 600, fontSize: '14px', color: '#1e293b' }}>
|
|
|
|
|
{card.name}
|
2026-02-10 11:04:01 +08:00
|
|
|
{card.slotNumber && (
|
|
|
|
|
<span style={{ color: '#94a3b8', marginLeft: 8 }}>插槽 {card.slotNumber}</span>
|
|
|
|
|
)}
|
2026-01-23 08:55:16 +08:00
|
|
|
</div>
|
|
|
|
|
<div style={{ fontSize: '12px', color: '#64748b' }}>
|
|
|
|
|
{card.description || (card.isUngrouped ? '未分配到网卡的端口' : '网卡')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Space size={12}>
|
|
|
|
|
<Badge count={stats.free} style={{ backgroundColor: designTokens.colors.success }} />
|
|
|
|
|
<span style={{ fontSize: '12px', color: '#64748b' }}>空闲</span>
|
|
|
|
|
<Badge count={stats.occupied} style={{ backgroundColor: '#1677ff' }} />
|
|
|
|
|
<span style={{ fontSize: '12px', color: '#64748b' }}>占用</span>
|
|
|
|
|
<Badge count={stats.fault} style={{ backgroundColor: designTokens.colors.error }} />
|
|
|
|
|
<span style={{ fontSize: '12px', color: '#64748b' }}>故障</span>
|
|
|
|
|
{!card.isUngrouped && (
|
|
|
|
|
<Popconfirm
|
|
|
|
|
title="确定要删除此网卡吗?"
|
|
|
|
|
description="删除网卡前需确保其下无端口"
|
|
|
|
|
onConfirm={() => handleDeleteCard(card)}
|
|
|
|
|
okText="确定"
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
>
|
|
|
|
|
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
|
|
|
|
删除网卡
|
|
|
|
|
</Button>
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
)}
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
size="small"
|
|
|
|
|
icon={<PlusOutlined />}
|
2026-02-10 11:04:01 +08:00
|
|
|
onClick={e => {
|
2026-01-23 08:55:16 +08:00
|
|
|
e.stopPropagation();
|
|
|
|
|
setSelectedCard(card);
|
|
|
|
|
setCreatePortModalVisible(true);
|
|
|
|
|
}}
|
|
|
|
|
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
|
|
|
|
>
|
|
|
|
|
添加端口
|
|
|
|
|
</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
|
|
|
|
<Spin size="large" tip="加载网卡数据中..." />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
const totalStats = cards.reduce(
|
|
|
|
|
(acc, card) => {
|
|
|
|
|
const stats = card.stats || {};
|
|
|
|
|
acc.total += stats.total || 0;
|
|
|
|
|
acc.free += stats.free || 0;
|
|
|
|
|
acc.occupied += stats.occupied || 0;
|
|
|
|
|
acc.fault += stats.fault || 0;
|
|
|
|
|
return acc;
|
|
|
|
|
},
|
|
|
|
|
{ total: 0, free: 0, occupied: 0, fault: 0 }
|
|
|
|
|
);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="network-card-panel">
|
2026-02-10 11:04:01 +08:00
|
|
|
<div
|
|
|
|
|
className="panel-header"
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-01-23 08:55:16 +08:00
|
|
|
<div className="stats" style={{ display: 'flex', gap: '24px' }}>
|
|
|
|
|
<Space size={16}>
|
2026-02-10 11:04:01 +08:00
|
|
|
<Badge
|
|
|
|
|
count={networkCards.length}
|
|
|
|
|
style={{ backgroundColor: designTokens.colors.primary.main }}
|
|
|
|
|
/>
|
2026-01-23 08:55:16 +08:00
|
|
|
<span style={{ color: '#64748b', fontSize: '13px' }}>个网卡</span>
|
|
|
|
|
<Badge count={totalStats.total} style={{ backgroundColor: '#667eea' }} />
|
|
|
|
|
<span style={{ color: '#64748b', fontSize: '13px' }}>个端口</span>
|
|
|
|
|
</Space>
|
|
|
|
|
</div>
|
|
|
|
|
<Space>
|
|
|
|
|
<Button icon={<ReloadOutlined />} onClick={fetchData} size="small">
|
|
|
|
|
刷新
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
onClick={() => setCreateCardModalVisible(true)}
|
|
|
|
|
style={{ background: designTokens.colors.primary.gradient, border: 'none' }}
|
|
|
|
|
>
|
|
|
|
|
新增网卡
|
|
|
|
|
</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{cards.length === 0 ? (
|
|
|
|
|
<div className="empty-state">
|
|
|
|
|
<Empty
|
|
|
|
|
description={
|
|
|
|
|
<span>
|
|
|
|
|
该设备暂无网卡和端口
|
|
|
|
|
<br />
|
|
|
|
|
<Button
|
|
|
|
|
type="link"
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
onClick={() => setCreateCardModalVisible(true)}
|
|
|
|
|
style={{ padding: 0, marginTop: 8 }}
|
|
|
|
|
>
|
|
|
|
|
立即添加网卡
|
|
|
|
|
</Button>
|
|
|
|
|
</span>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<CloudServerOutlined style={{ fontSize: 48, color: '#d9d9d9' }} />
|
|
|
|
|
</Empty>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<Collapse
|
|
|
|
|
activeKey={expandedCards}
|
2026-02-10 11:04:01 +08:00
|
|
|
onChange={keys => setExpandedCards(keys)}
|
2026-01-23 08:55:16 +08:00
|
|
|
expandIconPosition="end"
|
|
|
|
|
style={{ background: 'transparent' }}
|
|
|
|
|
>
|
2026-02-10 11:04:01 +08:00
|
|
|
{cards.map(card => (
|
2026-01-23 08:55:16 +08:00
|
|
|
<Panel
|
|
|
|
|
key={card.nicId}
|
|
|
|
|
header={renderCardHeader(card)}
|
|
|
|
|
style={{
|
|
|
|
|
background: '#fff',
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
marginBottom: '8px',
|
2026-02-10 11:04:01 +08:00
|
|
|
border: '1px solid #e2e8f0',
|
2026-01-23 08:55:16 +08:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{card.ports && card.ports.length > 0 ? (
|
|
|
|
|
renderPortTable(card.ports, card.nicId)
|
|
|
|
|
) : (
|
|
|
|
|
<div style={{ padding: '24px', textAlign: 'center', color: '#94a3b8' }}>
|
|
|
|
|
该{card.isUngrouped ? '分组' : '网卡'}暂无端口
|
|
|
|
|
<br />
|
|
|
|
|
<Button
|
|
|
|
|
type="link"
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setSelectedCard(card);
|
|
|
|
|
setCreatePortModalVisible(true);
|
|
|
|
|
}}
|
|
|
|
|
style={{ padding: 0, marginTop: 8 }}
|
|
|
|
|
>
|
|
|
|
|
添加端口
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</Panel>
|
|
|
|
|
))}
|
|
|
|
|
</Collapse>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<NetworkCardCreateModal
|
|
|
|
|
device={{ deviceId, name: deviceName }}
|
|
|
|
|
visible={createCardModalVisible}
|
|
|
|
|
onClose={() => setCreateCardModalVisible(false)}
|
|
|
|
|
onSuccess={handleCreateCardSuccess}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<PortCreateModal
|
|
|
|
|
device={{ deviceId, name: deviceName }}
|
|
|
|
|
visible={createPortModalVisible}
|
|
|
|
|
onClose={() => {
|
|
|
|
|
setCreatePortModalVisible(false);
|
|
|
|
|
setSelectedCard(null);
|
|
|
|
|
}}
|
|
|
|
|
onSuccess={handleCreatePortSuccess}
|
|
|
|
|
defaultNicId={selectedCard?.nicId}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default React.memo(NetworkCardPanel);
|