فهرست منبع

feat: 硬件监控仪表盘 - 完整项目

核心功能: 6类硬件采集器(CPU/内存/磁盘/GPU/网络/系统信息), REST API(8个端点)+WebSocket实时推送, H2时序数据存储7天自动清理, React18+Ant Design+ECharts仪表盘

本次修复: 磁盘采集器修复Windows下HWDiskStore与OSFileStore分区匹配失败导致使用率始终为0%的问题, IP地址脱敏(公网IP仅显示前两段), 磁盘S.M.A.R.T.检测, 采集频率30s/60s切换UI+后端动态调度, 实时刷新按钮(POST /api/v1/refresh), WebSocket连接状态指示器
肖天鹤 1 هفته پیش
کامیت
f3c4f40db2
55فایلهای تغییر یافته به همراه6757 افزوده شده و 0 حذف شده
  1. 30 0
      .gitignore
  2. 305 0
      README.md
  3. 13 0
      frontend/index.html
  4. 3389 0
      frontend/package-lock.json
  5. 30 0
      frontend/package.json
  6. 4 0
      frontend/public/vite.svg
  7. 29 0
      frontend/src/App.tsx
  8. 87 0
      frontend/src/components/CpuDetail/CpuDetail.tsx
  9. 194 0
      frontend/src/components/Dashboard/Dashboard.tsx
  10. 84 0
      frontend/src/components/DiskDetail/DiskDetail.tsx
  11. 95 0
      frontend/src/components/GpuDetail/GpuDetail.tsx
  12. 109 0
      frontend/src/components/History/History.tsx
  13. 162 0
      frontend/src/components/Layout/AppLayout.tsx
  14. 103 0
      frontend/src/components/MemoryDetail/MemoryDetail.tsx
  15. 89 0
      frontend/src/components/NetworkDetail/NetworkDetail.tsx
  16. 86 0
      frontend/src/components/SystemInfo/SystemInfo.tsx
  17. 34 0
      frontend/src/main.tsx
  18. 76 0
      frontend/src/services/api.ts
  19. 12 0
      frontend/src/store/metricsStore.ts
  20. 130 0
      frontend/src/types.ts
  21. 1 0
      frontend/src/vite-env.d.ts
  22. 21 0
      frontend/tsconfig.json
  23. 22 0
      frontend/vite.config.ts
  24. 121 0
      pom.xml
  25. 14 0
      src/main/java/com/hwmonitor/HwMonitorApplication.java
  26. 120 0
      src/main/java/com/hwmonitor/collector/CpuCollector.java
  27. 154 0
      src/main/java/com/hwmonitor/collector/DiskCollector.java
  28. 47 0
      src/main/java/com/hwmonitor/collector/GpuCollector.java
  29. 77 0
      src/main/java/com/hwmonitor/collector/MemoryCollector.java
  30. 118 0
      src/main/java/com/hwmonitor/collector/NetworkCollector.java
  31. 102 0
      src/main/java/com/hwmonitor/collector/SystemCollector.java
  32. 29 0
      src/main/java/com/hwmonitor/config/BrowserLauncher.java
  33. 29 0
      src/main/java/com/hwmonitor/config/CacheConfig.java
  34. 17 0
      src/main/java/com/hwmonitor/config/MonitorConfig.java
  35. 24 0
      src/main/java/com/hwmonitor/config/WebSocketConfig.java
  36. 39 0
      src/main/java/com/hwmonitor/controller/ConfigController.java
  37. 26 0
      src/main/java/com/hwmonitor/controller/HistoryController.java
  38. 65 0
      src/main/java/com/hwmonitor/controller/MetricsController.java
  39. 70 0
      src/main/java/com/hwmonitor/controller/MetricsWebSocketHandler.java
  40. 28 0
      src/main/java/com/hwmonitor/model/dto/CpuInfo.java
  41. 37 0
      src/main/java/com/hwmonitor/model/dto/DiskInfo.java
  42. 32 0
      src/main/java/com/hwmonitor/model/dto/GpuInfo.java
  43. 39 0
      src/main/java/com/hwmonitor/model/dto/MemoryInfo.java
  44. 34 0
      src/main/java/com/hwmonitor/model/dto/NetworkInfo.java
  45. 21 0
      src/main/java/com/hwmonitor/model/dto/OverviewInfo.java
  46. 28 0
      src/main/java/com/hwmonitor/model/dto/SystemInfo.java
  47. 39 0
      src/main/java/com/hwmonitor/model/entity/MetricRecord.java
  48. 19 0
      src/main/java/com/hwmonitor/repository/MetricRecordRepository.java
  49. 134 0
      src/main/java/com/hwmonitor/scheduler/MetricsScheduler.java
  50. 32 0
      src/main/java/com/hwmonitor/service/HistoryService.java
  51. 86 0
      src/main/java/com/hwmonitor/service/MetricsService.java
  52. 46 0
      src/main/resources/application.yml
  53. 8 0
      src/main/resources/static/assets/index-IcvV1nCx.js
  54. 13 0
      src/main/resources/static/index.html
  55. 4 0
      src/main/resources/static/vite.svg

+ 30 - 0
.gitignore

@@ -0,0 +1,30 @@
+# Maven
+target/
+*.jar
+*.war
+
+# IDE
+.idea/
+*.iml
+.vscode/
+.settings/
+.project
+.classpath
+
+# Node
+frontend/node_modules/
+frontend/dist/
+
+# Spring Boot
+*.log
+
+# H2 Database
+data/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Environment
+.env
+.env.local

+ 305 - 0
README.md

@@ -0,0 +1,305 @@
+# 网页端本机硬件设备基础信息监控系统
+
+## 1. 项目简介
+
+基于 **Java 21 + Spring Boot 3.4 + React 18** 的本机硬件监控系统,通过 OSHI 采集硬件指标,WebSocket 实时推送至前端仪表盘,支持历史数据持久化与趋势分析。
+
+### 技术栈
+
+| 层 | 技术 |
+|----|------|
+| 后端 | Java 21, Spring Boot 3.4.4, Spring WebSocket, Spring Data JPA, OSHI 6.6.5 |
+| 数据库 | H2 (嵌入式文件存储) |
+| 缓存 | Caffeine |
+| 前端 | React 18, TypeScript, Ant Design 5, ECharts 5, Zustand, Vite 6 |
+| 构建 | Maven, Vite |
+
+### 监控指标
+
+| 类别 | 指标 |
+|------|------|
+| CPU | 使用率、各核心负载、温度、频率、缓存 |
+| 内存 | 使用率、已用/可用/总量、交换分区 |
+| 磁盘 | 容量、读写速率、接口类型、S.M.A.R.T. 健康状态 |
+| GPU | 型号、显存、温度 |
+| 网络 | 上下行速率、接口详情、MAC/IP 脱敏 |
+| 系统 | 主板、BIOS、OS 版本 |
+
+---
+
+## 2. 使用说明
+
+### 2.1 启动方式
+
+#### 方式一:开发模式(前后端分离)
+
+```bash
+# 终端 1:启动后端
+cd hw-monitor
+mvn clean package -DskipTests
+java -jar target/hw-monitor-1.0.0.jar
+
+# 终端 2:启动前端开发服务器
+cd hw-monitor/frontend
+npm install
+npm run dev
+```
+
+访问 **http://127.0.0.1:3000** 查看仪表盘。
+
+#### 方式二:生产模式(前端打包进后端)
+
+```bash
+# 1. 构建前端
+cd hw-monitor/frontend
+npm install
+npm run build
+
+# 2. 复制构建产物到 static 目录
+cp -r dist/* ../src/main/resources/static/
+
+# 3. 打包并运行
+cd ..
+mvn clean package -DskipTests
+java -jar target/hw-monitor-1.0.0.jar
+```
+
+访问 **http://127.0.0.1:9988** 查看仪表盘。
+
+### 2.2 页面导航
+
+| 页面 | 路由 | 功能说明 |
+|------|------|----------|
+| 仪表盘总览 | `/` | 6 个核心指标卡片,异常告警高亮 |
+| CPU 详情 | `/cpu` | 各核心柱状图 + 温度 + 频率 |
+| 内存详情 | `/memory` | 环形图 + 交换分区 + 内存条表格 |
+| 磁盘详情 | `/disk` | 容量饼图 + 读写速率 + S.M.A.R.T. |
+| GPU 详情 | `/gpu` | 仪表盘 + 温度 + 显存 |
+| 网络详情 | `/network` | 实时速率对比 + 接口详情 |
+| 系统信息 | `/system` | 主板/BIOS/OS 静态信息 |
+| 历史趋势 | `/history` | 时间范围 + 指标类型筛选 |
+
+### 2.3 采集频率切换
+
+Header 右侧提供 **30 秒 / 60 秒** 两档切换按钮,点击即时生效,无需重启。
+
+### 2.4 告警阈值
+
+仪表盘卡片在以下情况自动红色高亮边框:
+
+| 指标 | 阈值 |
+|------|------|
+| CPU 使用率 | > 90% |
+| 内存使用率 | > 85% |
+| 磁盘使用率 | > 90% |
+| 温度 | > 80°C |
+
+### 2.5 REST API
+
+| 方法 | 端点 | 说明 |
+|------|------|------|
+| GET | `/api/v1/overview` | 获取所有硬件概览数据 |
+| GET | `/api/v1/cpu` | 获取 CPU 详情 |
+| GET | `/api/v1/memory` | 获取内存详情 |
+| GET | `/api/v1/disk` | 获取磁盘详情 |
+| GET | `/api/v1/gpu` | 获取 GPU 详情 |
+| GET | `/api/v1/network` | 获取网络详情 |
+| GET | `/api/v1/system` | 获取系统信息 |
+| GET | `/api/v1/history` | 获取历史数据(支持 `?start=&end=&category=` 筛选) |
+| GET | `/api/v1/config/interval` | 获取当前采集间隔 |
+| PUT | `/api/v1/config/interval` | 修改采集间隔(body: `{"intervalSeconds": 30}`) |
+| GET | `/api/v1/health` | 健康检查 |
+
+### 2.6 WebSocket
+
+- 地址:`ws://127.0.0.1:9988/ws/metrics`
+- 推送频率:与采集间隔一致(默认 30 秒)
+- 消息格式:
+
+```json
+{
+  "type": "metrics",
+  "timestamp": 1700000000000,
+  "data": {
+    "cpu": { "usagePercent": 23.0, "temperature": 45.0, ... },
+    "memory": { "usagePercent": 74.2, "usedBytes": 25125584896, ... },
+    "disk": { "partitions": [...] },
+    "gpu": { "name": "Intel(R) Arc(TM) 130T GPU", ... },
+    "network": { "interfaces": [...] },
+    "system": { "osName": "Windows 11", ... }
+  }
+}
+```
+
+---
+
+## 3. 部署说明
+
+### 3.1 环境要求
+
+| 依赖 | 版本 |
+|------|------|
+| JDK | 21+ |
+| Maven | 3.9+ |
+| Node.js | 18+ (仅前端开发) |
+| 操作系统 | Windows / Linux / macOS |
+
+### 3.2 配置参数
+
+所有可配置参数位于 `src/main/resources/application.yml`:
+
+```yaml
+server:
+  port: 9988                  # 监听端口
+  address: 127.0.0.1          # 仅监听本地,实现网络隔离
+
+spring:
+  threads:
+    virtual:
+      enabled: true           # 启用虚拟线程
+
+hwmonitor:
+  collect-interval-seconds: 30  # 采集间隔(30/60)
+  history:
+    retention-days: 7          # 历史数据保留天数
+  alert:
+    cpu-threshold: 90          # CPU 告警阈值 (%)
+    memory-threshold: 85       # 内存告警阈值 (%)
+    disk-threshold: 90         # 磁盘告警阈值 (%)
+    temp-threshold: 80         # 温度告警阈值 (°C)
+  security:
+    allowed-origins: http://localhost:9988
+    enable-token: false        # Token 认证(默认关闭)
+    read-only: true            # 只读模式
+```
+
+### 3.3 安全措施
+
+| 措施 | 说明 |
+|------|------|
+| 网络隔离 | 服务仅监听 `127.0.0.1`,不对外暴露 |
+| 只读原则 | 所有 API 均为 `GET` 请求(除配置修改外) |
+| 数据脱敏 | MAC 地址掩码、公网 IP 仅显示前两段、磁盘序列号脱敏 |
+| 日志安全 | 不输出敏感硬件信息 |
+| H2 Console | 已禁用 |
+| Actuator | 仅暴露 `/health` 端点 |
+
+### 3.4 数据存储
+
+- 数据库文件:`./data/hwmonitor.mv.db`
+- 历史数据每天凌晨 3:00 自动清理超过 7 天的记录
+- 若要重置数据,删除 `./data` 目录后重启即可
+
+### 3.5 系统服务部署(Windows)
+
+使用 WinSW 将 jar 注册为 Windows 服务:
+
+```xml
+<!-- hw-monitor-service.xml -->
+<service>
+  <id>hw-monitor</id>
+  <name>Hardware Monitor</name>
+  <description>本机硬件监控服务</description>
+  <executable>java</executable>
+  <arguments>-jar hw-monitor-1.0.0.jar</arguments>
+  <workingdirectory>D:\ideaFactory\hw-monitor</workingdirectory>
+  <log mode="roll-by-size">
+    <sizeThreshold>10240</sizeThreshold>
+    <keepFiles>8</keepFiles>
+  </log>
+</service>
+```
+
+```bash
+# 安装服务
+hw-monitor-service.exe install
+
+# 启动服务
+hw-monitor-service.exe start
+```
+
+### 3.6 系统服务部署(Linux)
+
+```ini
+# /etc/systemd/system/hw-monitor.service
+[Unit]
+Description=Hardware Monitor Service
+After=network.target
+
+[Service]
+Type=simple
+User=monitor
+WorkingDirectory=/opt/hw-monitor
+ExecStart=/usr/bin/java -jar /opt/hw-monitor/hw-monitor-1.0.0.jar
+Restart=on-failure
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+```
+
+```bash
+sudo systemctl daemon-reload
+sudo systemctl enable hw-monitor
+sudo systemctl start hw-monitor
+```
+
+---
+
+## 4. 项目结构
+
+```
+hw-monitor/
+├── src/main/java/com/hwmonitor/
+│   ├── HwMonitorApplication.java      # Spring Boot 启动类
+│   ├── collector/                      # 硬件采集器(6个)
+│   │   ├── CpuCollector.java
+│   │   ├── MemoryCollector.java
+│   │   ├── DiskCollector.java
+│   │   ├── GpuCollector.java
+│   │   ├── NetworkCollector.java
+│   │   └── SystemCollector.java
+│   ├── config/                         # 配置类
+│   │   ├── BrowserLauncher.java        # 启动自动打开浏览器
+│   │   ├── CacheConfig.java            # Caffeine 缓存
+│   │   ├── MonitorConfig.java          # 采集频率配置
+│   │   └── WebSocketConfig.java        # WebSocket 配置
+│   ├── controller/                     # 控制器
+│   │   ├── MetricsController.java      # REST API
+│   │   ├── ConfigController.java       # 配置 API
+│   │   ├── HistoryController.java      # 历史数据 API
+│   │   └── MetricsWebSocketHandler.java # WebSocket 处理器
+│   ├── model/                          # 数据模型
+│   │   ├── dto/                        # 7个 DTO
+│   │   └── entity/                     # 1个实体
+│   ├── repository/                     # JPA Repository
+│   ├── scheduler/                      # 定时调度
+│   └── service/                        # 业务服务
+├── src/main/resources/
+│   └── application.yml                 # 全局配置
+├── frontend/                           # 前端项目
+│   └── src/
+│       ├── components/                 # 页面组件(8个)
+│       ├── services/api.ts             # API 封装
+│       ├── store/metricsStore.ts       # 状态管理
+│       ├── types.ts                    # TypeScript 类型
+│       └── App.tsx                     # 根组件
+└── pom.xml                             # Maven 配置
+```
+
+## 5. 常见问题
+
+**Q: 启动后浏览器显示"连接断开"?**
+A: 等待 3-5 秒,WebSocket 会自动重连。若持续断开,检查后端是否正常启动在 9988 端口。
+
+**Q: 磁盘使用率显示 0%?**
+A: 某些磁盘分区信息需管理员权限才能获取使用量,以管理员身份运行可解决。
+
+**Q: GPU 信息显示 "--"?**
+A: OSHI 在某些平台无法获取 GPU 使用率,型号和显存信息正常即可。
+
+**Q: 如何修改采集频率?**
+A: 方式一:点击前端 Header 右侧 30秒/60秒 按钮即时切换;方式二:修改 `application.yml` 中的 `collect-interval-seconds` 配置后重启。
+
+**Q: 历史数据占用空间大吗?**
+A: 按默认 30 秒采集间隔,每条记录约 100 字节,7 天约产生 2MB 数据,可忽略不计。

+ 13 - 0
frontend/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>硬件监控仪表盘</title>
+  </head>
+  <body>
+    <div id="root"></div>
+    <script type="module" src="/src/main.tsx"></script>
+  </body>
+</html>

+ 3389 - 0
frontend/package-lock.json

@@ -0,0 +1,3389 @@
+{
+  "name": "hw-monitor-frontend",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "hw-monitor-frontend",
+      "version": "1.0.0",
+      "dependencies": {
+        "@ant-design/icons": "^5.5.0",
+        "@tanstack/react-query": "^5.60.0",
+        "antd": "^5.22.0",
+        "axios": "^1.7.0",
+        "echarts": "^5.5.0",
+        "echarts-for-react": "^3.0.2",
+        "react": "^18.3.1",
+        "react-dom": "^18.3.1",
+        "react-router-dom": "^6.28.0",
+        "zustand": "^5.0.0"
+      },
+      "devDependencies": {
+        "@types/react": "^18.3.12",
+        "@types/react-dom": "^18.3.1",
+        "@vitejs/plugin-react": "^4.3.4",
+        "typescript": "~5.6.0",
+        "vite": "^6.0.0"
+      }
+    },
+    "node_modules/@ant-design/colors": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz",
+      "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/fast-color": "^2.0.6"
+      }
+    },
+    "node_modules/@ant-design/cssinjs": {
+      "version": "1.24.0",
+      "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz",
+      "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "@emotion/hash": "^0.8.0",
+        "@emotion/unitless": "^0.7.5",
+        "classnames": "^2.3.1",
+        "csstype": "^3.1.3",
+        "rc-util": "^5.35.0",
+        "stylis": "^4.3.4"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0",
+        "react-dom": ">=16.0.0"
+      }
+    },
+    "node_modules/@ant-design/cssinjs-utils": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz",
+      "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/cssinjs": "^1.21.0",
+        "@babel/runtime": "^7.23.2",
+        "rc-util": "^5.38.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@ant-design/fast-color": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz",
+      "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.7"
+      },
+      "engines": {
+        "node": ">=8.x"
+      }
+    },
+    "node_modules/@ant-design/icons": {
+      "version": "5.6.1",
+      "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
+      "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^7.0.0",
+        "@ant-design/icons-svg": "^4.4.0",
+        "@babel/runtime": "^7.24.8",
+        "classnames": "^2.2.6",
+        "rc-util": "^5.31.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0",
+        "react-dom": ">=16.0.0"
+      }
+    },
+    "node_modules/@ant-design/icons-svg": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.5.0.tgz",
+      "integrity": "sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==",
+      "license": "MIT"
+    },
+    "node_modules/@ant-design/react-slick": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz",
+      "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.4",
+        "classnames": "^2.2.5",
+        "json2mq": "^0.2.0",
+        "resize-observer-polyfill": "^1.5.1",
+        "throttle-debounce": "^5.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+      "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+      "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+      "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/generator": "^7.29.7",
+        "@babel/helper-compilation-targets": "^7.29.7",
+        "@babel/helper-module-transforms": "^7.29.7",
+        "@babel/helpers": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/template": "^7.29.7",
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+      "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.8",
+        "@babel/types": "^7.29.8",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+      "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.29.7",
+        "@babel/helper-validator-option": "^7.29.7",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+      "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+      "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+      "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "@babel/traverse": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+      "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+      "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+      "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+      "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.8"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-self": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+      "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-source": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+      "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+      "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+      "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/parser": "^7.29.7",
+        "@babel/types": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
+      "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.7",
+        "@babel/generator": "^7.29.8",
+        "@babel/helper-globals": "^7.29.7",
+        "@babel/parser": "^7.29.8",
+        "@babel/template": "^7.29.7",
+        "@babel/types": "^7.29.8",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.8",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+      "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
+      "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
+      "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
+      "license": "MIT"
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+      "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+      "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+      "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+      "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+      "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+      "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+      "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+      "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+      "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+      "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+      "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+      "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+      "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+      "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+      "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+      "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+      "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+      "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+      "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+      "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+      "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
+      "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^22.20 || ^24.12 || >=25"
+      }
+    },
+    "node_modules/@rc-component/async-validator": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.2.tgz",
+      "integrity": "sha512-WYbrZSjzznU1ekD0qFq2qRxt309VoS61MTG5npnFQlKYcoy9IzU8T+ZCIhq5bGAXRbXysABFWTspicMfmWFwow==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.4"
+      },
+      "engines": {
+        "node": ">=14.x"
+      }
+    },
+    "node_modules/@rc-component/color-picker": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz",
+      "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/fast-color": "^2.0.6",
+        "@babel/runtime": "^7.23.6",
+        "classnames": "^2.2.6",
+        "rc-util": "^5.38.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/context": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz",
+      "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "rc-util": "^5.27.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/mini-decimal": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.4.tgz",
+      "integrity": "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      }
+    },
+    "node_modules/@rc-component/mutate-observer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz",
+      "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.24.4"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/portal": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz",
+      "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.24.4"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/qrcode": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.3.tgz",
+      "integrity": "sha512-aGv6alnn4HbDEsURzKP+jv13rbi1VxmAYfBNZr5GKF1iohMNWy5tAVoJ1E3cOvzMB1kbUPvCXchM6zSFlRGPhA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.7"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/tour": {
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz",
+      "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0",
+        "@rc-component/portal": "^1.0.0-9",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.24.4"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/trigger": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.1.tgz",
+      "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.23.2",
+        "@rc-component/portal": "^1.1.0",
+        "classnames": "^2.3.2",
+        "rc-motion": "^2.0.0",
+        "rc-resize-observer": "^1.3.1",
+        "rc-util": "^5.44.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@remix-run/router": {
+      "version": "1.23.3",
+      "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz",
+      "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-beta.27",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+      "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz",
+      "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz",
+      "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz",
+      "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz",
+      "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz",
+      "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz",
+      "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz",
+      "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz",
+      "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz",
+      "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz",
+      "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz",
+      "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz",
+      "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz",
+      "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz",
+      "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz",
+      "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz",
+      "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz",
+      "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz",
+      "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "glibc"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz",
+      "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "libc": [
+        "musl"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz",
+      "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz",
+      "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz",
+      "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz",
+      "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz",
+      "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz",
+      "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@tanstack/query-core": {
+      "version": "5.101.4",
+      "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz",
+      "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      }
+    },
+    "node_modules/@tanstack/react-query": {
+      "version": "5.101.4",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz",
+      "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==",
+      "license": "MIT",
+      "dependencies": {
+        "@tanstack/query-core": "5.101.4"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      },
+      "peerDependencies": {
+        "react": "^18 || ^19"
+      }
+    },
+    "node_modules/@types/babel__core": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7",
+        "@types/babel__generator": "*",
+        "@types/babel__template": "*",
+        "@types/babel__traverse": "*"
+      }
+    },
+    "node_modules/@types/babel__generator": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+      "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__template": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__traverse": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+      "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.2"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/prop-types": {
+      "version": "15.7.15",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+      "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/react": {
+      "version": "18.3.31",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
+      "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/prop-types": "*",
+        "csstype": "^3.2.2"
+      }
+    },
+    "node_modules/@types/react-dom": {
+      "version": "18.3.7",
+      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+      "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/react": "^18.0.0"
+      }
+    },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+      "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.28.0",
+        "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+        "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+        "@rolldown/pluginutils": "1.0.0-beta.27",
+        "@types/babel__core": "^7.20.5",
+        "react-refresh": "^0.17.0"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/antd": {
+      "version": "5.29.3",
+      "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz",
+      "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^7.2.1",
+        "@ant-design/cssinjs": "^1.23.0",
+        "@ant-design/cssinjs-utils": "^1.1.3",
+        "@ant-design/fast-color": "^2.0.6",
+        "@ant-design/icons": "^5.6.1",
+        "@ant-design/react-slick": "~1.1.2",
+        "@babel/runtime": "^7.26.0",
+        "@rc-component/color-picker": "~2.0.1",
+        "@rc-component/mutate-observer": "^1.1.0",
+        "@rc-component/qrcode": "~1.1.0",
+        "@rc-component/tour": "~1.15.1",
+        "@rc-component/trigger": "^2.3.0",
+        "classnames": "^2.5.1",
+        "copy-to-clipboard": "^3.3.3",
+        "dayjs": "^1.11.11",
+        "rc-cascader": "~3.34.0",
+        "rc-checkbox": "~3.5.0",
+        "rc-collapse": "~3.9.0",
+        "rc-dialog": "~9.6.0",
+        "rc-drawer": "~7.3.0",
+        "rc-dropdown": "~4.2.1",
+        "rc-field-form": "~2.7.1",
+        "rc-image": "~7.12.0",
+        "rc-input": "~1.8.0",
+        "rc-input-number": "~9.5.0",
+        "rc-mentions": "~2.20.0",
+        "rc-menu": "~9.16.1",
+        "rc-motion": "^2.9.5",
+        "rc-notification": "~5.6.4",
+        "rc-pagination": "~5.1.0",
+        "rc-picker": "~4.11.3",
+        "rc-progress": "~4.0.0",
+        "rc-rate": "~2.13.1",
+        "rc-resize-observer": "^1.4.3",
+        "rc-segmented": "~2.7.0",
+        "rc-select": "~14.16.8",
+        "rc-slider": "~11.1.9",
+        "rc-steps": "~6.0.1",
+        "rc-switch": "~4.1.0",
+        "rc-table": "~7.54.0",
+        "rc-tabs": "~15.7.0",
+        "rc-textarea": "~1.10.2",
+        "rc-tooltip": "~6.4.0",
+        "rc-tree": "~5.13.1",
+        "rc-tree-select": "~5.27.0",
+        "rc-upload": "~4.11.0",
+        "rc-util": "^5.44.4",
+        "scroll-into-view-if-needed": "^3.1.0",
+        "throttle-debounce": "^5.0.2"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/ant-design"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.19.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz",
+      "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.6",
+        "https-proxy-agent": "^5.0.1",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.11.13",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz",
+      "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.8",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
+      "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "baseline-browser-mapping": "^2.11.12",
+        "caniuse-lite": "^1.0.30001809",
+        "electron-to-chromium": "^1.5.402",
+        "node-releases": "^2.0.53",
+        "update-browserslist-db": "^1.3.0"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001809",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz",
+      "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/classnames": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+      "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+      "license": "MIT"
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/compute-scroll-into-view": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
+      "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
+      "license": "MIT"
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/copy-to-clipboard": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
+      "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
+      "license": "MIT",
+      "dependencies": {
+        "toggle-selection": "^1.0.6"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.21",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
+      "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/echarts": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
+      "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "2.3.0",
+        "zrender": "5.6.1"
+      }
+    },
+    "node_modules/echarts-for-react": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.6.tgz",
+      "integrity": "sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "size-sensor": "^1.0.1"
+      },
+      "peerDependencies": {
+        "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0",
+        "react": "^15.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.404",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.404.tgz",
+      "integrity": "sha512-3WJtd7/lVq2Jnuz6wed1l9+1ZD2u2Tet1/1NBc4Iedkmgbu+I7YuAqdAQ8T+VZtnwysMsAf3IqSq9D1gyZjA2g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+      "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.12",
+        "@esbuild/android-arm": "0.25.12",
+        "@esbuild/android-arm64": "0.25.12",
+        "@esbuild/android-x64": "0.25.12",
+        "@esbuild/darwin-arm64": "0.25.12",
+        "@esbuild/darwin-x64": "0.25.12",
+        "@esbuild/freebsd-arm64": "0.25.12",
+        "@esbuild/freebsd-x64": "0.25.12",
+        "@esbuild/linux-arm": "0.25.12",
+        "@esbuild/linux-arm64": "0.25.12",
+        "@esbuild/linux-ia32": "0.25.12",
+        "@esbuild/linux-loong64": "0.25.12",
+        "@esbuild/linux-mips64el": "0.25.12",
+        "@esbuild/linux-ppc64": "0.25.12",
+        "@esbuild/linux-riscv64": "0.25.12",
+        "@esbuild/linux-s390x": "0.25.12",
+        "@esbuild/linux-x64": "0.25.12",
+        "@esbuild/netbsd-arm64": "0.25.12",
+        "@esbuild/netbsd-x64": "0.25.12",
+        "@esbuild/openbsd-arm64": "0.25.12",
+        "@esbuild/openbsd-x64": "0.25.12",
+        "@esbuild/openharmony-arm64": "0.25.12",
+        "@esbuild/sunos-x64": "0.25.12",
+        "@esbuild/win32-arm64": "0.25.12",
+        "@esbuild/win32-ia32": "0.25.12",
+        "@esbuild/win32-x64": "0.25.12"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+      "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.4",
+        "mime-types": "^2.1.35"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json2mq": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
+      "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
+      "license": "MIT",
+      "dependencies": {
+        "string-convert": "^0.2.0"
+      }
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.18",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+      "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.53",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz",
+      "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+      "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.26",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+      "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.17",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/rc-cascader": {
+      "version": "3.34.0",
+      "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz",
+      "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.25.7",
+        "classnames": "^2.3.1",
+        "rc-select": "~14.16.2",
+        "rc-tree": "~5.13.0",
+        "rc-util": "^5.43.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-checkbox": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz",
+      "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.25.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-collapse": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz",
+      "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "2.x",
+        "rc-motion": "^2.3.4",
+        "rc-util": "^5.27.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-dialog": {
+      "version": "9.6.0",
+      "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz",
+      "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/portal": "^1.0.0-8",
+        "classnames": "^2.2.6",
+        "rc-motion": "^2.3.0",
+        "rc-util": "^5.21.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-drawer": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz",
+      "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.23.9",
+        "@rc-component/portal": "^1.1.1",
+        "classnames": "^2.2.6",
+        "rc-motion": "^2.6.1",
+        "rc-util": "^5.38.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-dropdown": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz",
+      "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.2.6",
+        "rc-util": "^5.44.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.11.0",
+        "react-dom": ">=16.11.0"
+      }
+    },
+    "node_modules/rc-field-form": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.1.tgz",
+      "integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0",
+        "@rc-component/async-validator": "^5.0.3",
+        "rc-util": "^5.32.2"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-image": {
+      "version": "7.12.0",
+      "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz",
+      "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "@rc-component/portal": "^1.0.2",
+        "classnames": "^2.2.6",
+        "rc-dialog": "~9.6.0",
+        "rc-motion": "^2.6.2",
+        "rc-util": "^5.34.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-input": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz",
+      "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "classnames": "^2.2.1",
+        "rc-util": "^5.18.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0",
+        "react-dom": ">=16.0.0"
+      }
+    },
+    "node_modules/rc-input-number": {
+      "version": "9.5.0",
+      "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz",
+      "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/mini-decimal": "^1.0.1",
+        "classnames": "^2.2.5",
+        "rc-input": "~1.8.0",
+        "rc-util": "^5.40.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-mentions": {
+      "version": "2.20.0",
+      "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz",
+      "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.22.5",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.2.6",
+        "rc-input": "~1.8.0",
+        "rc-menu": "~9.16.0",
+        "rc-textarea": "~1.10.0",
+        "rc-util": "^5.34.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-menu": {
+      "version": "9.16.1",
+      "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz",
+      "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "2.x",
+        "rc-motion": "^2.4.3",
+        "rc-overflow": "^1.3.1",
+        "rc-util": "^5.27.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-motion": {
+      "version": "2.9.5",
+      "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz",
+      "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "classnames": "^2.2.1",
+        "rc-util": "^5.44.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-notification": {
+      "version": "5.6.4",
+      "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz",
+      "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "2.x",
+        "rc-motion": "^2.9.0",
+        "rc-util": "^5.20.1"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-overflow": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.5.0.tgz",
+      "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "classnames": "^2.2.1",
+        "rc-resize-observer": "^1.0.0",
+        "rc-util": "^5.37.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-pagination": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz",
+      "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.38.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-picker": {
+      "version": "4.11.3",
+      "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz",
+      "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.7",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.2.1",
+        "rc-overflow": "^1.3.2",
+        "rc-resize-observer": "^1.4.0",
+        "rc-util": "^5.43.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "date-fns": ">= 2.x",
+        "dayjs": ">= 1.x",
+        "luxon": ">= 3.x",
+        "moment": ">= 2.x",
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      },
+      "peerDependenciesMeta": {
+        "date-fns": {
+          "optional": true
+        },
+        "dayjs": {
+          "optional": true
+        },
+        "luxon": {
+          "optional": true
+        },
+        "moment": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/rc-progress": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz",
+      "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.2.6",
+        "rc-util": "^5.16.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-rate": {
+      "version": "2.13.1",
+      "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz",
+      "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.2.5",
+        "rc-util": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-resize-observer": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz",
+      "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.20.7",
+        "classnames": "^2.2.1",
+        "rc-util": "^5.44.1",
+        "resize-observer-polyfill": "^1.5.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-segmented": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.1.tgz",
+      "integrity": "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "classnames": "^2.2.1",
+        "rc-motion": "^2.4.4",
+        "rc-util": "^5.17.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0",
+        "react-dom": ">=16.0.0"
+      }
+    },
+    "node_modules/rc-select": {
+      "version": "14.16.8",
+      "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz",
+      "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/trigger": "^2.1.1",
+        "classnames": "2.x",
+        "rc-motion": "^2.0.1",
+        "rc-overflow": "^1.3.1",
+        "rc-util": "^5.16.1",
+        "rc-virtual-list": "^3.5.2"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-dom": "*"
+      }
+    },
+    "node_modules/rc-slider": {
+      "version": "11.1.9",
+      "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz",
+      "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.2.5",
+        "rc-util": "^5.36.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-steps": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz",
+      "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.16.7",
+        "classnames": "^2.2.3",
+        "rc-util": "^5.16.1"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-switch": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz",
+      "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.21.0",
+        "classnames": "^2.2.1",
+        "rc-util": "^5.30.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-table": {
+      "version": "7.54.0",
+      "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.54.0.tgz",
+      "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/context": "^1.4.0",
+        "classnames": "^2.2.5",
+        "rc-resize-observer": "^1.1.0",
+        "rc-util": "^5.44.3",
+        "rc-virtual-list": "^3.14.2"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-tabs": {
+      "version": "15.7.0",
+      "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz",
+      "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "classnames": "2.x",
+        "rc-dropdown": "~4.2.0",
+        "rc-menu": "~9.16.0",
+        "rc-motion": "^2.6.2",
+        "rc-resize-observer": "^1.0.0",
+        "rc-util": "^5.34.1"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-textarea": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz",
+      "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.2.1",
+        "rc-input": "~1.8.0",
+        "rc-resize-observer": "^1.0.0",
+        "rc-util": "^5.27.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-tooltip": {
+      "version": "6.4.0",
+      "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz",
+      "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.3.1",
+        "rc-util": "^5.44.3"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-tree": {
+      "version": "5.13.1",
+      "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz",
+      "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "2.x",
+        "rc-motion": "^2.0.1",
+        "rc-util": "^5.16.1",
+        "rc-virtual-list": "^3.5.1"
+      },
+      "engines": {
+        "node": ">=10.x"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-dom": "*"
+      }
+    },
+    "node_modules/rc-tree-select": {
+      "version": "5.27.0",
+      "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz",
+      "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.25.7",
+        "classnames": "2.x",
+        "rc-select": "~14.16.2",
+        "rc-tree": "~5.13.0",
+        "rc-util": "^5.43.0"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-dom": "*"
+      }
+    },
+    "node_modules/rc-upload": {
+      "version": "4.11.0",
+      "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.11.0.tgz",
+      "integrity": "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "classnames": "^2.2.5",
+        "rc-util": "^5.2.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-util": {
+      "version": "5.44.4",
+      "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz",
+      "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "react-is": "^18.2.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-virtual-list": {
+      "version": "3.19.2",
+      "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz",
+      "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.20.0",
+        "classnames": "^2.2.6",
+        "rc-resize-observer": "^1.0.0",
+        "rc-util": "^5.36.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/react": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+      "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+      "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0",
+        "scheduler": "^0.23.2"
+      },
+      "peerDependencies": {
+        "react": "^18.3.1"
+      }
+    },
+    "node_modules/react-is": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+      "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+      "license": "MIT"
+    },
+    "node_modules/react-refresh": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+      "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-router": {
+      "version": "6.30.4",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz",
+      "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==",
+      "license": "MIT",
+      "dependencies": {
+        "@remix-run/router": "1.23.3"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.8"
+      }
+    },
+    "node_modules/react-router-dom": {
+      "version": "6.30.4",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz",
+      "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@remix-run/router": "1.23.3",
+        "react-router": "6.30.4"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.8",
+        "react-dom": ">=16.8"
+      }
+    },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+      "license": "MIT"
+    },
+    "node_modules/rollup": {
+      "version": "4.62.4",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
+      "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.9"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+        "@rollup/rollup-android-arm-eabi": "4.62.4",
+        "@rollup/rollup-android-arm64": "4.62.4",
+        "@rollup/rollup-darwin-arm64": "4.62.4",
+        "@rollup/rollup-darwin-x64": "4.62.4",
+        "@rollup/rollup-freebsd-arm64": "4.62.4",
+        "@rollup/rollup-freebsd-x64": "4.62.4",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.62.4",
+        "@rollup/rollup-linux-arm-musleabihf": "4.62.4",
+        "@rollup/rollup-linux-arm64-gnu": "4.62.4",
+        "@rollup/rollup-linux-arm64-musl": "4.62.4",
+        "@rollup/rollup-linux-loong64-gnu": "4.62.4",
+        "@rollup/rollup-linux-loong64-musl": "4.62.4",
+        "@rollup/rollup-linux-ppc64-gnu": "4.62.4",
+        "@rollup/rollup-linux-ppc64-musl": "4.62.4",
+        "@rollup/rollup-linux-riscv64-gnu": "4.62.4",
+        "@rollup/rollup-linux-riscv64-musl": "4.62.4",
+        "@rollup/rollup-linux-s390x-gnu": "4.62.4",
+        "@rollup/rollup-linux-x64-gnu": "4.62.4",
+        "@rollup/rollup-linux-x64-musl": "4.62.4",
+        "@rollup/rollup-openbsd-x64": "4.62.4",
+        "@rollup/rollup-openharmony-arm64": "4.62.4",
+        "@rollup/rollup-win32-arm64-msvc": "4.62.4",
+        "@rollup/rollup-win32-ia32-msvc": "4.62.4",
+        "@rollup/rollup-win32-x64-gnu": "4.62.4",
+        "@rollup/rollup-win32-x64-msvc": "4.62.4",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/scheduler": {
+      "version": "0.23.2",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+      "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      }
+    },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
+      "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
+      "license": "MIT",
+      "dependencies": {
+        "compute-scroll-into-view": "^3.0.2"
+      }
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/size-sensor": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.3.tgz",
+      "integrity": "sha512-+k9mJ2/rQMiRmQUcjn+qznch260leIXY8r4FyYKKyRBO/s5UoeMAHGkCJyE1R/4wrIhTJONfyloY55SkE7ve3A==",
+      "license": "ISC"
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/string-convert": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
+      "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
+      "license": "MIT"
+    },
+    "node_modules/stylis": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
+      "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
+      "license": "MIT"
+    },
+    "node_modules/throttle-debounce": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
+      "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.22"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.17",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+      "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/toggle-selection": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
+      "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
+      "license": "MIT"
+    },
+    "node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+      "license": "0BSD"
+    },
+    "node_modules/typescript": {
+      "version": "5.6.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+      "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz",
+      "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/vite": {
+      "version": "6.4.3",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+      "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.25.0",
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2",
+        "postcss": "^8.5.3",
+        "rollup": "^4.34.9",
+        "tinyglobby": "^0.2.13"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "jiti": ">=1.21.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/zrender": {
+      "version": "5.6.1",
+      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
+      "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tslib": "2.3.0"
+      }
+    },
+    "node_modules/zustand": {
+      "version": "5.0.14",
+      "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
+      "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.20.0"
+      },
+      "peerDependencies": {
+        "@types/react": ">=18.0.0",
+        "immer": ">=9.0.6",
+        "react": ">=18.0.0",
+        "use-sync-external-store": ">=1.2.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        },
+        "immer": {
+          "optional": true
+        },
+        "react": {
+          "optional": true
+        },
+        "use-sync-external-store": {
+          "optional": true
+        }
+      }
+    }
+  }
+}

+ 30 - 0
frontend/package.json

@@ -0,0 +1,30 @@
+{
+  "name": "hw-monitor-frontend",
+  "private": true,
+  "version": "1.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc -b && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "antd": "^5.22.0",
+    "@ant-design/icons": "^5.5.0",
+    "echarts": "^5.5.0",
+    "echarts-for-react": "^3.0.2",
+    "react": "^18.3.1",
+    "react-dom": "^18.3.1",
+    "react-router-dom": "^6.28.0",
+    "zustand": "^5.0.0",
+    "@tanstack/react-query": "^5.60.0",
+    "axios": "^1.7.0"
+  },
+  "devDependencies": {
+    "@types/react": "^18.3.12",
+    "@types/react-dom": "^18.3.1",
+    "@vitejs/plugin-react": "^4.3.4",
+    "typescript": "~5.6.0",
+    "vite": "^6.0.0"
+  }
+}

+ 4 - 0
frontend/public/vite.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
+  <rect width="100" height="100" rx="10" fill="#1677ff"/>
+  <text x="50" y="65" text-anchor="middle" fill="white" font-size="50" font-family="Arial">HW</text>
+</svg>

+ 29 - 0
frontend/src/App.tsx

@@ -0,0 +1,29 @@
+import { Routes, Route } from 'react-router-dom';
+import AppLayout from './components/Layout/AppLayout';
+import Dashboard from './components/Dashboard/Dashboard';
+import CpuDetail from './components/CpuDetail/CpuDetail';
+import MemoryDetail from './components/MemoryDetail/MemoryDetail';
+import DiskDetail from './components/DiskDetail/DiskDetail';
+import GpuDetail from './components/GpuDetail/GpuDetail';
+import NetworkDetail from './components/NetworkDetail/NetworkDetail';
+import SystemInfo from './components/SystemInfo/SystemInfo';
+import History from './components/History/History';
+
+function App() {
+  return (
+    <Routes>
+      <Route path="/" element={<AppLayout />}>
+        <Route index element={<Dashboard />} />
+        <Route path="cpu" element={<CpuDetail />} />
+        <Route path="memory" element={<MemoryDetail />} />
+        <Route path="disk" element={<DiskDetail />} />
+        <Route path="gpu" element={<GpuDetail />} />
+        <Route path="network" element={<NetworkDetail />} />
+        <Route path="system" element={<SystemInfo />} />
+        <Route path="history" element={<History />} />
+      </Route>
+    </Routes>
+  );
+}
+
+export default App;

+ 87 - 0
frontend/src/components/CpuDetail/CpuDetail.tsx

@@ -0,0 +1,87 @@
+import { Card, Row, Col, Statistic, Progress, Descriptions } from 'antd';
+import ReactECharts from 'echarts-for-react';
+import { useMetricsStore } from '../../store/metricsStore';
+
+export default function CpuDetail() {
+  const overview = useMetricsStore((s) => s.overview);
+  const cpu = overview?.cpu;
+
+  if (!cpu) {
+    return <Card>正在加载 CPU 数据...</Card>;
+  }
+
+  const coreChartOption = {
+    tooltip: { trigger: 'axis' },
+    grid: { left: 50, right: 20, top: 20, bottom: 40 },
+    xAxis: {
+      type: 'category',
+      data: cpu.coreUsagePercent?.map((_, i) => `核心 ${i}`) ?? [],
+      axisLabel: { rotate: 45 },
+    },
+    yAxis: { type: 'value', max: 100, axisLabel: { formatter: '{value}%' } },
+    series: [{
+      type: 'bar',
+      data: cpu.coreUsagePercent?.map((v) => ({ value: v, itemStyle: { color: v > 90 ? '#ff4d4f' : '#1677ff' } })) ?? [],
+    }],
+  };
+
+  return (
+    <Row gutter={[16, 16]}>
+      <Col span={24}>
+        <Card title="CPU 基本信息">
+          <Descriptions bordered size="small" column={{ xs: 1, sm: 2, lg: 3 }}>
+            <Descriptions.Item label="型号">{cpu.model}</Descriptions.Item>
+            <Descriptions.Item label="架构">{cpu.architecture}</Descriptions.Item>
+            <Descriptions.Item label="物理核心">{cpu.physicalCores}</Descriptions.Item>
+            <Descriptions.Item label="逻辑核心">{cpu.logicalCores}</Descriptions.Item>
+            <Descriptions.Item label="最大频率">{cpu.maxFreqMhz > 0 ? `${cpu.maxFreqMhz} MHz` : '未知'}</Descriptions.Item>
+            <Descriptions.Item label="L1 缓存">{cpu.cacheSizeL1 > 0 ? `${cpu.cacheSizeL1} B` : '未知'}</Descriptions.Item>
+            <Descriptions.Item label="L2 缓存">{cpu.cacheSizeL2 > 0 ? `${cpu.cacheSizeL2} B` : '未知'}</Descriptions.Item>
+            <Descriptions.Item label="L3 缓存">{cpu.cacheSizeL3 > 0 ? `${cpu.cacheSizeL3} B` : '未知'}</Descriptions.Item>
+          </Descriptions>
+        </Card>
+      </Col>
+      <Col xs={24} sm={12} lg={8}>
+        <Card>
+          <Statistic title="总使用率" value={cpu.usagePercent} suffix="%" precision={1} />
+          <Progress
+            percent={cpu.usagePercent}
+            status={cpu.usagePercent > 90 ? 'exception' : 'normal'}
+            style={{ marginTop: 8 }}
+          />
+        </Card>
+      </Col>
+      <Col xs={24} sm={12} lg={8}>
+        <Card>
+          <Statistic
+            title="温度"
+            value={cpu.temperature ?? '--'}
+            suffix={cpu.temperature != null ? '°C' : ''}
+          />
+          {cpu.temperature != null && (
+            <Progress
+              percent={Math.min((cpu.temperature / 100) * 100, 100)}
+              status={cpu.temperature > 80 ? 'exception' : 'normal'}
+              format={() => `${cpu.temperature}°C`}
+              style={{ marginTop: 8 }}
+            />
+          )}
+        </Card>
+      </Col>
+      <Col xs={24} sm={12} lg={8}>
+        <Card>
+          <Statistic
+            title="功耗"
+            value={cpu.powerWatts ?? '--'}
+            suffix={cpu.powerWatts != null ? 'W' : ''}
+          />
+        </Card>
+      </Col>
+      <Col span={24}>
+        <Card title="各核心使用率">
+          <ReactECharts option={coreChartOption} style={{ height: 350 }} />
+        </Card>
+      </Col>
+    </Row>
+  );
+}

+ 194 - 0
frontend/src/components/Dashboard/Dashboard.tsx

@@ -0,0 +1,194 @@
+import { Row, Col, Card, Statistic, Progress, Badge, Space, Button, message } from 'antd';
+import {
+  CloudServerOutlined,
+  DatabaseOutlined,
+  HddOutlined,
+  VideoCameraOutlined,
+  WifiOutlined,
+  ThunderboltOutlined,
+  ReloadOutlined,
+} from '@ant-design/icons';
+import { useMetricsStore } from '../../store/metricsStore';
+import { useNavigate } from 'react-router-dom';
+import { triggerRefresh } from '../../services/api';
+import { useState } from 'react';
+
+function formatBytes(bytes: number): string {
+  if (!bytes) return '0 B';
+  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+  let i = 0;
+  let val = bytes;
+  while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; }
+  return `${val.toFixed(1)} ${units[i]}`;
+}
+
+function formatSpeed(bytesPerSec: number): string {
+  if (!bytesPerSec) return '0 B/s';
+  const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
+  let i = 0;
+  let val = bytesPerSec;
+  while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; }
+  return `${val.toFixed(1)} ${units[i]}`;
+}
+
+export default function Dashboard() {
+  const overview = useMetricsStore((s) => s.overview);
+  const navigate = useNavigate();
+  const [refreshing, setRefreshing] = useState(false);
+
+  const handleRefresh = async () => {
+    setRefreshing(true);
+    try {
+      await triggerRefresh();
+      message.success('数据已刷新');
+    } catch {
+      message.error('刷新失败');
+    } finally {
+      setRefreshing(false);
+    }
+  };
+
+  if (!overview) {
+    return (
+      <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '60vh' }}>
+        <Card>正在连接硬件采集服务,请稍候...</Card>
+      </div>
+    );
+  }
+
+  const { cpu, memory, disk, gpu, network, alerts } = overview;
+  const cpuTemp = cpu?.temperature;
+  const cpuUsage = cpu?.usagePercent ?? 0;
+  const memUsage = memory?.usagePercent ?? 0;
+  const diskPartition = disk?.partitions?.[0];
+  const diskUsage = diskPartition?.usagePercent ?? 0;
+  const gpuDevice = gpu?.devices?.[0];
+  const netInterface = network?.interfaces?.find((n) => n.downloadBytesPerSec > 0 || n.uploadBytesPerSec > 0);
+
+  return (
+    <div>
+      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
+        <h2 style={{ margin: 0 }}>硬件监控</h2>
+        <Button
+          icon={<ReloadOutlined spin={refreshing} />}
+          onClick={handleRefresh}
+          loading={refreshing}
+          type="primary"
+          ghost
+        >
+          实时刷新
+        </Button>
+      </div>
+      <Row gutter={[16, 16]}>
+        {/* CPU 卡片 */}
+        <Col xs={24} sm={12} lg={8}>
+          <Card
+            hoverable
+            onClick={() => navigate('/cpu')}
+            style={{ border: alerts?.cpuHigh ? '2px solid #ff4d4f' : undefined }}
+          >
+            <Space>
+              <CloudServerOutlined style={{ fontSize: 28, color: '#1677ff' }} />
+              <Statistic title="CPU 使用率" value={cpuUsage} suffix="%" precision={1} />
+            </Space>
+            <Progress percent={cpuUsage} status={alerts?.cpuHigh ? 'exception' : 'normal'} size="small" style={{ marginTop: 8 }} />
+            <div style={{ marginTop: 8, color: '#8c8c8c', fontSize: 12 }}>
+              {cpu?.model?.substring(0, 40)} | {cpu?.physicalCores}C/{cpu?.logicalCores}T
+              {cpuTemp != null && ` | ${cpuTemp}°C`}
+            </div>
+          </Card>
+        </Col>
+
+        {/* 内存卡片 */}
+        <Col xs={24} sm={12} lg={8}>
+          <Card
+            hoverable
+            onClick={() => navigate('/memory')}
+            style={{ border: alerts?.memoryHigh ? '2px solid #ff4d4f' : undefined }}
+          >
+            <Space>
+              <DatabaseOutlined style={{ fontSize: 28, color: '#52c41a' }} />
+              <Statistic title="内存使用率" value={memUsage} suffix="%" precision={1} />
+            </Space>
+            <Progress percent={memUsage} status={alerts?.memoryHigh ? 'exception' : 'normal'} size="small" style={{ marginTop: 8 }} />
+            <div style={{ marginTop: 8, color: '#8c8c8c', fontSize: 12 }}>
+              {formatBytes(memory?.usedBytes ?? 0)} / {formatBytes(memory?.totalBytes ?? 0)}
+            </div>
+          </Card>
+        </Col>
+
+        {/* 磁盘卡片 */}
+        <Col xs={24} sm={12} lg={8}>
+          <Card
+            hoverable
+            onClick={() => navigate('/disk')}
+            style={{ border: alerts?.diskHigh ? '2px solid #ff4d4f' : undefined }}
+          >
+            <Space>
+              <HddOutlined style={{ fontSize: 28, color: '#faad14' }} />
+              <Statistic title="磁盘使用率" value={diskUsage} suffix="%" precision={1} />
+            </Space>
+            <Progress percent={diskUsage} status={alerts?.diskHigh ? 'exception' : 'normal'} size="small" style={{ marginTop: 8 }} />
+            <div style={{ marginTop: 8, color: '#8c8c8c', fontSize: 12 }}>
+              {diskPartition?.model ?? '未知'} | {formatBytes(diskPartition?.totalBytes ?? 0)}
+            </div>
+          </Card>
+        </Col>
+
+        {/* GPU 卡片 */}
+        <Col xs={24} sm={12} lg={8}>
+          <Card hoverable onClick={() => navigate('/gpu')}>
+            <Space>
+              <VideoCameraOutlined style={{ fontSize: 28, color: '#722ed1' }} />
+              <Statistic
+                title="GPU"
+                value={gpuDevice?.usagePercent ?? '--'}
+                suffix={gpuDevice?.usagePercent != null ? '%' : ''}
+              />
+            </Space>
+            <div style={{ marginTop: 8, color: '#8c8c8c', fontSize: 12 }}>
+              {gpuDevice?.name ?? '无 GPU 信息'}
+              {gpuDevice?.temperature != null && ` | ${gpuDevice.temperature}°C`}
+            </div>
+          </Card>
+        </Col>
+
+        {/* 网络卡片 */}
+        <Col xs={24} sm={12} lg={8}>
+          <Card hoverable onClick={() => navigate('/network')}>
+            <Space>
+              <WifiOutlined style={{ fontSize: 28, color: '#13c2c2' }} />
+              <div>
+                <div style={{ fontSize: 14, color: '#8c8c8c' }}>网络速率</div>
+                <div style={{ fontSize: 12 }}>
+                  <span style={{ color: '#52c41a' }}>↓ {formatSpeed(netInterface?.downloadBytesPerSec ?? 0)}</span>
+                  {' '}
+                  <span style={{ color: '#1677ff' }}>↑ {formatSpeed(netInterface?.uploadBytesPerSec ?? 0)}</span>
+                </div>
+              </div>
+            </Space>
+            <div style={{ marginTop: 8, color: '#8c8c8c', fontSize: 12 }}>
+              {network?.interfaces?.length ?? 0} 个网络接口
+            </div>
+          </Card>
+        </Col>
+
+        {/* 告警卡片 */}
+        <Col xs={24} sm={12} lg={8}>
+          <Card hoverable onClick={() => navigate('/system')}>
+            <Space>
+              <ThunderboltOutlined style={{ fontSize: 28, color: '#eb2f96' }} />
+              <div>
+                <div style={{ fontSize: 14, color: '#8c8c8c' }}>系统状态</div>
+                <Badge
+                  status={Object.values(alerts ?? {}).some(Boolean) ? 'error' : 'success'}
+                  text={Object.values(alerts ?? {}).some(Boolean) ? '有告警' : '正常'}
+                />
+              </div>
+            </Space>
+          </Card>
+        </Col>
+      </Row>
+    </div>
+  );
+}

+ 84 - 0
frontend/src/components/DiskDetail/DiskDetail.tsx

@@ -0,0 +1,84 @@
+import { Card, Row, Col, Statistic, Progress, Table } from 'antd';
+import ReactECharts from 'echarts-for-react';
+import { useMetricsStore } from '../../store/metricsStore';
+
+function formatBytes(bytes: number): string {
+  if (!bytes) return '0 B';
+  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+  let i = 0, val = bytes;
+  while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; }
+  return `${val.toFixed(1)} ${units[i]}`;
+}
+
+function formatSpeed(bytesPerSec: number): string {
+  if (!bytesPerSec) return '0 B/s';
+  const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
+  let i = 0, val = bytesPerSec;
+  while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; }
+  return `${val.toFixed(1)} ${units[i]}`;
+}
+
+export default function DiskDetail() {
+  const overview = useMetricsStore((s) => s.overview);
+  const disk = overview?.disk;
+
+  if (!disk?.partitions?.length) {
+    return <Card>正在加载磁盘数据...</Card>;
+  }
+
+  const pieChartOption = {
+    tooltip: { trigger: 'item' },
+    series: [{
+      type: 'pie',
+      radius: ['40%', '70%'],
+      label: { formatter: '{b}\n{d}%' },
+      data: disk.partitions.map((p) => ({
+        name: p.name,
+        value: p.totalBytes,
+      })),
+    }],
+  };
+
+  const columns = [
+    { title: '名称', dataIndex: 'name', key: 'name' },
+    { title: '型号', dataIndex: 'model', key: 'model' },
+    { title: '总容量', dataIndex: 'totalBytes', key: 'totalBytes', render: (v: number) => formatBytes(v) },
+    { title: '已用', dataIndex: 'usedBytes', key: 'usedBytes', render: (v: number) => formatBytes(v) },
+    { title: '可用', dataIndex: 'availableBytes', key: 'availableBytes', render: (v: number) => formatBytes(v) },
+    {
+      title: '使用率', dataIndex: 'usagePercent', key: 'usagePercent',
+      render: (v: number) => (
+        <Progress percent={v} size="small" status={v > 90 ? 'exception' : 'normal'} />
+      ),
+    },
+    {
+      title: '读速率', dataIndex: 'readBytesPerSec', key: 'readBytesPerSec',
+      render: (v: number) => formatSpeed(v),
+    },
+    {
+      title: '写速率', dataIndex: 'writeBytesPerSec', key: 'writeBytesPerSec',
+      render: (v: number) => formatSpeed(v),
+    },
+  ];
+
+  return (
+    <Row gutter={[16, 16]}>
+      <Col span={24}>
+        <Card title="磁盘容量分布">
+          <ReactECharts option={pieChartOption} style={{ height: 350 }} />
+        </Card>
+      </Col>
+      <Col span={24}>
+        <Card title="磁盘分区详情">
+          <Table
+            dataSource={disk.partitions.map((p, i) => ({ ...p, key: i }))}
+            columns={columns}
+            pagination={false}
+            size="small"
+            scroll={{ x: 900 }}
+          />
+        </Card>
+      </Col>
+    </Row>
+  );
+}

+ 95 - 0
frontend/src/components/GpuDetail/GpuDetail.tsx

@@ -0,0 +1,95 @@
+import { Card, Row, Col, Statistic, Progress, Descriptions, Empty } from 'antd';
+import ReactECharts from 'echarts-for-react';
+import { useMetricsStore } from '../../store/metricsStore';
+
+function formatBytes(bytes: number): string {
+  if (!bytes) return '0 B';
+  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+  let i = 0, val = bytes;
+  while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; }
+  return `${val.toFixed(1)} ${units[i]}`;
+}
+
+export default function GpuDetail() {
+  const overview = useMetricsStore((s) => s.overview);
+  const gpu = overview?.gpu;
+
+  if (!gpu?.devices?.length) {
+    return (
+      <Card>
+        <Empty description="未检测到 GPU 信息" />
+      </Card>
+    );
+  }
+
+  const gpuDevice = gpu.devices[0];
+
+  const gaugeOption = {
+    series: [{
+      type: 'gauge',
+      startAngle: 200, endAngle: -20,
+      min: 0, max: 100,
+      progress: { show: true, width: 12 },
+      axisLine: { lineStyle: { width: 12 } },
+      axisTick: { show: false },
+      splitLine: { show: false },
+      axisLabel: { show: false },
+      detail: { formatter: '{value}%', fontSize: 20 },
+      data: [{ value: gpuDevice.usagePercent ?? 0 }],
+    }],
+  };
+
+  const tempOption = {
+    series: [{
+      type: 'gauge',
+      startAngle: 200, endAngle: -20,
+      min: 0, max: 100,
+      progress: { show: true, width: 12 },
+      axisLine: { lineStyle: { width: 12 } },
+      axisTick: { show: false },
+      splitLine: { show: false },
+      axisLabel: { show: false },
+      detail: { formatter: '{value}°C', fontSize: 20 },
+      data: [{ value: gpuDevice.temperature ?? 0 }],
+    }],
+  };
+
+  return (
+    <Row gutter={[16, 16]}>
+      <Col span={24}>
+        <Card title="GPU 基本信息">
+          <Descriptions bordered size="small" column={{ xs: 1, sm: 2, lg: 3 }}>
+            <Descriptions.Item label="名称">{gpuDevice.name}</Descriptions.Item>
+            <Descriptions.Item label="厂商">{gpuDevice.vendor}</Descriptions.Item>
+            <Descriptions.Item label="显存容量">{formatBytes(gpuDevice.vramTotalBytes)}</Descriptions.Item>
+            <Descriptions.Item label="驱动版本">{gpuDevice.driverVersion}</Descriptions.Item>
+          </Descriptions>
+        </Card>
+      </Col>
+      <Col xs={24} lg={8}>
+        <Card title="GPU 使用率">
+          <ReactECharts option={gaugeOption} style={{ height: 250 }} />
+        </Card>
+      </Col>
+      <Col xs={24} lg={8}>
+        <Card title="GPU 温度">
+          <ReactECharts option={tempOption} style={{ height: 250 }} />
+        </Card>
+      </Col>
+      <Col xs={24} lg={8}>
+        <Card>
+          <Statistic title="显存占用" value={formatBytes(gpuDevice.vramUsedBytes)} />
+          <Progress
+            percent={gpuDevice.vramTotalBytes > 0 ? (gpuDevice.vramUsedBytes / gpuDevice.vramTotalBytes * 100) : 0}
+            style={{ marginTop: 8 }}
+          />
+          {gpuDevice.fanSpeedRpm?.length > 0 && (
+            <div style={{ marginTop: 16 }}>
+              <Statistic title="风扇转速" value={gpuDevice.fanSpeedRpm.join(', ')} suffix="RPM" />
+            </div>
+          )}
+        </Card>
+      </Col>
+    </Row>
+  );
+}

+ 109 - 0
frontend/src/components/History/History.tsx

@@ -0,0 +1,109 @@
+import { useState, useEffect } from 'react';
+import { Card, Row, Col, Select, DatePicker, Spin, Empty } from 'antd';
+import ReactECharts from 'echarts-for-react';
+import { fetchHistory } from '../../services/api';
+import type { MetricRecord } from '../../types';
+
+const { RangePicker } = DatePicker;
+
+const categoryOptions = [
+  { label: 'CPU 使用率', value: 'cpu' },
+  { label: '内存使用率', value: 'memory' },
+  { label: '磁盘', value: 'disk' },
+  { label: '网络', value: 'network' },
+];
+
+export default function History() {
+  const [category, setCategory] = useState('cpu');
+  const [dateRange, setDateRange] = useState<[number, number]>([
+    Date.now() - 3600000,
+    Date.now(),
+  ]);
+  const [records, setRecords] = useState<MetricRecord[]>([]);
+  const [loading, setLoading] = useState(false);
+
+  useEffect(() => {
+    if (!dateRange[0] || !dateRange[1]) return;
+    setLoading(true);
+    fetchHistory(category, dateRange[0], dateRange[1])
+      .then(setRecords)
+      .catch(() => setRecords([]))
+      .finally(() => setLoading(false));
+  }, [category, dateRange]);
+
+  // 按 metricKey 分组
+  const grouped = records.reduce<Record<string, MetricRecord[]>>((acc, r) => {
+    if (!acc[r.metricKey]) acc[r.metricKey] = [];
+    acc[r.metricKey].push(r);
+    return acc;
+  }, {});
+
+  const chartOption = {
+    tooltip: { trigger: 'axis' },
+    legend: {
+      data: Object.keys(grouped),
+      textStyle: { color: '#8c8c8c' },
+    },
+    grid: { left: 60, right: 20, top: 40, bottom: 40 },
+    xAxis: {
+      type: 'time',
+      axisLabel: { formatter: '{HH}:{mm}:{ss}' },
+    },
+    yAxis: {
+      type: 'value',
+      axisLabel: { formatter: '{value}' },
+    },
+    series: Object.entries(grouped).map(([key, data]) => ({
+      name: key,
+      type: 'line',
+      smooth: true,
+      data: data.map((r) => [r.timestamp, r.value]),
+      connectNulls: true,
+    })),
+  };
+
+  return (
+    <Row gutter={[16, 16]}>
+      <Col span={24}>
+        <Card>
+          <Row gutter={16} align="middle">
+            <Col>
+              <span style={{ marginRight: 8 }}>指标类型:</span>
+              <Select
+                value={category}
+                onChange={setCategory}
+                options={categoryOptions}
+                style={{ width: 160 }}
+              />
+            </Col>
+            <Col>
+              <span style={{ marginRight: 8 }}>时间范围:</span>
+              <RangePicker
+                showTime
+                onChange={(_, dateStrings) => {
+                  if (dateStrings[0] && dateStrings[1]) {
+                    setDateRange([
+                      new Date(dateStrings[0]).getTime(),
+                      new Date(dateStrings[1]).getTime(),
+                    ]);
+                  }
+                }}
+              />
+            </Col>
+          </Row>
+        </Card>
+      </Col>
+      <Col span={24}>
+        <Card title="历史趋势图">
+          {loading ? (
+            <Spin style={{ display: 'block', margin: '60px auto' }} />
+          ) : records.length > 0 ? (
+            <ReactECharts option={chartOption} style={{ height: 450 }} />
+          ) : (
+            <Empty description="暂无历史数据" />
+          )}
+        </Card>
+      </Col>
+    </Row>
+  );
+}

+ 162 - 0
frontend/src/components/Layout/AppLayout.tsx

@@ -0,0 +1,162 @@
+import { useState, useEffect } from 'react';
+import { Outlet, useNavigate, useLocation } from 'react-router-dom';
+import { Layout, Menu, theme, Radio, message, Badge } from 'antd';
+import {
+  DashboardOutlined,
+  CloudServerOutlined,
+  DatabaseOutlined,
+  HddOutlined,
+  VideoCameraOutlined,
+  WifiOutlined,
+  DesktopOutlined,
+  HistoryOutlined,
+  ClockCircleOutlined,
+} from '@ant-design/icons';
+import { useMetricsStore } from '../../store/metricsStore';
+import { fetchInterval, updateInterval } from '../../services/api';
+import type { OverviewInfo } from '../../types';
+
+const { Header, Sider, Content } = Layout;
+
+const menuItems = [
+  { key: '/', icon: <DashboardOutlined />, label: '仪表盘总览' },
+  { key: '/cpu', icon: <CloudServerOutlined />, label: 'CPU 详情' },
+  { key: '/memory', icon: <DatabaseOutlined />, label: '内存详情' },
+  { key: '/disk', icon: <HddOutlined />, label: '磁盘详情' },
+  { key: '/gpu', icon: <VideoCameraOutlined />, label: 'GPU 详情' },
+  { key: '/network', icon: <WifiOutlined />, label: '网络详情' },
+  { key: '/system', icon: <DesktopOutlined />, label: '系统信息' },
+  { key: '/history', icon: <HistoryOutlined />, label: '历史趋势' },
+];
+
+export default function AppLayout() {
+  const [collapsed, setCollapsed] = useState(false);
+  const [interval, setInterval] = useState(30);
+  const [wsConnected, setWsConnected] = useState(false);
+  const navigate = useNavigate();
+  const location = useLocation();
+  const { token: themeToken } = theme.useToken();
+  const setOverview = useMetricsStore((s) => s.setOverview);
+
+  // 加载当前采集间隔
+  useEffect(() => {
+    fetchInterval()
+      .then((res) => setInterval(res.intervalSeconds))
+      .catch(() => {});
+  }, []);
+
+  const handleIntervalChange = async (value: number) => {
+    try {
+      await updateInterval(value);
+      setInterval(value);
+      message.success(`采集间隔已切换为 ${value} 秒`);
+    } catch {
+      message.error('切换采集间隔失败');
+    }
+  };
+
+  useEffect(() => {
+    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+    const wsUrl = import.meta.env.DEV
+      ? `${protocol}//127.0.0.1:9988/ws/metrics`
+      : `${protocol}//127.0.0.1:9988/ws/metrics`;
+
+    let ws: WebSocket | null = null;
+    let reconnectTimer: ReturnType<typeof setTimeout>;
+
+    function connect() {
+      ws = new WebSocket(wsUrl);
+      ws.onopen = () => {
+        console.log('WebSocket 已连接');
+        setWsConnected(true);
+      };
+      ws.onmessage = (event) => {
+        try {
+          const msg = JSON.parse(event.data);
+          if (msg.type === 'metrics' && msg.data) {
+            setOverview(msg.data as OverviewInfo);
+          }
+        } catch (e) {
+          console.error('WebSocket 消息解析失败', e);
+        }
+      };
+      ws.onclose = () => {
+        console.log('WebSocket 已断开,3秒后重连');
+        setWsConnected(false);
+        reconnectTimer = setTimeout(connect, 3000);
+      };
+      ws.onerror = () => ws?.close();
+    }
+
+    connect();
+
+    return () => {
+      ws?.close();
+      clearTimeout(reconnectTimer);
+    };
+  }, [setOverview]);
+
+  return (
+    <Layout style={{ minHeight: '100vh' }}>
+      <Sider
+        collapsible
+        collapsed={collapsed}
+        onCollapse={setCollapsed}
+        theme="dark"
+        style={{ borderRight: `1px solid ${themeToken.colorBorderSecondary}` }}
+      >
+        <div style={{
+          height: 48, display: 'flex', alignItems: 'center', justifyContent: 'center',
+          color: '#fff', fontWeight: 700, fontSize: collapsed ? 14 : 18,
+          borderBottom: `1px solid ${themeToken.colorBorderSecondary}`,
+        }}>
+          {collapsed ? 'HW' : '硬件监控'}
+        </div>
+        <Menu
+          theme="dark"
+          mode="inline"
+          selectedKeys={[location.pathname]}
+          items={menuItems}
+          onClick={({ key }) => navigate(key)}
+        />
+      </Sider>
+      <Layout>
+        <Header style={{
+          background: themeToken.colorBgContainer,
+          padding: '0 24px',
+          borderBottom: `1px solid ${themeToken.colorBorderSecondary}`,
+          display: 'flex',
+          alignItems: 'center',
+          justifyContent: 'space-between',
+          fontSize: 16,
+          fontWeight: 600,
+        }}>
+          <span>网页端本机硬件设备基础信息监控系统</span>
+          <div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
+            <Badge
+              status={wsConnected ? 'success' : 'error'}
+              text={wsConnected ? '实时连接' : '连接断开'}
+            />
+            <Radio.Group
+              value={interval}
+              onChange={(e) => handleIntervalChange(e.target.value)}
+              optionType="button"
+              buttonStyle="solid"
+              size="small"
+            >
+              <Radio.Button value={30}>
+                <ClockCircleOutlined /> 30秒
+              </Radio.Button>
+              <Radio.Button value={60}>
+                <ClockCircleOutlined /> 60秒
+              </Radio.Button>
+            </Radio.Group>
+          </div>
+        </Header>
+        <Content style={{ margin: 16, overflow: 'auto' }}>
+          <Outlet />
+        </Content>
+      </Layout>
+    </Layout>
+  );
+}

+ 103 - 0
frontend/src/components/MemoryDetail/MemoryDetail.tsx

@@ -0,0 +1,103 @@
+import { Card, Row, Col, Statistic, Progress, Descriptions, Table } from 'antd';
+import ReactECharts from 'echarts-for-react';
+import { useMetricsStore } from '../../store/metricsStore';
+
+function formatBytes(bytes: number): string {
+  if (!bytes) return '0 B';
+  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+  let i = 0, val = bytes;
+  while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; }
+  return `${val.toFixed(1)} ${units[i]}`;
+}
+
+export default function MemoryDetail() {
+  const overview = useMetricsStore((s) => s.overview);
+  const memory = overview?.memory;
+
+  if (!memory) {
+    return <Card>正在加载内存数据...</Card>;
+  }
+
+  const ringChartOption = {
+    tooltip: { trigger: 'item' },
+    series: [{
+      type: 'pie',
+      radius: ['50%', '75%'],
+      center: ['50%', '50%'],
+      label: { show: false },
+      data: [
+        { value: memory.usedBytes, name: '已用', itemStyle: { color: '#1677ff' } },
+        { value: memory.availableBytes, name: '可用', itemStyle: { color: '#52c41a' } },
+      ],
+    }],
+  };
+
+  const swapChartOption = {
+    tooltip: { trigger: 'item' },
+    series: [{
+      type: 'pie',
+      radius: ['50%', '75%'],
+      center: ['50%', '50%'],
+      label: { show: false },
+      data: [
+        { value: memory.swapUsedBytes, name: '已用', itemStyle: { color: '#faad14' } },
+        { value: memory.swapTotalBytes - memory.swapUsedBytes, name: '可用', itemStyle: { color: '#13c2c2' } },
+      ],
+    }],
+  };
+
+  const moduleColumns = [
+    { title: '插槽', dataIndex: 'locator', key: 'locator' },
+    { title: '制造商', dataIndex: 'manufacturer', key: 'manufacturer' },
+    { title: '容量', dataIndex: 'capacityBytes', key: 'capacityBytes', render: (v: number) => formatBytes(v) },
+    { title: '类型', dataIndex: 'type', key: 'type' },
+    { title: '频率', dataIndex: 'speedMhz', key: 'speedMhz', render: (v: number) => `${v} MHz` },
+  ];
+
+  return (
+    <Row gutter={[16, 16]}>
+      <Col span={24}>
+        <Card title="内存基本信息">
+          <Descriptions bordered size="small" column={{ xs: 1, sm: 2, lg: 4 }}>
+            <Descriptions.Item label="总容量">{formatBytes(memory.totalBytes)}</Descriptions.Item>
+            <Descriptions.Item label="类型">{memory.type}</Descriptions.Item>
+            <Descriptions.Item label="频率">{memory.speedMhz} MHz</Descriptions.Item>
+            <Descriptions.Item label="内存条数">{memory.modules?.length ?? 0}</Descriptions.Item>
+          </Descriptions>
+        </Card>
+      </Col>
+      <Col xs={24} lg={8}>
+        <Card>
+          <Statistic title="使用率" value={memory.usagePercent} suffix="%" precision={1} />
+          <Progress percent={memory.usagePercent} status={memory.usagePercent > 85 ? 'exception' : 'normal'} style={{ marginTop: 8 }} />
+          <div style={{ marginTop: 8, color: '#8c8c8c', fontSize: 12 }}>
+            {formatBytes(memory.usedBytes)} / {formatBytes(memory.totalBytes)}
+          </div>
+        </Card>
+      </Col>
+      <Col xs={24} lg={8}>
+        <Card title="内存占用">
+          <ReactECharts option={ringChartOption} style={{ height: 250 }} />
+        </Card>
+      </Col>
+      <Col xs={24} lg={8}>
+        <Card title="交换分区">
+          <ReactECharts option={swapChartOption} style={{ height: 250 }} />
+          <div style={{ textAlign: 'center', color: '#8c8c8c', fontSize: 12 }}>
+            {formatBytes(memory.swapUsedBytes)} / {formatBytes(memory.swapTotalBytes)} ({memory.swapUsagePercent}%)
+          </div>
+        </Card>
+      </Col>
+      <Col span={24}>
+        <Card title="内存条详情">
+          <Table
+            dataSource={memory.modules?.map((m, i) => ({ ...m, key: i })) ?? []}
+            columns={moduleColumns}
+            pagination={false}
+            size="small"
+          />
+        </Card>
+      </Col>
+    </Row>
+  );
+}

+ 89 - 0
frontend/src/components/NetworkDetail/NetworkDetail.tsx

@@ -0,0 +1,89 @@
+import { Card, Row, Col, Statistic, Descriptions, Table } from 'antd';
+import ReactECharts from 'echarts-for-react';
+import { useMetricsStore } from '../../store/metricsStore';
+
+function formatSpeed(bytesPerSec: number): string {
+  if (!bytesPerSec) return '0 B/s';
+  const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
+  let i = 0, val = bytesPerSec;
+  while (val >= 1024 && i < units.length - 1) { val /= 1024; i++; }
+  return `${val.toFixed(1)} ${units[i]}`;
+}
+
+export default function NetworkDetail() {
+  const overview = useMetricsStore((s) => s.overview);
+  const network = overview?.network;
+
+  if (!network?.interfaces?.length) {
+    return <Card>正在加载网络数据...</Card>;
+  }
+
+  const activeInterfaces = network.interfaces.filter(
+    (n) => n.downloadBytesPerSec > 0 || n.uploadBytesPerSec > 0 || n.totalDownloadBytes > 0
+  );
+
+  const speedChartOption = {
+    tooltip: { trigger: 'axis' },
+    legend: { data: ['下载', '上传'], textStyle: { color: '#8c8c8c' } },
+    grid: { left: 60, right: 20, top: 40, bottom: 60 },
+    xAxis: {
+      type: 'category',
+      data: activeInterfaces.map((n) => n.name),
+      axisLabel: { rotate: 30 },
+    },
+    yAxis: { type: 'value', axisLabel: { formatter: (v: number) => formatSpeed(v) } },
+    series: [
+      {
+        name: '下载',
+        type: 'bar',
+        data: activeInterfaces.map((n) => n.downloadBytesPerSec),
+        itemStyle: { color: '#52c41a' },
+      },
+      {
+        name: '上传',
+        type: 'bar',
+        data: activeInterfaces.map((n) => n.uploadBytesPerSec),
+        itemStyle: { color: '#1677ff' },
+      },
+    ],
+  };
+
+  const columns = [
+    { title: '名称', dataIndex: 'name', key: 'name' },
+    { title: '显示名称', dataIndex: 'displayName', key: 'displayName' },
+    { title: 'MAC 地址', dataIndex: 'macAddress', key: 'macAddress' },
+    {
+      title: 'IPv4', dataIndex: 'ipv4', key: 'ipv4',
+      render: (v: string[]) => v?.join(', ') ?? '--',
+    },
+    {
+      title: '下载速率', dataIndex: 'downloadBytesPerSec', key: 'downloadBytesPerSec',
+      render: (v: number) => formatSpeed(v),
+    },
+    {
+      title: '上传速率', dataIndex: 'uploadBytesPerSec', key: 'uploadBytesPerSec',
+      render: (v: number) => formatSpeed(v),
+    },
+  ];
+
+  return (
+    <Row gutter={[16, 16]}>
+      <Col span={24}>
+        <Card title="实时速率对比">
+          <ReactECharts option={speedChartOption} style={{ height: 350 }} />
+        </Card>
+      </Col>
+      <Col span={24}>
+        <Card title="网络接口详情">
+          <Table
+            dataSource={network.interfaces.map((n, i) => ({ ...n, key: i }))}
+            columns={columns}
+            pagination={false}
+            size="small"
+            scroll={{ x: 900 }}
+          />
+        </Card>
+      </Col>
+    </Row>
+  );
+}

+ 86 - 0
frontend/src/components/SystemInfo/SystemInfo.tsx

@@ -0,0 +1,86 @@
+import React, { useState, useEffect } from 'react';
+import { Card, Row, Col, Descriptions, Statistic, Empty } from 'antd';
+import { useMetricsStore } from '../../store/metricsStore';
+
+export default function SystemInfo() {
+  const overview = useMetricsStore((s) => s.overview);
+  const [systemInfo, setSystemInfo] = useState<any>(null);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    fetch('/api/v1/system')
+      .then((res) => res.json())
+      .then((data) => {
+        setSystemInfo(data);
+        setLoading(false);
+      })
+      .catch(() => setLoading(false));
+  }, []);
+
+  if (loading) {
+    return <Card>正在加载系统信息...</Card>;
+  }
+
+  if (!systemInfo) {
+    return <Card><Empty description="无法获取系统信息" /></Card>;
+  }
+
+  return (
+    <Row gutter={[16, 16]}>
+      <Col span={24}>
+        <Card title="操作系统">
+          <Descriptions bordered size="small" column={{ xs: 1, sm: 2, lg: 3 }}>
+            <Descriptions.Item label="系统">{systemInfo.osName}</Descriptions.Item>
+            <Descriptions.Item label="版本">{systemInfo.osVersion}</Descriptions.Item>
+            <Descriptions.Item label="架构">{systemInfo.osArch}</Descriptions.Item>
+          </Descriptions>
+        </Card>
+      </Col>
+      <Col xs={24} lg={12}>
+        <Card title="主板信息">
+          <Descriptions bordered size="small" column={1}>
+            <Descriptions.Item label="制造商">{systemInfo.baseboardManufacturer}</Descriptions.Item>
+            <Descriptions.Item label="型号">{systemInfo.baseboardModel}</Descriptions.Item>
+            <Descriptions.Item label="序列号">{systemInfo.baseboardSerialNumber}</Descriptions.Item>
+          </Descriptions>
+        </Card>
+      </Col>
+      <Col xs={24} lg={12}>
+        <Card title="BIOS 信息">
+          <Descriptions bordered size="small" column={1}>
+            <Descriptions.Item label="厂商">{systemInfo.biosManufacturer}</Descriptions.Item>
+            <Descriptions.Item label="版本">{systemInfo.biosVersion}</Descriptions.Item>
+            <Descriptions.Item label="发布日期">{systemInfo.biosReleaseDate}</Descriptions.Item>
+          </Descriptions>
+        </Card>
+      </Col>
+      <Col xs={24} lg={12}>
+        <Card title="系统信息">
+          <Descriptions bordered size="small" column={1}>
+            <Descriptions.Item label="制造商">{systemInfo.manufacturer}</Descriptions.Item>
+            <Descriptions.Item label="型号">{systemInfo.model}</Descriptions.Item>
+            <Descriptions.Item label="序列号">{systemInfo.serialNumber}</Descriptions.Item>
+          </Descriptions>
+        </Card>
+      </Col>
+      <Col xs={24} sm={12} lg={6}>
+        <Card>
+          <Statistic
+            title="系统温度"
+            value={systemInfo.systemTemperature ?? '--'}
+            suffix={systemInfo.systemTemperature != null ? '°C' : ''}
+          />
+        </Card>
+      </Col>
+      <Col xs={24} sm={12} lg={6}>
+        <Card>
+          <Statistic
+            title="CPU 电压"
+            value={systemInfo.cpuVoltage ?? '--'}
+            suffix={systemInfo.cpuVoltage != null ? 'V' : ''}
+          />
+        </Card>
+      </Col>
+    </Row>
+  );
+}

+ 34 - 0
frontend/src/main.tsx

@@ -0,0 +1,34 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ConfigProvider, theme } from 'antd';
+import zhCN from 'antd/locale/zh_CN';
+import App from './App';
+
+const queryClient = new QueryClient({
+  defaultOptions: {
+    queries: { refetchOnWindowFocus: false, retry: 1 },
+  },
+});
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+  <React.StrictMode>
+    <QueryClientProvider client={queryClient}>
+      <ConfigProvider
+        locale={zhCN}
+        theme={{
+          algorithm: theme.darkAlgorithm,
+          token: {
+            colorPrimary: '#1677ff',
+            borderRadius: 6,
+          },
+        }}
+      >
+        <BrowserRouter>
+          <App />
+        </BrowserRouter>
+      </ConfigProvider>
+    </QueryClientProvider>
+  </React.StrictMode>
+);

+ 76 - 0
frontend/src/services/api.ts

@@ -0,0 +1,76 @@
+import axios from 'axios';
+import type {
+  CpuInfo, MemoryInfo, DiskInfo, GpuInfo, NetworkInfo,
+  SystemInfo, OverviewInfo, MetricRecord,
+} from '../types';
+
+const api = axios.create({
+  baseURL: '/api/v1',
+  timeout: 10000,
+});
+
+export async function fetchOverview(): Promise<OverviewInfo> {
+  const { data } = await api.get('/overview');
+  return data;
+}
+
+export async function fetchCpu(): Promise<CpuInfo> {
+  const { data } = await api.get('/cpu');
+  return data;
+}
+
+export async function fetchMemory(): Promise<MemoryInfo> {
+  const { data } = await api.get('/memory');
+  return data;
+}
+
+export async function fetchDisk(): Promise<DiskInfo> {
+  const { data } = await api.get('/disk');
+  return data;
+}
+
+export async function fetchGpu(): Promise<GpuInfo> {
+  const { data } = await api.get('/gpu');
+  return data;
+}
+
+export async function fetchNetwork(): Promise<NetworkInfo> {
+  const { data } = await api.get('/network');
+  return data;
+}
+
+export async function fetchSystem(): Promise<SystemInfo> {
+  const { data } = await api.get('/system');
+  return data;
+}
+
+export async function fetchHistory(
+  type: string,
+  from: number,
+  to: number
+): Promise<MetricRecord[]> {
+  const { data } = await api.get('/history', {
+    params: { type, from, to },
+  });
+  return data;
+}
+
+export async function fetchHealth(): Promise<string> {
+  const { data } = await api.get('/health');
+  return data;
+}
+
+export async function fetchInterval(): Promise<{ intervalSeconds: number; availableOptions: number[] }> {
+  const { data } = await api.get('/config/interval');
+  return data;
+}
+
+export async function updateInterval(intervalSeconds: number): Promise<{ intervalSeconds: number; status: string }> {
+  const { data } = await api.put('/config/interval', { intervalSeconds });
+  return data;
+}
+
+export async function triggerRefresh(): Promise<{ status: string; timestamp: number }> {
+  const { data } = await api.post('/refresh');
+  return data;
+}

+ 12 - 0
frontend/src/store/metricsStore.ts

@@ -0,0 +1,12 @@
+import { create } from 'zustand';
+import type { OverviewInfo } from '../types';
+
+interface MetricsState {
+  overview: OverviewInfo | null;
+  setOverview: (data: OverviewInfo) => void;
+}
+
+export const useMetricsStore = create<MetricsState>((set) => ({
+  overview: null,
+  setOverview: (data) => set({ overview: data }),
+}));

+ 130 - 0
frontend/src/types.ts

@@ -0,0 +1,130 @@
+export interface CpuInfo {
+  model: string;
+  physicalCores: number;
+  logicalCores: number;
+  architecture: string;
+  maxFreqMhz: number;
+  cacheSizeL1: number;
+  cacheSizeL2: number;
+  cacheSizeL3: number;
+  usagePercent: number;
+  coreUsagePercent: number[];
+  temperature: number | null;
+  powerWatts: number | null;
+  currentFreqMhz: number[];
+}
+
+export interface MemoryModuleInfo {
+  locator: string;
+  manufacturer: string;
+  capacityBytes: number;
+  type: string;
+  speedMhz: number;
+}
+
+export interface MemoryInfo {
+  totalBytes: number;
+  type: string;
+  speedMhz: number;
+  modules: MemoryModuleInfo[];
+  availableBytes: number;
+  usedBytes: number;
+  usagePercent: number;
+  swapTotalBytes: number;
+  swapUsedBytes: number;
+  swapUsagePercent: number;
+}
+
+export interface DiskPartitionInfo {
+  model: string;
+  serial: string;
+  interfaceType: string;
+  totalBytes: number;
+  name: string;
+  usedBytes: number;
+  availableBytes: number;
+  usagePercent: number;
+  readBytesPerSec: number;
+  writeBytesPerSec: number;
+  temperature: number | null;
+  smartStatus: string;
+}
+
+export interface DiskInfo {
+  partitions: DiskPartitionInfo[];
+}
+
+export interface GpuDeviceInfo {
+  name: string;
+  vendor: string;
+  vramTotalBytes: number;
+  driverVersion: string;
+  usagePercent: number | null;
+  vramUsedBytes: number;
+  temperature: number | null;
+  fanSpeedRpm: number[];
+}
+
+export interface GpuInfo {
+  devices: GpuDeviceInfo[];
+}
+
+export interface NetworkInterfaceInfo {
+  name: string;
+  displayName: string;
+  macAddress: string;
+  ipv4: string[];
+  ipv6: string[];
+  mtu: number;
+  uploadBytesPerSec: number;
+  downloadBytesPerSec: number;
+  totalUploadBytes: number;
+  totalDownloadBytes: number;
+}
+
+export interface NetworkInfo {
+  interfaces: NetworkInterfaceInfo[];
+}
+
+export interface SystemInfo {
+  manufacturer: string;
+  model: string;
+  serialNumber: string;
+  biosVersion: string;
+  biosManufacturer: string;
+  biosReleaseDate: string;
+  osName: string;
+  osVersion: string;
+  osArch: string;
+  baseboardManufacturer: string;
+  baseboardModel: string;
+  baseboardSerialNumber: string;
+  systemTemperature: number | null;
+  fanSpeeds: number[];
+  cpuVoltage: number | null;
+}
+
+export interface OverviewInfo {
+  cpu: CpuInfo;
+  memory: MemoryInfo;
+  disk: DiskInfo;
+  gpu: GpuInfo;
+  network: NetworkInfo;
+  alerts: Record<string, boolean>;
+}
+
+export interface MetricRecord {
+  id: number;
+  timestamp: number;
+  category: string;
+  metricKey: string;
+  value: number;
+  unit: string;
+  device: string;
+}
+
+export interface WebSocketMessage {
+  type: string;
+  timestamp: number;
+  data: OverviewInfo;
+}

+ 1 - 0
frontend/src/vite-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 21 - 0
frontend/tsconfig.json

@@ -0,0 +1,21 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "isolatedModules": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+    "jsx": "react-jsx",
+    "strict": true,
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+    "noFallthroughCasesInSwitch": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": ["src"]
+}

+ 22 - 0
frontend/vite.config.ts

@@ -0,0 +1,22 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+  plugins: [react()],
+  server: {
+    port: 3000,
+    proxy: {
+      '/api': {
+        target: 'http://127.0.0.1:9988',
+        changeOrigin: true,
+      },
+      '/ws': {
+        target: 'ws://127.0.0.1:9988',
+        ws: true,
+      },
+    },
+  },
+  build: {
+    outDir: 'dist',
+  },
+});

+ 121 - 0
pom.xml

@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>3.4.4</version>
+        <relativePath/>
+    </parent>
+
+    <groupId>com.hwmonitor</groupId>
+    <artifactId>hw-monitor</artifactId>
+    <version>1.0.0</version>
+    <name>hw-monitor</name>
+    <description>网页端本机硬件设备基础信息监控系统</description>
+
+    <properties>
+        <java.version>21</java.version>
+        <oshi.version>6.6.5</oshi.version>
+        <lombok.version>1.18.36</lombok.version>
+    </properties>
+
+    <dependencies>
+        <!-- Spring Boot Web -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- Spring Boot WebSocket -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+
+        <!-- Spring Boot Data JPA -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+
+        <!-- Spring Boot Actuator -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+
+        <!-- H2 Database -->
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
+        <!-- OSHI 硬件信息采集 -->
+        <dependency>
+            <groupId>com.github.oshi</groupId>
+            <artifactId>oshi-core</artifactId>
+            <version>${oshi.version}</version>
+        </dependency>
+
+        <!-- Spring Boot Cache -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-cache</artifactId>
+        </dependency>
+
+        <!-- Caffeine Cache -->
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+        </dependency>
+
+        <!-- Lombok -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- Test -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                            <version>${lombok.version}</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                        </exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 14 - 0
src/main/java/com/hwmonitor/HwMonitorApplication.java

@@ -0,0 +1,14 @@
+package com.hwmonitor;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@SpringBootApplication
+@EnableScheduling
+public class HwMonitorApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(HwMonitorApplication.class, args);
+    }
+}

+ 120 - 0
src/main/java/com/hwmonitor/collector/CpuCollector.java

@@ -0,0 +1,120 @@
+package com.hwmonitor.collector;
+
+import com.hwmonitor.model.dto.CpuInfo;
+import lombok.extern.slf4j.Slf4j;
+import oshi.SystemInfo;
+import oshi.hardware.CentralProcessor;
+import oshi.hardware.CentralProcessor.ProcessorCache;
+import oshi.hardware.Sensors;
+
+import java.util.List;
+
+@Slf4j
+public class CpuCollector {
+
+    private final CentralProcessor processor;
+    private final Sensors sensors;
+    private long[] prevSystemTicks;
+    private long[][] prevProcessorTicks;
+    private boolean staticInfoCollected;
+
+    private CpuInfo cachedStaticInfo;
+
+    public CpuCollector(SystemInfo systemInfo) {
+        this.processor = systemInfo.getHardware().getProcessor();
+        this.sensors = systemInfo.getHardware().getSensors();
+        this.prevSystemTicks = processor.getSystemCpuLoadTicks();
+        this.prevProcessorTicks = processor.getProcessorCpuLoadTicks();
+        this.staticInfoCollected = false;
+    }
+
+    public CpuInfo collect() {
+        try {
+            if (!staticInfoCollected) {
+                collectStaticInfo();
+            }
+            return CpuInfo.builder()
+                    .model(cachedStaticInfo.getModel())
+                    .physicalCores(cachedStaticInfo.getPhysicalCores())
+                    .logicalCores(cachedStaticInfo.getLogicalCores())
+                    .architecture(cachedStaticInfo.getArchitecture())
+                    .maxFreqMhz(cachedStaticInfo.getMaxFreqMhz())
+                    .cacheSizeL1(cachedStaticInfo.getCacheSizeL1())
+                    .cacheSizeL2(cachedStaticInfo.getCacheSizeL2())
+                    .cacheSizeL3(cachedStaticInfo.getCacheSizeL3())
+                    .usagePercent(collectUsagePercent())
+                    .coreUsagePercent(collectCoreUsagePercent())
+                    .temperature(collectTemperature())
+                    .powerWatts(collectPower())
+                    .currentFreqMhz(processor.getCurrentFreq())
+                    .build();
+        } catch (Exception e) {
+            log.error("CPU信息采集失败", e);
+            return CpuInfo.builder().build();
+        }
+    }
+
+    private void collectStaticInfo() {
+        var identifier = processor.getProcessorIdentifier();
+        List<ProcessorCache> caches = processor.getProcessorCaches();
+        cachedStaticInfo = CpuInfo.builder()
+                .model(identifier.getName())
+                .physicalCores(processor.getPhysicalProcessorCount())
+                .logicalCores(processor.getLogicalProcessorCount())
+                .architecture(System.getProperty("os.arch"))
+                .maxFreqMhz(processor.getMaxFreq())
+                .cacheSizeL1(sumCacheByLevel(caches, 1))
+                .cacheSizeL2(sumCacheByLevel(caches, 2))
+                .cacheSizeL3(sumCacheByLevel(caches, 3))
+                .build();
+        staticInfoCollected = true;
+    }
+
+    private double collectUsagePercent() {
+        long[] currentTicks = processor.getSystemCpuLoadTicks();
+        long[] prev = prevSystemTicks;
+        prevSystemTicks = currentTicks;
+        return Math.round(processor.getSystemCpuLoadBetweenTicks(prev) * 1000.0) / 10.0;
+    }
+
+    private double[] collectCoreUsagePercent() {
+        long[][] currentTicks = processor.getProcessorCpuLoadTicks();
+        long[][] prev = prevProcessorTicks;
+        prevProcessorTicks = currentTicks;
+        double[] loads = processor.getProcessorCpuLoadBetweenTicks(prev);
+        double[] result = new double[loads.length];
+        for (int i = 0; i < loads.length; i++) {
+            result[i] = Math.round(loads[i] * 1000.0) / 10.0;
+        }
+        return result;
+    }
+
+    private Double collectTemperature() {
+        try {
+            double temp = sensors.getCpuTemperature();
+            return temp > 0 ? Math.round(temp * 10.0) / 10.0 : null;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private Double collectPower() {
+        try {
+            double power = sensors.getCpuVoltage();
+            return power > 0 ? Math.round(power * 100.0) / 100.0 : null;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private long sumCacheByLevel(List<ProcessorCache> caches, int level) {
+        if (caches == null) return 0;
+        long sum = 0;
+        for (ProcessorCache cache : caches) {
+            if (cache.getLevel() == level) {
+                sum += cache.getCacheSize();
+            }
+        }
+        return sum;
+    }
+}

+ 154 - 0
src/main/java/com/hwmonitor/collector/DiskCollector.java

@@ -0,0 +1,154 @@
+package com.hwmonitor.collector;
+
+import com.hwmonitor.model.dto.DiskInfo;
+import lombok.extern.slf4j.Slf4j;
+import oshi.SystemInfo;
+import oshi.hardware.HWDiskStore;
+import oshi.software.os.OSFileStore;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+public class DiskCollector {
+
+    private final SystemInfo systemInfo;
+    private final Map<String, long[]> prevReadWriteBytes = new HashMap<>();
+    private final Map<String, Long> prevTimestamps = new HashMap<>();
+
+    public DiskCollector(SystemInfo systemInfo) {
+        this.systemInfo = systemInfo;
+    }
+
+    public DiskInfo collect() {
+        try {
+            List<DiskInfo.DiskPartitionInfo> partitions = new ArrayList<>();
+            List<HWDiskStore> disks = systemInfo.getHardware().getDiskStores();
+            List<OSFileStore> fileStores = systemInfo.getOperatingSystem().getFileSystem().getFileStores();
+
+            long now = System.currentTimeMillis();
+
+            // 按物理磁盘建立分区列表
+            for (HWDiskStore disk : disks) {
+                String diskName = disk.getName();
+                DiskInfo.DiskPartitionInfo.DiskPartitionInfoBuilder builder = DiskInfo.DiskPartitionInfo.builder()
+                        .model(disk.getModel())
+                        .serial(maskSerial(disk.getSerial()))
+                        .name(diskName)
+                        .interfaceType(detectInterfaceType(diskName))
+                        .smartStatus(detectSmartStatus(disk))
+                        .totalBytes(disk.getSize());
+
+                // 匹配文件系统分区:按磁盘索引匹配,或按分区名匹配
+                boolean matched = false;
+                for (OSFileStore fs : fileStores) {
+                    String volume = fs.getVolume() != null ? fs.getVolume() : "";
+                    String mount = fs.getMount() != null ? fs.getMount() : "";
+                    // Windows: \\.\PHYSICALDRIVE0 + C:\ -> 按分区名 C: 匹配
+                    // 使用磁盘的 partitions 属性来匹配
+                    if (disk.getPartitions() != null) {
+                        for (var dp : disk.getPartitions()) {
+                            String partName = dp.getIdentification() != null ? dp.getIdentification() : dp.getName();
+                            if (partName != null && (volume.contains(partName) || partName.contains(volume.replace("\\", "")) || mount.contains(partName))) {
+                                fillFSUsage(builder, fs);
+                                matched = true;
+                                break;
+                            }
+                        }
+                        if (matched) break;
+                    }
+                }
+
+                // 如果分区匹配失败,直接用第一个匹配的 OSFileStore
+                if (!matched && !fileStores.isEmpty() && disks.size() == 1) {
+                    // 单磁盘:累加所有分区
+                    long totalUsed = 0, totalAvail = 0, totalTotal = 0;
+                    for (OSFileStore fs : fileStores) {
+                        totalUsed += fs.getTotalSpace() - fs.getUsableSpace();
+                        totalAvail += fs.getUsableSpace();
+                        totalTotal += fs.getTotalSpace();
+                    }
+                    if (totalTotal > 0) {
+                        builder.usedBytes(totalUsed)
+                                .availableBytes(totalAvail)
+                                .usagePercent(Math.round((double) totalUsed / totalTotal * 1000.0) / 10.0);
+                    }
+                }
+
+                // 计算读写速率
+                long[] prev = prevReadWriteBytes.get(diskName);
+                Long prevTime = prevTimestamps.get(diskName);
+                long currentReadBytes = disk.getReadBytes();
+                long currentWriteBytes = disk.getWriteBytes();
+                if (prev != null && prevTime != null) {
+                    long elapsed = now - prevTime;
+                    if (elapsed > 0) {
+                        builder.readBytesPerSec((currentReadBytes - prev[0]) * 1000 / elapsed)
+                                .writeBytesPerSec((currentWriteBytes - prev[1]) * 1000 / elapsed);
+                    }
+                }
+                prevReadWriteBytes.put(diskName, new long[]{currentReadBytes, currentWriteBytes});
+                prevTimestamps.put(diskName, now);
+
+                partitions.add(builder.build());
+            }
+
+            // 如果没有物理磁盘信息,从 OSFileStore 创建分区
+            if (partitions.isEmpty() && !fileStores.isEmpty()) {
+                for (OSFileStore fs : fileStores) {
+                    String mount = fs.getMount() != null ? fs.getMount() : fs.getVolume();
+                    partitions.add(DiskInfo.DiskPartitionInfo.builder()
+                            .name(mount)
+                            .model(fs.getDescription())
+                            .totalBytes(fs.getTotalSpace())
+                            .usedBytes(fs.getTotalSpace() - fs.getUsableSpace())
+                            .availableBytes(fs.getUsableSpace())
+                            .usagePercent(Math.round((double) (fs.getTotalSpace() - fs.getUsableSpace()) / fs.getTotalSpace() * 1000.0) / 10.0)
+                            .build());
+                }
+            }
+
+            return DiskInfo.builder().partitions(partitions).build();
+        } catch (Exception e) {
+            log.error("磁盘信息采集失败", e);
+            return DiskInfo.builder().partitions(List.of()).build();
+        }
+    }
+
+    private void fillFSUsage(DiskInfo.DiskPartitionInfo.DiskPartitionInfoBuilder builder, OSFileStore fs) {
+        long total = fs.getTotalSpace();
+        long usable = fs.getUsableSpace();
+        builder.usedBytes(total - usable)
+                .availableBytes(usable)
+                .usagePercent(Math.round((double) (total - usable) / total * 1000.0) / 10.0);
+    }
+
+    private String maskSerial(String serial) {
+        if (serial == null || serial.length() < 6) return "****";
+        return serial.substring(0, 3) + "****";
+    }
+
+    private String detectInterfaceType(String name) {
+        if (name == null) return "Unknown";
+        String lower = name.toLowerCase();
+        if (lower.contains("nvme")) return "NVMe";
+        if (lower.contains("sd") || lower.contains("sata")) return "SATA";
+        if (lower.contains("hd")) return "IDE";
+        if (lower.contains("physicaldrive")) return "SATA";
+        if (lower.contains("mmc")) return "eMMC";
+        return "Unknown";
+    }
+
+    private String detectSmartStatus(HWDiskStore disk) {
+        try {
+            if (disk.getSize() > 0 && disk.getReads() >= 0) {
+                return "OK";
+            }
+            return "Unknown";
+        } catch (Exception e) {
+            return "Unknown";
+        }
+    }
+}

+ 47 - 0
src/main/java/com/hwmonitor/collector/GpuCollector.java

@@ -0,0 +1,47 @@
+package com.hwmonitor.collector;
+
+import com.hwmonitor.model.dto.GpuInfo;
+import lombok.extern.slf4j.Slf4j;
+import oshi.SystemInfo;
+import oshi.hardware.GraphicsCard;
+import oshi.hardware.Sensors;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+public class GpuCollector {
+
+    private final SystemInfo systemInfo;
+    private final Sensors sensors;
+
+    public GpuCollector(SystemInfo systemInfo) {
+        this.systemInfo = systemInfo;
+        this.sensors = systemInfo.getHardware().getSensors();
+    }
+
+    public GpuInfo collect() {
+        try {
+            List<GraphicsCard> cards = systemInfo.getHardware().getGraphicsCards();
+            List<GpuInfo.GpuDeviceInfo> devices = new ArrayList<>();
+
+            for (GraphicsCard card : cards) {
+                devices.add(GpuInfo.GpuDeviceInfo.builder()
+                        .name(card.getName())
+                        .vendor(card.getVendor())
+                        .vramTotalBytes(card.getVRam())
+                        .driverVersion(card.getVersionInfo())
+                        .usagePercent(null)  // OSHI 不直接提供 GPU 使用率
+                        .vramUsedBytes(0)
+                        .temperature(null)
+                        .fanSpeedRpm(sensors.getFanSpeeds())
+                        .build());
+            }
+
+            return GpuInfo.builder().devices(devices).build();
+        } catch (Exception e) {
+            log.error("GPU信息采集失败", e);
+            return GpuInfo.builder().devices(List.of()).build();
+        }
+    }
+}

+ 77 - 0
src/main/java/com/hwmonitor/collector/MemoryCollector.java

@@ -0,0 +1,77 @@
+package com.hwmonitor.collector;
+
+import com.hwmonitor.model.dto.MemoryInfo;
+import lombok.extern.slf4j.Slf4j;
+import oshi.SystemInfo;
+import oshi.hardware.GlobalMemory;
+import oshi.hardware.PhysicalMemory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+public class MemoryCollector {
+
+    private final GlobalMemory memory;
+    private boolean staticInfoCollected;
+    private MemoryInfo cachedStaticInfo;
+
+    public MemoryCollector(SystemInfo systemInfo) {
+        this.memory = systemInfo.getHardware().getMemory();
+    }
+
+    public MemoryInfo collect() {
+        try {
+            if (!staticInfoCollected) {
+                collectStaticInfo();
+            }
+            long totalBytes = memory.getTotal();
+            long availableBytes = memory.getAvailable();
+            long usedBytes = totalBytes - availableBytes;
+            double usagePercent = (double) usedBytes / totalBytes * 100;
+
+            var virtualMemory = memory.getVirtualMemory();
+            return MemoryInfo.builder()
+                    .totalBytes(cachedStaticInfo.getTotalBytes())
+                    .type(cachedStaticInfo.getType())
+                    .speedMhz(cachedStaticInfo.getSpeedMhz())
+                    .modules(cachedStaticInfo.getModules())
+                    .availableBytes(availableBytes)
+                    .usedBytes(usedBytes)
+                    .usagePercent(Math.round(usagePercent * 10.0) / 10.0)
+                    .swapTotalBytes(virtualMemory.getSwapTotal())
+                    .swapUsedBytes(virtualMemory.getSwapUsed())
+                    .swapUsagePercent(virtualMemory.getSwapTotal() > 0
+                            ? Math.round((double) virtualMemory.getSwapUsed() / virtualMemory.getSwapTotal() * 1000.0) / 10.0
+                            : 0)
+                    .build();
+        } catch (Exception e) {
+            log.error("内存信息采集失败", e);
+            return MemoryInfo.builder().build();
+        }
+    }
+
+    private void collectStaticInfo() {
+        List<MemoryInfo.MemoryModuleInfo> modules = new ArrayList<>();
+        long speed = 0;
+        String type = "Unknown";
+        for (PhysicalMemory pm : memory.getPhysicalMemory()) {
+            modules.add(MemoryInfo.MemoryModuleInfo.builder()
+                    .locator(pm.getBankLabel())
+                    .manufacturer(pm.getManufacturer())
+                    .capacityBytes(pm.getCapacity())
+                    .type(pm.getMemoryType())
+                    .speedMhz(pm.getClockSpeed())
+                    .build());
+            speed = pm.getClockSpeed();
+            type = pm.getMemoryType();
+        }
+        cachedStaticInfo = MemoryInfo.builder()
+                .totalBytes(memory.getTotal())
+                .type(type)
+                .speedMhz(speed)
+                .modules(modules)
+                .build();
+        staticInfoCollected = true;
+    }
+}

+ 118 - 0
src/main/java/com/hwmonitor/collector/NetworkCollector.java

@@ -0,0 +1,118 @@
+package com.hwmonitor.collector;
+
+import com.hwmonitor.model.dto.NetworkInfo;
+import lombok.extern.slf4j.Slf4j;
+import oshi.SystemInfo;
+import oshi.hardware.NetworkIF;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+public class NetworkCollector {
+
+    private final SystemInfo systemInfo;
+    private final Map<String, long[]> prevBytes = new HashMap<>();
+    private final Map<String, Long> prevTimestamps = new HashMap<>();
+
+    public NetworkCollector(SystemInfo systemInfo) {
+        this.systemInfo = systemInfo;
+    }
+
+    public NetworkInfo collect() {
+        try {
+            List<NetworkIF> networkIFs = systemInfo.getHardware().getNetworkIFs();
+            List<NetworkInfo.NetworkInterfaceInfo> interfaces = new ArrayList<>();
+            long now = System.currentTimeMillis();
+
+            for (NetworkIF net : networkIFs) {
+                net.updateAttributes();
+                String name = net.getName();
+
+                long uploadBytesPerSec = 0;
+                long downloadBytesPerSec = 0;
+                long[] prev = prevBytes.get(name);
+                Long prevTime = prevTimestamps.get(name);
+                long currentUpload = net.getBytesSent();
+                long currentDownload = net.getBytesRecv();
+
+                if (prev != null && prevTime != null) {
+                    long elapsed = now - prevTime;
+                    if (elapsed > 0) {
+                        uploadBytesPerSec = (currentUpload - prev[0]) * 1000 / elapsed;
+                        downloadBytesPerSec = (currentDownload - prev[1]) * 1000 / elapsed;
+                    }
+                }
+                prevBytes.put(name, new long[]{currentUpload, currentDownload});
+                prevTimestamps.put(name, now);
+
+                String[] ipv4 = net.getIPv4addr();
+                String[] maskedIpv4 = maskIpv4Array(ipv4);
+
+                interfaces.add(NetworkInfo.NetworkInterfaceInfo.builder()
+                        .name(name)
+                        .displayName(net.getDisplayName())
+                        .macAddress(maskMac(net.getMacaddr()))
+                        .ipv4(maskedIpv4)
+                        .ipv6(net.getIPv6addr())
+                        .mtu(net.getMTU())
+                        .uploadBytesPerSec(uploadBytesPerSec)
+                        .downloadBytesPerSec(downloadBytesPerSec)
+                        .totalUploadBytes(currentUpload)
+                        .totalDownloadBytes(currentDownload)
+                        .build());
+            }
+
+            return NetworkInfo.builder().interfaces(interfaces).build();
+        } catch (Exception e) {
+            log.error("网络信息采集失败", e);
+            return NetworkInfo.builder().interfaces(List.of()).build();
+        }
+    }
+
+    private String maskMac(String mac) {
+        if (mac == null || mac.length() < 17) return "**:**:**:**:**:**";
+        return mac.substring(0, 8) + "**:**:**" + mac.substring(15);
+    }
+
+    private String[] maskIpv4Array(String[] ipv4) {
+        if (ipv4 == null) return new String[0];
+        String[] result = new String[ipv4.length];
+        for (int i = 0; i < ipv4.length; i++) {
+            result[i] = maskIpv4(ipv4[i]);
+        }
+        return result;
+    }
+
+    private String maskIpv4(String ip) {
+        if (ip == null || ip.isEmpty()) return ip;
+        // 检查是否为私有/回环地址,这些不需要脱敏
+        if (isPrivateOrLoopback(ip)) return ip;
+        // 公网 IP:仅显示前两段,如 123.45.*.*
+        String[] parts = ip.split("\\.");
+        if (parts.length == 4) {
+            return parts[0] + "." + parts[1] + ".*.*";
+        }
+        return ip;
+    }
+
+    private boolean isPrivateOrLoopback(String ip) {
+        if (!ip.matches("\\d+\\.\\d+\\.\\d+\\.\\d+")) return true;
+        String[] parts = ip.split("\\.");
+        int first = Integer.parseInt(parts[0]);
+        int second = Integer.parseInt(parts[1]);
+        // 127.0.0.0/8 回环
+        if (first == 127) return true;
+        // 10.0.0.0/8 私有
+        if (first == 10) return true;
+        // 172.16.0.0/12 私有
+        if (first == 172 && second >= 16 && second <= 31) return true;
+        // 192.168.0.0/16 私有
+        if (first == 192 && second == 168) return true;
+        // 169.254.0.0/16 链路本地
+        if (first == 169 && second == 254) return true;
+        return false;
+    }
+}

+ 102 - 0
src/main/java/com/hwmonitor/collector/SystemCollector.java

@@ -0,0 +1,102 @@
+package com.hwmonitor.collector;
+
+import com.hwmonitor.model.dto.SystemInfo;
+import lombok.extern.slf4j.Slf4j;
+import oshi.hardware.ComputerSystem;
+import oshi.hardware.Sensors;
+import oshi.software.os.OperatingSystem;
+
+@Slf4j
+public class SystemCollector {
+
+    private final ComputerSystem computerSystem;
+    private final OperatingSystem os;
+    private final Sensors sensors;
+    private boolean staticInfoCollected;
+    private SystemInfo cachedStaticInfo;
+
+    public SystemCollector(oshi.SystemInfo systemInfo) {
+        this.computerSystem = systemInfo.getHardware().getComputerSystem();
+        this.os = systemInfo.getOperatingSystem();
+        this.sensors = systemInfo.getHardware().getSensors();
+    }
+
+    public SystemInfo collect() {
+        try {
+            if (!staticInfoCollected) {
+                collectStaticInfo();
+            }
+            return SystemInfo.builder()
+                    .manufacturer(cachedStaticInfo.getManufacturer())
+                    .model(cachedStaticInfo.getModel())
+                    .serialNumber(cachedStaticInfo.getSerialNumber())
+                    .biosVersion(cachedStaticInfo.getBiosVersion())
+                    .biosManufacturer(cachedStaticInfo.getBiosManufacturer())
+                    .biosReleaseDate(cachedStaticInfo.getBiosReleaseDate())
+                    .osName(cachedStaticInfo.getOsName())
+                    .osVersion(cachedStaticInfo.getOsVersion())
+                    .osArch(cachedStaticInfo.getOsArch())
+                    .baseboardManufacturer(cachedStaticInfo.getBaseboardManufacturer())
+                    .baseboardModel(cachedStaticInfo.getBaseboardModel())
+                    .baseboardSerialNumber(cachedStaticInfo.getBaseboardSerialNumber())
+                    .systemTemperature(collectSystemTemperature())
+                    .fanSpeeds(collectFanSpeeds())
+                    .cpuVoltage(collectCpuVoltage())
+                    .build();
+        } catch (Exception e) {
+            log.error("系统信息采集失败", e);
+            return SystemInfo.builder().build();
+        }
+    }
+
+    private void collectStaticInfo() {
+        var firmware = computerSystem.getFirmware();
+        var baseboard = computerSystem.getBaseboard();
+        cachedStaticInfo = SystemInfo.builder()
+                .manufacturer(computerSystem.getManufacturer())
+                .model(computerSystem.getModel())
+                .serialNumber(maskSerial(computerSystem.getSerialNumber()))
+                .biosVersion(firmware.getVersion())
+                .biosManufacturer(firmware.getManufacturer())
+                .biosReleaseDate(firmware.getReleaseDate())
+                .osName(os.getFamily() + " " + os.getVersionInfo().getVersion())
+                .osVersion(os.getVersionInfo().toString())
+                .osArch(System.getProperty("os.arch"))
+                .baseboardManufacturer(baseboard.getManufacturer())
+                .baseboardModel(baseboard.getModel())
+                .baseboardSerialNumber(maskSerial(baseboard.getSerialNumber()))
+                .build();
+        staticInfoCollected = true;
+    }
+
+    private Double collectSystemTemperature() {
+        try {
+            double temp = sensors.getCpuTemperature();
+            return temp > 0 ? temp : null;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private int[] collectFanSpeeds() {
+        try {
+            return sensors.getFanSpeeds();
+        } catch (Exception e) {
+            return new int[0];
+        }
+    }
+
+    private Double collectCpuVoltage() {
+        try {
+            double voltage = sensors.getCpuVoltage();
+            return voltage > 0 ? voltage : null;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    private String maskSerial(String serial) {
+        if (serial == null || serial.length() < 6) return "****";
+        return serial.substring(0, 3) + "****";
+    }
+}

+ 29 - 0
src/main/java/com/hwmonitor/config/BrowserLauncher.java

@@ -0,0 +1,29 @@
+package com.hwmonitor.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class BrowserLauncher implements ApplicationRunner {
+
+    @Override
+    public void run(ApplicationArguments args) {
+        String url = "http://localhost:9988";
+        try {
+            String os = System.getProperty("os.name").toLowerCase();
+            if (os.contains("win")) {
+                Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url);
+            } else if (os.contains("mac")) {
+                Runtime.getRuntime().exec("open " + url);
+            } else {
+                Runtime.getRuntime().exec("xdg-open " + url);
+            }
+            log.info("浏览器已打开: {}", url);
+        } catch (Exception e) {
+            log.warn("无法自动打开浏览器,请手动访问: {}", url);
+        }
+    }
+}

+ 29 - 0
src/main/java/com/hwmonitor/config/CacheConfig.java

@@ -0,0 +1,29 @@
+package com.hwmonitor.config;
+
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.caffeine.CaffeineCacheManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Configuration
+@EnableCaching
+public class CacheConfig {
+
+    @Bean
+    public CacheManager cacheManager() {
+        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
+        cacheManager.setCacheNames(List.of(
+                "cpuInfo", "memoryInfo", "diskInfo",
+                "gpuInfo", "networkInfo", "systemInfo"
+        ));
+        cacheManager.setCaffeine(Caffeine.newBuilder()
+                .expireAfterWrite(120, TimeUnit.SECONDS)
+                .maximumSize(1));
+        return cacheManager;
+    }
+}

+ 17 - 0
src/main/java/com/hwmonitor/config/MonitorConfig.java

@@ -0,0 +1,17 @@
+package com.hwmonitor.config;
+
+import lombok.Getter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+@Getter
+public class MonitorConfig {
+
+    @Value("${hwmonitor.collect-interval-seconds:30}")
+    private volatile int collectIntervalSeconds;
+
+    public synchronized void setCollectIntervalSeconds(int seconds) {
+        this.collectIntervalSeconds = seconds;
+    }
+}

+ 24 - 0
src/main/java/com/hwmonitor/config/WebSocketConfig.java

@@ -0,0 +1,24 @@
+package com.hwmonitor.config;
+
+import com.hwmonitor.controller.MetricsWebSocketHandler;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
+
+@Configuration
+@EnableWebSocket
+public class WebSocketConfig implements WebSocketConfigurer {
+
+    private final MetricsWebSocketHandler metricsWebSocketHandler;
+
+    public WebSocketConfig(MetricsWebSocketHandler metricsWebSocketHandler) {
+        this.metricsWebSocketHandler = metricsWebSocketHandler;
+    }
+
+    @Override
+    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
+        registry.addHandler(metricsWebSocketHandler, "/ws/metrics")
+                .setAllowedOriginPatterns("*");
+    }
+}

+ 39 - 0
src/main/java/com/hwmonitor/controller/ConfigController.java

@@ -0,0 +1,39 @@
+package com.hwmonitor.controller;
+
+import com.hwmonitor.config.MonitorConfig;
+import com.hwmonitor.scheduler.MetricsScheduler;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/v1/config")
+public class ConfigController {
+
+    private final MonitorConfig monitorConfig;
+    private final MetricsScheduler metricsScheduler;
+
+    public ConfigController(MonitorConfig monitorConfig, MetricsScheduler metricsScheduler) {
+        this.monitorConfig = monitorConfig;
+        this.metricsScheduler = metricsScheduler;
+    }
+
+    @GetMapping("/interval")
+    public Map<String, Object> getInterval() {
+        return Map.of(
+                "intervalSeconds", monitorConfig.getCollectIntervalSeconds(),
+                "availableOptions", new int[]{30, 60}
+        );
+    }
+
+    @PutMapping("/interval")
+    public Map<String, Object> setInterval(@RequestBody Map<String, Integer> body) {
+        int seconds = body.getOrDefault("intervalSeconds", 30);
+        if (seconds != 30 && seconds != 60) {
+            throw new IllegalArgumentException("采集间隔仅支持 30 或 60 秒");
+        }
+        monitorConfig.setCollectIntervalSeconds(seconds);
+        metricsScheduler.reschedule();
+        return Map.of("intervalSeconds", seconds, "status", "ok");
+    }
+}

+ 26 - 0
src/main/java/com/hwmonitor/controller/HistoryController.java

@@ -0,0 +1,26 @@
+package com.hwmonitor.controller;
+
+import com.hwmonitor.model.entity.MetricRecord;
+import com.hwmonitor.service.HistoryService;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/v1")
+public class HistoryController {
+
+    private final HistoryService historyService;
+
+    public HistoryController(HistoryService historyService) {
+        this.historyService = historyService;
+    }
+
+    @GetMapping("/history")
+    public List<MetricRecord> getHistory(
+            @RequestParam(required = false) String type,
+            @RequestParam Long from,
+            @RequestParam Long to) {
+        return historyService.queryHistory(type, from, to);
+    }
+}

+ 65 - 0
src/main/java/com/hwmonitor/controller/MetricsController.java

@@ -0,0 +1,65 @@
+package com.hwmonitor.controller;
+
+import com.hwmonitor.model.dto.*;
+import com.hwmonitor.scheduler.MetricsScheduler;
+import com.hwmonitor.service.MetricsService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/v1")
+public class MetricsController {
+
+    private final MetricsService metricsService;
+    private final MetricsScheduler metricsScheduler;
+
+    public MetricsController(MetricsService metricsService, MetricsScheduler metricsScheduler) {
+        this.metricsService = metricsService;
+        this.metricsScheduler = metricsScheduler;
+    }
+
+    @GetMapping("/overview")
+    public OverviewInfo getOverview() {
+        return metricsService.collectOverview();
+    }
+
+    @GetMapping("/cpu")
+    public CpuInfo getCpu() {
+        return metricsService.collectCpu();
+    }
+
+    @GetMapping("/memory")
+    public MemoryInfo getMemory() {
+        return metricsService.collectMemory();
+    }
+
+    @GetMapping("/disk")
+    public DiskInfo getDisk() {
+        return metricsService.collectDisk();
+    }
+
+    @GetMapping("/gpu")
+    public GpuInfo getGpu() {
+        return metricsService.collectGpu();
+    }
+
+    @GetMapping("/network")
+    public NetworkInfo getNetwork() {
+        return metricsService.collectNetwork();
+    }
+
+    @GetMapping("/system")
+    public SystemInfo getSystem() {
+        return metricsService.collectSystem();
+    }
+
+    @PostMapping("/refresh")
+    public Map<String, Object> refresh() {
+        metricsScheduler.collectAndPush();
+        return Map.of("status", "ok", "timestamp", System.currentTimeMillis());
+    }
+}

+ 70 - 0
src/main/java/com/hwmonitor/controller/MetricsWebSocketHandler.java

@@ -0,0 +1,70 @@
+package com.hwmonitor.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.hwmonitor.model.dto.OverviewInfo;
+import com.hwmonitor.service.MetricsService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+@Slf4j
+@Component
+public class MetricsWebSocketHandler extends TextWebSocketHandler {
+
+    private final MetricsService metricsService;
+    private final ObjectMapper objectMapper;
+    private final CopyOnWriteArraySet<WebSocketSession> sessions = new CopyOnWriteArraySet<>();
+
+    public MetricsWebSocketHandler(MetricsService metricsService, ObjectMapper objectMapper) {
+        this.metricsService = metricsService;
+        this.objectMapper = objectMapper;
+    }
+
+    @Override
+    public void afterConnectionEstablished(WebSocketSession session) {
+        sessions.add(session);
+        log.info("WebSocket 客户端已连接: {}", session.getId());
+    }
+
+    @Override
+    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
+        sessions.remove(session);
+        log.info("WebSocket 客户端已断开: {}", session.getId());
+    }
+
+    @Override
+    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
+        // 客户端可以发送调整采集频率的消息
+    }
+
+    public void broadcastMetrics(OverviewInfo overview) {
+        if (sessions.isEmpty()) return;
+        try {
+            var payload = Map.of(
+                    "type", "metrics",
+                    "timestamp", System.currentTimeMillis(),
+                    "data", overview
+            );
+            String json = objectMapper.writeValueAsString(payload);
+            TextMessage msg = new TextMessage(json);
+            for (WebSocketSession session : sessions) {
+                try {
+                    if (session.isOpen()) {
+                        session.sendMessage(msg);
+                    }
+                } catch (IOException e) {
+                    log.error("WebSocket 推送失败", e);
+                }
+            }
+        } catch (Exception e) {
+            log.error("WebSocket 消息序列化失败", e);
+        }
+    }
+}

+ 28 - 0
src/main/java/com/hwmonitor/model/dto/CpuInfo.java

@@ -0,0 +1,28 @@
+package com.hwmonitor.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CpuInfo {
+    // 静态信息
+    private String model;
+    private int physicalCores;
+    private int logicalCores;
+    private String architecture;
+    private long maxFreqMhz;
+    private long cacheSizeL1;
+    private long cacheSizeL2;
+    private long cacheSizeL3;
+    // 实时状态
+    private double usagePercent;
+    private double[] coreUsagePercent;
+    private Double temperature;
+    private Double powerWatts;
+    private long[] currentFreqMhz;
+}

+ 37 - 0
src/main/java/com/hwmonitor/model/dto/DiskInfo.java

@@ -0,0 +1,37 @@
+package com.hwmonitor.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DiskInfo {
+    private List<DiskPartitionInfo> partitions;
+
+    @Data
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class DiskPartitionInfo {
+        // 静态信息
+        private String model;
+        private String serial;
+        private String interfaceType;
+        private long totalBytes;
+        private String name;
+        // 实时状态
+        private long usedBytes;
+        private long availableBytes;
+        private double usagePercent;
+        private long readBytesPerSec;
+        private long writeBytesPerSec;
+        private Double temperature;
+        private String smartStatus;
+    }
+}

+ 32 - 0
src/main/java/com/hwmonitor/model/dto/GpuInfo.java

@@ -0,0 +1,32 @@
+package com.hwmonitor.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class GpuInfo {
+    private List<GpuDeviceInfo> devices;
+
+    @Data
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class GpuDeviceInfo {
+        private String name;
+        private String vendor;
+        private long vramTotalBytes;
+        private String driverVersion;
+        // 实时状态
+        private Double usagePercent;
+        private long vramUsedBytes;
+        private Double temperature;
+        private int[] fanSpeedRpm;
+    }
+}

+ 39 - 0
src/main/java/com/hwmonitor/model/dto/MemoryInfo.java

@@ -0,0 +1,39 @@
+package com.hwmonitor.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MemoryInfo {
+    // 静态信息
+    private long totalBytes;
+    private String type;
+    private long speedMhz;
+    private List<MemoryModuleInfo> modules;
+    // 实时状态
+    private long availableBytes;
+    private long usedBytes;
+    private double usagePercent;
+    private long swapTotalBytes;
+    private long swapUsedBytes;
+    private double swapUsagePercent;
+
+    @Data
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class MemoryModuleInfo {
+        private String locator;
+        private String manufacturer;
+        private long capacityBytes;
+        private String type;
+        private long speedMhz;
+    }
+}

+ 34 - 0
src/main/java/com/hwmonitor/model/dto/NetworkInfo.java

@@ -0,0 +1,34 @@
+package com.hwmonitor.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class NetworkInfo {
+    private List<NetworkInterfaceInfo> interfaces;
+
+    @Data
+    @Builder
+    @NoArgsConstructor
+    @AllArgsConstructor
+    public static class NetworkInterfaceInfo {
+        private String name;
+        private String displayName;
+        private String macAddress;
+        private String[] ipv4;
+        private String[] ipv6;
+        private long mtu;
+        // 实时状态
+        private long uploadBytesPerSec;
+        private long downloadBytesPerSec;
+        private long totalUploadBytes;
+        private long totalDownloadBytes;
+    }
+}

+ 21 - 0
src/main/java/com/hwmonitor/model/dto/OverviewInfo.java

@@ -0,0 +1,21 @@
+package com.hwmonitor.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class OverviewInfo {
+    private CpuInfo cpu;
+    private MemoryInfo memory;
+    private DiskInfo disk;
+    private GpuInfo gpu;
+    private NetworkInfo network;
+    private Map<String, Boolean> alerts;
+}

+ 28 - 0
src/main/java/com/hwmonitor/model/dto/SystemInfo.java

@@ -0,0 +1,28 @@
+package com.hwmonitor.model.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SystemInfo {
+    private String manufacturer;
+    private String model;
+    private String serialNumber;
+    private String biosVersion;
+    private String biosManufacturer;
+    private String biosReleaseDate;
+    private String osName;
+    private String osVersion;
+    private String osArch;
+    private String baseboardManufacturer;
+    private String baseboardModel;
+    private String baseboardSerialNumber;
+    private Double systemTemperature;
+    private int[] fanSpeeds;
+    private Double cpuVoltage;
+}

+ 39 - 0
src/main/java/com/hwmonitor/model/entity/MetricRecord.java

@@ -0,0 +1,39 @@
+package com.hwmonitor.model.entity;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "metric_records", indexes = {
+        @Index(name = "idx_ts", columnList = "timestamp"),
+        @Index(name = "idx_cat", columnList = "category, metricKey, timestamp")
+})
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MetricRecord {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @Column(nullable = false)
+    private Long timestamp;
+
+    @Column(nullable = false)
+    private String category;
+
+    @Column(nullable = false)
+    private String metricKey;
+
+    @Column(name = "metric_value", nullable = false)
+    private Double metricValue;
+
+    private String unit;
+
+    private String device;
+}

+ 19 - 0
src/main/java/com/hwmonitor/repository/MetricRecordRepository.java

@@ -0,0 +1,19 @@
+package com.hwmonitor.repository;
+
+import com.hwmonitor.model.entity.MetricRecord;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface MetricRecordRepository extends JpaRepository<MetricRecord, Long> {
+
+    List<MetricRecord> findByCategoryAndTimestampBetweenOrderByTimestampAsc(
+            String category, Long from, Long to);
+
+    List<MetricRecord> findByCategoryAndMetricKeyAndTimestampBetweenOrderByTimestampAsc(
+            String category, String metricKey, Long from, Long to);
+
+    void deleteByTimestampBefore(Long timestamp);
+}

+ 134 - 0
src/main/java/com/hwmonitor/scheduler/MetricsScheduler.java

@@ -0,0 +1,134 @@
+package com.hwmonitor.scheduler;
+
+import com.hwmonitor.config.MonitorConfig;
+import com.hwmonitor.controller.MetricsWebSocketHandler;
+import com.hwmonitor.model.dto.OverviewInfo;
+import com.hwmonitor.model.entity.MetricRecord;
+import com.hwmonitor.service.HistoryService;
+import com.hwmonitor.service.MetricsService;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Slf4j
+@Component
+public class MetricsScheduler {
+
+    private final MetricsService metricsService;
+    private final HistoryService historyService;
+    private final MetricsWebSocketHandler webSocketHandler;
+    private final MonitorConfig monitorConfig;
+    private final TaskScheduler taskScheduler;
+
+    private final AtomicReference<ScheduledFuture<?>> scheduledFuture = new AtomicReference<>();
+
+    public MetricsScheduler(MetricsService metricsService,
+                            HistoryService historyService,
+                            MetricsWebSocketHandler webSocketHandler,
+                            MonitorConfig monitorConfig,
+                            TaskScheduler taskScheduler) {
+        this.metricsService = metricsService;
+        this.historyService = historyService;
+        this.webSocketHandler = webSocketHandler;
+        this.monitorConfig = monitorConfig;
+        this.taskScheduler = taskScheduler;
+    }
+
+    @PostConstruct
+    public void start() {
+        scheduleTask();
+        log.info("采集调度器已启动,间隔: {} 秒", monitorConfig.getCollectIntervalSeconds());
+    }
+
+    @PreDestroy
+    public void stop() {
+        ScheduledFuture<?> future = scheduledFuture.get();
+        if (future != null) {
+            future.cancel(false);
+        }
+    }
+
+    public void reschedule() {
+        ScheduledFuture<?> future = scheduledFuture.get();
+        if (future != null) {
+            future.cancel(false);
+        }
+        scheduleTask();
+        log.info("采集间隔已更新为: {} 秒", monitorConfig.getCollectIntervalSeconds());
+    }
+
+    private void scheduleTask() {
+        ScheduledFuture<?> future = taskScheduler.scheduleWithFixedDelay(
+                this::collectAndPush,
+                monitorConfig.getCollectIntervalSeconds() * 1000L
+        );
+        scheduledFuture.set(future);
+    }
+
+    public void collectAndPush() {
+        try {
+            OverviewInfo overview = metricsService.collectOverview();
+            long now = System.currentTimeMillis();
+
+            // 持久化到 H2
+            List<MetricRecord> records = new ArrayList<>();
+            if (overview.getCpu() != null) {
+                records.add(buildRecord(now, "cpu", "usage", overview.getCpu().getUsagePercent(), "%"));
+                if (overview.getCpu().getTemperature() != null) {
+                    records.add(buildRecord(now, "cpu", "temperature", overview.getCpu().getTemperature(), "°C"));
+                }
+            }
+            if (overview.getMemory() != null) {
+                records.add(buildRecord(now, "memory", "usage", overview.getMemory().getUsagePercent(), "%"));
+                records.add(buildRecord(now, "memory", "usedGb", overview.getMemory().getUsedBytes() / 1073741824.0, "GB"));
+            }
+            if (overview.getDisk() != null && overview.getDisk().getPartitions() != null) {
+                for (var p : overview.getDisk().getPartitions()) {
+                    records.add(buildRecord(now, "disk", p.getName() + "_usage", p.getUsagePercent(), "%"));
+                    records.add(buildRecord(now, "disk", p.getName() + "_read", p.getReadBytesPerSec(), "B/s"));
+                    records.add(buildRecord(now, "disk", p.getName() + "_write", p.getWriteBytesPerSec(), "B/s"));
+                }
+            }
+            if (overview.getNetwork() != null && overview.getNetwork().getInterfaces() != null) {
+                for (var net : overview.getNetwork().getInterfaces()) {
+                    records.add(buildRecord(now, "network", net.getName() + "_upload", net.getUploadBytesPerSec(), "B/s"));
+                    records.add(buildRecord(now, "network", net.getName() + "_download", net.getDownloadBytesPerSec(), "B/s"));
+                }
+            }
+
+            if (!records.isEmpty()) {
+                historyService.saveAll(records);
+            }
+
+            // WebSocket 推送
+            webSocketHandler.broadcastMetrics(overview);
+        } catch (Exception e) {
+            log.error("定时采集任务执行失败", e);
+        }
+    }
+
+    @Scheduled(cron = "0 0 3 * * ?")
+    public void cleanupOldRecords() {
+        long cutoff = System.currentTimeMillis() - 7L * 24 * 60 * 60 * 1000;
+        historyService.cleanupOldRecords(cutoff);
+        log.info("历史数据清理完成,清理时间点: {}", cutoff);
+    }
+
+    private MetricRecord buildRecord(long timestamp, String category, String key, double value, String unit) {
+        return MetricRecord.builder()
+                .timestamp(timestamp)
+                .category(category)
+                .metricKey(key)
+                .metricValue(value)
+                .unit(unit)
+                .build();
+    }
+}

+ 32 - 0
src/main/java/com/hwmonitor/service/HistoryService.java

@@ -0,0 +1,32 @@
+package com.hwmonitor.service;
+
+import com.hwmonitor.model.entity.MetricRecord;
+import com.hwmonitor.repository.MetricRecordRepository;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class HistoryService {
+
+    private final MetricRecordRepository repository;
+
+    public HistoryService(MetricRecordRepository repository) {
+        this.repository = repository;
+    }
+
+    public List<MetricRecord> queryHistory(String category, Long from, Long to) {
+        if (category != null && !category.isBlank()) {
+            return repository.findByCategoryAndTimestampBetweenOrderByTimestampAsc(category, from, to);
+        }
+        return repository.findByCategoryAndTimestampBetweenOrderByTimestampAsc("", from, to);
+    }
+
+    public void saveAll(List<MetricRecord> records) {
+        repository.saveAll(records);
+    }
+
+    public void cleanupOldRecords(long beforeTimestamp) {
+        repository.deleteByTimestampBefore(beforeTimestamp);
+    }
+}

+ 86 - 0
src/main/java/com/hwmonitor/service/MetricsService.java

@@ -0,0 +1,86 @@
+package com.hwmonitor.service;
+
+import com.hwmonitor.collector.*;
+import com.hwmonitor.model.dto.*;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import jakarta.annotation.PostConstruct;
+
+@Service
+@Slf4j
+@Getter
+public class MetricsService {
+
+    private final oshi.SystemInfo systemInfo;
+    private CpuCollector cpuCollector;
+    private MemoryCollector memoryCollector;
+    private DiskCollector diskCollector;
+    private GpuCollector gpuCollector;
+    private NetworkCollector networkCollector;
+    private SystemCollector systemCollector;
+
+    public MetricsService() {
+        this.systemInfo = new oshi.SystemInfo();
+    }
+
+    @PostConstruct
+    public void init() {
+        this.cpuCollector = new CpuCollector(systemInfo);
+        this.memoryCollector = new MemoryCollector(systemInfo);
+        this.diskCollector = new DiskCollector(systemInfo);
+        this.gpuCollector = new GpuCollector(systemInfo);
+        this.networkCollector = new NetworkCollector(systemInfo);
+        this.systemCollector = new SystemCollector(systemInfo);
+        log.info("硬件采集模块初始化完成");
+    }
+
+    public CpuInfo collectCpu() {
+        return cpuCollector.collect();
+    }
+
+    public MemoryInfo collectMemory() {
+        return memoryCollector.collect();
+    }
+
+    public DiskInfo collectDisk() {
+        return diskCollector.collect();
+    }
+
+    public GpuInfo collectGpu() {
+        return gpuCollector.collect();
+    }
+
+    public NetworkInfo collectNetwork() {
+        return networkCollector.collect();
+    }
+
+    public SystemInfo collectSystem() {
+        return systemCollector.collect();
+    }
+
+    public OverviewInfo collectOverview() {
+        CpuInfo cpu = collectCpu();
+        MemoryInfo memory = collectMemory();
+        DiskInfo disk = collectDisk();
+        GpuInfo gpu = collectGpu();
+        NetworkInfo network = collectNetwork();
+
+        var alerts = new java.util.HashMap<String, Boolean>();
+        alerts.put("cpuHigh", cpu.getUsagePercent() > 90);
+        alerts.put("memoryHigh", memory.getUsagePercent() > 85);
+        alerts.put("diskHigh", disk.getPartitions().stream()
+                .anyMatch(p -> p.getUsagePercent() > 90));
+        alerts.put("cpuTempHigh", cpu.getTemperature() != null && cpu.getTemperature() > 80);
+
+        return OverviewInfo.builder()
+                .cpu(cpu)
+                .memory(memory)
+                .disk(disk)
+                .gpu(gpu)
+                .network(network)
+                .alerts(alerts)
+                .build();
+    }
+}

+ 46 - 0
src/main/resources/application.yml

@@ -0,0 +1,46 @@
+server:
+  port: 9988
+  address: 127.0.0.1
+  error:
+    include-stacktrace: never
+    include-message: never
+    include-binding-errors: never
+
+spring:
+  threads:
+    virtual:
+      enabled: true
+  datasource:
+    url: jdbc:h2:file:./data/hwmonitor
+    driver-class-name: org.h2.Driver
+    username: sa
+    password:
+  jpa:
+    hibernate:
+      ddl-auto: update
+    show-sql: false
+    open-in-view: false
+  h2:
+    console:
+      enabled: false
+
+management:
+  endpoints:
+    web:
+      exposure:
+        include: health
+      base-path: /api/v1
+
+hwmonitor:
+  collect-interval-seconds: 30
+  history:
+    retention-days: 7
+  alert:
+    cpu-threshold: 90
+    memory-threshold: 85
+    disk-threshold: 90
+    temp-threshold: 80
+  security:
+    allowed-origins: http://localhost:9988
+    enable-token: false
+    read-only: true

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 8 - 0
src/main/resources/static/assets/index-IcvV1nCx.js


+ 13 - 0
src/main/resources/static/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>硬件监控仪表盘</title>
+    <script type="module" crossorigin src="/assets/index-IcvV1nCx.js"></script>
+  </head>
+  <body>
+    <div id="root"></div>
+  </body>
+</html>

+ 4 - 0
src/main/resources/static/vite.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
+  <rect width="100" height="100" rx="10" fill="#1677ff"/>
+  <text x="50" y="65" text-anchor="middle" fill="white" font-size="50" font-family="Arial">HW</text>
+</svg>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است