diff --git a/README.md b/README.md index 1620854..590af69 100644 --- a/README.md +++ b/README.md @@ -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` | 后端服务健康状态 | + ### 通用响应格式 **成功响应:** diff --git a/backend/routes/statistics.js b/backend/routes/statistics.js new file mode 100644 index 0000000..4a64438 --- /dev/null +++ b/backend/routes/statistics.js @@ -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; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 9ff5682..299d924 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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')); diff --git a/frontend/src/components/dashboard/QuickStats.jsx b/frontend/src/components/dashboard/QuickStats.jsx index d87c207..089ce8f 100644 --- a/frontend/src/components/dashboard/QuickStats.jsx +++ b/frontend/src/components/dashboard/QuickStats.jsx @@ -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, }, ]; diff --git a/frontend/src/components/dashboard/StatCard.jsx b/frontend/src/components/dashboard/StatCard.jsx index 86bd4ba..ded936b 100644 --- a/frontend/src/components/dashboard/StatCard.jsx +++ b/frontend/src/components/dashboard/StatCard.jsx @@ -46,6 +46,7 @@ const StatCard = ({ trend, tagColor, customStatus, + hideTrend, xs, sm, lg, @@ -135,7 +136,7 @@ const StatCard = ({ /> {statKey === 'totalRacks' ? '正常运行中' : '全部在线'} - ) : ( + ) : !hideTrend ? (
- )} + ) : null} diff --git a/frontend/src/components/dashboard/StatusLegend.jsx b/frontend/src/components/dashboard/StatusLegend.jsx index d1c0011..c4b1cd0 100644 --- a/frontend/src/components/dashboard/StatusLegend.jsx +++ b/frontend/src/components/dashboard/StatusLegend.jsx @@ -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 ( diff --git a/frontend/src/hooks/useSWR.js b/frontend/src/hooks/useSWR.js index df5d2b4..d998986 100644 --- a/frontend/src/hooks/useSWR.js +++ b/frontend/src/hooks/useSWR.js @@ -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 = () => { diff --git a/frontend/src/pages/CableManagement.jsx b/frontend/src/pages/CableManagement.jsx index 7c10576..ca7082d 100644 --- a/frontend/src/pages/CableManagement.jsx +++ b/frontend/src/pages/CableManagement.jsx @@ -317,21 +317,6 @@ function CableManagement() { } }; - const handleDeleteSwitch = async switchId => { - try { - await axios.delete(`/api/devices/${switchId}`); - message.success({ - content: '删除设备成功', - icon: , - }); - 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 -; return (
@@ -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 -; 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 -; return cable.cableLength ? {cable.cableLength}m : -; }, @@ -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 -; + } return ( - {cable && ( - <> - -
+ } + extra={ + e.stopPropagation()}> + +