diff --git a/backend/package-lock.json b/backend/package-lock.json
index d752c5d..52ca0de 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -15,7 +15,8 @@
"express-fileupload": "^1.5.2",
"iconv-lite": "^0.6.3",
"sequelize": "^6.32.1",
- "sqlite3": "^5.1.6"
+ "sqlite3": "^5.1.6",
+ "three": "^0.182.0"
},
"devDependencies": {
"nodemon": "^3.0.1"
@@ -2730,6 +2731,12 @@
"node": ">=8"
}
},
+ "node_modules/three": {
+ "version": "0.182.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
+ "integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
+ "license": "MIT"
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
diff --git a/backend/package.json b/backend/package.json
index 805bcf9..c1a5ca3 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -14,7 +14,8 @@
"express-fileupload": "^1.5.2",
"iconv-lite": "^0.6.3",
"sequelize": "^6.32.1",
- "sqlite3": "^5.1.6"
+ "sqlite3": "^5.1.6",
+ "three": "^0.182.0"
},
"devDependencies": {
"nodemon": "^3.0.1"
diff --git a/backend/routes/devices.js b/backend/routes/devices.js
index 0a58855..7660e8b 100644
--- a/backend/routes/devices.js
+++ b/backend/routes/devices.js
@@ -14,7 +14,7 @@ const DeviceField = require('../models/DeviceField');
// 获取所有设备(支持搜索和筛选)
router.get('/', async (req, res) => {
try {
- const { keyword, status, type, page = 1, pageSize = 10 } = req.query;
+ const { keyword, status, type, rackId, page = 1, pageSize = 10 } = req.query;
const offset = (page - 1) * pageSize;
// 构建查询条件
@@ -44,6 +44,11 @@ router.get('/', async (req, res) => {
where.type = type;
}
+ // 机柜筛选(用于机柜可视化功能)
+ if (rackId) {
+ where.rackId = rackId;
+ }
+
// 执行查询
const { count, rows } = await Device.findAndCountAll({
where,
diff --git a/backend/server.js b/backend/server.js
index 1d01b38..fd475bf 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -5,7 +5,7 @@ const { sequelize } = require('./db');
// 创建Express应用
const app = express();
-const PORT = process.env.PORT || 3001;
+const PORT = process.env.PORT || 8000;
// 中间件
app.use(cors());
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e3db2ec..0e66a5a 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,7 +13,8 @@
"axios": "^1.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router-dom": "^6.15.0"
+ "react-router-dom": "^6.15.0",
+ "three": "^0.160.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.3",
@@ -2575,6 +2576,12 @@
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
"license": "MIT"
},
+ "node_modules/three": {
+ "version": "0.160.1",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.160.1.tgz",
+ "integrity": "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==",
+ "license": "MIT"
+ },
"node_modules/throttle-debounce": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 6f22dd8..1ad1e54 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,7 +14,8 @@
"axios": "^1.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router-dom": "^6.15.0"
+ "react-router-dom": "^6.15.0",
+ "three": "^0.160.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.0.3",
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 89e6ca9..05280a9 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,12 +1,13 @@
import React, { useState } from 'react';
import { Layout, Menu, theme, Button } from 'antd';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
-import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons';
+import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined } from '@ant-design/icons';
import Dashboard from './pages/Dashboard';
import DeviceManagement from './pages/DeviceManagement';
import RackManagement from './pages/RackManagement';
import RoomManagement from './pages/RoomManagement';
import DeviceFieldManagement from './pages/DeviceFieldManagement';
+import RackVisualization from './pages/RackVisualization';
const { Content, Sider } = Layout;
@@ -87,6 +88,11 @@ function App() {
icon: ,
label: 字段管理,
},
+ {
+ key: '6',
+ icon: ,
+ label: 机柜可视化,
+ },
]}
/>
@@ -106,6 +112,7 @@ function App() {
} />
} />
} />
+ } />
diff --git a/frontend/src/pages/RackVisualization.jsx b/frontend/src/pages/RackVisualization.jsx
new file mode 100644
index 0000000..0d55682
--- /dev/null
+++ b/frontend/src/pages/RackVisualization.jsx
@@ -0,0 +1,1235 @@
+import React, { useState, useEffect } from 'react';
+import { Card, Select, Button, Space, message, Tooltip } from 'antd';
+import {
+ ReloadOutlined,
+ ZoomInOutlined,
+ ZoomOutOutlined,
+ RotateRightOutlined,
+ CloudServerOutlined,
+ SwitcherOutlined,
+ DatabaseOutlined,
+ CloudOutlined,
+ LaptopOutlined,
+ MobileOutlined,
+ PrinterOutlined
+} from '@ant-design/icons';
+import axios from 'axios';
+
+const { Option } = Select;
+
+// 添加动画样式
+const style = document.createElement('style');
+style.textContent = `
+ @keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-50%) translateX(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(-50%) translateX(0);
+ }
+ }
+`;
+document.head.appendChild(style);
+
+// 错误边界组件
+class ErrorBoundary extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false, error: null };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error, errorInfo) {
+ console.error('机柜可视化组件错误:', error, errorInfo);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
可视化组件出错了
+
+ 错误信息: {this.state.error?.toString()}
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
+
+function RackVisualization() {
+ const [racks, setRacks] = useState([]);
+ const [selectedRack, setSelectedRack] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [devices, setDevices] = useState([]);
+ const [loadingDevices, setLoadingDevices] = useState(false);
+ const [zoom, setZoom] = useState(1);
+ const [rotation, setRotation] = useState(0);
+ const [error, setError] = useState(null);
+ const [hoveredDevice, setHoveredDevice] = useState(null);
+ const [hoverTimeout, setHoverTimeout] = useState(null);
+
+ // 获取所有机柜
+ const fetchRacks = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const response = await axios.get('/api/racks');
+
+ // 验证数据结构并过滤有效机柜
+ const validRacks = response.data.filter(rack =>
+ rack &&
+ typeof rack === 'object' &&
+ rack.rackId &&
+ typeof rack.height === 'number'
+ );
+
+ setRacks(validRacks);
+ if (validRacks.length > 0) {
+ setSelectedRack(validRacks[0]);
+ fetchDevices(validRacks[0].rackId);
+ } else {
+ setSelectedRack(null);
+ setDevices([]);
+ message.warning('未找到有效的机柜数据');
+ }
+ } catch (error) {
+ message.error('获取机柜列表失败');
+ console.error('获取机柜列表失败:', error);
+ setError(error);
+ setRacks([]);
+ setSelectedRack(null);
+ setDevices([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 组件卸载时清理定时器
+ useEffect(() => {
+ return () => {
+ if (hoverTimeout) {
+ clearTimeout(hoverTimeout);
+ }
+ };
+ }, [hoverTimeout]);
+
+ // 获取机柜内的设备
+ const fetchDevices = async (rackId) => {
+ try {
+ setLoadingDevices(true);
+ console.log(`=== 开始获取机柜 ${rackId} 的设备数据 ===`);
+ console.log('API URL:', `/api/devices?rackId=${rackId}`);
+
+ const response = await axios.get(`/api/devices?rackId=${rackId}`);
+
+ console.log('=== API响应完整信息 ===');
+ console.log('HTTP状态码:', response.status);
+ console.log('响应头:', response.headers);
+ console.log('响应数据类型:', typeof response.data);
+ console.log('响应数据:', JSON.stringify(response.data, null, 2));
+
+ // 处理不同的响应格式
+ let devicesData;
+ if (Array.isArray(response.data)) {
+ devicesData = response.data;
+ console.log('响应是数组格式');
+ } else if (response.data && Array.isArray(response.data.data)) {
+ devicesData = response.data.data;
+ console.log('响应是对象格式,数组在data字段中');
+ } else if (response.data && Array.isArray(response.data.devices)) {
+ devicesData = response.data.devices;
+ console.log('响应是对象格式,数组在devices字段中');
+ } else {
+ devicesData = [];
+ console.log('响应不是预期格式,设置为空数组');
+ }
+
+ console.log('=== 处理后的设备数据 ===');
+ console.log('设备数组长度:', devicesData.length);
+ console.log('设备数据:', devicesData);
+
+ // 如果没有数据,显示警告
+ if (devicesData.length === 0) {
+ console.warn('=== 没有找到设备数据 ===');
+ console.warn('可能的原因:');
+ console.warn('1. 数据库中该机柜确实没有设备');
+ console.warn('2. API路由配置错误');
+ console.warn('3. 数据库查询条件问题');
+ message.warning(`机柜 ${rackId} 暂无设备数据`);
+ setDevices([]);
+ return;
+ }
+
+ // 显示原始数据结构以供调试
+ console.log('=== 数据结构分析 ===');
+ if (devicesData.length > 0) {
+ const firstDevice = devicesData[0];
+ console.log('第一个设备完整数据:', JSON.stringify(firstDevice, null, 2));
+ console.log('第一个设备的字段:', Object.keys(firstDevice));
+ console.log('第一个设备的字段类型:');
+ Object.keys(firstDevice).forEach(key => {
+ console.log(` ${key}: ${typeof firstDevice[key]} = ${JSON.stringify(firstDevice[key])}`);
+ });
+ }
+
+ // 过滤有效的设备数据 - 放宽验证条件
+ const validDevices = devicesData.filter(device => {
+ if (!device) {
+ console.log('跳过空设备');
+ return false;
+ }
+
+ console.log('设备数据:', device);
+
+ // 只要求有任意ID字段或标识符
+ const hasId = device.deviceId || device.id || device.device_id || device.device;
+ if (!hasId) {
+ console.log('跳过缺少ID的设备:', device);
+ return false;
+ }
+
+ // 放宽名称验证
+ const hasName = device.name || device.deviceName || device.device_name || device.title;
+ if (!hasName) {
+ console.log('跳过缺少名称的设备:', device);
+ return false;
+ }
+
+ // 检查位置和高度,允许字符串类型并转换
+ let position = device.position || device.u_position || device.uPos || device.u;
+ let height = device.height || device.u_height || device.uHeight || device.size || 1;
+
+ // 如果是字符串,尝试转换为数字
+ if (typeof position === 'string') {
+ position = parseInt(position, 10);
+ }
+ if (typeof height === 'string') {
+ height = parseInt(height, 10);
+ }
+
+ if (typeof position !== 'number' || isNaN(position) || position <= 0) {
+ console.log('跳过位置无效的设备:', device);
+ return false;
+ }
+
+ if (typeof height !== 'number' || isNaN(height) || height <= 0) {
+ console.log('跳过高度无效的设备:', device);
+ return false;
+ }
+
+ // 统一化数据格式
+ device.position = position;
+ device.height = height;
+ device.name = hasName;
+ device.deviceId = hasId;
+
+ console.log('有效的设备:', device);
+ return true;
+ });
+
+ console.log('最终有效设备列表:', validDevices);
+ setDevices(validDevices);
+
+ if (validDevices.length === 0) {
+ console.warn('未找到有效设备数据');
+ message.warning('该机柜内暂无设备数据');
+ }
+ } catch (error) {
+ message.error('获取设备列表失败');
+ console.error('获取设备列表失败:', error);
+ setDevices([]);
+ } finally {
+ setLoadingDevices(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchRacks();
+ }, []);
+
+ // 根据设备类型获取图标
+ const getDeviceIcon = (deviceType) => {
+ try {
+ if (!deviceType) return ;
+ const type = deviceType.toLowerCase();
+
+ if (type.includes('server') || type.includes('服务器')) return ;
+ if (type.includes('switch') || type.includes('交换机')) return ;
+ if (type.includes('storage') || type.includes('存储')) return ;
+ if (type.includes('router') || type.includes('路由器')) return ;
+ if (type.includes('laptop') || type.includes('笔记本')) return ;
+ if (type.includes('mobile') || type.includes('手机')) return ;
+ if (type.includes('printer') || type.includes('打印机')) return ;
+
+ return ;
+ } catch (error) {
+ console.error('设备图标渲染错误:', error);
+ return ;
+ }
+ };
+
+ // 根据设备类型获取背景色
+ const getDeviceColor = (deviceType) => {
+ if (!deviceType) return '#1890ff';
+ const type = deviceType.toLowerCase();
+
+ if (type.includes('server') || type.includes('服务器')) return '#1890ff'; // 蓝色
+ if (type.includes('switch') || type.includes('交换机')) return '#52c41a'; // 绿色
+ if (type.includes('storage') || type.includes('存储')) return '#faad14'; // 黄色
+ if (type.includes('router') || type.includes('路由器')) return '#f5222d'; // 红色
+ if (type.includes('laptop') || type.includes('笔记本')) return '#722ed1'; // 紫色
+ if (type.includes('mobile') || type.includes('手机')) return '#eb2f96'; // 粉色
+ if (type.includes('printer') || type.includes('打印机')) return '#13c2c2'; // 青色
+
+ return '#1890ff'; // 默认蓝色
+ };
+
+ const getDeviceStyle = (device, rackHeight) => {
+ // 添加参数验证
+ if (!device || typeof device.position !== 'number' || typeof device.height !== 'number') {
+ return {};
+ }
+
+ const uHeight = 18; // 每个U的高度(像素)
+ 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);
+
+ // 计算设备的顶部位置(从机柜顶部算起)
+ // 设备占用从 position 到 position + height - 1 的U
+ // 机柜顶部是U0,所以设备顶部的topPosition是:
+ const deviceBottomU = position; // 设备底部U数
+ const deviceTopU = position + deviceUHeight - 1; // 设备顶部U数
+ const topPosition = (rackHeight - deviceTopU - 1) * uHeight;
+
+ return {
+ height: `${deviceHeight}px`, // 确保设备高度精确等于U位高度
+ top: `${topPosition}px`, // 确保设备顶部与U位网格线对齐
+ // 移除任何可能影响占满U位的样式
+ margin: 0,
+ padding: 0
+ };
+ };
+
+ // 格式化日期
+ const formatDate = (dateString) => {
+ if (!dateString) return '未知';
+ try {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('zh-CN');
+ } catch (error) {
+ return '无效日期';
+ }
+ };
+
+ // 获取设备类型中文名称
+ const getDeviceTypeName = (type) => {
+ const typeMap = {
+ 'server': '服务器',
+ 'switch': '交换机',
+ 'router': '路由器',
+ 'storage': '存储设备',
+ 'ups': 'UPS',
+ 'firewall': '防火墙',
+ 'other': '其他设备'
+ };
+ return typeMap[type?.toLowerCase()] || '未知设备';
+ };
+
+ // 生成U数标记(每个U都有标记)
+ const generateUMarks = (height) => {
+ const marks = [];
+ const uHeight = 18;
+
+ for (let i = 1; i <= height; i++) {
+ marks.push(
+
+
+ {i}
+
+
+ );
+ }
+
+ return marks;
+ };
+
+ // 生成左侧U数标记(从实际范围开始显示)
+ const generateLeftUMarks = (height) => {
+ const marks = [];
+ const uHeight = 18;
+
+ // 从实际U位范围开始(通常是4-42U或类似范围)
+ const startU = Math.max(1, height - 40); // 假设机柜有40U左右,从较高位置开始显示
+
+ for (let i = 1; i <= height; i++) {
+ const actualU = startU + i - 1;
+ if (actualU <= 42) { // 只显示到42U
+ marks.push(
+
+ {/* U位网格背景 */}
+
+ {/* U位序号 */}
+
+ {actualU}
+
+
+ );
+ }
+ }
+
+ return marks;
+ };
+
+ // 生成右侧U数标记(从实际范围开始显示)
+ const generateRightUMarks = (height) => {
+ const marks = [];
+ const uHeight = 18;
+
+ // 从实际U位范围开始(通常是4-42U或类似范围)
+ const startU = Math.max(1, height - 40); // 假设机柜有40U左右,从较高位置开始显示
+
+ for (let i = 1; i <= height; i++) {
+ const actualU = startU + i - 1;
+ if (actualU <= 42) { // 只显示到42U
+ marks.push(
+
+ {/* U位网格背景 */}
+
+ {/* U位序号 */}
+
+ {actualU}
+
+
+ );
+ }
+ }
+
+ return marks;
+ };
+
+ // 选择机柜
+ const handleRackChange = (rackId) => {
+ const rack = racks.find(r => r.rackId === rackId);
+ if (rack) {
+ setSelectedRack(rack);
+ fetchDevices(rackId);
+ }
+ };
+
+ // 重新加载数据
+ const handleReload = () => {
+ setLoading(true);
+ fetchRacks().then(() => {
+ message.success('数据已刷新');
+ });
+ };
+
+ // 重置视角
+ const handleResetView = () => {
+ setZoom(1);
+ setRotation(0);
+ };
+
+ // 放大
+ const handleZoomIn = () => {
+ setZoom(prev => Math.min(prev + 0.2, 2.5));
+ };
+
+ // 缩小
+ const handleZoomOut = () => {
+ setZoom(prev => Math.max(prev - 0.2, 0.5));
+ };
+
+ // 无需额外的初始化,CSS 3D变换直接在JSX中实现
+
+ return (
+
+
+
+ }
+ onClick={handleReload}
+ loading={loading}
+ disabled={loading}
+ >
+ 刷新
+
+
+ }
+ >
+
+ } onClick={handleZoomIn}>放大
+ } onClick={handleZoomOut}>缩小
+ } onClick={handleResetView}>重置视角
+
+
+
+ {loading ? (
+
+ 加载机柜数据中...
+
+ ) : selectedRack ? (
+
+ {loadingDevices && (
+
+ )}
+
+ {/* 机柜主体 */}
+
+ {/* 前面板 */}
+
+ {/* 左侧U数标记 */}
+
+ {generateLeftUMarks(selectedRack.height)}
+
+
+ {/* 右侧U数标记 */}
+
+ {generateRightUMarks(selectedRack.height)}
+
+
+ {/* 原有的U数标记(内部) */}
+ {generateUMarks(selectedRack.height)}
+
+ {/* 设备 */}
+ {/* 调试信息 */}
+
+ 设备数量: {devices.length}
+
+
+ {devices.length > 0 ? devices.map(device => {
+ try {
+ // 处理统一化后的设备数据
+ const deviceId = device?.deviceId || device?.id || device?.device_id || device?.device || `device-${Math.random()}`;
+ const deviceName = device?.name || device?.deviceName || device?.device_name || device?.title || '未知设备';
+ const position = device?.position || 1;
+ const height = device?.height || 1;
+
+ return (
+
{
+ e.currentTarget.style.transform = 'translateZ(25px) scale(1.01)';
+ e.currentTarget.style.boxShadow = '0 6px 16px rgba(0,0,0,0.4)';
+
+ // 清除之前的定时器
+ if (hoverTimeout) {
+ clearTimeout(hoverTimeout);
+ setHoverTimeout(null);
+ }
+
+ // 延迟显示设备信息,避免快速移动时的闪烁
+ const timeout = setTimeout(() => {
+ // 使用设备ID确保状态设置正确
+ setHoveredDevice({
+ ...device,
+ _key: device.deviceId || device.id || device.device_id || device.device
+ });
+ }, 100);
+
+ setHoverTimeout(timeout);
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.transform = 'translateZ(20px) scale(1)';
+ e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
+
+ // 清除之前的定时器
+ if (hoverTimeout) {
+ clearTimeout(hoverTimeout);
+ setHoverTimeout(null);
+ }
+
+ // 延迟隐藏设备信息,给用户一些缓冲时间
+ const timeout = setTimeout(() => {
+ setHoveredDevice(null);
+ }, 200); // 增加延迟时间到200ms
+
+ setHoverTimeout(timeout);
+ }}
+ >
+ {/* 左侧指示灯区域 */}
+
+
+ {/* 中间模块/接口槽位区域 */}
+
+ {/* 模拟接口槽位 */}
+
+ {Array.from({length: Math.min(6, Math.max(3, Math.floor(height * 1.5)))}, (_, i) => (
+
+ ))}
+
+
+ {/* 设备图标和名称 - 居中显示 */}
+
+
+ {getDeviceIcon(device.type)}
+
+
+ {deviceName}
+
+
+
+
+ {/* 右侧U高度标签 */}
+
+ {height}U
+
+
+ );
+ } catch (error) {
+ console.error('设备渲染错误:', error, device);
+ // 渲染一个简单的错误显示元素
+ return (
+
+ 设备加载错误
+
+ );
+ }
+ }) : (
+
+ 暂无设备数据
+
+ )}
+
+
+ {/* 侧面 */}
+
+
+ {/* 机柜顶部 */}
+
+
+ {/* 机柜名称 */}
+
+ {selectedRack.name} ({selectedRack.rackId})
+
+
+
+ {/* 设备详情面板 */}
+ {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';
+ }}
+ >
+ ×
+
+
+ )}
+
+
+ ) : (
+
+ 请选择机柜
+
+ )}
+
+
+
+ );
+}
+
+export default RackVisualization;
\ No newline at end of file
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index 8fe8591..a6748bc 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -7,7 +7,7 @@ export default defineConfig({
port: 3000,
proxy: {
'/api': {
- target: 'http://localhost:3001',
+ target: 'http://localhost:8000',
changeOrigin: true
}
}