2026-01-23 08:55:16 +08:00
|
|
|
|
import React, { useState, useCallback, useMemo } from 'react';
|
2026-02-10 11:04:01 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Drawer,
|
|
|
|
|
|
Tabs,
|
|
|
|
|
|
Tag,
|
|
|
|
|
|
Space,
|
|
|
|
|
|
Typography,
|
|
|
|
|
|
Empty,
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Tooltip,
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Popconfirm,
|
|
|
|
|
|
} from 'antd';
|
|
|
|
|
|
import {
|
|
|
|
|
|
ApiOutlined,
|
|
|
|
|
|
CloudServerOutlined,
|
|
|
|
|
|
EnvironmentOutlined,
|
|
|
|
|
|
EditOutlined,
|
|
|
|
|
|
PlusCircleOutlined,
|
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
|
} from '@ant-design/icons';
|
2026-01-23 08:55:16 +08:00
|
|
|
|
import NetworkCardPanel from './NetworkCardPanel';
|
|
|
|
|
|
|
|
|
|
|
|
const { Text, Title } = Typography;
|
|
|
|
|
|
|
|
|
|
|
|
const designTokens = {
|
|
|
|
|
|
colors: {
|
|
|
|
|
|
primary: '#667eea',
|
|
|
|
|
|
success: '#10b981',
|
|
|
|
|
|
error: '#ef4444',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
warning: '#f59e0b',
|
2026-01-23 08:55:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
spacing: {
|
|
|
|
|
|
sm: 8,
|
2026-02-10 11:04:01 +08:00
|
|
|
|
md: 16,
|
|
|
|
|
|
},
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
function DeviceDetailDrawer({
|
|
|
|
|
|
device,
|
|
|
|
|
|
visible,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
cables,
|
|
|
|
|
|
onRefreshCables,
|
|
|
|
|
|
onEdit,
|
|
|
|
|
|
onAddNic,
|
|
|
|
|
|
onAddPort,
|
|
|
|
|
|
onAddCable,
|
|
|
|
|
|
onDeleteCable,
|
|
|
|
|
|
tooltipFields,
|
|
|
|
|
|
refreshTrigger,
|
|
|
|
|
|
}) {
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const [activeTab, setActiveTab] = useState('ports');
|
|
|
|
|
|
|
|
|
|
|
|
const deviceCables = useMemo(() => {
|
|
|
|
|
|
if (!device || !cables) return [];
|
2026-02-10 11:04:01 +08:00
|
|
|
|
return cables.filter(
|
|
|
|
|
|
c => c.sourceDeviceId === device.deviceId || c.targetDeviceId === device.deviceId
|
2026-01-23 08:55:16 +08:00
|
|
|
|
);
|
|
|
|
|
|
}, [device, cables]);
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const getStatusTag = useCallback(status => {
|
2026-01-23 08:55:16 +08:00
|
|
|
|
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: '离线' },
|
2026-02-10 11:04:01 +08:00
|
|
|
|
maintenance: { color: 'processing', 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 getDeviceTypeName = useCallback(type => {
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const typeMap = {
|
|
|
|
|
|
server: '服务器',
|
|
|
|
|
|
switch: '交换机',
|
|
|
|
|
|
router: '路由器',
|
|
|
|
|
|
storage: '存储设备',
|
|
|
|
|
|
firewall: '防火墙',
|
|
|
|
|
|
ups: 'UPS',
|
2026-02-10 11:04:01 +08:00
|
|
|
|
pdu: 'PDU',
|
2026-01-23 08:55:16 +08:00
|
|
|
|
};
|
|
|
|
|
|
return typeMap[type?.toLowerCase()] || type || '未知设备';
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
const renderFieldValue = useCallback(
|
|
|
|
|
|
(field, device) => {
|
2026-01-26 17:21:20 +08:00
|
|
|
|
const fieldKey = field.field;
|
|
|
|
|
|
if (fieldKey === 'status') return getStatusTag(device.status);
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-02-09 14:54:18 +08:00
|
|
|
|
// 优先从device对象获取值,如果没有则从customFields中获取
|
2026-01-26 17:21:20 +08:00
|
|
|
|
let value = device[fieldKey];
|
2026-02-10 11:04:01 +08:00
|
|
|
|
if (
|
|
|
|
|
|
(value === undefined || value === null) &&
|
|
|
|
|
|
device.customFields &&
|
|
|
|
|
|
typeof device.customFields === 'object'
|
|
|
|
|
|
) {
|
|
|
|
|
|
value = device.customFields[fieldKey];
|
2026-02-09 14:54:18 +08:00
|
|
|
|
}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
|
2026-01-26 17:21:20 +08:00
|
|
|
|
if (fieldKey === 'type') value = getDeviceTypeName(value);
|
2026-02-10 11:04:01 +08:00
|
|
|
|
else if (fieldKey === 'position')
|
|
|
|
|
|
value = `U${device.position} ${device.height ? `(${device.height}U)` : ''}`;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Text strong style={{ fontSize: '14px' }}>
|
|
|
|
|
|
{value !== undefined && value !== null ? value : '-'}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
[getStatusTag, getDeviceTypeName]
|
|
|
|
|
|
);
|
2026-01-26 17:21:20 +08:00
|
|
|
|
|
|
|
|
|
|
const displayFields = useMemo(() => {
|
2026-02-10 11:04:01 +08:00
|
|
|
|
if (tooltipFields && Object.keys(tooltipFields).length > 0) {
|
|
|
|
|
|
return Object.values(tooltipFields).filter(f => f.enabled);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Default fallback fields if no config
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ field: 'deviceId', label: '设备ID' },
|
|
|
|
|
|
{ field: 'type', label: '设备类型' },
|
|
|
|
|
|
{ field: 'status', label: '设备状态' },
|
|
|
|
|
|
{ field: 'position', label: '位置' },
|
|
|
|
|
|
{ field: 'ipAddress', label: 'IP地址' },
|
|
|
|
|
|
{ field: 'brand', label: '品牌' },
|
|
|
|
|
|
];
|
2026-01-26 17:21:20 +08:00
|
|
|
|
}, [tooltipFields]);
|
|
|
|
|
|
|
2026-01-23 08:55:16 +08:00
|
|
|
|
const tabItems = [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'ports',
|
|
|
|
|
|
label: (
|
|
|
|
|
|
<span>
|
|
|
|
|
|
<ApiOutlined />
|
|
|
|
|
|
端口与网卡
|
|
|
|
|
|
</span>
|
|
|
|
|
|
),
|
|
|
|
|
|
children: (
|
|
|
|
|
|
<NetworkCardPanel
|
|
|
|
|
|
deviceId={device?.deviceId}
|
|
|
|
|
|
deviceName={device?.name}
|
|
|
|
|
|
onRefresh={onRefreshCables}
|
2026-01-26 17:21:20 +08:00
|
|
|
|
refreshTrigger={refreshTrigger}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
/>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
),
|
2026-01-23 08:55:16 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'cables',
|
|
|
|
|
|
label: (
|
|
|
|
|
|
<span>
|
|
|
|
|
|
<EnvironmentOutlined />
|
|
|
|
|
|
接线 ({deviceCables.length})
|
|
|
|
|
|
</span>
|
|
|
|
|
|
),
|
|
|
|
|
|
children: (
|
|
|
|
|
|
<div className="cable-panel">
|
|
|
|
|
|
{deviceCables.length === 0 ? (
|
|
|
|
|
|
<Empty description="该设备暂无接线" />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Space direction="vertical" size={designTokens.spacing.md} style={{ width: '100%' }}>
|
|
|
|
|
|
{deviceCables.map(cable => (
|
|
|
|
|
|
<Card
|
|
|
|
|
|
key={cable.cableId}
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
style={{ borderRadius: '8px' }}
|
2026-01-26 17:21:20 +08:00
|
|
|
|
extra={
|
|
|
|
|
|
<Popconfirm
|
|
|
|
|
|
title="确定要删除这条接线吗?"
|
|
|
|
|
|
onConfirm={() => onDeleteCable?.(cable.cableId)}
|
|
|
|
|
|
okText="确定"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button type="text" danger icon={<DeleteOutlined />} size="small" />
|
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
|
}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
>
|
|
|
|
|
|
<div style={{ marginBottom: designTokens.spacing.sm }}>
|
|
|
|
|
|
<Space direction="vertical" size={4}>
|
|
|
|
|
|
<div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
|
|
|
|
源设备:
|
|
|
|
|
|
</Text>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
<div style={{ fontWeight: 500 }}>
|
|
|
|
|
|
{cable.sourceDevice?.name || '-'}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Tag color="blue" style={{ marginLeft: '8px' }}>
|
|
|
|
|
|
{cable.sourcePort}
|
|
|
|
|
|
</Tag>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
|
|
|
|
目标设备:
|
|
|
|
|
|
</Text>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
<div style={{ fontWeight: 500 }}>
|
|
|
|
|
|
{cable.targetDevice?.name || '-'}
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Tag color="green" style={{ marginLeft: '8px' }}>
|
|
|
|
|
|
{cable.targetPort}
|
|
|
|
|
|
</Tag>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Space wrap>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Tag
|
|
|
|
|
|
color={
|
|
|
|
|
|
cable.status === 'normal'
|
|
|
|
|
|
? 'success'
|
|
|
|
|
|
: cable.status === 'fault'
|
|
|
|
|
|
? 'error'
|
|
|
|
|
|
: 'default'
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
{cable.status === 'normal'
|
|
|
|
|
|
? '正常'
|
|
|
|
|
|
: cable.status === 'fault'
|
|
|
|
|
|
? '故障'
|
|
|
|
|
|
: '未连接'}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</Tag>
|
|
|
|
|
|
<Tag color="purple">
|
2026-02-10 11:04:01 +08:00
|
|
|
|
{cable.cableType === 'ethernet'
|
|
|
|
|
|
? '网线'
|
|
|
|
|
|
: cable.cableType === 'fiber'
|
|
|
|
|
|
? '光纤'
|
|
|
|
|
|
: '铜缆'}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</Tag>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
{cable.cableLength && <Tag color="orange">{cable.cableLength}m</Tag>}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</Space>
|
|
|
|
|
|
|
|
|
|
|
|
{cable.description && (
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginTop: designTokens.spacing.sm,
|
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
|
color: '#666',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
{cable.description}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
),
|
|
|
|
|
|
},
|
2026-01-23 08:55:16 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if (!device) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Drawer
|
|
|
|
|
|
title={
|
2026-01-30 17:53:05 +08:00
|
|
|
|
<Space style={{ maxWidth: '280px', overflow: 'hidden' }}>
|
|
|
|
|
|
<CloudServerOutlined style={{ color: designTokens.colors.primary, flexShrink: 0 }} />
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<span
|
|
|
|
|
|
style={{
|
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
|
|
whiteSpace: 'nowrap',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
设备详情 - {device.name}
|
|
|
|
|
|
</span>
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
|
|
|
|
|
placement="right"
|
2026-01-26 17:21:20 +08:00
|
|
|
|
width={600}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
open={visible}
|
|
|
|
|
|
onClose={onClose}
|
2026-01-26 17:21:20 +08:00
|
|
|
|
extra={
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Tooltip title="编辑设备信息">
|
|
|
|
|
|
<Button icon={<EditOutlined />} onClick={() => onEdit?.(device)} />
|
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip title="添加网卡">
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button icon={<PlusCircleOutlined />} onClick={() => onAddNic?.(device)}>
|
|
|
|
|
|
加网卡
|
|
|
|
|
|
</Button>
|
2026-01-26 17:21:20 +08:00
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip title="添加端口">
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button icon={<ApiOutlined />} onClick={() => onAddPort?.(device)}>
|
|
|
|
|
|
加端口
|
|
|
|
|
|
</Button>
|
2026-01-26 17:21:20 +08:00
|
|
|
|
</Tooltip>
|
|
|
|
|
|
<Tooltip title="添加接线">
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Button icon={<EnvironmentOutlined />} onClick={() => onAddCable?.(device)}>
|
|
|
|
|
|
加接线
|
|
|
|
|
|
</Button>
|
2026-01-26 17:21:20 +08:00
|
|
|
|
</Tooltip>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
styles={{ body: { padding: '16px 20px', overflow: 'auto' } }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="device-info-section" style={{ marginBottom: '20px' }}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Title level={5} style={{ margin: '0 0 12px 0', color: '#1e293b' }}>
|
|
|
|
|
|
基本信息
|
|
|
|
|
|
</Title>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="info-grid"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
|
|
|
|
gap: '12px',
|
|
|
|
|
|
background: '#f8fafc',
|
|
|
|
|
|
padding: '16px',
|
|
|
|
|
|
borderRadius: '10px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-01-26 17:21:20 +08:00
|
|
|
|
{displayFields.map(field => (
|
|
|
|
|
|
<div className="info-item" key={field.field}>
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Text type="secondary" style={{ fontSize: '12px', display: 'block' }}>
|
|
|
|
|
|
{field.label}
|
|
|
|
|
|
</Text>
|
2026-01-26 17:21:20 +08:00
|
|
|
|
{renderFieldValue(field, device)}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</div>
|
2026-01-26 17:21:20 +08:00
|
|
|
|
))}
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-10 11:04:01 +08:00
|
|
|
|
<Tabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
|
2026-01-23 08:55:16 +08:00
|
|
|
|
</Drawer>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default React.memo(DeviceDetailDrawer);
|