2026-01-23 08:55:16 +08:00
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
|
|
|
import { Table, Button, Space, Tag, Tooltip, Popconfirm, Empty, Spin, Badge } from 'antd';
|
2026-02-10 11:04:01 +08:00
|
|
|
import {
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
EditOutlined,
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
ApiOutlined,
|
|
|
|
|
} from '@ant-design/icons';
|
2026-01-23 08:55:16 +08:00
|
|
|
import axios from 'axios';
|
|
|
|
|
import PortCreateModal from './PortCreateModal';
|
2026-03-10 15:49:02 +08:00
|
|
|
import { designTokens } from '../config/theme';
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
|
|
|
function PortManagementPanel({ deviceId, deviceName, onRefresh }) {
|
|
|
|
|
const [ports, setPorts] = useState([]);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [createModalVisible, setCreateModalVisible] = useState(false);
|
|
|
|
|
|
|
|
|
|
const fetchPorts = useCallback(async () => {
|
|
|
|
|
if (!deviceId) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
const response = await axios.get(`/api/device-ports/device/${deviceId}`);
|
|
|
|
|
setPorts(response.data || []);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取端口列表失败:', error);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, [deviceId]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchPorts();
|
|
|
|
|
}, [fetchPorts]);
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
const handleDelete = useCallback(
|
|
|
|
|
async port => {
|
|
|
|
|
try {
|
|
|
|
|
await axios.delete(`/api/device-ports/${port.portId}`);
|
|
|
|
|
import('antd').then(({ message }) => message.success('端口删除成功'));
|
|
|
|
|
fetchPorts();
|
|
|
|
|
onRefresh?.();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
import('antd').then(({ message }) => message.error('端口删除失败'));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[fetchPorts, onRefresh]
|
|
|
|
|
);
|
2026-01-23 08:55:16 +08:00
|
|
|
|
|
|
|
|
const handleCreateSuccess = useCallback(() => {
|
|
|
|
|
fetchPorts();
|
|
|
|
|
onRefresh?.();
|
|
|
|
|
}, [fetchPorts, onRefresh]);
|
|
|
|
|
|
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: '占用' },
|
2026-02-10 11:04:01 +08:00
|
|
|
fault: { color: 'error', 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 columns = [
|
|
|
|
|
{
|
|
|
|
|
title: '端口名称',
|
|
|
|
|
dataIndex: 'portName',
|
|
|
|
|
key: 'portName',
|
|
|
|
|
width: 120,
|
2026-02-10 11:04:01 +08:00
|
|
|
render: text => (
|
2026-01-23 08:55:16 +08:00
|
|
|
<Tooltip title={text}>
|
|
|
|
|
<span style={{ fontWeight: 500 }}>{text}</span>
|
|
|
|
|
</Tooltip>
|
2026-02-10 11:04:01 +08:00
|
|
|
),
|
2026-01-23 08:55:16 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '类型',
|
|
|
|
|
dataIndex: 'portType',
|
|
|
|
|
key: 'portType',
|
|
|
|
|
width: 90,
|
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: 80,
|
2026-01-23 08:55:16 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '状态',
|
|
|
|
|
dataIndex: 'status',
|
|
|
|
|
key: 'status',
|
|
|
|
|
width: 80,
|
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: 70,
|
2026-02-10 11:04:01 +08:00
|
|
|
render: vlanId => vlanId || '-',
|
2026-01-23 08:55:16 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '操作',
|
|
|
|
|
key: 'action',
|
|
|
|
|
width: 100,
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
<Space size="small">
|
|
|
|
|
<Popconfirm
|
|
|
|
|
title="确定要删除此端口吗?"
|
|
|
|
|
onConfirm={() => handleDelete(record)}
|
|
|
|
|
okText="确定"
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
>
|
2026-02-10 11:04:01 +08:00
|
|
|
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
2026-01-23 08:55:16 +08:00
|
|
|
删除
|
|
|
|
|
</Button>
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
</Space>
|
2026-02-10 11:04:01 +08:00
|
|
|
),
|
|
|
|
|
},
|
2026-01-23 08:55:16 +08:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const freeCount = ports.filter(p => p.status === 'free').length;
|
|
|
|
|
const occupiedCount = ports.filter(p => p.status === 'occupied').length;
|
|
|
|
|
const faultCount = ports.filter(p => p.status === 'fault').length;
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
|
|
|
|
<Spin size="large" tip="加载端口数据中..." />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="port-management-panel">
|
|
|
|
|
<div className="port-panel-header">
|
|
|
|
|
<div className="port-stats">
|
|
|
|
|
<Space size={16}>
|
|
|
|
|
<Badge count={freeCount} style={{ backgroundColor: designTokens.colors.success }} />
|
|
|
|
|
<span className="stat-label">空闲</span>
|
|
|
|
|
<Badge count={occupiedCount} style={{ backgroundColor: '#1677ff' }} />
|
|
|
|
|
<span className="stat-label">占用</span>
|
|
|
|
|
<Badge count={faultCount} style={{ backgroundColor: designTokens.colors.error }} />
|
|
|
|
|
<span className="stat-label">故障</span>
|
|
|
|
|
</Space>
|
|
|
|
|
</div>
|
|
|
|
|
<Space>
|
2026-02-10 11:04:01 +08:00
|
|
|
<Button icon={<ReloadOutlined />} onClick={fetchPorts} size="small">
|
2026-01-23 08:55:16 +08:00
|
|
|
刷新
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
onClick={() => setCreateModalVisible(true)}
|
|
|
|
|
style={{
|
|
|
|
|
background: designTokens.colors.primary.gradient,
|
2026-02-10 11:04:01 +08:00
|
|
|
border: 'none',
|
2026-01-23 08:55:16 +08:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
新增端口
|
|
|
|
|
</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{ports.length === 0 ? (
|
|
|
|
|
<div className="port-empty">
|
|
|
|
|
<Empty
|
|
|
|
|
description={
|
|
|
|
|
<span>
|
|
|
|
|
该设备暂无端口
|
|
|
|
|
<br />
|
|
|
|
|
<Button
|
|
|
|
|
type="link"
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
onClick={() => setCreateModalVisible(true)}
|
|
|
|
|
style={{ padding: 0, marginTop: 8 }}
|
|
|
|
|
>
|
|
|
|
|
立即添加端口
|
|
|
|
|
</Button>
|
|
|
|
|
</span>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<ApiOutlined style={{ fontSize: 48, color: '#d9d9d9' }} />
|
|
|
|
|
</Empty>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<Table
|
|
|
|
|
columns={columns}
|
|
|
|
|
dataSource={ports}
|
|
|
|
|
rowKey="portId"
|
|
|
|
|
pagination={false}
|
|
|
|
|
size="small"
|
|
|
|
|
scroll={{ x: 600 }}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<PortCreateModal
|
|
|
|
|
device={{ deviceId, name: deviceName }}
|
|
|
|
|
visible={createModalVisible}
|
|
|
|
|
onClose={() => setCreateModalVisible(false)}
|
|
|
|
|
onSuccess={handleCreateSuccess}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default React.memo(PortManagementPanel);
|