feat(仪表盘): 添加统计API并优化仪表盘组件
This commit is contained in:
@@ -25,13 +25,15 @@
|
||||
| **设备管理** | 服务器、网络设备、存储设备全生命周期管理,支持批量导入/导出 |
|
||||
| **端口管理** | 设备端口配置与管理,支持网卡绑定 |
|
||||
| **线缆管理** | 机柜间线缆连接管理,可视化追踪 |
|
||||
| **工单管理** | 故障报修、维护工单全流程管理,支持自定义字段 |
|
||||
| **耗材管理** | 耗材库存、领用记录、统计报表管理 |
|
||||
| **工单管理** | 故障报修、维护工单全流程管理,支持自定义字段、故障分类 |
|
||||
| **耗材管理** | 耗材库存、领用记录、统计报表管理,支持SN序列号追踪 |
|
||||
| **盘点管理** | 盘点计划制定与执行,盘点任务分配与结果记录 |
|
||||
| **数据看板** | 实时监控数据中心整体运行状态 |
|
||||
| **3D可视化** | 三维机柜可视化展示,支持设备悬停详情、LOD优化 |
|
||||
| **系统配置** | 设备字段、工单字段自定义管理,背景配置 |
|
||||
| **用户权限** | 基于角色的权限控制(RBAC) |
|
||||
| **数据看板** | 实时监控数据中心整体运行状态,设备趋势图表 |
|
||||
| **3D可视化** | 三维机柜可视化展示,支持设备悬停详情、LOD优化、性能模式 |
|
||||
| **系统配置** | 设备字段、工单字段自定义管理,背景配置,系统设置 |
|
||||
| **用户权限** | 基于角色的权限控制(RBAC),用户审核机制 |
|
||||
| **备份管理** | 数据库备份与恢复,自动定时备份,多协议远程备份(FTP/SFTP/WebDAV/SMB) |
|
||||
| **统计分析** | 设备、工单、耗材等多维度统计报表,用户增长趋势分析 |
|
||||
|
||||
### 项目截图
|
||||
|
||||
@@ -58,17 +60,19 @@
|
||||
| React | 18.2.0 | 前端框架(用户界面开发库) |
|
||||
| Vite | 4.4.9 | 构建工具(前端项目打包工具) |
|
||||
| Ant Design | 5.8.6 | UI组件库(企业级设计系统) |
|
||||
| Three.js | 0.160.0 | 3D渲染引擎 |
|
||||
| Three.js | 0.183.2 | 3D渲染引擎 |
|
||||
| @react-three/fiber | 8.18.0 | React Three.js 集成 |
|
||||
| @react-three/drei | 9.122.0 | Three.js 辅助组件库 |
|
||||
| React Router | 6.15.0 | 路由管理 |
|
||||
| Axios | 1.5.0 | HTTP客户端 |
|
||||
| Axios | 1.13.6 | HTTP客户端 |
|
||||
| Day.js | 1.11.19 | 日期处理库 |
|
||||
| SheetJS (xlsx) | 0.18.5 | Excel文件处理 |
|
||||
| PapaParse | 5.5.3 | CSV文件解析 |
|
||||
| Framer Motion | 12.34.0 | 动画库 |
|
||||
| Styled Components | 6.3.9 | CSS-in-JS 样式方案 |
|
||||
| SWR | 2.4.0 | 数据请求与缓存 |
|
||||
| @ant-design/icons | 6.1.0 | 图标库 |
|
||||
| Vitest | 4.0.16 | 前端测试框架 |
|
||||
|
||||
### 后端技术栈
|
||||
|
||||
@@ -76,14 +80,30 @@
|
||||
|------|------|------|
|
||||
| Node.js | ≥14.0.0(推荐 20.x LTS) | 运行环境 |
|
||||
| Express | 4.18.2 | Web框架(后端服务框架) |
|
||||
| Sequelize | 6.32.1 | ORM框架(数据库对象关系映射) |
|
||||
| SQLite | 5.1.6 | 嵌入式数据库(开发/小规模) |
|
||||
| Sequelize | 6.37.8 | ORM框架(数据库对象关系映射) |
|
||||
| SQLite | 5.1.7 | 嵌入式数据库(开发/小规模) |
|
||||
| MySQL | 8.0+ | 关系型数据库(生产环境) |
|
||||
| JWT | 9.0.3 | 身份认证 |
|
||||
| bcryptjs | 3.0.3 | 密码加密 |
|
||||
| Winston | 3.19.0 | 日志管理 |
|
||||
| Winston Daily Rotate File | 5.0.0 | 日志文件轮转 |
|
||||
| Joi | 18.0.2 | 数据验证 |
|
||||
| Jest | 30.2.0 | 测试框架 |
|
||||
| Multer | 2.1.1 | 文件上传处理 |
|
||||
| Express File Upload | 1.5.2 | 文件上传中间件 |
|
||||
| Node Cron | 4.2.1 | 定时任务调度 |
|
||||
| Axios | 1.13.6 | HTTP客户端 |
|
||||
| CSV Parser | 3.2.0 | CSV文件解析 |
|
||||
| CSV Writer | 1.6.0 | CSV文件生成 |
|
||||
| SheetJS (xlsx) | 0.18.5 | Excel文件处理 |
|
||||
| Day.js | 1.11.19 | 日期处理库 |
|
||||
| Basic FTP | 5.0.3 | FTP协议支持 |
|
||||
| SSH2 SFTP Client | 9.1.0 | SFTP协议支持 |
|
||||
| WebDAV | 5.3.1 | WebDAV协议支持 |
|
||||
| SMB2 | 0.2.2 | SMB/CIFS协议支持 |
|
||||
| MySQL2 | 3.19.1 | MySQL数据库驱动 |
|
||||
| Nodemon | 3.0.1 | 开发环境自动重启 |
|
||||
| Supertest | 7.1.4 | API测试工具 |
|
||||
|
||||
---
|
||||
|
||||
@@ -165,6 +185,10 @@ cd idc_assest
|
||||
#### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
# 一键安装所有依赖(推荐)
|
||||
npm run install:all
|
||||
|
||||
# 或分别安装
|
||||
# 安装后端依赖
|
||||
cd backend
|
||||
npm install
|
||||
@@ -226,8 +250,16 @@ node scripts/migrate-all.js
|
||||
|
||||
#### 6. 启动服务
|
||||
|
||||
**开发环境(同时启动前后端):**
|
||||
```bash
|
||||
# 在项目根目录运行
|
||||
npm start
|
||||
```
|
||||
|
||||
**或分别启动:**
|
||||
```bash
|
||||
# 启动后端(端口8000)
|
||||
cd backend
|
||||
npm run dev
|
||||
|
||||
# 启动前端(端口3000)- 新终端
|
||||
@@ -297,6 +329,24 @@ npm run dev
|
||||
- 管理耗材库存,设置库存预警阈值
|
||||
- 记录耗材领用,追踪使用去向
|
||||
- 查看耗材统计报表
|
||||
- 支持SN序列号追踪
|
||||
- 耗材操作日志归档
|
||||
|
||||
#### 7. 备份管理
|
||||
|
||||
- **手动备份**:立即创建数据库备份
|
||||
- **自动备份**:配置定时自动备份(支持cron表达式)
|
||||
- **备份恢复**:从备份文件恢复数据库
|
||||
- **远程备份**:支持多种协议(FTP/SFTP/WebDAV/SMB)上传备份到远程服务器
|
||||
- **备份列表**:查看和管理所有备份文件
|
||||
|
||||
#### 8. 统计分析
|
||||
|
||||
- **设备统计**:设备总数、状态分布、趋势图表
|
||||
- **工单统计**:工单数量、处理时效、分类统计
|
||||
- **耗材统计**:库存状况、领用趋势、分类统计
|
||||
- **用户统计**:用户增长趋势、活跃用户分析
|
||||
- **数据看板**:实时展示关键指标
|
||||
|
||||
---
|
||||
|
||||
@@ -307,50 +357,117 @@ jigui/
|
||||
├── frontend/ # 前端项目
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # API接口封装
|
||||
│ │ ├── assets/ # 静态资源(字体、3D环境贴图)
|
||||
│ │ │ ├── index.js # API请求封装
|
||||
│ │ │ └── cache.js # 缓存管理
|
||||
│ │ ├── components/ # 通用组件
|
||||
│ │ │ ├── 3d/ # 3D可视化组件
|
||||
│ │ │ │ ├── materials/ # 3D材质组件
|
||||
│ │ │ │ │ ├── constants.jsx
|
||||
│ │ │ │ │ ├── devicePanel.jsx
|
||||
│ │ │ │ │ ├── index.jsx
|
||||
│ │ │ │ │ ├── rackFrame.jsx
|
||||
│ │ │ │ │ └── utils.jsx
|
||||
│ │ │ │ ├── DeviceModel.jsx # 设备模型
|
||||
│ │ │ │ ├── RackModel.jsx # 机柜模型
|
||||
│ │ │ │ ├── Scene.jsx # 3D场景
|
||||
│ │ │ │ ├── LODManager.jsx # LOD管理器
|
||||
│ │ │ │ └── utils.jsx # 3D工具函数
|
||||
│ │ │ ├── DeviceDetailDrawer.jsx # 设备详情抽屉
|
||||
│ │ │ ├── PortManagementPanel.jsx # 端口管理面板
|
||||
│ │ │ └── ...
|
||||
│ │ │ │ └── LODManager.jsx # LOD管理器
|
||||
│ │ │ ├── dashboard/ # 数据看板组件
|
||||
│ │ │ │ ├── AnimatedCounter.jsx
|
||||
│ │ │ │ ├── CircularProgress.jsx
|
||||
│ │ │ │ ├── DeviceTrendChart.jsx
|
||||
│ │ │ │ ├── NavigationGrid.jsx
|
||||
│ │ │ │ ├── PowerGauge.jsx
|
||||
│ │ │ │ ├── QuickStats.jsx
|
||||
│ │ │ │ ├── StatCard.jsx
|
||||
│ │ │ │ ├── StatusLegend.jsx
|
||||
│ │ │ │ └── SystemInfo.jsx
|
||||
│ │ │ ├── device/ # 设备管理组件
|
||||
│ │ │ │ ├── BatchStatusModal.jsx
|
||||
│ │ │ │ ├── DeviceDetailModal.jsx
|
||||
│ │ │ │ ├── DeviceFormModal.jsx
|
||||
│ │ │ │ ├── ExportModal.jsx
|
||||
│ │ │ │ ├── FieldConfigModal.jsx
|
||||
│ │ │ │ ├── ImportModal.jsx
|
||||
│ │ │ │ └── ResizableTitle.jsx
|
||||
│ │ │ ├── CableCreateModal.jsx
|
||||
│ │ │ ├── CloseButton.jsx
|
||||
│ │ │ ├── DeviceDetailDrawer.jsx
|
||||
│ │ │ ├── ErrorBoundary.jsx
|
||||
│ │ │ ├── NetworkCardCreateModal.jsx
|
||||
│ │ │ ├── NetworkCardPanel.jsx
|
||||
│ │ │ ├── PortCreateModal.jsx
|
||||
│ │ │ ├── PortManagementPanel.jsx
|
||||
│ │ │ ├── PortPanel.jsx
|
||||
│ │ │ ├── ProtectedRoute.jsx
|
||||
│ │ │ ├── ServerBackplanePanel.jsx
|
||||
│ │ │ └── VirtualDeviceList.jsx
|
||||
│ │ ├── config/ # 配置文件
|
||||
│ │ │ ├── api.js # API配置
|
||||
│ │ │ └── theme.js # 主题配置
|
||||
│ │ ├── constants/ # 常量定义
|
||||
│ │ │ └── deviceManagementConstants.js
|
||||
│ │ ├── context/ # React Context状态管理
|
||||
│ │ │ ├── AuthContext.jsx # 认证上下文
|
||||
│ │ │ ├── ConfigContext.jsx # 配置上下文
|
||||
│ │ │ └── Scene3DContext.jsx # 3D场景上下文
|
||||
│ │ ├── hooks/ # 自定义Hooks
|
||||
│ │ │ ├── useApi.js # API请求Hook
|
||||
│ │ │ ├── useDebounce.js # 防抖Hook
|
||||
│ │ │ ├── useDesignTokens.js # 设计令牌Hook
|
||||
│ │ │ └── useIdleTimeout.js # 空闲超时Hook
|
||||
│ │ │ ├── useIdleTimeout.js # 空闲超时Hook
|
||||
│ │ │ └── useSWR.js # SWR数据请求Hook
|
||||
│ │ ├── pages/ # 页面模块
|
||||
│ │ │ ├── Dashboard.jsx # 数据看板
|
||||
│ │ │ ├── Login.jsx # 登录页面
|
||||
│ │ │ ├── RoomManagement.jsx # 机房管理
|
||||
│ │ │ ├── RackManagement.jsx # 机柜管理
|
||||
│ │ │ ├── DeviceManagement.jsx # 设备管理
|
||||
│ │ │ ├── DeviceFieldManagement.jsx # 设备字段管理
|
||||
│ │ │ ├── PendingDeviceManagement.jsx # 待审核设备管理
|
||||
│ │ │ ├── PortManagement.jsx # 端口管理
|
||||
│ │ │ ├── CableManagement.jsx # 线缆管理
|
||||
│ │ │ ├── TicketManagement.jsx # 工单管理
|
||||
│ │ │ ├── TicketCategoryManagement.jsx # 工单分类管理
|
||||
│ │ │ ├── TicketFieldManagement.jsx # 工单字段管理
|
||||
│ │ │ ├── TicketStatistics.jsx # 工单统计
|
||||
│ │ │ ├── ConsumableManagement.jsx # 耗材管理
|
||||
│ │ │ ├── CategoryManagement.jsx # 耗材分类管理
|
||||
│ │ │ ├── ConsumableLogs.jsx # 耗材日志
|
||||
│ │ │ ├── ConsumableStatistics.jsx # 耗材统计
|
||||
│ │ │ ├── InventoryManagement.jsx # 盘点管理
|
||||
│ │ │ ├── InventoryTaskExecution.jsx # 盘点任务执行
|
||||
│ │ │ ├── UserManagement.jsx # 用户管理
|
||||
│ │ │ ├── RolesManagement.jsx # 角色管理
|
||||
│ │ │ ├── SystemSettings.jsx # 系统设置
|
||||
│ │ │ └── Rack3DVisualization.jsx # 3D可视化
|
||||
│ │ │ ├── BackupManagement.jsx # 备份管理
|
||||
│ │ │ ├── AutoBackupSettings.jsx # 自动备份设置
|
||||
│ │ │ ├── RemoteBackupSettings.jsx # 远程备份设置
|
||||
│ │ │ ├── Rack3DVisualization.jsx # 3D可视化
|
||||
│ │ │ └── ErrorBoundaryTest.jsx # 错误边界测试
|
||||
│ │ ├── styles/ # 样式文件
|
||||
│ │ │ └── deviceManagementStyles.js
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ ├── config/ # 配置文件
|
||||
│ │ ├── constants/ # 常量定义
|
||||
│ │ │ ├── common.js
|
||||
│ │ │ ├── crypto.js
|
||||
│ │ │ └── deviceUtils.jsx
|
||||
│ │ ├── App.jsx # 应用入口
|
||||
│ │ ├── main.jsx # 渲染入口
|
||||
│ │ └── index.css # 全局样式
|
||||
│ ├── public/ # 公共资源
|
||||
│ │ ├── assets/3d/ # 3D资源
|
||||
│ │ │ └── env.hdr # 环境贴图
|
||||
│ │ └── fonts/ # 字体文件
|
||||
│ │ └── Inter-Regular.woff2
|
||||
│ ├── vite.config.mjs # Vite配置
|
||||
│ ├── .env.example # 环境变量示例
|
||||
│ └── package.json # 前端依赖
|
||||
├── backend/ # 后端项目
|
||||
│ ├── config/ # 配置文件
|
||||
│ │ ├── index.js # 配置统一导出
|
||||
│ │ ├── constants.js # 常量配置
|
||||
│ │ ├── security.js # 安全配置
|
||||
│ │ ├── auto-backup-settings.json # 自动备份设置
|
||||
│ │ └── remote-backup-configs.json # 远程备份配置
|
||||
│ ├── middleware/ # 中间件
|
||||
│ │ ├── auth.js # 认证中间件
|
||||
│ │ └── validation.js # 验证中间件
|
||||
@@ -360,53 +477,113 @@ jigui/
|
||||
│ │ ├── Device.js # 设备模型
|
||||
│ │ ├── DeviceField.js # 设备字段模型
|
||||
│ │ ├── DevicePort.js # 设备端口模型
|
||||
│ │ ├── NetworkCard.js # 网卡模型
|
||||
│ │ ├── NetworkCard.js # 网卡模型
|
||||
│ │ ├── Cable.js # 线缆模型
|
||||
│ │ ├── Ticket.js # 工单模型
|
||||
│ │ ├── TicketField.js # 工单字段模型
|
||||
│ │ ├── TicketCategory.js # 工单分类模型
|
||||
│ │ ├── Consumable.js # 耗材模型
|
||||
│ │ ├── ConsumableCategory.js # 耗材分类模型
|
||||
│ │ ├── InventoryPlan.js # 盘点计划模型
|
||||
│ │ ├── InventoryTask.js # 盘点任务模型
|
||||
│ │ ├── InventoryRecord.js # 盘点记录模型
|
||||
│ │ ├── User.js # 用户模型
|
||||
│ │ ├── Role.js # 角色模型
|
||||
│ │ ├── Permission.js # 权限模型
|
||||
│ │ └── SystemSetting.js # 系统设置模型
|
||||
│ │ ├── TicketOperationRecord.js # 工单操作记录模型
|
||||
│ │ ├── FaultCategory.js # 故障分类模型
|
||||
│ │ ├── Consumable.js # 耗材模型
|
||||
│ │ ├── ConsumableCategory.js # 耗材分类模型
|
||||
│ │ ├── ConsumableRecord.js # 耗材领用记录模型
|
||||
│ │ ├── ConsumableLog.js # 耗材日志模型
|
||||
│ │ ├── ConsumableLogArchive.js # 耗材日志归档模型
|
||||
│ │ ├── InventoryPlan.js # 盘点计划模型
|
||||
│ │ ├── InventoryTask.js # 盘点任务模型
|
||||
│ │ ├── InventoryRecord.js # 盘点记录模型
|
||||
│ │ ├── User.js # 用户模型
|
||||
│ │ ├── Role.js # 角色模型
|
||||
│ │ ├── Permission.js # 权限模型
|
||||
│ │ ├── UserRole.js # 用户角色关联模型
|
||||
│ │ ├── SystemSetting.js # 系统设置模型
|
||||
│ │ ├── PendingDevice.js # 待审核设备模型
|
||||
│ │ └── ticketIndex.js # 工单模型初始化
|
||||
│ ├── routes/ # API路由
|
||||
│ │ ├── auth.js # 认证路由
|
||||
│ │ ├── rooms.js # 机房路由
|
||||
│ │ ├── racks.js # 机柜路由
|
||||
│ │ ├── devices.js # 设备路由
|
||||
│ │ ├── deviceFields.js # 设备字段路由
|
||||
│ │ ├── devicePorts.js # 设备端口路由
|
||||
│ │ ├── networkCards.js # 网卡路由
|
||||
│ │ ├── cables.js # 线缆路由
|
||||
│ │ ├── tickets.js # 工单路由
|
||||
│ │ ├── consumables.js # 耗材路由
|
||||
│ │ ├── ticketCategories.js # 工单分类路由
|
||||
│ │ ├── ticketFields.js # 工单字段路由
|
||||
│ │ ├── consumables.js # 耗材路由
|
||||
│ │ ├── consumableCategories.js # 耗材分类路由
|
||||
│ │ ├── consumableRecords.js # 耗材记录路由
|
||||
│ │ ├── inventory.js # 盘点路由
|
||||
│ │ ├── users.js # 用户路由
|
||||
│ │ └── ...
|
||||
│ │ ├── roles.js # 角色路由
|
||||
│ │ ├── systemSettings.js # 系统设置路由
|
||||
│ │ ├── background.js # 背景配置路由
|
||||
│ │ ├── backup.js # 备份管理路由
|
||||
│ │ ├── statistics.js # 统计分析路由
|
||||
│ │ └── ticketCategories.js # 工单分类路由
|
||||
│ ├── scripts/ # 数据库脚本
|
||||
│ │ ├── init-database.js # 数据库初始化
|
||||
│ │ ├── migrate-*.js # 数据迁移脚本
|
||||
│ │ └── archive/ # 归档脚本
|
||||
│ ├── uploads/ # 文件上传目录
|
||||
│ │ ├── migrate-all.js # 执行所有迁移脚本
|
||||
│ │ ├── archive/ # 归档迁移脚本
|
||||
│ │ │ ├── migrate-v2.js
|
||||
│ │ │ ├── migrate-consumable-logs.js
|
||||
│ │ │ ├── migrate-consumable-version.js
|
||||
│ │ │ ├── migrate-consumable-log-decouple.js
|
||||
│ │ │ ├── migrate-consumable-log-archive.js
|
||||
│ │ │ ├── remove-consumable-log-fk.js
|
||||
│ │ │ └── migrate-add-pending-status.js
|
||||
│ │ ├── backup.js # 备份脚本
|
||||
│ │ ├── restore.js # 恢复脚本
|
||||
│ │ ├── ensure_nic_schema.js # 确保网卡表结构
|
||||
│ │ ├── add-isSystem-column.js
|
||||
│ │ ├── add-isSystem-mysql.js
|
||||
│ │ ├── add-isSystem-sqlite.js
|
||||
│ │ ├── update-system-fields.js
|
||||
│ │ └── update-system-fields-v2.js
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── autoBackupScheduler.js # 自动备份调度器
|
||||
│ │ ├── backup.js # 备份工具
|
||||
│ │ ├── remoteBackup.js # 远程备份工具
|
||||
│ │ └── remoteBackupConfig.js # 远程备份配置
|
||||
│ ├── validation/ # 数据验证Schema
|
||||
│ │ ├── deviceSchema.js
|
||||
│ │ ├── rackSchema.js
|
||||
│ │ └── roomSchema.js
|
||||
│ ├── uploads/ # 文件上传目录
|
||||
│ │ └── avatars/ # 用户头像目录
|
||||
│ ├── .env.example # 环境变量示例
|
||||
│ ├── server.js # 服务入口
|
||||
│ ├── db.js # 数据库连接
|
||||
│ ├── initConfig.js # 初始化配置
|
||||
│ ├── initDeviceFields.js # 初始化设备字段
|
||||
│ ├── initTicketFields.js # 初始化工单字段
|
||||
│ ├── create_indexes.js # 创建数据库索引
|
||||
│ ├── backgroundSettings.json # 背景设置
|
||||
│ ├── unlock_user.js # 用户解锁脚本
|
||||
│ └── package.json # 后端依赖
|
||||
├── docs/ # 项目文档
|
||||
│ ├── api/ # 接口文档
|
||||
│ └── images/ # 文档图片
|
||||
│ │ └── README.md
|
||||
│ ├── images/ # 文档图片
|
||||
│ │ ├── dashboard.png
|
||||
│ │ ├── 3d-visualization.png
|
||||
│ │ ├── device-management.png
|
||||
│ │ └── room-management.png
|
||||
│ └── USER_GUIDE.md # 用户指南
|
||||
├── scripts/ # 项目脚本
|
||||
│ └── frontend-manager.js # 前端管理脚本
|
||||
├── install.js # 交互式安装脚本 ⭐
|
||||
├── update.js # 一键更新脚本 ⭐
|
||||
├── uninstall.js # 卸载脚本
|
||||
├── check.js # 环境检查脚本
|
||||
├── modify.js # 配置修改脚本
|
||||
├── package.json # 项目根依赖
|
||||
├── README.md # 项目说明
|
||||
├── CHANGELOG.md # 版本记录
|
||||
├── DEPLOYMENT.md # 部署指南
|
||||
└── LICENSE # 许可证
|
||||
├── LICENSE # 许可证
|
||||
├── .eslintrc.js # ESLint配置
|
||||
├── .prettierrc # Prettier配置
|
||||
└── .prettierignore # Prettier忽略文件
|
||||
```
|
||||
|
||||
---
|
||||
@@ -451,6 +628,33 @@ pm2 restart idc-backend
|
||||
| Content-Type | `application/json` |
|
||||
| 认证方式 | Bearer Token (JWT) |
|
||||
|
||||
### API端点列表
|
||||
|
||||
| 模块 | 路由端点 | 功能说明 |
|
||||
|------|----------|----------|
|
||||
| 认证 | `/api/auth` | 登录、注册、登出、令牌刷新 |
|
||||
| 机房 | `/api/rooms` | 机房增删改查 |
|
||||
| 机柜 | `/api/racks` | 机柜增删改查 |
|
||||
| 设备 | `/api/devices` | 设备管理、批量导入导出 |
|
||||
| 设备字段 | `/api/deviceFields` | 设备自定义字段配置 |
|
||||
| 设备端口 | `/api/device-ports` | 设备端口管理 |
|
||||
| 网卡 | `/api/network-cards` | 网卡管理 |
|
||||
| 线缆 | `/api/cables` | 线缆连接管理 |
|
||||
| 工单 | `/api/tickets` | 工单管理、状态更新 |
|
||||
| 工单分类 | `/api/ticket-categories` | 工单分类管理 |
|
||||
| 工单字段 | `/api/ticket-fields` | 工单自定义字段配置 |
|
||||
| 耗材 | `/api/consumables` | 耗材库存管理 |
|
||||
| 耗材分类 | `/api/consumable-categories` | 耗材分类管理 |
|
||||
| 耗材记录 | `/api/consumable-records` | 耗材领用记录 |
|
||||
| 盘点 | `/api/inventory` | 盘点计划、任务、记录 |
|
||||
| 用户 | `/api/users` | 用户管理 |
|
||||
| 角色 | `/api/roles` | 角色权限管理 |
|
||||
| 系统设置 | `/api/system-settings` | 系统配置管理 |
|
||||
| 背景配置 | `/api/background` | 系统背景配置 |
|
||||
| 备份管理 | `/api/backup` | 数据库备份、恢复、自动备份设置 |
|
||||
| 统计分析 | `/api/statistics` | 多维度数据统计报表 |
|
||||
| 健康检查 | `/health` | 后端服务健康状态 |
|
||||
|
||||
### 通用响应格式
|
||||
|
||||
**成功响应:**
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Op } = require('sequelize');
|
||||
const Device = require('../models/Device');
|
||||
const Rack = require('../models/Rack');
|
||||
const Room = require('../models/Room');
|
||||
const User = require('../models/User');
|
||||
const Ticket = require('../models/Ticket');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const now = new Date();
|
||||
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
const twoWeeksAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
const dayQueries = [];
|
||||
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const dayStart = new Date(now);
|
||||
dayStart.setDate(now.getDate() - i);
|
||||
dayStart.setHours(0, 0, 0, 0);
|
||||
|
||||
const dayEnd = new Date(dayStart);
|
||||
dayEnd.setHours(23, 59, 59, 999);
|
||||
|
||||
const dayOfWeek = (dayStart.getDay() + 6) % 7;
|
||||
|
||||
dayQueries.push({
|
||||
label: weekDays[dayOfWeek],
|
||||
query: Device.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
[Op.gte]: dayStart,
|
||||
[Op.lt]: dayEnd
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const dayResults = await Promise.all(dayQueries.map(d => d.query));
|
||||
const deviceTrendData = dayQueries.map((d, index) => ({
|
||||
label: d.label,
|
||||
value: dayResults[index]
|
||||
}));
|
||||
|
||||
const [
|
||||
totalDevices, faultDevices, totalRacks, rooms, totalUsers, activeTickets,
|
||||
newDevicesThisWeek, newDevicesLastWeek,
|
||||
faultDevicesThisWeek, faultDevicesLastWeek,
|
||||
newUsersThisWeek, newUsersLastWeek,
|
||||
newTicketsThisWeek, newTicketsLastWeek,
|
||||
runningDevices, maintenanceDevices, offlineDevices
|
||||
] = await Promise.all([
|
||||
Device.count(),
|
||||
Device.count({ where: { status: 'fault' } }),
|
||||
Rack.count(),
|
||||
Room.findAll(),
|
||||
User.count(),
|
||||
Ticket.count({ where: { status: 'open' } }),
|
||||
Device.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
|
||||
Device.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
|
||||
Device.count({ where: { status: 'fault', updatedAt: { [Op.gte]: oneWeekAgo } } }),
|
||||
Device.count({ where: { status: 'fault', updatedAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
|
||||
User.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
|
||||
User.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
|
||||
Ticket.count({ where: { createdAt: { [Op.gte]: oneWeekAgo } } }),
|
||||
Ticket.count({ where: { createdAt: { [Op.gte]: twoWeeksAgo, [Op.lt]: oneWeekAgo } } }),
|
||||
Device.count({ where: { status: 'running' } }),
|
||||
Device.count({ where: { status: 'maintenance' } }),
|
||||
Device.count({ where: { status: 'offline' } }),
|
||||
]);
|
||||
|
||||
let deviceGrowth = 0;
|
||||
if (newDevicesLastWeek > 0) {
|
||||
deviceGrowth = parseFloat(((newDevicesThisWeek - newDevicesLastWeek) / newDevicesLastWeek * 100).toFixed(1));
|
||||
} else if (newDevicesThisWeek > 0) {
|
||||
deviceGrowth = 100;
|
||||
} else {
|
||||
deviceGrowth = 0;
|
||||
}
|
||||
|
||||
let faultTrend = 0;
|
||||
if (faultDevicesLastWeek > 0) {
|
||||
faultTrend = parseFloat(((faultDevicesThisWeek - faultDevicesLastWeek) / faultDevicesLastWeek * 100).toFixed(1));
|
||||
} else if (faultDevicesThisWeek > 0) {
|
||||
faultTrend = 100;
|
||||
} else {
|
||||
faultTrend = 0;
|
||||
}
|
||||
|
||||
let userGrowth = 0;
|
||||
if (newUsersLastWeek > 0) {
|
||||
userGrowth = parseFloat(((newUsersThisWeek - newUsersLastWeek) / newUsersLastWeek * 100).toFixed(1));
|
||||
} else if (newUsersThisWeek > 0) {
|
||||
userGrowth = 100;
|
||||
} else {
|
||||
userGrowth = 0;
|
||||
}
|
||||
|
||||
let ticketTrend = 0;
|
||||
if (newTicketsLastWeek > 0) {
|
||||
ticketTrend = parseFloat(((newTicketsThisWeek - newTicketsLastWeek) / newTicketsLastWeek * 100).toFixed(1));
|
||||
} else if (newTicketsThisWeek > 0) {
|
||||
ticketTrend = 100;
|
||||
} else {
|
||||
ticketTrend = 0;
|
||||
}
|
||||
|
||||
const onlineRate = totalDevices > 0
|
||||
? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1)
|
||||
: 100;
|
||||
|
||||
const totalRooms = rooms.length;
|
||||
|
||||
const racks = await Rack.findAll();
|
||||
const totalPower = racks.reduce((sum, rack) => sum + (rack.currentPower || 0), 0);
|
||||
const totalMaxPower = racks.reduce((sum, rack) => sum + (rack.maxPower || 0), 0);
|
||||
|
||||
res.json({
|
||||
totalDevices,
|
||||
totalRacks,
|
||||
totalRooms,
|
||||
totalUsers,
|
||||
activeTickets,
|
||||
faultDevices,
|
||||
deviceGrowth,
|
||||
faultTrend,
|
||||
userGrowth,
|
||||
ticketTrend,
|
||||
onlineRate,
|
||||
powerUsage: Math.floor(totalPower) || 0,
|
||||
totalMaxPower: Math.floor(totalMaxPower) || 10000,
|
||||
deviceStatus: {
|
||||
running: runningDevices,
|
||||
maintenance: maintenanceDevices,
|
||||
fault: faultDevices,
|
||||
offline: offlineDevices,
|
||||
},
|
||||
deviceTrendData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -170,6 +170,7 @@ const devicePortRoutes = require('./routes/devicePorts');
|
||||
const networkCardRoutes = require('./routes/networkCards');
|
||||
const inventoryRoutes = require('./routes/inventory');
|
||||
const backupRoutes = require('./routes/backup');
|
||||
const statisticsRoutes = require('./routes/statistics');
|
||||
|
||||
app.use('/api/devices', deviceRoutes);
|
||||
app.use('/api/racks', rackRoutes);
|
||||
@@ -191,6 +192,7 @@ app.use('/api/device-ports', devicePortRoutes);
|
||||
app.use('/api/network-cards', networkCardRoutes);
|
||||
app.use('/api/inventory', inventoryRoutes);
|
||||
app.use('/api/backup', backupRoutes);
|
||||
app.use('/api/statistics', statisticsRoutes);
|
||||
|
||||
app.use('/uploads', express.static('uploads'));
|
||||
|
||||
|
||||
@@ -16,7 +16,11 @@ const quickStatItemStyle = {
|
||||
boxShadow: designTokens.shadows.small,
|
||||
};
|
||||
|
||||
const QuickStats = ({ onlineRate, powerUsage }) => {
|
||||
const QuickStats = ({ onlineRate, powerUsage, totalMaxPower }) => {
|
||||
const powerUsagePercent = totalMaxPower > 0
|
||||
? ((powerUsage / totalMaxPower) * 100).toFixed(1)
|
||||
: '0.0';
|
||||
|
||||
const quickStats = [
|
||||
{
|
||||
icon: LineChartOutlined,
|
||||
@@ -33,7 +37,7 @@ const QuickStats = ({ onlineRate, powerUsage }) => {
|
||||
{
|
||||
icon: ThunderboltOutlined,
|
||||
label: '功率使用',
|
||||
value: `${powerUsage}W`,
|
||||
value: `${powerUsagePercent}%`,
|
||||
color: designTokens.colors.warning.main,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -46,6 +46,7 @@ const StatCard = ({
|
||||
trend,
|
||||
tagColor,
|
||||
customStatus,
|
||||
hideTrend,
|
||||
xs,
|
||||
sm,
|
||||
lg,
|
||||
@@ -135,7 +136,7 @@ const StatCard = ({
|
||||
/>
|
||||
<span>{statKey === 'totalRacks' ? '正常运行中' : '全部在线'}</span>
|
||||
</div>
|
||||
) : (
|
||||
) : !hideTrend ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -166,7 +167,7 @@ const StatCard = ({
|
||||
环比
|
||||
</Tag>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -4,12 +4,12 @@ import { designTokens } from '../../config/theme';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const StatusLegend = () => {
|
||||
const StatusLegend = ({ deviceStatusPercentages }) => {
|
||||
const legends = [
|
||||
{ color: designTokens.colors.success.main, label: '运行中', percent: 60 },
|
||||
{ color: designTokens.colors.warning.main, label: '维护中', percent: 20 },
|
||||
{ color: designTokens.colors.error.main, label: '故障', percent: 10 },
|
||||
{ color: designTokens.colors.primary.main, label: '离线', percent: 10 },
|
||||
{ color: designTokens.colors.success.main, label: '运行中', percent: deviceStatusPercentages?.running ?? 0 },
|
||||
{ color: designTokens.colors.warning.main, label: '维护中', percent: deviceStatusPercentages?.maintenance ?? 0 },
|
||||
{ color: designTokens.colors.error.main, label: '故障', percent: deviceStatusPercentages?.fault ?? 0 },
|
||||
{ color: designTokens.colors.primary.main, label: '离线', percent: deviceStatusPercentages?.offline ?? 0 },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,11 +5,13 @@ const fetcher = url => api.get(url).then(res => res);
|
||||
|
||||
export const swrConfig = {
|
||||
fetcher,
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnFocus: true,
|
||||
revalidateOnReconnect: true,
|
||||
shouldRetryOnError: false,
|
||||
dedupingInterval: 5000,
|
||||
shouldRetryOnError: true,
|
||||
dedupingInterval: 2000,
|
||||
errorRetryCount: 2,
|
||||
refreshInterval: 30000,
|
||||
refreshWhenHidden: false,
|
||||
};
|
||||
|
||||
export const useSWRConfig = () => {
|
||||
|
||||
@@ -317,21 +317,6 @@ function CableManagement() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSwitch = async switchId => {
|
||||
try {
|
||||
await axios.delete(`/api/devices/${switchId}`);
|
||||
message.success({
|
||||
content: '删除设备成功',
|
||||
icon: <CheckCircleOutlined style={{ color: designTokens.colors.success.main }} />,
|
||||
});
|
||||
fetchDevices();
|
||||
fetchCables();
|
||||
} catch (error) {
|
||||
message.error('删除设备失败');
|
||||
console.error('删除设备失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
@@ -662,7 +647,11 @@ function CableManagement() {
|
||||
};
|
||||
|
||||
const getPortConnectionStatus = (portName, switchData) => {
|
||||
const cable = switchData.cables.find(c => c.sourcePort === portName);
|
||||
const cable = switchData.cables.find(c => {
|
||||
if (c.sourcePort === portName) return true;
|
||||
if (c.sourcePort?.toLowerCase() === portName?.toLowerCase()) return true;
|
||||
return false;
|
||||
});
|
||||
if (!cable) {
|
||||
return { status: 'disconnected', text: '未连接', color: 'default' };
|
||||
}
|
||||
@@ -727,7 +716,11 @@ function CableManagement() {
|
||||
key: 'targetDevice',
|
||||
width: 200,
|
||||
render: (_, record) => {
|
||||
const cable = record.switchData.cables.find(c => c.sourcePort === record.portName);
|
||||
const cable = record.switchData.cables.find(c => {
|
||||
if (c.sourcePort === record.portName) return true;
|
||||
if (c.sourcePort?.toLowerCase() === record.portName?.toLowerCase()) return true;
|
||||
return false;
|
||||
});
|
||||
if (!cable) return <Text type="secondary">-</Text>;
|
||||
return (
|
||||
<div>
|
||||
@@ -743,7 +736,11 @@ function CableManagement() {
|
||||
key: 'cableType',
|
||||
width: 100,
|
||||
render: (_, record) => {
|
||||
const cable = record.switchData.cables.find(c => c.sourcePort === record.portName);
|
||||
const cable = record.switchData.cables.find(c => {
|
||||
if (c.sourcePort === record.portName) return true;
|
||||
if (c.sourcePort?.toLowerCase() === record.portName?.toLowerCase()) return true;
|
||||
return false;
|
||||
});
|
||||
if (!cable) return <Text type="secondary">-</Text>;
|
||||
return getCableTypeTag(cable.cableType);
|
||||
},
|
||||
@@ -754,7 +751,11 @@ function CableManagement() {
|
||||
key: 'cableLength',
|
||||
width: 80,
|
||||
render: (_, record) => {
|
||||
const cable = record.switchData.cables.find(c => c.sourcePort === record.portName);
|
||||
const cable = record.switchData.cables.find(c => {
|
||||
if (c.sourcePort === record.portName) return true;
|
||||
if (c.sourcePort?.toLowerCase() === record.portName?.toLowerCase()) return true;
|
||||
return false;
|
||||
});
|
||||
if (!cable) return <Text type="secondary">-</Text>;
|
||||
return cable.cableLength ? <Tag color="orange" style={{ borderRadius: '4px' }}>{cable.cableLength}m</Tag> : <Text type="secondary">-</Text>;
|
||||
},
|
||||
@@ -762,35 +763,37 @@ function CableManagement() {
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
width: 160,
|
||||
render: (_, record) => {
|
||||
const cable = record.switchData.cables.find(c => c.sourcePort === record.portName);
|
||||
const cable = record.switchData.cables.find(c => {
|
||||
if (c.sourcePort === record.portName) return true;
|
||||
if (c.sourcePort?.toLowerCase() === record.portName?.toLowerCase()) return true;
|
||||
return false;
|
||||
});
|
||||
if (!cable) {
|
||||
return <Text type="secondary">-</Text>;
|
||||
}
|
||||
return (
|
||||
<Space size="small">
|
||||
{cable && (
|
||||
<>
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(cable)}
|
||||
style={{ color: designTokens.colors.primary.main }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="删除">
|
||||
<Popconfirm
|
||||
title="确定要删除这条接线吗?"
|
||||
onConfirm={() => handleDelete(cable.cableId)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(cable)}
|
||||
style={{ color: designTokens.colors.primary.main }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="删除">
|
||||
<Popconfirm
|
||||
title="确定要删除这条接线吗?"
|
||||
onConfirm={() => handleDelete(cable.cableId)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
@@ -1047,10 +1050,8 @@ function CableManagement() {
|
||||
activeKey={expandedKeys}
|
||||
onChange={setExpandedKeys}
|
||||
style={{ background: 'transparent', border: 'none' }}
|
||||
expandIconPosition="end"
|
||||
>
|
||||
<AnimatePresence>
|
||||
{Object.entries(groupedCables).map(([switchId, switchData], index) => {
|
||||
{Object.entries(groupedCables).map(([switchId, switchData], index) => {
|
||||
const switchDevice = switchData.switch;
|
||||
const switchPorts = devicePorts[switchId] || [];
|
||||
const connectedCount = switchData.cables.filter(c => c.status === 'normal').length;
|
||||
@@ -1058,147 +1059,123 @@ function CableManagement() {
|
||||
const faultCount = switchData.cables.filter(c => c.status === 'fault').length;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<Panel
|
||||
key={switchId}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.05 }}
|
||||
>
|
||||
<Panel
|
||||
header={
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
paddingRight: '16px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: designTokens.borderRadius.md,
|
||||
background: designTokens.colors.primary.gradient,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: '24px',
|
||||
boxShadow: designTokens.shadows.md,
|
||||
}}
|
||||
>
|
||||
<AppstoreOutlined />
|
||||
header={
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
paddingRight: '16px',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<div
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: designTokens.borderRadius.md,
|
||||
background: designTokens.colors.primary.gradient,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: '24px',
|
||||
boxShadow: designTokens.shadows.md,
|
||||
}}
|
||||
>
|
||||
<AppstoreOutlined />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: '16px', color: designTokens.colors.neutral[800] }}>
|
||||
{switchDevice?.name || '未知设备'}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: '16px', color: designTokens.colors.neutral[800] }}>
|
||||
{switchDevice?.name || '未知设备'}
|
||||
</div>
|
||||
<div style={{ fontSize: '13px', color: designTokens.colors.neutral[500], marginTop: '2px' }}>
|
||||
{switchDevice?.deviceId || '-'} · {switchDevice?.model || '交换机'}
|
||||
</div>
|
||||
<div style={{ fontSize: '13px', color: designTokens.colors.neutral[500], marginTop: '2px' }}>
|
||||
{switchDevice?.deviceId || '-'} · {switchDevice?.model || '交换机'}
|
||||
</div>
|
||||
</div>
|
||||
<Space size="middle">
|
||||
<Tooltip title="正常连接">
|
||||
<Tag
|
||||
color="success"
|
||||
style={{ borderRadius: '4px', padding: '4px 12px' }}
|
||||
icon={<CheckCircleOutlined />}
|
||||
>
|
||||
{connectedCount}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip title="未连接">
|
||||
<Tag
|
||||
style={{ borderRadius: '4px', padding: '4px 12px' }}
|
||||
icon={<DisconnectOutlined />}
|
||||
>
|
||||
{disconnectedCount}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
{faultCount > 0 && (
|
||||
<Tooltip title="故障">
|
||||
<Tag
|
||||
color="error"
|
||||
style={{ borderRadius: '4px', padding: '4px 12px' }}
|
||||
icon={<ExclamationCircleOutlined />}
|
||||
>
|
||||
{faultCount}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ borderRadius: '4px', padding: '4px 12px', fontWeight: 500 }}
|
||||
>
|
||||
总计: {switchData.cables.length}
|
||||
</Tag>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Space size="small" onClick={e => e.stopPropagation()}>
|
||||
<Tooltip title="添加接线">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setEditingCable(null);
|
||||
form.setFieldsValue({ sourceDeviceId: switchId });
|
||||
setModalVisible(true);
|
||||
}}
|
||||
style={{ color: designTokens.colors.primary.main }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="删除设备">
|
||||
<Popconfirm
|
||||
title={`确定要删除交换机 ${switchDevice?.name} 吗?`}
|
||||
onConfirm={() => handleDeleteSwitch(switchId)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
<Space size="middle">
|
||||
<Tooltip title="正常连接">
|
||||
<Tag
|
||||
color="success"
|
||||
style={{ borderRadius: '4px', padding: '4px 12px' }}
|
||||
icon={<CheckCircleOutlined />}
|
||||
>
|
||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
{connectedCount}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
<Tooltip title="未连接">
|
||||
<Tag
|
||||
style={{ borderRadius: '4px', padding: '4px 12px' }}
|
||||
icon={<DisconnectOutlined />}
|
||||
>
|
||||
{disconnectedCount}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
{faultCount > 0 && (
|
||||
<Tooltip title="故障">
|
||||
<Tag
|
||||
color="error"
|
||||
style={{ borderRadius: '4px', padding: '4px 12px' }}
|
||||
icon={<ExclamationCircleOutlined />}
|
||||
>
|
||||
{faultCount}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tag
|
||||
color="blue"
|
||||
style={{ borderRadius: '4px', padding: '4px 12px', fontWeight: 500 }}
|
||||
>
|
||||
总计: {switchData.cables.length}
|
||||
</Tag>
|
||||
</Space>
|
||||
}
|
||||
style={{
|
||||
background: '#fff',
|
||||
borderRadius: designTokens.borderRadius.lg,
|
||||
marginBottom: '12px',
|
||||
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Space size="small" onClick={e => e.stopPropagation()}>
|
||||
<Tooltip title="添加接线">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setEditingCable(null);
|
||||
form.setFieldsValue({ sourceDeviceId: switchId });
|
||||
setModalVisible(true);
|
||||
}}
|
||||
style={{ color: designTokens.colors.primary.main }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
style={{
|
||||
background: '#fff',
|
||||
borderRadius: designTokens.borderRadius.lg,
|
||||
marginBottom: '12px',
|
||||
border: `1px solid ${designTokens.colors.neutral[200]}`,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
columns={portColumns}
|
||||
dataSource={switchPorts.map(port => ({
|
||||
...port,
|
||||
switchData: switchData,
|
||||
}))}
|
||||
rowKey="portId"
|
||||
pagination={false}
|
||||
size="middle"
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.md,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Table
|
||||
columns={portColumns}
|
||||
dataSource={switchPorts.map(port => ({
|
||||
...port,
|
||||
switchData: switchData,
|
||||
}))}
|
||||
rowKey="portId"
|
||||
pagination={false}
|
||||
size="middle"
|
||||
scroll={{ x: 1100 }}
|
||||
style={{
|
||||
borderRadius: designTokens.borderRadius.md,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</Panel>
|
||||
</motion.div>
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</Collapse>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -79,22 +79,6 @@ const PIE_CHART_COLORS = {
|
||||
primary: designTokens.colors.primary.main,
|
||||
};
|
||||
|
||||
const pieChartStyle = {
|
||||
width: '180px',
|
||||
height: '180px',
|
||||
borderRadius: '50%',
|
||||
background: `conic-gradient(
|
||||
${PIE_CHART_COLORS.success} 0deg 216deg,
|
||||
${PIE_CHART_COLORS.warning} 216deg 288deg,
|
||||
${PIE_CHART_COLORS.error} 288deg 324deg,
|
||||
${PIE_CHART_COLORS.primary} 324deg 360deg
|
||||
)`,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
const pieChartInner = {
|
||||
width: '120px',
|
||||
height: '120px',
|
||||
@@ -118,50 +102,122 @@ function Dashboard() {
|
||||
const [hoveredCard, setHoveredCard] = useState(null);
|
||||
const [animatedKey, setAnimatedKey] = useState(0);
|
||||
|
||||
const { data: devicesData, isLoading: devicesLoading, mutate: mutateDevices } = useFetch(
|
||||
'/devices?pageSize=1'
|
||||
);
|
||||
const { data: racksData, isLoading: racksLoading } = useFetch('/racks?pageSize=1');
|
||||
const { data: roomsData, isLoading: roomsLoading } = useFetch('/rooms');
|
||||
const { data: usersData, isLoading: usersLoading } = useFetch('/users?pageSize=1');
|
||||
const { data: ticketsData, isLoading: ticketsLoading } = useFetch('/tickets?pageSize=1&status=open');
|
||||
const { data: faultData, isLoading: faultLoading } = useFetch('/devices?status=fault&pageSize=1');
|
||||
const { data: statsData, isLoading: loading, mutate: mutateStats } = useFetch('/statistics');
|
||||
|
||||
const loading = devicesLoading || racksLoading || roomsLoading || usersLoading || ticketsLoading;
|
||||
const isRefreshing = devicesLoading && devicesData;
|
||||
const isRefreshing = loading && statsData;
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const totalDevices = devicesData?.total || 0;
|
||||
const totalRacks = racksData?.total || 0;
|
||||
const rooms = roomsData || [];
|
||||
const totalRooms = Array.isArray(rooms) ? rooms.length : 0;
|
||||
const totalUsers = usersData?.total || 0;
|
||||
const activeTickets = ticketsData?.total || 0;
|
||||
const faultDevices = faultData?.total || 0;
|
||||
if (!statsData) {
|
||||
return {
|
||||
totalDevices: 0,
|
||||
totalRacks: 0,
|
||||
totalRooms: 0,
|
||||
totalUsers: 0,
|
||||
activeTickets: 0,
|
||||
faultDevices: 0,
|
||||
deviceGrowth: 0,
|
||||
faultTrend: 0,
|
||||
userGrowth: 0,
|
||||
ticketTrend: 0,
|
||||
onlineRate: 100,
|
||||
powerUsage: 0,
|
||||
totalMaxPower: 10000,
|
||||
deviceStatus: {
|
||||
running: 0,
|
||||
maintenance: 0,
|
||||
fault: 0,
|
||||
offline: 0,
|
||||
},
|
||||
deviceTrendData: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
totalDevices,
|
||||
totalRacks,
|
||||
totalRooms,
|
||||
totalUsers,
|
||||
activeTickets,
|
||||
faultDevices,
|
||||
deviceGrowth: 2.5,
|
||||
faultTrend: -12.3,
|
||||
onlineRate:
|
||||
totalDevices > 0
|
||||
? (((totalDevices - faultDevices) / totalDevices) * 100).toFixed(1)
|
||||
: 100,
|
||||
powerUsage: Math.floor(Math.random() * 5000) + 2000,
|
||||
totalDevices: statsData.totalDevices || 0,
|
||||
totalRacks: statsData.totalRacks || 0,
|
||||
totalRooms: statsData.totalRooms || 0,
|
||||
totalUsers: statsData.totalUsers || 0,
|
||||
activeTickets: statsData.activeTickets || 0,
|
||||
faultDevices: statsData.faultDevices || 0,
|
||||
deviceGrowth: statsData.deviceGrowth || 0,
|
||||
faultTrend: statsData.faultTrend || 0,
|
||||
userGrowth: statsData.userGrowth || 0,
|
||||
ticketTrend: statsData.ticketTrend || 0,
|
||||
onlineRate: statsData.onlineRate || 100,
|
||||
powerUsage: statsData.powerUsage || 0,
|
||||
totalMaxPower: statsData.totalMaxPower || 10000,
|
||||
deviceStatus: statsData.deviceStatus || {
|
||||
running: 0,
|
||||
maintenance: 0,
|
||||
fault: 0,
|
||||
offline: 0,
|
||||
},
|
||||
deviceTrendData: statsData.deviceTrendData || [],
|
||||
};
|
||||
}, [devicesData, racksData, roomsData, usersData, ticketsData, faultData]);
|
||||
}, [statsData]);
|
||||
|
||||
const deviceStatusPercentages = useMemo(() => {
|
||||
const total = stats.totalDevices;
|
||||
if (total === 0) {
|
||||
return {
|
||||
running: 0,
|
||||
maintenance: 0,
|
||||
fault: 0,
|
||||
offline: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
running: Math.round((stats.deviceStatus.running / total) * 100),
|
||||
maintenance: Math.round((stats.deviceStatus.maintenance / total) * 100),
|
||||
fault: Math.round((stats.deviceStatus.fault / total) * 100),
|
||||
offline: Math.round((stats.deviceStatus.offline / total) * 100),
|
||||
};
|
||||
}, [stats.totalDevices, stats.deviceStatus]);
|
||||
|
||||
const pieChartStyle = useMemo(() => {
|
||||
const runningDeg = (deviceStatusPercentages.running / 100) * 360;
|
||||
const maintenanceDeg = (deviceStatusPercentages.maintenance / 100) * 360;
|
||||
const faultDeg = (deviceStatusPercentages.fault / 100) * 360;
|
||||
const offlineDeg = (deviceStatusPercentages.offline / 100) * 360;
|
||||
|
||||
let gradientParts = [];
|
||||
let currentAngle = 0;
|
||||
|
||||
if (runningDeg > 0) {
|
||||
gradientParts.push(`${PIE_CHART_COLORS.success} ${currentAngle}deg ${currentAngle + runningDeg}deg`);
|
||||
currentAngle += runningDeg;
|
||||
}
|
||||
if (maintenanceDeg > 0) {
|
||||
gradientParts.push(`${PIE_CHART_COLORS.warning} ${currentAngle}deg ${currentAngle + maintenanceDeg}deg`);
|
||||
currentAngle += maintenanceDeg;
|
||||
}
|
||||
if (faultDeg > 0) {
|
||||
gradientParts.push(`${PIE_CHART_COLORS.error} ${currentAngle}deg ${currentAngle + faultDeg}deg`);
|
||||
currentAngle += faultDeg;
|
||||
}
|
||||
if (offlineDeg > 0) {
|
||||
gradientParts.push(`${PIE_CHART_COLORS.primary} ${currentAngle}deg ${currentAngle + offlineDeg}deg`);
|
||||
}
|
||||
|
||||
const conicGradient = gradientParts.length > 0 ? `conic-gradient(${gradientParts.join(', ')})` : 'conic-gradient(#e5e7eb 0deg 360deg)';
|
||||
|
||||
return {
|
||||
width: '180px',
|
||||
height: '180px',
|
||||
borderRadius: '50%',
|
||||
background: conicGradient,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
}, [deviceStatusPercentages]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setAnimatedKey((prev) => prev + 1);
|
||||
await Promise.all([
|
||||
mutateDevices(),
|
||||
]);
|
||||
}, [mutateDevices]);
|
||||
await mutateStats();
|
||||
}, [mutateStats]);
|
||||
|
||||
const handleHover = useCallback((key) => {
|
||||
setHoveredCard(key);
|
||||
@@ -182,6 +238,7 @@ function Dashboard() {
|
||||
title: '总设备数',
|
||||
trend: stats.deviceGrowth,
|
||||
tagColor: 'blue',
|
||||
hideTrend: true,
|
||||
delay: 0,
|
||||
},
|
||||
{
|
||||
@@ -197,7 +254,7 @@ function Dashboard() {
|
||||
title: '总机柜数',
|
||||
trend: 0,
|
||||
tagColor: 'green',
|
||||
customStatus: true,
|
||||
hideTrend: true,
|
||||
delay: 1,
|
||||
},
|
||||
{
|
||||
@@ -213,7 +270,7 @@ function Dashboard() {
|
||||
title: '总机房数',
|
||||
trend: 0,
|
||||
tagColor: 'green',
|
||||
customStatus: true,
|
||||
hideTrend: true,
|
||||
delay: 2,
|
||||
},
|
||||
{
|
||||
@@ -229,6 +286,7 @@ function Dashboard() {
|
||||
title: '故障设备',
|
||||
trend: stats.faultTrend,
|
||||
tagColor: 'red',
|
||||
hideTrend: true,
|
||||
delay: 3,
|
||||
},
|
||||
{
|
||||
@@ -242,8 +300,9 @@ function Dashboard() {
|
||||
color: '#13c2c2',
|
||||
statKey: 'totalUsers',
|
||||
title: '用户总数',
|
||||
trend: 5.2,
|
||||
trend: stats.userGrowth,
|
||||
tagColor: 'cyan',
|
||||
hideTrend: true,
|
||||
delay: 4,
|
||||
},
|
||||
{
|
||||
@@ -257,25 +316,25 @@ function Dashboard() {
|
||||
color: '#fa8c16',
|
||||
statKey: 'activeTickets',
|
||||
title: '待处理工单',
|
||||
trend: -8.5,
|
||||
trend: stats.ticketTrend,
|
||||
tagColor: 'orange',
|
||||
hideTrend: true,
|
||||
delay: 5,
|
||||
},
|
||||
],
|
||||
[stats.deviceGrowth, stats.faultTrend]
|
||||
[stats.deviceGrowth, stats.faultTrend, stats.userGrowth, stats.ticketTrend]
|
||||
);
|
||||
|
||||
const deviceTrendData = useMemo(
|
||||
() => [
|
||||
{ label: '周一', value: 45, color: designTokens.colors.primary.main },
|
||||
{ label: '周二', value: 52, color: designTokens.colors.primary.main },
|
||||
{ label: '周三', value: 48, color: designTokens.colors.primary.main },
|
||||
{ label: '周四', value: 60, color: designTokens.colors.success.main },
|
||||
{ label: '周五', value: 55, color: designTokens.colors.success.main },
|
||||
{ label: '周六', value: 42, color: designTokens.colors.warning.main },
|
||||
{ label: '周日', value: 58, color: designTokens.colors.primary.main },
|
||||
],
|
||||
[]
|
||||
() => {
|
||||
const rawData = statsData?.deviceTrendData || [];
|
||||
return rawData.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value,
|
||||
color: designTokens.colors.primary.main
|
||||
}));
|
||||
},
|
||||
[statsData?.deviceTrendData]
|
||||
);
|
||||
|
||||
const styles = `
|
||||
@@ -358,7 +417,7 @@ function Dashboard() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<StatusLegend />
|
||||
<StatusLegend deviceStatusPercentages={deviceStatusPercentages} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -403,7 +462,7 @@ function Dashboard() {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PowerGauge value={stats.powerUsage} maxValue={10000} />
|
||||
<PowerGauge value={stats.powerUsage} maxValue={stats.totalMaxPower} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
@@ -485,7 +544,7 @@ function Dashboard() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<QuickStats onlineRate={stats.onlineRate} powerUsage={stats.powerUsage} />
|
||||
<QuickStats onlineRate={stats.onlineRate} powerUsage={stats.powerUsage} totalMaxPower={stats.totalMaxPower} />
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={8}>
|
||||
|
||||
@@ -204,7 +204,7 @@ function PortManagement() {
|
||||
const deviceId = port.deviceId;
|
||||
if (!grouped[deviceId]) {
|
||||
grouped[deviceId] = {
|
||||
device: devices.find(d => d.deviceId === deviceId),
|
||||
device: port.device || devices.find(d => d.deviceId === deviceId),
|
||||
ports: [],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user