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() {
>
刷新
+ }
+ onClick={() => setShowTooltipConfig(true)}
+ type={showTooltipConfig ? 'primary' : 'default'}
+ >
+ 字段配置
+
}
>
@@ -804,46 +935,46 @@ function RackVisualization() {
- {getDeviceTypeName(hoveredDevice.type)}
-
- {hoveredDevice.name || hoveredDevice.deviceName || hoveredDevice.device_name || '未知设备'}
-
-