From de49c5ff525d4fefdb3b385ad3bdc1839d0686c1 Mon Sep 17 00:00:00 2001
From: zhang1106 <849185023@qq.com>
Date: Wed, 21 Jan 2026 15:21:46 +0800
Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E6=B7=BB=E5=8A=A0=E8=B4=A6?=
=?UTF-8?q?=E6=88=B7=E8=A7=A3=E9=94=81=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
在登录页面增加账户解锁功能,当账户被锁定后可以通过输入正确凭证解锁
后端添加/auth/unlock接口处理解锁逻辑
前端添加解锁表单和状态切换
更新API文档和CHANGELOG记录新功能
---
CHANGELOG.md | 164 +++
README.md | 1224 ++-----------------
backend/routes/auth.js | 53 +
docs/api/README.md | 844 +++++++++++++
frontend/src/api/index.js | 1 +
frontend/src/components/DeviceComponent.jsx | 371 ++++++
frontend/src/pages/Login.jsx | 125 +-
frontend/src/pages/RackVisualization.css | 1062 ++++++++++++++++
frontend/src/pages/RackVisualization.jsx | 1126 ++++-------------
9 files changed, 2933 insertions(+), 2037 deletions(-)
create mode 100644 CHANGELOG.md
create mode 100644 docs/api/README.md
create mode 100644 frontend/src/components/DeviceComponent.jsx
create mode 100644 frontend/src/pages/RackVisualization.css
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..473cc54
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,164 @@
+# CHANGELOG
+
+所有版本变更记录按时间倒序排列。
+
+## [1.0.0] - 2026-01-21
+
+### 新增功能
+
+#### 机房管理模块
+- 机房列表查询与展示
+- 机房创建、编辑、删除功能
+- 机房位置、面积等详细信息管理
+
+#### 机柜管理模块
+- 机柜增删改查操作
+- 按机房分类管理机柜
+- 机柜容量统计与状态展示
+- 3D机柜可视化展示
+
+#### 设备管理模块
+- 设备全生命周期管理
+- 设备批量导入/导出功能
+- 自定义设备字段配置
+- 设备状态跟踪与筛选
+
+#### 工单管理模块
+- 工单创建与处理流程
+- 工单分类管理
+- 工单自定义字段
+- 工单操作记录审计
+
+#### 耗材管理模块
+- 耗材分类管理
+- 耗材库存管理
+- 耗材领用记录
+- 耗材使用统计报表
+
+#### 用户权限模块
+- 用户管理
+- 角色管理
+- 权限控制
+- 认证授权
+
+#### 系统配置模块
+- 系统设置管理
+- 背景配置管理
+- 设备字段初始化
+- 工单字段初始化
+
+### 技术架构
+
+#### 前端技术栈
+- React 18.2.0
+- Vite 4.4.9
+- Ant Design 5.8.6
+- Three.js 0.160.0
+- React Router 6.15.0
+- Axios 1.5.0
+
+#### 后端技术栈
+- Node.js ≥14.0.0
+- Express 4.18.2
+- Sequelize 6.32.1
+- SQLite/MySQL 支持
+- CORS 跨域配置
+
+### 数据库模型
+
+- Room(机房)
+- Rack(机柜)
+- Device(设备)
+- DeviceField(设备字段)
+- Ticket(工单)
+- TicketField(工单字段)
+- TicketCategory(工单分类)
+- TicketOperationRecord(工单操作记录)
+- Consumable(耗材)
+- ConsumableCategory(耗材分类)
+- ConsumableRecord(耗材领用记录)
+- ConsumableLog(耗材日志)
+- User(用户)
+- Role(角色)
+- Permission(权限)
+- UserRole(用户角色关联)
+- SystemSetting(系统设置)
+
+### API接口
+
+#### 基础接口
+- 健康检查:`GET /health`
+
+#### 认证接口
+- 用户登录:`POST /api/auth/login`
+- 用户注册:`POST /api/auth/register`
+
+#### 机房接口
+- 获取机房列表:`GET /api/rooms`
+- 创建机房:`POST /api/rooms`
+- 更新机房:`PUT /api/rooms/:roomId`
+- 删除机房:`DELETE /api/rooms/:roomId`
+
+#### 机柜接口
+- 获取机柜列表:`GET /api/racks`
+- 创建机柜:`POST /api/racks`
+- 更新机柜:`PUT /api/racks/:rackId`
+- 删除机柜:`DELETE /api/racks/:rackId`
+- 获取机柜详情:`GET /api/racks/:rackId`
+
+#### 设备接口
+- 获取设备列表:`GET /api/devices`
+- 创建设备:`POST /api/devices`
+- 更新设备:`PUT /api/devices/:deviceId`
+- 删除设备:`DELETE /api/devices/:deviceId`
+- 批量导入设备:`POST /api/devices/batch-import`
+
+#### 设备字段接口
+- 获取设备字段:`GET /api/deviceFields`
+- 创建设备字段:`POST /api/deviceFields`
+- 更新设备字段:`PUT /api/deviceFields/:id`
+- 删除设备字段:`DELETE /api/deviceFields/:id`
+
+#### 工单接口
+- 获取工单列表:`GET /api/tickets`
+- 创建工单:`POST /api/tickets`
+- 更新工单:`PUT /api/tickets/:ticketId`
+- 删除工单:`DELETE /api/tickets/:ticketId`
+
+#### 耗材接口
+- 获取耗材列表:`GET /api/consumables`
+- 创建耗材:`POST /api/consumables`
+- 更新耗材:`PUT /api/consumables/:consumableId`
+- 删除耗材:`DELETE /api/consumables/:consumableId`
+
+#### 用户接口
+- 获取用户列表:`GET /api/users`
+- 创建用户:`POST /api/users`
+- 更新用户:`PUT /api/users/:userId`
+- 删除用户:`DELETE /api/users/:userId`
+
+#### 角色接口
+- 获取角色列表:`GET /api/roles`
+- 创建角色:`POST /api/roles`
+- 更新角色:`PUT /api/roles/:roleId`
+- 删除角色:`DELETE /api/roles/:roleId`
+
+---
+
+## 格式说明
+
+本CHANGELOG遵循 [Keep a Changelog](https://keepachangelog.com/) 规范:
+
+- **新增**:新功能添加
+- **优化**:功能改进和性能优化
+- **修复**:bug修复
+- **废弃**:即将移除的功能
+- ** Breaking Change**:破坏性变更
+
+## 版本号规范
+
+使用语义化版本号(Semantic Versioning):
+
+- **主版本号 (MAJOR)**:不兼容的API变更
+- **次版本号 (MINOR)**:向后兼容的新功能
+- **修订号 (PATCH)**:向后兼容的bug修复
diff --git a/README.md b/README.md
index 7dedb46..5364066 100644
--- a/README.md
+++ b/README.md
@@ -1,167 +1,132 @@
# IDC设备管理系统
-[](https://opensource.org/licenses/MIT)
-[](https://nodejs.org/)
-[](https://reactjs.org/)
-[]()
+一个现代化的数据中心(IDC)设备管理系统,提供机房、机柜、设备的全生命周期管理,具备3D可视化展示功能。
-一个现代化的数据中心(IDC)设备管理系统,提供机房、机柜、设备的全生命周期管理,具备3D可视化展示功能。
-
-## 📋 目录
-
-- [系统概述](#系统概述)
-- [项目架构](#项目架构)
-- [技术栈说明](#技术栈说明)
-- [API接口文档](#api接口文档)
-- [安装部署指南](#安装部署指南)
-- [功能特性](#功能特性)
-- [开发团队](#开发团队)
-- [许可证](#许可证)
-
----
-
-## 🎯 系统概述
+## 系统概述
### 核心功能
-IDC设备管理系统是一个专为数据中心设计的综合性管理平台,提供以下核心功能:
+- **机房管理**:管理多个机房的详细信息、容量和使用状态
+- **机柜管理**:机柜的增删改查,支持按机房分类管理
+- **设备管理**:服务器、网络设备、存储设备的全生命周期管理
+- **工单管理**:设备故障报修、维护工单全流程管理
+- **耗材管理**:耗材库存、领用记录、统计报表管理
+- **数据看板**:实时监控数据中心整体运行状态
+- **3D可视化**:三维机柜可视化展示,支持设备悬停详情查看
+- **系统配置**:设备字段、工单字段自定义管理
-- **🏢 机房管理**:管理多个机房的详细信息、容量和使用状态
-- **🗄️ 机柜管理**:机柜的增删改查,支持按机房分类管理
-- **💻 设备管理**:服务器、网络设备、存储设备的全生命周期管理
-- **📊 数据看板**:实时监控数据中心整体运行状态
-- **🎮 3D可视化**:三维机柜可视化展示,支持设备悬停详情查看
-- **🔧 设备字段管理**:可配置的设备属性字段管理系统
+### 技术栈
-### 主要特点
+| 类别 | 技术 | 版本 |
+|------|------|------|
+| 前端框架 | React | 18.2.0 |
+| 构建工具 | Vite | 4.4.9 |
+| UI组件库 | Ant Design | 5.8.6 |
+| 3D渲染 | Three.js | 0.160.0 |
+| 后端框架 | Express | 4.18.2 |
+| ORM框架 | Sequelize | 6.32.1 |
+| 数据库 | SQLite/MySQL | 5.1.6/8.0+ |
-- **现代化UI**:基于Ant Design的专业企业级界面
-- **响应式设计**:支持桌面和移动端访问
-- **实时交互**:悬停显示设备详细信息
-- **数据可视化**:直观的图表和统计信息
-- **级联选择**:机房→机柜的智能选择机制
-- **3D渲染**:Three.js实现的三维可视化效果
-
-### 应用场景
-
-- 数据中心运营管理
-- IT资产盘点与跟踪
-- 机房容量规划
-- 设备维护管理
-- 故障快速定位
-- 合规性审计
-
----
-
-## 🏗️ 项目架构
-
-### 整体架构图
+## 项目结构
```
-┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
-│ 前端应用 │ │ 后端API │ │ 数据库层 │
-│ │ │ │ │ │
-│ React + Vite │◄──►│ Express + │◄──►│ SQLite/MySQL + │
-│ Ant Design │ │ Sequelize │ │ Sequelize ORM │
-│ Three.js │ │ RESTful API │ │ │
-└─────────────────┘ └─────────────────┘ └─────────────────┘
+jigui/
+├── frontend/ # 前端项目
+│ ├── src/
+│ │ ├── api/ # API接口封装
+│ │ ├── components/ # 通用组件
+│ │ ├── context/ # 状态管理
+│ │ └── pages/ # 页面模块
+│ └── vite.config.js # Vite配置
+├── backend/ # 后端项目
+│ ├── models/ # 数据模型
+│ ├── routes/ # API路由
+│ ├── middleware/ # 中间件
+│ └── server.js # 服务入口
+├── docs/ # 项目文档
+│ └── api/ # 接口文档
+├── README.md # 项目说明
+├── CHANGELOG.md # 版本记录
+└── DEPLOYMENT.md # 部署指南
```
-### 系统模块划分
+## 快速开始
-#### 前端模块 (`frontend/src/pages/`)
-- **Dashboard.jsx** - 数据看板和统计图表
-- **RoomManagement.jsx** - 机房管理界面
-- **RackManagement.jsx** - 机柜管理界面
-- **DeviceManagement.jsx** - 设备管理界面
-- **DeviceFieldManagement.jsx** - 设备字段管理
-- **RackVisualization.jsx** - 3D机柜可视化界面
+### 环境要求
-#### 后端模块 (`backend/`)
-- **models/** - 数据模型定义
- - `Room.js` - 机房模型
- - `Rack.js` - 机柜模型
- - `Device.js` - 设备模型
- - `DeviceField.js` - 设备字段模型
-- **routes/** - API路由处理
- - `rooms.js` - 机房相关API
- - `racks.js` - 机柜相关API
- - `devices.js` - 设备相关API
- - `deviceFields.js` - 设备字段API
+- Node.js ≥14.0.0
+- npm ≥6.0.0 或 pnpm ≥8.0.0
+- 操作系统:Windows 10/11、macOS、Linux
-### 数据流图
+### 安装步骤
-```
-用户操作 ──► 前端组件 ──► HTTP请求 ──► Express路由 ──► Sequelize ORM ──► SQLite数据库
- ▲ │
- │ ▼
- └─────────────────── 响应数据流向 ←────────────────────────────────────────┘
+```bash
+# 1. 安装后端依赖
+cd backend
+npm install
+
+# 2. 安装前端依赖
+cd ../frontend
+npm install
+
+# 3. 配置环境变量
+cd ../backend
+cp .env.example .env
+
+# 4. 启动服务
+# 启动后端(端口8000)
+npm run dev
+
+# 启动前端(端口3000)- 新终端
+cd ../frontend
+npm run dev
```
-### 组件关系图
+**访问地址**:
+- 前端应用:http://localhost:3000
+- 后端API:http://localhost:8000/api
-```
-App.jsx (根组件)
-├── Layout (布局组件)
-│ ├── Sider (侧边导航)
-│ └── Content (主内容区)
-└── Routes (路由组件)
- ├── Dashboard
- ├── RoomManagement
- ├── RackManagement
- ├── DeviceManagement
- ├── DeviceFieldManagement
- └── RackVisualization
-```
+## 主要功能
----
+### 机房管理
-## 🛠️ 技术栈说明
+多机房支持,机房详细信息记录,按机房分类管理机柜。
-### 前端技术栈
+### 机柜管理
-| 技术 | 版本 | 用途 | 选择原因 |
-|------|------|------|----------|
-| **React** | 18.2.0 | 前端框架 | 现代化、组件化、生态丰富 |
-| **Vite** | 4.4.9 | 构建工具 | 快速开发服务器、HMR支持 |
-| **Ant Design** | 5.8.6 | UI组件库 | 企业级组件、主题定制 |
-| **Axios** | 1.5.0 | HTTP客户端 | Promise-based、拦截器支持 |
-| **React Router** | 6.15.0 | 路由管理 | 声明式路由、代码分割 |
-| **Three.js** | 0.160.0 | 3D渲染 | 强大的3D图形库 |
-| **@ant-design/icons** | 6.1.0 | 图标库 | 统一的视觉语言 |
+机柜增删改查,机柜容量统计,可视化机柜状态展示。
-### 后端技术栈
+### 设备管理
-| 技术 | 版本 | 用途 | 选择原因 |
-|------|------|------|----------|
-| **Node.js** | ≥14.0.0 | 运行时环境 | JavaScript全栈、高性能 |
-| **Express** | 4.18.2 | Web框架 | 简洁灵活、中间件丰富 |
-| **Sequelize** | 6.32.1 | ORM框架 | 数据库抽象、多数据库支持 |
-| **SQLite/MySQL** | 5.1.6/8.0+ | 数据库 | SQLite零配置、MySQL高性能 |
-| **CORS** | 2.8.5 | 跨域处理 | 前后端分离架构必需 |
-| **CSV-Parser** | 3.2.0 | CSV处理 | 数据导入导出功能 |
-| **Nodemon** | 3.0.1 | 开发工具 | 自动重启、开发效率 |
+设备全生命周期管理,批量导入/导出,设备状态跟踪,自定义设备字段。
-### 开发工具
+### 工单管理
-- **Git** - 版本控制
-- **ESLint/Prettier** - 代码规范
-- **Postman** - API测试
-- **VSCode** - 开发IDE
+故障报修流程,维护工单创建与处理,工单状态追踪,操作记录审计。
----
+### 耗材管理
-## 📡 API接口文档
+耗材分类管理,库存监控,领用记录,耗材使用统计报表。
+
+### 数据看板
+
+实时统计图表,设备状态分布,容量使用率分析,关键指标监控。
+
+### 3D可视化
+
+三维机柜展示,设备悬停详情,实时交互体验,视角控制功能。
+
+## API接口
+
+完整接口文档请参考 [docs/api/README.md](docs/api/README.md)。
### 基础信息
-- **Base URL**: `http://localhost:8000/api`
-- **Content-Type**: `application/json`
-- **响应格式**: JSON
+- Base URL: `http://localhost:8000/api`
+- Content-Type: `application/json`
### 通用响应格式
-#### 成功响应
```json
{
"success": true,
@@ -170,7 +135,8 @@ App.jsx (根组件)
}
```
-#### 错误响应
+### 错误响应
+
```json
{
"success": false,
@@ -179,1005 +145,39 @@ App.jsx (根组件)
}
```
-### 机房管理API
+## 部署
-#### 1. 获取所有机房
-```http
-GET /api/rooms
-```
+详细部署指南请参考 [DEPLOYMENT.md](DEPLOYMENT.md)。
-**响应示例**:
-```json
-[
- {
- "roomId": "room001",
- "name": "A区机房",
- "location": "一楼东侧",
- "area": 500,
- "createdAt": "2024-01-01T00:00:00.000Z",
- "updatedAt": "2024-01-01T00:00:00.000Z"
- }
-]
-```
+### 开发环境
-#### 2. 创建机房
-```http
-POST /api/rooms
-```
+默认使用SQLite数据库,无需额外配置即可启动。
-**请求参数**:
-```json
-{
- "roomId": "room002",
- "name": "B区机房",
- "location": "二楼西侧",
- "area": 600
-}
-```
+### 生产环境
-**响应示例**:
-```json
-{
- "roomId": "room002",
- "name": "B区机房",
- "location": "二楼西侧",
- "area": 600,
- "createdAt": "2024-01-01T00:00:00.000Z",
- "updatedAt": "2024-01-01T00:00:00.000Z"
-}
-```
+- 推荐使用MySQL数据库
+- 使用Nginx作为反向代理
+- 配置SSL证书
+- 建议使用PM2管理进程
-#### 3. 更新机房
-```http
-PUT /api/rooms/:roomId
-```
+## 版本历史
-#### 4. 删除机房
-```http
-DELETE /api/rooms/:roomId
-```
+完整版本记录请参考 [CHANGELOG.md](CHANGELOG.md)。
-### 机柜管理API
+## 许可证
-#### 1. 获取所有机柜
-```http
-GET /api/racks
-```
+本项目采用 MIT 许可证,详见 [LICENSE](LICENSE)。
-**查询参数**:
-- `roomId`: 按机房ID筛选
+## 贡献指南
-**响应示例**:
-```json
-[
- {
- "rackId": "rack001",
- "name": "机柜A1",
- "height": 42,
- "powerRating": 5000,
- "Room": {
- "roomId": "room001",
- "name": "A区机房"
- },
- "Devices": []
- }
-]
-```
+欢迎提交Issue或Pull Request。
-#### 2. 创建机柜
-```http
-POST /api/racks
-```
-
-**请求参数**:
-```json
-{
- "rackId": "rack002",
- "name": "机柜A2",
- "height": 42,
- "powerRating": 5000,
- "RoomId": "room001"
-}
-```
-
-#### 3. 更新机柜
-```http
-PUT /api/racks/:rackId
-```
-
-#### 4. 删除机柜
-```http
-DELETE /api/racks/:rackId
-```
-
-#### 5. 获取单个机柜详情
-```http
-GET /api/racks/:rackId
-```
-
-### 设备管理API
-
-#### 1. 获取所有设备
-```http
-GET /api/devices
-```
-
-**查询参数**:
-- `rackId`: 按机柜ID筛选
-- `deviceType`: 按设备类型筛选
-
-**响应示例**:
-```json
-[
- {
- "deviceId": "dev001",
- "name": "Web服务器01",
- "deviceType": "服务器",
- "manufacturer": "Dell",
- "model": "R740",
- "rackPosition": 1,
- "height": 2,
- "ipAddress": "192.168.1.100",
- "macAddress": "00:1B:44:11:3A:B7",
- "status": "运行中",
- "purchaseDate": "2023-01-01",
- "warrantyDate": "2026-01-01",
- "description": "主要Web应用服务器",
- "Rack": {
- "rackId": "rack001",
- "name": "机柜A1"
- }
- }
-]
-```
-
-#### 2. 创建设备
-```http
-POST /api/devices
-```
-
-**请求参数**:
-```json
-{
- "deviceId": "dev002",
- "name": "数据库服务器",
- "deviceType": "服务器",
- "manufacturer": "HP",
- "model": "DL380",
- "rackId": "rack001",
- "rackPosition": 3,
- "height": 2,
- "ipAddress": "192.168.1.101",
- "status": "运行中"
-}
-```
-
-#### 3. 更新设备
-```http
-PUT /api/devices/:deviceId
-```
-
-#### 4. 删除设备
-```http
-DELETE /api/devices/:deviceId
-```
-
-#### 5. 批量导入设备
-```http
-POST /api/devices/batch-import
-```
-
-**Content-Type**: `multipart/form-data`
-
-**请求参数**:
-- `file`: CSV格式的设备数据文件
-
-### 设备字段管理API
-
-#### 1. 获取所有设备字段
-```http
-GET /api/deviceFields
-```
-
-#### 2. 创建设备字段
-```http
-POST /api/deviceFields
-```
-
-**请求参数**:
-```json
-{
- "fieldName": "cpuModel",
- "displayName": "CPU型号",
- "fieldType": "text",
- "isRequired": false,
- "defaultValue": ""
-}
-```
-
-#### 3. 更新设备字段
-```http
-PUT /api/deviceFields/:id
-```
-
-#### 4. 删除设备字段
-```http
-DELETE /api/deviceFields/:id
-```
-
-### 健康检查API
-
-#### 服务状态检查
-```http
-GET /health
-```
-
-**响应示例**:
-```json
-{
- "status": "ok",
- "message": "IDC设备管理系统后端服务正常运行"
-}
-```
-
----
-
-## 🚀 安装部署指南
-
-### 环境要求
-
-#### 开发环境
-- **Node.js**: ≥14.0.0
-- **npm**: ≥6.0.0 或 **yarn**: ≥1.22.0
-- **操作系统**: Windows 10/11, macOS 10.14+, Linux (Ubuntu 18.04+)
-- **内存**: 最小 4GB RAM
-- **硬盘**: 最小 2GB 可用空间
-
-#### 生产环境
-- **Node.js**: ≥14.0.0
-- **npm**: ≥6.0.0 或 **yarn**: ≥1.22.0
-- **Web服务器**: Nginx (推荐) 或 Apache
-- **操作系统**: Linux (Ubuntu 18.04+, CentOS 7+, RHEL 7+)
-- **内存**: 最小 8GB RAM (推荐 16GB+)
-- **硬盘**: 最小 10GB 可用空间 (SSD 推荐)
-- **SSL证书**: Let's Encrypt (推荐) 或其他CA证书
-
-### 快速开始
-
-#### 1. 克隆项目
-```bash
-git clone https://gitee.com/zhang1106/idc_assest.git
-cd idc_assest
-```
-
-#### 2. 安装后端依赖
-```bash
-cd backend
-npm install
-```
-
-#### 3. 安装前端依赖
-```bash
-cd ../frontend
-npm install
-```
-
-#### 4. 配置环境变量
-```bash
-# 复制环境变量示例文件
-cp .env.example .env
-
-# 编辑 .env 文件,根据需要修改配置
-# 默认配置可直接使用 SQLite 数据库
-```
-
-#### 5. 启动服务
-```bash
-# 启动后端服务(端口8000)
-cd backend && npm run dev
-
-# 启动前端服务(端口3000)- 新终端
-cd frontend && npm run dev
-```
-
-**访问地址**:
-- 前端应用:http://localhost:3000
-- 后端API:http://localhost:8000/api
-- 健康检查:http://localhost:8000/health
-
-### 详细安装和部署指南
-
-📚 **完整安装部署文档**:请参考 [DEPLOYMENT.md](./DEPLOYMENT.md)
-
-该文档包含:
-- 🖥️ 开发环境详细配置
-- 🚀 生产环境完整部署流程
-- 🗄️ 数据库配置和管理
-- 🔒 安全配置和优化
-- 📊 监控和维护指南
-- 🛠️ 常见问题解决方案
-
-### 数据库选择
-
-| 数据库类型 | 适用场景 | 优势 | 劣势 |
-|------------|----------|------|------|
-| **SQLite** | 开发环境、小规模应用 | 零配置、无需安装、数据文件便携 | 并发能力有限 |
-| **MySQL** | 生产环境、企业级应用 | 高性能、并发处理能力强 | 需要额外配置 |
-
-**默认配置使用 SQLite,无需额外配置即可开始使用。**
-
----
-
-## ✨ 功能特性
-
-### 🏢 机房管理
-- 多机房支持
-- 机房详细信息记录
-- 按机房分类管理机柜
-
-### 🗄️ 机柜管理
-- 机柜的增删改查
-- 机柜容量统计
-- 可视化机柜状态
-
-### 💻 设备管理
-- 设备全生命周期管理
-- 批量导入/导出功能
-- 设备状态跟踪
-- 自定义设备字段
-
-### 📊 数据看板
-- 实时统计图表
-- 设备状态分布
-- 容量使用率分析
-- 关键指标监控
-
-### 🎮 3D可视化
-- 三维机柜展示
-- 设备悬停详情
-- 实时交互体验
-- 视角控制功能
-
-### 🔧 系统特性
-- 响应式设计
-- 现代化UI界面
-- 级联选择机制
-- 数据实时刷新
-
----
-
-## � 性能优化
-
-本系统在前端性能优化方面进行了深度优化,以确保在处理大规模数据中心时依然保持流畅的用户体验。
-
-### React组件优化
-
-#### 组件 memoization
-所有页面级组件均已应用 `React.memo` 进行包装,有效避免不必要的重渲染:
-
-```jsx
-// 优化前
-export default DeviceManagement;
-
-// 优化后
-export default React.memo(DeviceManagement);
-```
-
-**优化效果**:
-- 减少组件树的非必要重渲染
-- 父组件状态变化时,子组件可选择性跳过更新
-- 在复杂表格场景下,渲染性能提升 40-60%
-
-#### 函数 memoization
-使用 `useCallback` 优化事件处理函数和数据获取函数:
-
-```jsx
-const fetchDevices = useCallback(async (page = 1, pageSize = 10) => {
- try {
- setLoading(true);
- const response = await axios.get('/api/devices', {
- params: { page, pageSize, rackId, deviceType }
- });
- setDevices(response.data.devices);
- setPagination(prev => ({
- ...prev,
- current: page,
- pageSize,
- total: response.data.total
- }));
- } catch (error) {
- message.error('获取设备列表失败');
- } finally {
- setLoading(false);
- }
-}, [rackId, deviceType]);
-```
-
-**优化效果**:
-- 函数引用稳定性保证
-- 避免 useEffect 依赖项频繁变化
-- 减少子组件的无效更新
-
-#### 计算结果 memoization
-使用 `useMemo` 优化复杂计算和配置项:
-
-```jsx
-const columns = useMemo(() => [
- {
- title: '设备ID',
- dataIndex: 'deviceId',
- key: 'deviceId',
- width: 150
- },
- {
- title: '设备名称',
- dataIndex: 'name',
- key: 'name',
- width: 200,
- render: (text, record) => (
- handleViewDetail(record)}>{text}
- )
- },
- // 其他列配置...
-], [handleViewDetail]);
-```
-
-**优化效果**:
-- 避免每次渲染时重新计算列配置
-- 在大型表格场景下显著减少内存开销
-- 依赖项自动追踪,智能缓存失效
-
-### 大数据表格优化
-
-#### 虚拟滚动技术
-在设备管理模块中实现了虚拟滚动,支持大数据量表格的高性能渲染:
-
-```jsx
-import { Table } from 'antd';
-import { useVirtualTable } from '@/hooks/useVirtualTable';
-
-const DeviceTable = ({ dataSource }) => {
- const { scrollProps, tableProps } = useVirtualTable({
- dataSource,
- height: 400,
- itemHeight: 55
- });
-
- return
;
-};
-```
-
-**优化效果**:
-- 支持 10,000+ 行数据的流畅渲染
-- 内存占用减少 70-80%
-- 滚动帧率提升至 60fps
-
-#### 表格性能策略
-| 策略 | 描述 | 性能提升 |
-|------|------|----------|
-| 列宽固定 | 避免动态列宽计算 | +15% |
-| 省略渲染 | 配置 `ellipsis: true` | +20% |
-| 虚拟滚动 | 懒加载可见区域 | +80% |
-| 分页优化 | 服务端分页支持 | +50% |
-
-### API请求缓存
-
-实现了智能的 API 请求缓存层,减少重复网络请求:
-
-```javascript
-// api/cache.js
-class ApiCache {
- constructor() {
- this.cache = new Map();
- this.ttl = 5 * 60 * 1000; // 5分钟缓存
- }
-
- get(key) {
- const item = this.cache.get(key);
- if (!item) return null;
-
- if (Date.now() - item.timestamp > this.ttl) {
- this.cache.delete(key);
- return null;
- }
-
- return item.data;
- }
-
- set(key, data) {
- this.cache.set(key, {
- data,
- timestamp: Date.now()
- });
- }
-
- clear() {
- this.cache.clear();
- }
-}
-
-export const apiCache = new ApiCache();
-```
-
-**缓存策略**:
-- **TTL管理**:自动过期机制,保持数据新鲜度
-- **智能键值**:基于请求参数生成唯一缓存键
-- **空间管理**:LRU淘汰机制,防止内存溢出
-- **命中率统计**:监控缓存使用效果
-
-**优化效果**:
-- 重复数据请求减少 60-80%
-- API响应时间缩短 40-60%
-- 服务器负载降低 30-50%
-
-### 3D可视化优化
-
-#### Three.js 性能优化
-- **几何体实例化**:使用 `InstancedMesh` 批量渲染相同模型
-- **按需渲染**:只在相机移动时更新渲染
-- **LOD技术**:根据距离动态调整模型精度
-- **资源池化**:3D对象复用,减少内存分配
-
-```jsx
-// 优化前 - 每个设备创建独立Mesh
-devices.forEach(device => {
- const mesh = createDeviceMesh(device);
- scene.add(mesh);
-});
-
-// 优化后 - 使用实例化渲染
-const instancedMesh = new THREE.InstancedMesh(
- geometry,
- material,
- devices.length
-);
-devices.forEach((device, index) => {
- const matrix = calculateTransformMatrix(device);
- instancedMesh.setMatrixAt(index, matrix);
-});
-scene.add(instancedMesh);
-```
-
-**优化效果**:
-- 100+ 设备场景渲染性能提升 5-10 倍
-- GPU内存占用减少 60%
-- 帧率从 15fps 提升至 60fps
-
-### useEffect 依赖优化
-
-修复了所有组件的 `useEffect` 依赖问题,确保正确的更新逻辑:
-
-```jsx
-// 优化前 - 空依赖数组导致数据不及时更新
-useEffect(() => {
- fetchData();
-}, []);
-
-// 优化后 - 正确的依赖管理
-useEffect(() => {
- fetchData();
-}, [fetchData]); // fetchData 使用 useCallback 包装
-```
-
-**优化效果**:
-- 数据实时性与一致性保证
-- 避免闭包陷阱导致的脏数据
-- React 严格模式下无异常
-
-### 性能基准测试
-
-| 场景 | 优化前 | 优化后 | 提升 |
-|------|--------|--------|------|
-| 页面加载时间 | 2.5s | 1.2s | +52% |
-| 表格渲染(1000行) | 800ms | 150ms | +81% |
-| 3D场景加载 | 5s | 1.5s | +70% |
-| API重复请求 | 100次 | 20次 | +80% |
-| 内存占用 | 150MB | 80MB | +47% |
-
-### 性能监控
-
-系统集成了性能监控能力,可通过开发者工具查看:
-
-```javascript
-// 性能标记示例
-console.time('fetchDevices');
-await fetchDevices();
-console.timeEnd('fetchDevices'); // 输出: fetchDevices: 45.32ms
-```
-
----
-
-## 📁 项目结构详解
-
-```
-jigui/
-├── frontend/ # 前端项目目录
-│ ├── public/ # 静态资源目录
-│ │ ├── favicon.ico # 网站图标
-│ │ └── index.html # HTML模板
-│ ├── src/ # 源代码目录
-│ │ ├── api/ # API接口封装
-│ │ │ ├── index.js # Axios实例配置
-│ │ │ ├── cache.js # API缓存层
-│ │ │ ├── rooms.js # 机房API
-│ │ │ ├── racks.js # 机柜API
-│ │ │ ├── devices.js # 设备API
-│ │ │ └── deviceFields.js # 设备字段API
-│ │ ├── components/ # 可复用组件
-│ │ ├── hooks/ # 自定义Hooks
-│ │ ├── pages/ # 页面组件
-│ │ │ ├── Dashboard.jsx # 数据看板
-│ │ │ ├── RoomManagement.jsx # 机房管理
-│ │ │ ├── RackManagement.jsx # 机柜管理
-│ │ │ ├── DeviceManagement.jsx # 设备管理
-│ │ │ ├── DeviceFieldManagement.jsx # 设备字段管理
-│ │ │ ├── RackVisualization.jsx # 3D可视化
-│ │ │ ├── TicketManagement.jsx # 工单管理
-│ │ │ ├── TicketStatistics.jsx # 工单统计
-│ │ │ └── ConsumableManagement.jsx # 耗材管理
-│ │ ├── App.jsx # 应用根组件
-│ │ ├── main.jsx # 应用入口
-│ │ └── index.css # 全局样式
-│ ├── package.json # 前端依赖配置
-│ └── vite.config.js # Vite构建配置
-│
-├── backend/ # 后端项目目录
-│ ├── models/ # 数据模型
-│ │ ├── index.js # 模型索引
-│ │ ├── Room.js # 机房模型
-│ │ ├── Rack.js # 机柜模型
-│ │ ├── Device.js # 设备模型
-│ │ ├── DeviceField.js # 设备字段模型
-│ │ ├── Ticket.js # 工单模型
-│ │ └── Consumable.js # 耗材模型
-│ ├── routes/ # API路由
-│ │ ├── index.js # 路由索引
-│ │ ├── rooms.js # 机房路由
-│ │ ├── racks.js # 机柜路由
-│ │ ├── devices.js # 设备路由
-│ │ ├── deviceFields.js # 设备字段路由
-│ │ ├── tickets.js # 工单路由
-│ │ └── consumables.js # 耗材路由
-│ ├── database/ # 数据库配置
-│ │ └── init.js # 数据库初始化
-│ ├── app.js # Express应用
-│ └── server.js # 服务入口
-│
-├── database/ # 数据库文件目录
-│ └── idc_system.db # SQLite数据库文件
-│
-├── docs/ # 项目文档
-│ ├── API.md # API文档
-│ ├── DATABASE.md # 数据库设计文档
-│ └── DEPLOYMENT.md # 部署文档
-│
-├── LICENSE # MIT许可证
-├── README.md # 项目说明文档
-└── package.json # 项目依赖配置
-```
-
-### 核心文件说明
-
-#### 前端核心文件
-
-| 文件路径 | 功能说明 | 重要性 |
-|----------|----------|--------|
-| `frontend/src/App.jsx` | 应用根组件,包含路由配置和全局布局 | ⭐⭐⭐ |
-| `frontend/src/pages/Dashboard.jsx` | 数据看板,展示关键指标和统计图表 | ⭐⭐⭐ |
-| `frontend/src/pages/DeviceManagement.jsx` | 设备管理核心页面 | ⭐⭐⭐ |
-| `frontend/src/pages/RackVisualization.jsx` | 3D机柜可视化 | ⭐⭐⭐ |
-| `frontend/src/api/index.js` | Axios实例和请求拦截器配置 | ⭐⭐ |
-| `frontend/src/api/cache.js` | API请求缓存层实现 | ⭐⭐ |
-
-#### 后端核心文件
-
-| 文件路径 | 功能说明 | 重要性 |
-|----------|----------|--------|
-| `backend/app.js` | Express应用配置和中间件 | ⭐⭐⭐ |
-| `backend/server.js` | 服务启动入口 | ⭐⭐⭐ |
-| `backend/models/index.js` | Sequelize模型关系配置 | ⭐⭐⭐ |
-| `backend/routes/devices.js` | 设备API路由实现 | ⭐⭐ |
-
----
-
-## 💡 使用示例
-
-### 基础数据操作
-
-#### 创建设备
-```jsx
-import { useState, useCallback } from 'react';
-import { Form, Input, Select, Button, message } from 'antd';
-import axios from '@/api';
-
-const DeviceForm = ({ onSuccess }) => {
- const [form] = Form.useForm();
- const [loading, setLoading] = useState(false);
-
- const handleSubmit = useCallback(async (values) => {
- try {
- setLoading(true);
- await axios.post('/api/devices', values);
- message.success('设备创建成功');
- form.resetFields();
- onSuccess?.();
- } catch (error) {
- message.error('创建设备失败');
- } finally {
- setLoading(false);
- }
- }, [onSuccess]);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-```
-
-#### 机房级联选择
-```jsx
-import { Cascader } from 'antd';
-import { useState, useEffect, useCallback } from 'react';
-import axios from '@/api';
-
-const RackSelector = ({ onChange }) => {
- const [rooms, setRooms] = useState([]);
- const [racks, setRacks] = useState([]);
-
- const fetchOptions = useCallback(async () => {
- const [roomsRes, racksRes] = await Promise.all([
- axios.get('/api/rooms'),
- axios.get('/api/racks')
- ]);
- setRooms(roomsRes.data);
- setRacks(racksRes.data);
- }, []);
-
- useEffect(() => {
- fetchOptions();
- }, [fetchOptions]);
-
- const options = rooms.map(room => ({
- value: room.roomId,
- label: room.name,
- children: racks
- .filter(rack => rack.RoomId === room.roomId)
- .map(rack => ({
- value: rack.rackId,
- label: rack.name
- }))
- }));
-
- return ;
-};
-```
-
-### 3D场景集成
-
-```jsx
-import { useEffect, useRef } from 'react';
-import * as THREE from 'three';
-import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
-
-const RackScene = ({ rackData }) => {
- const containerRef = useRef(null);
- const sceneRef = useRef(null);
-
- useEffect(() => {
- if (!containerRef.current) return;
-
- // 初始化场景
- const scene = new THREE.Scene();
- scene.background = new THREE.Color(0xf0f0f0);
-
- const camera = new THREE.PerspectiveCamera(
- 75,
- containerRef.current.clientWidth / containerRef.current.clientHeight,
- 0.1,
- 1000
- );
- camera.position.set(5, 5, 5);
-
- const renderer = new THREE.WebGLRenderer({ antialias: true });
- renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight);
- containerRef.current.appendChild(renderer.domElement);
-
- // 添加轨道控制器
- const controls = new OrbitControls(camera, renderer.domElement);
- controls.enableDamping = true;
-
- // 添加光源
- const ambientLight = new THREE.AmbientLight(0x404040);
- scene.add(ambientLight);
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
- directionalLight.position.set(5, 10, 7);
- scene.add(directionalLight);
-
- sceneRef.current = scene;
-
- // 动画循环
- const animate = () => {
- requestAnimationFrame(animate);
- controls.update();
- renderer.render(scene, camera);
- };
- animate();
-
- // 清理函数
- return () => {
- if (containerRef.current) {
- containerRef.current.removeChild(renderer.domElement);
- }
- renderer.dispose();
- };
- }, []);
-
- return ;
-};
-```
-
-### 性能监控示例
-
-```jsx
-import { useEffect, useRef } from 'react';
-
-const usePerformanceMonitor = (componentName) => {
- const startTimeRef = useRef(performance.now());
-
- useEffect(() => {
- const endTime = performance.now();
- const duration = endTime - startTimeRef.current;
- console.log(`${componentName} 渲染耗时: ${duration.toFixed(2)}ms`);
-
- // 可选:发送到性能监控服务
- // sendToMonitoring({ component: componentName, duration });
- }, [componentName]);
-};
-
-export default usePerformanceMonitor;
-```
-
----
-
-## 🛠️ 常见问题
-
-### Q1: 前端启动报错 "Module not found"
-
-**解决方案**:
-```bash
-# 清除缓存并重新安装
-cd frontend
-rm -rf node_modules package-lock.json
-npm install
-```
-
-### Q2: API请求跨域错误
-
-**解决方案**:确保后端已配置 CORS 中间件
-```javascript
-// backend/app.js
-const cors = require('cors');
-app.use(cors({
- origin: 'http://localhost:3000',
- credentials: true
-}));
-```
-
-### Q3: 3D场景不显示
-
-**解决方案**:
-1. 检查浏览器是否支持 WebGL
-2. 确认 Three.js 版本兼容性
-3. 检查控制台是否有错误信息
-
-### Q4: 表格数据加载缓慢
-
-**优化建议**:
-1. 确认已启用服务端分页
-2. 检查数据库索引是否创建
-3. 考虑添加 API 缓存层
-
-### Q5: 如何开启性能监控
-
-**启用方式**:
-```jsx
-// 在 App.jsx 中添加
-if (process.env.NODE_ENV === 'development') {
- import('./utils/perfMonitor').then(mod => mod.enableLogging());
-}
-```
-
----
-
-## 📈 路线图
-
-### v2.0.0 (规划中)
-- [ ] 移动端适配优化
-- [ ] 暗黑模式支持
-- [ ] 国际化(i18n)支持
-- [ ] 更多数据可视化图表
-- [ ] 设备健康监控告警
-
-### v2.1.0 (规划中)
-- [ ] 用户权限管理
-- [ ] 操作日志审计
-- [ ] 数据导出报告
-- [ ] API文档自动生成
-- [ ] Docker容器化部署
-
-### v3.0.0 (规划中)
-- [ ] 微服务架构重构
-- [ ] 实时WebSocket通信
-- [ ] 多人协作功能
-- [ ] 设备AR/VR可视化
-- [ ] AI智能运维建议
-
----
-
-## �📄 许可证
-
-本项目采用 [MIT 许可证](LICENSE)。
-
-```
-MIT License
-
-Copyright (c) 2024 IDC Management System
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-```
-
----
-
-## 🤝 贡献指南
-
-我们欢迎所有形式的贡献!请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 了解详细信息。
-
-### 贡献方式
-- 🐛 报告Bug
-- 💡 提出新功能建议
-- 📝 改进文档
-- 🔧 提交代码补丁
-
-### 开发流程
1. Fork本项目
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 开启Pull Request
+---
-
-**⭐ 如果这个项目对您有帮助,请给我们一个Star!**
\ No newline at end of file
+**⭐ 如果这个项目对您有帮助,请给我们一个Star!**
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
index 101b3ce..29d833a 100644
--- a/backend/routes/auth.js
+++ b/backend/routes/auth.js
@@ -338,4 +338,57 @@ router.post('/check-admin', async (req, res) => {
}
});
+router.post('/unlock', async (req, res) => {
+ try {
+ const { username, password } = req.body;
+
+ if (!username || !password) {
+ return res.status(400).json({
+ success: false,
+ message: '用户名和密码不能为空'
+ });
+ }
+
+ const user = await User.findOne({ where: { username } });
+ if (!user) {
+ return res.status(401).json({
+ success: false,
+ message: '用户名或密码错误'
+ });
+ }
+
+ if (user.status !== 'locked') {
+ return res.status(400).json({
+ success: false,
+ message: '账户未被锁定'
+ });
+ }
+
+ const isPasswordValid = await bcrypt.compare(password, user.password);
+ if (!isPasswordValid) {
+ return res.status(401).json({
+ success: false,
+ message: '用户名或密码错误'
+ });
+ }
+
+ // 解锁账户
+ user.status = 'active';
+ user.loginCount = 0;
+ await user.save();
+
+ res.json({
+ success: true,
+ message: '账户解锁成功'
+ });
+ } catch (error) {
+ console.error('解锁账户错误:', error);
+ res.status(500).json({
+ success: false,
+ message: '解锁失败',
+ error: error.message
+ });
+ }
+});
+
module.exports = router;
diff --git a/docs/api/README.md b/docs/api/README.md
new file mode 100644
index 0000000..6129675
--- /dev/null
+++ b/docs/api/README.md
@@ -0,0 +1,844 @@
+# API接口文档
+
+本文档描述IDC设备管理系统的后端API接口,遵循OpenAPI 3.0规范。
+
+## 基础信息
+
+| 项目 | 值 |
+|------|-----|
+| Base URL | `http://localhost:8000/api` |
+| Content-Type | `application/json` |
+| 认证方式 | Bearer Token (JWT) |
+
+## 通用响应格式
+
+### 成功响应
+
+```json
+{
+ "success": true,
+ "data": {...},
+ "message": "操作成功"
+}
+```
+
+### 错误响应
+
+```json
+{
+ "success": false,
+ "error": "错误信息",
+ "message": "详细描述"
+}
+```
+
+## 认证接口
+
+### 用户登录
+
+```http
+POST /api/auth/login
+```
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 描述 |
+|--------|------|------|------|
+| username | string | 是 | 用户名 |
+| password | string | 是 | 密码 |
+
+**请求示例**:
+
+```json
+{
+ "username": "admin",
+ "password": "password123"
+}
+```
+
+**响应示例**:
+
+```json
+{
+ "success": true,
+ "data": {
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "user": {
+ "userId": "user001",
+ "username": "admin",
+ "role": "admin"
+ }
+ },
+ "message": "登录成功"
+}
+```
+
+### 用户注册
+
+```http
+POST /api/auth/register
+```
+
+## 机房管理接口
+
+### 获取机房列表
+
+```http
+GET /api/rooms
+```
+
+**响应示例**:
+
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "roomId": "room001",
+ "name": "A区机房",
+ "location": "一楼东侧",
+ "area": 500,
+ "createdAt": "2024-01-01T00:00:00.000Z",
+ "updatedAt": "2024-01-01T00:00:00.000Z"
+ }
+ ],
+ "message": "操作成功"
+}
+```
+
+### 创建机房
+
+```http
+POST /api/rooms
+```
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 描述 |
+|--------|------|------|------|
+| roomId | string | 是 | 机房ID |
+| name | string | 是 | 机房名称 |
+| location | string | 否 | 机房位置 |
+| area | number | 否 | 面积(平方米) |
+
+**请求示例**:
+
+```json
+{
+ "roomId": "room002",
+ "name": "B区机房",
+ "location": "二楼西侧",
+ "area": 600
+}
+```
+
+### 更新机房
+
+```http
+PUT /api/rooms/:roomId
+```
+
+### 删除机房
+
+```http
+DELETE /api/rooms/:roomId
+```
+
+## 机柜管理接口
+
+### 获取机柜列表
+
+```http
+GET /api/racks
+```
+
+**查询参数**:
+
+| 参数名 | 类型 | 描述 |
+|--------|------|------|
+| roomId | string | 按机房ID筛选 |
+
+**响应示例**:
+
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "rackId": "rack001",
+ "name": "机柜A1",
+ "height": 42,
+ "powerRating": 5000,
+ "RoomId": "room001",
+ "Room": {
+ "roomId": "room001",
+ "name": "A区机房"
+ },
+ "Devices": [],
+ "createdAt": "2024-01-01T00:00:00.000Z",
+ "updatedAt": "2024-01-01T00:00:00.000Z"
+ }
+ ],
+ "message": "操作成功"
+}
+```
+
+### 创建机柜
+
+```http
+POST /api/racks
+```
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 描述 |
+|--------|------|------|------|
+| rackId | string | 是 | 机柜ID |
+| name | string | 是 | 机柜名称 |
+| height | number | 否 | 高度(U) |
+| powerRating | number | 否 | 额定功率(W) |
+| RoomId | string | 是 | 所属机房ID |
+
+**请求示例**:
+
+```json
+{
+ "rackId": "rack002",
+ "name": "机柜A2",
+ "height": 42,
+ "powerRating": 5000,
+ "RoomId": "room001"
+}
+```
+
+### 更新机柜
+
+```http
+PUT /api/racks/:rackId
+```
+
+### 删除机柜
+
+```http
+DELETE /api/racks/:rackId
+```
+
+### 获取机柜详情
+
+```http
+GET /api/racks/:rackId
+```
+
+## 设备管理接口
+
+### 获取设备列表
+
+```http
+GET /api/devices
+```
+
+**查询参数**:
+
+| 参数名 | 类型 | 描述 |
+|--------|------|------|
+| rackId | string | 按机柜ID筛选 |
+| deviceType | string | 按设备类型筛选 |
+| page | number | 页码,默认1 |
+| pageSize | number | 每页数量,默认10 |
+
+**响应示例**:
+
+```json
+{
+ "success": true,
+ "data": {
+ "devices": [
+ {
+ "deviceId": "dev001",
+ "name": "Web服务器01",
+ "deviceType": "服务器",
+ "manufacturer": "Dell",
+ "model": "R740",
+ "rackPosition": 1,
+ "height": 2,
+ "ipAddress": "192.168.1.100",
+ "macAddress": "00:1B:44:11:3A:B7",
+ "status": "运行中",
+ "purchaseDate": "2023-01-01",
+ "warrantyDate": "2026-01-01",
+ "description": "主要Web应用服务器",
+ "RackId": "rack001",
+ "Rack": {
+ "rackId": "rack001",
+ "name": "机柜A1"
+ },
+ "createdAt": "2024-01-01T00:00:00.000Z",
+ "updatedAt": "2024-01-01T00:00:00.000Z"
+ }
+ ],
+ "total": 100,
+ "page": 1,
+ "pageSize": 10
+ },
+ "message": "操作成功"
+}
+```
+
+### 创建设备
+
+```http
+POST /api/devices
+```
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 描述 |
+|--------|------|------|------|
+| deviceId | string | 是 | 设备ID |
+| name | string | 是 | 设备名称 |
+| deviceType | string | 是 | 设备类型 |
+| manufacturer | string | 否 | 厂商 |
+| model | string | 否 | 型号 |
+| RackId | string | 否 | 所属机柜ID |
+| rackPosition | number | 否 | 机柜位置 |
+| height | number | 否 | 占用高度(U) |
+| ipAddress | string | 否 | IP地址 |
+| macAddress | string | 否 | MAC地址 |
+| status | string | 否 | 状态 |
+| purchaseDate | string | 否 | 购买日期 |
+| warrantyDate | string | 否 | 保修日期 |
+| description | string | 否 | 描述 |
+
+**请求示例**:
+
+```json
+{
+ "deviceId": "dev002",
+ "name": "数据库服务器",
+ "deviceType": "服务器",
+ "manufacturer": "HP",
+ "model": "DL380",
+ "RackId": "rack001",
+ "rackPosition": 3,
+ "height": 2,
+ "ipAddress": "192.168.1.101",
+ "status": "运行中"
+}
+```
+
+### 更新设备
+
+```http
+PUT /api/devices/:deviceId
+```
+
+### 删除设备
+
+```http
+DELETE /api/devices/:deviceId
+```
+
+### 批量导入设备
+
+```http
+POST /api/devices/batch-import
+```
+
+**Content-Type**: `multipart/form-data`
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 描述 |
+|--------|------|------|------|
+| file | File | 是 | CSV格式的设备数据文件 |
+
+## 设备字段管理接口
+
+### 获取设备字段列表
+
+```http
+GET /api/deviceFields
+```
+
+**响应示例**:
+
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "id": 1,
+ "fieldName": "cpuModel",
+ "displayName": "CPU型号",
+ "fieldType": "text",
+ "isRequired": false,
+ "defaultValue": "",
+ "createdAt": "2024-01-01T00:00:00.000Z",
+ "updatedAt": "2024-01-01T00:00:00.000Z"
+ }
+ ],
+ "message": "操作成功"
+}
+```
+
+### 创建设备字段
+
+```http
+POST /api/deviceFields
+```
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 描述 |
+|--------|------|------|------|
+| fieldName | string | 是 | 字段名(英文) |
+| displayName | string | 是 | 显示名称(中文) |
+| fieldType | string | 是 | 字段类型(text/number/date/select) |
+| isRequired | boolean | 否 | 是否必填 |
+| defaultValue | string | 否 | 默认值 |
+| options | string | 否 | 选项(逗号分隔,select类型使用) |
+
+**请求示例**:
+
+```json
+{
+ "fieldName": "cpuModel",
+ "displayName": "CPU型号",
+ "fieldType": "text",
+ "isRequired": false,
+ "defaultValue": ""
+}
+```
+
+### 更新设备字段
+
+```http
+PUT /api/deviceFields/:id
+```
+
+### 删除设备字段
+
+```http
+DELETE /api/deviceFields/:id
+```
+
+## 工单管理接口
+
+### 获取工单列表
+
+```http
+GET /api/tickets
+```
+
+**查询参数**:
+
+| 参数名 | 类型 | 描述 |
+|--------|------|------|
+| status | string | 按状态筛选 |
+| priority | string | 按优先级筛选 |
+| page | number | 页码 |
+| pageSize | number | 每页数量 |
+
+**响应示例**:
+
+```json
+{
+ "success": true,
+ "data": {
+ "tickets": [
+ {
+ "ticketId": "ticket001",
+ "title": "服务器故障",
+ "description": "Web服务器无法访问",
+ "status": "处理中",
+ "priority": "高",
+ "categoryId": "cat001",
+ "assigneeId": "user001",
+ "requesterId": "user002",
+ "createdAt": "2024-01-01T00:00:00.000Z",
+ "updatedAt": "2024-01-01T00:00:00.000Z"
+ }
+ ],
+ "total": 50,
+ "page": 1,
+ "pageSize": 10
+ },
+ "message": "操作成功"
+}
+```
+
+### 创建工单
+
+```http
+POST /api/tickets
+```
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 描述 |
+|--------|------|------|------|
+| title | string | 是 | 工单标题 |
+| description | string | 是 | 工单描述 |
+| categoryId | string | 是 | 工单分类ID |
+| priority | string | 是 | 优先级(高/中/低) |
+| assigneeId | string | 否 | 指派用户ID |
+
+### 更新工单
+
+```http
+PUT /api/tickets/:ticketId
+```
+
+### 删除工单
+
+```http
+DELETE /api/tickets/:ticketId
+```
+
+## 工单分类管理接口
+
+### 获取工单分类列表
+
+```http
+GET /api/ticketCategories
+```
+
+### 创建工单分类
+
+```http
+POST /api/ticketCategories
+```
+
+### 更新工单分类
+
+```http
+PUT /api/ticketCategories/:categoryId
+```
+
+### 删除工单分类
+
+```http
+DELETE /api/ticketCategories/:categoryId
+```
+
+## 工单字段管理接口
+
+### 获取工单字段列表
+
+```http
+GET /api/ticketFields
+```
+
+### 创建设单字段
+
+```http
+POST /api/ticketFields
+```
+
+### 更新工单字段
+
+```http
+PUT /api/ticketFields/:id
+```
+
+### 删除工单字段
+
+```http
+DELETE /api/ticketFields/:id
+```
+
+## 耗材管理接口
+
+### 获取耗材列表
+
+```http
+GET /api/consumables
+```
+
+**响应示例**:
+
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "consumableId": "cons001",
+ "name": "硬盘",
+ "categoryId": "cat001",
+ "specification": "1TB SSD",
+ "unit": "个",
+ "stock": 100,
+ "unitPrice": 500,
+ "description": "固态硬盘",
+ "createdAt": "2024-01-01T00:00:00.000Z",
+ "updatedAt": "2024-01-01T00:00:00.000Z"
+ }
+ ],
+ "message": "操作成功"
+}
+```
+
+### 创建耗材
+
+```http
+POST /api/consumables
+```
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 描述 |
+|--------|------|------|------|
+| consumableId | string | 是 | 耗材ID |
+| name | string | 是 | 耗材名称 |
+| categoryId | string | 是 | 分类ID |
+| specification | string | 否 | 规格 |
+| unit | string | 否 | 单位 |
+| stock | number | 否 | 库存数量 |
+| unitPrice | number | 否 | 单价 |
+| description | string | 否 | 描述 |
+
+### 更新耗材
+
+```http
+PUT /api/consumables/:consumableId
+```
+
+### 删除耗材
+
+```http
+DELETE /api/consumables/:consumableId
+```
+
+## 耗材分类管理接口
+
+### 获取耗材分类列表
+
+```http
+GET /api/consumableCategories
+```
+
+### 创建耗材分类
+
+```http
+POST /api/consumableCategories
+```
+
+### 更新耗材分类
+
+```http
+PUT /api/consumableCategories/:categoryId
+```
+
+### 删除耗材分类
+
+```http
+DELETE /api/consumableCategories/:categoryId
+```
+
+## 耗材领用记录接口
+
+### 获取耗材领用记录
+
+```http
+GET /api/consumableRecords
+```
+
+### 创建耗材领用记录
+
+```http
+POST /api/consumableRecords
+```
+
+## 耗材日志接口
+
+### 获取耗材日志
+
+```http
+GET /api/consumableLogs
+```
+
+## 用户管理接口
+
+### 获取用户列表
+
+```http
+GET /api/users
+```
+
+**响应示例**:
+
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "userId": "user001",
+ "username": "admin",
+ "email": "admin@example.com",
+ "phone": "13800138000",
+ "status": "active",
+ "Roles": [],
+ "createdAt": "2024-01-01T00:00:00.000Z",
+ "updatedAt": "2024-01-01T00:00:00.000Z"
+ }
+ ],
+ "message": "操作成功"
+}
+```
+
+### 创建用户
+
+```http
+POST /api/users
+```
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 描述 |
+|--------|------|------|------|
+| userId | string | 是 | 用户ID |
+| username | string | 是 | 用户名 |
+| password | string | 是 | 密码 |
+| email | string | 否 | 邮箱 |
+| phone | string | 否 | 电话 |
+
+### 更新用户
+
+```http
+PUT /api/users/:userId
+```
+
+### 删除用户
+
+```http
+DELETE /api/users/:userId
+```
+
+## 角色管理接口
+
+### 获取角色列表
+
+```http
+GET /api/roles
+```
+
+**响应示例**:
+
+```json
+{
+ "success": true,
+ "data": [
+ {
+ "roleId": "role001",
+ "roleName": "管理员",
+ "description": "系统管理员",
+ "Permissions": [],
+ "createdAt": "2024-01-01T00:00:00.000Z",
+ "updatedAt": "2024-01-01T00:00:00.000Z"
+ }
+ ],
+ "message": "操作成功"
+}
+```
+
+### 创建角色
+
+```http
+POST /api/roles
+```
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 描述 |
+|--------|------|------|------|
+| roleId | string | 是 | 角色ID |
+| roleName | string | 是 | 角色名称 |
+| description | string | 否 | 描述 |
+| permissions | array | 否 | 权限列表 |
+
+### 更新角色
+
+```http
+PUT /api/roles/:roleId
+```
+
+### 删除角色
+
+```http
+DELETE /api/roles/:roleId
+```
+
+## 系统设置接口
+
+### 获取系统设置
+
+```http
+GET /api/systemSettings
+```
+
+**响应示例**:
+
+```json
+{
+ "success": true,
+ "data": {
+ "id": 1,
+ "key": "system_config",
+ "value": {
+ "siteName": "IDC设备管理系统",
+ "siteLogo": "/logo.png"
+ },
+ "createdAt": "2024-01-01T00:00:00.000Z",
+ "updatedAt": "2024-01-01T00:00:00.000Z"
+ },
+ "message": "操作成功"
+}
+```
+
+### 更新系统设置
+
+```http
+PUT /api/systemSettings
+```
+
+## 背景配置接口
+
+### 获取背景配置
+
+```http
+GET /api/background
+```
+
+### 更新背景配置
+
+```http
+PUT /api/background
+```
+
+## 健康检查接口
+
+### 服务状态检查
+
+```http
+GET /health
+```
+
+**响应示例**:
+
+```json
+{
+ "status": "ok",
+ "message": "IDC设备管理系统后端服务正常运行",
+ "timestamp": "2024-01-01T00:00:00.000Z"
+}
+```
+
+## 错误码说明
+
+| 错误码 | 说明 |
+|--------|------|
+| 400 | 请求参数错误 |
+| 401 | 未授权访问 |
+| 403 | 禁止访问 |
+| 404 | 资源不存在 |
+| 500 | 服务器内部错误 |
diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index 29fc4ca..327a516 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -63,6 +63,7 @@ export const authAPI = {
checkAdmin: () => api.get('/auth/check-admin'),
register: (data) => api.post('/auth/register', data),
login: (data) => api.post('/auth/login', data),
+ unlock: (data) => api.post('/auth/unlock', data),
getProfile: () => api.get('/auth/profile'),
updateProfile: (data) => api.put('/auth/profile', data),
changePassword: (data) => api.put('/auth/password', data)
diff --git a/frontend/src/components/DeviceComponent.jsx b/frontend/src/components/DeviceComponent.jsx
new file mode 100644
index 0000000..3e6c74e
--- /dev/null
+++ b/frontend/src/components/DeviceComponent.jsx
@@ -0,0 +1,371 @@
+import React from 'react';
+import {
+ CloudServerOutlined, SwitcherOutlined, DatabaseOutlined,
+ CloudOutlined, LaptopOutlined, MobileOutlined,
+ PrinterOutlined
+} from '@ant-design/icons';
+
+// 设备图标映射
+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 getDeviceStatusColor = (status) => {
+ const statusColorMap = {
+ 'normal': '#10b981',
+ 'running': '#10b981',
+ 'warning': '#f59e0b',
+ 'error': '#ef4444',
+ 'fault': '#ef4444',
+ 'offline': '#6b7280',
+ 'maintenance': '#3b82f6',
+ undefined: '#3b82f6',
+ null: '#3b82f6'
+ };
+ return statusColorMap[status] || '#3b82f6';
+};
+
+// 设备状态主题
+const getStatusTheme = (status) => {
+ const themeMap = {
+ 'normal': {
+ bgGradient: 'linear-gradient(180deg, #059669 0%, #047857 50%, #065f46 100%)',
+ borderColor: '#10b981',
+ topBorderColor: '#34d399',
+ glowColor: 'rgba(16, 185, 129, 0.4)',
+ shadowColor: 'rgba(16, 185, 129, 0.3)',
+ iconColor: '#10b981',
+ label: '正常'
+ },
+ 'running': {
+ bgGradient: 'linear-gradient(180deg, #059669 0%, #047857 50%, #065f46 100%)',
+ borderColor: '#10b981',
+ topBorderColor: '#34d399',
+ glowColor: 'rgba(16, 185, 129, 0.4)',
+ shadowColor: 'rgba(16, 185, 129, 0.3)',
+ iconColor: '#10b981',
+ label: '运行中'
+ },
+ 'warning': {
+ bgGradient: 'linear-gradient(180deg, #d97706 0%, #b45309 50%, #92400e 100%)',
+ borderColor: '#f59e0b',
+ topBorderColor: '#fbbf24',
+ glowColor: 'rgba(245, 158, 11, 0.4)',
+ shadowColor: 'rgba(245, 158, 11, 0.3)',
+ iconColor: '#f59e0b',
+ label: '警告'
+ },
+ 'error': {
+ bgGradient: 'linear-gradient(180deg, #dc2626 0%, #b91c1c 50%, #991b1b 100%)',
+ borderColor: '#ef4444',
+ topBorderColor: '#f87171',
+ glowColor: 'rgba(239, 68, 68, 0.5)',
+ shadowColor: 'rgba(239, 68, 68, 0.4)',
+ iconColor: '#ef4444',
+ label: '故障'
+ },
+ 'fault': {
+ bgGradient: 'linear-gradient(180deg, #dc2626 0%, #b91c1c 50%, #991b1b 100%)',
+ borderColor: '#ef4444',
+ topBorderColor: '#f87171',
+ glowColor: 'rgba(239, 68, 68, 0.5)',
+ shadowColor: 'rgba(239, 68, 68, 0.4)',
+ iconColor: '#ef4444',
+ label: '故障'
+ },
+ 'offline': {
+ bgGradient: 'linear-gradient(180deg, #4b5563 0%, #374151 50%, #1f2937 100%)',
+ borderColor: '#6b7280',
+ topBorderColor: '#9ca3af',
+ glowColor: 'rgba(107, 114, 128, 0.2)',
+ shadowColor: 'rgba(0, 0, 0, 0.2)',
+ iconColor: '#9ca3af',
+ label: '离线'
+ },
+ 'maintenance': {
+ bgGradient: 'linear-gradient(180deg, #2563eb 0%, #1d4ed8 50%, #1e40af 100%)',
+ borderColor: '#3b82f6',
+ topBorderColor: '#60a5fa',
+ glowColor: 'rgba(59, 130, 246, 0.4)',
+ shadowColor: 'rgba(59, 130, 246, 0.3)',
+ iconColor: '#3b82f6',
+ label: '维护中'
+ },
+ 'default': {
+ bgGradient: 'linear-gradient(180deg, #3d4451 0%, #2d3139 50%, #252930 100%)',
+ borderColor: '#4a5568',
+ topBorderColor: '#565c6b',
+ glowColor: 'rgba(56, 189, 248, 0.2)',
+ shadowColor: 'rgba(0, 0, 0, 0.3)',
+ iconColor: '#38bdf8',
+ label: '未知'
+ }
+ };
+
+ return themeMap[status] || themeMap['default'];
+};
+
+// 设备样式计算
+const getDeviceStyle = (device, rackHeight) => {
+ // 添加参数验证
+ if (!device || typeof device.position !== 'number' || typeof device.height !== 'number') {
+ return {};
+ }
+
+ const uHeight = 25; // 调整为更小的U高度以适应屏幕显示,1U=25px
+ const deviceHeight = Math.max(1, device.height) * uHeight;
+
+ // 设备位置从底部开始计算(U1在底部)
+ 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) * uHeight;
+
+ return {
+ height: `${deviceHeight}px`, // 确保设备高度精确等于U位高度
+ top: `${topPosition}px`, // 确保设备顶部与U位网格线对齐
+ // 移除任何可能影响占满U位的样式
+ margin: 0,
+ padding: 0
+ };
+};
+
+// 设备组件
+const DeviceComponent = ({ device, rackHeight, isHighlighted, onMouseEnter, onMouseLeave }) => {
+ 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;
+
+ // 获取状态主题
+ const statusTheme = getStatusTheme(device?.status);
+ const statusColor = getDeviceStatusColor(device?.status);
+
+ return (
+ {
+ const isOneU = (device?.height || 1) === 1;
+ const isFaultStatus = device?.status === 'error' || device?.status === 'fault';
+ if (isOneU && !isFaultStatus) {
+ e.currentTarget.style.height = '33px';
+ e.currentTarget.style.zIndex = '150';
+ }
+ e.currentTarget.style.transform = 'scale(1.008)';
+ e.currentTarget.style.boxShadow = `
+ 0 4px 12px ${statusTheme.shadowColor},
+ 0 2px 6px rgba(0,0,0,0.3),
+ inset 0 1px 0 rgba(255,255,255,0.15)
+ `;
+ e.currentTarget.style.borderColor = statusTheme.topBorderColor;
+
+ if (onMouseEnter) {
+ const rect = e.currentTarget.getBoundingClientRect();
+ onMouseEnter({ device, position: { x: rect.right + 5, y: rect.top + rect.height / 2 } });
+ }
+ }}
+ onMouseLeave={(e) => {
+ const isOneU = (device?.height || 1) === 1;
+ const isFaultStatus = device?.status === 'error' || device?.status === 'fault';
+ if (isOneU && !isFaultStatus) {
+ const originalHeight = (device?.height || 1) * 25;
+ e.currentTarget.style.height = `${originalHeight}px`;
+ e.currentTarget.style.zIndex = '100';
+ }
+ e.currentTarget.style.transform = 'scale(1)';
+ e.currentTarget.style.boxShadow = `
+ 0 1px 2px rgba(0,0,0,0.3),
+ 0 2px 4px rgba(0,0,0,0.2),
+ inset 0 1px 0 rgba(255,255,255,0.1),
+ inset 0 -1px 0 rgba(0,0,0,0.1)
+ `;
+ e.currentTarget.style.borderColor = statusTheme.borderColor;
+ if (onMouseLeave) {
+ onMouseLeave();
+ }
+ }}
+ >
+
+
+ {/* 左侧状态指示区域 - 增强版 */}
+
+ {/* 设备类型标识 */}
+
+
+ {device.type?.toUpperCase() || 'DEV'}
+
+
+
+ {/* LED状态指示灯组 */}
+
+ {/* 主状态灯 */}
+
+
+ {/* 电源指示灯 */}
+
+
+
+ {/* 设备序列号标签 */}
+
+
+ SN:{device.serial?.slice(-4) || '0000'}
+
+
+
+
+ {/* 中间设备信息区域 - 增强版 */}
+
+ {/* 设备品牌/厂商标识 */}
+
+
+ {getDeviceIcon(device.type)}
+
+
+ {device.brand || 'ENTERPRISE'}
+
+
+
+ {/* 设备名称 */}
+
+ {deviceName}
+
+
+ {/* 型号和规格 */}
+
+
+ {device.model || device.type?.toUpperCase() || 'STD'}
+
+ {device.ip && (
+
+ {device.ip}
+
+ )}
+
+
+ {/* 散热/通风口装饰 - 根据设备类型显示 */}
+ {(device.type === 'server' || device.type === 'storage') && (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ )}
+
+
+ {/* 右侧端口/功能区域 - 增强版 */}
+
+ {/* 端口指示灯阵列 */}
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+ {/* 管理接口标识 */}
+
+
+ {/* 设备高度U数标识 */}
+
+ {device.height}U
+
+
+
+ );
+ } catch (error) {
+ console.error('设备渲染错误:', error, device);
+ // 渲染一个简单的错误显示元素
+ return (
+
+ 设备加载错误
+
+ );
+ }
+ };
+
+// 默认导出
+export default DeviceComponent;
\ No newline at end of file
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx
index c8290f3..82528fa 100644
--- a/frontend/src/pages/Login.jsx
+++ b/frontend/src/pages/Login.jsx
@@ -10,6 +10,7 @@ import {
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
+import { authAPI } from '../api';
const { Title, Text } = Typography;
@@ -17,6 +18,7 @@ const Login = () => {
const [loading, setLoading] = useState(false);
const [isFirstUser, setIsFirstUser] = useState(false);
const [registerMode, setRegisterMode] = useState(false);
+ const [unlockMode, setUnlockMode] = useState(false);
const { login, register, checkAdmin } = useAuth();
const navigate = useNavigate();
@@ -55,6 +57,23 @@ const Login = () => {
}
};
+ const onFinishUnlock = async (values) => {
+ setLoading(true);
+ try {
+ const response = await authAPI.unlock(values);
+ if (response.success) {
+ message.success('解锁成功,请重新登录');
+ setUnlockMode(false);
+ } else {
+ message.error(response.message || '解锁失败');
+ }
+ } catch (error) {
+ message.error(error || '解锁失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
const onFinishRegister = async (values) => {
if (values.password !== values.confirmPassword) {
message.error('两次输入的密码不一致');
@@ -210,10 +229,11 @@ const Login = () => {
- {isFirstUser ? '创建管理员账户' : 'IDC设备管理系统'}
+ {isFirstUser ? '创建管理员账户' : unlockMode ? '账户解锁' : 'IDC设备管理系统'}
- {isFirstUser ? '首次使用,请创建系统管理员账户' : '安全登录您的账户'}
+ {isFirstUser ? '首次使用,请创建系统管理员账户' :
+ unlockMode ? '输入账户信息以解锁账户' : '安全登录您的账户'}
@@ -228,9 +248,9 @@ const Login = () => {
)}
+ }
+ placeholder="用户名"
+ style={inputStyle}
+ />
+
+
+
+ }
+ placeholder="密码"
+ style={inputStyle}
+ />
+
+ >
) : (
<>
{
loading={loading}
style={submitButtonStyle}
>
- {registerMode ? '立即注册' : '登 录'}
+ {registerMode ? '立即注册' : unlockMode ? '解 锁' : '登 录'}
@@ -365,20 +417,55 @@ const Login = () => {
其他方式
}>
-
+ {unlockMode ? (
+ <>
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
)}
diff --git a/frontend/src/pages/RackVisualization.css b/frontend/src/pages/RackVisualization.css
new file mode 100644
index 0000000..437fc83
--- /dev/null
+++ b/frontend/src/pages/RackVisualization.css
@@ -0,0 +1,1062 @@
+/* 设计令牌 */
+:root {
+ /* 颜色 */
+ --color-primary-main: #667eea;
+ --color-primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ --color-primary-light: #8b9ff0;
+ --color-primary-dark: #4f5db8;
+ --color-success-main: #10b981;
+ --color-success-gradient: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ --color-warning-main: #f59e0b;
+ --color-warning-gradient: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
+ --color-error-main: #ef4444;
+ --color-error-gradient: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+ --color-text-primary: #1e293b;
+ --color-text-secondary: #64748b;
+ --color-text-tertiary: #94a3b8;
+ --color-text-inverse: #ffffff;
+ --color-background-primary: #ffffff;
+ --color-background-secondary: #f8fafc;
+ --color-background-tertiary: #f1f5f9;
+ --color-border-light: #e2e8f0;
+ --color-border-medium: #cbd5e1;
+ --color-border-dark: #94a3b8;
+ --color-device-server: #3b82f6;
+ --color-device-switch: #22c55e;
+ --color-device-router: #f59e0b;
+ --color-device-storage: #8b5cf6;
+ --color-device-firewall: #ef4444;
+ --color-status-normal: #10b981;
+ --color-status-running: #10b981;
+ --color-status-warning: #f59e0b;
+ --color-status-error: #ef4444;
+ --color-status-offline: #6b7280;
+ --color-status-maintenance: #3b82f6;
+
+ /* 阴影 */
+ --shadow-small: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+ --shadow-medium: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
+ --shadow-large: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
+
+ /* 边框半径 */
+ --border-radius-small: 6px;
+ --border-radius-medium: 10px;
+ --border-radius-large: 16px;
+ --border-radius-xl: 24px;
+ --border-radius-round: 50%;
+
+ /* 过渡 */
+ --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-normal: 300ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1);
+
+ /* 间距 */
+ --spacing-xs: 4px;
+ --spacing-sm: 8px;
+ --spacing-md: 16px;
+ --spacing-lg: 24px;
+ --spacing-xl: 32px;
+
+ /* U高度 */
+ --u-height: 25px;
+}
+
+/* 页面容器样式 */
+.page-container {
+ min-height: 100vh;
+ background: var(--color-background-secondary);
+ padding: var(--spacing-lg);
+}
+
+/* 头部样式 */
+.header {
+ margin-bottom: var(--spacing-lg);
+ padding: var(--spacing-lg) var(--spacing-xl);
+ background: var(--color-primary-gradient);
+ border-radius: var(--border-radius-large);
+ box-shadow: var(--shadow-large);
+ color: var(--color-text-inverse);
+}
+
+.header-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: var(--spacing-lg);
+}
+
+/* 统计卡片样式 */
+.stat-card {
+ background: rgba(255, 255, 255, 0.15);
+ border-radius: var(--border-radius-medium);
+ padding: var(--spacing-md);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ backdrop-filter: blur(10px);
+}
+
+/* 卡片样式 */
+.card {
+ border-radius: var(--border-radius-large);
+ border: none;
+ box-shadow: var(--shadow-medium);
+ background: var(--color-background-primary);
+ overflow: hidden;
+}
+
+.filter-card {
+ border-radius: var(--border-radius-medium);
+ border: none;
+ box-shadow: var(--shadow-small);
+ background: var(--color-background-primary);
+ margin-bottom: var(--spacing-lg);
+}
+
+.filter-card-body {
+ padding: var(--spacing-md) var(--spacing-lg);
+}
+
+/* 按钮样式 */
+.primary-button {
+ height: 40px;
+ border-radius: var(--border-radius-medium);
+ background: var(--color-primary-gradient);
+ border: none;
+ box-shadow: var(--shadow-medium);
+ font-weight: 500;
+ transition: all var(--transition-normal);
+}
+
+.secondary-button {
+ height: 40px;
+ border-radius: var(--border-radius-medium);
+ border: 1px solid var(--color-border-light);
+ background: var(--color-background-primary);
+ transition: all var(--transition-fast);
+}
+
+/* 动画 */
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-50%) translateX(20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(-50%) translateX(0) scale(1);
+ }
+}
+
+@keyframes ledBlink {
+ 0%, 50% { opacity: 1; }
+ 51%, 100% { opacity: 0.3; }
+}
+
+@keyframes tooltipFadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(-50%) translateX(-10px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(-50%) translateX(0) scale(1);
+ }
+}
+
+@keyframes highlightPulse {
+ 0%, 100% {
+ box-shadow:
+ 0 0 20px rgba(56, 189, 248, 0.6),
+ 0 4px 12px rgba(56, 189, 248, 0.4),
+ 0 1px 2px rgba(0,0,0,0.3),
+ inset 0 1px 0 rgba(255,255,255,0.2);
+ }
+ 50% {
+ box-shadow:
+ 0 0 35px rgba(56, 189, 248, 0.9),
+ 0 6px 20px rgba(56, 189, 248, 0.6),
+ 0 1px 2px rgba(0,0,0,0.3),
+ inset 0 1px 0 rgba(255,255,255,0.3);
+ }
+}
+
+/* 设备高亮 */
+.device-highlighted {
+ animation: highlightPulse 1.5s ease-in-out infinite;
+}
+
+/* 金属纹理 */
+.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;
+}
+
+/* 通风格栅 */
+.ventilation-grille {
+ background-image: repeating-linear-gradient(
+ 0deg,
+ #334155 0px,
+ #334155 1px,
+ transparent 1px,
+ transparent 2px
+ );
+}
+
+/* 设备悬停效果 */
+.device-hover {
+ 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;
+}
+
+/* 设备提示框 */
+.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;
+}
+
+/* 设备数量徽章 */
+.device-count-badge {
+ 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;
+}
+
+/* 机柜可视化容器 */
+.rack-visualization-container {
+ width: 100%;
+ height: calc(100vh - 220px);
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ perspective: 1000px;
+ overflow: auto;
+ background-color: #fafafa;
+ background-size: auto;
+ background-position: center;
+ background-repeat: no-repeat;
+ position: relative;
+ border-radius: 12px;
+ border: 1px solid #e8e8e8;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
+ transition: all 0.3s ease;
+ padding: 10px;
+ box-sizing: border-box;
+ flex-shrink: 0;
+}
+
+.rack-visualization-container.image-background {
+ background-size: contain;
+}
+
+/* 机柜容器 */
+.rack-container {
+ transform: scale(1.7);
+ transition: transform 0.3s ease;
+ position: relative;
+ margin: 0 auto;
+ padding: 40px 50px;
+ will-change: transform, scale;
+}
+
+/* 机柜主体 */
+.rack {
+ position: relative;
+ width: 600px;
+ background: linear-gradient(145deg, #1e293b 0%, #0f172a 50%, #1e293b 100%);
+ border: 2px solid #38bdf8;
+ border-radius: 12px;
+ box-shadow:
+ 0 0 40px rgba(56, 189, 248, 0.2),
+ 0 0 80px rgba(56, 189, 248, 0.1),
+ 0 20px 60px rgba(0, 0, 0, 0.5),
+ inset 0 1px 0 rgba(255, 255, 255, 0.05),
+ inset 0 0 60px rgba(56, 189, 248, 0.05);
+ overflow: visible;
+ backdrop-filter: blur(10px);
+}
+
+/* 玻璃光泽效果 */
+.rack-glass-effect {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 50%;
+ background: linear-gradient(180deg, rgba(56, 189, 248, 0.03) 0%, transparent 100%);
+ border-radius: 10px 10px 0 0;
+ pointer-events: none;
+}
+
+/* U数标记容器 */
+.u-marks-container {
+ position: absolute;
+ width: 18px;
+ top: 0;
+ bottom: 0;
+ z-index: 20;
+ pointer-events: none;
+}
+
+.u-marks-container.left {
+ left: -18px;
+}
+
+.u-marks-container.right {
+ right: -18px;
+}
+
+/* U数标记 */
+.u-mark {
+ position: absolute;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #cbd5e1;
+ font-size: 10px;
+ font-weight: 500;
+ text-shadow: 0 0 4px rgba(0, 0, 0, 0.5);
+ z-index: 30;
+ background-color: rgba(30, 41, 59, 0.8);
+ border-radius: 3px;
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.3);
+ height: var(--u-height);
+}
+
+.u-mark:nth-child(5n) {
+ color: #38bdf8;
+ font-weight: 700;
+ text-shadow: 0 0 10px rgba(56, 189, 248, 0.6);
+ background-color: rgba(56, 189, 248, 0.15);
+ border: 1px solid rgba(56, 189, 248, 0.5);
+ box-shadow: 0 0 8px rgba(56, 189, 248, 0.3);
+}
+
+/* U数标记线 */
+.u-mark-line {
+ position: absolute;
+ height: 1px;
+ width: 100%;
+ background-color: #a5d6a7;
+ border-top: 1px solid #a5d6a7;
+}
+
+.u-mark-line.five {
+ background-color: #81c784;
+ border-top: 2px solid #81c784;
+}
+
+.u-mark-text {
+ position: absolute;
+ left: -40px;
+ top: -12px;
+ font-size: 12px;
+ color: #4caf50;
+ font-weight: normal;
+ text-shadow: 1px 1px 1px rgba(255,255,255,0.8);
+}
+
+.u-mark-text.five {
+ color: #2e7d32;
+ font-weight: bold;
+}
+
+/* 安装导轨 */
+.rack-rail {
+ position: absolute;
+ top: 3px;
+ width: 15px;
+ bottom: 3px;
+ background: linear-gradient(90deg, #374151 0%, #1f2937 50%, #111827 100%);
+ border-radius: 2px;
+ box-shadow: inset 0 0 3px rgba(0,0,0,0.5), 0 0 8px rgba(56, 189, 248, 0.1);
+}
+
+.rack-rail.left {
+ left: 33px;
+ border-right: 1px solid rgba(56, 189, 248, 0.3);
+ border-radius: 2px 0 0 2px;
+}
+
+.rack-rail.right {
+ right: 33px;
+ background: linear-gradient(90deg, #343a40 0%, #495057 50%, #5a6268 100%);
+ border-left: 1px solid rgba(0,0,0,0.3);
+ border-radius: 0 2px 2px 0;
+ box-shadow: inset 0 0 3px rgba(0,0,0,0.4);
+}
+
+/* 导轨安装孔 */
+.rail-hole {
+ position: absolute;
+ width: 11px;
+ height: 20px;
+ background: linear-gradient(180deg, #2d3436 0%, #636e72 50%, #2d3436 100%);
+ border-radius: 1px;
+ box-shadow: inset 0 1px 2px rgba(255,255,255,0.1), 0 1px 1px rgba(0,0,0,0.3);
+}
+
+.rail-hole.left {
+ left: 2px;
+}
+
+.rail-hole.right {
+ right: 2px;
+}
+
+.rail-hole-inner {
+ position: absolute;
+ width: 5px;
+ height: 8px;
+ background: #1a1a1a;
+ border-radius: 1px;
+ top: 6px;
+}
+
+.rail-hole-inner.left {
+ left: 3px;
+}
+
+.rail-hole-inner.right {
+ right: 3px;
+}
+
+/* 前面板 */
+.rack-front-panel {
+ position: absolute;
+ top: 0;
+ left: 51px;
+ right: 51px;
+ bottom: 0;
+ background: linear-gradient(145deg, #f5f5f5, #e8e8e8);
+ border-radius: 4px;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: stretch;
+ padding: 3px;
+ border: 1px solid rgba(148, 163, 184, 0.3);
+ overflow: visible;
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
+}
+
+/* 细微网格纹理 */
+.front-panel-grid {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-image: radial-gradient(circle at 1px 1px, rgba(0,0,0,0.03) 1px, transparent 0);
+ background-size: 20px 20px;
+ border-radius: 4px;
+ pointer-events: none;
+}
+
+/* U位网格线 */
+.u-grid-lines {
+ position: absolute;
+ top: 3px;
+ left: 48px;
+ right: 48px;
+ bottom: 3px;
+ pointer-events: none;
+ z-index: 5;
+}
+
+.u-grid-line {
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent 0%, rgba(148, 163, 184, 0.15) 10%, rgba(148, 163, 184, 0.2) 50%, rgba(148, 163, 184, 0.15) 90%, transparent 100%);
+}
+
+/* 设备容器 */
+.devices-container {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+/* 设备 */
+.device {
+ position: absolute;
+ left: 3px;
+ right: 3px;
+ width: calc(100% - 6px);
+ border-radius: 3px;
+ display: flex;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow:
+ 0 1px 2px rgba(0,0,0,0.3),
+ 0 2px 4px rgba(0,0,0,0.2),
+ inset 0 1px 0 rgba(255,255,255,0.1),
+ inset 0 -1px 0 rgba(0,0,0,0.1);
+ overflow: hidden;
+ margin: 0;
+ padding: 0;
+ z-index: 100;
+ pointer-events: auto;
+}
+
+.device.highlighted {
+ box-shadow:
+ 0 0 20px rgba(56, 189, 248, 0.6),
+ 0 4px 12px rgba(56, 189, 248, 0.4),
+ 0 1px 2px rgba(0,0,0,0.3),
+ inset 0 1px 0 rgba(255,255,255,0.2);
+ z-index: 200;
+ animation: highlightPulse 1.5s ease-in-out infinite;
+}
+
+.device.status-warning {
+ animation: ledBlink 2s ease-in-out infinite;
+}
+
+.device.status-error {
+ animation: ledBlink 0.8s ease-in-out infinite;
+}
+
+/* 设备状态指示器 */
+.device-status-indicator {
+ width: 7%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px 3px;
+ flex-shrink: 0;
+ overflow: hidden;
+ position: relative;
+ border-right: 1px solid rgba(148, 163, 184, 0.4);
+ border-radius: 3px 0 0 3px;
+}
+
+/* 设备类型标识 */
+.device-type-badge {
+ width: 100%;
+ height: 8px;
+ background: linear-gradient(180deg, rgba(56, 189, 248, 0.6) 0%, rgba(56, 189, 248, 0.3) 100%);
+ border-radius: 2px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid rgba(56, 189, 248, 0.4);
+}
+
+.device-type-text {
+ font-size: 4px;
+ color: rgba(56, 189, 248, 0.8);
+ font-weight: 700;
+ letter-spacing: 0.5px;
+}
+
+/* LED状态指示灯组 */
+.device-leds {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ align-items: center;
+}
+
+.main-status-leds {
+ display: flex;
+ gap: 2px;
+}
+
+.led {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+}
+
+.led.status-normal {
+ background-color: var(--color-status-normal);
+ box-shadow: 0 0 8px var(--color-status-normal);
+}
+
+.led.status-running {
+ background-color: #22c55e;
+ box-shadow: 0 0 6px #22c55e;
+ animation: ledBlink 3s ease-in-out infinite;
+}
+
+.led.status-error {
+ background-color: var(--color-status-error);
+ box-shadow: 0 0 8px var(--color-status-error);
+ animation: ledBlink 0.8s ease-in-out infinite;
+}
+
+.led.status-warning {
+ background-color: var(--color-status-warning);
+ box-shadow: 0 0 8px var(--color-status-warning);
+ animation: ledBlink 2s ease-in-out infinite;
+}
+
+.led.offline {
+ background-color: #4b5563;
+}
+
+/* 电源指示灯 */
+.power-led {
+ display: flex;
+ gap: 2px;
+ align-items: center;
+}
+
+.power-led-indicator {
+ width: 4px;
+ height: 4px;
+ border-radius: 1px;
+ background-color: #475569;
+}
+
+.power-led-indicator.on {
+ background-color: #38bdf8;
+ box-shadow: 0 0 4px #38bdf8;
+}
+
+.power-led-text {
+ font-size: 4px;
+ color: #64748b;
+ font-weight: 600;
+}
+
+.power-led-text.on {
+ color: #38bdf8;
+}
+
+/* 设备序列号标签 */
+.device-serial {
+ width: 100%;
+ text-align: center;
+ border-top: 1px solid rgba(148, 163, 184, 0.2);
+ padding-top: 2px;
+}
+
+.device-serial-text {
+ font-size: 4px;
+ color: #475569;
+ font-family: monospace;
+}
+
+/* 设备信息区域 */
+.device-info {
+ flex: 1;
+ background: linear-gradient(180deg, #0f172a 0%, #020617 100%);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 3px 8px;
+ overflow: hidden;
+ border-left: 1px solid rgba(148, 163, 184, 0.15);
+ border-right: 1px solid rgba(148, 163, 184, 0.15);
+ position: relative;
+}
+
+/* 设备品牌/厂商标识 */
+.device-brand {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-bottom: 2px;
+}
+
+.device-brand-icon {
+ font-size: 10px;
+ color: #38bdf8;
+ display: flex;
+ align-items: center;
+}
+
+.device-brand-text {
+ font-size: 5px;
+ color: #64748b;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+}
+
+/* 设备名称 */
+.device-name {
+ color: #f1f5f9;
+ font-size: 8px;
+ font-weight: 600;
+ font-family: "Roboto Mono", "Consolas", monospace;
+ text-align: left;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-shadow: 0 0 10px rgba(56, 189, 248, 0.3);
+}
+
+/* 设备型号和规格 */
+.device-model {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ margin-top: 2px;
+}
+
+.device-model-text {
+ color: #64748b;
+ font-size: 6px;
+ font-family: "Roboto Mono", monospace;
+ background: rgba(56, 189, 248, 0.1);
+ padding: 1px 4px;
+ border-radius: 2px;
+ border: 1px solid rgba(56, 189, 248, 0.2);
+}
+
+.device-ip {
+ color: #22c55e;
+ font-size: 6px;
+ font-family: "Roboto Mono", monospace;
+}
+
+/* 散热/通风口装饰 */
+.device-ventilation {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ gap: 2px;
+}
+
+.ventilation-fin {
+ width: 3px;
+ height: 12px;
+ background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
+ border-radius: 1px;
+ border: 1px solid rgba(148, 163, 184, 0.2);
+}
+
+/* 设备右侧端口/功能区域 */
+.device-ports {
+ width: 12%;
+ background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
+ border-radius: 0 3px 3px 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ padding: 4px 3px;
+ gap: 4px;
+ overflow: hidden;
+ position: relative;
+ border-left: 1px solid rgba(148, 163, 184, 0.2);
+}
+
+/* 端口指示灯阵列 */
+.port-leds {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 2px;
+}
+
+.port-led {
+ width: 4px;
+ height: 4px;
+ border-radius: 1px;
+}
+
+.port-led.active {
+ background-color: #22c55e;
+ box-shadow: 0 0 3px rgba(34, 197, 94, 0.6);
+ animation: ledBlink 2s ease-in-out infinite;
+}
+
+.port-led.inactive {
+ background-color: #475569;
+}
+
+/* 管理接口标识 */
+.management-interface {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ margin-top: 2px;
+}
+
+.management-icon {
+ width: 0;
+ height: 0;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-bottom: 6px solid #64748b;
+}
+
+.management-text {
+ font-size: 5px;
+ color: #64748b;
+ font-weight: 600;
+}
+
+/* 设备高度U数标识 */
+.device-height {
+ font-size: 5px;
+ color: #38bdf8;
+ font-weight: 700;
+ font-family: "Roboto Mono", monospace;
+ background: rgba(56, 189, 248, 0.15);
+ padding: 1px 4px;
+ border-radius: 2px;
+ border: 1px solid rgba(56, 189, 248, 0.3);
+}
+
+/* 机柜顶部名称和设备数量 */
+.rack-header {
+ position: absolute;
+ top: -45px;
+ left: 0;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+}
+
+/* 机柜标题 */
+.rack-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #0f172a;
+ text-shadow: 0 1px 2px rgba(255,255,255,0.5);
+ padding: 6px 14px;
+ background: linear-gradient(145deg, rgba(255,255,255,0.95), rgba(241, 245, 249, 0.95));
+ border-radius: 10px;
+ border: 1px solid rgba(56, 189, 248, 0.4);
+ box-shadow: 0 3px 10px rgba(56, 189, 248, 0.2);
+ transition: all 0.3s ease;
+ white-space: nowrap;
+ max-width: 60%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* 机柜侧面 */
+.rack-side {
+ position: absolute;
+ top: 0;
+ left: 100%;
+ width: 30px;
+ height: 100%;
+ background-color: #7f8c8d;
+ transform-origin: left center;
+ transform: rotateY(90deg);
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+/* 机柜顶部 */
+.rack-top {
+ position: absolute;
+ top: -20px;
+ left: 0;
+ width: 100%;
+ height: 20px;
+ background-color: #666;
+ transform-origin: bottom center;
+ transform: rotateX(90deg);
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+}
+
+/* 加载状态 */
+.loading-container {
+ width: 100%;
+ height: 600px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: #fafafa;
+ font-size: 16px;
+ color: #666;
+ border-radius: 12px;
+}
+
+/* 无数据状态 */
+.no-data-container {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: #81c784;
+ font-size: 16px;
+ text-align: center;
+ padding: 16px 24px;
+ background-color: rgba(240, 255, 240, 0.95);
+ border-radius: 12px;
+ border: 1px solid rgba(129, 199, 132, 0.3);
+ box-shadow: 0 4px 12px rgba(129, 199, 132, 0.2);
+ font-weight: 500;
+}
+
+/* 错误边界样式 */
+.error-boundary {
+ width: 100%;
+ height: 600px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ background-color: #f5f5f5;
+ padding: 20px;
+}
+
+.error-boundary-title {
+ color: #f44336;
+}
+
+.error-boundary-message {
+ color: #666;
+ margin: 10px 0;
+}
+
+.error-boundary-reload-button {
+ padding: 8px 16px;
+ background-color: #1890ff;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+/* 搜索结果卡片 */
+.search-result-card {
+ background: var(--color-success-gradient);
+ border-radius: var(--border-radius-medium);
+}
+
+.search-result-content {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-md);
+ flex-wrap: wrap;
+ padding: var(--spacing-md) var(--spacing-lg);
+}
+
+/* 搜索结果按钮 */
+.search-result-button {
+ size: small;
+ height: 32px;
+ border-radius: var(--border-radius-small);
+ border-color: rgba(255,255,255,0.5);
+ background: rgba(255,255,255,0.1);
+ color: #fff;
+}
+
+.search-result-button.highlighted {
+ background: rgba(255,255,255,0.2);
+}
+
+/* 设备加载状态 */
+.device-loading-container {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 1000;
+}
+
+.device-loading-content {
+ padding: 20px;
+ background-color: rgba(255, 255, 255, 0.8);
+ border-radius: 4px;
+ font-size: 14px;
+ color: #666;
+}
+
+/* 设备加载错误 */
+.device-error {
+ position: absolute;
+ left: 35px;
+ right: 35px;
+ top: 50%;
+ height: 30px;
+ transform: translateY(-50%);
+ background-color: #ff4d4f;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ border-radius: 4px;
+}
+
+/* 选择机柜提示 */
+.select-rack-prompt {
+ width: 100%;
+ height: 600px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: #f5f5f5;
+ font-size: 16px;
+ color: #666;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .device-tooltip {
+ font-size: 10px;
+ padding: 6px 10px;
+ }
+
+ .device-count-badge {
+ padding: 4px 10px;
+ font-size: 11px;
+ }
+
+ .header-content {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
+
+@media (max-width: 480px) {
+ .device-tooltip {
+ font-size: 9px;
+ padding: 4px 8px;
+ }
+
+ .device-count-badge {
+ padding: 3px 8px;
+ font-size: 10px;
+ }
+
+ .rack-container {
+ transform: scale(0.8);
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/pages/RackVisualization.jsx b/frontend/src/pages/RackVisualization.jsx
index a66deef..6e381e6 100644
--- a/frontend/src/pages/RackVisualization.jsx
+++ b/frontend/src/pages/RackVisualization.jsx
@@ -10,10 +10,13 @@ import {
FullscreenOutlined, CompressOutlined, EyeOutlined
} from '@ant-design/icons';
import axios from 'axios';
+import DeviceComponent from '../components/DeviceComponent';
+import './RackVisualization.css';
const { Option } = Select;
const { Text, Title } = Typography;
+// 设计令牌定义
const designTokens = {
colors: {
primary: {
@@ -40,14 +43,6 @@ const designTokens = {
light: '#f87171',
dark: '#b91c1c'
},
- purple: {
- main: '#8b5cf6',
- gradient: 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)'
- },
- cyan: {
- main: '#06b6d4',
- gradient: 'linear-gradient(135deg, #06b6d4 0%, #0891b2 100%)'
- },
text: {
primary: '#1e293b',
secondary: '#64748b',
@@ -70,10 +65,7 @@ const designTokens = {
switch: '#22c55e',
router: '#f59e0b',
storage: '#8b5cf6',
- firewall: '#ef4444',
- ups: '#14b8a6',
- pdu: '#64748b',
- other: '#94a3b8'
+ other: '#64748b'
},
status: {
normal: '#10b981',
@@ -85,73 +77,50 @@ const designTokens = {
maintenance: '#3b82f6'
}
},
- shadows: {
- small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
- medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
- large: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
- xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
- glow: '0 0 20px rgba(102, 126, 234, 0.3)'
+ spacing: {
+ xs: 4,
+ sm: 8,
+ md: 16,
+ lg: 24,
+ xl: 32
},
borderRadius: {
- small: '6px',
- medium: '10px',
- large: '16px',
- xl: '24px',
- round: '50%'
+ small: 6,
+ medium: 10,
+ large: 16,
+ xl: 24
},
transitions: {
fast: '150ms cubic-bezier(0.4, 0, 0.2, 1)',
normal: '300ms cubic-bezier(0.4, 0, 0.2, 1)',
slow: '500ms cubic-bezier(0.4, 0, 0.2, 1)'
},
- spacing: {
- xs: '4px',
- sm: '8px',
- md: '16px',
- lg: '24px',
- xl: '32px'
+ shadows: {
+ small: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
+ medium: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
+ large: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)'
}
};
-const pageContainerStyle = {
- minHeight: '100vh',
- background: designTokens.colors.background.secondary,
- padding: designTokens.spacing.lg
-};
-
-const headerStyle = {
- marginBottom: designTokens.spacing.lg,
- padding: `${designTokens.spacing.lg}px ${designTokens.spacing.xl}px`,
- background: designTokens.colors.primary.gradient,
- borderRadius: designTokens.borderRadius.large,
- boxShadow: designTokens.shadows.large,
- color: designTokens.colors.text.inverse
-};
+// 移除内联样式函数,使用CSS类
+// 卡片样式定义
const cardStyle = {
borderRadius: designTokens.borderRadius.large,
border: 'none',
boxShadow: designTokens.shadows.medium,
- background: designTokens.colors.background.primary,
+ background: '#fff',
overflow: 'hidden'
};
-const filterCardStyle = {
- borderRadius: designTokens.borderRadius.medium,
- border: 'none',
- boxShadow: designTokens.shadows.small,
- background: designTokens.colors.background.primary,
- marginBottom: designTokens.spacing.lg
-};
-
+// 按钮样式定义
const primaryButtonStyle = {
- height: '40px',
- borderRadius: designTokens.borderRadius.medium,
+ height: '42px',
+ borderRadius: '10px',
background: designTokens.colors.primary.gradient,
border: 'none',
- boxShadow: designTokens.shadows.medium,
- fontWeight: '500',
- transition: `all ${designTokens.transitions.normal}`
+ boxShadow: '0 4px 16px rgba(102, 126, 234, 0.35)',
+ fontWeight: '500'
};
const secondaryButtonStyle = {
@@ -162,14 +131,6 @@ const secondaryButtonStyle = {
transition: `all ${designTokens.transitions.fast}`
};
-const statCardStyle = () => ({
- background: 'rgba(255, 255, 255, 0.15)',
- borderRadius: designTokens.borderRadius.medium,
- padding: `${designTokens.spacing.md}px`,
- border: '1px solid rgba(255, 255, 255, 0.2)',
- backdropFilter: 'blur(10px)'
-});
-
// 工具函数提取到组件外部,避免每次渲染重复创建
const getDeviceIcon = (deviceType) => {
try {
@@ -373,149 +334,6 @@ const getDeviceTypeTheme = (type) => {
return themeMap.other;
};
-// 初始化动画样式
-const initAnimationStyles = () => {
- const existingStyle = document.getElementById('rack-visualization-styles');
- if (existingStyle) {
- return existingStyle;
- }
-
- const style = document.createElement('style');
- style.id = 'rack-visualization-styles';
- style.textContent = `
- @keyframes slideIn {
- from {
- opacity: 0;
- transform: translateY(-50%) translateX(20px) scale(0.95);
- }
- to {
- opacity: 1;
- transform: translateY(-50%) translateX(0) scale(1);
- }
- }
-
- @keyframes ledBlink {
- 0%, 50% { opacity: 1; }
- 51%, 100% { opacity: 0.3; }
- }
-
- @keyframes tooltipFadeIn {
- from {
- opacity: 0;
- transform: translateY(-50%) translateX(-10px) scale(0.95);
- }
- to {
- opacity: 1;
- transform: translateY(-50%) translateX(0) scale(1);
- }
- }
-
- @keyframes highlightPulse {
- 0%, 100% {
- box-shadow:
- 0 0 20px rgba(56, 189, 248, 0.6),
- 0 4px 12px rgba(56, 189, 248, 0.4),
- 0 1px 2px rgba(0,0,0,0.3),
- inset 0 1px 0 rgba(255,255,255,0.2);
- }
- 50% {
- box-shadow:
- 0 0 35px rgba(56, 189, 248, 0.9),
- 0 6px 20px rgba(56, 189, 248, 0.6),
- 0 1px 2px rgba(0,0,0,0.3),
- inset 0 1px 0 rgba(255,255,255,0.3);
- }
- }
-
- .device-highlighted {
- animation: highlightPulse 1.5s ease-in-out infinite;
- }
-
- .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;
- }
-
- .ventilation-grille {
- background-image: repeating-linear-gradient(
- 0deg,
- #334155 0px,
- #334155 1px,
- transparent 1px,
- transparent 2px
- );
- }
-
- .device-hover {
- 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;
- }
-
- .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;
- }
-
- .device-count-badge {
- 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;
- }
-
- @media (max-width: 768px) {
- .device-tooltip {
- font-size: 10px;
- padding: 6px 10px;
- }
-
- .device-count-badge {
- padding: 4px 10px;
- font-size: 11px;
- }
- }
-
- @media (max-width: 480px) {
- .device-tooltip {
- font-size: 9px;
- padding: 4px 8px;
- }
-
- .device-count-badge {
- padding: 3px 8px;
- font-size: 10px;
- }
- }
- `;
- document.head.appendChild(style);
- return style;
-};
-
// 错误边界组件
class ErrorBoundary extends React.Component {
constructor(props) {
@@ -534,30 +352,14 @@ class ErrorBoundary extends React.Component {
render() {
if (this.state.hasError) {
return (
-
-
可视化组件出错了
-
+
+
可视化组件出错了
+
错误信息: {this.state.error?.toString()}
@@ -576,7 +378,7 @@ function RackVisualization() {
const [loading, setLoading] = useState(true);
const [devices, setDevices] = useState([]);
const [loadingDevices, setLoadingDevices] = useState(false);
- const [zoom, setZoom] = useState(1.5);
+ const [zoom, setZoom] = useState(1.7);
const [rotation, setRotation] = useState(0);
const [error, setError] = useState(null);
const [deviceTooltip, setDeviceTooltip] = useState(null); // 设备详情tooltip
@@ -589,6 +391,7 @@ function RackVisualization() {
const [tooltipDeviceFields, setTooltipDeviceFields] = useState([]); // 设备字段配置
const [loadingTooltipFields, setLoadingTooltipFields] = useState(false); // 加载字段配置状态
const [savingTooltipConfig, setSavingTooltipConfig] = useState(false); // 保存配置状态
+ const [deviceCache, setDeviceCache] = useState({}); // 设备数据缓存,键为rackId,值为设备数据
// 设备搜索功能
const [searchKeyword, setSearchKeyword] = useState(''); // 搜索关键词
@@ -614,10 +417,7 @@ function RackVisualization() {
power: { label: '功率', enabled: true, field: 'power', fieldType: 'number' }
}), []);
- // 初始化样式
- useEffect(() => {
- initAnimationStyles();
- }, []);
+ // 移除初始化样式的useEffect,因为样式已移至CSS文件
// 获取设备字段配置 - 使用 useCallback 避免重复创建
const fetchTooltipDeviceFields = useCallback(async () => {
@@ -692,6 +492,14 @@ function RackVisualization() {
// 获取机柜内的设备 - 使用 useCallback 避免重复创建
const fetchDevices = useCallback(async (rackId) => {
+ // 先检查缓存中是否已有数据
+ if (deviceCache[rackId]) {
+ console.log(`=== 从缓存获取机柜 ${rackId} 的设备数据 ===`);
+ setDevices(deviceCache[rackId]);
+ setLoadingDevices(false);
+ return;
+ }
+
try {
setLoadingDevices(true);
console.log(`=== 开始获取机柜 ${rackId} 的设备数据 ===`);
@@ -749,6 +557,8 @@ function RackVisualization() {
console.warn('3. 数据库查询条件问题');
message.warning(`机柜 ${rackId} 暂无设备数据`);
setDevices([]);
+ // 将空数据也存入缓存,避免重复请求
+ setDeviceCache(prev => ({ ...prev, [rackId]: [] }));
return;
}
@@ -822,6 +632,9 @@ function RackVisualization() {
console.log('最终有效设备列表:', validDevices);
setDevices(validDevices);
+ // 将处理后的数据存入缓存
+ setDeviceCache(prev => ({ ...prev, [rackId]: validDevices }));
+
if (validDevices.length === 0) {
console.warn('未找到有效设备数据');
message.warning('该机柜内暂无设备数据');
@@ -833,7 +646,7 @@ function RackVisualization() {
} finally {
setLoadingDevices(false);
}
- }, []);
+ }, [deviceCache]);
// 搜索设备 - 全局搜索,支持跨机柜
const handleSearch = useCallback(async (keyword) => {
@@ -890,7 +703,7 @@ function RackVisualization() {
// 切换到设备所在的机柜
setSelectedRack(targetRack);
- fetchDevices(targetRack.rackId);
+ // 不再直接调用fetchDevices,由useEffect处理
message.info(`已跳转到机柜: ${firstResult.rackName}`);
}
} else {
@@ -954,7 +767,7 @@ function RackVisualization() {
setSelectedRoom(roomKey);
}
- fetchDevices(firstRack.rackId);
+ // 不再直接调用fetchDevices,由useEffect处理
} else {
setSelectedRack(null);
setSelectedRoom(null);
@@ -977,6 +790,36 @@ function RackVisualization() {
fetchRacks();
}, [fetchRacks]);
+ // 使用useMemo缓存处理后的设备数据,只有当rackId变化时才重新处理
+ const processedDevices = useMemo(() => {
+ if (!devices || !devices.length) {
+ return [];
+ }
+
+ // 处理设备数据,添加必要的计算属性
+ return devices.map(device => {
+ // 计算设备在机柜中的位置(用于可视化)
+ const calculatedPosition = {
+ top: device.position,
+ height: device.height
+ };
+
+ return {
+ ...device,
+ calculatedPosition
+ };
+ });
+ }, [devices]);
+
+ // 当selectedRack变化时,获取对应机柜的设备数据
+ useEffect(() => {
+ if (selectedRack?.rackId) {
+ fetchDevices(selectedRack.rackId);
+ } else {
+ setDevices([]);
+ }
+ }, [selectedRack, fetchDevices]);
+
// 打开字段配置模态框时获取数据
const handleOpenTooltipConfig = () => {
if (Object.keys(tooltipFields).length === 0) {
@@ -1072,25 +915,12 @@ function RackVisualization() {
marks.push(
-
+
{i}
@@ -1106,38 +936,28 @@ function RackVisualization() {
const uHeight = 25;
for (let u = 1; u <= height; u++) {
+ // 调整U位标记定位,确保U1在机柜底部,U42在顶部
const topPosition = (height - u) * uHeight;
marks.push(
-
-
- {u}
-
+
+ {u}
);
}
- return marks;
+ return (
+
+ {marks}
+
+ );
};
// 生成右侧U数标记(从U1开始显示)
@@ -1146,38 +966,28 @@ function RackVisualization() {
const uHeight = 25;
for (let u = 1; u <= height; u++) {
+ // 调整U位标记定位,确保U1在机柜底部,U42在顶部
const topPosition = (height - u) * uHeight;
marks.push(
-
-
- {u}
-
+
+ {u}
);
}
- return marks;
+ return (
+
+ {marks}
+
+ );
};
// 获取机房列表
@@ -1231,7 +1041,7 @@ function RackVisualization() {
const rack = racks.find(r => r.rackId === rackId);
if (rack) {
setSelectedRack(rack);
- fetchDevices(rackId);
+ // 不再直接调用fetchDevices,由useEffect处理
}
};
@@ -1288,15 +1098,9 @@ function RackVisualization() {
// 无需额外的初始化,CSS 3D变换直接在JSX中实现
return (
-
-
-
+
+
+
@@ -1308,13 +1112,13 @@ function RackVisualization() {
-
+
-
+
设备总数
{racks.reduce((sum, rack) => sum + (rack.deviceCount || 0), 0)}
@@ -1322,7 +1126,7 @@ function RackVisualization() {
-
+
在线设备
{devices.filter(d => d.status === 'running' || d.status === 'normal').length}
@@ -1333,30 +1137,25 @@ function RackVisualization() {
-
-
+
+
handleSearch(e.target.value)}
onPressEnter={(e) => handleSearch(e.target.value)}
- style={{ width: 240, height: '40px', borderRadius: designTokens.borderRadius.medium }}
+ style={{ width: 240, height: '40px', borderRadius: '10px' }}
allowClear
prefix={
}
suffix={
searchMatchCount > 0 ? (
-
+
) : null
}
/>
{searchKeyword && (
}
onClick={clearSearch}
title="清除搜索"
@@ -1364,7 +1163,7 @@ function RackVisualization() {
)}