From fd3c6f5225d96279e5656a445b8b3c0edaec846b Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Tue, 23 Dec 2025 16:11:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E6=9C=BA=E6=9F=9C?= =?UTF-8?q?=E5=8F=AF=E8=A7=86=E5=8C=96=E9=A1=B5=E9=9D=A2=EF=BC=8C=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8F=AF=E8=A7=86=E5=8C=96=E7=95=8C=E9=9D=A2=E4=B8=AD?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E8=AF=A6=E6=83=85=E7=9A=84=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=8F=AF=E9=80=89=E6=8B=A9=E5=86=85=E5=AE=B9=EF=BC=8CU?= =?UTF-8?q?=E6=95=B0=E5=92=8C=E6=9C=BA=E6=9F=9C=E4=BD=8D=E7=BD=AE=E5=8C=B9?= =?UTF-8?q?=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/RackVisualization.jsx | 1077 ++++++++++++++-------- 1 file changed, 670 insertions(+), 407 deletions(-) diff --git a/frontend/src/pages/RackVisualization.jsx b/frontend/src/pages/RackVisualization.jsx index 8cd9946..ed1ee0b 100644 --- a/frontend/src/pages/RackVisualization.jsx +++ b/frontend/src/pages/RackVisualization.jsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { Card, Select, Button, Space, message, Tooltip } from 'antd'; +import { Card, Select, Button, Space, message, Tooltip, Modal, Form, Switch, Checkbox } from 'antd'; import { ReloadOutlined, ZoomInOutlined, - ZoomOutOutlined, + ZoomOutOutlined, RotateRightOutlined, CloudServerOutlined, SwitcherOutlined, @@ -78,8 +78,9 @@ style.textContent = ` /* 悬停提亮效果 */ .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; + background: linear-gradient(145deg, #1e293b, #0f172a) !important; + box-shadow: 0 6px 16px rgba(56, 189, 248, 0.3), 0 0 12px rgba(56, 189, 248, 0.2) !important; + border-color: #38bdf8 !important; } /* Tooltip样式 */ @@ -97,28 +98,21 @@ style.textContent = ` /* 设备数量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'; + background: linear-gradient(135deg, rgba(56, 189, 248, 0.2), rgba(14, 165, 233, 0.2)); + color: #38bdf8; + padding: 6px 14px; + border-radius: 20px; + font-size: 13px; + font-weight: 600; + box-shadow: 0 4px 15px rgba(56, 189, 248, 0.2); + border: 1px solid rgba(56, 189, 248, 0.3); + backdrop-filter: blur(10px); + transition: all 0.3s ease; + white-space: nowrap; + font-family: 'JetBrains Mono', 'Roboto Mono', monospace; } - - .device-count-badge:hover { - transform: translateY(-1px); - box-shadow: 0 3px 10px rgba(129, 199, 132, 0.4); - } - - `; document.head.appendChild(style); - // 错误边界组件 class ErrorBoundary extends React.Component { constructor(props) { @@ -196,27 +190,55 @@ function RackVisualization() { // 设备详情tooltip字段配置 const [tooltipFields, setTooltipFields] = useState({}); + // 默认设备字段配置 + const defaultTooltipFields = { + name: { label: '设备名称', enabled: true, field: 'name', fieldType: 'string' }, + deviceId: { label: '设备ID', enabled: true, field: 'deviceId', fieldType: 'string' }, + type: { label: '设备类型', enabled: true, field: 'type', fieldType: 'string' }, + brand: { label: '品牌', enabled: true, field: 'brand', fieldType: 'string' }, + model: { label: '型号', enabled: true, field: 'model', fieldType: 'string' }, + status: { label: '状态', enabled: true, field: 'status', fieldType: 'string' }, + position: { label: '位置', enabled: true, field: 'position', fieldType: 'number' }, + height: { label: '高度', enabled: true, field: 'height', fieldType: 'number' }, + ipAddress: { label: 'IP地址', enabled: true, field: 'ipAddress', fieldType: 'string' }, + power: { label: '功率', enabled: true, field: 'power', fieldType: 'number' } + }; + // 获取设备字段配置 const fetchTooltipDeviceFields = async () => { try { setLoadingTooltipFields(true); + console.log('开始获取设备字段配置...'); const response = await axios.get('/api/deviceFields'); - const sortedFields = response.data.sort((a, b) => a.order - b.order); - setTooltipDeviceFields(sortedFields); + console.log('API响应:', response.data); + const fieldsData = response.data; + + if (Array.isArray(fieldsData) && fieldsData.length > 0) { + console.log('使用API返回的字段配置'); + const sortedFields = fieldsData.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); + // 根据字段配置初始化tooltipFields + const initialFields = {}; + sortedFields.forEach(field => { + console.log('字段:', field.fieldName, field.displayName, field.visible); + initialFields[field.fieldName] = { + label: field.displayName, + enabled: field.visible, + field: field.fieldName, + fieldType: field.fieldType + }; + }); + setTooltipFields(initialFields); + } else { + console.log('使用默认字段配置'); + // 使用默认字段配置 + setTooltipFields(defaultTooltipFields); + } } catch (error) { - console.error('获取设备字段配置失败:', error); + console.error('获取设备字段配置失败,使用默认配置:', error); + // 使用默认字段配置 + setTooltipFields(defaultTooltipFields); } finally { setLoadingTooltipFields(false); } @@ -226,14 +248,20 @@ function RackVisualization() { const saveTooltipConfig = async () => { try { setSavingTooltipConfig(true); - const fieldConfigs = Object.values(tooltipFields).map(field => ({ + + const fieldConfigs = Object.entries(tooltipFields).map(([key, field]) => ({ fieldName: field.field, - visible: field.enabled + visible: field.enabled, + displayName: field.label, + fieldType: field.fieldType })); await axios.post('/api/deviceFields/config', fieldConfigs); message.success('字段配置已保存'); setShowTooltipConfig(false); + + // 重新获取配置以确保数据一致 + await fetchTooltipDeviceFields(); } catch (error) { console.error('保存字段配置失败:', error); message.error('保存字段配置失败'); @@ -325,9 +353,24 @@ function RackVisualization() { console.log('=== 处理后的设备数据 ==='); console.log('设备数组长度:', devicesData.length); console.log('设备数据:', devicesData); + + // 展开customFields中的字段到设备对象的直接属性 + const processedDevices = devicesData.map(device => { + const deviceWithFields = { ...device }; + + if (device.customFields && typeof device.customFields === 'object') { + Object.entries(device.customFields).forEach(([fieldName, value]) => { + deviceWithFields[fieldName] = value; + }); + } + + return deviceWithFields; + }); + + console.log('展开customFields后的设备数据:', processedDevices); // 如果没有数据,显示警告 - if (devicesData.length === 0) { + if (processedDevices.length === 0) { console.warn('=== 没有找到设备数据 ==='); console.warn('可能的原因:'); console.warn('1. 数据库中该机柜确实没有设备'); @@ -340,8 +383,8 @@ function RackVisualization() { // 显示原始数据结构以供调试 console.log('=== 数据结构分析 ==='); - if (devicesData.length > 0) { - const firstDevice = devicesData[0]; + if (processedDevices.length > 0) { + const firstDevice = processedDevices[0]; console.log('第一个设备完整数据:', JSON.stringify(firstDevice, null, 2)); console.log('第一个设备的字段:', Object.keys(firstDevice)); console.log('第一个设备的字段类型:'); @@ -351,7 +394,7 @@ function RackVisualization() { } // 过滤有效的设备数据 - 放宽验证条件 - const validDevices = devicesData.filter(device => { + const validDevices = processedDevices.filter(device => { if (!device) { console.log('跳过空设备'); return false; @@ -427,6 +470,14 @@ function RackVisualization() { fetchTooltipDeviceFields(); }, []); + // 打开字段配置模态框时获取数据 + const handleOpenTooltipConfig = () => { + if (Object.keys(tooltipFields).length === 0) { + fetchTooltipDeviceFields(); + } + setShowTooltipConfig(true); + }; + // 根据设备类型获取图标 const getDeviceIcon = (deviceType) => { try { @@ -478,6 +529,69 @@ function RackVisualization() { return statusColorMap[status] || '#3b82f6'; }; + // 获取设备类型对应的颜色主题 + const getDeviceTypeTheme = (type) => { + const themeMap = { + 'server': { + borderColor: '#38bdf8', + accentColor: '#0ea5e9', + glowColor: 'rgba(56, 189, 248, 0.3)', + iconColor: '#38bdf8', + label: '服务器' + }, + 'switch': { + borderColor: '#22c55e', + accentColor: '#16a34a', + glowColor: 'rgba(34, 197, 94, 0.3)', + iconColor: '#22c55e', + label: '交换机' + }, + 'router': { + borderColor: '#f59e0b', + accentColor: '#d97706', + glowColor: 'rgba(245, 158, 11, 0.3)', + iconColor: '#f59e0b', + label: '路由器' + }, + 'storage': { + borderColor: '#8b5cf6', + accentColor: '#7c3aed', + glowColor: 'rgba(139, 92, 246, 0.3)', + iconColor: '#8b5cf6', + label: '存储' + }, + 'firewall': { + borderColor: '#ef4444', + accentColor: '#dc2626', + glowColor: 'rgba(239, 68, 68, 0.3)', + iconColor: '#ef4444', + label: '防火墙' + }, + 'ups': { + borderColor: '#14b8a6', + accentColor: '#0d9488', + glowColor: 'rgba(20, 184, 166, 0.3)', + iconColor: '#14b8a6', + label: 'UPS' + }, + 'pdus': { + borderColor: '#64748b', + accentColor: '#475569', + glowColor: 'rgba(100, 116, 139, 0.3)', + iconColor: '#64748b', + label: 'PDU' + }, + 'other': { + borderColor: '#94a3b8', + accentColor: '#64748b', + glowColor: 'rgba(148, 163, 184, 0.3)', + iconColor: '#94a3b8', + label: '其他' + } + }; + return themeMap[type?.toLowerCase()] || themeMap['other']; + }; + // 生成模拟监控数据 const generateMonitoringData = (device) => { const baseTemp = device.type?.includes('服务器') ? 65 : @@ -596,35 +710,32 @@ function RackVisualization() { // 生成左侧U数标记(从U1开始显示) const generateLeftUMarks = (height) => { const marks = []; - const uHeight = 25; // 调整为更小的U高度以适应屏幕显示 + const uHeight = 25; for (let u = 1; u <= height; u++) { - // 使用与设备位置计算一致的逻辑 - // U1在最底部,计算U位标记的顶部位置(从机柜顶部算起) const topPosition = (height - u) * uHeight; marks.push(
- {/* U位序号 */}
{u} @@ -639,35 +750,32 @@ function RackVisualization() { // 生成右侧U数标记(从U1开始显示) const generateRightUMarks = (height) => { const marks = []; - const uHeight = 25; // 调整为更小的U高度以适应屏幕显示 + const uHeight = 25; for (let u = 1; u <= height; u++) { - // 使用与设备位置计算一致的逻辑 - // U1在最底部,计算U位标记的顶部位置(从机柜顶部算起) const topPosition = (height - u) * uHeight; marks.push(
- {/* U位序号 */}
{u} @@ -832,7 +940,7 @@ function RackVisualization() {
- {loadingTooltipFields ? ( -
- 加载字段配置中... -
- ) : Object.keys(tooltipFields).length === 0 ? ( -
- 暂无字段配置,请先在设备字段管理中添加字段 -
- ) : ( - <> -
+ +
- -
- - - -
- - )} + 全选/取消全选 + + + + +
-
- )} + )} + {/* 设备详情tooltip - 放在容器外部确保不被遮挡 */} {deviceTooltip && ( @@ -1686,36 +1915,70 @@ function RackVisualization() { 设备详情
- {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` : '-'}
- )} + {console.log('tooltipFields:', tooltipFields)} + {Object.entries(tooltipFields).map(([key, field]) => { + if (!field.enabled) return null; + + let value = deviceTooltip.device[field.field]; + let displayValue = '-'; + + if (value !== undefined && value !== null && value !== '') { + // 特殊字段格式化 + if (field.field === 'status') { + const statusMap = { + 'running': '运行中', + 'maintenance': '维护中', + 'offline': '离线', + 'fault': '故障', + 'warning': '警告', + 'error': '故障' + }; + displayValue = statusMap[value] || value; + } else if (field.field === 'type') { + const typeMap = { + 'server': '服务器', + 'switch': '交换机', + 'router': '路由器', + 'storage': '存储设备', + 'other': '其他设备' + }; + displayValue = typeMap[value] || value; + } else if (field.field === 'position') { + displayValue = `U${value}`; + } else if (field.field === 'height') { + displayValue = `${value}U`; + } else if (field.field === 'powerConsumption' || field.field === 'power') { + displayValue = `${value}W`; + } else if (field.fieldType === 'date') { + try { + displayValue = new Date(value).toLocaleDateString('zh-CN'); + } catch (e) { + displayValue = value; + } + } else if (field.field === 'rackId') { + displayValue = value; + } else { + displayValue = value; + } + } + + // 获取字段标签(优先使用配置中的label,否则使用fieldName) + const label = field.label || field.fieldName; + + // 状态字段使用特殊颜色 + let valueColor = '#e2e8f0'; + if (field.field === 'status') { + if (value === 'running') valueColor = '#4ade80'; + else if (value === 'warning') valueColor = '#fbbf24'; + else if (value === 'error' || value === 'fault') valueColor = '#f87171'; + } + + return ( +
+ {label}: {displayValue} +
+ ); + })}
)}