feat(auth): 添加账户解锁功能
在登录页面增加账户解锁功能,当账户被锁定后可以通过输入正确凭证解锁 后端添加/auth/unlock接口处理解锁逻辑 前端添加解锁表单和状态切换 更新API文档和CHANGELOG记录新功能
This commit is contained in:
+164
@@ -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修复
|
||||
@@ -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;
|
||||
|
||||
@@ -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 | 服务器内部错误 |
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <CloudServerOutlined style={{ color: '#ffffff' }} />;
|
||||
const type = deviceType.toLowerCase();
|
||||
|
||||
if (type.includes('server') || type.includes('服务器')) return <CloudServerOutlined style={{ color: '#ffffff' }} />;
|
||||
if (type.includes('switch') || type.includes('交换机')) return <SwitcherOutlined style={{ color: '#ffffff' }} />;
|
||||
if (type.includes('storage') || type.includes('存储')) return <DatabaseOutlined style={{ color: '#ffffff' }} />;
|
||||
if (type.includes('router') || type.includes('路由器')) return <CloudOutlined style={{ color: '#ffffff' }} />;
|
||||
if (type.includes('laptop') || type.includes('笔记本')) return <LaptopOutlined style={{ color: '#ffffff' }} />;
|
||||
if (type.includes('mobile') || type.includes('手机')) return <MobileOutlined style={{ color: '#ffffff' }} />;
|
||||
if (type.includes('printer') || type.includes('打印机')) return <PrinterOutlined style={{ color: '#ffffff' }} />;
|
||||
|
||||
return <CloudServerOutlined style={{ color: '#ffffff' }} />;
|
||||
} catch (error) {
|
||||
console.error('设备图标渲染错误:', error);
|
||||
return <CloudServerOutlined style={{ color: '#ffffff' }} />;
|
||||
}
|
||||
};
|
||||
|
||||
// 设备状态颜色映射
|
||||
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 (
|
||||
<div
|
||||
key={deviceId}
|
||||
className={`device ${isHighlighted ? 'highlighted' : ''} ${device?.status === 'warning' ? 'status-warning' : ''} ${(device?.status === 'error' || device?.status === 'fault') ? 'status-error' : ''}`}
|
||||
style={{
|
||||
...getDeviceStyle(device, rackHeight),
|
||||
background: statusTheme.bgGradient,
|
||||
border: isHighlighted
|
||||
? `2px solid ${statusTheme.topBorderColor}`
|
||||
: `1px solid ${statusTheme.borderColor}`,
|
||||
borderTop: isHighlighted
|
||||
? `2px solid ${statusTheme.topBorderColor}`
|
||||
: `1px solid ${statusTheme.topBorderColor}`
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
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();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="device-status-top-bar" style={{
|
||||
background: `linear-gradient(90deg, ${statusTheme.topBorderColor} 0%, ${statusTheme.borderColor} 50%, ${statusTheme.topBorderColor} 100%)`
|
||||
}} />
|
||||
|
||||
{/* 左侧状态指示区域 - 增强版 */}
|
||||
<div className="device-status-indicator" style={{
|
||||
background: isHighlighted
|
||||
? `linear-gradient(180deg, ${statusTheme.borderColor}33 0%, ${statusTheme.borderColor}22 100%)`
|
||||
: `linear-gradient(180deg, ${statusTheme.borderColor}44 0%, ${statusTheme.borderColor}22 100%)`,
|
||||
borderRight: `1px solid ${statusTheme.borderColor}66`
|
||||
}}>
|
||||
{/* 设备类型标识 */}
|
||||
<div className="device-type-badge" style={{
|
||||
background: `linear-gradient(180deg, ${statusTheme.borderColor}66 0%, ${statusTheme.borderColor}33 100%)`,
|
||||
border: `1px solid ${statusTheme.borderColor}44`
|
||||
}}>
|
||||
<span className="device-type-text" style={{ color: statusTheme.topBorderColor }}>
|
||||
{device.type?.toUpperCase() || 'DEV'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* LED状态指示灯组 */}
|
||||
<div className="device-leds">
|
||||
{/* 主状态灯 */}
|
||||
<div className="main-status-leds">
|
||||
<div className={`led ${device.status === 'warning' ? 'status-warning' : ''} ${device.status === 'error' ? 'status-error' : ''} ${device.status === 'normal' || device.status === 'running' ? 'status-normal' : 'offline'}`} style={{
|
||||
backgroundColor: getDeviceStatusColor(device.status),
|
||||
boxShadow: `0 0 8px ${getDeviceStatusColor(device.status)}`
|
||||
}} />
|
||||
<div className={`led ${device.status === 'running' ? 'status-running' : 'offline'}`} />
|
||||
<div className="led status-running" />
|
||||
</div>
|
||||
|
||||
{/* 电源指示灯 */}
|
||||
<div className="power-led">
|
||||
<div className={`power-led-indicator ${device.status !== 'offline' ? 'on' : ''}`} />
|
||||
<span className={`power-led-text ${device.status !== 'offline' ? 'on' : ''}`}>
|
||||
PWR
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 设备序列号标签 */}
|
||||
<div className="device-serial">
|
||||
<span className="device-serial-text">
|
||||
SN:{device.serial?.slice(-4) || '0000'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 中间设备信息区域 - 增强版 */}
|
||||
<div className="device-info">
|
||||
{/* 设备品牌/厂商标识 */}
|
||||
<div className="device-brand">
|
||||
<div className="device-brand-icon">
|
||||
{getDeviceIcon(device.type)}
|
||||
</div>
|
||||
<span className="device-brand-text">
|
||||
{device.brand || 'ENTERPRISE'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 设备名称 */}
|
||||
<div className="device-name">
|
||||
{deviceName}
|
||||
</div>
|
||||
|
||||
{/* 型号和规格 */}
|
||||
<div className="device-model">
|
||||
<span className="device-model-text">
|
||||
{device.model || device.type?.toUpperCase() || 'STD'}
|
||||
</span>
|
||||
{device.ip && (
|
||||
<span className="device-ip">
|
||||
{device.ip}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 散热/通风口装饰 - 根据设备类型显示 */}
|
||||
{(device.type === 'server' || device.type === 'storage') && (
|
||||
<div className="device-ventilation">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="ventilation-fin" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧端口/功能区域 - 增强版 */}
|
||||
<div className="device-ports">
|
||||
{/* 端口指示灯阵列 */}
|
||||
<div className="port-leds">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className={`port-led ${i < 3 ? 'active' : 'inactive'}`} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 管理接口标识 */}
|
||||
<div className="management-interface">
|
||||
<div className="management-icon" />
|
||||
<span className="management-text">
|
||||
MGMT
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 设备高度U数标识 */}
|
||||
<div className="device-height">
|
||||
{device.height}U
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('设备渲染错误:', error, device);
|
||||
// 渲染一个简单的错误显示元素
|
||||
return (
|
||||
<div
|
||||
key={`error-${Math.random()}`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '35px',
|
||||
right: '35px',
|
||||
top: '50%',
|
||||
height: '30px',
|
||||
transform: 'translateY(-50%)',
|
||||
backgroundColor: '#ff4d4f',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '12px',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
设备加载错误
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 默认导出
|
||||
export default DeviceComponent;
|
||||
+106
-19
@@ -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 = () => {
|
||||
<RobotOutlined style={{ fontSize: '40px', color: '#fff' }} />
|
||||
</div>
|
||||
<Title level={2} style={titleStyle}>
|
||||
{isFirstUser ? '创建管理员账户' : 'IDC设备管理系统'}
|
||||
{isFirstUser ? '创建管理员账户' : unlockMode ? '账户解锁' : 'IDC设备管理系统'}
|
||||
</Title>
|
||||
<Text style={subtitleStyle}>
|
||||
{isFirstUser ? '首次使用,请创建系统管理员账户' : '安全登录您的账户'}
|
||||
{isFirstUser ? '首次使用,请创建系统管理员账户' :
|
||||
unlockMode ? '输入账户信息以解锁账户' : '安全登录您的账户'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@@ -228,9 +248,9 @@ const Login = () => {
|
||||
)}
|
||||
|
||||
<Form
|
||||
name={registerMode ? 'register' : 'login'}
|
||||
name={unlockMode ? 'unlock' : registerMode ? 'register' : 'login'}
|
||||
size="large"
|
||||
onFinish={registerMode ? onFinishRegister : onFinishLogin}
|
||||
onFinish={unlockMode ? onFinishUnlock : registerMode ? onFinishRegister : onFinishLogin}
|
||||
style={formStyle}
|
||||
>
|
||||
{registerMode ? (
|
||||
@@ -321,6 +341,38 @@ const Login = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
) : unlockMode ? (
|
||||
<>
|
||||
<Alert
|
||||
message="账户解锁说明"
|
||||
description="当您的账户连续5次登录失败后会被锁定,请输入正确的用户名和密码进行解锁。"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '24px', borderRadius: '8px' }}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined style={inputPrefixStyle} />}
|
||||
placeholder="用户名"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined style={inputPrefixStyle} />}
|
||||
placeholder="密码"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Form.Item
|
||||
@@ -354,7 +406,7 @@ const Login = () => {
|
||||
loading={loading}
|
||||
style={submitButtonStyle}
|
||||
>
|
||||
{registerMode ? '立即注册' : '登 录'}
|
||||
{registerMode ? '立即注册' : unlockMode ? '解 锁' : '登 录'}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
@@ -365,20 +417,55 @@ const Login = () => {
|
||||
<Text style={{ color: '#8c8c8c', fontSize: '12px' }}>其他方式</Text>
|
||||
</Divider>
|
||||
<Space split={<Divider type="vertical" />}>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={toggleButtonStyle}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = 'transparent';
|
||||
}}
|
||||
onClick={() => setRegisterMode(!registerMode)}
|
||||
>
|
||||
{registerMode ? '已有账户?去登录' : '注册新账户'}
|
||||
</Button>
|
||||
{unlockMode ? (
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={toggleButtonStyle}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = 'transparent';
|
||||
}}
|
||||
onClick={() => setUnlockMode(false)}
|
||||
>
|
||||
返回登录
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={toggleButtonStyle}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = 'transparent';
|
||||
}}
|
||||
onClick={() => setRegisterMode(!registerMode)}
|
||||
>
|
||||
{registerMode ? '已有账户?去登录' : '注册新账户'}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
style={toggleButtonStyle}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.background = 'rgba(102, 126, 234, 0.1)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.background = 'transparent';
|
||||
}}
|
||||
onClick={() => setUnlockMode(true)}
|
||||
>
|
||||
账户解锁
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user