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