diff --git a/backend/backgroundSettings.json b/backend/backgroundSettings.json index 27260c5..e5d2719 100644 --- a/backend/backgroundSettings.json +++ b/backend/backgroundSettings.json @@ -1,5 +1,5 @@ { - "type": "image", + "type": "gradient", "image": "/uploads/1766291127756_生成情侣图片.png", "size": "contain" } \ No newline at end of file diff --git a/frontend/src/pages/RackVisualization.jsx b/frontend/src/pages/RackVisualization.jsx index 5a2de38..8cd9946 100644 --- a/frontend/src/pages/RackVisualization.jsx +++ b/frontend/src/pages/RackVisualization.jsx @@ -11,7 +11,8 @@ import { CloudOutlined, LaptopOutlined, MobileOutlined, - PrinterOutlined + PrinterOutlined, + SettingOutlined } from '@ant-design/icons'; import axios from 'axios'; @@ -31,49 +32,90 @@ style.textContent = ` } } - /* 强制设备详情面板在最顶层,超越所有3D空间 */ - .device-detail-panel { - z-index: 2147483647 !important; /* 使用最大z-index值 */ - position: fixed !important; - transform: translateY(-50%) !important; /* 强制2D变换 */ - backface-visibility: hidden !important; - -webkit-backface-visibility: hidden !important; - perspective: none !important; - transform-style: flat !important; - isolation: isolate !important; - will-change: transform !important; - contain: layout style paint !important; - overflow: visible !important; - pointer-events: auto !important; - /* 强制独立层 */ - -webkit-transform: translateY(-50%) !important; - -moz-transform: translateY(-50%) !important; - -ms-transform: translateY(-50%) !important; - -o-transform: translateY(-50%) !important; - /* 强制最高优先级 */ - filter: drop-shadow(0 0 0 transparent) !important; - box-sizing: border-box !important; - max-width: none !important; - min-width: auto !important; + /* LED指示灯闪烁动画 */ + @keyframes ledBlink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.3; } } - /* 防止页面3D空间影响设备详情面板 */ - .rack-visualization-container { - perspective: none !important; - transform-style: flat !important; - contain: layout !important; - overflow: hidden !important; + /* Tooltip淡入动画 */ + @keyframes tooltipFadeIn { + from { + opacity: 0; + transform: translateY(-50%) translateX(-10px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(-50%) translateX(0) scale(1); + } } - /* 确保页面所有元素都不会覆盖设备详情面板 */ - html, body { - overflow-x: hidden !important; + /* 金属拉丝纹理 */ + .metal-texture { + background-image: + linear-gradient(90deg, + transparent 0%, + rgba(255,255,255,0.03) 50%, + transparent 100%), + repeating-linear-gradient(0deg, + transparent 0px, + rgba(255,255,255,0.02) 1px, + transparent 2px, + transparent 3px); + background-size: 100% 100%, 4px 4px; } - /* 强制提升页面所有容器的层级 */ - .ant-layout, .ant-card, .ant-card-head, .ant-card-body { - contain: none !important; + /* 散热格栅效果 */ + .ventilation-grille { + background-image: repeating-linear-gradient( + 0deg, + #334155 0px, + #334155 1px, + transparent 1px, + transparent 2px + ); } + + /* 悬停提亮效果 */ + .device-hover { + background: linear-gradient(145deg, #e8f5e8, #d4edda) !important; + box-shadow: 0 6px 16px rgba(0,0,0,0.3), 0 0 8px rgba(129, 199, 132, 0.4) !important; + } + + /* Tooltip样式 */ + .device-tooltip { + background: rgba(0, 0, 0, 0.9); + color: #5eead4; + padding: 8px 12px; + border-radius: 4px; + font-family: 'Roboto Mono', monospace; + font-size: 11px; + border: 1px solid rgba(94, 234, 212, 0.3); + box-shadow: 0 4px 12px rgba(0,0,0,0.5); + white-space: nowrap; + } + + /* 设备数量badge */ + .device-count-badge { + background: linear-gradient(145deg, #a5d6a7, #81c784); + color: #2e7d32; + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + box-shadow: 0 2px 6px rgba(129, 199, 132, 0.3); + border: 1px solid rgba(129, 199, 132, 0.5); + backdrop-filter: blur(4px); + transition: all 0.2s ease; + white-space: 'nowrap'; + } + + .device-count-badge:hover { + transform: translateY(-1px); + box-shadow: 0 3px 10px rgba(129, 199, 132, 0.4); + } + + `; document.head.appendChild(style); @@ -137,15 +179,68 @@ function RackVisualization() { const [loading, setLoading] = useState(true); const [devices, setDevices] = useState([]); const [loadingDevices, setLoadingDevices] = useState(false); - const [zoom, setZoom] = useState(1); + const [zoom, setZoom] = useState(1.5); const [rotation, setRotation] = useState(0); const [error, setError] = useState(null); - const [hoveredDevice, setHoveredDevice] = useState(null); - const [hoverTimeout, setHoverTimeout] = useState(null); + const [deviceTooltip, setDeviceTooltip] = useState(null); // 设备详情tooltip + const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); // tooltip位置 const [backgroundImage, setBackgroundImage] = useState(null); const [backgroundType, setBackgroundType] = useState('gradient'); // gradient or image const [backgroundSize, setBackgroundSize] = useState('contain'); // cover, contain, auto const [uploading, setUploading] = useState(false); // 上传状态 + const [showTooltipConfig, setShowTooltipConfig] = useState(false); // 显示tooltip配置面板 + const [tooltipDeviceFields, setTooltipDeviceFields] = useState([]); // 设备字段配置 + const [loadingTooltipFields, setLoadingTooltipFields] = useState(false); // 加载字段配置状态 + const [savingTooltipConfig, setSavingTooltipConfig] = useState(false); // 保存配置状态 + + // 设备详情tooltip字段配置 + const [tooltipFields, setTooltipFields] = useState({}); + + // 获取设备字段配置 + const fetchTooltipDeviceFields = async () => { + try { + setLoadingTooltipFields(true); + const response = await axios.get('/api/deviceFields'); + const sortedFields = response.data.sort((a, b) => a.order - b.order); + setTooltipDeviceFields(sortedFields); + + // 根据字段配置初始化tooltipFields + const initialFields = {}; + sortedFields.forEach(field => { + initialFields[field.fieldName] = { + label: field.displayName, + enabled: field.visible, + field: field.fieldName, + fieldType: field.fieldType + }; + }); + setTooltipFields(initialFields); + } catch (error) { + console.error('获取设备字段配置失败:', error); + } finally { + setLoadingTooltipFields(false); + } + }; + + // 保存tooltip字段配置 + const saveTooltipConfig = async () => { + try { + setSavingTooltipConfig(true); + const fieldConfigs = Object.values(tooltipFields).map(field => ({ + fieldName: field.field, + visible: field.enabled + })); + + await axios.post('/api/deviceFields/config', fieldConfigs); + message.success('字段配置已保存'); + setShowTooltipConfig(false); + } catch (error) { + console.error('保存字段配置失败:', error); + message.error('保存字段配置失败'); + } finally { + setSavingTooltipConfig(false); + } + }; // 获取所有机柜 const fetchRacks = async () => { @@ -196,15 +291,6 @@ function RackVisualization() { } }; - // 组件卸载时清理定时器 - useEffect(() => { - return () => { - if (hoverTimeout) { - clearTimeout(hoverTimeout); - } - }; - }, [hoverTimeout]); - // 获取机柜内的设备 const fetchDevices = async (rackId) => { try { @@ -338,6 +424,7 @@ function RackVisualization() { useEffect(() => { fetchRacks(); loadBackgroundSettings(); + fetchTooltipDeviceFields(); }, []); // 根据设备类型获取图标 @@ -377,25 +464,63 @@ function RackVisualization() { return '#1890ff'; // 默认蓝色 }; + // 获取设备状态颜色 + const getDeviceStatusColor = (status) => { + const statusColorMap = { + 'normal': '#10b981', // 正常 - 绿色常亮 + 'warning': '#f59e0b', // 预警 - 黄色常亮 + 'error': '#ef4444', // 告警 - 红色慢闪 + 'offline': '#6b7280', // 离线 - 灰色 + 'maintenance': '#3b82f6', // 维护 - 蓝色常亮 + undefined: '#3b82f6', // 默认普通设备 - 蓝色常亮 + null: '#3b82f6' + }; + return statusColorMap[status] || '#3b82f6'; + }; + + // 生成模拟监控数据 + const generateMonitoringData = (device) => { + const baseTemp = device.type?.includes('服务器') ? 65 : + device.type?.includes('存储') ? 55 : + device.type?.includes('网络') ? 45 : 40; + + const baseVoltage = 220; + const baseUptime = 24 * 365; // 假设运行了1年 + + return { + temperature: (baseTemp + Math.random() * 20 - 10).toFixed(1), // 温度 + voltage: (baseVoltage + Math.random() * 10 - 5).toFixed(1), // 电压 + uptime: Math.floor(baseUptime + Math.random() * 24 * 30), // 运行时长(小时) + power: (50 + Math.random() * 200).toFixed(1), // 功耗 + load: Math.floor(Math.random() * 80 + 10) // 负载百分比 + }; + }; + const getDeviceStyle = (device, rackHeight) => { // 添加参数验证 if (!device || typeof device.position !== 'number' || typeof device.height !== 'number') { return {}; } - const uHeight = 18; // 每个U的高度(像素) + const uHeight = 25; // 调整为更小的U高度以适应屏幕显示,1U=25px const deviceHeight = Math.max(1, device.height) * uHeight; // 设备位置从底部开始计算(U1在底部) - const position = Math.max(1, Math.min(device.position, rackHeight)); - const deviceUHeight = Math.max(1, device.height); + let position = Math.max(1, device.position); + let deviceUHeight = Math.max(1, device.height); + + // 确保设备不会超出机柜范围 + if (position + deviceUHeight - 1 > rackHeight) { + // 如果设备会超出机柜,调整位置或高度 + position = Math.max(1, rackHeight - deviceUHeight + 1); + } // 计算设备的顶部位置(从机柜顶部算起) // 设备占用从 position 到 position + height - 1 的U // 机柜顶部是U0,所以设备顶部的topPosition是: const deviceBottomU = position; // 设备底部U数 const deviceTopU = position + deviceUHeight - 1; // 设备顶部U数 - const topPosition = (rackHeight - deviceTopU - 1) * uHeight; + const topPosition = (rackHeight - deviceTopU) * uHeight; return { height: `${deviceHeight}px`, // 确保设备高度精确等于U位高度 @@ -434,7 +559,7 @@ function RackVisualization() { // 生成U数标记(每个U都有标记) const generateUMarks = (height) => { const marks = []; - const uHeight = 18; + const uHeight = 25; // 调整为更小的U高度以适应屏幕显示 for (let i = 1; i <= height; i++) { marks.push( @@ -444,10 +569,10 @@ function RackVisualization() { style={{ top: `${(height - i) * uHeight}px`, height: '1px', - backgroundColor: i % 5 === 0 ? '#1890ff' : '#999', + backgroundColor: i % 5 === 0 ? '#81c784' : '#a5d6a7', width: '100%', position: 'absolute', - borderTop: i % 5 === 0 ? '2px solid #1890ff' : '1px solid #999' + borderTop: i % 5 === 0 ? '2px solid #81c784' : '1px solid #a5d6a7' }} > {i} @@ -471,7 +596,7 @@ function RackVisualization() { // 生成左侧U数标记(从U1开始显示) const generateLeftUMarks = (height) => { const marks = []; - const uHeight = 18; + const uHeight = 25; // 调整为更小的U高度以适应屏幕显示 for (let u = 1; u <= height; u++) { // 使用与设备位置计算一致的逻辑 @@ -491,15 +616,15 @@ function RackVisualization() { display: 'flex', alignItems: 'center', justifyContent: 'center', - color: u % 5 === 0 ? '#ff4757' : '#ff9500', // 亮红色和亮橙色 - fontSize: '11px', + color: u % 5 === 0 ? '#2e7d32' : '#4caf50', // 绿色系 + fontSize: '12px', fontWeight: u % 5 === 0 ? 'bold' : 'normal', - textShadow: '1px 1px 2px rgba(0,0,0,0.9), 0 0 4px rgba(255,255,255,0.3)', + textShadow: '1px 1px 2px rgba(255,255,255,0.8)', zIndex: 30, // 提高z-index确保在前面板之上 - backgroundColor: 'rgba(0,0,0,0.8)', - borderRadius: '3px', - border: '1px solid rgba(255,255,255,0.3)', - boxShadow: '0 2px 4px rgba(0,0,0,0.3)' + backgroundColor: 'rgba(255,255,255,0.9)', + borderRadius: '4px', + border: '1px solid rgba(129, 199, 132, 0.5)', + boxShadow: '0 2px 6px rgba(129, 199, 132, 0.3)' }} > {u} @@ -514,7 +639,7 @@ function RackVisualization() { // 生成右侧U数标记(从U1开始显示) const generateRightUMarks = (height) => { const marks = []; - const uHeight = 18; + const uHeight = 25; // 调整为更小的U高度以适应屏幕显示 for (let u = 1; u <= height; u++) { // 使用与设备位置计算一致的逻辑 @@ -534,15 +659,15 @@ function RackVisualization() { display: 'flex', alignItems: 'center', justifyContent: 'center', - color: u % 5 === 0 ? '#ff4757' : '#ff9500', // 亮红色和亮橙色 - fontSize: '11px', + color: u % 5 === 0 ? '#2e7d32' : '#4caf50', // 绿色系 + fontSize: '12px', fontWeight: u % 5 === 0 ? 'bold' : 'normal', - textShadow: '1px 1px 2px rgba(0,0,0,0.9), 0 0 4px rgba(255,255,255,0.3)', + textShadow: '1px 1px 2px rgba(255,255,255,0.8)', zIndex: 30, // 提高z-index确保在前面板之上 - backgroundColor: 'rgba(0,0,0,0.8)', - borderRadius: '3px', - border: '1px solid rgba(255,255,255,0.3)', - boxShadow: '0 2px 4px rgba(0,0,0,0.3)' + backgroundColor: 'rgba(255,255,255,0.9)', + borderRadius: '4px', + border: '1px solid rgba(129, 199, 132, 0.5)', + boxShadow: '0 2px 6px rgba(129, 199, 132, 0.3)' }} > {u} @@ -652,12 +777,11 @@ function RackVisualization() { // 放大 const handleZoomIn = () => { - setZoom(prev => Math.min(prev + 0.2, 2.5)); + setZoom(prev => Math.min(prev + 0.3, 3.0)); }; - // 缩小 const handleZoomOut = () => { - setZoom(prev => Math.max(prev - 0.2, 0.5)); + setZoom(prev => Math.max(prev - 0.3, 0.7)); }; // 无需额外的初始化,CSS 3D变换直接在JSX中实现 @@ -706,6 +830,13 @@ function RackVisualization() { > 刷新 + } > @@ -804,46 +935,46 @@ function RackVisualization() { - {loading ? ( -
- 加载机柜数据中... -
- ) : selectedRack ? ( -
+ {loading ? ( +
+ 加载机柜数据中... +
+ ) : selectedRack ? ( +
{loadingDevices && (
{/* 机柜主体 */}
{/* 左侧U数标记 */}
{generateLeftUMarks(selectedRack.height)}
@@ -901,51 +1031,131 @@ function RackVisualization() { {/* 右侧U数标记 */}
{generateRightUMarks(selectedRack.height)}
+ {/* 左侧U型安装导轨 */} +
+ {/* 导轨安装孔 - 左侧 */} + {Array.from({ length: Math.max(1, Math.floor((selectedRack.height * 25 - 6) / 25)) }).map((_, index) => ( +
+
+
+ ))} +
+ + {/* 右侧U型安装导轨 */} +
+ {/* 导轨安装孔 - 右侧 */} + {Array.from({ length: Math.max(1, Math.floor((selectedRack.height * 25 - 6) / 25)) }).map((_, index) => ( +
+
+
+ ))} +
+ {/* 前面板 */}
+ {/* 细微网格纹理 */} +
+ {/* 移除了内侧U数标记,因为外侧已有U数显示 */} {/* 设备 */} - {/* 调试信息 */} -
- 设备数量: {devices.length} -
+ {/* 移除了设备数量显示,现在在机柜标题旁边 */} {devices.length > 0 ? devices.map(device => { try { @@ -962,176 +1172,189 @@ function RackVisualization() { style={{ ...getDeviceStyle(device, selectedRack.height), position: 'absolute', - left: '0px', // 移除左边界 - right: '0px', // 移除右边界 - borderRadius: '4px', + left: '2px', + right: '2px', + width: 'calc(100% - 4px)', + borderRadius: '2px', display: 'flex', cursor: 'pointer', - transform: 'translateZ(20px)', transition: 'all 0.2s ease', - boxShadow: '0 4px 12px rgba(0,0,0,0.3)', - backgroundColor: getDeviceColor(device.type), - opacity: 0.95, - border: '1px solid rgba(255,255,255,0.2)', - overflow: 'hidden', // 确保内容不溢出 + boxShadow: '0 1px 3px rgba(0,0,0,0.3), 0 4px 6px rgba(0,0,0,0.2)', + background: 'linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%)', + border: '1px solid #cbd5e1', + overflow: 'hidden', margin: 0, padding: 0, - zIndex: 100, // 确保设备在前景 - pointerEvents: 'auto' // 确保可以接收鼠标事件 + zIndex: 100, + pointerEvents: 'auto' }} onMouseEnter={(e) => { - e.currentTarget.style.transform = 'translateZ(25px) scale(1.01)'; - e.currentTarget.style.boxShadow = '0 6px 16px rgba(0,0,0,0.4)'; + e.currentTarget.style.transform = 'scale(1.01)'; + e.currentTarget.classList.add('device-hover'); - // 清除之前的定时器 - if (hoverTimeout) { - clearTimeout(hoverTimeout); - setHoverTimeout(null); - } + // 计算tooltip位置(机柜右侧) + const rect = e.currentTarget.getBoundingClientRect(); + const tooltipX = rect.right + 5; // 设备右侧 + const tooltipY = rect.top + rect.height / 2; // 设备中间高度 - // 延迟显示设备信息,避免快速移动时的闪烁 - const timeout = setTimeout(() => { - // 使用设备ID确保状态设置正确 - setHoveredDevice({ - ...device, - _key: device.deviceId || device.id || device.device_id || device.device - }); - }, 100); + setTooltipPosition({ x: tooltipX, y: tooltipY }); - setHoverTimeout(timeout); + // 设置设备详情tooltip + setDeviceTooltip({ + device: device + }); }} onMouseLeave={(e) => { - e.currentTarget.style.transform = 'translateZ(20px) scale(1)'; - e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)'; + e.currentTarget.style.transform = 'scale(1)'; + e.currentTarget.classList.remove('device-hover'); - // 清除之前的定时器 - if (hoverTimeout) { - clearTimeout(hoverTimeout); - setHoverTimeout(null); - } - - // 延迟隐藏设备信息,给用户一些缓冲时间 - const timeout = setTimeout(() => { - setHoveredDevice(null); - }, 200); // 增加延迟时间到200ms - - setHoverTimeout(timeout); + // 清除tooltip + setDeviceTooltip(null); }} > - {/* 左侧指示灯区域 */} + {/* 左侧状态指示区域 */}
{/* 状态指示灯 */}
+ {/* 电源指示 */}
- {/* 中间模块/接口槽位区域 */} + {/* 中间设备信息区域 */}
- {/* 模拟接口槽位 */} + {/* 设备图标 */}
- {Array.from({length: Math.min(6, Math.max(3, Math.floor(height * 1.5)))}, (_, i) => ( -
- ))} + {getDeviceIcon(device.type)}
- {/* 设备图标和名称 - 居中显示 */} + {/* 分隔符 */}
+ + {/* 设备名称 */} +
- - {getDeviceIcon(device.type)} - - - {deviceName} - + {deviceName} +
+ + {/* 型号/类型 */} +
+ {device.model || device.type || ''}
- {/* 右侧U高度标签 */} + {/* 右侧功能区域 */}
- {height}U + {/* 端口指示 */} +
+
+
); @@ -1167,9 +1390,15 @@ function RackVisualization() { top: '50%', left: '50%', transform: 'translate(-50%, -50%)', - color: '#999', - fontSize: '14px', - textAlign: 'center' + color: '#81c784', + fontSize: '16px', + textAlign: 'center', + padding: '16px 24px', + backgroundColor: 'rgba(240, 255, 240, 0.95)', + borderRadius: '12px', + border: '1px solid rgba(129, 199, 132, 0.3)', + boxShadow: '0 4px 12px rgba(129, 199, 132, 0.2)', + fontWeight: '500' }}> 暂无设备数据
@@ -1181,9 +1410,9 @@ function RackVisualization() { position: 'absolute', top: '0', left: '100%', - width: '20px', + width: '30px', height: '100%', - backgroundColor: '#555', + backgroundColor: '#7f8c8d', transformOrigin: 'left center', transform: 'rotateY(90deg)', borderTopRightRadius: '4px', @@ -1204,269 +1433,43 @@ function RackVisualization() { borderTopRightRadius: '4px' }} /> - {/* 机柜名称 */} + {/* 机柜名称和设备数量 */}
- {selectedRack.name} ({selectedRack.rackId}) + {/* 机柜标题 */} +
+ {selectedRack.name} ({selectedRack.rackId}) +
+ + {/* 设备数量badge */} +
+ 设备数量: {devices.length} +
- - {/* 设备详情面板 */} - {hoveredDevice && ( -
{ - // 鼠标进入面板时,清除隐藏定时器 - if (hoverTimeout) { - clearTimeout(hoverTimeout); - setHoverTimeout(null); - } - e.stopPropagation(); // 防止事件冒泡 - }} - onMouseLeave={(e) => { - // 鼠标离开面板时,延迟隐藏 - const timeout = setTimeout(() => { - setHoveredDevice(null); - }, 300); // 增加延迟时间 - setHoverTimeout(timeout); - e.stopPropagation(); // 防止事件冒泡 - }} - onMouseMove={(e) => { - // 鼠标在面板内移动时,保持面板打开 - if (hoverTimeout) { - clearTimeout(hoverTimeout); - setHoverTimeout(null); - } - e.stopPropagation(); - }} - > - -
-
- {getDeviceIcon(hoveredDevice.type)} -
-
-

- {hoveredDevice.name || hoveredDevice.deviceName || hoveredDevice.device_name || '未知设备'} -

-

- {getDeviceTypeName(hoveredDevice.type)} -

-
-
- -
-
- 设备类型: - {getDeviceTypeName(hoveredDevice.type)} -
- -
- U位位置: - - U{hoveredDevice.position} - U{hoveredDevice.position + (hoveredDevice.height || 1) - 1} - -
- -
- 设备高度: - {hoveredDevice.height || 1}U -
- -
- 运行状态: - - {hoveredDevice.status === 'running' ? '运行中' : '已停止'} - -
- - {hoveredDevice.serialNumber && ( -
- 序列号: - - {hoveredDevice.serialNumber} - -
- )} - - {hoveredDevice.installDate && ( -
- 安装日期: - - {formatDate(hoveredDevice.installDate)} - -
- )} - - {hoveredDevice.model && ( -
- 设备型号: - - {hoveredDevice.model} - -
- )} - - {hoveredDevice.ipAddress && ( -
- IP地址: - - {hoveredDevice.ipAddress} - -
- )} -
- - {/* 关闭按钮 */} -
setHoveredDevice(null)} - onMouseEnter={(e) => { - e.currentTarget.style.backgroundColor = '#e6f7ff'; - e.currentTarget.style.color = '#1890ff'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = '#f5f5f5'; - e.currentTarget.style.color = '#999'; - }} - > - × -
-
- )}
) : ( @@ -1484,6 +1487,238 @@ function RackVisualization() {
)} + + {/* Tooltip字段配置面板 */} + {showTooltipConfig && ( +
+
+
+
+ 设备详情字段配置 +
+ +
+ + {loadingTooltipFields ? ( +
+ 加载字段配置中... +
+ ) : Object.keys(tooltipFields).length === 0 ? ( +
+ 暂无字段配置,请先在设备字段管理中添加字段 +
+ ) : ( + <> +
+
+ 选择要在设备详情中显示的字段: +
+ + {Object.entries(tooltipFields).map(([key, field]) => ( + + ))} +
+ +
+ + + +
+ + )} +
+
+ )} + + {/* 设备详情tooltip - 放在容器外部确保不被遮挡 */} + {deviceTooltip && ( +
+
+ 设备详情 +
+
+ {tooltipFields.name?.enabled && ( +
名称: {deviceTooltip.device.name || '未知设备'}
+ )} + {tooltipFields.deviceId?.enabled && ( +
设备ID: {deviceTooltip.device.deviceId || deviceTooltip.device.id || '-'}
+ )} + {tooltipFields.type?.enabled && ( +
类型: {deviceTooltip.device.type || deviceTooltip.device.device_type || '-'}
+ )} + {tooltipFields.brand?.enabled && ( +
品牌: {deviceTooltip.device.brand || '-'}
+ )} + {tooltipFields.model?.enabled && ( +
型号: {deviceTooltip.device.model || '-'}
+ )} + {tooltipFields.status?.enabled && ( +
状态: {deviceTooltip.device.status === 'running' ? '运行中' : deviceTooltip.device.status === 'warning' ? '警告' : deviceTooltip.device.status === 'error' ? '故障' : deviceTooltip.device.status || '未知'}
+ )} + {tooltipFields.position?.enabled && ( +
位置: U{deviceTooltip.device.position} ({deviceTooltip.device.height}U)
+ )} + {tooltipFields.height?.enabled && ( +
高度: {deviceTooltip.device.height || '-'}U
+ )} + {tooltipFields.ipAddress?.enabled && ( +
IP地址: {deviceTooltip.device.ipAddress || deviceTooltip.device.ip_address || '-'}
+ )} + {tooltipFields.power?.enabled && ( +
功率: {deviceTooltip.device.power ? `${deviceTooltip.device.power}W` : '-'}
+ )} +
+
+ )}
);