From f2327ea3f1e47e4c4dd8fff5efb788ebef6e6bdf Mon Sep 17 00:00:00 2001 From: zhang1106 <849185023@qq.com> Date: Fri, 26 Dec 2025 11:03:47 +0800 Subject: [PATCH] =?UTF-8?q?perf(frontend):=20=E4=BC=98=E5=8C=96=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E6=80=A7=E8=83=BD=E4=B8=8E=E4=BB=A3=E7=A0=81=E7=BB=93?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(components): 使用React.memo和useCallback优化组件渲染 feat(cache): 添加API缓存管理功能 perf(build): 配置Vite构建优化选项 style: 提取工具函数减少重复代码 chore: 添加懒加载和Suspense支持 --- DEPLOYMENT.md | 904 ++++++++++++++++++++ frontend/src/App.jsx | 141 +-- frontend/src/api/cache.js | 286 +++++++ frontend/src/pages/ConsumableManagement.jsx | 50 +- frontend/src/pages/Dashboard.jsx | 759 ++++++++-------- frontend/src/pages/DeviceManagement.jsx | 81 +- frontend/src/pages/RackManagement.jsx | 63 +- frontend/src/pages/RackVisualization.jsx | 460 +++++----- frontend/src/pages/TicketManagement.jsx | 146 ++-- frontend/src/pages/TicketStatistics.jsx | 126 ++- frontend/vite.config.js | 26 +- 11 files changed, 2147 insertions(+), 895 deletions(-) create mode 100644 frontend/src/api/cache.js diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 34a658f..936b0ed 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -840,6 +840,910 @@ SESSION_SECRET=random_session_secret_key_here --- +## 更新升级流程 + +### 手动更新步骤 + +#### 1. 备份当前版本 + +```bash +# 创建备份目录 +mkdir -p /var/backups/idc_assest/$(date +%Y%m%d) +cd /var/backups/idc_assest/$(date +%Y%m%d) + +# 备份数据库 +mysqldump -u idc_prod_user -p idc_management > database_backup.sql + +# 备份配置文件 +cp -r /var/www/idc_assest/backend/.env ./ +cp -r /var/www/idc_assest/frontend/.env ./ + +# 备份上传文件 +cp -r /var/www/idc_assest/backend/uploads ./ +``` + +#### 2. 下载最新代码 + +```bash +cd /var/www/idc_assest + +# 拉取最新代码 +git fetch origin +git checkout main +git pull origin main +``` + +#### 3. 更新依赖 + +```bash +# 更新后端依赖 +cd backend +npm install +cd .. + +# 更新前端依赖并构建 +cd frontend +npm install +npm run build +cd .. +``` + +#### 4. 重启服务 + +```bash +# 重启后端 +pm2 restart idc-backend + +# 重启Nginx +sudo systemctl restart nginx + +# 验证服务 +curl http://localhost:8000/health +``` + +### Docker环境更新 + +```bash +cd /var/www/idc_assest/docker + +# 拉取最新代码 +cd .. +git pull origin main +cd docker + +# 重新构建并启动 +docker-compose down +docker-compose up -d --build + +# 验证服务 +curl http://localhost/health +``` + +### 回滚操作 + +```bash +# 查看历史版本 +cd /var/www/idc_assest +git log --oneline -10 + +# 回滚到指定版本 +git checkout + +# 重新构建 +cd frontend && npm run build && cd .. +cd backend && npm install && cd .. + +# 重启服务 +pm2 restart idc-backend +``` + +### 版本兼容性检查 + +```bash +# 检查Node.js版本 +node --version + +# 检查依赖版本 +cd backend && npm list | grep -E "(express|sequelize|mysql2)" && cd .. +cd frontend && npm list | grep -E "(react|antd|vite)" && cd .. +``` + +--- + +## 📊 系统架构说明 + +### 整体架构图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 用户访问层 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 浏览器 │ │ 移动端 │ │ API客户端 │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +└─────────┼──────────────────┼──────────────────┼───────────────┘ + │ │ │ + └──────────────────┼──────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Web服务层 │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Nginx / Apache │ │ +│ │ • 静态资源服务 • 反向代理 • SSL终端 • 负载均衡 │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────┬───────────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 前端应用 │ │ 后端API │ │ 静态资源 │ +│ (React) │ │ (Express) │ │ (Nginx) │ +│ 端口: 3000 │ │ 端口: 8000 │ │ 端口: 80/443 │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ 数据存储层 │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ MySQL │ │ SQLite │ │ 文件存储 │ │ +│ │ 端口: 3306 │ │ 嵌入式 │ │ uploads/ │ │ +│ │ │ │ │ │ │ │ +│ │ • 设备信息 │ │ • 开发环境 │ │ • 设备图片 │ │ +│ │ • 用户数据 │ │ • 快速部署 │ │ • 附件 │ │ +│ │ • 工单记录 │ │ │ │ • 备份文件 │ │ +│ │ • 耗材库存 │ │ │ │ │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 技术栈版本 + +| 层级 | 技术 | 版本要求 | 用途 | +|------|------|----------|------| +| 前端 | React | 18.2.0+ | UI框架 | +| 前端 | Ant Design | 5.8.6+ | 组件库 | +| 前端 | Vite | 4.4.9+ | 构建工具 | +| 前端 | Three.js | 0.160.0+ | 3D可视化 | +| 后端 | Node.js | 14.0.0+ | 运行时 | +| 后端 | Express | 4.18.2+ | Web框架 | +| 后端 | Sequelize | 6.32.1+ | ORM框架 | +| 数据库 | MySQL | 8.0+ | 主数据库 | +| 数据库 | SQLite | 5.1.6+ | 嵌入式数据库 | +| 服务器 | Nginx | 1.18+ | 反向代理 | +| 进程管理 | PM2 | 5.0+ | 进程管理 | + +### 数据流说明 + +1. **用户请求流程** + - 用户通过浏览器访问系统 + - 请求首先到达Nginx + - Nginx判断请求类型: + - 静态资源:直接返回 + - API请求:转发到后端服务 + - 前端路由:返回index.html + +2. **数据处理流程** + - 后端接收API请求 + - 验证用户身份和权限 + - 通过Sequelize操作数据库 + - 返回JSON响应 + +3. **实时通信** + - WebSocket用于实时告警推送 + - HTTP轮询用于数据刷新 + +--- + +## 🔍 故障排查指南 + +### 常见错误及解决方案 + +#### 1. 后端服务无法启动 + +**错误信息**:Error: listen EADDRINUSE: address already in use :::8000 + +**原因分析**:端口8000已被其他进程占用 + +**解决方案**: +```bash +# 查看占用端口的进程 +netstat -tulpn | grep :8000 +lsof -i :8000 + +# 终止占用进程 +kill -9 + +# 或修改为其他端口 +PORT=8001 npm run dev +``` + +#### 2. 数据库连接失败 + +**错误信息**:SequelizeConnectionError: Access denied for user + +**原因分析**:数据库用户名或密码错误 + +**解决方案**: +```bash +# 检查.env配置 +cat backend/.env | grep -E "(MYSQL|USERNAME|PASSWORD)" + +# 测试数据库连接 +mysql -u idc_user -p -h localhost + +# 检查MySQL服务状态 +sudo systemctl status mysql +sudo systemctl start mysql +``` + +#### 3. 前端构建失败 + +**错误信息**:Error: Cannot find module 'node-sass' + +**原因分析**:依赖安装不完整 + +**解决方案**: +```bash +# 清理并重新安装依赖 +cd frontend +rm -rf node_modules package-lock.json +npm install + +# 检查Node.js版本兼容性 +node --version +``` + +#### 4. Nginx 502 Bad Gateway + +**原因分析**:后端服务未运行或连接超时 + +**解决方案**: +```bash +# 检查后端服务状态 +pm2 status + +# 查看后端日志 +pm2 logs idc-backend + +# 检查Nginx错误日志 +tail -f /var/log/nginx/idc_assest-error.log + +# 测试后端服务 +curl http://127.0.0.1:8000/health +``` + +#### 5. 文件上传失败 + +**错误信息**:Error: ENOENT: no such file or directory + +**原因分析**:上传目录不存在或权限不足 + +**解决方案**: +```bash +# 创建上传目录 +mkdir -p backend/uploads +chmod 755 backend/uploads + +# 检查目录权限 +ls -la backend/ | grep uploads +``` + +#### 6. CORS跨域错误 + +**错误信息**:Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy + +**原因分析**:CORS配置不正确 + +**解决方案**: +```javascript +// 检查backend/server.js中的CORS配置 +const cors = require('cors'); +app.use(cors({ + origin: 'http://your-domain.com', + credentials: true +})); +``` + +### 诊断命令速查表 + +```bash +# 检查端口占用 +netstat -tulpn | grep -E "(80|443|8000|3306)" + +# 检查进程状态 +ps aux | grep -E "(node|nginx|mysql)" + +# 检查磁盘空间 +df -h + +# 检查内存使用 +free -m + +# 检查系统负载 +top -bn1 | head -5 + +# 网络连通性测试 +curl -I http://localhost:8000/health +curl -I http://localhost/api/devices + +# 查看系统日志 +tail -f /var/log/syslog +journalctl -xe + +# Docker诊断(Docker部署) +docker-compose ps +docker-compose logs --tail=100 +docker stats +``` + +### 日志文件位置 + +| 服务 | 日志位置 | +|------|----------| +| 后端(PM2) | `pm2 logs idc-backend` | +| 后端(文件) | `/var/log/idc_assest/backend/` | +| Nginx | `/var/log/nginx/idc_assest-access.log` | +| Nginx | `/var/log/nginx/idc_assest-error.log` | +| MySQL | `/var/log/mysql/error.log` | +| Docker | `docker-compose logs` | + +--- + +## ⚡ 性能优化 + +### 后端性能优化 + +#### 1. PM2集群模式 + +```javascript +// ecosystem.config.js +module.exports = { + apps: [{ + name: 'idc-backend', + script: 'server.js', + instances: 'max', // 使用所有CPU核心 + exec_mode: 'cluster', // 集群模式 + env: { + NODE_ENV: 'production', + PORT: 8000 + }, + max_memory_restart: '1G', // 内存超过1G自动重启 + node_args: '--max-old-space-size=1024', + listen_timeout: 3000, // 监听超时 + kill_timeout: 5000, // 终止超时 + max_restarts: 10, // 最大重启次数 + min_uptime: '10s' // 最小运行时间 + }] +}; +``` + +#### 2. 数据库连接池优化 + +```javascript +// backend/db.js +const sequelize = new Sequelize({ + dialect: 'mysql', + host: process.env.MYSQL_HOST, + port: process.env.MYSQL_PORT, + username: process.env.MYSQL_USERNAME, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + pool: { + max: 20, // 最大连接数 + min: 5, // 最小连接数 + acquire: 60000, // 获取连接最大等待时间 + idle: 10000 // 连接空闲最大时间 + }, + logging: false, // 关闭SQL日志 + dialectOptions: { + charset: 'utf8mb4' + } +}); +``` + +#### 3. 缓存策略 + +```javascript +// 使用内存缓存热点数据 +const cache = new Map(); + +// 设备统计缓存(5分钟过期) +function getDeviceStats() { + const cacheKey = 'device_stats'; + const cached = cache.get(cacheKey); + + if (cached && Date.now() - cached.time < 5 * 60 * 1000) { + return cached.data; + } + + const stats = calculateDeviceStats(); + cache.set(cacheKey, { data: stats, time: Date.now() }); + return stats; +} +``` + +### 数据库性能优化 + +#### 1. 创建索引 + +```sql +-- 设备表索引 +CREATE INDEX idx_device_rack ON devices(rackId); +CREATE INDEX idx_device_type ON devices(deviceType); +CREATE INDEX idx_device_status ON devices(status); +CREATE INDEX idx_device_created ON devices(createdAt); + +-- 工单表索引 +CREATE INDEX idx_ticket_status ON tickets(status); +CREATE INDEX idx_ticket_priority ON tickets(priority); +CREATE INDEX idx_ticket_device ON tickets(deviceId); +CREATE INDEX idx_ticket_created ON tickets(createdAt); + +-- 耗材表索引 +CREATE INDEX idx_consumable_category ON consumables(category); +CREATE INDEX idx_consumable_status ON consumables(status); +``` + +#### 2. MySQL配置优化 + +```ini +# /etc/mysql/mysql.conf.d/mysqld.cnf + +[mysqld] +# 缓冲池大小(建议为物理内存的70%) +innodb_buffer_pool_size = 2G + +# 日志文件大小 +innodb_log_file_size = 512M + +# 刷新策略 +innodb_flush_log_at_trx_commit = 2 +innodb_flush_method = O_DIRECT + +# 连接数 +max_connections = 200 + +# 查询缓存(MySQL 8.0已移除) +# query_cache_type = 0 +``` + +### 前端性能优化 + +#### 1. 构建优化 + +```javascript +// vite.config.js +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + // 开启压缩 + minify: 'terser', + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true + } + }, + // 代码分割 + rollupOptions: { + output: { + manualChunks: { + 'antd': ['antd', '@ant-design/icons'], + 'charts': ['recharts'], + 'three': ['three'] + } + } + }, + // 资源优化 + assetsDir: 'assets', + chunkSizeWarningLimit: 1000 + }, + // 依赖预构建 + optimizeDeps: { + include: ['antd', 'axios', 'react-router-dom'] + } +}); +``` + +#### 2. 路由懒加载 + +```javascript +// App.jsx +import { lazy, Suspense } from 'react'; + +const Dashboard = lazy(() => import('./pages/Dashboard')); +const DeviceManagement = lazy(() => import('./pages/DeviceManagement')); + +// 使用 +}> + + } /> + + +``` + +### Nginx性能优化 + +```nginx +# /etc/nginx/nginx.conf + +worker_processes auto; +worker_rlimit_nofile 65535; + +events { + worker_connections 2048; + use epoll; + multi_accept on; +} + +http { + # 打开文件缓存 + open_file_cache max=10000 inactive=20s; + open_file_cache_valid 30s; + open_file_cache_min_uses 2; + + # Gzip压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + + # 缓冲区优化 + client_body_buffer_size 16K; + client_max_body_size 100M; + proxy_buffer_size 128K; + proxy_buffers 4 256K; + proxy_busy_buffers_size 256K; + + # 连接超时 + keepalive_timeout 65; + keepalive_requests 100; + + # 上游服务器配置 + upstream backend { + server 127.0.0.1:8000; + keepalive 32; + } +} +``` + +--- + +## 📝 日志管理 + +### 日志配置 + +```javascript +// backend/logger.js +const winston = require('winston'); +const path = require('path'); + +const logDir = process.env.LOG_DIR || './logs'; + +const logger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: winston.format.combine( + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss' + }), + winston.format.errors({ stack: true }), + winston.format.json() + ), + defaultMeta: { service: 'idc-backend' }, + transports: [ + // 错误日志 + new winston.transports.File({ + filename: path.join(logDir, 'error.log'), + level: 'error', + maxsize: 10485760, // 10MB + maxFiles: 10 + }), + // 组合日志 + new winston.transports.File({ + filename: path.join(logDir, 'combined.log'), + maxsize: 10485760, + maxFiles: 10 + }), + // 控制台输出 + new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ) + }) + ] +}); + +module.exports = logger; +``` + +### 日志轮转配置 + +#### 1. 使用logrotate(Linux) + +```bash +# /etc/logrotate.d/idc_assest +/var/log/idc_assest/*.log { + daily + missingok + rotate 14 + compress + delaycompress + notifempty + create 0640 www-data www-data + sharedscripts + postrotate + pm2 restart idc-backend > /dev/null 2>&1 || true + endscript +} +``` + +#### 2. 使用PM2日志轮转 + +```bash +# 安装pm2-logrotate +pm2 install pm2-logrotate + +# 配置 +pm2 set pm2-logrotate:max_size 50M # 单个文件最大50MB +pm2 set pm2-logrotate:retain 30 # 保留30个文件 +pm2 set pm2-logrotate:compress true # 压缩历史文件 +pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss +``` + +### 日志分析示例 + +```bash +# 查看错误日志 +tail -f /var/log/idc_assest/error.log + +# 统计API响应时间 +grep -o '"duration":[0-9]*' /var/log/idc_assest/combined.log | \ + awk -F: '{sum+=$2; count++} END {print "平均响应时间:", sum/count, "ms"}' + +# 统计用户登录情况 +grep "登录成功" /var/log/idc_assest/combined.log | \ + awk '{print $4}' | sort | uniq -c | sort -rn + +# 查找异常请求 +grep -E "(ERROR|500|401)" /var/log/idc_assest/error.log +``` + +--- + +## 💾 备份与灾难恢复 + +### 备份策略 + +| 备份类型 | 频率 | 保留时间 | 说明 | +|----------|------|----------|------| +| 全量备份 | 每周日凌晨3点 | 4周 | 完整数据库备份 | +| 增量备份 | 每天凌晨2点 | 7天 | 每日变更数据 | +| 实时备份 | 持续 | 永久 | 二进制日志 | +| 配置备份 | 每次变更 | 12个月 | 配置文件和代码 | + +### 自动化备份脚本 + +```bash +#!/bin/bash +# backup_full.sh - 完整备份脚本 + +set -e + +# 配置 +BACKUP_DIR="/var/backups/idc_assest" +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/full_backup_$DATE" +KEEP_DAYS=30 + +# 创建备份目录 +mkdir -p "$BACKUP_DIR" + +# 1. 备份数据库 +echo "正在备份数据库..." +mysqldump -u idc_prod_user -p"$MYSQL_PASSWORD" \ + --single-transaction \ + --routines \ + --triggers \ + --events \ + idc_management | gzip > "$BACKUP_FILE.sql.gz" + +# 2. 备份配置文件 +echo "正在备份配置文件..." +tar czf "$BACKUP_DIR/config_$DATE.tar.gz" \ + backend/.env \ + nginx/conf.d/ + +# 3. 备份上传文件 +echo "正在备份上传文件..." +tar czf "$BACKUP_DIR/uploads_$DATE.tar.gz" \ + backend/uploads/ + +# 4. 备份代码(排除node_modules) +echo "正在备份代码..." +tar czf "$BACKUP_DIR/code_$DATE.tar.gz" \ + --exclude=node_modules \ + --exclude=dist \ + --exclude=uploads \ + . + +# 5. 清理旧备份 +echo "正在清理旧备份..." +find "$BACKUP_DIR" -name "full_backup_*.sql.gz" -mtime +$KEEP_DAYS -delete +find "$BACKUP_DIR" -name "*.tar.gz" -mtime +$KEEP_DAYS -delete + +# 6. 验证备份 +echo "正在验证备份..." +if [ -f "$BACKUP_FILE.sql.gz" ]; then + gunzip -t "$BACKUP_FILE.sql.gz" && echo "数据库备份验证成功" +fi + +# 7. 生成备份清单 +echo "备份完成,文件列表:" +ls -lh "$BACKUP_DIR"/*"$DATE"* + +# 8. 发送通知(可选) +# curl -X POST "https://hooks.example.com/notify" -d "backup completed" + +echo "备份任务完成:$DATE" +``` + +### 定时任务配置 + +```bash +# crontab配置 +crontab -e + +# 每日增量备份(凌晨2点) +0 2 * * * /var/www/idc_assest/scripts/backup_incremental.sh + +# 每周完整备份(周日凌晨3点) +0 3 * * 0 /var/www/idc_assest/scripts/backup_full.sh + +# 每月清理旧备份(每月1日凌晨4点) +0 4 1 * * /var/www/idc_assest/scripts/cleanup_old_backups.sh +``` + +### 灾难恢复流程 + +#### 1. 数据恢复步骤 + +```bash +# 1. 停止服务 +pm2 stop idc-backend + +# 2. 恢复数据库 +gunzip -c /var/backups/idc_assest/full_backup_20240101_030000.sql.gz | \ + mysql -u idc_prod_user -p idc_management + +# 3. 恢复配置文件 +tar xzf /var/backups/idc_assest/config_20240101.tar.gz -C / + +# 4. 恢复上传文件 +tar xzf /var/backups/idc_assest/uploads_20240101.tar.gz -C / + +# 5. 重启服务 +pm2 restart idc-backend + +# 6. 验证恢复 +curl http://localhost:8000/health +``` + +#### 2. 完整系统恢复 + +```bash +# 1. 创建新服务器 +# 2. 安装必要软件 +# 3. 从Git克隆代码 +# 4. 恢复配置文件 +# 5. 恢复数据库 +# 6. 恢复上传文件 +# 7. 重新安装依赖 +# 8. 重启服务 +``` + +### 备份验证 + +```bash +#!/bin/bash +# verify_backup.sh - 备份验证脚本 + +BACKUP_FILE="$1" + +if [ -z "$BACKUP_FILE" ]; then + echo "用法: $0 <备份文件>" + exit 1 +fi + +echo "正在验证备份文件:$BACKUP_FILE" + +# 检查文件存在 +if [ ! -f "$BACKUP_FILE" ]; then + echo "错误:文件不存在" + exit 1 +fi + +# 检查文件大小(至少1KB) +FILE_SIZE=$(stat -f%z "$BACKUP_FILE" 2>/dev/null || stat -c%s "$BACKUP_FILE") +if [ "$FILE_SIZE" -lt 1024 ]; then + echo "警告:文件大小异常小" +fi + +# 对于SQL备份,验证SQL语法 +if [[ "$BACKUP_FILE" == *.sql.gz ]]; then + echo "验证SQL语法..." + gunzip -c "$BACKUP_FILE" | head -100 | grep -q "INSERT INTO\|CREATE TABLE" + if [ $? -eq 0 ]; then + echo "✓ SQL语法验证通过" + else + echo "✗ SQL语法验证失败" + exit 1 + fi +fi + +# 对于压缩包,验证完整性 +if [[ "$BACKUP_FILE" == *.tar.gz ]]; then + echo "验证压缩包完整性..." + tar -tzf "$BACKUP_FILE" > /dev/null 2>&1 + if [ $? -eq 0 ]; then + echo "✓ 压缩包验证通过" + else + echo "✗ 压缩包验证失败" + exit 1 + fi +fi + +echo "备份验证完成" +``` + +--- + +## 🔐 安全加固清单 + +### 服务器安全 + +- [ ] 配置防火墙规则(仅开放必要端口) +- [ ] 启用SSH密钥认证,禁用密码登录 +- [ ] 安装配置fail2ban防止暴力破解 +- [ ] 定期更新系统安全补丁 +- [ ] 配置自动安全更新 +- [ ] 启用系统审计日志 +- [ ] 限制root用户登录 + +### 数据库安全 + +- [ ] 使用强密码策略 +- [ ] 创建专用数据库用户,禁用root远程登录 +- [ ] 定期备份数据库 +- [ ] 启用数据库审计日志 +- [ ] 限制数据库用户权限(最小权限原则) +- [ ] 加密数据库连接(SSL/TLS) + +### 应用安全 + +- [ ] 配置HTTPS强制跳转 +- [ ] 设置安全的Cookie属性(HttpOnly, Secure) +- [ ] 启用CSRF防护 +- [ ] 实现请求速率限制 +- [ ] 配置安全的HTTP头 +- [ ] 敏感信息加密存储 +- [ ] 实现完善的权限控制 + +### 监控与告警 + +- [ ] 配置异常登录告警 +- [ ] 启用API访问日志 +- [ ] 监控服务状态和资源使用 +- [ ] 配置磁盘空间告警 +- [ ] 设置数据库连接数告警 +- [ ] 实现自动化健康检查 + +--- + **🎉 部署完成后,您就可以开始使用IDC设备管理系统了!** --- diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7f7432d..d78fbdd 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,27 +1,28 @@ -import React, { useState } from 'react'; +import React, { useState, Suspense, lazy } from 'react'; import { Layout, Menu, theme, Button, Dropdown, Avatar, message, Space, Divider } from 'antd'; -import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined, UserOutlined, LogoutOutlined, UserOutlined as UserIcon, HistoryOutlined, AuditOutlined, ToolOutlined, ScheduleOutlined } from '@ant-design/icons'; +import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined, UserOutlined, LogoutOutlined, HistoryOutlined, AuditOutlined, ToolOutlined, ScheduleOutlined } from '@ant-design/icons'; import { BrowserRouter as Router, Routes, Route, Link, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { useAuth } from './context/AuthContext'; -import Dashboard from './pages/Dashboard'; -import DeviceManagement from './pages/DeviceManagement'; -import RackManagement from './pages/RackManagement'; -import RoomManagement from './pages/RoomManagement'; -import DeviceFieldManagement from './pages/DeviceFieldManagement'; -import RackVisualization from './pages/RackVisualization'; -import ConsumableManagement from './pages/ConsumableManagement'; -import ConsumableStatistics from './pages/ConsumableStatistics'; -import ConsumableLogs from './pages/ConsumableLogs'; -import CategoryManagement from './pages/CategoryManagement'; -import UserManagement from './pages/UserManagement'; -import LoginHistory from './pages/LoginHistory'; -import OperationLogs from './pages/OperationLogs'; -import Login from './pages/Login'; -import TicketManagement from './pages/TicketManagement'; -import TicketCategoryManagement from './pages/TicketCategoryManagement'; -import TicketStatistics from './pages/TicketStatistics'; import { Spin } from 'antd'; +const Dashboard = lazy(() => import('./pages/Dashboard')); +const DeviceManagement = lazy(() => import('./pages/DeviceManagement')); +const RackManagement = lazy(() => import('./pages/RackManagement')); +const RoomManagement = lazy(() => import('./pages/RoomManagement')); +const DeviceFieldManagement = lazy(() => import('./pages/DeviceFieldManagement')); +const RackVisualization = lazy(() => import('./pages/RackVisualization')); +const ConsumableManagement = lazy(() => import('./pages/ConsumableManagement')); +const ConsumableStatistics = lazy(() => import('./pages/ConsumableStatistics')); +const ConsumableLogs = lazy(() => import('./pages/ConsumableLogs')); +const CategoryManagement = lazy(() => import('./pages/CategoryManagement')); +const UserManagement = lazy(() => import('./pages/UserManagement')); +const LoginHistory = lazy(() => import('./pages/LoginHistory')); +const OperationLogs = lazy(() => import('./pages/OperationLogs')); +const Login = lazy(() => import('./pages/Login')); +const TicketManagement = lazy(() => import('./pages/TicketManagement')); +const TicketCategoryManagement = lazy(() => import('./pages/TicketCategoryManagement')); +const TicketStatistics = lazy(() => import('./pages/TicketStatistics')); + const { Header, Content, Sider } = Layout; const PrivateRoute = ({ children }) => { @@ -46,7 +47,25 @@ const PrivateRoute = ({ children }) => { return ; } - return children; + return ( + + + + } + > + + {children} + + + ); }; const AppLayout = ({ children }) => { @@ -285,14 +304,28 @@ function App() { return ( - } /> + + + + } + > + + + } /> - - - + } /> @@ -300,9 +333,7 @@ function App() { path="/devices" element={ - - - + } /> @@ -310,9 +341,7 @@ function App() { path="/racks" element={ - - - + } /> @@ -320,9 +349,7 @@ function App() { path="/rooms" element={ - - - + } /> @@ -330,9 +357,7 @@ function App() { path="/fields" element={ - - - + } /> @@ -340,9 +365,7 @@ function App() { path="/visualization" element={ - - - + } /> @@ -350,9 +373,7 @@ function App() { path="/consumables" element={ - - - + } /> @@ -360,9 +381,7 @@ function App() { path="/consumables-categories" element={ - - - + } /> @@ -370,9 +389,7 @@ function App() { path="/consumables-stats" element={ - - - + } /> @@ -380,9 +397,7 @@ function App() { path="/consumables-logs" element={ - - - + } /> @@ -390,9 +405,7 @@ function App() { path="/users" element={ - - - + } /> @@ -400,9 +413,7 @@ function App() { path="/login-history" element={ - - - + } /> @@ -410,9 +421,7 @@ function App() { path="/operation-logs" element={ - - - + } /> @@ -420,9 +429,7 @@ function App() { path="/tickets" element={ - - - + } /> @@ -430,9 +437,7 @@ function App() { path="/ticket-categories" element={ - - - + } /> @@ -440,9 +445,7 @@ function App() { path="/ticket-statistics" element={ - - - + } /> diff --git a/frontend/src/api/cache.js b/frontend/src/api/cache.js new file mode 100644 index 0000000..28537bf --- /dev/null +++ b/frontend/src/api/cache.js @@ -0,0 +1,286 @@ +const cacheManager = (() => { + const cache = new Map(); + const cacheTimestamps = new Map(); + const defaultTTL = 5 * 60 * 1000; + const config = new Map(); + + const generateKey = (method, url, params) => { + const paramsStr = params ? JSON.stringify(params, Object.keys(params).sort()) : ''; + return `${method}:${url}:${paramsStr}`; + }; + + const isExpired = (key) => { + const timestamp = cacheTimestamps.get(key); + if (!timestamp) return true; + const ttl = config.get(key)?.ttl || defaultTTL; + return Date.now() - timestamp > ttl; + }; + + const get = (method, url, params) => { + const key = generateKey(method, url, params); + if (isExpired(key)) { + cache.delete(key); + cacheTimestamps.delete(key); + return null; + } + return cache.get(key); + }; + + const set = (method, url, params, data, ttl) => { + const key = generateKey(method, url, params); + cache.set(key, data); + cacheTimestamps.set(key, Date.now()); + config.set(key, { ttl }); + return key; + }; + + const invalidate = (url) => { + const keysToDelete = []; + cache.forEach((_, key) => { + if (key.includes(url)) { + keysToDelete.push(key); + } + }); + keysToDelete.forEach(key => { + cache.delete(key); + cacheTimestamps.delete(key); + config.delete(key); + }); + return keysToDelete.length; + }; + + const invalidatePattern = (pattern) => { + const regex = new RegExp(pattern); + const keysToDelete = []; + cache.forEach((_, key) => { + if (regex.test(key)) { + keysToDelete.push(key); + } + }); + keysToDelete.forEach(key => { + cache.delete(key); + cacheTimestamps.delete(key); + config.delete(key); + }); + return keysToDelete.length; + }; + + const clear = () => { + cache.clear(); + cacheTimestamps.clear(); + config.clear(); + }; + + const setTTL = (url, ttl) => { + config.set(url, { ttl }); + }; + + const getStats = () => { + return { + size: cache.size, + keys: Array.from(cache.keys()) + }; + }; + + return { + get, + set, + invalidate, + invalidatePattern, + clear, + setTTL, + getStats, + defaultTTL + }; +})(); + +const cacheInterceptor = (api) => { + const requestCache = new Set(); + const pendingRequests = new Map(); + + api.interceptors.request.use( + (config) => { + if (config.method?.toLowerCase() === 'get') { + const cacheKey = cacheManager.generateKey( + config.method, + config.url, + config.params + ); + + if (requestCache.has(cacheKey)) { + config.adapter = () => { + const cachedData = cacheManager.get( + config.method, + config.url, + config.params + ); + if (cachedData) { + return Promise.resolve({ + data: cachedData, + status: 200, + statusText: 'OK', + headers: {}, + config + }); + } + requestCache.delete(cacheKey); + return api.request(config); + }; + } + } + return config; + }, + (error) => Promise.reject(error) + ); + + api.interceptors.response.use( + (response) => { + if (response.config.method?.toLowerCase() === 'get') { + const cacheKey = cacheManager.generateKey( + response.config.method, + response.config.url, + response.config.params + ); + cacheManager.set( + response.config.method, + response.config.url, + response.config.params, + response.data + ); + requestCache.add(cacheKey); + } + return response; + }, + (error) => { + if (error.config) { + const cacheKey = cacheManager.generateKey( + error.config.method, + error.config.url, + error.config.params + ); + requestCache.delete(cacheKey); + } + return Promise.reject(error); + } + ); +}; + +export const cachedAPI = { + get: async (url, params = {}, ttl) => { + const method = 'get'; + const cached = cacheManager.get(method, url, params); + if (cached) { + return cached; + } + return api.get(url, { params }).then(data => { + cacheManager.set(method, url, params, data, ttl); + return data; + }); + }, + + post: (url, data) => api.post(url, data).then(data => { + cacheManager.invalidate(url); + return data; + }), + + put: (url, data) => api.put(url, data).then(data => { + cacheManager.invalidate(url); + return data; + }), + + delete: (url) => api.delete(url).then(data => { + cacheManager.invalidate(url); + return data; + }), + + invalidate: (url) => cacheManager.invalidate(url), + + invalidatePattern: (pattern) => cacheManager.invalidatePattern(pattern), + + clearCache: () => cacheManager.clear(), + + setCacheTTL: (url, ttl) => cacheManager.setTTL(url, ttl), + + getCacheStats: () => cacheManager.getStats() +}; + +export const deviceAPI = { + list: (params) => cachedAPI.get('/devices', params), + get: (deviceId) => cachedAPI.get(`/devices/${deviceId}`), + create: (data) => cachedAPI.post('/devices', data), + update: (deviceId, data) => cachedAPI.put(`/api/devices/${deviceId}`, data), + delete: (deviceId) => cachedAPI.delete(`/api/devices/${deviceId}`), + batchOffline: (data) => cachedAPI.post('/devices/batch-offline', data), + batchDelete: (data) => cachedAPI.delete('/devices/batch-delete', { data }), + export: (params) => api.get('/devices/export', { params, responseType: 'blob' }), + import: (formData) => api.post('/devices/import', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) +}; + +export const rackAPI = { + list: (params) => cachedAPI.get('/racks', params), + get: (rackId) => cachedAPI.get(`/racks/${rackId}`), + create: (data) => cachedAPI.post('/racks', data), + update: (rackId, data) => cachedAPI.put(`/racks/${rackId}`, data), + delete: (rackId) => cachedAPI.delete(`/racks/${rackId}`), + import: (formData) => api.post('/racks/import', formData, { + headers: { 'Content-Type': 'multipart/form-data' } + }) +}; + +export const roomAPI = { + list: (params) => cachedAPI.get('/rooms', params), + get: (roomId) => cachedAPI.get(`/rooms/${roomId}`), + create: (data) => cachedAPI.post('/rooms', data), + update: (roomId, data) => cachedAPI.put(`/rooms/${roomId}`, data), + delete: (roomId) => cachedAPI.delete(`/rooms/${roomId}`) +}; + +export const deviceFieldAPI = { + list: () => cachedAPI.get('/deviceFields'), + get: (fieldId) => cachedAPI.get(`/deviceFields/${fieldId}`), + create: (data) => cachedAPI.post('/deviceFields', data), + update: (fieldId, data) => cachedAPI.put(`/deviceFields/${fieldId}`, data), + delete: (fieldId) => cachedAPI.delete(`/deviceFields/${fieldId}`), + updateConfig: (data) => cachedAPI.post('/deviceFields/config', data) +}; + +export const consumableAPI = { + list: (params) => cachedAPI.get('/consumables', params), + get: (consumableId) => cachedAPI.get(`/consumables/${consumableId}`), + create: (data) => cachedAPI.post('/consumables', data), + update: (consumableId, data) => cachedAPI.put(`/consumables/${consumableId}`, data), + delete: (consumableId) => cachedAPI.delete(`/consumables/${consumableId}`), + import: (data) => cachedAPI.post('/consumables/import', data), + quickInOut: (data) => cachedAPI.post('/consumables/quick-inout', data), + getStatistics: () => cachedAPI.get('/consumables/statistics/summary'), + getLowStock: () => cachedAPI.get('/consumables/low-stock') +}; + +export const consumableCategoryAPI = { + list: (params) => cachedAPI.get('/consumable-categories', params), + getList: (params) => cachedAPI.get('/consumable-categories/list', params), + create: (data) => cachedAPI.post('/consumable-categories', data), + update: (id, data) => cachedAPI.put(`/consumable-categories/${id}`, data), + delete: (id) => cachedAPI.delete(`/consumable-categories/${id}`) +}; + +export const consumableLogAPI = { + list: (params) => cachedAPI.get('/consumables/logs', params), + create: (data) => cachedAPI.post('/consumables/logs', data), + export: (params) => api.get('/consumables/logs/export', { params, responseType: 'blob' }), + import: (data) => cachedAPI.post('/consumables/logs/import', data) +}; + +export const ticketCategoryAPI = { + list: (params) => cachedAPI.get('/ticket-categories', params), + create: (data) => cachedAPI.post('/ticket-categories', data), + update: (code, data) => cachedAPI.put(`/ticket-categories/${code}`, data), + delete: (code) => cachedAPI.delete(`/ticket-categories/${code}`), + getTree: () => cachedAPI.get('/ticket-categories/tree'), + init: () => cachedAPI.post('/ticket-categories/init') +}; + +export { cacheManager }; +export default { cachedAPI, cacheManager }; diff --git a/frontend/src/pages/ConsumableManagement.jsx b/frontend/src/pages/ConsumableManagement.jsx index 91c3c0e..d4638ee 100644 --- a/frontend/src/pages/ConsumableManagement.jsx +++ b/frontend/src/pages/ConsumableManagement.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { Table, Button, Modal, Form, Input, Select, InputNumber, message, Card, Space, Popconfirm, Upload, Table as AntTable } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ExportOutlined, ImportOutlined, UploadOutlined, FileExcelOutlined, InboxOutlined } from '@ant-design/icons'; import axios from 'axios'; @@ -31,7 +31,7 @@ function ConsumableManagement() { const [stockType, setStockType] = useState('in'); const [stockForm] = Form.useForm(); - const fetchConsumables = async (page = 1, pageSize = 10) => { + const fetchConsumables = useCallback(async (page = 1, pageSize = 10) => { try { setLoading(true); const response = await axios.get('/api/consumables', { @@ -45,23 +45,23 @@ function ConsumableManagement() { } finally { setLoading(false); } - }; + }, [keyword, category, status]); - const fetchCategories = async () => { + const fetchCategories = useCallback(async () => { try { const response = await axios.get('/api/consumable-categories/list'); setCategories(response.data); } catch (error) { console.error('获取分类列表失败:', error); } - }; + }, []); useEffect(() => { fetchConsumables(); fetchCategories(); - }, [keyword, category, status]); + }, [fetchConsumables, fetchCategories]); - const showModal = (consumable = null) => { + const showModal = useCallback((consumable = null) => { setEditingConsumable(consumable); if (consumable) { form.setFieldsValue(consumable); @@ -69,14 +69,14 @@ function ConsumableManagement() { form.resetFields(); } setModalVisible(true); - }; + }, [form]); - const handleCancel = () => { + const handleCancel = useCallback(() => { setModalVisible(false); setEditingConsumable(null); - }; + }, []); - const handleSubmit = async (values) => { + const handleSubmit = useCallback(async (values) => { try { if (editingConsumable) { await axios.put(`/api/consumables/${editingConsumable.consumableId}`, values); @@ -95,9 +95,9 @@ function ConsumableManagement() { message.error(editingConsumable ? '耗材更新失败' : '耗材创建失败'); console.error('提交失败:', error); } - }; + }, [editingConsumable, fetchConsumables]); - const handleDelete = async (consumableId) => { + const handleDelete = useCallback(async (consumableId) => { try { await axios.delete(`/api/consumables/${consumableId}`); message.success('删除成功'); @@ -106,11 +106,11 @@ function ConsumableManagement() { message.error('删除失败'); console.error('删除失败:', error); } - }; + }, [fetchConsumables]); - const handleSearch = (value) => { + const handleSearch = useCallback((value) => { setKeyword(value); - }; + }, []); const exportToCSV = (data, filename) => { const headers = ['耗材ID', '名称', '分类', '单位', '当前库存', '最小库存', '最大库存', '单价', '供应商', '存放位置', '状态']; @@ -267,7 +267,7 @@ function ConsumableManagement() { window.URL.revokeObjectURL(url); }; - const showStockModal = (record, type) => { + const showStockModal = useCallback((record, type) => { setStockRecord(record); setStockType(type); stockForm.setFieldsValue({ @@ -278,14 +278,14 @@ function ConsumableManagement() { notes: '' }); setStockModalVisible(true); - }; + }, [stockForm]); - const handleStockCancel = () => { + const handleStockCancel = useCallback(() => { setStockModalVisible(false); setStockRecord(null); - }; + }, []); - const handleStockSubmit = async (values) => { + const handleStockSubmit = useCallback(async (values) => { try { const response = await axios.post('/api/consumables/quick-inout', { consumableId: stockRecord.consumableId, @@ -302,9 +302,9 @@ function ConsumableManagement() { message.error(error.response?.data?.error || `${stockType === 'in' ? '入库' : '出库'}操作失败`); console.error('操作失败:', error); } - }; + }, [stockRecord, stockType, fetchConsumables]); - const columns = [ + const columns = useMemo(() => [ { title: '耗材ID', dataIndex: 'consumableId', @@ -402,7 +402,7 @@ function ConsumableManagement() { ) } - ]; + ], [showModal, showStockModal, handleDelete]); const previewColumns = [ { title: '名称', dataIndex: '名称', key: 'name', width: 120 }, @@ -608,4 +608,4 @@ function ConsumableManagement() { ); } -export default ConsumableManagement; +export default React.memo(ConsumableManagement); diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 05f9166..12d253c 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -1,12 +1,11 @@ -import React, { useState, useEffect } from 'react'; -import { Card, Row, Col, Statistic, Spin, message, Button, Tag } from 'antd'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { Card, Row, Col, Statistic, message, Button, Tag } from 'antd'; import { DatabaseOutlined, CloudServerOutlined, WarningOutlined, PoweroffOutlined, HomeOutlined, - MonitorOutlined, SettingOutlined, ArrowUpOutlined, ArrowDownOutlined, @@ -15,197 +14,351 @@ import { } from '@ant-design/icons'; import axios from 'axios'; +const theme = { + primary: '#1890ff', + primaryDark: '#096dd9', + secondary: '#722ed1', + success: '#52c41a', + warning: '#faad14', + error: '#ff4d4f', + background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + cardBg: 'rgba(255, 255, 255, 0.95)', + textPrimary: '#262626', + textSecondary: '#8c8c8c' +}; + +const containerStyle = { + minHeight: '100vh', + background: 'linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%)', + padding: '24px' +}; + +const headerStyle = { + textAlign: 'center', + marginBottom: '32px' +}; + +const titleStyle = { + fontSize: '2.5rem', + fontWeight: '700', + background: theme.background, + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + margin: '0 0 8px 0' +}; + +const subtitleStyle = { + fontSize: '1.1rem', + color: theme.textSecondary, + margin: '0' +}; + +const statCardStyle = { + borderRadius: '16px', + border: 'none', + boxShadow: '0 4px 16px rgba(24, 144, 255, 0.1)', + background: theme.cardBg, + backdropFilter: 'blur(10px)', + transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + position: 'relative', + overflow: 'hidden', + cursor: 'pointer' +}; + +const systemOverviewStyle = { + marginTop: '32px' +}; + +const overviewCardStyle = { + borderRadius: '20px', + border: 'none', + boxShadow: '0 8px 32px rgba(24, 144, 255, 0.15)', + background: theme.cardBg, + backdropFilter: 'blur(10px)' +}; + +const welcomeSectionStyle = { + background: theme.background, + borderRadius: '12px', + padding: '20px', + textAlign: 'center', + color: 'white', + marginBottom: '24px' +}; + +const navigationGridStyle = { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', + gap: '16px', + marginBottom: '24px' +}; + +const navButtonStyle = { + height: 'auto', + padding: '20px 16px', + borderRadius: '12px', + border: '2px solid rgba(24, 144, 255, 0.1)', + background: 'rgba(24, 144, 255, 0.02)', + transition: 'all 0.3s ease', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '8px' +}; + +const navIconStyle = { + fontSize: '2rem', + color: theme.primary +}; + +const navTextStyle = { + fontSize: '0.95rem', + fontWeight: '600', + color: theme.textPrimary +}; + +const systemInfoStyle = { + background: 'linear-gradient(135deg, #e8f4fd 0%, #f0f9ff 100%)', + borderRadius: '12px', + padding: '16px', + border: '1px solid rgba(24, 144, 255, 0.1)' +}; + +const navButtonsData = [ + { key: 'devices', icon: CloudServerOutlined, text: '设备管理', path: '/devices' }, + { key: 'racks', icon: DatabaseOutlined, text: '资源规划', path: '/racks' }, + { key: 'faults', icon: WarningOutlined, text: '故障监控', path: '/faults' }, + { key: 'settings', icon: SettingOutlined, text: '系统配置', path: '/settings' } +]; + +const createTrendStyle = (trend) => ({ + display: 'flex', + alignItems: 'center', + fontSize: '0.875rem', + fontWeight: '500', + color: trend > 0 ? theme.success : theme.error, + marginTop: '8px' +}); + +const createStatCardBg = (color) => ({ + position: 'absolute', + top: '0', + right: '0', + width: '60px', + height: '60px', + background: `linear-gradient(135deg, ${color} 0%, ${color}99 100%)`, + borderRadius: '50%', + opacity: '0.1' +}); + function Dashboard() { const [stats, setStats] = useState({ - totalDevices: 0, - totalRacks: 0, - totalRooms: 0, - faultDevices: 0, - deviceGrowth: 2.5, // 示例增长数据 - faultTrend: -12.3 // 示例趋势数据 - }); + totalDevices: 0, + totalRacks: 0, + totalRooms: 0, + faultDevices: 0, + deviceGrowth: 2.5, + faultTrend: -12.3 + }); const [loading, setLoading] = useState(true); - useEffect(() => { - // 获取统计数据 - const fetchStats = async () => { - try { - setLoading(true); - - // 获取所有设备 - const devicesRes = await axios.get('/api/devices'); - // 设备API返回的是包含total和devices数组的对象 - const totalDevices = devicesRes.data.total; - // 获取所有设备以统计故障设备数量 - const allDevicesRes = await axios.get('/api/devices', { params: { pageSize: totalDevices } }); - const allDevices = allDevicesRes.data.devices || allDevicesRes.data; - const faultDevices = allDevices.filter(device => device.status === 'fault').length; - - // 获取所有机柜 - const racksRes = await axios.get('/api/racks'); - // 机柜API返回的是包含total和racks数组的对象 - const totalRacks = racksRes.data.total; - const racks = racksRes.data.racks || []; - - // 获取所有机房 - const roomsRes = await axios.get('/api/rooms'); - const rooms = roomsRes.data; - const totalRooms = rooms.length; - - setStats({ - totalDevices, - totalRacks, - totalRooms, - faultDevices, - // 保留或更新趋势数据 - deviceGrowth: stats.deviceGrowth || 0, - faultTrend: stats.faultTrend || 0 - }); - } catch (error) { - message.error('获取统计数据失败'); - console.error('获取统计数据失败:', error); - } finally { - setLoading(false); + const fetchStats = useCallback(async () => { + try { + setLoading(true); + + const [devicesRes, racksRes, roomsRes] = await Promise.all([ + axios.get('/api/devices', { params: { pageSize: 1 } }), + axios.get('/api/racks', { params: { pageSize: 1 } }), + axios.get('/api/rooms') + ]); + + const totalDevices = devicesRes.data.total || 0; + const totalRacks = racksRes.data.total || 0; + const rooms = roomsRes.data || []; + const totalRooms = rooms.length; + + let faultDevices = 0; + if (totalDevices > 0) { + try { + const faultRes = await axios.get('/api/devices/count', { + params: { status: 'fault' } + }); + faultDevices = faultRes.data.count || 0; + } catch { + faultDevices = 0; + } } - }; - - fetchStats(); + + setStats({ + totalDevices, + totalRacks, + totalRooms, + faultDevices, + deviceGrowth: 2.5, + faultTrend: -12.3 + }); + } catch (error) { + message.error('获取统计数据失败'); + console.error('获取统计数据失败:', error); + } finally { + setLoading(false); + } }, []); - // 科技感配色主题 - const theme = { - primary: '#1890ff', // 科技蓝 - primaryDark: '#096dd9', // 深蓝 - secondary: '#722ed1', // 紫色 - success: '#52c41a', // 绿色 - warning: '#faad14', // 橙色 - error: '#ff4d4f', // 红色 - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - cardBg: 'rgba(255, 255, 255, 0.95)', - textPrimary: '#262626', - textSecondary: '#8c8c8c' - }; + useEffect(() => { + fetchStats(); + }, [fetchStats]); - const containerStyle = { - minHeight: '100vh', - background: 'linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%)', - padding: '24px' - }; + const handleNavHover = useCallback((e, isEnter) => { + if (isEnter) { + e.currentTarget.style.borderColor = theme.primary; + e.currentTarget.style.background = 'rgba(24, 144, 255, 0.1)'; + e.currentTarget.style.transform = 'translateY(-2px)'; + } else { + e.currentTarget.style.borderColor = 'rgba(24, 144, 255, 0.1)'; + e.currentTarget.style.background = 'rgba(24, 144, 255, 0.02)'; + e.currentTarget.style.transform = 'translateY(0)'; + } + }, []); - const headerStyle = { - textAlign: 'center', - marginBottom: '32px' - }; + const handleRefresh = useCallback(() => { + fetchStats(); + }, [fetchStats]); - const titleStyle = { - fontSize: '2.5rem', - fontWeight: '700', - background: theme.background, - WebkitBackgroundClip: 'text', - WebkitTextFillColor: 'transparent', - margin: '0 0 8px 0' - }; + const statCards = useMemo(() => [ + { + key: 'devices', + xs: 24, sm: 12, lg: 6, + icon: CloudServerOutlined, + color: '#1890ff', + statKey: 'totalDevices', + title: '总设备数', + trend: stats.deviceGrowth, + tagColor: 'blue' + }, + { + key: 'racks', + xs: 24, sm: 12, lg: 6, + icon: DatabaseOutlined, + color: '#722ed1', + statKey: 'totalRacks', + title: '总机柜数', + trend: 0, + tagColor: 'green', + customStatus: true + }, + { + key: 'rooms', + xs: 24, sm: 12, lg: 6, + icon: HomeOutlined, + color: '#52c41a', + statKey: 'totalRooms', + title: '总机房数', + trend: 0, + tagColor: 'green', + customStatus: true + }, + { + key: 'faults', + xs: 24, sm: 12, lg: 6, + icon: WarningOutlined, + color: '#ff4d4f', + statKey: 'faultDevices', + title: '故障设备', + trend: stats.faultTrend, + tagColor: 'red' + } + ], [stats.deviceGrowth, stats.faultTrend]); - const subtitleStyle = { - fontSize: '1.1rem', - color: theme.textSecondary, - margin: '0' - }; + const renderStatCard = useCallback((config) => { + const { icon: Icon, color, statKey, title, trend, tagColor, customStatus, xs, sm, lg } = config; + const colProps = { xs, sm, lg }; + + return ( + + +
+
+ + {title} + + } + value={stats[statKey]} + prefix={} + valueStyle={{ + fontSize: '2rem', + fontWeight: '700', + color: theme.textPrimary, + marginBottom: '8px' + }} + loading={loading} + /> + {customStatus ? ( + statKey === 'totalRacks' ? ( +
+ + 正常运行 +
+ ) : ( +
+ + 全部在线 +
+ ) + ) : ( +
+ {trend > 0 ? : } + {Math.abs(trend)}% + 本月 +
+ )} +
+ + + ); + }, [stats, loading]); - const statCardStyle = { - borderRadius: '16px', - border: 'none', - boxShadow: '0 4px 16px rgba(24, 144, 255, 0.1)', - background: theme.cardBg, - backdropFilter: 'blur(10px)', - transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', - position: 'relative', - overflow: 'hidden', - cursor: 'pointer' - }; + const navButtons = useMemo(() => navButtonsData.map(({ key, icon: Icon, text }) => ( + + )), [handleNavHover]); - const statCardHoverStyle = { - transform: 'translateY(-4px)', - boxShadow: '0 8px 32px rgba(24, 144, 255, 0.2)' - }; - - const trendStyle = (trend) => ({ - display: 'flex', - alignItems: 'center', - fontSize: '0.875rem', - fontWeight: '500', - color: trend > 0 ? theme.success : theme.error, - marginTop: '8px' - }); - - const systemOverviewStyle = { - marginTop: '32px' - }; - - const overviewCardStyle = { - borderRadius: '20px', - border: 'none', - boxShadow: '0 8px 32px rgba(24, 144, 255, 0.15)', - background: theme.cardBg, - backdropFilter: 'blur(10px)' - }; - - const welcomeSectionStyle = { - background: theme.background, - borderRadius: '12px', - padding: '20px', - textAlign: 'center', - color: 'white', - marginBottom: '24px' - }; - - const navigationGridStyle = { - display: 'grid', - gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', - gap: '16px', - marginBottom: '24px' - }; - - const navButtonStyle = { - height: 'auto', - padding: '20px 16px', - borderRadius: '12px', - border: '2px solid rgba(24, 144, 255, 0.1)', - background: 'rgba(24, 144, 255, 0.02)', - transition: 'all 0.3s ease', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: '8px' - }; - - const navButtonHoverStyle = { - borderColor: theme.primary, - background: 'rgba(24, 144, 255, 0.1)', - transform: 'translateY(-2px)' - }; - - const navIconStyle = { - fontSize: '2rem', - color: theme.primary - }; - - const navTextStyle = { - fontSize: '0.95rem', - fontWeight: '600', - color: theme.textPrimary - }; - - const systemInfoStyle = { - background: 'linear-gradient(135deg, #e8f4fd 0%, #f0f9ff 100%)', - borderRadius: '12px', - padding: '16px', - border: '1px solid rgba(24, 144, 255, 0.1)' - }; + const systemInfo = useMemo(() => ( +
+
+
+

+ 系统版本: v1.0.0 +

+

+ 最后更新:{new Date().toLocaleDateString()} +

+
+ +
+
+ ), [handleRefresh]); return (
- {/* 页面头部 */}

@@ -214,164 +367,13 @@ function Dashboard() {

实时监控 • 智能管理 • 高效运维

- {/* 数据概览卡片 */} - - -
-
- - 总设备数 - - } - value={stats.totalDevices} - prefix={} - valueStyle={{ - fontSize: '2rem', - fontWeight: '700', - color: theme.textPrimary, - marginBottom: '8px' - }} - loading={loading} - /> -
- {stats.deviceGrowth > 0 ? : } - {Math.abs(stats.deviceGrowth)}% - 本月 -
-
- - - - - -
-
- - 总机柜数 - - } - value={stats.totalRacks} - prefix={} - valueStyle={{ - fontSize: '2rem', - fontWeight: '700', - color: theme.textPrimary, - marginBottom: '8px' - }} - loading={loading} - /> -
- - 正常运行 -
-
- - - - - -
-
- - 总机房数 - - } - value={stats.totalRooms} - prefix={} - valueStyle={{ - fontSize: '2rem', - fontWeight: '700', - color: theme.textPrimary, - marginBottom: '8px' - }} - loading={loading} - /> -
- - 全部在线 -
-
- - - - - -
-
- - 故障设备 - - } - value={stats.faultDevices} - prefix={} - valueStyle={{ - fontSize: '2rem', - fontWeight: '700', - color: theme.error, - marginBottom: '8px' - }} - loading={loading} - /> -
- {stats.faultTrend < 0 ? : } - {Math.abs(stats.faultTrend)}% - 本周 -
-
- - + {statCards.map(renderStatCard)} - {/* 系统概览 */}
- {/* 欢迎区域 */}

- {/* 导航功能网格 */}
- - - - - - - + {navButtons}
- {/* 系统信息 */} -
-
-
-

- 系统版本: v1.0.0 -

-

- 最后更新:{new Date().toLocaleDateString()} -

-
- -
-
+ {systemInfo}
@@ -494,4 +405,4 @@ function Dashboard() { ); } -export default Dashboard; \ No newline at end of file +export default React.memo(Dashboard); diff --git a/frontend/src/pages/DeviceManagement.jsx b/frontend/src/pages/DeviceManagement.jsx index 33716cb..4204e49 100644 --- a/frontend/src/pages/DeviceManagement.jsx +++ b/frontend/src/pages/DeviceManagement.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Table, Button, Modal, Form, Input, Select, DatePicker, message, Card, Space, InputNumber, Switch, Upload, Progress, Checkbox } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, UploadOutlined, DownloadOutlined, SettingOutlined, UndoOutlined, CloudServerOutlined, SwapOutlined, SafetyOutlined, DatabaseOutlined, AppstoreOutlined } from '@ant-design/icons'; import axios from 'axios'; @@ -7,6 +7,79 @@ import dayjs from 'dayjs'; const { Option } = Select; const { RangePicker } = DatePicker; +// 工具函数提取到组件外部,避免每次渲染重复创建 +const getStatusConfig = (status) => { + const statusMap = { + running: { text: '运行中', color: 'green' }, + maintenance: { text: '维护中', color: 'orange' }, + offline: { text: '离线', color: 'gray' }, + fault: { text: '故障', color: 'red' } + }; + return statusMap[status] || { text: status, color: 'black' }; +}; + +const getTypeLabel = (type) => { + const typeMap = { + server: '服务器', + switch: '交换机', + router: '路由器', + storage: '存储设备', + other: '其他设备' + }; + return typeMap[type] || type; +}; + +const getDeviceTypeIcon = (type) => { + const iconMap = { + server: , + switch: , + router: , + storage: , + other: + }; + return iconMap[type] || ; +}; + +// 格式化日期 +const formatDate = (date, fieldName) => { + if (!date) return ''; + + const dateObj = new Date(date); + const formattedDate = dateObj.toLocaleDateString('zh-CN'); + + if (fieldName === 'warrantyExpiry') { + const today = new Date(); + today.setHours(0, 0, 0, 0); + dateObj.setHours(0, 0, 0, 0); + + if (dateObj < today) { + return {formattedDate}; + } + } + + return formattedDate; +}; + +// 默认设备字段配置 +const defaultDeviceFields = [ + { fieldName: 'deviceId', displayName: '设备ID', fieldType: 'string', required: true, order: 1, visible: true }, + { fieldName: 'name', displayName: '设备名称', fieldType: 'string', required: true, order: 2, visible: true }, + { fieldName: 'type', displayName: '设备类型', fieldType: 'select', required: true, order: 3, visible: true, + options: [{ value: 'server', label: '服务器' }, { value: 'switch', label: '交换机' }, { value: 'router', label: '路由器' }, { value: 'storage', label: '存储设备' }, { value: 'other', label: '其他设备' }] }, + { fieldName: 'model', displayName: '型号', fieldType: 'string', required: true, order: 4, visible: true }, + { fieldName: 'serialNumber', displayName: '序列号', fieldType: 'string', required: true, order: 5, visible: true }, + { fieldName: 'rackId', displayName: '所在机柜', fieldType: 'select', required: true, order: 6, visible: true }, + { fieldName: 'position', displayName: '位置(U)', fieldType: 'number', required: true, order: 7, visible: true }, + { fieldName: 'height', displayName: '高度(U)', fieldType: 'number', required: true, order: 8, visible: true }, + { fieldName: 'powerConsumption', displayName: '功率(W)', fieldType: 'number', required: true, order: 9, visible: true }, + { fieldName: 'status', displayName: '状态', fieldType: 'select', required: true, order: 10, visible: true, + options: [{ value: 'running', label: '运行中' }, { value: 'maintenance', label: '维护中' }, { value: 'offline', label: '离线' }, { value: 'fault', label: '故障' }] }, + { fieldName: 'purchaseDate', displayName: '购买日期', fieldType: 'date', required: true, order: 11, visible: true }, + { fieldName: 'warrantyExpiry', displayName: '保修到期', fieldType: 'date', required: true, order: 12, visible: true }, + { fieldName: 'ipAddress', displayName: 'IP地址', fieldType: 'string', required: false, order: 13, visible: true }, + { fieldName: 'description', displayName: '描述', fieldType: 'textarea', required: false, order: 14, visible: true } +]; + // 可调整列宽的表头组件 const ResizeableTitle = (props) => { const { onResize, width, ...restProps } = props; @@ -18,13 +91,12 @@ const ResizeableTitle = (props) => { const handleMouseDown = (e) => { if (!onResize) return; - const startX = e.pageX; const startWidth = width; const handleMouseMove = (moveEvent) => { const diff = moveEvent.pageX - startX; - const newWidth = Math.max(50, startWidth + diff); // 最小宽度50px + const newWidth = Math.max(50, startWidth + diff); onResize(newWidth); }; @@ -855,7 +927,8 @@ function DeviceManagement() { loading={loading} pagination={pagination} onChange={handleTableChange} - scroll={{ x: 'max-content' }} + scroll={{ y: 600, x: 'max-content' }} + virtual rowSelection={{ selectedRowKeys: selectedDevices, onChange: setSelectedDevices, diff --git a/frontend/src/pages/RackManagement.jsx b/frontend/src/pages/RackManagement.jsx index 47423b2..16609b9 100644 --- a/frontend/src/pages/RackManagement.jsx +++ b/frontend/src/pages/RackManagement.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Table, Button, Modal, Form, Input, Select, message, Card, Space, InputNumber, Upload, Progress } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons'; import axios from 'axios'; @@ -6,6 +6,13 @@ import * as XLSX from 'xlsx'; const { Option } = Select; +// 状态映射函数 +const statusMap = { + active: { text: '在用', color: 'green' }, + maintenance: { text: '维护中', color: 'orange' }, + inactive: { text: '停用', color: 'gray' } +}; + function RackManagement() { const [racks, setRacks] = useState([]); const [rooms, setRooms] = useState([]); @@ -28,10 +35,7 @@ function RackManagement() { const [isImporting, setIsImporting] = useState(false); const [importResult, setImportResult] = useState(null); - - - // 获取所有机柜 - const fetchRacks = async (page = 1, pageSize = 10) => { + const fetchRacks = useCallback(async (page = 1, pageSize = 10) => { try { setLoading(true); const response = await axios.get('/api/racks', { @@ -51,10 +55,9 @@ function RackManagement() { } finally { setLoading(false); } - }; + }, []); - // 获取所有机房 - const fetchRooms = async () => { + const fetchRooms = useCallback(async () => { try { const response = await axios.get('/api/rooms'); setRooms(response.data); @@ -62,20 +65,19 @@ function RackManagement() { message.error('获取机房列表失败'); console.error('获取机房列表失败:', error); } - }; + }, []); - // 处理表格分页变化 - const handleTableChange = (pagination) => { + const handleTableChange = useCallback((pagination) => { fetchRacks(pagination.current, pagination.pageSize); - }; + }, [fetchRacks]); useEffect(() => { fetchRacks(pagination.current, pagination.pageSize); fetchRooms(); - }, []); + }, [fetchRacks, fetchRooms]); // 打开模态框 - const showModal = (rack = null) => { + const showModal = useCallback((rack = null) => { setEditingRack(rack); if (rack) { form.setFieldsValue(rack); @@ -83,16 +85,16 @@ function RackManagement() { form.resetFields(); } setModalVisible(true); - }; + }, []); // 关闭模态框 - const handleCancel = () => { + const handleCancel = useCallback(() => { setModalVisible(false); setEditingRack(null); - }; + }, []); // 提交表单 - const handleSubmit = async (values) => { + const handleSubmit = useCallback(async (values) => { try { if (editingRack) { // 更新机柜 @@ -111,10 +113,10 @@ function RackManagement() { message.error(editingRack ? '机柜更新失败' : '机柜创建失败'); console.error(editingRack ? '机柜更新失败:' : '机柜创建失败:', error); } - }; + }, [editingRack, fetchRacks]); // 删除机柜 - const handleDelete = async (rackId) => { + const handleDelete = useCallback(async (rackId) => { Modal.confirm({ title: '确认删除', content: '确定要删除这个机柜吗?', @@ -132,17 +134,17 @@ function RackManagement() { } } }); - }; + }, [fetchRacks]); // 下载导入模板 - const handleDownloadTemplate = () => { + const handleDownloadTemplate = useCallback(() => { // 调用后端API下载模板 window.open('/api/racks/import-template', '_blank'); message.success('模板下载成功'); - }; + }, []); // 导入机柜数据 - const handleImport = async (file) => { + const handleImport = useCallback(async (file) => { try { setIsImporting(true); setImportProgress(0); @@ -213,17 +215,10 @@ function RackManagement() { // 阻止自动上传 return false; } - }; - - // 状态标签映射 - const statusMap = { - active: { text: '在用', color: 'green' }, - maintenance: { text: '维护中', color: 'orange' }, - inactive: { text: '停用', color: 'gray' } - }; + }, []); // 表格列配置 - const columns = [ + const columns = useMemo(() => [ { title: '机柜ID', dataIndex: 'rackId', @@ -493,4 +488,4 @@ function RackManagement() { ); } -export default RackManagement; \ No newline at end of file +export default React.memo(RackManagement); \ No newline at end of file diff --git a/frontend/src/pages/RackVisualization.jsx b/frontend/src/pages/RackVisualization.jsx index ed1ee0b..9bdc974 100644 --- a/frontend/src/pages/RackVisualization.jsx +++ b/frontend/src/pages/RackVisualization.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Card, Select, Button, Space, message, Tooltip, Modal, Form, Switch, Checkbox } from 'antd'; import { ReloadOutlined, @@ -18,101 +18,226 @@ import axios from 'axios'; const { Option } = Select; -// 添加动画样式 -const style = document.createElement('style'); -style.textContent = ` - @keyframes slideIn { - from { - opacity: 0; - transform: translateY(-50%) translateX(20px) scale(0.95); +// 工具函数提取到组件外部,避免每次渲染重复创建 +const getDeviceIcon = (deviceType) => { + try { + if (!deviceType) return ; + const type = deviceType.toLowerCase(); + + if (type.includes('server') || type.includes('服务器')) return ; + if (type.includes('switch') || type.includes('交换机')) return ; + if (type.includes('storage') || type.includes('存储')) return ; + if (type.includes('router') || type.includes('路由器')) return ; + if (type.includes('laptop') || type.includes('笔记本')) return ; + if (type.includes('mobile') || type.includes('手机')) return ; + if (type.includes('printer') || type.includes('打印机')) return ; + + return ; + } catch (error) { + console.error('设备图标渲染错误:', error); + return ; + } +}; + +const getDeviceColor = (deviceType) => { + if (!deviceType) return '#1890ff'; + const type = deviceType.toLowerCase(); + + if (type.includes('server') || type.includes('服务器')) return '#1890ff'; + if (type.includes('switch') || type.includes('交换机')) return '#52c41a'; + if (type.includes('storage') || type.includes('存储')) return '#faad14'; + if (type.includes('router') || type.includes('路由器')) return '#f5222d'; + if (type.includes('laptop') || type.includes('笔记本')) return '#722ed1'; + if (type.includes('mobile') || type.includes('手机')) return '#eb2f96'; + if (type.includes('printer') || type.includes('打印机')) return '#13c2c2'; + + return '#1890ff'; +}; + +const getDeviceStatusColor = (status) => { + const statusColorMap = { + 'normal': '#10b981', + 'warning': '#f59e0b', + 'error': '#ef4444', + 'offline': '#6b7280', + 'maintenance': '#3b82f6', + undefined: '#3b82f6', + null: '#3b82f6' + }; + return statusColorMap[status] || '#3b82f6'; +}; + +const getDeviceTypeTheme = (type) => { + const themeMap = { + 'server': { + borderColor: '#38bdf8', + accentColor: '#0ea5e9', + glowColor: 'rgba(56, 189, 248, 0.3)', + iconColor: '#38bdf8', + label: '服务器' + }, + 'switch': { + borderColor: '#22c55e', + accentColor: '#16a34a', + glowColor: 'rgba(34, 197, 94, 0.3)', + iconColor: '#22c55e', + label: '交换机' + }, + 'router': { + borderColor: '#f59e0b', + accentColor: '#d97706', + glowColor: 'rgba(245, 158, 11, 0.3)', + iconColor: '#f59e0b', + label: '路由器' + }, + 'storage': { + borderColor: '#8b5cf6', + accentColor: '#7c3aed', + glowColor: 'rgba(139, 92, 246, 0.3)', + iconColor: '#8b5cf6', + label: '存储' + }, + 'firewall': { + borderColor: '#ef4444', + accentColor: '#dc2626', + glowColor: 'rgba(239, 68, 68, 0.3)', + iconColor: '#ef4444', + label: '防火墙' + }, + 'ups': { + borderColor: '#14b8a6', + accentColor: '#0d9488', + glowColor: 'rgba(20, 184, 166, 0.3)', + iconColor: '#14b8a6', + label: 'UPS' + }, + 'pdus': { + borderColor: '#64748b', + accentColor: '#475569', + glowColor: 'rgba(100, 116, 139, 0.3)', + iconColor: '#64748b', + label: 'PDU' + }, + 'other': { + borderColor: '#94a3b8', + accentColor: '#64748b', + glowColor: 'rgba(148, 163, 184, 0.3)', + iconColor: '#94a3b8', + label: '其他设备' } - to { - opacity: 1; - transform: translateY(-50%) translateX(0) scale(1); + }; + + const normalizedType = type?.toLowerCase(); + + if (normalizedType?.includes('server') || normalizedType?.includes('服务器')) return themeMap.server; + if (normalizedType?.includes('switch') || normalizedType?.includes('交换机')) return themeMap.switch; + if (normalizedType?.includes('router') || normalizedType?.includes('路由器')) return themeMap.router; + if (normalizedType?.includes('storage') || normalizedType?.includes('存储')) return themeMap.storage; + if (normalizedType?.includes('firewall') || normalizedType?.includes('防火墙')) return themeMap.firewall; + if (normalizedType?.includes('ups') || normalizedType?.includes('不间断电源')) return themeMap.ups; + if (normalizedType?.includes('pdu') || normalizedType?.includes('电源分配')) return themeMap.pdus; + + return themeMap.other; +}; + +// 初始化动画样式 +const initAnimationStyles = () => { + const existingStyle = document.getElementById('rack-visualization-styles'); + if (existingStyle) { + return existingStyle; + } + + const style = document.createElement('style'); + style.id = 'rack-visualization-styles'; + style.textContent = ` + @keyframes slideIn { + from { + opacity: 0; + transform: translateY(-50%) translateX(20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(-50%) translateX(0) scale(1); + } } - } - - /* LED指示灯闪烁动画 */ - @keyframes ledBlink { - 0%, 50% { opacity: 1; } - 51%, 100% { opacity: 0.3; } - } - - /* Tooltip淡入动画 */ - @keyframes tooltipFadeIn { - from { - opacity: 0; - transform: translateY(-50%) translateX(-10px) scale(0.95); + + @keyframes ledBlink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.3; } } - to { - opacity: 1; - transform: translateY(-50%) translateX(0) scale(1); + + @keyframes tooltipFadeIn { + from { + opacity: 0; + transform: translateY(-50%) translateX(-10px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(-50%) translateX(0) scale(1); + } } - } - - /* 金属拉丝纹理 */ - .metal-texture { - background-image: - linear-gradient(90deg, - transparent 0%, - rgba(255,255,255,0.03) 50%, - transparent 100%), - repeating-linear-gradient(0deg, - transparent 0px, - rgba(255,255,255,0.02) 1px, - transparent 2px, - transparent 3px); - background-size: 100% 100%, 4px 4px; - } - - /* 散热格栅效果 */ - .ventilation-grille { - background-image: repeating-linear-gradient( - 0deg, - #334155 0px, - #334155 1px, - transparent 1px, - transparent 2px - ); - } - - /* 悬停提亮效果 */ - .device-hover { - background: linear-gradient(145deg, #1e293b, #0f172a) !important; - box-shadow: 0 6px 16px rgba(56, 189, 248, 0.3), 0 0 12px rgba(56, 189, 248, 0.2) !important; - border-color: #38bdf8 !important; - } - - /* Tooltip样式 */ - .device-tooltip { - background: rgba(0, 0, 0, 0.9); - color: #5eead4; - padding: 8px 12px; - border-radius: 4px; - font-family: 'Roboto Mono', monospace; - font-size: 11px; - border: 1px solid rgba(94, 234, 212, 0.3); - box-shadow: 0 4px 12px rgba(0,0,0,0.5); - white-space: nowrap; - } - - /* 设备数量badge */ - .device-count-badge { - background: linear-gradient(135deg, rgba(56, 189, 248, 0.2), rgba(14, 165, 233, 0.2)); - color: #38bdf8; - padding: 6px 14px; - border-radius: 20px; - font-size: 13px; - font-weight: 600; - box-shadow: 0 4px 15px rgba(56, 189, 248, 0.2); - border: 1px solid rgba(56, 189, 248, 0.3); - backdrop-filter: blur(10px); - transition: all 0.3s ease; - white-space: nowrap; - font-family: 'JetBrains Mono', 'Roboto Mono', monospace; - } -`; -document.head.appendChild(style); + + .metal-texture { + background-image: + linear-gradient(90deg, + transparent 0%, + rgba(255,255,255,0.03) 50%, + transparent 100%), + repeating-linear-gradient(0deg, + transparent 0px, + rgba(255,255,255,0.02) 1px, + transparent 2px, + transparent 3px); + background-size: 100% 100%, 4px 4px; + } + + .ventilation-grille { + background-image: repeating-linear-gradient( + 0deg, + #334155 0px, + #334155 1px, + transparent 1px, + transparent 2px + ); + } + + .device-hover { + background: linear-gradient(145deg, #1e293b, #0f172a) !important; + box-shadow: 0 6px 16px rgba(56, 189, 248, 0.3), 0 0 12px rgba(56, 189, 248, 0.2) !important; + border-color: #38bdf8 !important; + } + + .device-tooltip { + background: rgba(0, 0, 0, 0.9); + color: #5eead4; + padding: 8px 12px; + border-radius: 4px; + font-family: 'Roboto Mono', monospace; + font-size: 11px; + border: 1px solid rgba(94, 234, 212, 0.3); + box-shadow: 0 4px 12px rgba(0,0,0,0.5); + white-space: nowrap; + } + + .device-count-badge { + background: linear-gradient(135deg, rgba(56, 189, 248, 0.2), rgba(14, 165, 233, 0.2)); + color: #38bdf8; + padding: 6px 14px; + border-radius: 20px; + font-size: 13px; + font-weight: 600; + box-shadow: 0 4px 15px rgba(56, 189, 248, 0.2); + border: 1px solid rgba(56, 189, 248, 0.3); + backdrop-filter: blur(10px); + transition: all 0.3s ease; + white-space: nowrap; + font-family: 'JetBrains Mono', 'Roboto Mono', monospace; + } + `; + document.head.appendChild(style); + return style; +}; + // 错误边界组件 class ErrorBoundary extends React.Component { constructor(props) { @@ -191,7 +316,7 @@ function RackVisualization() { const [tooltipFields, setTooltipFields] = useState({}); // 默认设备字段配置 - const defaultTooltipFields = { + const defaultTooltipFields = useMemo(() => ({ name: { label: '设备名称', enabled: true, field: 'name', fieldType: 'string' }, deviceId: { label: '设备ID', enabled: true, field: 'deviceId', fieldType: 'string' }, type: { label: '设备类型', enabled: true, field: 'type', fieldType: 'string' }, @@ -202,10 +327,15 @@ function RackVisualization() { height: { label: '高度', enabled: true, field: 'height', fieldType: 'number' }, ipAddress: { label: 'IP地址', enabled: true, field: 'ipAddress', fieldType: 'string' }, power: { label: '功率', enabled: true, field: 'power', fieldType: 'number' } - }; + }), []); - // 获取设备字段配置 - const fetchTooltipDeviceFields = async () => { + // 初始化样式 + useEffect(() => { + initAnimationStyles(); + }, []); + + // 获取设备字段配置 - 使用 useCallback 避免重复创建 + const fetchTooltipDeviceFields = useCallback(async () => { try { setLoadingTooltipFields(true); console.log('开始获取设备字段配置...'); @@ -242,10 +372,10 @@ function RackVisualization() { } finally { setLoadingTooltipFields(false); } - }; + }, [defaultTooltipFields]); // 保存tooltip字段配置 - const saveTooltipConfig = async () => { + const saveTooltipConfig = useCallback(async () => { try { setSavingTooltipConfig(true); @@ -268,10 +398,10 @@ function RackVisualization() { } finally { setSavingTooltipConfig(false); } - }; + }, [tooltipFields, fetchTooltipDeviceFields]); - // 获取所有机柜 - const fetchRacks = async () => { + // 获取所有机柜 - 使用 useCallback 避免重复创建 + const fetchRacks = useCallback(async () => { try { setLoading(true); setError(null); @@ -317,10 +447,10 @@ function RackVisualization() { } finally { setLoading(false); } - }; + }, []); - // 获取机柜内的设备 - const fetchDevices = async (rackId) => { + // 获取机柜内的设备 - 使用 useCallback 避免重复创建 + const fetchDevices = useCallback(async (rackId) => { try { setLoadingDevices(true); console.log(`=== 开始获取机柜 ${rackId} 的设备数据 ===`); @@ -462,13 +592,11 @@ function RackVisualization() { } finally { setLoadingDevices(false); } - }; + }, []); useEffect(() => { fetchRacks(); - loadBackgroundSettings(); - fetchTooltipDeviceFields(); - }, []); + }, [fetchRacks]); // 打开字段配置模态框时获取数据 const handleOpenTooltipConfig = () => { @@ -478,120 +606,6 @@ function RackVisualization() { setShowTooltipConfig(true); }; - // 根据设备类型获取图标 - const getDeviceIcon = (deviceType) => { - try { - if (!deviceType) return ; - const type = deviceType.toLowerCase(); - - if (type.includes('server') || type.includes('服务器')) return ; - if (type.includes('switch') || type.includes('交换机')) return ; - if (type.includes('storage') || type.includes('存储')) return ; - if (type.includes('router') || type.includes('路由器')) return ; - if (type.includes('laptop') || type.includes('笔记本')) return ; - if (type.includes('mobile') || type.includes('手机')) return ; - if (type.includes('printer') || type.includes('打印机')) return ; - - return ; - } catch (error) { - console.error('设备图标渲染错误:', error); - return ; - } - }; - - // 根据设备类型获取背景色 - const getDeviceColor = (deviceType) => { - if (!deviceType) return '#1890ff'; - const type = deviceType.toLowerCase(); - - if (type.includes('server') || type.includes('服务器')) return '#1890ff'; // 蓝色 - if (type.includes('switch') || type.includes('交换机')) return '#52c41a'; // 绿色 - if (type.includes('storage') || type.includes('存储')) return '#faad14'; // 黄色 - if (type.includes('router') || type.includes('路由器')) return '#f5222d'; // 红色 - if (type.includes('laptop') || type.includes('笔记本')) return '#722ed1'; // 紫色 - if (type.includes('mobile') || type.includes('手机')) return '#eb2f96'; // 粉色 - if (type.includes('printer') || type.includes('打印机')) return '#13c2c2'; // 青色 - - return '#1890ff'; // 默认蓝色 - }; - - // 获取设备状态颜色 - const getDeviceStatusColor = (status) => { - const statusColorMap = { - 'normal': '#10b981', // 正常 - 绿色常亮 - 'warning': '#f59e0b', // 预警 - 黄色常亮 - 'error': '#ef4444', // 告警 - 红色慢闪 - 'offline': '#6b7280', // 离线 - 灰色 - 'maintenance': '#3b82f6', // 维护 - 蓝色常亮 - undefined: '#3b82f6', // 默认普通设备 - 蓝色常亮 - null: '#3b82f6' - }; - return statusColorMap[status] || '#3b82f6'; - }; - - // 获取设备类型对应的颜色主题 - const getDeviceTypeTheme = (type) => { - const themeMap = { - 'server': { - borderColor: '#38bdf8', - accentColor: '#0ea5e9', - glowColor: 'rgba(56, 189, 248, 0.3)', - iconColor: '#38bdf8', - label: '服务器' - }, - 'switch': { - borderColor: '#22c55e', - accentColor: '#16a34a', - glowColor: 'rgba(34, 197, 94, 0.3)', - iconColor: '#22c55e', - label: '交换机' - }, - 'router': { - borderColor: '#f59e0b', - accentColor: '#d97706', - glowColor: 'rgba(245, 158, 11, 0.3)', - iconColor: '#f59e0b', - label: '路由器' - }, - 'storage': { - borderColor: '#8b5cf6', - accentColor: '#7c3aed', - glowColor: 'rgba(139, 92, 246, 0.3)', - iconColor: '#8b5cf6', - label: '存储' - }, - 'firewall': { - borderColor: '#ef4444', - accentColor: '#dc2626', - glowColor: 'rgba(239, 68, 68, 0.3)', - iconColor: '#ef4444', - label: '防火墙' - }, - 'ups': { - borderColor: '#14b8a6', - accentColor: '#0d9488', - glowColor: 'rgba(20, 184, 166, 0.3)', - iconColor: '#14b8a6', - label: 'UPS' - }, - 'pdus': { - borderColor: '#64748b', - accentColor: '#475569', - glowColor: 'rgba(100, 116, 139, 0.3)', - iconColor: '#64748b', - label: 'PDU' - }, - 'other': { - borderColor: '#94a3b8', - accentColor: '#64748b', - glowColor: 'rgba(148, 163, 184, 0.3)', - iconColor: '#94a3b8', - label: '其他' - } - }; - return themeMap[type?.toLowerCase()] || themeMap['other']; - }; - // 生成模拟监控数据 const generateMonitoringData = (device) => { const baseTemp = device.type?.includes('服务器') ? 65 : @@ -1987,4 +2001,4 @@ function RackVisualization() { ); } -export default RackVisualization; \ No newline at end of file +export default React.memo(RackVisualization); \ No newline at end of file diff --git a/frontend/src/pages/TicketManagement.jsx b/frontend/src/pages/TicketManagement.jsx index ba8e7ed..d2903ba 100644 --- a/frontend/src/pages/TicketManagement.jsx +++ b/frontend/src/pages/TicketManagement.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Table, Button, Modal, Form, Input, Select, DatePicker, message, Card, Space, Tag, Dropdown, Menu, Tabs, Timeline, Descriptions } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, EyeOutlined, MoreOutlined, UserOutlined, ToolOutlined, CheckCircleOutlined, SyncOutlined, ClockCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; import axios from 'axios'; @@ -9,6 +9,46 @@ const { RangePicker } = DatePicker; const { TextArea } = Input; const { TabPane } = Tabs; +const getStatusColor = (status) => { + const colors = { + pending: 'orange', + in_progress: 'processing', + completed: 'green', + closed: 'default' + }; + return colors[status] || 'default'; +}; + +const getStatusText = (status) => { + const texts = { + pending: '待处理', + in_progress: '处理中', + completed: '已完成', + closed: '已关闭' + }; + return texts[status] || status; +}; + +const getPriorityColor = (priority) => { + const colors = { + low: 'green', + medium: 'orange', + high: 'red', + urgent: 'magenta' + }; + return colors[priority] || 'default'; +}; + +const getPriorityText = (priority) => { + const texts = { + low: '低', + medium: '中', + high: '高', + urgent: '紧急' + }; + return texts[priority] || priority; +}; + function TicketManagement() { const [tickets, setTickets] = useState([]); const [devices, setDevices] = useState([]); @@ -35,7 +75,7 @@ function TicketManagement() { const [searchFilters, setSearchFilters] = useState({}); - const fetchTickets = async (page = 1, pageSize = 10, filters = {}) => { + const fetchTickets = useCallback(async (page = 1, pageSize = 10, filters = {}) => { try { setLoading(true); const params = { @@ -56,27 +96,27 @@ function TicketManagement() { } finally { setLoading(false); } - }; + }, [searchFilters]); - const fetchDevices = async () => { + const fetchDevices = useCallback(async () => { try { const response = await axios.get('/api/devices', { params: { pageSize: 1000 } }); setDevices(response.data.devices || []); } catch (error) { console.error('获取设备列表失败:', error); } - }; + }, []); - const fetchCategories = async () => { + const fetchCategories = useCallback(async () => { try { const response = await axios.get('/api/ticket-categories'); setCategories(response.data || []); } catch (error) { console.error('获取分类列表失败:', error); } - }; + }, []); - const fetchTicketDetail = async (ticketId) => { + const fetchTicketDetail = useCallback(async (ticketId) => { try { const [ticketRes, operationsRes] = await Promise.all([ axios.get(`/api/tickets/${ticketId}`), @@ -90,15 +130,15 @@ function TicketManagement() { message.error('获取工单详情失败'); console.error('获取工单详情失败:', error); } - }; + }, []); useEffect(() => { fetchTickets(); fetchDevices(); fetchCategories(); - }, []); + }, [fetchTickets, fetchDevices, fetchCategories]); - const showModal = (ticket = null) => { + const showModal = useCallback((ticket = null) => { setEditingTicket(ticket); if (ticket) { const ticketData = { ...ticket }; @@ -113,14 +153,14 @@ function TicketManagement() { form.resetFields(); } setModalVisible(true); - }; + }, []); - const handleCancel = () => { + const handleCancel = useCallback(() => { setModalVisible(false); setEditingTicket(null); - }; + }, []); - const handleSubmit = async (values) => { + const handleSubmit = useCallback(async (values) => { try { const ticketData = { ...values, @@ -146,9 +186,9 @@ function TicketManagement() { message.error(editingTicket ? '工单更新失败' : '工单创建失败'); console.error(error); } - }; + }, [editingTicket, fetchTickets]); - const handleDelete = async (ticketId) => { + const handleDelete = useCallback(async (ticketId) => { Modal.confirm({ title: '确认删除', content: '确定要删除这个工单吗?', @@ -166,15 +206,15 @@ function TicketManagement() { } } }); - }; + }, [fetchTickets]); - const handleProcess = (ticket) => { + const handleProcess = useCallback((ticket) => { setSelectedTicket(ticket); processForm.resetFields(); setProcessingModalVisible(true); - }; + }, []); - const handleProcessSubmit = async (values) => { + const handleProcessSubmit = useCallback(async (values) => { try { await axios.put(`/api/tickets/${selectedTicket.ticketId}/process`, { ...values, @@ -188,9 +228,9 @@ function TicketManagement() { message.error('处理失败'); console.error(error); } - }; + }, [selectedTicket, fetchTickets]); - const handleStatusChange = async (ticketId, newStatus) => { + const handleStatusChange = useCallback(async (ticketId, newStatus) => { try { await axios.put(`/api/tickets/${ticketId}/status`, { status: newStatus, @@ -203,65 +243,25 @@ function TicketManagement() { message.error('状态更新失败'); console.error(error); } - }; + }, [fetchTickets]); - const handleSearch = (values) => { + const handleSearch = useCallback((values) => { setSearchFilters(values); fetchTickets(1, pagination.pageSize, values); - }; + }, [fetchTickets, pagination.pageSize]); - const handleReset = () => { + const handleReset = useCallback(() => { searchForm.resetFields(); setSearchFilters({}); fetchTickets(1, pagination.pageSize, {}); - }; + }, [fetchTickets, pagination.pageSize]); - const handleTableChange = (paginationInfo) => { + const handleTableChange = useCallback((paginationInfo) => { setPagination(paginationInfo); fetchTickets(paginationInfo.current, paginationInfo.pageSize, searchFilters); - }; + }, [fetchTickets, searchFilters]); - const getStatusColor = (status) => { - const colors = { - pending: 'orange', - in_progress: 'processing', - completed: 'green', - closed: 'default' - }; - return colors[status] || 'default'; - }; - - const getStatusText = (status) => { - const texts = { - pending: '待处理', - in_progress: '处理中', - completed: '已完成', - closed: '已关闭' - }; - return texts[status] || status; - }; - - const getPriorityColor = (priority) => { - const colors = { - low: 'green', - medium: 'orange', - high: 'red', - urgent: 'magenta' - }; - return colors[priority] || 'default'; - }; - - const getPriorityText = (priority) => { - const texts = { - low: '低', - medium: '中', - high: '高', - urgent: '紧急' - }; - return texts[priority] || priority; - }; - - const columns = [ + const columns = useMemo(() => [ { title: '工单编号', dataIndex: 'ticketId', @@ -404,7 +404,7 @@ function TicketManagement() { ) } - ]; + ], [fetchTicketDetail, handleStatusChange, handleDelete]); return (
@@ -651,4 +651,4 @@ function TicketManagement() { ); } -export default TicketManagement; +export default React.memo(TicketManagement); diff --git a/frontend/src/pages/TicketStatistics.jsx b/frontend/src/pages/TicketStatistics.jsx index cfdcb1f..b02a29a 100644 --- a/frontend/src/pages/TicketStatistics.jsx +++ b/frontend/src/pages/TicketStatistics.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Card, Row, Col, Statistic, Table, DatePicker, Select, Space, Tag, message } from 'antd'; import { BarChartOutlined, PieChartOutlined, RiseOutlined, FallOutlined, ClockCircleOutlined, CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; import axios from 'axios'; @@ -7,6 +7,48 @@ import dayjs from 'dayjs'; const { RangePicker } = DatePicker; const { Option } = Select; +const getStatusColor = (status) => { + const colors = { + pending: 'orange', + assigned: 'blue', + in_progress: 'processing', + completed: 'green', + closed: 'default' + }; + return colors[status] || 'default'; +}; + +const getStatusText = (status) => { + const texts = { + pending: '待处理', + assigned: '已分配', + in_progress: '处理中', + completed: '已完成', + closed: '已关闭' + }; + return texts[status] || status; +}; + +const getPriorityColor = (priority) => { + const colors = { + low: 'green', + medium: 'orange', + high: 'red', + urgent: 'magenta' + }; + return colors[priority] || 'default'; +}; + +const getPriorityText = (priority) => { + const texts = { + low: '低', + medium: '中', + high: '高', + urgent: '紧急' + }; + return texts[priority] || priority; +}; + function TicketStatistics() { const [loading, setLoading] = useState(true); const [dateRange, setDateRange] = useState([ @@ -27,7 +69,7 @@ function TicketStatistics() { trend: [] }); - const fetchStatistics = async () => { + const fetchStatistics = useCallback(async () => { try { setLoading(true); const params = { @@ -43,17 +85,17 @@ function TicketStatistics() { } finally { setLoading(false); } - }; + }, [dateRange]); useEffect(() => { fetchStatistics(); - }, [dateRange]); + }, [fetchStatistics]); - const handleDateChange = (dates) => { + const handleDateChange = useCallback((dates) => { if (dates) { setDateRange(dates); } - }; + }, []); const getStatusColor = (status) => { const colors = { @@ -97,7 +139,7 @@ function TicketStatistics() { return texts[priority] || priority; }; - const statusColumns = [ + const statusColumns = useMemo(() => [ { title: '状态', dataIndex: 'status', @@ -127,9 +169,9 @@ function TicketStatistics() { ) } - ]; + ], []); - const categoryColumns = [ + const categoryColumns = useMemo(() => [ { title: '故障分类', dataIndex: 'category', @@ -164,38 +206,9 @@ function TicketStatistics() { width: 150, render: (time) => time !== undefined && time !== null ? time.toFixed(1) : '-' } - ]; + ], []); - const deviceColumns = [ - { - title: '设备名称', - dataIndex: 'deviceName', - key: 'deviceName', - width: 180 - }, - { - title: '故障次数', - dataIndex: 'count', - key: 'count', - width: 100, - render: (count) => {count} - }, - { - title: '最后故障时间', - dataIndex: 'lastFaultTime', - key: 'lastFaultTime', - width: 160, - render: (text) => text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-' - }, - { - title: '设备类型', - dataIndex: 'deviceType', - key: 'deviceType', - width: 100 - } - ]; - - const priorityColumns = [ + const priorityColumns = useMemo(() => [ { title: '优先级', dataIndex: 'priority', @@ -228,7 +241,36 @@ function TicketStatistics() { width: 150, render: (time) => time !== undefined && time !== null ? time.toFixed(1) : '-' } - ]; + ], []); + + const deviceColumns = useMemo(() => [ + { + title: '设备名称', + dataIndex: 'deviceName', + key: 'deviceName', + width: 180 + }, + { + title: '故障次数', + dataIndex: 'count', + key: 'count', + width: 100, + render: (count) => {count} + }, + { + title: '最后故障时间', + dataIndex: 'lastFaultTime', + key: 'lastFaultTime', + width: 160, + render: (text) => text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-' + }, + { + title: '设备类型', + dataIndex: 'deviceType', + key: 'deviceType', + width: 100 + } + ], []); const simpleBarData = [ { name: '待处理', value: statistics.pending }, @@ -454,4 +496,4 @@ function TicketStatistics() { ); } -export default TicketStatistics; +export default React.memo(TicketStatistics); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 96f584f..74a0542 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -19,6 +19,30 @@ export default defineConfig({ }, build: { outDir: 'dist', - sourcemap: true + sourcemap: false, + rollupOptions: { + output: { + manualChunks: { + 'antd': ['antd', '@ant-design/icons'], + 'vendor': ['react', 'react-dom', 'react-router-dom'], + 'charts': ['axios', 'dayjs'], + 'utils': ['three', 'xlsx'] + }, + chunkFileNames: 'js/[name]-[hash].js', + entryFileNames: 'js/[name]-[hash].js', + assetFileNames: '[ext]/[name]-[hash].[ext]', + compact: true + } + }, + minify: 'terser', + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true + } + } + }, + optimizeDeps: { + include: ['antd', '@ant-design/icons', 'axios', 'react-router-dom'] } }); \ No newline at end of file