feat(仪表盘): 添加统计API并优化仪表盘组件

This commit is contained in:
zhang1106
2026-03-15 12:19:13 +08:00
parent 7aa09ac9e1
commit a7f051172a
10 changed files with 696 additions and 298 deletions
+243 -39
View File
@@ -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` | 后端服务健康状态 |
### 通用响应格式
**成功响应:**
+149
View File
@@ -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;
+2
View File
@@ -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 -3
View File
@@ -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 = () => {
+154 -177
View File
@@ -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>
+128 -69
View File
@@ -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}>
+1 -1
View File
@@ -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: [],
};
}