From 139ca4c12831dc796fbf335c26f74dea82d48e54 Mon Sep 17 00:00:00 2001 From: Du Wenbo Date: Sun, 5 Apr 2026 23:39:31 +0800 Subject: [PATCH] feat!: backend-only architecture, remove frontend/nginx (v1.2.0) - Remove frontend/ and nginx/ from core (each customer owns their frontend) - Update docker-compose.yml to backend-only (postgres + redis + backend) - Update docker-compose.prod.yml to backend-only - Add CLAUDE.md with dev guidelines - Update README.md with new architecture diagram Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 33 + README.md | 99 +- VERSION | 2 +- VERSIONS.json | 6 + docker-compose.prod.yml | 22 - docker-compose.yml | 12 - frontend/.gitignore | 24 - frontend/Dockerfile | 11 - frontend/Dockerfile.prod | 20 - frontend/README.md | 73 - frontend/eslint.config.js | 23 - frontend/index.html | 13 - frontend/package-lock.json | 6012 ----------------- frontend/package.json | 45 - frontend/public/devices/default.svg | 19 - frontend/public/devices/heat_meter.svg | 30 - frontend/public/devices/heat_pump.svg | 46 - frontend/public/devices/meter.svg | 41 - frontend/public/devices/pv_inverter.svg | 42 - frontend/public/devices/sensor.svg | 39 - frontend/public/devices/water_meter.svg | 41 - frontend/public/favicon.svg | 1 - frontend/public/icons.svg | 24 - frontend/src/App.tsx | 84 - frontend/src/assets/hero.png | Bin 44919 -> 0 bytes frontend/src/assets/react.svg | 1 - frontend/src/assets/vite.svg | 1 - frontend/src/contexts/ThemeContext.tsx | 33 - frontend/src/hooks/useRealtimeWebSocket.ts | 196 - frontend/src/i18n/index.ts | 18 - frontend/src/i18n/locales/en.json | 64 - frontend/src/i18n/locales/zh.json | 64 - frontend/src/index.css | 84 - frontend/src/layouts/MainLayout.tsx | 224 - frontend/src/main.tsx | 10 - frontend/src/pages/AIOperations/index.tsx | 859 --- frontend/src/pages/Alarms/index.tsx | 314 - frontend/src/pages/Analysis/CostAnalysis.tsx | 245 - frontend/src/pages/Analysis/LossAnalysis.tsx | 107 - frontend/src/pages/Analysis/MomAnalysis.tsx | 130 - .../src/pages/Analysis/SubitemAnalysis.tsx | 222 - frontend/src/pages/Analysis/YoyAnalysis.tsx | 108 - frontend/src/pages/Analysis/index.tsx | 336 - .../pages/BigScreen/components/AlarmCard.tsx | 91 - .../BigScreen/components/AnimatedNumber.tsx | 38 - .../pages/BigScreen/components/CarbonCard.tsx | 139 - .../components/EnergyFlowDiagram.tsx | 190 - .../components/EnergyOverviewCard.tsx | 101 - .../BigScreen/components/HeatPumpCard.tsx | 96 - .../BigScreen/components/LoadCurveCard.tsx | 83 - .../src/pages/BigScreen/components/PVCard.tsx | 110 - frontend/src/pages/BigScreen/index.tsx | 195 - .../src/pages/BigScreen/styles.module.css | 658 -- .../BigScreen3D/components/Buildings.tsx | 130 - .../BigScreen3D/components/CampusScene.tsx | 182 - .../components/DeviceDetailView.tsx | 490 -- .../components/DeviceInfoPanel.tsx | 178 - .../components/DeviceListPanel.tsx | 85 - .../BigScreen3D/components/DeviceMarkers.tsx | 200 - .../components/EnergyParticles.tsx | 164 - .../pages/BigScreen3D/components/Ground.tsx | 22 - .../BigScreen3D/components/HUDOverlay.tsx | 93 - .../BigScreen3D/components/HeatPumps.tsx | 147 - .../pages/BigScreen3D/components/PVPanels.tsx | 107 - .../components/SceneEnvironment.tsx | 24 - frontend/src/pages/BigScreen3D/constants.ts | 120 - .../BigScreen3D/hooks/useCameraAnimation.ts | 69 - .../pages/BigScreen3D/hooks/useDeviceData.ts | 129 - .../pages/BigScreen3D/hooks/useEnergyFlow.ts | 33 - frontend/src/pages/BigScreen3D/index.tsx | 132 - .../src/pages/BigScreen3D/styles.module.css | 329 - frontend/src/pages/BigScreen3D/types.ts | 73 - frontend/src/pages/Carbon/index.tsx | 626 -- frontend/src/pages/Charging/Dashboard.tsx | 169 - frontend/src/pages/Charging/Orders.tsx | 201 - frontend/src/pages/Charging/Piles.tsx | 203 - frontend/src/pages/Charging/Pricing.tsx | 193 - frontend/src/pages/Charging/Stations.tsx | 180 - frontend/src/pages/Charging/index.tsx | 23 - .../Dashboard/components/DeviceStatus.tsx | 26 - .../pages/Dashboard/components/EnergyFlow.tsx | 96 - .../Dashboard/components/EnergyOverview.tsx | 33 - .../pages/Dashboard/components/LoadCurve.tsx | 40 - .../Dashboard/components/PowerGeneration.tsx | 45 - frontend/src/pages/Dashboard/index.tsx | 143 - frontend/src/pages/DataQuery/index.tsx | 365 - frontend/src/pages/DeviceDetail/index.tsx | 490 -- frontend/src/pages/Devices/Topology.tsx | 186 - frontend/src/pages/Devices/index.tsx | 312 - .../src/pages/EnergyStrategy/CostAnalysis.tsx | 130 - .../pages/EnergyStrategy/PricingConfig.tsx | 259 - .../pages/EnergyStrategy/SavingsReport.tsx | 98 - .../pages/EnergyStrategy/StrategyManager.tsx | 91 - .../EnergyStrategy/StrategySimulator.tsx | 157 - .../src/pages/EnergyStrategy/WeatherPanel.tsx | 145 - frontend/src/pages/EnergyStrategy/index.tsx | 57 - frontend/src/pages/Login/index.tsx | 88 - frontend/src/pages/Maintenance/index.tsx | 399 -- frontend/src/pages/Management/index.tsx | 524 -- frontend/src/pages/Monitoring/index.tsx | 79 - .../src/pages/Prediction/AccuracyMetrics.tsx | 151 - .../src/pages/Prediction/LoadForecast.tsx | 119 - .../pages/Prediction/OptimizationPanel.tsx | 212 - frontend/src/pages/Prediction/PVForecast.tsx | 132 - .../src/pages/Prediction/SavingsReport.tsx | 180 - frontend/src/pages/Prediction/index.tsx | 43 - frontend/src/pages/Quota/index.tsx | 263 - frontend/src/pages/Reports/index.tsx | 129 - frontend/src/pages/System/AuditLog.tsx | 174 - frontend/src/pages/System/Settings.tsx | 110 - frontend/src/pages/System/index.tsx | 142 - frontend/src/services/api.ts | 353 - frontend/src/utils/auth.ts | 9 - frontend/src/utils/devicePhoto.ts | 21 - frontend/tsconfig.app.json | 28 - frontend/tsconfig.json | 7 - frontend/tsconfig.node.json | 26 - frontend/vite.config.ts | 29 - nginx/Dockerfile | 20 - nginx/nginx.conf | 128 - 120 files changed, 64 insertions(+), 21756 deletions(-) create mode 100644 CLAUDE.md create mode 100644 VERSIONS.json delete mode 100644 frontend/.gitignore delete mode 100644 frontend/Dockerfile delete mode 100644 frontend/Dockerfile.prod delete mode 100644 frontend/README.md delete mode 100644 frontend/eslint.config.js delete mode 100644 frontend/index.html delete mode 100644 frontend/package-lock.json delete mode 100644 frontend/package.json delete mode 100644 frontend/public/devices/default.svg delete mode 100644 frontend/public/devices/heat_meter.svg delete mode 100644 frontend/public/devices/heat_pump.svg delete mode 100644 frontend/public/devices/meter.svg delete mode 100644 frontend/public/devices/pv_inverter.svg delete mode 100644 frontend/public/devices/sensor.svg delete mode 100644 frontend/public/devices/water_meter.svg delete mode 100644 frontend/public/favicon.svg delete mode 100644 frontend/public/icons.svg delete mode 100644 frontend/src/App.tsx delete mode 100644 frontend/src/assets/hero.png delete mode 100644 frontend/src/assets/react.svg delete mode 100644 frontend/src/assets/vite.svg delete mode 100644 frontend/src/contexts/ThemeContext.tsx delete mode 100644 frontend/src/hooks/useRealtimeWebSocket.ts delete mode 100644 frontend/src/i18n/index.ts delete mode 100644 frontend/src/i18n/locales/en.json delete mode 100644 frontend/src/i18n/locales/zh.json delete mode 100644 frontend/src/index.css delete mode 100644 frontend/src/layouts/MainLayout.tsx delete mode 100644 frontend/src/main.tsx delete mode 100644 frontend/src/pages/AIOperations/index.tsx delete mode 100644 frontend/src/pages/Alarms/index.tsx delete mode 100644 frontend/src/pages/Analysis/CostAnalysis.tsx delete mode 100644 frontend/src/pages/Analysis/LossAnalysis.tsx delete mode 100644 frontend/src/pages/Analysis/MomAnalysis.tsx delete mode 100644 frontend/src/pages/Analysis/SubitemAnalysis.tsx delete mode 100644 frontend/src/pages/Analysis/YoyAnalysis.tsx delete mode 100644 frontend/src/pages/Analysis/index.tsx delete mode 100644 frontend/src/pages/BigScreen/components/AlarmCard.tsx delete mode 100644 frontend/src/pages/BigScreen/components/AnimatedNumber.tsx delete mode 100644 frontend/src/pages/BigScreen/components/CarbonCard.tsx delete mode 100644 frontend/src/pages/BigScreen/components/EnergyFlowDiagram.tsx delete mode 100644 frontend/src/pages/BigScreen/components/EnergyOverviewCard.tsx delete mode 100644 frontend/src/pages/BigScreen/components/HeatPumpCard.tsx delete mode 100644 frontend/src/pages/BigScreen/components/LoadCurveCard.tsx delete mode 100644 frontend/src/pages/BigScreen/components/PVCard.tsx delete mode 100644 frontend/src/pages/BigScreen/index.tsx delete mode 100644 frontend/src/pages/BigScreen/styles.module.css delete mode 100644 frontend/src/pages/BigScreen3D/components/Buildings.tsx delete mode 100644 frontend/src/pages/BigScreen3D/components/CampusScene.tsx delete mode 100644 frontend/src/pages/BigScreen3D/components/DeviceDetailView.tsx delete mode 100644 frontend/src/pages/BigScreen3D/components/DeviceInfoPanel.tsx delete mode 100644 frontend/src/pages/BigScreen3D/components/DeviceListPanel.tsx delete mode 100644 frontend/src/pages/BigScreen3D/components/DeviceMarkers.tsx delete mode 100644 frontend/src/pages/BigScreen3D/components/EnergyParticles.tsx delete mode 100644 frontend/src/pages/BigScreen3D/components/Ground.tsx delete mode 100644 frontend/src/pages/BigScreen3D/components/HUDOverlay.tsx delete mode 100644 frontend/src/pages/BigScreen3D/components/HeatPumps.tsx delete mode 100644 frontend/src/pages/BigScreen3D/components/PVPanels.tsx delete mode 100644 frontend/src/pages/BigScreen3D/components/SceneEnvironment.tsx delete mode 100644 frontend/src/pages/BigScreen3D/constants.ts delete mode 100644 frontend/src/pages/BigScreen3D/hooks/useCameraAnimation.ts delete mode 100644 frontend/src/pages/BigScreen3D/hooks/useDeviceData.ts delete mode 100644 frontend/src/pages/BigScreen3D/hooks/useEnergyFlow.ts delete mode 100644 frontend/src/pages/BigScreen3D/index.tsx delete mode 100644 frontend/src/pages/BigScreen3D/styles.module.css delete mode 100644 frontend/src/pages/BigScreen3D/types.ts delete mode 100644 frontend/src/pages/Carbon/index.tsx delete mode 100644 frontend/src/pages/Charging/Dashboard.tsx delete mode 100644 frontend/src/pages/Charging/Orders.tsx delete mode 100644 frontend/src/pages/Charging/Piles.tsx delete mode 100644 frontend/src/pages/Charging/Pricing.tsx delete mode 100644 frontend/src/pages/Charging/Stations.tsx delete mode 100644 frontend/src/pages/Charging/index.tsx delete mode 100644 frontend/src/pages/Dashboard/components/DeviceStatus.tsx delete mode 100644 frontend/src/pages/Dashboard/components/EnergyFlow.tsx delete mode 100644 frontend/src/pages/Dashboard/components/EnergyOverview.tsx delete mode 100644 frontend/src/pages/Dashboard/components/LoadCurve.tsx delete mode 100644 frontend/src/pages/Dashboard/components/PowerGeneration.tsx delete mode 100644 frontend/src/pages/Dashboard/index.tsx delete mode 100644 frontend/src/pages/DataQuery/index.tsx delete mode 100644 frontend/src/pages/DeviceDetail/index.tsx delete mode 100644 frontend/src/pages/Devices/Topology.tsx delete mode 100644 frontend/src/pages/Devices/index.tsx delete mode 100644 frontend/src/pages/EnergyStrategy/CostAnalysis.tsx delete mode 100644 frontend/src/pages/EnergyStrategy/PricingConfig.tsx delete mode 100644 frontend/src/pages/EnergyStrategy/SavingsReport.tsx delete mode 100644 frontend/src/pages/EnergyStrategy/StrategyManager.tsx delete mode 100644 frontend/src/pages/EnergyStrategy/StrategySimulator.tsx delete mode 100644 frontend/src/pages/EnergyStrategy/WeatherPanel.tsx delete mode 100644 frontend/src/pages/EnergyStrategy/index.tsx delete mode 100644 frontend/src/pages/Login/index.tsx delete mode 100644 frontend/src/pages/Maintenance/index.tsx delete mode 100644 frontend/src/pages/Management/index.tsx delete mode 100644 frontend/src/pages/Monitoring/index.tsx delete mode 100644 frontend/src/pages/Prediction/AccuracyMetrics.tsx delete mode 100644 frontend/src/pages/Prediction/LoadForecast.tsx delete mode 100644 frontend/src/pages/Prediction/OptimizationPanel.tsx delete mode 100644 frontend/src/pages/Prediction/PVForecast.tsx delete mode 100644 frontend/src/pages/Prediction/SavingsReport.tsx delete mode 100644 frontend/src/pages/Prediction/index.tsx delete mode 100644 frontend/src/pages/Quota/index.tsx delete mode 100644 frontend/src/pages/Reports/index.tsx delete mode 100644 frontend/src/pages/System/AuditLog.tsx delete mode 100644 frontend/src/pages/System/Settings.tsx delete mode 100644 frontend/src/pages/System/index.tsx delete mode 100644 frontend/src/services/api.ts delete mode 100644 frontend/src/utils/auth.ts delete mode 100644 frontend/src/utils/devicePhoto.ts delete mode 100644 frontend/tsconfig.app.json delete mode 100644 frontend/tsconfig.json delete mode 100644 frontend/tsconfig.node.json delete mode 100644 frontend/vite.config.ts delete mode 100644 nginx/Dockerfile delete mode 100644 nginx/nginx.conf diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..68e28ac --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,33 @@ +# EMS Core Development Guidelines + +## Overview +This is the shared core library for all EMS customer projects. Changes here affect ALL customer deployments. + +## Golden Rules +- All changes must go through Pull Request with code review +- Follow Conventional Commits: `(): ` +- Types: feat, fix, refactor, docs, test, chore, style, perf +- Scopes: api, models, services, collector, hooks, tasks, config + +## Architecture +- `backend/app/api/v1/` — API endpoint modules +- `backend/app/collectors/` — Device data collectors (Sungrow, Modbus, MQTT, HTTP) +- `backend/app/hooks/` — Customer plugin system (base + loader) +- `backend/app/models/` — SQLAlchemy ORM models +- `backend/app/services/` — Business logic layer +- `backend/app/tasks/` — Background tasks (Celery + APScheduler) +- `backend/alembic/` — Database migrations +- **Note:** Frontend is NOT included in ems-core. Each customer repo owns its own frontend. + +## Database Migrations +- Every model change requires an Alembic migration +- Test migrations before deploying: `alembic upgrade head` +- Never manually edit `alembic_version` table + +## Testing +- Run tests: `cd backend && pytest` +- Minimum test coverage for new features + +## Version +- Follow SemVer (MAJOR.MINOR.PATCH) +- Current: see VERSION file diff --git a/README.md b/README.md index c21e5da..ca9c28e 100644 --- a/README.md +++ b/README.md @@ -21,29 +21,23 @@ ## 系统架构 ``` - ┌──────────────┐ - │ Nginx │ - │ 反向代理 │ - │ :80 / :443 │ - └──────┬───────┘ - │ - ┌───────────────┼───────────────┐ - │ │ - ┌──────▼──────┐ ┌────────▼────────┐ - │ Frontend │ │ Backend │ - │ React 19 │ │ FastAPI │ - │ Ant Design │ │ :8000 │ - │ ECharts │ │ │ - │ Three.js │ │ /api/v1/* │ - └─────────────┘ └───────┬──────────┘ - │ - ┌──────────────┼──────────────┐ - │ │ - ┌──────▼──────┐ ┌───────▼───────┐ - │ TimescaleDB │ │ Redis │ - │ PostgreSQL │ │ 缓存/队列 │ - │ :5432 │ │ :6379 │ - └─────────────┘ └───────────────┘ + ┌────────────────────┐ + │ Backend (Core) │ + │ FastAPI │ + │ :8000 │ + │ /api/v1/* │ + └─────────┬──────────┘ + │ + ┌────────────┼────────────┐ + │ │ +┌──────▼──────┐ ┌───────▼───────┐ +│ TimescaleDB │ │ Redis │ +│ PostgreSQL │ │ 缓存/队列 │ +│ :5432 │ │ :6379 │ +└─────────────┘ └───────────────┘ + +Note: Frontend and Nginx are NOT part of ems-core. +Each customer repo provides its own frontend and reverse proxy. ``` --- @@ -52,10 +46,6 @@ | 层级 | 技术 | |------|------| -| 前端框架 | React 19 + TypeScript | -| UI 组件库 | Ant Design 5 + ProComponents | -| 数据可视化 | ECharts 6 | -| 3D 渲染 | Three.js + React Three Fiber | | 后端框架 | FastAPI (Python 3.11) | | ORM | SQLAlchemy 2.0 (async) | | 数据库 | TimescaleDB (PostgreSQL 16) | @@ -101,7 +91,6 @@ bash scripts/quick-start.sh | 服务 | 地址 | |------|------| -| 前端页面 | http://localhost:3000 | | 后端 API | http://localhost:8000 | | API 文档 (Swagger) | http://localhost:8000/docs | | 健康检查 | http://localhost:8000/health | @@ -134,18 +123,6 @@ pip install -r requirements.txt uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload ``` -### 前端开发 - -```bash -cd frontend - -# 安装依赖 -npm install - -# 启动开发服务器 -npm run dev -``` - ### 仅启动基础设施 ```bash @@ -162,49 +139,28 @@ docker-compose up -d postgres redis docker-compose -f docker-compose.prod.yml up -d ``` -生产环境使用 Nginx 反向代理,前端编译为静态文件,后端使用 Gunicorn + Uvicorn workers。 +生产环境后端使用 Gunicorn + Uvicorn workers。Nginx 和前端由各客户项目自行提供。 --- ## 项目结构 ``` -tianpu-ems/ -├── backend/ # 后端服务 +ems-core/ +├── backend/ # 后端服务 (Backend-only core) │ ├── app/ │ │ ├── api/ # API 路由 │ │ │ └── v1/ # v1 版本接口 │ │ ├── collectors/ # 数据采集器 │ │ ├── core/ # 核心配置 +│ │ ├── hooks/ # 客户插件系统 │ │ ├── models/ # 数据模型 │ │ ├── services/ # 业务逻辑 │ │ ├── tasks/ # 后台任务 │ │ └── main.py # 应用入口 +│ ├── alembic/ # 数据库迁移 │ ├── Dockerfile │ └── requirements.txt -├── frontend/ # 前端应用 -│ ├── src/ -│ │ ├── components/ # 公共组件 -│ │ ├── layouts/ # 布局组件 -│ │ ├── pages/ # 页面组件 -│ │ │ ├── BigScreen/ # 数据大屏 -│ │ │ ├── BigScreen3D/ # 3D 大屏 -│ │ │ ├── Dashboard/ # 仪表盘 -│ │ │ ├── Monitoring/ # 实时监控 -│ │ │ ├── Devices/ # 设备管理 -│ │ │ ├── Analysis/ # 能耗分析 -│ │ │ ├── Carbon/ # 碳排放管理 -│ │ │ ├── Alarms/ # 告警中心 -│ │ │ ├── Reports/ # 报表中心 -│ │ │ ├── System/ # 系统管理 -│ │ │ └── Login/ # 登录页 -│ │ ├── services/ # API 服务 -│ │ └── utils/ # 工具函数 -│ ├── Dockerfile -│ └── package.json -├── nginx/ # Nginx 配置 -│ ├── nginx.conf -│ └── Dockerfile ├── scripts/ # 脚本工具 │ ├── init_db.py # 数据库初始化 │ ├── seed_data.py # 种子数据 @@ -234,16 +190,9 @@ tianpu-ems/ --- -## 截图预览 +## 说明 -> 截图待补充 - -| 页面 | 说明 | -|------|------| -| 数据大屏 | 园区能源全景概览 | -| 3D 园区 | 三维可视化园区模型 | -| 仪表盘 | 关键能耗指标看板 | -| 设备监控 | 设备运行状态实时监控 | +ems-core 是后端核心库,不包含前端代码。每个客户项目(如 tianpu-ems)拥有自己的前端仓库,独立部署和定制 UI。 --- diff --git a/VERSION b/VERSION index 9084fa2..26aaba0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.0 +1.2.0 diff --git a/VERSIONS.json b/VERSIONS.json new file mode 100644 index 0000000..dd3132f --- /dev/null +++ b/VERSIONS.json @@ -0,0 +1,6 @@ +{ + "project": "ems-core", + "project_version": "1.1.0", + "last_updated": "2026-04-05", + "notes": "Core platform with hooks plugin system" +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index bedcc98..f4f4d60 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,28 +1,6 @@ version: '3.8' services: - nginx: - build: - context: . - dockerfile: nginx/Dockerfile - container_name: tianpu_nginx - ports: - - "80:80" - depends_on: - backend: - condition: service_healthy - restart: always - healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost/health"] - interval: 30s - timeout: 10s - retries: 3 - logging: - driver: json-file - options: - max-size: "10m" - max-file: "3" - postgres: image: timescale/timescaledb:latest-pg16 container_name: tianpu_db diff --git a/docker-compose.yml b/docker-compose.yml index 74b07fc..070b626 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,18 +46,6 @@ services: condition: service_healthy command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload - frontend: - build: ./frontend - container_name: tianpu_frontend - ports: - - "3000:3000" - volumes: - - ./frontend:/app - - /app/node_modules - depends_on: - - backend - command: npm run dev - volumes: pgdata: redisdata: diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index a547bf3..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index 381b906..0000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM node:20-alpine - -WORKDIR /app - -COPY package*.json ./ -RUN npm install - -COPY . . - -EXPOSE 3000 -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod deleted file mode 100644 index 983f6c9..0000000 --- a/frontend/Dockerfile.prod +++ /dev/null @@ -1,20 +0,0 @@ -# Multi-stage production build for standalone use -# In docker-compose.prod.yml, the nginx Dockerfile handles frontend building directly - -# Stage 1: Build -FROM node:20-alpine AS builder -WORKDIR /app -COPY package*.json ./ -RUN npm ci -COPY . . -RUN npm run build - -# Stage 2: Serve with nginx -FROM nginx:1.27-alpine -COPY --from=builder /app/dist /usr/share/nginx/html - -# SPA fallback -RUN echo 'server { listen 80; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }' > /etc/nginx/conf.d/default.conf - -EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 7dbf7eb..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` - -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index 5e6b472..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index 14bcc3c..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - 天普智慧能源管理平台 - - -
- - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index e26b189..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,6012 +0,0 @@ -{ - "name": "frontend", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.0.0", - "dependencies": { - "@ant-design/icons": "^6.1.1", - "@ant-design/pro-components": "^2.8.10", - "@react-three/drei": "^10.7.7", - "@react-three/fiber": "^9.5.0", - "@react-three/postprocessing": "^3.0.4", - "antd": "^5.29.3", - "axios": "^1.14.0", - "dayjs": "^1.11.20", - "echarts": "^6.0.0", - "echarts-for-react": "^3.0.6", - "i18next": "^24.2.2", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "react-i18next": "^15.4.1", - "react-router-dom": "^6.30.3", - "three": "^0.183.2" - }, - "devDependencies": { - "@eslint/js": "^9.39.4", - "@types/node": "^24.12.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@types/three": "^0.183.1", - "@vitejs/plugin-react": "^6.0.1", - "eslint": "^9.39.4", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.4.0", - "typescript": "~5.9.3", - "typescript-eslint": "^8.57.0", - "vite": "^8.0.1" - } - }, - "node_modules/@ant-design/colors": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", - "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", - "license": "MIT", - "dependencies": { - "@ant-design/fast-color": "^3.0.0" - } - }, - "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": "3.0.1", - "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz", - "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==", - "license": "MIT", - "engines": { - "node": ">=8.x" - } - }, - "node_modules/@ant-design/icons": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.1.tgz", - "integrity": "sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q==", - "license": "MIT", - "dependencies": { - "@ant-design/colors": "^8.0.0", - "@ant-design/icons-svg": "^4.4.0", - "@rc-component/util": "^1.3.0", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/@ant-design/icons-svg": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", - "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", - "license": "MIT" - }, - "node_modules/@ant-design/pro-card": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@ant-design/pro-card/-/pro-card-2.10.0.tgz", - "integrity": "sha512-sLONn1odmE0Wkbse8pol4WiaEzBV8JU5s3FAMflPpycfUcbSaa1ktXzQ7LCo2SAvOS7gkfmpFjBPtrfbigKh4g==", - "license": "MIT", - "dependencies": { - "@ant-design/cssinjs": "^1.21.1", - "@ant-design/icons": "^5.0.0", - "@ant-design/pro-provider": "2.16.2", - "@ant-design/pro-utils": "2.18.0", - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "rc-resize-observer": "^1.0.0", - "rc-util": "^5.4.0" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "react": ">=17.0.0" - } - }, - "node_modules/@ant-design/pro-card/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/pro-card/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/pro-card/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/pro-components": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/@ant-design/pro-components/-/pro-components-2.8.10.tgz", - "integrity": "sha512-QHnnIXdmC5GTAtm6i8eeJy5yT9npPlFyxpDm+duiDrTRKRFaAQBduArxlH3DA/hoRCCypzPONxfK9BQNIhIyZA==", - "license": "MIT", - "dependencies": { - "@ant-design/pro-card": "2.10.0", - "@ant-design/pro-descriptions": "2.6.10", - "@ant-design/pro-field": "3.1.0", - "@ant-design/pro-form": "2.32.0", - "@ant-design/pro-layout": "7.22.7", - "@ant-design/pro-list": "2.6.10", - "@ant-design/pro-provider": "2.16.2", - "@ant-design/pro-skeleton": "2.2.1", - "@ant-design/pro-table": "3.21.0", - "@ant-design/pro-utils": "2.18.0", - "@babel/runtime": "^7.16.3" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@ant-design/pro-descriptions": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/@ant-design/pro-descriptions/-/pro-descriptions-2.6.10.tgz", - "integrity": "sha512-+4MbiOfumnWlW0Awm4m8JML5o3lR649FD24AaivCmr8BQvIAAXdTITnDMXEg8BqvdP4KOvNsStZrvYfqoev33A==", - "license": "MIT", - "dependencies": { - "@ant-design/pro-field": "3.1.0", - "@ant-design/pro-form": "2.32.0", - "@ant-design/pro-provider": "2.16.2", - "@ant-design/pro-skeleton": "2.2.1", - "@ant-design/pro-utils": "2.18.0", - "@babel/runtime": "^7.18.0", - "rc-resize-observer": "^0.2.3", - "rc-util": "^5.0.6" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "react": ">=17.0.0" - } - }, - "node_modules/@ant-design/pro-descriptions/node_modules/rc-resize-observer": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-0.2.6.tgz", - "integrity": "sha512-YX6nYnd6fk7zbuvT6oSDMKiZjyngjHoy+fz+vL3Tez38d/G5iGdaDJa2yE7345G6sc4Mm1IGRUIwclvltddhmA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.2.1", - "rc-util": "^5.0.0", - "resize-observer-polyfill": "^1.5.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@ant-design/pro-field": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@ant-design/pro-field/-/pro-field-3.1.0.tgz", - "integrity": "sha512-+Dgp31WjD+iwg9KIRAMgNkfQivkJKMcYBrIBmho1e8ep/O0HgWSp48g70tBIWi/Lfem/Ky2schF7O8XCFouczw==", - "license": "MIT", - "dependencies": { - "@ant-design/icons": "^5.0.0", - "@ant-design/pro-provider": "2.16.2", - "@ant-design/pro-utils": "2.18.0", - "@babel/runtime": "^7.18.0", - "@chenshuai2144/sketch-color": "^1.0.8", - "classnames": "^2.3.2", - "dayjs": "^1.11.10", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "rc-util": "^5.4.0", - "swr": "^2.0.0" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "react": ">=17.0.0" - } - }, - "node_modules/@ant-design/pro-field/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/pro-field/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/pro-field/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/pro-form": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/@ant-design/pro-form/-/pro-form-2.32.0.tgz", - "integrity": "sha512-GZnVAMeYv+YHJb17lJ7rX5PYuQPvEA6EotQnPbHi9tGLN3PfexcAd21rqzuO+OrulU2x7TEMDIxtY9MzvvOGbg==", - "license": "MIT", - "dependencies": { - "@ant-design/icons": "^5.0.0", - "@ant-design/pro-field": "3.1.0", - "@ant-design/pro-provider": "2.16.2", - "@ant-design/pro-utils": "2.18.0", - "@babel/runtime": "^7.18.0", - "@chenshuai2144/sketch-color": "^1.0.7", - "@umijs/use-params": "^1.0.9", - "classnames": "^2.3.2", - "dayjs": "^1.11.10", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "rc-resize-observer": "^1.1.0", - "rc-util": "^5.0.6" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "rc-field-form": ">=1.22.0", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@ant-design/pro-form/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/pro-form/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/pro-form/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/pro-layout": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.7.tgz", - "integrity": "sha512-fvmtNA1r9SaasVIQIQt611VSlNxtVxDbQ3e+1GhYQza3tVJi/3gCZuDyfMfTnbLmf3PaW/YvLkn7MqDbzAzoLA==", - "license": "MIT", - "dependencies": { - "@ant-design/cssinjs": "^1.21.1", - "@ant-design/icons": "^5.0.0", - "@ant-design/pro-provider": "2.16.2", - "@ant-design/pro-utils": "2.18.0", - "@babel/runtime": "^7.18.0", - "@umijs/route-utils": "^4.0.0", - "@umijs/use-params": "^1.0.9", - "classnames": "^2.3.2", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "path-to-regexp": "8.2.0", - "rc-resize-observer": "^1.1.0", - "rc-util": "^5.0.6", - "swr": "^2.0.0", - "warning": "^4.0.3" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@ant-design/pro-layout/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/pro-layout/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/pro-layout/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/pro-list": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/@ant-design/pro-list/-/pro-list-2.6.10.tgz", - "integrity": "sha512-xSWwnqCr+hPEYR4qY7nFUaxO5RQBxNlFaPNmobP2i+Im31slk9JuAusgWeIYO0mNhLJuLbxd8CCma2AZij3fBQ==", - "license": "MIT", - "dependencies": { - "@ant-design/cssinjs": "^1.21.1", - "@ant-design/icons": "^5.0.0", - "@ant-design/pro-card": "2.10.0", - "@ant-design/pro-field": "3.1.0", - "@ant-design/pro-table": "3.21.0", - "@ant-design/pro-utils": "2.18.0", - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "dayjs": "^1.11.10", - "rc-resize-observer": "^1.0.0", - "rc-util": "^4.19.0" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@ant-design/pro-list/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/pro-list/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/pro-list/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/pro-list/node_modules/@ant-design/icons/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/@ant-design/pro-list/node_modules/rc-util": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.21.1.tgz", - "integrity": "sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==", - "license": "MIT", - "dependencies": { - "add-dom-event-listener": "^1.1.0", - "prop-types": "^15.5.10", - "react-is": "^16.12.0", - "react-lifecycles-compat": "^3.0.4", - "shallowequal": "^1.1.0" - } - }, - "node_modules/@ant-design/pro-list/node_modules/rc-util/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/@ant-design/pro-provider": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@ant-design/pro-provider/-/pro-provider-2.16.2.tgz", - "integrity": "sha512-0KmCH1EaOND787Jz6VRMYtLNZmqfT0JPjdUfxhyOxFfnBRfrjyfZgIa6CQoAJLEUMWv57PccWS8wRHVUUk2Yiw==", - "license": "MIT", - "dependencies": { - "@ant-design/cssinjs": "^1.21.1", - "@babel/runtime": "^7.18.0", - "@ctrl/tinycolor": "^3.4.0", - "dayjs": "^1.11.10", - "rc-util": "^5.0.1", - "swr": "^2.0.0" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@ant-design/pro-skeleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ant-design/pro-skeleton/-/pro-skeleton-2.2.1.tgz", - "integrity": "sha512-3M2jNOZQZWEDR8pheY00OkHREfb0rquvFZLCa6DypGmiksiuuYuR9Y4iA82ZF+mva2FmpHekdwbje/GpbxqBeg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.0" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@ant-design/pro-table": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@ant-design/pro-table/-/pro-table-3.21.0.tgz", - "integrity": "sha512-sI81d3FYRv5sXamUc+M5CsHZ9CchuUQgOAPzo5H4oPAVL5h+mkYGRsBzPsxQX7khTNpWjrAtPoRm5ipx3vvWog==", - "license": "MIT", - "dependencies": { - "@ant-design/cssinjs": "^1.21.1", - "@ant-design/icons": "^5.0.0", - "@ant-design/pro-card": "2.10.0", - "@ant-design/pro-field": "3.1.0", - "@ant-design/pro-form": "2.32.0", - "@ant-design/pro-provider": "2.16.2", - "@ant-design/pro-utils": "2.18.0", - "@babel/runtime": "^7.18.0", - "@dnd-kit/core": "^6.0.8", - "@dnd-kit/modifiers": "^6.0.1", - "@dnd-kit/sortable": "^7.0.2", - "@dnd-kit/utilities": "^3.2.1", - "classnames": "^2.3.2", - "dayjs": "^1.11.10", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "rc-resize-observer": "^1.0.0", - "rc-util": "^5.0.1" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "rc-field-form": ">=1.22.0", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@ant-design/pro-table/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/pro-table/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/pro-table/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/pro-utils": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.18.0.tgz", - "integrity": "sha512-8+ikyrN8L8a8Ph4oeHTOJEiranTj18+9+WHCHjKNdEfukI7Rjn8xpYdLJWb2AUJkb9d4eoAqjd5+k+7w81Df0w==", - "license": "MIT", - "dependencies": { - "@ant-design/icons": "^5.0.0", - "@ant-design/pro-provider": "2.16.2", - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "dayjs": "^1.11.10", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "rc-util": "^5.0.6", - "safe-stable-stringify": "^2.4.3", - "swr": "^2.0.0" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@ant-design/pro-utils/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/pro-utils/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/pro-utils/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/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.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@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.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@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.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "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.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@chenshuai2144/sketch-color": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@chenshuai2144/sketch-color/-/sketch-color-1.0.9.tgz", - "integrity": "sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w==", - "license": "MIT", - "dependencies": { - "reactcss": "^1.2.3", - "tinycolor2": "^1.4.2" - }, - "peerDependencies": { - "react": ">=16.12.0" - } - }, - "node_modules/@ctrl/tinycolor": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", - "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@dimforge/rapier3d-compat": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", - "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", - "license": "Apache-2.0" - }, - "node_modules/@dnd-kit/accessibility": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", - "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", - "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@dnd-kit/accessibility": "^3.1.1", - "@dnd-kit/utilities": "^3.2.2", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/modifiers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz", - "integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==", - "license": "MIT", - "dependencies": { - "@dnd-kit/utilities": "^3.2.1", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.0.6", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/sortable": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", - "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", - "license": "MIT", - "dependencies": { - "@dnd-kit/utilities": "^3.2.0", - "tslib": "^2.0.0" - }, - "peerDependencies": { - "@dnd-kit/core": "^6.0.7", - "react": ">=16.8.0" - } - }, - "node_modules/@dnd-kit/utilities": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", - "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.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/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "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/@mediapipe/tasks-vision": { - "version": "0.10.17", - "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", - "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", - "license": "Apache-2.0" - }, - "node_modules/@monogrid/gainmap-js": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", - "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", - "license": "MIT", - "dependencies": { - "promise-worker-transferable": "^1.0.4" - }, - "peerDependencies": { - "three": ">= 0.159.0" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@rc-component/async-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz", - "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", - "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/color-picker/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/@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.3", - "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz", - "integrity": "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==", - "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.1", - "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", - "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", - "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/@rc-component/util": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.10.0.tgz", - "integrity": "sha512-aY9GLBuiUdpyfIUpAWSYer4Tu3mVaZCo5A0q9NtXcazT3MRiI3/WNHCR+DUn5VAtR6iRRf0ynCqQUcHli5UdYw==", - "license": "MIT", - "dependencies": { - "is-mobile": "^5.0.0", - "react-is": "^18.2.0" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@react-three/drei": { - "version": "10.7.7", - "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz", - "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mediapipe/tasks-vision": "0.10.17", - "@monogrid/gainmap-js": "^3.0.6", - "@use-gesture/react": "^10.3.1", - "camera-controls": "^3.1.0", - "cross-env": "^7.0.3", - "detect-gpu": "^5.0.56", - "glsl-noise": "^0.0.0", - "hls.js": "^1.5.17", - "maath": "^0.10.8", - "meshline": "^3.3.1", - "stats-gl": "^2.2.8", - "stats.js": "^0.17.0", - "suspend-react": "^0.1.3", - "three-mesh-bvh": "^0.8.3", - "three-stdlib": "^2.35.6", - "troika-three-text": "^0.52.4", - "tunnel-rat": "^0.1.2", - "use-sync-external-store": "^1.4.0", - "utility-types": "^3.11.0", - "zustand": "^5.0.1" - }, - "peerDependencies": { - "@react-three/fiber": "^9.0.0", - "react": "^19", - "react-dom": "^19", - "three": ">=0.159" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/@react-three/fiber": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", - "integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.17.8", - "@types/webxr": "*", - "base64-js": "^1.5.1", - "buffer": "^6.0.3", - "its-fine": "^2.0.0", - "react-use-measure": "^2.1.7", - "scheduler": "^0.27.0", - "suspend-react": "^0.1.3", - "use-sync-external-store": "^1.4.0", - "zustand": "^5.0.3" - }, - "peerDependencies": { - "expo": ">=43.0", - "expo-asset": ">=8.4", - "expo-file-system": ">=11.0", - "expo-gl": ">=11.0", - "react": ">=19 <19.3", - "react-dom": ">=19 <19.3", - "react-native": ">=0.78", - "three": ">=0.156" - }, - "peerDependenciesMeta": { - "expo": { - "optional": true - }, - "expo-asset": { - "optional": true - }, - "expo-file-system": { - "optional": true - }, - "expo-gl": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "node_modules/@react-three/postprocessing": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@react-three/postprocessing/-/postprocessing-3.0.4.tgz", - "integrity": "sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==", - "license": "MIT", - "dependencies": { - "maath": "^0.6.0", - "n8ao": "^1.9.4", - "postprocessing": "^6.36.6" - }, - "peerDependencies": { - "@react-three/fiber": "^9.0.0", - "react": "^19.0", - "three": ">= 0.156.0" - } - }, - "node_modules/@react-three/postprocessing/node_modules/maath": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/maath/-/maath-0.6.0.tgz", - "integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==", - "license": "MIT", - "peerDependencies": { - "@types/three": ">=0.144.0", - "three": ">=0.144.0" - } - }, - "node_modules/@remix-run/router": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", - "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", - "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.7", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", - "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tweenjs/tween.js": { - "version": "23.1.3", - "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", - "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/draco3d": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", - "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.12.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", - "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/offscreencanvas": { - "version": "2019.7.3", - "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", - "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/react-reconciler": { - "version": "0.28.9", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", - "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/stats.js": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", - "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", - "license": "MIT" - }, - "node_modules/@types/three": { - "version": "0.183.1", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.183.1.tgz", - "integrity": "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@dimforge/rapier3d-compat": "~0.12.0", - "@tweenjs/tween.js": "~23.1.3", - "@types/stats.js": "*", - "@types/webxr": ">=0.5.17", - "@webgpu/types": "*", - "fflate": "~0.8.2", - "meshoptimizer": "~1.0.1" - } - }, - "node_modules/@types/webxr": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", - "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", - "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.58.0", - "@typescript-eslint/type-utils": "8.58.0", - "@typescript-eslint/utils": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.58.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", - "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@typescript-eslint/scope-manager": "8.58.0", - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", - "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.58.0", - "@typescript-eslint/types": "^8.58.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", - "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", - "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", - "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0", - "@typescript-eslint/utils": "8.58.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", - "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", - "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.58.0", - "@typescript-eslint/tsconfig-utils": "8.58.0", - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/visitor-keys": "8.58.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", - "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.58.0", - "@typescript-eslint/types": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", - "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.58.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@umijs/route-utils": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@umijs/route-utils/-/route-utils-4.0.3.tgz", - "integrity": "sha512-zPEcYhl1cSfkSRDzzGgoD1mDvGjxoOTJFvkn55srfgdQ3NZe2ZMCScCU6DEnOxuKP1XDVf8pqyqCDVd2+RCQIw==", - "license": "MIT" - }, - "node_modules/@umijs/use-params": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@umijs/use-params/-/use-params-1.0.9.tgz", - "integrity": "sha512-QlN0RJSBVQBwLRNxbxjQ5qzqYIGn+K7USppMoIOVlf7fxXHsnQZ2bEsa6Pm74bt6DVQxpUE8HqvdStn6Y9FV1w==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, - "node_modules/@use-gesture/core": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", - "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", - "license": "MIT" - }, - "node_modules/@use-gesture/react": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", - "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", - "license": "MIT", - "dependencies": { - "@use-gesture/core": "10.3.1" - }, - "peerDependencies": { - "react": ">= 16.8.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", - "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.7" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } - } - }, - "node_modules/@webgpu/types": { - "version": "0.1.69", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", - "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/add-dom-event-listener": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz", - "integrity": "sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==", - "license": "MIT", - "dependencies": { - "object-assign": "4.x" - } - }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "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", - "peer": true, - "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/antd/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/antd/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/antd/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/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.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.14.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", - "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz", - "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "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", - "peer": true, - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "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/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camera-controls": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", - "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", - "license": "MIT", - "engines": { - "node": ">=22.0.0", - "npm": ">=10.5.1" - }, - "peerDependencies": { - "three": ">=0.126.1" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001782", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz", - "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==", - "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/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "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/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "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/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "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/cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" - }, - "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "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.20", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", - "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "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/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-gpu": { - "version": "5.0.70", - "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", - "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", - "license": "MIT", - "dependencies": { - "webgl-constants": "^1.1.1" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/draco3d": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", - "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", - "license": "Apache-2.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": "6.0.0", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", - "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "2.3.0", - "zrender": "6.0.0" - } - }, - "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/echarts/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/electron-to-chromium": { - "version": "1.5.329", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.329.tgz", - "integrity": "sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==", - "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.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "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/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/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", - "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": "^9 || ^10" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "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/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "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/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "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.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "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/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glsl-noise": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", - "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", - "license": "MIT" - }, - "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-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/hls.js": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", - "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", - "license": "Apache-2.0" - }, - "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "license": "MIT", - "dependencies": { - "void-elements": "3.1.0" - } - }, - "node_modules/i18next": { - "version": "24.2.3", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz", - "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.26.10" - }, - "peerDependencies": { - "typescript": "^5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-mobile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", - "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", - "license": "MIT" - }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/its-fine": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", - "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", - "license": "MIT", - "dependencies": { - "@types/react-reconciler": "^0.28.9" - }, - "peerDependencies": { - "react": "^19.0.0" - } - }, - "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/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "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/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "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/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "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/maath": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", - "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", - "license": "MIT", - "peerDependencies": { - "@types/three": ">=0.134.0", - "three": ">=0.134.0" - } - }, - "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/meshline": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", - "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", - "license": "MIT", - "peerDependencies": { - "three": ">=0.137" - } - }, - "node_modules/meshoptimizer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", - "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", - "license": "MIT" - }, - "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/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/n8ao": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.1.tgz", - "integrity": "sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==", - "license": "ISC", - "peerDependencies": { - "postprocessing": ">=6.30.0", - "three": ">=0.137" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "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/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "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.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "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.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postprocessing": { - "version": "6.39.0", - "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.39.0.tgz", - "integrity": "sha512-/G6JY8hs426lcto/pBZlnFSkyEo1fHsh4gy7FPJtq1SaSUOzJgDW6f6f1K/+aMOYzK/eQEefyOb3++jPPIUeDA==", - "license": "Zlib", - "peer": true, - "peerDependencies": { - "three": ">= 0.168.0 < 0.184.0" - } - }, - "node_modules/potpack": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", - "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", - "license": "ISC" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/promise-worker-transferable": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", - "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", - "license": "Apache-2.0", - "dependencies": { - "is-promise": "^2.1.0", - "lie": "^3.0.2" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "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/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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", - "peer": true, - "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": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, - "node_modules/react-i18next": { - "version": "15.7.4", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz", - "integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.27.6", - "html-parse-stringify": "^3.0.1" - }, - "peerDependencies": { - "i18next": ">= 23.4.0", - "react": ">= 16.8.0", - "typescript": "^5" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "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-lifecycles-compat": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", - "license": "MIT" - }, - "node_modules/react-router": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", - "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", - "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.2", - "react-router": "6.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-use-measure": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", - "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.13", - "react-dom": ">=16.13" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/reactcss": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", - "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", - "license": "MIT", - "dependencies": { - "lodash": "^4.0.1" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/rolldown": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", - "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.12" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-x64": "1.0.0-rc.12", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" - } - }, - "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "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/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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/stats-gl": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", - "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", - "license": "MIT", - "dependencies": { - "@types/three": "*", - "three": "^0.170.0" - }, - "peerDependencies": { - "@types/three": "*", - "three": "*" - } - }, - "node_modules/stats-gl/node_modules/three": { - "version": "0.170.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", - "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", - "license": "MIT" - }, - "node_modules/stats.js": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", - "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", - "license": "MIT" - }, - "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/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stylis": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", - "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", - "license": "MIT" - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/suspend-react": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", - "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", - "license": "MIT", - "peerDependencies": { - "react": ">=17.0" - } - }, - "node_modules/swr": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", - "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/three": { - "version": "0.183.2", - "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", - "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", - "license": "MIT", - "peer": true - }, - "node_modules/three-mesh-bvh": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", - "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", - "license": "MIT", - "peerDependencies": { - "three": ">= 0.159.0" - } - }, - "node_modules/three-stdlib": { - "version": "2.36.1", - "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz", - "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==", - "license": "MIT", - "dependencies": { - "@types/draco3d": "^1.4.0", - "@types/offscreencanvas": "^2019.6.4", - "@types/webxr": "^0.5.2", - "draco3d": "^1.4.1", - "fflate": "^0.6.9", - "potpack": "^1.0.1" - }, - "peerDependencies": { - "three": ">=0.128.0" - } - }, - "node_modules/three-stdlib/node_modules/fflate": { - "version": "0.6.10", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", - "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", - "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/tinycolor2": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", - "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "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/troika-three-text": { - "version": "0.52.4", - "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", - "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", - "license": "MIT", - "dependencies": { - "bidi-js": "^1.0.2", - "troika-three-utils": "^0.52.4", - "troika-worker-utils": "^0.52.0", - "webgl-sdf-generator": "1.1.1" - }, - "peerDependencies": { - "three": ">=0.125.0" - } - }, - "node_modules/troika-three-utils": { - "version": "0.52.4", - "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", - "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", - "license": "MIT", - "peerDependencies": { - "three": ">=0.125.0" - } - }, - "node_modules/troika-worker-utils": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", - "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", - "license": "MIT" - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tunnel-rat": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", - "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", - "license": "MIT", - "dependencies": { - "zustand": "^4.3.2" - } - }, - "node_modules/tunnel-rat/node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.58.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", - "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.58.0", - "@typescript-eslint/parser": "8.58.0", - "@typescript-eslint/typescript-estree": "8.58.0", - "@typescript-eslint/utils": "8.58.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "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/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/utility-types": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", - "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/vite": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", - "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.12", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "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/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/webgl-constants": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", - "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" - }, - "node_modules/webgl-sdf-generator": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", - "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", - "license": "MIT" - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "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/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/zrender": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", - "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", - "license": "BSD-3-Clause", - "dependencies": { - "tslib": "2.3.0" - } - }, - "node_modules/zrender/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/zustand": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", - "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", - "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 - } - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index b23556e..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "frontend", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@ant-design/icons": "^6.1.1", - "@ant-design/pro-components": "^2.8.10", - "@react-three/drei": "^10.7.7", - "@react-three/fiber": "^9.5.0", - "@react-three/postprocessing": "^3.0.4", - "antd": "^5.29.3", - "axios": "^1.14.0", - "dayjs": "^1.11.20", - "echarts": "^6.0.0", - "echarts-for-react": "^3.0.6", - "i18next": "^24.2.2", - "react-i18next": "^15.4.1", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "react-router-dom": "^6.30.3", - "three": "^0.183.2" - }, - "devDependencies": { - "@eslint/js": "^9.39.4", - "@types/node": "^24.12.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@types/three": "^0.183.1", - "@vitejs/plugin-react": "^6.0.1", - "eslint": "^9.39.4", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.4.0", - "typescript": "~5.9.3", - "typescript-eslint": "^8.57.0", - "vite": "^8.0.1" - } -} diff --git a/frontend/public/devices/default.svg b/frontend/public/devices/default.svg deleted file mode 100644 index 73a9d56..0000000 --- a/frontend/public/devices/default.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - IoT Device - 通用设备 - \ No newline at end of file diff --git a/frontend/public/devices/heat_meter.svg b/frontend/public/devices/heat_meter.svg deleted file mode 100644 index b086c1f..0000000 --- a/frontend/public/devices/heat_meter.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - 256.8 - GJ - 累计热量 - - 流量: 2.4 m³/h - 温差: 8.2°C - - - - 供水 - 回水 - - - - - 热量表 - Ultrasonic Heat Meter - \ No newline at end of file diff --git a/frontend/public/devices/heat_pump.svg b/frontend/public/devices/heat_pump.svg deleted file mode 100644 index 05dc57a..0000000 --- a/frontend/public/devices/heat_pump.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 空气源热泵 - Air Source Heat Pump - \ No newline at end of file diff --git a/frontend/public/devices/meter.svg b/frontend/public/devices/meter.svg deleted file mode 100644 index 1253a37..0000000 --- a/frontend/public/devices/meter.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - 1847.5 - kWh - 正向有功总 - - - - - 运行 - - - - - - OK - - - - - - - - - - - - 智能电表 - Smart Power Meter - \ No newline at end of file diff --git a/frontend/public/devices/pv_inverter.svg b/frontend/public/devices/pv_inverter.svg deleted file mode 100644 index 48bb479..0000000 --- a/frontend/public/devices/pv_inverter.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 光伏逆变器 - Huawei SUN2000-110KTL - \ No newline at end of file diff --git a/frontend/public/devices/sensor.svg b/frontend/public/devices/sensor.svg deleted file mode 100644 index df10953..0000000 --- a/frontend/public/devices/sensor.svg +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - 23.5 - °C - - - - 湿度 64% - - - - - - - - - - - - - 温湿度传感器 - Temperature & Humidity Sensor - \ No newline at end of file diff --git a/frontend/public/devices/water_meter.svg b/frontend/public/devices/water_meter.svg deleted file mode 100644 index 1253a37..0000000 --- a/frontend/public/devices/water_meter.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - 1847.5 - kWh - 正向有功总 - - - - - 运行 - - - - - - OK - - - - - - - - - - - - 智能电表 - Smart Power Meter - \ No newline at end of file diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg deleted file mode 100644 index 6893eb1..0000000 --- a/frontend/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg deleted file mode 100644 index e952219..0000000 --- a/frontend/public/icons.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 3026007..0000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; -import { ConfigProvider, theme } from 'antd'; -import zhCN from 'antd/locale/zh_CN'; -import enUS from 'antd/locale/en_US'; -import { useTranslation } from 'react-i18next'; -import { ThemeProvider, useTheme } from './contexts/ThemeContext'; -import './i18n'; -import MainLayout from './layouts/MainLayout'; -import LoginPage from './pages/Login'; -import Dashboard from './pages/Dashboard'; -import Monitoring from './pages/Monitoring'; -import Analysis from './pages/Analysis'; -import Alarms from './pages/Alarms'; -import Carbon from './pages/Carbon'; -import Reports from './pages/Reports'; -import Devices from './pages/Devices'; -import DeviceDetail from './pages/DeviceDetail'; -import SystemManagement from './pages/System'; -import Quota from './pages/Quota'; -import Charging from './pages/Charging'; -import Maintenance from './pages/Maintenance'; -import DataQuery from './pages/DataQuery'; -import Management from './pages/Management'; -import Prediction from './pages/Prediction'; -import EnergyStrategy from './pages/EnergyStrategy'; -import AIOperations from './pages/AIOperations'; -import BigScreen from './pages/BigScreen'; -import BigScreen3D from './pages/BigScreen3D'; -import { isLoggedIn } from './utils/auth'; - -function ProtectedRoute({ children }: { children: React.ReactNode }) { - if (!isLoggedIn()) return ; - return <>{children}; -} - -function AppContent() { - const { darkMode } = useTheme(); - const { i18n } = useTranslation(); - - return ( - - - - } /> - } /> - } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - - ); -} - -export default function App() { - return ( - - - - ); -} diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png deleted file mode 100644 index cc51a3d20ad4bc961b596a6adfd686685cd84bb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44919 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 JPDHLkV1myZcL)Fg diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/frontend/src/contexts/ThemeContext.tsx b/frontend/src/contexts/ThemeContext.tsx deleted file mode 100644 index f20eb9e..0000000 --- a/frontend/src/contexts/ThemeContext.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'; - -interface ThemeContextType { - darkMode: boolean; - toggleDarkMode: () => void; -} - -const ThemeContext = createContext({ - darkMode: false, - toggleDarkMode: () => {}, -}); - -export function ThemeProvider({ children }: { children: ReactNode }) { - const [darkMode, setDarkMode] = useState(() => { - const saved = localStorage.getItem('tianpu-dark-mode'); - return saved === 'true'; - }); - - useEffect(() => { - localStorage.setItem('tianpu-dark-mode', String(darkMode)); - document.documentElement.setAttribute('data-theme', darkMode ? 'dark' : 'light'); - }, [darkMode]); - - const toggleDarkMode = () => setDarkMode(prev => !prev); - - return ( - - {children} - - ); -} - -export const useTheme = () => useContext(ThemeContext); diff --git a/frontend/src/hooks/useRealtimeWebSocket.ts b/frontend/src/hooks/useRealtimeWebSocket.ts deleted file mode 100644 index 51a14c9..0000000 --- a/frontend/src/hooks/useRealtimeWebSocket.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { useEffect, useRef, useState, useCallback } from 'react'; -import { getToken } from '../utils/auth'; - -export interface RealtimeData { - pv_power: number; - heatpump_power: number; - total_load: number; - grid_power: number; - active_alarms: number; - timestamp: string; -} - -export interface AlarmEventData { - id: number; - title: string; - severity: string; - message?: string; - device_name?: string; - triggered_at?: string; -} - -interface WebSocketMessage { - type: 'realtime_update' | 'alarm_event' | 'pong'; - data?: RealtimeData | AlarmEventData; -} - -interface UseRealtimeWebSocketOptions { - /** Called when a new alarm event arrives */ - onAlarmEvent?: (alarm: AlarmEventData) => void; - /** Polling interval in ms when WS is unavailable (default: 15000) */ - fallbackInterval?: number; - /** Whether the hook is enabled (default: true) */ - enabled?: boolean; -} - -interface UseRealtimeWebSocketResult { - /** Latest realtime data from WebSocket */ - data: RealtimeData | null; - /** Whether WebSocket is currently connected */ - connected: boolean; - /** Whether we are using fallback polling */ - usingFallback: boolean; -} - -const MAX_RECONNECT_DELAY = 30000; -const INITIAL_RECONNECT_DELAY = 1000; - -export default function useRealtimeWebSocket( - options: UseRealtimeWebSocketOptions = {} -): UseRealtimeWebSocketResult { - const { onAlarmEvent, fallbackInterval = 15000, enabled = true } = options; - const [data, setData] = useState(null); - const [connected, setConnected] = useState(false); - const [usingFallback, setUsingFallback] = useState(false); - - const wsRef = useRef(null); - const reconnectDelayRef = useRef(INITIAL_RECONNECT_DELAY); - const reconnectTimerRef = useRef | null>(null); - const pingTimerRef = useRef | null>(null); - const fallbackTimerRef = useRef | null>(null); - const onAlarmEventRef = useRef(onAlarmEvent); - const mountedRef = useRef(true); - - // Keep callback ref up to date - useEffect(() => { - onAlarmEventRef.current = onAlarmEvent; - }, [onAlarmEvent]); - - const cleanup = useCallback(() => { - if (reconnectTimerRef.current) { - clearTimeout(reconnectTimerRef.current); - reconnectTimerRef.current = null; - } - if (pingTimerRef.current) { - clearInterval(pingTimerRef.current); - pingTimerRef.current = null; - } - if (wsRef.current) { - wsRef.current.onclose = null; - wsRef.current.onerror = null; - wsRef.current.onmessage = null; - wsRef.current.close(); - wsRef.current = null; - } - }, []); - - const startFallbackPolling = useCallback(() => { - if (fallbackTimerRef.current) return; - setUsingFallback(true); - // We don't do actual polling here - the parent component's - // existing polling handles data fetch. This flag signals the - // parent to keep its polling active. - }, []); - - const stopFallbackPolling = useCallback(() => { - if (fallbackTimerRef.current) { - clearInterval(fallbackTimerRef.current); - fallbackTimerRef.current = null; - } - setUsingFallback(false); - }, []); - - const connect = useCallback(() => { - if (!mountedRef.current || !enabled) return; - - const token = getToken(); - if (!token) { - startFallbackPolling(); - return; - } - - cleanup(); - - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const host = window.location.host; - const wsUrl = `${protocol}//${host}/api/v1/ws/realtime?token=${encodeURIComponent(token)}`; - - try { - const ws = new WebSocket(wsUrl); - wsRef.current = ws; - - ws.onopen = () => { - if (!mountedRef.current) return; - setConnected(true); - stopFallbackPolling(); - reconnectDelayRef.current = INITIAL_RECONNECT_DELAY; - - // Ping every 30s to keep alive - pingTimerRef.current = setInterval(() => { - if (ws.readyState === WebSocket.OPEN) { - ws.send('ping'); - } - }, 30000); - }; - - ws.onmessage = (event) => { - if (!mountedRef.current) return; - try { - const msg: WebSocketMessage = JSON.parse(event.data); - if (msg.type === 'realtime_update' && msg.data) { - setData(msg.data as RealtimeData); - } else if (msg.type === 'alarm_event' && msg.data) { - onAlarmEventRef.current?.(msg.data as AlarmEventData); - } - // pong is just a keepalive ack, ignore - } catch { - // ignore parse errors - } - }; - - ws.onclose = (event) => { - if (!mountedRef.current) return; - setConnected(false); - if (pingTimerRef.current) { - clearInterval(pingTimerRef.current); - pingTimerRef.current = null; - } - - // Don't reconnect if closed intentionally (4001 = auth error) - if (event.code === 4001) { - startFallbackPolling(); - return; - } - - // Reconnect with exponential backoff - startFallbackPolling(); - const delay = reconnectDelayRef.current; - reconnectDelayRef.current = Math.min(delay * 2, MAX_RECONNECT_DELAY); - reconnectTimerRef.current = setTimeout(connect, delay); - }; - - ws.onerror = () => { - // onclose will fire after this, which handles reconnection - }; - } catch { - startFallbackPolling(); - const delay = reconnectDelayRef.current; - reconnectDelayRef.current = Math.min(delay * 2, MAX_RECONNECT_DELAY); - reconnectTimerRef.current = setTimeout(connect, delay); - } - }, [enabled, cleanup, startFallbackPolling, stopFallbackPolling]); - - useEffect(() => { - mountedRef.current = true; - if (enabled) { - connect(); - } - return () => { - mountedRef.current = false; - cleanup(); - stopFallbackPolling(); - }; - }, [enabled, connect, cleanup, stopFallbackPolling]); - - return { data, connected, usingFallback }; -} diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts deleted file mode 100644 index 99508f2..0000000 --- a/frontend/src/i18n/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; -import zh from './locales/zh.json'; -import en from './locales/en.json'; - -i18n.use(initReactI18next).init({ - resources: { - zh: { translation: zh }, - en: { translation: en }, - }, - lng: localStorage.getItem('tianpu-lang') || 'zh', - fallbackLng: 'zh', - interpolation: { - escapeValue: false, - }, -}); - -export default i18n; diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json deleted file mode 100644 index ef7d2ef..0000000 --- a/frontend/src/i18n/locales/en.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "menu": { - "dashboard": "Energy Overview", - "monitoring": "Real-time Monitoring", - "devices": "Device Management", - "analysis": "Energy Analysis", - "alarms": "Alarm Management", - "carbon": "Carbon Management", - "reports": "Reports", - "bigscreen": "Visual Dashboard", - "bigscreen2d": "2D Energy Screen", - "bigscreen3d": "3D Park Screen", - "system": "System Management", - "users": "User Management", - "roles": "Roles & Permissions", - "settings": "System Settings", - "audit": "Audit Log", - "quota": "Quota Management", - "charging": "Charging Management", - "maintenance": "O&M Management", - "dataQuery": "Data Query", - "management": "Management System", - "prediction": "AI Prediction" - }, - "header": { - "alarmNotification": "Alarm Notifications", - "activeAlarms": "active", - "noActiveAlarms": "No active alarms", - "viewAllAlarms": "View all alarms", - "profile": "Profile", - "logout": "Sign Out", - "brandName": "Tianpu EMS" - }, - "common": { - "save": "Save", - "cancel": "Cancel", - "confirm": "Confirm", - "delete": "Delete", - "edit": "Edit", - "add": "Add", - "search": "Search", - "reset": "Reset", - "export": "Export", - "import": "Import", - "loading": "Loading", - "success": "Success", - "error": "Error", - "noData": "No data" - }, - "analysis": { - "dataComparison": "Data Comparison", - "energyTrend": "Energy Trend", - "dailySummary": "Daily Energy Summary", - "period1": "Period 1", - "period2": "Period 2", - "totalConsumption": "Total Consumption", - "peakPower": "Peak Power", - "avgLoad": "Average Load", - "carbonEmission": "Carbon Emission", - "change": "Change", - "compare": "Compare", - "selectDateRange": "Select date range" - } -} diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json deleted file mode 100644 index d6839d3..0000000 --- a/frontend/src/i18n/locales/zh.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "menu": { - "dashboard": "能源总览", - "monitoring": "实时监控", - "devices": "设备管理", - "analysis": "能耗分析", - "alarms": "告警管理", - "carbon": "碳排放管理", - "reports": "报表管理", - "bigscreen": "可视化大屏", - "bigscreen2d": "2D 能源大屏", - "bigscreen3d": "3D 园区大屏", - "system": "系统管理", - "users": "用户管理", - "roles": "角色权限", - "settings": "系统设置", - "audit": "审计日志", - "quota": "定额管理", - "charging": "充电管理", - "maintenance": "运维管理", - "dataQuery": "数据查询", - "management": "管理体系", - "prediction": "AI预测" - }, - "header": { - "alarmNotification": "告警通知", - "activeAlarms": "条活跃", - "noActiveAlarms": "暂无活跃告警", - "viewAllAlarms": "查看全部告警", - "profile": "个人信息", - "logout": "退出登录", - "brandName": "天普EMS" - }, - "common": { - "save": "保存", - "cancel": "取消", - "confirm": "确认", - "delete": "删除", - "edit": "编辑", - "add": "新增", - "search": "搜索", - "reset": "重置", - "export": "导出", - "import": "导入", - "loading": "加载中", - "success": "操作成功", - "error": "操作失败", - "noData": "暂无数据" - }, - "analysis": { - "dataComparison": "数据对比", - "energyTrend": "能耗趋势", - "dailySummary": "每日能耗汇总", - "period1": "时段一", - "period2": "时段二", - "totalConsumption": "总用电量", - "peakPower": "峰值功率", - "avgLoad": "平均负荷", - "carbonEmission": "碳排放", - "change": "变化", - "compare": "对比", - "selectDateRange": "选择日期范围" - } -} diff --git a/frontend/src/index.css b/frontend/src/index.css deleted file mode 100644 index ccd2059..0000000 --- a/frontend/src/index.css +++ /dev/null @@ -1,84 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -#root { - width: 100%; - min-height: 100vh; -} - -/* ============================================ - MainLayout responsive styles - ============================================ */ - -/* Tablet: collapse sidebar by default */ -@media (max-width: 768px) { - .ant-layout-sider { - position: fixed !important; - z-index: 1000; - height: 100vh; - } - - .ant-layout-sider-collapsed { - width: 0 !important; - min-width: 0 !important; - max-width: 0 !important; - flex: 0 0 0 !important; - overflow: hidden; - } - - .ant-layout-header { - padding: 0 12px !important; - } - - .ant-layout-content { - margin: 8px !important; - padding: 12px !important; - } -} - -/* ============================================ - Dark mode support - ============================================ */ - -[data-theme='dark'] body { - background: #141414; - color: rgba(255, 255, 255, 0.85); -} - -[data-theme='dark'] .ant-layout-content { - background: #1f1f1f !important; -} - -[data-theme='dark'] .ant-card { - background: #1f1f1f; - border-color: #303030; -} - -[data-theme='dark'] .ant-table { - background: #1f1f1f; -} - -/* BigScreen pages are already dark themed, no overrides needed */ - -/* Mobile: tighter spacing */ -@media (max-width: 375px) { - .ant-layout-header { - padding: 0 8px !important; - height: 48px !important; - line-height: 48px !important; - } - - .ant-layout-content { - margin: 4px !important; - padding: 8px !important; - } -} diff --git a/frontend/src/layouts/MainLayout.tsx b/frontend/src/layouts/MainLayout.tsx deleted file mode 100644 index ac1d705..0000000 --- a/frontend/src/layouts/MainLayout.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { Layout, Menu, Avatar, Dropdown, Typography, Badge, Popover, List, Tag, Empty, Select } from 'antd'; -import { - DashboardOutlined, MonitorOutlined, BarChartOutlined, AlertOutlined, - FileTextOutlined, CloudOutlined, SettingOutlined, UserOutlined, - MenuFoldOutlined, MenuUnfoldOutlined, LogoutOutlined, BellOutlined, - ThunderboltOutlined, AppstoreOutlined, WarningOutlined, CloseCircleOutlined, - InfoCircleOutlined, FundProjectionScreenOutlined, GlobalOutlined, - BulbOutlined, BulbFilled, FundOutlined, CarOutlined, ToolOutlined, - SearchOutlined, SolutionOutlined, RobotOutlined, ExperimentOutlined, -} from '@ant-design/icons'; -import { Outlet, useNavigate, useLocation } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { getUser, removeToken } from '../utils/auth'; -import { getAlarmStats, getAlarmEvents } from '../services/api'; -import { useTheme } from '../contexts/ThemeContext'; - -const { Header, Sider, Content } = Layout; -const { Text } = Typography; - -const SEVERITY_CONFIG: Record = { - critical: { icon: , color: 'red' }, - warning: { icon: , color: 'orange' }, - info: { icon: , color: 'blue' }, -}; - -export default function MainLayout() { - const [collapsed, setCollapsed] = useState(false); - const [alarmCount, setAlarmCount] = useState(0); - const [recentAlarms, setRecentAlarms] = useState([]); - const navigate = useNavigate(); - const location = useLocation(); - const user = getUser(); - const { darkMode, toggleDarkMode } = useTheme(); - const { t, i18n } = useTranslation(); - - const menuItems = [ - { key: '/', icon: , label: t('menu.dashboard') }, - { key: '/monitoring', icon: , label: t('menu.monitoring') }, - { key: '/devices', icon: , label: t('menu.devices') }, - { key: '/analysis', icon: , label: t('menu.analysis') }, - { key: '/alarms', icon: , label: t('menu.alarms') }, - { key: '/carbon', icon: , label: t('menu.carbon') }, - { key: '/reports', icon: , label: t('menu.reports') }, - { key: '/quota', icon: , label: t('menu.quota', '定额管理') }, - { key: '/charging', icon: , label: t('menu.charging', '充电管理') }, - { key: '/maintenance', icon: , label: t('menu.maintenance', '运维管理') }, - { key: '/data-query', icon: , label: t('menu.dataQuery', '数据查询') }, - { key: '/prediction', icon: , label: t('menu.prediction', 'AI预测') }, - { key: '/management', icon: , label: t('menu.management', '管理体系') }, - { key: '/energy-strategy', icon: , label: t('menu.energyStrategy', '策略优化') }, - { key: '/ai-operations', icon: , label: t('menu.aiOperations', 'AI运维') }, - { key: 'bigscreen-group', icon: , label: t('menu.bigscreen'), - children: [ - { key: '/bigscreen', icon: , label: t('menu.bigscreen2d') }, - { key: '/bigscreen-3d', icon: , label: t('menu.bigscreen3d') }, - ], - }, - { key: '/system', icon: , label: t('menu.system'), - children: [ - { key: '/system/users', label: t('menu.users') }, - { key: '/system/roles', label: t('menu.roles') }, - { key: '/system/settings', label: t('menu.settings') }, - { key: '/system/audit', label: t('menu.audit', '审计日志') }, - ], - }, - ]; - - const fetchAlarms = useCallback(async () => { - try { - const [stats, events] = await Promise.all([ - getAlarmStats(), - getAlarmEvents({ status: 'active', page_size: 5 }), - ]); - const statsData = (stats as any) || {}; - let activeTotal = 0; - for (const severity of Object.values(statsData)) { - if (severity && typeof severity === 'object') { - activeTotal += (severity as any).active || 0; - } - } - setAlarmCount(activeTotal); - const items = (events as any)?.items || (events as any) || []; - setRecentAlarms(Array.isArray(items) ? items : []); - } catch { - // silently ignore - notifications are non-critical - } - }, []); - - useEffect(() => { - fetchAlarms(); - const timer = setInterval(fetchAlarms, 30000); - return () => clearInterval(timer); - }, [fetchAlarms]); - - const handleLogout = () => { - removeToken(); - localStorage.removeItem('user'); - navigate('/login'); - }; - - const handleLanguageChange = (lang: string) => { - i18n.changeLanguage(lang); - localStorage.setItem('tianpu-lang', lang); - }; - - const userMenu = { - items: [ - { key: 'profile', icon: , label: t('header.profile') }, - { type: 'divider' as const }, - { key: 'logout', icon: , label: t('header.logout'), onClick: handleLogout }, - ], - }; - - return ( - - -
- - {!collapsed && {t('header.brandName')}} -
- { - if (key === '/bigscreen' || key === '/bigscreen-3d') { - window.open(key, '_blank'); - } else { - navigate(key); - } - }} - /> - - -
-
setCollapsed(!collapsed)}> - {collapsed ? : - } -
-
- setFilters((f) => ({ ...f, severity: v }))} - options={[ - { label: '严重', value: 'critical' }, - { label: '警告', value: 'warning' }, - { label: '信息', value: 'info' }, - ]} - /> - handleRun(v)} - options={devices.map((d: any) => ({ label: `${d.device_name} (${d.device_type})`, value: d.device_id }))} - /> -
- `共 ${t} 条` }} - size="small" - /> - - setDetailReport(null)} - width={640} - > - {detailReport && ( - <> - - {detailReport.device_name} - {detailReport.report_type} - {dayjs(detailReport.generated_at).format('YYYY-MM-DD HH:mm')} - - - - ({ - color: f.severity === 'warning' ? 'orange' : f.severity === 'critical' ? 'red' : 'blue', - children: ( -
-
{f.finding}
-
{f.detail}
-
- ), - }))} - /> -
- - {detailReport.recommendations?.length > 0 && ( - - ( - - {r.priority === 'high' ? '高' : r.priority === 'medium' ? '中' : '低'}} - title={r.action} - description={r.detail} - /> - - )} - /> - - )} - - {detailReport.estimated_impact && ( - - -
- - - - - - - - )} - - )} - - - ); -} - -// ── Tab: Maintenance Predictor ───────────────────────────────────── - -function MaintenancePredictor() { - const [predictions, setPredictions] = useState({ total: 0, items: [] }); - const [schedule, setSchedule] = useState([]); - const [loading, setLoading] = useState(true); - const [page, setPage] = useState(1); - - useEffect(() => { loadData(); }, [page]); - - const loadData = async () => { - setLoading(true); - try { - const [pred, sched] = await Promise.all([ - getAiOpsPredictions({ page, page_size: 15 }), - getAiOpsMaintenanceSchedule(), - ]); - setPredictions(pred || { total: 0, items: [] }); - setSchedule(Array.isArray(sched) ? sched : []); - } catch { message.error('加载预测数据失败'); } - finally { setLoading(false); } - }; - - const handleGenerate = async () => { - message.loading({ content: '正在生成维护预测...', key: 'pred' }); - try { - const result = await triggerPredictions() as any; - message.success({ content: `生成 ${result?.generated || 0} 条预测`, key: 'pred' }); - loadData(); - } catch { message.error({ content: '生成失败', key: 'pred' }); } - }; - - const columns = [ - { - title: '设备', dataIndex: 'device_name', width: 120, - }, - { title: '部件', dataIndex: 'component', width: 120 }, - { title: '故障模式', dataIndex: 'failure_mode', ellipsis: true }, - { - title: '概率', dataIndex: 'probability', width: 80, - render: (v: number) => , - }, - { - title: '预计故障日期', dataIndex: 'predicted_failure_date', width: 120, - render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-', - }, - { - title: '紧急度', dataIndex: 'urgency', width: 80, - render: (v: string) => {v === 'critical' ? '紧急' : v === 'high' ? '高' : v === 'medium' ? '中' : '低'}, - }, - { - title: '停机(h)', dataIndex: 'estimated_downtime_hours', width: 80, - }, - { - title: '维修费(元)', dataIndex: 'estimated_repair_cost', width: 100, - render: (v: number) => v ? `${v.toLocaleString()}` : '-', - }, - { - title: '状态', dataIndex: 'status', width: 80, - render: (v: string) => {v === 'predicted' ? '预测' : v === 'scheduled' ? '已排期' : v === 'completed' ? '完成' : '误报'}, - }, - ]; - - const calendarSchedule = schedule.reduce((acc: any, item: any) => { - if (item.predicted_failure_date) { - const key = dayjs(item.predicted_failure_date).format('YYYY-MM-DD'); - if (!acc[key]) acc[key] = []; - acc[key].push(item); - } - return acc; - }, {} as Record); - - const dateCellRender = (value: dayjs.Dayjs) => { - const key = value.format('YYYY-MM-DD'); - const items = calendarSchedule[key]; - if (!items) return null; - return ( -
    - {items.slice(0, 2).map((item: any, i: number) => ( -
  • - {item.device_name}} /> -
  • - ))} - {items.length > 2 &&
  • +{items.length - 2} more
  • } -
- ); - }; - - return ( -
-
- - - 预测性维护 - - -
- - `共 ${t} 条` }} - size="small" - /> - ), - }, - { - key: 'calendar', - label: '维护日历', - children: ( - - { - if (info.type === 'date') return dateCellRender(value); - return null; - }} /> - - ), - }, - ]} - /> -
- ); -} - -// ── Tab: Insights Board ──────────────────────────────────────────── - -function InsightsBoard() { - const [insights, setInsights] = useState({ total: 0, items: [] }); - const [loading, setLoading] = useState(true); - - useEffect(() => { loadInsights(); }, []); - - const loadInsights = async () => { - setLoading(true); - try { - const data = await getAiOpsInsights({ page_size: 50 }); - setInsights(data || { total: 0, items: [] }); - } catch { message.error('加载洞察数据失败'); } - finally { setLoading(false); } - }; - - const handleGenerate = async () => { - message.loading({ content: '正在生成运营洞察...', key: 'ins' }); - try { - await triggerInsights(); - message.success({ content: '洞察生成完成', key: 'ins' }); - loadInsights(); - } catch { message.error({ content: '生成失败', key: 'ins' }); } - }; - - const typeIcons: Record = { - efficiency_trend: , - cost_anomaly: , - performance_comparison: , - seasonal_pattern: , - }; - - const BarChartOutlined = () => {"#"}; - - return ( -
-
- - - 运营洞察 - - -
- {loading ? : insights.items?.length === 0 ? ( - - ) : ( - - {insights.items?.map((insight: any) => ( -
- -
- - {insight.impact_level === 'high' ? '高影响' : insight.impact_level === 'medium' ? '中影响' : '低影响'} - - {insightTypeLabels[insight.insight_type] || insight.insight_type} -
-
{insight.title}
-
{insight.description}
- {insight.actionable && insight.recommended_action && ( -
- - {insight.recommended_action} -
- )} -
- {dayjs(insight.generated_at).format('YYYY-MM-DD HH:mm')} - {insight.valid_until && ` | 有效至 ${dayjs(insight.valid_until).format('MM-DD')}`} -
-
- - ))} - - )} - - ); -} - -// ── Main Page ────────────────────────────────────────────────────── - -export default function AIOperations() { - const [dashboard, setDashboard] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { loadDashboard(); }, []); - - const loadDashboard = async () => { - setLoading(true); - try { - const data = await getAiOpsDashboard(); - setDashboard(data); - } catch { /* initial load may fail if no data */ } - finally { setLoading(false); } - }; - - const health = dashboard?.health || {}; - const anomalyStats = dashboard?.anomalies?.stats || {}; - - return ( -
- {/* Overview cards */} - -
- - } - loading={loading} - /> - - - - - } - loading={loading} - /> - - - - - } - loading={loading} - /> - - - - - } - loading={loading} - /> - - - - - {/* Tabs */} - - 设备健康, - children: , - }, - { - key: 'anomalies', - label: 异常检测, - children: , - }, - { - key: 'diagnostics', - label: 智能诊断, - children: , - }, - { - key: 'maintenance', - label: 预测维护, - children: , - }, - { - key: 'insights', - label: 运营洞察, - children: , - }, - ]} - /> - - - ); -} diff --git a/frontend/src/pages/Alarms/index.tsx b/frontend/src/pages/Alarms/index.tsx deleted file mode 100644 index b34d30a..0000000 --- a/frontend/src/pages/Alarms/index.tsx +++ /dev/null @@ -1,314 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Card, Table, Tag, Button, Tabs, Modal, Form, Input, Select, InputNumber, Space, Switch, Drawer, Row, Col, Statistic, message } from 'antd'; -import { PlusOutlined, CheckOutlined, ToolOutlined, HistoryOutlined } from '@ant-design/icons'; -import ReactECharts from 'echarts-for-react'; -import { - getAlarmEvents, getAlarmRules, createAlarmRule, acknowledgeAlarm, resolveAlarm, - getAlarmAnalytics, getTopAlarmDevices, getAlarmMttr, toggleAlarmRule, getAlarmRuleHistory, -} from '../../services/api'; - -const severityMap: Record = { - critical: { color: 'red', text: '紧急' }, - major: { color: 'orange', text: '重要' }, - warning: { color: 'gold', text: '一般' }, -}; - -const statusMap: Record = { - active: { color: 'red', text: '活跃' }, - acknowledged: { color: 'orange', text: '已确认' }, - resolved: { color: 'green', text: '已解决' }, -}; - -function AlarmAnalyticsTab() { - const [analytics, setAnalytics] = useState(null); - const [topDevices, setTopDevices] = useState([]); - const [mttr, setMttr] = useState({}); - const [loading, setLoading] = useState(true); - - useEffect(() => { - loadAnalytics(); - }, []); - - const loadAnalytics = async () => { - setLoading(true); - try { - const [ana, top, mt] = await Promise.all([ - getAlarmAnalytics({}), - getTopAlarmDevices({}), - getAlarmMttr({}), - ]); - setAnalytics(ana); - setTopDevices(top as any[]); - setMttr(mt); - } catch { - message.error('加载告警分析数据失败'); - } finally { - setLoading(false); - } - }; - - const trendOption = analytics ? { - tooltip: { trigger: 'axis' }, - legend: { data: ['紧急', '重要', '一般'] }, - grid: { top: 50, right: 40, bottom: 30, left: 60 }, - xAxis: { type: 'category', data: analytics.daily_trend.map((d: any) => d.date) }, - yAxis: { type: 'value', name: '次数' }, - series: [ - { name: '紧急', type: 'line', smooth: true, data: analytics.daily_trend.map((d: any) => d.critical), lineStyle: { color: '#f5222d' }, itemStyle: { color: '#f5222d' } }, - { name: '重要', type: 'line', smooth: true, data: analytics.daily_trend.map((d: any) => d.major), lineStyle: { color: '#fa8c16' }, itemStyle: { color: '#fa8c16' } }, - { name: '一般', type: 'line', smooth: true, data: analytics.daily_trend.map((d: any) => d.warning), lineStyle: { color: '#fadb14' }, itemStyle: { color: '#fadb14' } }, - ], - } : {}; - - const topDevicesOption = { - tooltip: { trigger: 'axis' }, - grid: { top: 20, right: 40, bottom: 30, left: 120 }, - xAxis: { type: 'value', name: '告警次数' }, - yAxis: { type: 'category', data: [...topDevices].reverse().map(d => d.device_name) }, - series: [{ - type: 'bar', - data: [...topDevices].reverse().map(d => d.alarm_count), - itemStyle: { color: '#fa8c16' }, - }], - }; - - const totals = analytics?.totals || {}; - const pieOption = { - tooltip: { trigger: 'item' }, - series: [{ - type: 'pie', - radius: ['40%', '70%'], - data: [ - { value: totals.critical || 0, name: '紧急', itemStyle: { color: '#f5222d' } }, - { value: totals.major || 0, name: '重要', itemStyle: { color: '#fa8c16' } }, - { value: totals.warning || 0, name: '一般', itemStyle: { color: '#fadb14' } }, - ], - }], - }; - - return ( -
- - {(['critical', 'major', 'warning'] as const).map(sev => ( -
- - -
- 已解决 {mttr[sev]?.count || 0} 条 -
-
- - ))} - - - - - - {analytics && } - - - - - - - - - - - - - - ); -} - -export default function Alarms() { - const [events, setEvents] = useState({ total: 0, items: [] }); - const [rules, setRules] = useState([]); - const [loading, setLoading] = useState(true); - const [showRuleModal, setShowRuleModal] = useState(false); - const [form] = Form.useForm(); - const [historyDrawer, setHistoryDrawer] = useState<{ visible: boolean; ruleId: number; ruleName: string }>({ visible: false, ruleId: 0, ruleName: '' }); - const [historyData, setHistoryData] = useState({ total: 0, items: [] }); - const [historyLoading, setHistoryLoading] = useState(false); - - useEffect(() => { loadData(); }, []); - - const loadData = async () => { - setLoading(true); - try { - const [ev, ru] = await Promise.all([getAlarmEvents({}), getAlarmRules()]); - setEvents(ev); - setRules(ru as any[]); - } catch { message.error('加载告警数据失败'); } - finally { setLoading(false); } - }; - - const handleAcknowledge = async (id: number) => { - try { - await acknowledgeAlarm(id); - message.success('已确认'); - loadData(); - } catch { message.error('确认操作失败'); } - }; - - const handleResolve = async (id: number) => { - try { - await resolveAlarm(id); - message.success('已解决'); - loadData(); - } catch { message.error('解决操作失败'); } - }; - - const handleCreateRule = async (values: any) => { - try { - await createAlarmRule(values); - message.success('规则创建成功'); - setShowRuleModal(false); - form.resetFields(); - loadData(); - } catch { message.error('规则创建失败'); } - }; - - const handleToggleRule = async (ruleId: number) => { - try { - await toggleAlarmRule(ruleId); - message.success('状态已更新'); - loadData(); - } catch { message.error('切换状态失败'); } - }; - - const handleShowHistory = async (ruleId: number, ruleName: string) => { - setHistoryDrawer({ visible: true, ruleId, ruleName }); - setHistoryLoading(true); - try { - const res = await getAlarmRuleHistory(ruleId, { page: 1, page_size: 20 }); - setHistoryData(res); - } catch { message.error('加载规则历史失败'); } - finally { setHistoryLoading(false); } - }; - - const eventColumns = [ - { title: '级别', dataIndex: 'severity', width: 80, render: (s: string) => { - const sv = severityMap[s] || { color: 'default', text: s }; - return {sv.text}; - }}, - { title: '告警标题', dataIndex: 'title' }, - { title: '触发值', dataIndex: 'value', render: (v: number) => v?.toFixed(2) }, - { title: '阈值', dataIndex: 'threshold', render: (v: number) => v?.toFixed(2) }, - { title: '状态', dataIndex: 'status', render: (s: string) => { - const st = statusMap[s] || { color: 'default', text: s }; - return {st.text}; - }}, - { title: '触发时间', dataIndex: 'triggered_at', width: 180 }, - { title: '操作', key: 'action', width: 180, render: (_: any, r: any) => ( - - {r.status === 'active' && } - {r.status !== 'resolved' && } - - )}, - ]; - - const ruleColumns = [ - { title: '规则名称', dataIndex: 'name' }, - { title: '数据类型', dataIndex: 'data_type' }, - { title: '条件', dataIndex: 'condition' }, - { title: '阈值', dataIndex: 'threshold' }, - { title: '级别', dataIndex: 'severity', render: (s: string) => {severityMap[s]?.text} }, - { - title: '启用', - dataIndex: 'is_active', - width: 80, - render: (v: boolean, r: any) => ( - handleToggleRule(r.id)} size="small" /> - ), - }, - { - title: '操作', - key: 'action', - width: 100, - render: (_: any, r: any) => ( - - ), - }, - ]; - - const historyColumns = [ - { title: '级别', dataIndex: 'severity', width: 80, render: (s: string) => {severityMap[s]?.text} }, - { title: '告警标题', dataIndex: 'title' }, - { title: '触发值', dataIndex: 'value', render: (v: number) => v?.toFixed(2) }, - { title: '状态', dataIndex: 'status', render: (s: string) => {statusMap[s]?.text} }, - { title: '触发时间', dataIndex: 'triggered_at', width: 180 }, - { title: '解决时间', dataIndex: 'resolved_at', width: 180 }, - ]; - - return ( -
- -
- - )}, - { key: 'rules', label: '告警规则', children: ( - } - onClick={() => setShowRuleModal(true)}>新建规则}> -
- - )}, - { key: 'analytics', label: '分析', children: }, - ]} /> - - setShowRuleModal(false)} - onOk={() => form.submit()} okText="创建" cancelText="取消"> -
- - - - - - - - - - -
- - - ); -} diff --git a/frontend/src/pages/Analysis/CostAnalysis.tsx b/frontend/src/pages/Analysis/CostAnalysis.tsx deleted file mode 100644 index eb1135a..0000000 --- a/frontend/src/pages/Analysis/CostAnalysis.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Card, Row, Col, DatePicker, Select, Statistic, Button, Space, message } from 'antd'; -import { ArrowUpOutlined, ArrowDownOutlined, DownloadOutlined } from '@ant-design/icons'; -import ReactECharts from 'echarts-for-react'; -import dayjs, { type Dayjs } from 'dayjs'; -import { getCostSummary, getCostComparison, getCostBreakdown } from '../../services/api'; - -const { RangePicker } = DatePicker; - -export default function CostAnalysis() { - const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([ - dayjs().subtract(30, 'day'), dayjs(), - ]); - const [groupBy, setGroupBy] = useState('day'); - const [comparison, setComparison] = useState(null); - const [summary, setSummary] = useState([]); - const [breakdown, setBreakdown] = useState(null); - const [loading, setLoading] = useState(false); - - const loadData = async () => { - setLoading(true); - try { - const start = dateRange[0].format('YYYY-MM-DD'); - const end = dateRange[1].format('YYYY-MM-DD'); - const [comp, sum, bkd] = await Promise.all([ - getCostComparison({ energy_type: 'electricity', period: 'month' }), - getCostSummary({ start_date: start, end_date: end, group_by: groupBy, energy_type: 'electricity' }), - getCostBreakdown({ start_date: start, end_date: end, energy_type: 'electricity' }), - ]); - setComparison(comp); - setSummary(sum as any[]); - setBreakdown(bkd); - } catch (e) { - console.error(e); - message.error('加载费用数据失败'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - loadData(); - }, [groupBy]); - - // KPI calculations - const todayCost = comparison?.current || 0; - const monthCost = comparison?.current || 0; - const yearCost = comparison?.yoy || 0; - const momChange = comparison?.mom_change || 0; - const yoyChange = comparison?.yoy_change || 0; - - // Breakdown pie chart - const breakdownPieOption = { - tooltip: { trigger: 'item', formatter: '{b}: {c} 元 ({d}%)' }, - legend: { bottom: 10 }, - series: [{ - type: 'pie', - radius: ['40%', '70%'], - avoidLabelOverlap: false, - itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 }, - label: { show: true, formatter: '{b}\n{d}%' }, - data: (breakdown?.periods || []).map((p: any) => ({ - value: p.cost, - name: p.period_label || p.period_name, - itemStyle: { - color: p.period_name === 'peak' || p.period_name === 'sharp' ? '#f5222d' - : p.period_name === 'valley' || p.period_name === 'off_peak' ? '#52c41a' - : p.period_name === 'flat' ? '#1890ff' : '#faad14', - }, - })), - }], - }; - - // Cost trend line chart - const trendChartOption = { - tooltip: { trigger: 'axis' }, - legend: { data: ['费用(元)', '用电量(kWh)'] }, - grid: { top: 50, right: 60, bottom: 30, left: 60 }, - xAxis: { - type: 'category', - data: summary.map((d: any) => { - if (d.date) return dayjs(d.date).format('MM/DD'); - if (d.period) return d.period; - if (d.device_name) return d.device_name; - return ''; - }), - }, - yAxis: [ - { type: 'value', name: '元', position: 'left' }, - { type: 'value', name: 'kWh', position: 'right' }, - ], - series: [ - { - name: '费用(元)', - type: groupBy === 'device' ? 'bar' : 'line', - smooth: true, - data: summary.map((d: any) => d.cost || 0), - lineStyle: { color: '#f5222d' }, - itemStyle: { color: '#f5222d' }, - yAxisIndex: 0, - }, - { - name: '用电量(kWh)', - type: groupBy === 'device' ? 'bar' : 'line', - smooth: true, - data: summary.map((d: any) => d.consumption || 0), - lineStyle: { color: '#1890ff' }, - itemStyle: { color: '#1890ff' }, - yAxisIndex: 1, - }, - ], - }; - - // Cost by building bar chart (using device grouping) - const [deviceSummary, setDeviceSummary] = useState([]); - useEffect(() => { - const loadDeviceSummary = async () => { - try { - const start = dateRange[0].format('YYYY-MM-DD'); - const end = dateRange[1].format('YYYY-MM-DD'); - const data = await getCostSummary({ - start_date: start, end_date: end, group_by: 'device', energy_type: 'electricity', - }); - setDeviceSummary(data as any[]); - } catch (e) { - console.error(e); - } - }; - loadDeviceSummary(); - }, [dateRange]); - - const deviceBarOption = { - tooltip: { trigger: 'axis' }, - grid: { top: 30, right: 20, bottom: 60, left: 60 }, - xAxis: { - type: 'category', - data: deviceSummary.map((d: any) => d.device_name || `#${d.device_id}`), - axisLabel: { rotate: 30, fontSize: 11 }, - }, - yAxis: { type: 'value', name: '元' }, - series: [{ - type: 'bar', - data: deviceSummary.map((d: any) => d.cost || 0), - itemStyle: { - color: { - type: 'linear', x: 0, y: 0, x2: 0, y2: 1, - colorStops: [ - { offset: 0, color: '#1890ff' }, - { offset: 1, color: '#69c0ff' }, - ], - }, - }, - barMaxWidth: 40, - }], - }; - - const handleExport = () => { - const rows = summary.map((d: any) => { - const label = d.date || d.period || d.device_name || ''; - return `${label},${d.consumption || 0},${d.cost || 0}`; - }); - const csv = '\ufeff日期/分组,用电量(kWh),费用(元)\n' + rows.join('\n'); - const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `cost_analysis_${dateRange[0].format('YYYYMMDD')}_${dateRange[1].format('YYYYMMDD')}.csv`; - document.body.appendChild(link); - link.click(); - link.remove(); - URL.revokeObjectURL(url); - message.success('导出成功'); - }; - - return ( -
- {/* Controls */} - - - dates && setDateRange(dates as [Dayjs, Dayjs])} - /> - - - - - - - - - -
- - - ); -} diff --git a/frontend/src/pages/Analysis/MomAnalysis.tsx b/frontend/src/pages/Analysis/MomAnalysis.tsx deleted file mode 100644 index 236ec6c..0000000 --- a/frontend/src/pages/Analysis/MomAnalysis.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Card, Row, Col, Select, Space, Statistic, message } from 'antd'; -import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; -import ReactECharts from 'echarts-for-react'; -import { getEnergyMom } from '../../services/api'; - -interface MomItem { - label: string; - current_period: number; - previous_period: number; - change_pct: number; -} - -interface MomData { - items: MomItem[]; - total_current: number; - total_previous: number; - total_change_pct: number; -} - -export default function MomAnalysis() { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - const [period, setPeriod] = useState('month'); - const [energyType, setEnergyType] = useState('electricity'); - - const loadData = async () => { - setLoading(true); - try { - const res = await getEnergyMom({ period, energy_type: energyType }); - setData(res as MomData); - } catch { - message.error('加载环比数据失败'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { loadData(); }, [period, energyType]); - - const periodLabels: Record = { - month: ['本月', '上月'], - week: ['本周', '上周'], - day: ['今日', '昨日'], - }; - const [curLabel, prevLabel] = periodLabels[period] || ['当前', '上期']; - - const chartOption = data ? { - tooltip: { trigger: 'axis' }, - legend: { data: [curLabel, prevLabel] }, - grid: { top: 50, right: 40, bottom: 30, left: 60 }, - xAxis: { type: 'category', data: data.items.map(d => d.label) }, - yAxis: { type: 'value', name: 'kWh' }, - series: [ - { - name: curLabel, - type: 'line', - smooth: true, - data: data.items.map(d => d.current_period), - lineStyle: { color: '#1890ff' }, - itemStyle: { color: '#1890ff' }, - }, - { - name: prevLabel, - type: 'line', - smooth: true, - data: data.items.map(d => d.previous_period), - lineStyle: { color: '#faad14', type: 'dashed' }, - itemStyle: { color: '#faad14' }, - }, - ], - } : {}; - - const changePct = data?.total_change_pct || 0; - - return ( -
- - - 对比周期: - - - - - -
- - - - - - - - - - - - = 0 ? : } - valueStyle={{ color: changePct >= 0 ? '#f5222d' : '#52c41a' }} - precision={1} - /> - - - - - - {data && } - - - ); -} diff --git a/frontend/src/pages/Analysis/SubitemAnalysis.tsx b/frontend/src/pages/Analysis/SubitemAnalysis.tsx deleted file mode 100644 index a01c86f..0000000 --- a/frontend/src/pages/Analysis/SubitemAnalysis.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Card, Row, Col, DatePicker, Checkbox, Table, message } from 'antd'; -import ReactECharts from 'echarts-for-react'; -import dayjs, { type Dayjs } from 'dayjs'; -import api from '../../services/api'; - -const { RangePicker } = DatePicker; - -interface Category { - id: number; - name: string; - code: string; - color: string; - children?: Category[]; -} - -interface ByCategory { - id: number; - name: string; - code: string; - color: string; - consumption: number; - percentage: number; -} - -interface RankingItem { - name: string; - color: string; - consumption: number; -} - -interface TrendItem { - date: string; - category: string; - color: string; - consumption: number; -} - -export default function SubitemAnalysis() { - const [categories, setCategories] = useState([]); - const [flatCategories, setFlatCategories] = useState([]); - const [selectedCodes, setSelectedCodes] = useState([]); - const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([ - dayjs().subtract(30, 'day'), dayjs(), - ]); - const [byCategory, setByCategory] = useState([]); - const [ranking, setRanking] = useState([]); - const [trend, setTrend] = useState([]); - const [loading, setLoading] = useState(false); - - const flatten = (cats: Category[]): Category[] => { - const result: Category[] = []; - const walk = (list: Category[]) => { - for (const c of list) { - result.push(c); - if (c.children) walk(c.children); - } - }; - walk(cats); - return result; - }; - - useEffect(() => { - (async () => { - try { - const cats = await api.get('/energy/categories') as any as Category[]; - setCategories(cats); - const flat = flatten(cats); - setFlatCategories(flat); - setSelectedCodes(flat.map(c => c.code)); - } catch { - message.error('加载分项类别失败'); - } - })(); - }, []); - - useEffect(() => { - if (selectedCodes.length > 0) loadData(); - }, [selectedCodes, dateRange]); - - const loadData = async () => { - setLoading(true); - const params = { - start_date: dateRange[0].format('YYYY-MM-DD'), - end_date: dateRange[1].format('YYYY-MM-DD'), - energy_type: 'electricity', - }; - try { - const [byCat, rank, trendData] = await Promise.all([ - api.get('/energy/by-category', { params }), - api.get('/energy/category-ranking', { params }), - api.get('/energy/category-trend', { params }), - ]); - setByCategory((byCat as any[]).filter(c => selectedCodes.includes(c.code))); - setRanking((rank as any[]).filter(c => selectedCodes.includes(c.name) || true)); - setTrend(trendData as any[]); - } catch { - message.error('加载分项数据失败'); - } finally { - setLoading(false); - } - }; - - const pieOption = { - tooltip: { trigger: 'item', formatter: '{b}: {c} kWh ({d}%)' }, - legend: { orient: 'vertical' as const, right: 10, top: 'center' }, - series: [{ - type: 'pie', - radius: ['40%', '70%'], - avoidLabelOverlap: true, - itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 }, - label: { show: true, formatter: '{b}\n{d}%' }, - data: byCategory.map(c => ({ - name: c.name, value: c.consumption, - itemStyle: c.color ? { color: c.color } : undefined, - })), - }], - }; - - const barOption = { - tooltip: { trigger: 'axis' }, - grid: { top: 10, right: 30, bottom: 30, left: 100 }, - xAxis: { type: 'value' as const, name: 'kWh' }, - yAxis: { - type: 'category' as const, - data: [...ranking].reverse().map(r => r.name), - }, - series: [{ - type: 'bar', - data: [...ranking].reverse().map(r => ({ - value: r.consumption, - itemStyle: r.color ? { color: r.color } : undefined, - })), - }], - }; - - // Group trend data by category for line chart - const trendCategories = [...new Set(trend.map(t => t.category))]; - const trendDates = [...new Set(trend.map(t => t.date))].sort(); - const colorMap: Record = {}; - trend.forEach(t => { if (t.color) colorMap[t.category] = t.color; }); - - const lineOption = { - tooltip: { trigger: 'axis' }, - legend: { data: trendCategories }, - grid: { top: 40, right: 20, bottom: 30, left: 60 }, - xAxis: { - type: 'category' as const, - data: trendDates.map(d => dayjs(d).format('MM/DD')), - }, - yAxis: { type: 'value' as const, name: 'kWh' }, - series: trendCategories.map(cat => ({ - name: cat, - type: 'line', - smooth: true, - data: trendDates.map(d => { - const item = trend.find(t => t.date === d && t.category === cat); - return item ? item.consumption : 0; - }), - lineStyle: colorMap[cat] ? { color: colorMap[cat] } : undefined, - itemStyle: colorMap[cat] ? { color: colorMap[cat] } : undefined, - })), - }; - - const tableColumns = [ - { title: '分项名称', dataIndex: 'name' }, - { title: '用量 (kWh)', dataIndex: 'consumption', render: (v: number) => v?.toFixed(2) }, - { title: '占比 (%)', dataIndex: 'percentage', render: (v: number) => v?.toFixed(1) }, - ]; - - return ( -
- - -
- 日期范围: - dates && setDateRange(dates as [Dayjs, Dayjs])} - /> - - - 分项类别: - setSelectedCodes(vals as string[])} - options={flatCategories.map(c => ({ label: c.name, value: c.code }))} - /> - - - - - - - - - - - - - - - - - - - - - - -
- - - ); -} diff --git a/frontend/src/pages/Analysis/YoyAnalysis.tsx b/frontend/src/pages/Analysis/YoyAnalysis.tsx deleted file mode 100644 index 8d16a37..0000000 --- a/frontend/src/pages/Analysis/YoyAnalysis.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Card, Table, DatePicker, Select, Space, message } from 'antd'; -import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; -import ReactECharts from 'echarts-for-react'; -import dayjs from 'dayjs'; -import { getEnergyYoy } from '../../services/api'; - -interface YoyItem { - month: number; - current_year: number; - previous_year: number; - change_pct: number; -} - -export default function YoyAnalysis() { - const [data, setData] = useState([]); - const [loading, setLoading] = useState(false); - const [year, setYear] = useState(dayjs().year()); - const [energyType, setEnergyType] = useState('electricity'); - - const loadData = async () => { - setLoading(true); - try { - const res = await getEnergyYoy({ year, energy_type: energyType }); - setData(res as YoyItem[]); - } catch { - message.error('加载同比数据失败'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { loadData(); }, [year, energyType]); - - const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; - - const chartOption = { - tooltip: { trigger: 'axis' }, - legend: { data: [`${year}年`, `${year - 1}年`] }, - grid: { top: 50, right: 40, bottom: 30, left: 60 }, - xAxis: { type: 'category', data: months }, - yAxis: { type: 'value', name: 'kWh' }, - series: [ - { - name: `${year}年`, - type: 'bar', - data: data.map(d => d.current_year), - itemStyle: { color: '#1890ff' }, - }, - { - name: `${year - 1}年`, - type: 'bar', - data: data.map(d => d.previous_year), - itemStyle: { color: '#faad14' }, - }, - ], - }; - - const columns = [ - { title: '月份', dataIndex: 'month', render: (v: number) => `${v}月` }, - { title: `${year}年 (kWh)`, dataIndex: 'current_year', render: (v: number) => v?.toFixed(2) }, - { title: `${year - 1}年 (kWh)`, dataIndex: 'previous_year', render: (v: number) => v?.toFixed(2) }, - { - title: '同比变化', - dataIndex: 'change_pct', - render: (v: number) => ( - 0 ? '#f5222d' : v < 0 ? '#52c41a' : '#666' }}> - {v > 0 ? : v < 0 ? : null} - {' '}{Math.abs(v).toFixed(1)}% - - ), - }, - ]; - - const yearOptions = []; - for (let y = dayjs().year(); y >= dayjs().year() - 5; y--) { - yearOptions.push({ label: `${y}年`, value: y }); - } - - return ( -
- - - 年份: - - - - - - - - - -
- - - ); -} diff --git a/frontend/src/pages/Analysis/index.tsx b/frontend/src/pages/Analysis/index.tsx deleted file mode 100644 index b24a1ca..0000000 --- a/frontend/src/pages/Analysis/index.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Card, Row, Col, DatePicker, Select, Statistic, Table, Button, Space, message, Tabs } from 'antd'; -import { ArrowUpOutlined, ArrowDownOutlined, DownloadOutlined, SwapOutlined } from '@ant-design/icons'; -import ReactECharts from 'echarts-for-react'; -import dayjs, { type Dayjs } from 'dayjs'; -import { getEnergyHistory, getEnergyComparison, getEnergyDailySummary, exportEnergyData } from '../../services/api'; -import LossAnalysis from './LossAnalysis'; -import YoyAnalysis from './YoyAnalysis'; -import MomAnalysis from './MomAnalysis'; -import CostAnalysis from './CostAnalysis'; -import SubitemAnalysis from './SubitemAnalysis'; - -const { RangePicker } = DatePicker; - -function ComparisonView() { - const [range1, setRange1] = useState<[Dayjs, Dayjs]>([ - dayjs().subtract(30, 'day'), dayjs(), - ]); - const [range2, setRange2] = useState<[Dayjs, Dayjs]>([ - dayjs().subtract(60, 'day'), dayjs().subtract(30, 'day'), - ]); - const [data1, setData1] = useState([]); - const [data2, setData2] = useState([]); - const [summary1, setSummary1] = useState([]); - const [summary2, setSummary2] = useState([]); - const [comparison, setComparison] = useState(null); - const [loading, setLoading] = useState(false); - - const loadComparisonData = async () => { - setLoading(true); - try { - const [hist1, hist2, daily1, daily2, comp] = await Promise.all([ - getEnergyHistory({ - data_type: 'power', granularity: 'day', - start_time: range1[0].format('YYYY-MM-DD'), - end_time: range1[1].format('YYYY-MM-DD'), - }), - getEnergyHistory({ - data_type: 'power', granularity: 'day', - start_time: range2[0].format('YYYY-MM-DD'), - end_time: range2[1].format('YYYY-MM-DD'), - }), - getEnergyDailySummary({ - energy_type: 'electricity', - start_date: range1[0].format('YYYY-MM-DD'), - end_date: range1[1].format('YYYY-MM-DD'), - }), - getEnergyDailySummary({ - energy_type: 'electricity', - start_date: range2[0].format('YYYY-MM-DD'), - end_date: range2[1].format('YYYY-MM-DD'), - }), - getEnergyComparison({ energy_type: 'electricity', period: 'month' }), - ]); - setData1(hist1 as any[]); - setData2(hist2 as any[]); - setSummary1(daily1 as any[]); - setSummary2(daily2 as any[]); - setComparison(comp); - } catch (e) { - console.error(e); - message.error('加载对比数据失败'); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - loadComparisonData(); - }, []); - - const calcMetrics = (summaryData: any[]) => { - if (!summaryData || summaryData.length === 0) { - return { totalConsumption: 0, peakPower: 0, avgLoad: 0, carbonEmission: 0 }; - } - const totalConsumption = summaryData.reduce((s, d) => s + (d.consumption || 0), 0); - const peakPower = Math.max(...summaryData.map(d => d.peak_power || 0)); - const avgLoad = summaryData.reduce((s, d) => s + (d.avg_power || 0), 0) / summaryData.length; - const carbonEmission = summaryData.reduce((s, d) => s + (d.carbon_emission || 0), 0); - return { totalConsumption, peakPower, avgLoad, carbonEmission }; - }; - - const m1 = calcMetrics(summary1); - const m2 = calcMetrics(summary2); - - const pctChange = (v1: number, v2: number) => { - if (v2 === 0) return 0; - return ((v1 - v2) / v2) * 100; - }; - - const comparisonChartOption = { - tooltip: { trigger: 'axis' }, - legend: { data: ['时段一', '时段二'] }, - grid: { top: 50, right: 40, bottom: 30, left: 60 }, - xAxis: { - type: 'category', - data: (data1.length >= data2.length ? data1 : data2).map((_, i) => `Day ${i + 1}`), - }, - yAxis: [ - { type: 'value', name: 'kW', position: 'left' }, - { type: 'value', name: 'kW', position: 'right' }, - ], - series: [ - { - name: '时段一', - type: 'line', - smooth: true, - data: data1.map(d => d.avg), - lineStyle: { color: '#1890ff' }, - itemStyle: { color: '#1890ff' }, - yAxisIndex: 0, - }, - { - name: '时段二', - type: 'line', - smooth: true, - data: data2.map(d => d.avg), - lineStyle: { color: '#faad14' }, - itemStyle: { color: '#faad14' }, - yAxisIndex: 1, - }, - ], - }; - - const renderMetricCard = ( - label: string, v1: number, v2: number, unit: string, precision = 1, - ) => { - const change = pctChange(v1, v2); - const isImproved = change < 0; // less consumption = improvement - return ( - - -
{label}
- -
-
时段一
-
{v1.toFixed(precision)}
-
{unit}
- - -
时段二
-
{v2.toFixed(precision)}
-
{unit}
- - -
0 ? '#f5222d' : '#666', - }}> - {change > 0 ? : change < 0 ? : null} - {' '}{Math.abs(change).toFixed(1)}% {isImproved ? '减少' : change > 0 ? '增加' : '持平'} -
- - - ); - }; - - return ( -
- - - 时段一: - dates && setRange1(dates as [Dayjs, Dayjs])} - /> - 时段二: - dates && setRange2(dates as [Dayjs, Dayjs])} - /> - - - - - - {renderMetricCard('总用电量', m1.totalConsumption, m2.totalConsumption, 'kWh')} - {renderMetricCard('峰值功率', m1.peakPower, m2.peakPower, 'kW')} - {renderMetricCard('平均负荷', m1.avgLoad, m2.avgLoad, 'kW')} - {renderMetricCard('碳排放', m1.carbonEmission, m2.carbonEmission, 'kg', 2)} - - - - - -
- ); -} - -export default function Analysis() { - const [historyData, setHistoryData] = useState([]); - const [comparison, setComparison] = useState(null); - const [dailySummary, setDailySummary] = useState([]); - const [granularity, setGranularity] = useState('hour'); - const [exporting, setExporting] = useState(false); - const [activeTab, setActiveTab] = useState('overview'); - - useEffect(() => { - loadData(); - }, [granularity]); - - const loadData = async () => { - try { - const [hist, comp, daily] = await Promise.all([ - getEnergyHistory({ data_type: 'power', granularity }), - getEnergyComparison({ energy_type: 'electricity', period: 'month' }), - getEnergyDailySummary({ energy_type: 'electricity' }), - ]); - setHistoryData(hist as any[]); - setComparison(comp); - setDailySummary(daily as any[]); - } catch (e) { console.error(e); } - }; - - const handleExport = async (format: 'csv' | 'xlsx' = 'csv') => { - setExporting(true); - try { - const end = dayjs().format('YYYY-MM-DD'); - const start = dayjs().subtract(30, 'day').format('YYYY-MM-DD'); - await exportEnergyData({ - start_time: start, - end_time: end, - format, - }); - message.success('导出成功'); - } catch (e) { - message.error('导出失败,请重试'); - console.error(e); - } finally { - setExporting(false); - } - }; - - const historyChartOption = { - tooltip: { trigger: 'axis' }, - legend: { data: ['平均', '最大', '最小'] }, - grid: { top: 40, right: 20, bottom: 30, left: 60 }, - xAxis: { - type: 'category', - data: historyData.map(d => { - const t = new Date(d.time); - return `${t.getMonth() + 1}/${t.getDate()} ${t.getHours()}:00`; - }), - }, - yAxis: { type: 'value', name: 'kW' }, - series: [ - { name: '平均', type: 'line', smooth: true, data: historyData.map(d => d.avg), lineStyle: { color: '#1890ff' }, itemStyle: { color: '#1890ff' } }, - { name: '最大', type: 'line', smooth: true, data: historyData.map(d => d.max), lineStyle: { color: '#f5222d', type: 'dashed' }, itemStyle: { color: '#f5222d' } }, - { name: '最小', type: 'line', smooth: true, data: historyData.map(d => d.min), lineStyle: { color: '#52c41a', type: 'dashed' }, itemStyle: { color: '#52c41a' } }, - ], - }; - - const dailyColumns = [ - { title: '日期', dataIndex: 'date', render: (v: string) => dayjs(v).format('YYYY-MM-DD') }, - { title: '消耗(kWh)', dataIndex: 'consumption', render: (v: number) => v?.toFixed(1) }, - { title: '产出(kWh)', dataIndex: 'generation', render: (v: number) => v?.toFixed(1) }, - { title: '峰值功率(kW)', dataIndex: 'peak_power', render: (v: number) => v?.toFixed(1) }, - { title: '平均功率(kW)', dataIndex: 'avg_power', render: (v: number) => v?.toFixed(1) }, - { title: '碳排放(kg)', dataIndex: 'carbon_emission', render: (v: number) => v?.toFixed(2) }, - ]; - - const overviewContent = ( -
- -
- - - - - - - - - - - - - - - = 0 ? : } - valueStyle={{ color: comparison?.mom_change >= 0 ? '#f5222d' : '#52c41a' }} precision={1} /> - - - - - = 0 ? : } - valueStyle={{ color: comparison?.yoy_change >= 0 ? '#f5222d' : '#52c41a' }} precision={1} /> - - - - - - }> - - - - -
- - - ); - - return ( -
- }, - { key: 'loss', label: '损耗分析', children: }, - { key: 'yoy', label: '同比分析', children: }, - { key: 'mom', label: '环比分析', children: }, - { key: 'cost', label: '费用分析', children: }, - { key: 'subitem', label: '分项分析', children: }, - ]} - /> -
- ); -} diff --git a/frontend/src/pages/BigScreen/components/AlarmCard.tsx b/frontend/src/pages/BigScreen/components/AlarmCard.tsx deleted file mode 100644 index 53faf4a..0000000 --- a/frontend/src/pages/BigScreen/components/AlarmCard.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import ReactECharts from 'echarts-for-react'; -import styles from '../styles.module.css'; -import AnimatedNumber from './AnimatedNumber'; - -interface Props { - alarmEvents: any[]; - alarmStats: any; -} - -export default function AlarmCard({ alarmEvents, alarmStats }: Props) { - const activeCount = alarmStats?.active_count ?? 0; - const weeklyTrend = alarmStats?.weekly_trend ?? [3, 5, 2, 8, 4, 6, 1]; - const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; - - const trendOption = { - grid: { left: 30, right: 8, top: 8, bottom: 20 }, - xAxis: { - type: 'category' as const, - data: weekDays, - axisLine: { lineStyle: { color: 'rgba(0,212,255,0.2)' } }, - axisLabel: { fontSize: 10, color: 'rgba(224,232,240,0.5)' }, - axisTick: { show: false }, - }, - yAxis: { - type: 'value' as const, - splitLine: { lineStyle: { color: 'rgba(0,212,255,0.08)' } }, - axisLabel: { fontSize: 10, color: 'rgba(224,232,240,0.4)' }, - }, - series: [{ - type: 'bar', - data: weeklyTrend, - itemStyle: { - color: { - type: 'linear', - x: 0, y: 0, x2: 0, y2: 1, - colorStops: [ - { offset: 0, color: '#ff8c00' }, - { offset: 1, color: 'rgba(255, 140, 0, 0.2)' }, - ], - } as any, - borderRadius: [2, 2, 0, 0], - }, - barWidth: '50%', - }], - tooltip: { trigger: 'axis' as const, backgroundColor: 'rgba(6,30,62,0.9)', borderColor: 'rgba(0,212,255,0.3)', textStyle: { color: '#e0e8f0', fontSize: 12 } }, - }; - - const getSeverityClass = (severity: string) => { - if (severity === 'critical' || severity === 'high') return styles.alarmSeverityCritical; - if (severity === 'warning' || severity === 'medium') return styles.alarmSeverityWarning; - return styles.alarmSeverityInfo; - }; - - const formatTime = (ts: string) => { - if (!ts) return ''; - const d = new Date(ts); - return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; - }; - - return ( -
-
告警信息
-
-
-
- 活跃告警 -
- - -
-
-
-
- {(alarmEvents ?? []).slice(0, 5).map((alarm: any, idx: number) => ( -
- - {alarm.message ?? alarm.description ?? '未知告警'} - {formatTime(alarm.triggered_at ?? alarm.created_at)} -
- ))} - {(!alarmEvents || alarmEvents.length === 0) && ( -
暂无告警
- )} -
-
- -
-
-
- ); -} diff --git a/frontend/src/pages/BigScreen/components/AnimatedNumber.tsx b/frontend/src/pages/BigScreen/components/AnimatedNumber.tsx deleted file mode 100644 index 82605a0..0000000 --- a/frontend/src/pages/BigScreen/components/AnimatedNumber.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -interface Props { - value: number; - duration?: number; - decimals?: number; - className?: string; -} - -export default function AnimatedNumber({ value, duration = 1500, decimals = 0, className }: Props) { - const [display, setDisplay] = useState(0); - const rafRef = useRef(0); - const startRef = useRef(0); - const fromRef = useRef(0); - - useEffect(() => { - fromRef.current = display; - startRef.current = performance.now(); - - const animate = (now: number) => { - const elapsed = now - startRef.current; - const progress = Math.min(elapsed / duration, 1); - // ease-out cubic - const eased = 1 - Math.pow(1 - progress, 3); - const current = fromRef.current + (value - fromRef.current) * eased; - setDisplay(current); - if (progress < 1) { - rafRef.current = requestAnimationFrame(animate); - } - }; - - rafRef.current = requestAnimationFrame(animate); - return () => cancelAnimationFrame(rafRef.current); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value, duration]); - - return {display.toFixed(decimals)}; -} diff --git a/frontend/src/pages/BigScreen/components/CarbonCard.tsx b/frontend/src/pages/BigScreen/components/CarbonCard.tsx deleted file mode 100644 index e0ef6e9..0000000 --- a/frontend/src/pages/BigScreen/components/CarbonCard.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import ReactECharts from 'echarts-for-react'; -import styles from '../styles.module.css'; -import AnimatedNumber from './AnimatedNumber'; - -interface Props { - carbonOverview: any; - carbonTrend: any; -} - -export default function CarbonCard({ carbonOverview, carbonTrend }: Props) { - const annualEmission = carbonOverview?.annual_emission ?? 0; - const annualReduction = carbonOverview?.annual_reduction ?? 0; - const neutralityRate = annualEmission > 0 - ? Math.min((annualReduction / annualEmission) * 100, 100) - : 0; - - // Monthly trend - const months = carbonTrend?.months ?? ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; - const emissionData = carbonTrend?.emission ?? []; - const reductionData = carbonTrend?.reduction ?? []; - - const trendOption = { - grid: { left: 40, right: 12, top: 24, bottom: 24 }, - legend: { - data: ['碳排放', '碳减排'], - textStyle: { color: 'rgba(224,232,240,0.6)', fontSize: 10 }, - top: 0, - right: 8, - itemWidth: 12, - itemHeight: 8, - }, - tooltip: { - trigger: 'axis' as const, - backgroundColor: 'rgba(6,30,62,0.9)', - borderColor: 'rgba(0,212,255,0.3)', - textStyle: { color: '#e0e8f0', fontSize: 12 }, - }, - xAxis: { - type: 'category' as const, - data: months, - axisLine: { lineStyle: { color: 'rgba(0,212,255,0.2)' } }, - axisLabel: { fontSize: 10, color: 'rgba(224,232,240,0.5)' }, - axisTick: { show: false }, - }, - yAxis: { - type: 'value' as const, - name: 'kgCO2', - nameTextStyle: { color: 'rgba(224,232,240,0.4)', fontSize: 10 }, - splitLine: { lineStyle: { color: 'rgba(0,212,255,0.08)' } }, - axisLabel: { fontSize: 10, color: 'rgba(224,232,240,0.4)' }, - }, - series: [ - { - name: '碳排放', - type: 'line', - data: emissionData, - smooth: true, - symbol: 'none', - lineStyle: { color: '#ff8c00', width: 2 }, - areaStyle: { color: 'rgba(255, 140, 0, 0.08)' }, - }, - { - name: '碳减排', - type: 'line', - data: reductionData, - smooth: true, - symbol: 'none', - lineStyle: { color: '#00ff88', width: 2 }, - areaStyle: { color: 'rgba(0, 255, 136, 0.08)' }, - }, - ], - }; - - // Neutrality gauge - const gaugeOption = { - series: [{ - type: 'gauge', - startAngle: 220, - endAngle: -40, - radius: '90%', - center: ['50%', '55%'], - min: 0, - max: 100, - progress: { - show: true, - width: 10, - itemStyle: { - color: neutralityRate >= 80 ? '#00ff88' : neutralityRate >= 50 ? '#ff8c00' : '#ff4757', - }, - }, - axisLine: { lineStyle: { width: 10, color: [[1, 'rgba(0, 255, 136, 0.1)']] } }, - axisTick: { show: false }, - splitLine: { show: false }, - axisLabel: { show: false }, - pointer: { show: false }, - title: { show: false }, - detail: { - fontSize: 18, - fontWeight: 700, - color: neutralityRate >= 80 ? '#00ff88' : neutralityRate >= 50 ? '#ff8c00' : '#ff4757', - offsetCenter: [0, '10%'], - formatter: '{value}%', - }, - data: [{ value: neutralityRate.toFixed(1) }], - }], - }; - - return ( -
-
碳排放
-
-
-
- -
-
-
- 年碳排放 - - - kg - -
-
- 年碳减排 - - - kg - -
-
-
-
- -
-
-
- ); -} diff --git a/frontend/src/pages/BigScreen/components/EnergyFlowDiagram.tsx b/frontend/src/pages/BigScreen/components/EnergyFlowDiagram.tsx deleted file mode 100644 index f7416b5..0000000 --- a/frontend/src/pages/BigScreen/components/EnergyFlowDiagram.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { useEffect, useRef } from 'react'; -import styles from '../styles.module.css'; - -interface Props { - realtime: any; - overview: any; -} - -interface Particle { - x: number; - y: number; - progress: number; - speed: number; - pathIndex: number; -} - -export default function EnergyFlowDiagram({ realtime, overview }: Props) { - const canvasRef = useRef(null); - const containerRef = useRef(null); - const particlesRef = useRef([]); - const rafRef = useRef(0); - - const gridPower = realtime?.grid_power ?? 0; - const pvPower = realtime?.pv_power ?? 0; - const totalPower = realtime?.total_power ?? 0; - const hpPower = realtime?.heatpump_power ?? 0; - - useEffect(() => { - const container = containerRef.current; - const canvas = canvasRef.current; - if (!container || !canvas) return; - - const resizeObserver = new ResizeObserver(() => { - const rect = container.getBoundingClientRect(); - canvas.width = rect.width * window.devicePixelRatio; - canvas.height = rect.height * window.devicePixelRatio; - canvas.style.width = rect.width + 'px'; - canvas.style.height = rect.height + 'px'; - }); - resizeObserver.observe(container); - - // Initialize particles - particlesRef.current = []; - for (let i = 0; i < 60; i++) { - particlesRef.current.push({ - x: 0, y: 0, - progress: Math.random(), - speed: 0.002 + Math.random() * 0.003, - pathIndex: Math.floor(Math.random() * 4), - }); - } - - const animate = () => { - const ctx = canvas.getContext('2d'); - if (!ctx) return; - const dpr = window.devicePixelRatio; - const w = canvas.width / dpr; - const h = canvas.height / dpr; - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); - ctx.clearRect(0, 0, w, h); - - // Node positions - const nodes = { - grid: { x: w * 0.12, y: h * 0.32, label: '电网', color: '#ff8c00' }, - pv: { x: w * 0.12, y: h * 0.68, label: '光伏', color: '#00ff88' }, - building: { x: w * 0.5, y: h * 0.5, label: '建筑负载', color: '#00d4ff' }, - heatpump: { x: w * 0.85, y: h * 0.38, label: '热泵', color: '#00d4ff' }, - heating: { x: w * 0.85, y: h * 0.68, label: '供暖', color: '#ff4757' }, - }; - - // Define paths: [from, to] - const paths = [ - { from: nodes.grid, to: nodes.building, color: '#ff8c00', value: gridPower }, - { from: nodes.pv, to: nodes.building, color: '#00ff88', value: pvPower }, - { from: nodes.building, to: nodes.heatpump, color: '#00d4ff', value: hpPower }, - { from: nodes.heatpump, to: nodes.heating, color: '#ff4757', value: hpPower * 3.5 }, - ]; - - // Draw paths - paths.forEach((path) => { - ctx.beginPath(); - ctx.moveTo(path.from.x, path.from.y); - // Bezier curve - const mx = (path.from.x + path.to.x) / 2; - ctx.bezierCurveTo(mx, path.from.y, mx, path.to.y, path.to.x, path.to.y); - ctx.strokeStyle = path.color + '30'; - ctx.lineWidth = 3; - ctx.stroke(); - }); - - // Animate particles - particlesRef.current.forEach((p) => { - p.progress += p.speed; - if (p.progress > 1) { - p.progress = 0; - p.pathIndex = Math.floor(Math.random() * paths.length); - } - - const path = paths[p.pathIndex]; - if (!path) return; - const t = p.progress; - const mx = (path.from.x + path.to.x) / 2; - // Cubic bezier interpolation - const u = 1 - t; - const x = u * u * u * path.from.x + 3 * u * u * t * mx + 3 * u * t * t * mx + t * t * t * path.to.x; - const y = u * u * u * path.from.y + 3 * u * u * t * path.from.y + 3 * u * t * t * path.to.y + t * t * t * path.to.y; - - const alpha = t < 0.1 ? t / 0.1 : t > 0.9 ? (1 - t) / 0.1 : 1; - ctx.beginPath(); - ctx.arc(x, y, 3, 0, Math.PI * 2); - ctx.fillStyle = path.color; - ctx.globalAlpha = alpha * 0.9; - ctx.fill(); - - // Glow - ctx.beginPath(); - ctx.arc(x, y, 8, 0, Math.PI * 2); - ctx.fillStyle = path.color; - ctx.globalAlpha = alpha * 0.2; - ctx.fill(); - - ctx.globalAlpha = 1; - }); - - // Draw nodes - Object.values(nodes).forEach((node) => { - // Node bg - ctx.beginPath(); - const rw = 60, rh = 36, r = 8; - const nx = node.x - rw, ny = node.y - rh; - const nw = rw * 2, nh = rh * 2; - ctx.moveTo(nx + r, ny); - ctx.lineTo(nx + nw - r, ny); - ctx.quadraticCurveTo(nx + nw, ny, nx + nw, ny + r); - ctx.lineTo(nx + nw, ny + nh - r); - ctx.quadraticCurveTo(nx + nw, ny + nh, nx + nw - r, ny + nh); - ctx.lineTo(nx + r, ny + nh); - ctx.quadraticCurveTo(nx, ny + nh, nx, ny + nh - r); - ctx.lineTo(nx, ny + r); - ctx.quadraticCurveTo(nx, ny, nx + r, ny); - ctx.closePath(); - ctx.fillStyle = 'rgba(6, 30, 62, 0.95)'; - ctx.fill(); - ctx.strokeStyle = node.color + '66'; - ctx.lineWidth = 1.5; - ctx.stroke(); - - // Shadow glow - ctx.shadowColor = node.color; - ctx.shadowBlur = 12; - ctx.strokeStyle = node.color + '33'; - ctx.stroke(); - ctx.shadowBlur = 0; - - // Label - ctx.fillStyle = 'rgba(224, 232, 240, 0.7)'; - ctx.font = '12px system-ui, sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(node.label, node.x, node.y - 8); - - // Value - let val = '0 kW'; - if (node.label === '电网') val = gridPower.toFixed(1) + ' kW'; - else if (node.label === '光伏') val = pvPower.toFixed(1) + ' kW'; - else if (node.label === '建筑负载') val = totalPower.toFixed(1) + ' kW'; - else if (node.label === '热泵') val = hpPower.toFixed(1) + ' kW'; - else if (node.label === '供暖') val = (hpPower * 3.5).toFixed(1) + ' kW'; - - ctx.fillStyle = node.color; - ctx.font = 'bold 15px system-ui, sans-serif'; - ctx.fillText(val, node.x, node.y + 12); - }); - - rafRef.current = requestAnimationFrame(animate); - }; - - rafRef.current = requestAnimationFrame(animate); - - return () => { - cancelAnimationFrame(rafRef.current); - resizeObserver.disconnect(); - }; - }, [gridPower, pvPower, totalPower, hpPower]); - - return ( -
- -
- ); -} diff --git a/frontend/src/pages/BigScreen/components/EnergyOverviewCard.tsx b/frontend/src/pages/BigScreen/components/EnergyOverviewCard.tsx deleted file mode 100644 index c347514..0000000 --- a/frontend/src/pages/BigScreen/components/EnergyOverviewCard.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import ReactECharts from 'echarts-for-react'; -import styles from '../styles.module.css'; -import AnimatedNumber from './AnimatedNumber'; - -interface Props { - data: any; - realtime: any; -} - -export default function EnergyOverviewCard({ data, realtime }: Props) { - const selfUseRate = data?.self_consumption_rate ?? 0; - - const gaugeOption = { - series: [{ - type: 'gauge', - startAngle: 220, - endAngle: -40, - radius: '90%', - center: ['50%', '55%'], - min: 0, - max: 100, - splitNumber: 5, - progress: { show: true, width: 12, itemStyle: { color: '#00d4ff' } }, - axisLine: { lineStyle: { width: 12, color: [[1, 'rgba(0, 212, 255, 0.15)']] } }, - axisTick: { show: false }, - splitLine: { show: false }, - axisLabel: { show: false }, - pointer: { show: false }, - title: { show: false }, - detail: { - fontSize: 22, - fontWeight: 700, - color: '#00d4ff', - offsetCenter: [0, '10%'], - formatter: '{value}%', - }, - data: [{ value: selfUseRate.toFixed(1) }], - }], - }; - - return ( -
-
综合能源概览
-
-
-
- 今日用电 - - - kWh - -
-
- 光伏发电 - - - kWh - -
-
-
-
- 电网购电 - - - kWh - -
-
- 实时功率 - - - kW - -
-
-
-
- -
-
-
- 碳排放 - - - kgCO2 - -
-
- 碳减排 - - - kgCO2 - -
-
-
-
-
- ); -} diff --git a/frontend/src/pages/BigScreen/components/HeatPumpCard.tsx b/frontend/src/pages/BigScreen/components/HeatPumpCard.tsx deleted file mode 100644 index 80567a6..0000000 --- a/frontend/src/pages/BigScreen/components/HeatPumpCard.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import ReactECharts from 'echarts-for-react'; -import styles from '../styles.module.css'; -import AnimatedNumber from './AnimatedNumber'; - -interface Props { - realtime: any; - overview: any; -} - -export default function HeatPumpCard({ realtime, overview }: Props) { - const hpPower = realtime?.heatpump_power ?? 0; - const cop = overview?.heatpump_cop ?? 3.5; - const todayConsumption = overview?.heatpump_consumption_today ?? 0; - const monthlyConsumption = overview?.heatpump_monthly_consumption ?? 0; - const operatingHours = overview?.heatpump_operating_hours ?? 0; - - const copGaugeOption = { - series: [{ - type: 'gauge', - startAngle: 220, - endAngle: -40, - radius: '90%', - center: ['50%', '55%'], - min: 0, - max: 6, - splitNumber: 3, - progress: { - show: true, - width: 10, - itemStyle: { color: cop >= 3 ? '#00ff88' : '#ff8c00' }, - }, - axisLine: { lineStyle: { width: 10, color: [[1, 'rgba(0, 255, 136, 0.12)']] } }, - axisTick: { show: false }, - splitLine: { show: false }, - axisLabel: { show: false }, - pointer: { show: false }, - title: { show: false }, - detail: { - fontSize: 20, - fontWeight: 700, - color: '#00ff88', - offsetCenter: [0, '10%'], - formatter: '{value}', - }, - data: [{ value: cop.toFixed(2) }], - }], - }; - - return ( -
-
热泵系统
-
-
-
-
实时功率
- - - kW - -
-
- -
-
- 平均COP -
-
-
-
- 今日用电 - - - kWh - -
-
- 本月用电 - - - kWh - -
-
-
-
- 今日运行 - - - h - -
-
-
-
- ); -} diff --git a/frontend/src/pages/BigScreen/components/LoadCurveCard.tsx b/frontend/src/pages/BigScreen/components/LoadCurveCard.tsx deleted file mode 100644 index d282d78..0000000 --- a/frontend/src/pages/BigScreen/components/LoadCurveCard.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import ReactECharts from 'echarts-for-react'; -import styles from '../styles.module.css'; - -interface Props { - loadData: any; -} - -export default function LoadCurveCard({ loadData }: Props) { - const hours = loadData?.hours ?? Array.from({ length: 24 }, (_, i) => `${i}:00`); - const values = loadData?.values ?? new Array(24).fill(0); - const peak = values.length ? Math.max(...values) : 0; - const valley = values.length ? Math.min(...values.filter((v: number) => v > 0)) || 0 : 0; - const avg = values.length ? values.reduce((a: number, b: number) => a + b, 0) / values.filter((v: number) => v > 0).length || 0 : 0; - - const option = { - grid: { left: 40, right: 12, top: 30, bottom: 24 }, - tooltip: { - trigger: 'axis' as const, - backgroundColor: 'rgba(6,30,62,0.9)', - borderColor: 'rgba(0,212,255,0.3)', - textStyle: { color: '#e0e8f0', fontSize: 12 }, - }, - xAxis: { - type: 'category' as const, - data: hours, - axisLine: { lineStyle: { color: 'rgba(0,212,255,0.2)' } }, - axisLabel: { fontSize: 10, color: 'rgba(224,232,240,0.5)', interval: 3 }, - axisTick: { show: false }, - }, - yAxis: { - type: 'value' as const, - name: 'kW', - nameTextStyle: { color: 'rgba(224,232,240,0.4)', fontSize: 10 }, - splitLine: { lineStyle: { color: 'rgba(0,212,255,0.08)' } }, - axisLabel: { fontSize: 10, color: 'rgba(224,232,240,0.4)' }, - }, - series: [{ - type: 'line', - data: values, - smooth: true, - symbol: 'none', - lineStyle: { color: '#00d4ff', width: 2 }, - areaStyle: { - color: { - type: 'linear', - x: 0, y: 0, x2: 0, y2: 1, - colorStops: [ - { offset: 0, color: 'rgba(0, 212, 255, 0.3)' }, - { offset: 1, color: 'rgba(0, 212, 255, 0.02)' }, - ], - } as any, - }, - markLine: { - silent: true, - symbol: 'none', - lineStyle: { type: 'dashed' as const, width: 1 }, - data: [ - { yAxis: peak, label: { formatter: '峰值 {c}kW', color: '#ff4757', fontSize: 10 }, lineStyle: { color: '#ff475740' } }, - { yAxis: avg, label: { formatter: '均值 {c}kW', color: '#ff8c00', fontSize: 10 }, lineStyle: { color: '#ff8c0040' } }, - ], - }, - }], - }; - - return ( -
-
用电分析
-
-
-
- 峰值 {peak.toFixed(1)} kW -
-
- 谷值 {valley.toFixed(1)} kW -
-
-
- -
-
-
- ); -} diff --git a/frontend/src/pages/BigScreen/components/PVCard.tsx b/frontend/src/pages/BigScreen/components/PVCard.tsx deleted file mode 100644 index 05bc661..0000000 --- a/frontend/src/pages/BigScreen/components/PVCard.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import ReactECharts from 'echarts-for-react'; -import styles from '../styles.module.css'; -import AnimatedNumber from './AnimatedNumber'; - -interface Props { - realtime: any; - overview: any; -} - -export default function PVCard({ realtime, overview }: Props) { - const pvPower = realtime?.pv_power ?? 0; - const todayGen = overview?.pv_generation_today ?? 0; - const monthlyGen = overview?.pv_monthly_generation ?? 0; - const selfUseRate = overview?.self_consumption_rate ?? 0; - - // Donut for self-use ratio - const donutOption = { - series: [{ - type: 'pie', - radius: ['60%', '80%'], - center: ['50%', '50%'], - silent: true, - label: { show: false }, - data: [ - { value: selfUseRate, itemStyle: { color: '#00ff88' } }, - { value: 100 - selfUseRate, itemStyle: { color: 'rgba(0, 255, 136, 0.1)' } }, - ], - }], - }; - - // Monthly PV bar chart (mock 12 months) - const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; - const monthlyData = overview?.monthly_pv_data ?? months.map(() => Math.round(Math.random() * 3000 + 1000)); - - const barOption = { - grid: { left: 35, right: 8, top: 8, bottom: 20 }, - xAxis: { - type: 'category' as const, - data: months, - axisLine: { lineStyle: { color: 'rgba(0,212,255,0.2)' } }, - axisLabel: { fontSize: 10, color: 'rgba(224,232,240,0.5)' }, - axisTick: { show: false }, - }, - yAxis: { - type: 'value' as const, - splitLine: { lineStyle: { color: 'rgba(0,212,255,0.08)' } }, - axisLabel: { fontSize: 10, color: 'rgba(224,232,240,0.4)' }, - }, - series: [{ - type: 'bar', - data: monthlyData, - itemStyle: { - color: { - type: 'linear', - x: 0, y: 0, x2: 0, y2: 1, - colorStops: [ - { offset: 0, color: '#00ff88' }, - { offset: 1, color: 'rgba(0, 255, 136, 0.2)' }, - ], - } as any, - borderRadius: [2, 2, 0, 0], - }, - barWidth: '50%', - }], - tooltip: { trigger: 'axis' as const, backgroundColor: 'rgba(6,30,62,0.9)', borderColor: 'rgba(0,212,255,0.3)', textStyle: { color: '#e0e8f0', fontSize: 12 } }, - }; - - return ( -
-
光伏发电
-
-
-
-
实时功率
- - - kW - -
-
- -
-
- 自用率
- {selfUseRate.toFixed(1)}% -
-
-
-
- 今日发电 - - - kWh - -
-
- 本月发电 - - - kWh - -
-
-
- -
-
-
- ); -} diff --git a/frontend/src/pages/BigScreen/index.tsx b/frontend/src/pages/BigScreen/index.tsx deleted file mode 100644 index af92b77..0000000 --- a/frontend/src/pages/BigScreen/index.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { useEffect, useState, useCallback, useRef } from 'react'; -import styles from './styles.module.css'; -import EnergyOverviewCard from './components/EnergyOverviewCard'; -import PVCard from './components/PVCard'; -import HeatPumpCard from './components/HeatPumpCard'; -import EnergyFlowDiagram from './components/EnergyFlowDiagram'; -import LoadCurveCard from './components/LoadCurveCard'; -import AlarmCard from './components/AlarmCard'; -import CarbonCard from './components/CarbonCard'; -import AnimatedNumber from './components/AnimatedNumber'; -import useRealtimeWebSocket from '../../hooks/useRealtimeWebSocket'; -import { - getDashboardOverview, - getRealtimeData, - getLoadCurve, - getAlarmEvents, - getAlarmStats, - getCarbonOverview, - getCarbonTrend, - getDeviceStats, -} from '../../services/api'; - -export default function BigScreen() { - const [clock, setClock] = useState(new Date()); - const [overview, setOverview] = useState(null); - const [realtime, setRealtime] = useState(null); - const [loadData, setLoadData] = useState(null); - const [alarmEvents, setAlarmEvents] = useState([]); - const [alarmStats, setAlarmStats] = useState(null); - const [carbonOverview, setCarbonOverview] = useState(null); - const [carbonTrend, setCarbonTrend] = useState(null); - const [deviceStats, setDeviceStats] = useState(null); - const timerRef = useRef(null); - - // WebSocket for real-time updates - const { data: wsData, connected: wsConnected, usingFallback } = useRealtimeWebSocket({ - onAlarmEvent: (alarm) => { - // Prepend new alarm to events list - setAlarmEvents((prev) => [alarm as any, ...prev].slice(0, 5)); - // Increment active alarm count - setAlarmStats((prev: any) => prev ? { ...prev, active_count: (prev.active_count ?? 0) + 1 } : prev); - }, - }); - - // Merge WebSocket realtime data into state - useEffect(() => { - if (wsData) { - setRealtime((prev: any) => ({ - ...prev, - pv_power: wsData.pv_power, - heatpump_power: wsData.heatpump_power, - total_power: wsData.total_load, - grid_power: wsData.grid_power, - })); - } - }, [wsData]); - - // Clock update every second - useEffect(() => { - const t = setInterval(() => setClock(new Date()), 1000); - return () => clearInterval(t); - }, []); - - // Fetch all data - const fetchAll = useCallback(async () => { - try { - const results = await Promise.allSettled([ - getDashboardOverview(), - getRealtimeData(), - getLoadCurve(24), - getAlarmEvents({ limit: 5 }), - getAlarmStats(), - getCarbonOverview(), - getCarbonTrend(365), - getDeviceStats(), - ]); - - const get = (i: number) => { - const r = results[i]; - if (r.status === 'fulfilled') { - const val = r.value as any; - return val?.data ?? val; - } - return null; - }; - - if (get(0)) setOverview(get(0)); - if (get(1)) setRealtime(get(1)); - if (get(2)) setLoadData(get(2)); - if (get(3)) { - const alarms = get(3); - setAlarmEvents(Array.isArray(alarms) ? alarms : alarms?.items ?? alarms?.events ?? []); - } - if (get(4)) setAlarmStats(get(4)); - if (get(5)) setCarbonOverview(get(5)); - if (get(6)) setCarbonTrend(get(6)); - if (get(7)) setDeviceStats(get(7)); - } catch (e) { - console.error('BigScreen fetch error:', e); - } - }, []); - - // Initial fetch always. Polling at 15s only if WS is disconnected (fallback). - // When WS is connected, poll at 60s for non-realtime data (overview, load curve, carbon, etc.) - useEffect(() => { - fetchAll(); - const interval = wsConnected && !usingFallback ? 60000 : 15000; - timerRef.current = setInterval(fetchAll, interval); - return () => clearInterval(timerRef.current); - }, [fetchAll, wsConnected, usingFallback]); - - const formatDate = (d: Date) => { - const y = d.getFullYear(); - const m = (d.getMonth() + 1).toString().padStart(2, '0'); - const day = d.getDate().toString().padStart(2, '0'); - const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']; - return `${y}年${m}月${day}日 ${weekdays[d.getDay()]}`; - }; - - const formatTime = (d: Date) => { - return d.toLocaleTimeString('zh-CN', { hour12: false }); - }; - - const totalDevices = deviceStats?.total ?? 0; - const onlineDevices = deviceStats?.online ?? 0; - const offlineDevices = deviceStats?.offline ?? 0; - const alarmDevices = deviceStats?.alarm_count ?? alarmStats?.active_count ?? 0; - - return ( -
- {/* Header */} -
- {formatDate(clock)} -

天普零碳园区智慧能源管理平台

- {formatTime(clock)} -
- - {/* WebSocket connection indicator */} -
- - {wsConnected ? '实时' : '轮询'} -
- - {/* Main 3-column grid */} -
- {/* Left Column */} -
- - - -
- - {/* Center Column */} -
-
-
能源流向
- -
-
-
设备状态
-
-
- - 总设备 - -
-
- - 在线 - -
-
- - 离线 - -
-
- - 告警 - -
-
-
-
- - {/* Right Column */} -
- - - -
-
-
- ); -} diff --git a/frontend/src/pages/BigScreen/styles.module.css b/frontend/src/pages/BigScreen/styles.module.css deleted file mode 100644 index 04697bf..0000000 --- a/frontend/src/pages/BigScreen/styles.module.css +++ /dev/null @@ -1,658 +0,0 @@ -/* Big Screen Visualization Dashboard - Dark Theme */ - -.container { - width: 100vw; - height: 100vh; - background: #0a1628; - background-image: - radial-gradient(circle at 1px 1px, rgba(0, 212, 255, 0.06) 1px, transparent 0); - background-size: 40px 40px; - color: #e0e8f0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; - overflow: hidden; - display: flex; - flex-direction: column; -} - -/* Header */ -.header { - height: 72px; - display: flex; - align-items: center; - justify-content: center; - position: relative; - background: linear-gradient(180deg, rgba(0, 212, 255, 0.12) 0%, transparent 100%); - border-bottom: 1px solid rgba(0, 212, 255, 0.2); - flex-shrink: 0; -} - -.headerTitle { - font-size: 28px; - font-weight: 700; - letter-spacing: 6px; - background: linear-gradient(90deg, #00d4ff, #00ff88, #00d4ff); - background-size: 200% 100%; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - animation: shimmer 4s ease-in-out infinite; -} - -@keyframes shimmer { - 0%, 100% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } -} - -.headerTime { - position: absolute; - right: 32px; - font-size: 16px; - color: rgba(0, 212, 255, 0.8); - letter-spacing: 2px; -} - -.headerDate { - position: absolute; - left: 32px; - font-size: 14px; - color: rgba(0, 212, 255, 0.6); - letter-spacing: 1px; -} - -/* Main Grid */ -.mainGrid { - flex: 1; - display: grid; - grid-template-columns: 1fr 2fr 1fr; - gap: 12px; - padding: 12px; - min-height: 0; -} - -.column { - display: flex; - flex-direction: column; - gap: 12px; - min-height: 0; -} - -/* Cards */ -.card { - background: rgba(6, 30, 62, 0.85); - border: 1px solid rgba(0, 212, 255, 0.25); - border-radius: 8px; - padding: 14px 16px; - position: relative; - overflow: hidden; - box-shadow: - 0 0 12px rgba(0, 212, 255, 0.08), - inset 0 1px 0 rgba(0, 212, 255, 0.1); - display: flex; - flex-direction: column; -} - -.card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 2px; - background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.5), transparent); -} - -.cardTitle { - font-size: 15px; - font-weight: 600; - color: #00d4ff; - margin-bottom: 12px; - padding-left: 10px; - border-left: 3px solid #00d4ff; - letter-spacing: 2px; - flex-shrink: 0; -} - -.cardBody { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; -} - -/* Big numbers */ -.bigNumber { - font-size: 32px; - font-weight: 700; - color: #00ff88; - line-height: 1; -} - -.bigNumberCyan { - font-size: 32px; - font-weight: 700; - color: #00d4ff; - line-height: 1; -} - -.bigNumberOrange { - font-size: 32px; - font-weight: 700; - color: #ff8c00; - line-height: 1; -} - -.bigNumberRed { - font-size: 24px; - font-weight: 700; - color: #ff4757; - line-height: 1; -} - -.unit { - font-size: 13px; - font-weight: 400; - color: rgba(224, 232, 240, 0.6); - margin-left: 4px; -} - -/* Stat grid rows */ -.statRow { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; - margin-bottom: 8px; -} - -.statItem { - display: flex; - flex-direction: column; - gap: 2px; -} - -.statLabel { - font-size: 12px; - color: rgba(224, 232, 240, 0.5); -} - -.statValue { - font-size: 18px; - font-weight: 600; - color: #e0e8f0; -} - -.statValueCyan { - font-size: 18px; - font-weight: 600; - color: #00d4ff; -} - -.statValueGreen { - font-size: 18px; - font-weight: 600; - color: #00ff88; -} - -.statValueOrange { - font-size: 18px; - font-weight: 600; - color: #ff8c00; -} - -/* Center - Energy Flow */ -.centerCard { - background: rgba(6, 30, 62, 0.85); - border: 1px solid rgba(0, 212, 255, 0.25); - border-radius: 8px; - padding: 16px; - flex: 1; - position: relative; - overflow: hidden; - box-shadow: - 0 0 12px rgba(0, 212, 255, 0.08), - inset 0 1px 0 rgba(0, 212, 255, 0.1); - display: flex; - flex-direction: column; -} - -.centerCard::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 2px; - background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.5), transparent); -} - -/* Device status bar */ -.deviceStatusBar { - display: flex; - gap: 24px; - justify-content: center; - padding: 8px 0; - flex-shrink: 0; -} - -.deviceStatusItem { - display: flex; - align-items: center; - gap: 8px; -} - -.statusDot { - width: 10px; - height: 10px; - border-radius: 50%; -} - -.statusDotGreen { - composes: statusDot; - background: #00ff88; - box-shadow: 0 0 8px rgba(0, 255, 136, 0.5); -} - -.statusDotRed { - composes: statusDot; - background: #ff4757; - box-shadow: 0 0 8px rgba(255, 71, 87, 0.5); -} - -.statusDotOrange { - composes: statusDot; - background: #ff8c00; - box-shadow: 0 0 8px rgba(255, 140, 0, 0.5); -} - -.statusDotCyan { - composes: statusDot; - background: #00d4ff; - box-shadow: 0 0 8px rgba(0, 212, 255, 0.5); -} - -.statusLabel { - font-size: 13px; - color: rgba(224, 232, 240, 0.6); -} - -.statusCount { - font-size: 18px; - font-weight: 700; - color: #e0e8f0; -} - -/* Alarm list */ -.alarmList { - flex: 1; - overflow: hidden; -} - -.alarmItem { - display: flex; - align-items: center; - gap: 8px; - padding: 6px 0; - border-bottom: 1px solid rgba(0, 212, 255, 0.1); - font-size: 12px; -} - -.alarmSeverity { - width: 6px; - height: 6px; - border-radius: 50%; - flex-shrink: 0; -} - -.alarmSeverityCritical { - composes: alarmSeverity; - background: #ff4757; - box-shadow: 0 0 6px rgba(255, 71, 87, 0.6); -} - -.alarmSeverityWarning { - composes: alarmSeverity; - background: #ff8c00; - box-shadow: 0 0 6px rgba(255, 140, 0, 0.6); -} - -.alarmSeverityInfo { - composes: alarmSeverity; - background: #00d4ff; - box-shadow: 0 0 6px rgba(0, 212, 255, 0.6); -} - -.alarmMsg { - flex: 1; - color: rgba(224, 232, 240, 0.8); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.alarmTime { - color: rgba(224, 232, 240, 0.4); - flex-shrink: 0; - font-size: 11px; -} - -/* Progress ring */ -.progressRing { - display: flex; - align-items: center; - justify-content: center; - gap: 16px; -} - -/* Chart wrapper */ -.chartWrap { - flex: 1; - min-height: 0; -} - -/* Energy Flow SVG area */ -.energyFlowWrap { - flex: 1; - min-height: 0; - position: relative; -} - -/* Animated flow particles */ -@keyframes flowRight { - 0% { offset-distance: 0%; opacity: 0; } - 10% { opacity: 1; } - 90% { opacity: 1; } - 100% { offset-distance: 100%; opacity: 0; } -} - -@keyframes flowLeft { - 0% { offset-distance: 100%; opacity: 0; } - 10% { opacity: 1; } - 90% { opacity: 1; } - 100% { offset-distance: 0%; opacity: 0; } -} - -@keyframes pulse { - 0%, 100% { opacity: 0.6; transform: scale(1); } - 50% { opacity: 1; transform: scale(1.05); } -} - -.pulseAnim { - animation: pulse 2s ease-in-out infinite; -} - -/* Energy flow node */ -.flowNode { - position: absolute; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 140px; - height: 100px; - background: rgba(6, 30, 62, 0.95); - border: 1.5px solid rgba(0, 212, 255, 0.4); - border-radius: 12px; - box-shadow: 0 0 20px rgba(0, 212, 255, 0.15); - z-index: 2; -} - -.flowNodeLabel { - font-size: 13px; - color: rgba(224, 232, 240, 0.7); - margin-bottom: 4px; -} - -.flowNodeValue { - font-size: 22px; - font-weight: 700; - color: #00d4ff; -} - -.flowNodeUnit { - font-size: 11px; - color: rgba(224, 232, 240, 0.5); -} - -/* Gauge display */ -.gaugeWrap { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 8px; -} - -.gaugeLabel { - font-size: 12px; - color: rgba(224, 232, 240, 0.5); -} - -/* WebSocket connection indicator */ -.wsIndicator { - position: fixed; - bottom: 8px; - right: 8px; - font-size: 11px; - color: rgba(224, 232, 240, 0.4); - z-index: 100; - display: flex; - align-items: center; - gap: 4px; -} - -.wsIndicatorDot { - width: 6px; - height: 6px; - border-radius: 50%; -} - -.wsIndicatorDotConnected { - composes: wsIndicatorDot; - background: #00ff88; - box-shadow: 0 0 6px rgba(0, 255, 136, 0.5); -} - -.wsIndicatorDotDisconnected { - composes: wsIndicatorDot; - background: #ff8c00; - box-shadow: 0 0 6px rgba(255, 140, 0, 0.5); -} - -/* ============================================ - Responsive: Tablet (768px and below) - ============================================ */ -@media (max-width: 768px) { - .container { - overflow-y: auto; - overflow-x: hidden; - height: auto; - min-height: 100vh; - } - - .header { - height: 56px; - flex-wrap: wrap; - padding: 0 12px; - } - - .headerTitle { - font-size: 18px; - letter-spacing: 2px; - } - - .headerDate { - position: static; - font-size: 12px; - order: 2; - } - - .headerTime { - position: static; - font-size: 14px; - order: 3; - } - - .mainGrid { - grid-template-columns: 1fr; - gap: 10px; - padding: 10px; - } - - .column { - gap: 10px; - } - - .card { - padding: 12px; - } - - .centerCard { - padding: 12px; - min-height: 300px; - } - - .cardTitle { - font-size: 14px; - margin-bottom: 8px; - } - - .bigNumber, - .bigNumberCyan, - .bigNumberOrange { - font-size: 24px; - } - - .bigNumberRed { - font-size: 20px; - } - - .statValue, - .statValueCyan, - .statValueGreen, - .statValueOrange { - font-size: 16px; - } - - .deviceStatusBar { - flex-wrap: wrap; - gap: 12px; - justify-content: space-around; - } - - .statusCount { - font-size: 16px; - } - - .flowNode { - width: 110px; - height: 80px; - } - - .flowNodeValue { - font-size: 18px; - } - - .flowNodeLabel { - font-size: 11px; - } -} - -/* ============================================ - Responsive: Mobile (375px and below) - ============================================ */ -@media (max-width: 375px) { - .header { - height: 48px; - padding: 0 8px; - } - - .headerTitle { - font-size: 14px; - letter-spacing: 1px; - } - - .headerDate { - display: none; - } - - .headerTime { - font-size: 12px; - } - - .mainGrid { - padding: 6px; - gap: 8px; - } - - .column { - gap: 8px; - } - - .card { - padding: 10px 12px; - } - - .centerCard { - min-height: 250px; - padding: 10px 12px; - } - - .cardTitle { - font-size: 13px; - margin-bottom: 6px; - padding-left: 8px; - } - - .bigNumber, - .bigNumberCyan, - .bigNumberOrange { - font-size: 20px; - } - - .bigNumberRed { - font-size: 18px; - } - - .statRow { - grid-template-columns: 1fr; - gap: 6px; - } - - .statValue, - .statValueCyan, - .statValueGreen, - .statValueOrange { - font-size: 14px; - } - - .statLabel { - font-size: 11px; - } - - .deviceStatusBar { - flex-direction: column; - gap: 8px; - align-items: flex-start; - padding: 4px 8px; - } - - .alarmItem { - font-size: 11px; - padding: 4px 0; - } - - .flowNode { - width: 90px; - height: 64px; - } - - .flowNodeValue { - font-size: 14px; - } - - .flowNodeLabel { - font-size: 10px; - } - - .unit { - font-size: 11px; - } -} diff --git a/frontend/src/pages/BigScreen3D/components/Buildings.tsx b/frontend/src/pages/BigScreen3D/components/Buildings.tsx deleted file mode 100644 index 1d8b3b0..0000000 --- a/frontend/src/pages/BigScreen3D/components/Buildings.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useMemo } from 'react'; -import * as THREE from 'three'; -import { Html } from '@react-three/drei'; -import { BUILDINGS, COLORS } from '../constants'; - -interface BuildingsProps { - detailMode?: boolean; - onBuildingClick?: (building: string) => void; -} - -function WindowGrid({ width, height, depth }: { width: number; height: number; depth: number }) { - const windows = useMemo(() => { - const cols = 4; - const rows = 3; - const winW = 1.5; - const winH = 0.8; - const winD = 0.05; - const gapX = (width - cols * winW) / (cols + 1); - const gapY = (height - rows * winH) / (rows + 1); - const items: { pos: [number, number, number] }[] = []; - - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const x = -width / 2 + gapX + winW / 2 + c * (winW + gapX); - const y = -height / 2 + gapY + winH / 2 + r * (winH + gapY); - items.push({ pos: [x, y, depth / 2 + winD / 2] }); - } - } - return items; - }, [width, height, depth]); - - return ( - - {windows.map((w, i) => ( - - - - - ))} - - ); -} - -function Building({ - label, - position, - size, - opacity, - onClick, -}: { - label: string; - position: [number, number, number]; - size: [number, number, number]; - opacity: number; - onClick?: () => void; -}) { - const [w, h, d] = size; - - const edgesGeo = useMemo(() => { - const box = new THREE.BoxGeometry(w, h, d); - return new THREE.EdgesGeometry(box); - }, [w, h, d]); - - const labelStyle: React.CSSProperties = { - fontSize: '13px', - color: COLORS.text, - background: 'rgba(6, 30, 62, 0.8)', - padding: '2px 8px', - borderRadius: '4px', - border: '1px solid rgba(0, 212, 255, 0.2)', - whiteSpace: 'nowrap', - pointerEvents: 'none', - }; - - return ( - { e.stopPropagation(); onClick(); } : undefined}> - {/* Main body */} - - - - - - {/* Edge highlight */} - - - - - {/* Windows on front face */} - - - {/* Label */} - -
{label}
- -
- ); -} - -export default function Buildings({ detailMode, onBuildingClick }: BuildingsProps) { - const opacity = detailMode ? 0.15 : 0.85; - - return ( - - onBuildingClick('east') : undefined} - /> - onBuildingClick('west') : undefined} - /> - - ); -} diff --git a/frontend/src/pages/BigScreen3D/components/CampusScene.tsx b/frontend/src/pages/BigScreen3D/components/CampusScene.tsx deleted file mode 100644 index bd87228..0000000 --- a/frontend/src/pages/BigScreen3D/components/CampusScene.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { useEffect, useRef, useMemo } from 'react'; -import { Canvas } from '@react-three/fiber'; -import { OrbitControls } from '@react-three/drei'; -import { EffectComposer, Bloom } from '@react-three/postprocessing'; -import { CAMERA, COLORS } from '../constants'; -import { useCameraAnimation } from '../hooks/useCameraAnimation'; -import SceneEnvironment from './SceneEnvironment'; -import Ground from './Ground'; -import Buildings from './Buildings'; -import PVPanels from './PVPanels'; -import HeatPumps from './HeatPumps'; -import DeviceMarkers from './DeviceMarkers'; -import EnergyParticles from './EnergyParticles'; -import DeviceDetailView from './DeviceDetailView'; - -interface CampusSceneProps { - devices: Array; - energyFlow: { nodes: any[]; links: any[] }; - realtimeData: any | null; - selectedDevice: any | null; - selectedDevicePosition: [number, number, number] | null; - detailRealtimeData: Record | null; - hoveredDeviceId: number | null; - viewMode: 'campus' | 'device-detail'; - onDeviceSelect: (device: any) => void; - onDeviceHover: (id: number | null) => void; -} - -// Inner component: handles camera animation from within Canvas context -function CameraController({ - selectedDevicePosition, - viewMode, -}: { - selectedDevicePosition: [number, number, number] | null; - viewMode: 'campus' | 'device-detail'; -}) { - const { animateTo, resetToOverview } = useCameraAnimation(); - const prevViewMode = useRef(viewMode); - - useEffect(() => { - if (viewMode === 'device-detail' && selectedDevicePosition) { - const [x, y, z] = selectedDevicePosition; - animateTo([x + 8, y + 6, z + 10], [x, y, z], 1.5); - } else if (viewMode === 'campus' && prevViewMode.current === 'device-detail') { - resetToOverview(); - } - prevViewMode.current = viewMode; - }, [viewMode, selectedDevicePosition, animateTo, resetToOverview]); - - return null; -} - -// Inner scene content (must be inside Canvas to use R3F hooks) -function SceneContent({ - devices, - energyFlow, - realtimeData, - selectedDevice, - selectedDevicePosition, - detailRealtimeData, - hoveredDeviceId, - viewMode, - onDeviceSelect, - onDeviceHover, -}: CampusSceneProps) { - const detailMode = viewMode === 'device-detail'; - - const { pvDevices, hpDevices, markerDevices } = useMemo(() => { - const pv: Array<{ id: number; code: string; status: string; power?: number; rated_power?: number }> = []; - const hp: Array<{ id: number; code: string; status: string; power?: number }> = []; - const markers: Array<{ - id: number; - code: string; - device_type: string; - name: string; - status: string; - primaryValue?: string; - }> = []; - - devices.forEach((d: any) => { - const type: string = d.device_type ?? ''; - const code: string = d.code ?? ''; - if (type === 'pv_inverter') { - pv.push({ id: d.id, code, status: d.status, power: d.power, rated_power: d.rated_power }); - } else if (type === 'heat_pump') { - hp.push({ id: d.id, code, status: d.status, power: d.power }); - } else if (['meter', 'sensor', 'heat_meter'].includes(type)) { - markers.push({ - id: d.id, - code, - device_type: type, - name: d.name ?? code, - status: d.status, - primaryValue: d.primaryValue, - }); - } - }); - - return { pvDevices: pv, hpDevices: hp, markerDevices: markers }; - }, [devices]); - - return ( - <> - - - - - - - - - - - - - - - - {viewMode === 'device-detail' && selectedDevice && selectedDevicePosition && ( - - )} - - - - - - - - ); -} - -export default function CampusScene(props: CampusSceneProps) { - return ( - - - - ); -} diff --git a/frontend/src/pages/BigScreen3D/components/DeviceDetailView.tsx b/frontend/src/pages/BigScreen3D/components/DeviceDetailView.tsx deleted file mode 100644 index 353d748..0000000 --- a/frontend/src/pages/BigScreen3D/components/DeviceDetailView.tsx +++ /dev/null @@ -1,490 +0,0 @@ -import { useRef, useMemo } from 'react'; -import { useFrame } from '@react-three/fiber'; -import { Html } from '@react-three/drei'; -import * as THREE from 'three'; - -interface DeviceDetailViewProps { - device: { - id: number; - name: string; - code: string; - device_type: string; - status: string; - rated_power?: number; - }; - position: [number, number, number]; - realtimeData: Record | null; -} - -const overlayStyle: React.CSSProperties = { - background: 'rgba(6, 30, 62, 0.95)', - border: '1px solid rgba(0, 212, 255, 0.3)', - padding: 12, - borderRadius: 8, - color: '#e0e8f0', - fontSize: 12, - minWidth: 200, - pointerEvents: 'none' as const, - fontFamily: 'monospace', -}; - -// ─── PV Inverter Detail ───────────────────────────────────────────── -function PVDetail({ - getValue, -}: { - getValue: (key: string) => number; -}) { - const groupRef = useRef(null); - - useFrame((_, delta) => { - if (groupRef.current) { - groupRef.current.rotation.y += 0.1 * delta; - } - }); - - const panels = useMemo(() => { - const items: { pos: [number, number, number] }[] = []; - const cols = 5; - const rows = 3; - const pw = 2.5; - const ph = 1.2; - const depth = 0.08; - const gap = 0.3; - const totalW = cols * pw + (cols - 1) * gap; - const totalD = rows * ph + (rows - 1) * gap; - - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const x = -totalW / 2 + pw / 2 + c * (pw + gap); - const z = -totalD / 2 + ph / 2 + r * (ph + gap); - items.push({ pos: [x, 0, z] }); - } - } - return { items, totalW, depth, pw, ph }; - }, []); - - const tilt = Math.PI / 6; // 30 degrees - - return ( - - {/* Scale the whole panel array 1.5x */} - - {panels.items.map((p, i) => ( - - - - - ))} - {/* Mounting rails */} - - - - - - - - - - - {/* HTML overlay – block diagram + live data */} - -
-
-{`┌─────────┐ ┌──────┐ ┌─────────┐ -│ DC Input │ → │ MPPT │ → │ AC Output│ -└─────────┘ └──────┘ └─────────┘`} -
-
-
DC电压: {getValue('dc_voltage').toFixed(1)} V
-
AC电压: {getValue('ac_voltage').toFixed(1)} V
-
功率: {getValue('power').toFixed(1)} kW
-
温度: {getValue('temperature').toFixed(1)} ℃
-
-
- -
- ); -} - -// ─── Heat Pump Detail ─────────────────────────────────────────────── -function HeatPumpDetail({ - getValue, -}: { - getValue: (key: string) => number; -}) { - const particlesRef = useRef(null); - const fanRef = useRef(null); - - // Refrigerant circulation path - const { curve, particleCount } = useMemo(() => { - const points = [ - new THREE.Vector3(0, 0, 0), // compressor center - new THREE.Vector3(1, 0, 0), // condenser - new THREE.Vector3(1, -0.8, 0), // bottom right - new THREE.Vector3(0, -0.9, 0), // expansion valve - new THREE.Vector3(-1, -0.8, 0), // bottom left - new THREE.Vector3(-1, 0, 0), // evaporator - new THREE.Vector3(-0.5, 0.4, 0), // top left - new THREE.Vector3(0, 0.4, 0), // top center back to compressor - ]; - return { - curve: new THREE.CatmullRomCurve3(points, true), - particleCount: 12, - }; - }, []); - - useFrame((state, delta) => { - // Animate particles along path - if (particlesRef.current) { - const t = state.clock.elapsedTime; - particlesRef.current.children.forEach((child, i) => { - const offset = i / particleCount; - const pos = curve.getPointAt((t * 0.15 + offset) % 1); - child.position.copy(pos); - }); - } - // Spin fan - if (fanRef.current) { - fanRef.current.rotation.y += 3 * delta; - } - }); - - return ( - - {/* Main housing – transparent cutaway */} - - - - - - {/* Compressor */} - - - - - - {/* Evaporator (left) */} - - - - - - {/* Condenser (right) */} - - - - - - {/* Expansion valve */} - - - - - - {/* Refrigerant particles */} - - {Array.from({ length: particleCount }).map((_, i) => ( - - - - - ))} - - - {/* Fan on top */} - - {[0, 1, 2].map((i) => ( - - - - - ))} - {/* Fan hub */} - - - - - - - {/* HTML overlay */} - -
-
热泵详情
-
功率: {getValue('power').toFixed(1)} kW
-
COP: {getValue('cop').toFixed(2)}
-
进水温度: {getValue('inlet_temp').toFixed(1)} ℃
-
出水温度: {getValue('outlet_temp').toFixed(1)} ℃
-
流量: {getValue('flow_rate').toFixed(2)} m³/h
-
室外温度: {getValue('outdoor_temp').toFixed(1)} ℃
-
- -
- ); -} - -// ─── Meter Detail ─────────────────────────────────────────────────── -function MeterDetail({ - getValue, -}: { - getValue: (key: string) => number; -}) { - const needleRef = useRef(null); - - useFrame(() => { - if (needleRef.current) { - const power = getValue('power'); - const angle = (power / 150) * Math.PI - Math.PI / 2; - needleRef.current.rotation.z = angle; - } - }); - - return ( - - {/* Body */} - - - - - - {/* Screen */} - - - - - - {/* Dial face */} - - - - - - {/* Needle */} - - - - - - {/* HTML overlay */} - -
-
电表详情
-
功率: {getValue('power').toFixed(1)} kW
-
电压: {getValue('voltage').toFixed(1)} V
-
电流: {getValue('current').toFixed(2)} A
-
功率因数: {getValue('power_factor').toFixed(3)}
-
- -
- ); -} - -// ─── Heat Meter Detail ────────────────────────────────────────────── -function HeatMeterDetail({ - getValue, -}: { - getValue: (key: string) => number; -}) { - const needleRef = useRef(null); - - useFrame(() => { - if (needleRef.current) { - const power = getValue('heat_power'); - const angle = (power / 200) * Math.PI - Math.PI / 2; - needleRef.current.rotation.z = angle; - } - }); - - return ( - - {/* Body */} - - - - - - {/* Display screen */} - - - - - - {/* Dial */} - - - - - - {/* Needle */} - - - - - - {/* HTML overlay */} - -
-
热量表详情
-
热功率: {getValue('heat_power').toFixed(1)} kW
-
流量: {getValue('flow_rate').toFixed(2)} m³/h
-
供水温度: {getValue('supply_temp').toFixed(1)} ℃
-
回水温度: {getValue('return_temp').toFixed(1)} ℃
-
累计热量: {getValue('cumulative_heat').toFixed(3)} GJ
-
- -
- ); -} - -// ─── Sensor Detail ────────────────────────────────────────────────── -function SensorDetail({ - getValue, -}: { - getValue: (key: string) => number; -}) { - const sphereRef = useRef(null); - const ringRef = useRef(null); - - useFrame((state, delta) => { - // Pulsing glow - if (sphereRef.current) { - const mat = sphereRef.current.material as THREE.MeshStandardMaterial; - mat.emissiveIntensity = 0.3 + 0.3 * Math.sin(state.clock.elapsedTime * 2); - } - // Rotating ring - if (ringRef.current) { - ringRef.current.rotation.y += 0.8 * delta; - } - }); - - return ( - - {/* Sensor sphere */} - - - - - - {/* Antenna */} - - - - - - {/* Ring */} - - - - - - {/* HTML overlay */} - -
-
传感器详情
-
温度: {getValue('temperature').toFixed(1)} ℃
-
湿度: {getValue('humidity').toFixed(1)} %
-
- -
- ); -} - -// ─── Fallback ─────────────────────────────────────────────────────── -function DefaultDetail({ name }: { name: string }) { - const sphereRef = useRef(null); - - useFrame((state) => { - if (sphereRef.current) { - const mat = sphereRef.current.material as THREE.MeshStandardMaterial; - mat.emissiveIntensity = 0.3 + 0.2 * Math.sin(state.clock.elapsedTime * 2); - } - }); - - return ( - - - - - - -
-
{name}
-
- -
- ); -} - -// ─── Main Component ───────────────────────────────────────────────── -export default function DeviceDetailView({ - device, - position, - realtimeData, -}: DeviceDetailViewProps) { - const groupRef = useRef(null); - - useFrame((_, delta) => { - if (groupRef.current) { - groupRef.current.rotation.y += 0.05 * delta; - } - }); - - const getValue = (key: string) => realtimeData?.[key]?.value ?? 0; - - const renderDetail = () => { - switch (device.device_type) { - case 'pv_inverter': - return ; - case 'heat_pump': - return ; - case 'meter': - return ; - case 'heat_meter': - return ; - case 'sensor': - return ; - default: - return ; - } - }; - - return ( - - {renderDetail()} - - ); -} diff --git a/frontend/src/pages/BigScreen3D/components/DeviceInfoPanel.tsx b/frontend/src/pages/BigScreen3D/components/DeviceInfoPanel.tsx deleted file mode 100644 index 8a83728..0000000 --- a/frontend/src/pages/BigScreen3D/components/DeviceInfoPanel.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import styles from '../styles.module.css'; -import { getDeviceRealtime } from '../../../services/api'; -import { getDevicePhoto } from '../../../utils/devicePhoto'; - -interface Device { - id: number; - name: string; - code: string; - device_type: string; - status: string; - model?: string; - manufacturer?: string; - rated_power?: number; -} - -interface DeviceInfoPanelProps { - device: Device | null; - onClose: () => void; - onViewDetail: (device: Device) => void; -} - -interface ParamDef { - key: string; - label: string; - unit: string; -} - -const PARAMS_BY_TYPE: Record = { - pv_inverter: [ - { key: 'power', label: '功率', unit: 'kW' }, - { key: 'daily_energy', label: '日发电量', unit: 'kWh' }, - { key: 'total_energy', label: '累计发电', unit: 'kWh' }, - { key: 'dc_voltage', label: '直流电压', unit: 'V' }, - { key: 'ac_voltage', label: '交流电压', unit: 'V' }, - { key: 'temperature', label: '温度', unit: '℃' }, - ], - heat_pump: [ - { key: 'power', label: '功率', unit: 'kW' }, - { key: 'cop', label: 'COP', unit: '' }, - { key: 'inlet_temp', label: '进水温度', unit: '℃' }, - { key: 'outlet_temp', label: '出水温度', unit: '℃' }, - { key: 'flow_rate', label: '流量', unit: 'm³/h' }, - { key: 'outdoor_temp', label: '室外温度', unit: '℃' }, - ], - meter: [ - { key: 'power', label: '功率', unit: 'kW' }, - { key: 'voltage', label: '电压', unit: 'V' }, - { key: 'current', label: '电流', unit: 'A' }, - { key: 'power_factor', label: '功率因数', unit: '' }, - ], - sensor: [ - { key: 'temperature', label: '温度', unit: '℃' }, - { key: 'humidity', label: '湿度', unit: '%' }, - ], - heat_meter: [ - { key: 'heat_power', label: '热功率', unit: 'kW' }, - { key: 'flow_rate', label: '流量', unit: 'm³/h' }, - { key: 'supply_temp', label: '供水温度', unit: '℃' }, - { key: 'return_temp', label: '回水温度', unit: '℃' }, - { key: 'cumulative_heat', label: '累计热量', unit: 'GJ' }, - ], -}; - -const STATUS_COLORS: Record = { - online: '#00ff88', - offline: '#666666', - alarm: '#ff4757', - maintenance: '#ff8c00', -}; - -const STATUS_LABELS: Record = { - online: '在线', - offline: '离线', - alarm: '告警', - maintenance: '维护', -}; - -export default function DeviceInfoPanel({ device, onClose, onViewDetail }: DeviceInfoPanelProps) { - const [realtimeData, setRealtimeData] = useState>({}); - const timerRef = useRef | null>(null); - - useEffect(() => { - if (!device) { - setRealtimeData({}); - return; - } - - const fetchData = async () => { - try { - const resp = await getDeviceRealtime(device.id) as any; - // API returns { device: {...}, data: { power: {...}, ... } } - const realtimeMap = resp?.data ?? resp; - setRealtimeData(realtimeMap as Record); - } catch { - // ignore fetch errors - } - }; - - fetchData(); - timerRef.current = setInterval(fetchData, 5000); - - return () => { - if (timerRef.current) clearInterval(timerRef.current); - }; - }, [device?.id]); - - if (!device) return null; - - const params = PARAMS_BY_TYPE[device.device_type] || []; - - return ( -
-
- {device.name} - -
- -
- {device.name} -
- -
-
- 状态 - - {STATUS_LABELS[device.status] || device.status} - -
- {device.model && ( -
- 型号 - {device.model} -
- )} - {device.manufacturer && ( -
- 厂家 - {device.manufacturer} -
- )} - {device.rated_power != null && ( -
- 额定功率 - {device.rated_power} kW -
- )} -
- -
- {params.map(param => { - const data = realtimeData[param.key]; - const valueStr = data != null ? `${data.value}${param.unit ? ' ' + param.unit : ''}` : '--'; - return ( -
- {param.label} - {valueStr} -
- ); - })} -
- - -
- ); -} diff --git a/frontend/src/pages/BigScreen3D/components/DeviceListPanel.tsx b/frontend/src/pages/BigScreen3D/components/DeviceListPanel.tsx deleted file mode 100644 index a38c457..0000000 --- a/frontend/src/pages/BigScreen3D/components/DeviceListPanel.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useState } from 'react'; -import styles from '../styles.module.css'; -import { getDevicePhoto } from '../../../utils/devicePhoto'; - -interface Device { - id: number; - name: string; - code: string; - device_type: string; - status: string; - primaryValue?: string; -} - -interface DeviceListPanelProps { - devices: Device[]; - selectedDeviceId: number | null; - onDeviceSelect: (device: Device) => void; -} - -const TYPE_LABELS: Record = { - pv_inverter: '光伏逆变器', - heat_pump: '空气源热泵', - meter: '电表', - sensor: '温湿度传感器', - heat_meter: '热量表', -}; - -const STATUS_COLORS: Record = { - online: '#00ff88', - offline: '#666666', - alarm: '#ff4757', - maintenance: '#ff8c00', -}; - -export default function DeviceListPanel({ devices, selectedDeviceId, onDeviceSelect }: DeviceListPanelProps) { - const [collapsed, setCollapsed] = useState>({}); - - const groups: Record = {}; - for (const device of devices) { - const type = device.device_type; - if (!groups[type]) groups[type] = []; - groups[type].push(device); - } - - const toggleGroup = (type: string) => { - setCollapsed(prev => ({ ...prev, [type]: !prev[type] })); - }; - - return ( -
- {Object.entries(TYPE_LABELS).map(([type, label]) => { - const group = groups[type]; - if (!group || group.length === 0) return null; - const isCollapsed = collapsed[type] ?? false; - - return ( -
-
toggleGroup(type)}> - {isCollapsed ? '▸' : '▾'} {label} -
- {!isCollapsed && - group.map(device => ( -
onDeviceSelect(device)} - > - - - {device.name} - {device.primaryValue && ( - {device.primaryValue} - )} -
- ))} -
- ); - })} -
- ); -} diff --git a/frontend/src/pages/BigScreen3D/components/DeviceMarkers.tsx b/frontend/src/pages/BigScreen3D/components/DeviceMarkers.tsx deleted file mode 100644 index 2fb0bbb..0000000 --- a/frontend/src/pages/BigScreen3D/components/DeviceMarkers.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { useMemo } from 'react'; -import { Html } from '@react-three/drei'; -import { DEVICE_POSITIONS, COLORS } from '../constants'; - -interface DeviceMarkersProps { - devices: Array<{ - id: number; - code: string; - device_type: string; - name: string; - status: string; - primaryValue?: string; - }>; - hoveredId: number | null; - onHover: (id: number | null) => void; - onClick: (device: { id: number; code: string; device_type: string; name: string; status: string; primaryValue?: string }) => void; - detailMode?: boolean; -} - -const labelStyle: React.CSSProperties = { - fontSize: '11px', - color: COLORS.text, - background: 'rgba(6, 30, 62, 0.85)', - padding: '2px 6px', - borderRadius: '3px', - border: '1px solid rgba(0, 212, 255, 0.2)', - whiteSpace: 'nowrap', - pointerEvents: 'none', - textAlign: 'center', -}; - -function MeterMarker({ - device, - position, - isHovered, - accentColor, - onHover, - onClick, -}: { - device: DeviceMarkersProps['devices'][number]; - position: [number, number, number]; - isHovered: boolean; - accentColor: string; - onHover: (id: number | null) => void; - onClick: (device: DeviceMarkersProps['devices'][number]) => void; -}) { - const scale: [number, number, number] = isHovered ? [1.2, 1.2, 1.2] : [1, 1, 1]; - - return ( - { e.stopPropagation(); onHover(device.id); }} - onPointerOut={(e) => { e.stopPropagation(); onHover(null); }} - onClick={(e) => { e.stopPropagation(); onClick(device); }} - > - {/* Body */} - - - - - {/* Front dial */} - - - - - {/* Label */} - -
-
{device.name}
- {device.primaryValue &&
{device.primaryValue}
} -
- -
- ); -} - -function SensorMarker({ - device, - position, - isHovered, - onHover, - onClick, -}: { - device: DeviceMarkersProps['devices'][number]; - position: [number, number, number]; - isHovered: boolean; - onHover: (id: number | null) => void; - onClick: (device: DeviceMarkersProps['devices'][number]) => void; -}) { - const scale: [number, number, number] = isHovered ? [1.2, 1.2, 1.2] : [1, 1, 1]; - - return ( - { e.stopPropagation(); onHover(device.id); }} - onPointerOut={(e) => { e.stopPropagation(); onHover(null); }} - onClick={(e) => { e.stopPropagation(); onClick(device); }} - > - {/* Sphere */} - - - - - {/* Antenna */} - - - - - {/* Label */} - -
-
{device.name}
- {device.primaryValue &&
{device.primaryValue}
} -
- -
- ); -} - -export default function DeviceMarkers({ devices, hoveredId, onHover, onClick }: DeviceMarkersProps) { - const categorized = useMemo(() => { - const meters: typeof devices = []; - const sensors: typeof devices = []; - const heatMeters: typeof devices = []; - - devices.forEach((d) => { - if (d.device_type === 'heat_meter' || d.code.startsWith('HM-')) { - heatMeters.push(d); - } else if (d.code.startsWith('MTR-')) { - meters.push(d); - } else if (d.code.startsWith('SENSOR-')) { - sensors.push(d); - } - }); - - return { meters, sensors, heatMeters }; - }, [devices]); - - return ( - - {/* Regular meters */} - {categorized.meters.map((d) => { - const posInfo = DEVICE_POSITIONS[d.code]; - if (!posInfo) return null; - return ( - - ); - })} - - {/* Heat meters */} - {categorized.heatMeters.map((d) => { - const posInfo = DEVICE_POSITIONS[d.code]; - if (!posInfo) return null; - return ( - - ); - })} - - {/* Sensors */} - {categorized.sensors.map((d) => { - const posInfo = DEVICE_POSITIONS[d.code]; - if (!posInfo) return null; - return ( - - ); - })} - - ); -} diff --git a/frontend/src/pages/BigScreen3D/components/EnergyParticles.tsx b/frontend/src/pages/BigScreen3D/components/EnergyParticles.tsx deleted file mode 100644 index ccab272..0000000 --- a/frontend/src/pages/BigScreen3D/components/EnergyParticles.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useRef, useMemo } from 'react'; -import { useFrame } from '@react-three/fiber'; -import * as THREE from 'three'; -import { ENERGY_FLOW_PATHS } from '../constants'; - -interface EnergyParticlesProps { - energyFlow: { nodes: any[]; links: any[] }; - realtimeData: { pv_power?: number; grid_power?: number; heatpump_power?: number } | null; -} - -interface ParticlePathConfig { - key: string; - curve: THREE.CatmullRomCurve3; - color: string; - power: number; -} - -const MAX_PARTICLES_PER_PATH = 40; -const MIN_PARTICLES = 3; -const PARTICLE_SIZE = 0.4; -const BASE_SPEED = 0.003; -const SPEED_VARIANCE = 0.002; - -export default function EnergyParticles({ energyFlow, realtimeData }: EnergyParticlesProps) { - const pvPower = realtimeData?.pv_power ?? 0; - const gridPower = realtimeData?.grid_power ?? 0; - const hpPower = realtimeData?.heatpump_power ?? 0; - - const paths = useMemo(() => { - const configs: ParticlePathConfig[] = []; - - // PV -> Building - if (pvPower > 0) { - configs.push({ - key: 'pv', - curve: new THREE.CatmullRomCurve3( - ENERGY_FLOW_PATHS.pvToBuilding.waypoints.map((p) => new THREE.Vector3(...p)), - ), - color: ENERGY_FLOW_PATHS.pvToBuilding.color, - power: pvPower, - }); - } - - // Grid -> Building (only when importing, i.e. positive) - if (gridPower > 0) { - configs.push({ - key: 'grid', - curve: new THREE.CatmullRomCurve3( - ENERGY_FLOW_PATHS.gridToBuilding.waypoints.map((p) => new THREE.Vector3(...p)), - ), - color: ENERGY_FLOW_PATHS.gridToBuilding.color, - power: gridPower, - }); - } - - // Building -> HeatPump - if (hpPower > 0) { - configs.push({ - key: 'hp', - curve: new THREE.CatmullRomCurve3( - ENERGY_FLOW_PATHS.buildingToHeatPump.waypoints.map((p) => new THREE.Vector3(...p)), - ), - color: ENERGY_FLOW_PATHS.buildingToHeatPump.color, - power: hpPower, - }); - } - - return configs; - }, [pvPower, gridPower, hpPower]); - - return ( - - {paths.map((path) => ( - - ))} - - ); -} - -function ParticlePath({ config }: { config: ParticlePathConfig }) { - const { curve, color, power } = config; - - const count = Math.max( - MIN_PARTICLES, - Math.min(Math.floor(power / 5), MAX_PARTICLES_PER_PATH), - ); - - // Stable random speeds per particle - const speeds = useMemo( - () => Array.from({ length: count }, () => BASE_SPEED + Math.random() * SPEED_VARIANCE), - [count], - ); - - // Progress values (0..1) for each particle along the curve - const progressRef = useRef(new Float32Array(0)); - if (progressRef.current.length !== count) { - progressRef.current = new Float32Array(count); - for (let i = 0; i < count; i++) { - progressRef.current[i] = Math.random(); - } - } - - const positionsRef = useRef(new Float32Array(count * 3)); - if (positionsRef.current.length !== count * 3) { - positionsRef.current = new Float32Array(count * 3); - } - - const geomRef = useRef(null); - - // Initialize positions - useMemo(() => { - const pos = positionsRef.current; - for (let i = 0; i < count; i++) { - const pt = curve.getPointAt(progressRef.current[i]); - pos[i * 3] = pt.x; - pos[i * 3 + 1] = pt.y; - pos[i * 3 + 2] = pt.z; - } - }, [curve, count]); - - useFrame(() => { - const prog = progressRef.current; - const pos = positionsRef.current; - - for (let i = 0; i < count; i++) { - prog[i] = (prog[i] + speeds[i]) % 1; - const pt = curve.getPointAt(prog[i]); - pos[i * 3] = pt.x; - pos[i * 3 + 1] = pt.y; - pos[i * 3 + 2] = pt.z; - } - - if (geomRef.current) { - const attr = geomRef.current.getAttribute('position') as THREE.BufferAttribute; - attr.needsUpdate = true; - } - }); - - const material = useMemo( - () => - new THREE.PointsMaterial({ - size: PARTICLE_SIZE, - color: new THREE.Color(color), - sizeAttenuation: true, - blending: THREE.AdditiveBlending, - transparent: true, - opacity: 0.8, - depthWrite: false, - }), - [color], - ); - - return ( - - - - - - ); -} diff --git a/frontend/src/pages/BigScreen3D/components/Ground.tsx b/frontend/src/pages/BigScreen3D/components/Ground.tsx deleted file mode 100644 index 89728d8..0000000 --- a/frontend/src/pages/BigScreen3D/components/Ground.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Grid } from '@react-three/drei'; - -export default function Ground() { - return ( - - - - - - - - ); -} diff --git a/frontend/src/pages/BigScreen3D/components/HUDOverlay.tsx b/frontend/src/pages/BigScreen3D/components/HUDOverlay.tsx deleted file mode 100644 index 862a575..0000000 --- a/frontend/src/pages/BigScreen3D/components/HUDOverlay.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { useEffect, useState } from 'react'; -import styles from '../styles.module.css'; -import AnimatedNumber from '../../BigScreen/components/AnimatedNumber'; - -interface HUDOverlayProps { - overview: { - today_generation?: number; - today_consumption?: number; - carbon_reduction?: number; - active_alarms?: number; - } | null; - realtimeData: { - pv_power?: number; - grid_power?: number; - heat_pump_power?: number; - total_load?: number; - } | null; - deviceStats: { - online?: number; - total?: number; - } | null; -} - -const WEEKDAYS = ['日', '一', '二', '三', '四', '五', '六']; - -function formatDate(date: Date): string { - const y = date.getFullYear(); - const m = String(date.getMonth() + 1).padStart(2, '0'); - const d = String(date.getDate()).padStart(2, '0'); - const w = WEEKDAYS[date.getDay()]; - return `${y}年${m}月${d}日 星期${w}`; -} - -function formatTime(date: Date): string { - return date.toLocaleTimeString('zh-CN', { hour12: false }); -} - -export default function HUDOverlay({ overview, realtimeData }: HUDOverlayProps) { - const [now, setNow] = useState(new Date()); - - useEffect(() => { - const timer = setInterval(() => setNow(new Date()), 1000); - return () => clearInterval(timer); - }, []); - - return ( -
-
- {formatDate(now)} - 天普零碳园区 3D智慧能源管理平台 - {formatTime(now)} -
- -
-
- 光伏发电 - - - kW - -
-
- 电网功率 - - - kW - -
-
- 总负荷 - - - kW - -
-
- 今日发电 - - - kWh - -
-
- 碳减排 - - - kg - -
-
-
- ); -} diff --git a/frontend/src/pages/BigScreen3D/components/HeatPumps.tsx b/frontend/src/pages/BigScreen3D/components/HeatPumps.tsx deleted file mode 100644 index 8a1b786..0000000 --- a/frontend/src/pages/BigScreen3D/components/HeatPumps.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useRef, useMemo } from 'react'; -import * as THREE from 'three'; -import { useFrame } from '@react-three/fiber'; -import { DEVICE_POSITIONS } from '../constants'; - -interface HeatPumpsProps { - devices: Array<{ id: number; code: string; status: string; power?: number }>; - hoveredId: number | null; - onHover: (id: number | null) => void; - onClick: (device: { id: number; code: string; status: string; power?: number }) => void; - detailMode?: boolean; -} - -const HP_CODES = ['HP-01', 'HP-02', 'HP-03', 'HP-04'] as const; - -function FanBlades({ speed, status }: { speed: number; status: string }) { - const bladesRef = useRef(null); - - useFrame((_, delta) => { - if (!bladesRef.current) return; - if (status === 'offline') return; - bladesRef.current.rotation.y += speed * delta; - }); - - return ( - - {/* Fan housing */} - - - - - {/* Blades */} - - {[0, 1, 2].map((i) => ( - - - - - ))} - - - ); -} - -function HeatPumpUnit({ - position, - device, - isHovered, - onHover, - onClick, -}: { - position: [number, number, number]; - device: { id: number; code: string; status: string; power?: number } | undefined; - isHovered: boolean; - onHover: (id: number | null) => void; - onClick: (device: { id: number; code: string; status: string; power?: number }) => void; -}) { - const meshRef = useRef(null); - const status = device?.status ?? 'offline'; - const power = device?.power ?? 0; - const fanSpeed = status !== 'offline' ? (power / 35) * 2 : 0; - - useFrame((state) => { - if (!meshRef.current) return; - const mat = meshRef.current.material as THREE.MeshStandardMaterial; - if (status === 'alarm') { - mat.emissiveIntensity = Math.sin(state.clock.elapsedTime * 3) * 0.3 + 0.2; - } - }); - - const bodyColor = status === 'offline' ? '#444444' : '#2a4a6a'; - const emissiveColor = status === 'alarm' ? '#ff4757' : status === 'online' ? '#00d4ff' : '#000000'; - const emissiveIntensity = status === 'offline' ? 0 : isHovered ? 0.3 : 0.1; - const scale: [number, number, number] = isHovered ? [1.05, 1.05, 1.05] : [1, 1, 1]; - - return ( - { e.stopPropagation(); if (device) onHover(device.id); }} - onPointerOut={(e) => { e.stopPropagation(); onHover(null); }} - onClick={(e) => { e.stopPropagation(); if (device) onClick(device); }} - > - {/* Main body */} - - - - - - {/* Fan on top */} - - - {/* Side pipes - left */} - - - - - - - - - - {/* Side pipes - right */} - - - - - - - - - - ); -} - -export default function HeatPumps({ devices, hoveredId, onHover, onClick }: HeatPumpsProps) { - const deviceMap = useMemo(() => { - const map = new Map(); - devices.forEach((d) => map.set(d.code, d)); - return map; - }, [devices]); - - return ( - - {HP_CODES.map((code) => { - const pos = DEVICE_POSITIONS[code].position; - const device = deviceMap.get(code); - return ( - - ); - })} - - ); -} diff --git a/frontend/src/pages/BigScreen3D/components/PVPanels.tsx b/frontend/src/pages/BigScreen3D/components/PVPanels.tsx deleted file mode 100644 index 00dffe0..0000000 --- a/frontend/src/pages/BigScreen3D/components/PVPanels.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useRef, useMemo } from 'react'; -import * as THREE from 'three'; -import { useFrame } from '@react-three/fiber'; -import { DEVICE_POSITIONS, PV_ARRAY, COLORS } from '../constants'; - -interface PVPanelsProps { - devices: Array<{ id: number; code: string; status: string; power?: number; rated_power?: number }>; - hoveredId: number | null; - onHover: (id: number | null) => void; - onClick: (device: { id: number; code: string; status: string; power?: number; rated_power?: number }) => void; - detailMode?: boolean; -} - -const PV_ZONES = [ - { code: 'PV-INV-01', center: DEVICE_POSITIONS['PV-INV-01'].position }, - { code: 'PV-INV-02', center: DEVICE_POSITIONS['PV-INV-02'].position }, - { code: 'PV-INV-03', center: DEVICE_POSITIONS['PV-INV-03'].position }, -] as const; - -function PVZone({ - center, - device, - isHovered, - onHover, - onClick, -}: { - center: readonly [number, number, number]; - device: { id: number; code: string; status: string; power?: number; rated_power?: number } | undefined; - isHovered: boolean; - onHover: (id: number | null) => void; - onClick: (device: { id: number; code: string; status: string; power?: number; rated_power?: number }) => void; -}) { - const groupRef = useRef(null); - - const panels = useMemo(() => { - const items: { pos: [number, number, number] }[] = []; - const { cols, rows, panelWidth, panelHeight, gap } = PV_ARRAY; - const totalW = cols * panelWidth + (cols - 1) * gap; - const totalD = rows * panelHeight + (rows - 1) * gap; - - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const x = -totalW / 2 + panelWidth / 2 + c * (panelWidth + gap); - const z = -totalD / 2 + panelHeight / 2 + r * (panelHeight + gap); - items.push({ pos: [x, 0, z] }); - } - } - return items; - }, []); - - const ratio = device && device.rated_power ? (device.power ?? 0) / device.rated_power : 0; - const emissiveIntensity = Math.min(ratio * 0.5, 0.5); - - return ( - { e.stopPropagation(); if (device) onHover(device.id); }} - onPointerOut={(e) => { e.stopPropagation(); onHover(null); }} - onClick={(e) => { e.stopPropagation(); if (device) onClick(device); }} - > - {panels.map((p, i) => ( - - - - - ))} - - ); -} - -export default function PVPanels({ devices, hoveredId, onHover, onClick }: PVPanelsProps) { - const deviceMap = useMemo(() => { - const map = new Map(); - devices.forEach((d) => map.set(d.code, d)); - return map; - }, [devices]); - - return ( - - {PV_ZONES.map((zone) => { - const device = deviceMap.get(zone.code); - return ( - - ); - })} - - ); -} diff --git a/frontend/src/pages/BigScreen3D/components/SceneEnvironment.tsx b/frontend/src/pages/BigScreen3D/components/SceneEnvironment.tsx deleted file mode 100644 index eebd480..0000000 --- a/frontend/src/pages/BigScreen3D/components/SceneEnvironment.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Stars } from '@react-three/drei'; - -export default function SceneEnvironment() { - return ( - <> - - - - - - ); -} diff --git a/frontend/src/pages/BigScreen3D/constants.ts b/frontend/src/pages/BigScreen3D/constants.ts deleted file mode 100644 index b9fd874..0000000 --- a/frontend/src/pages/BigScreen3D/constants.ts +++ /dev/null @@ -1,120 +0,0 @@ -// Campus layout constants for 3D scene -// Scale: 1 unit ≈ 2 meters - -// ============ Colors (matching existing BigScreen dark theme) ============ -export const COLORS = { - background: '#0a1628', - primary: '#00d4ff', - pvGreen: '#00ff88', - gridOrange: '#ff8c00', - alarmRed: '#ff4757', - sensorPurple: '#a78bfa', - heatPumpCyan: '#00d4ff', - buildingBase: '#1a3a5c', - buildingEdge: 'rgba(0, 212, 255, 0.3)', - windowGlow: '#ffcc66', - cardBg: 'rgba(6, 30, 62, 0.85)', - cardBorder: 'rgba(0, 212, 255, 0.25)', - text: '#e0e8f0', - textSecondary: '#8899aa', - groundGrid: '#0d2137', - groundLine: '#1a3a5c', -} as const; - -// ============ Buildings ============ -export const BUILDINGS = { - east: { - label: '东楼', - position: [12, 6, -5] as [number, number, number], - size: [20, 12, 15] as [number, number, number], - }, - west: { - label: '西楼', - position: [-12, 5, -5] as [number, number, number], - size: [16, 10, 12] as [number, number, number], - }, -} as const; - -// ============ Device Positions ============ -export const DEVICE_POSITIONS: Record = { - // PV Inverters (on rooftops) - 'PV-INV-01': { position: [6, 12.3, -8], type: 'pv_inverter' }, - 'PV-INV-02': { position: [18, 12.3, -8], type: 'pv_inverter' }, - 'PV-INV-03': { position: [-12, 10.3, -8], type: 'pv_inverter' }, - - // Heat Pumps (ground level, beside buildings) - 'HP-01': { position: [24, 0, 2], type: 'heat_pump' }, - 'HP-02': { position: [24, 0, 6], type: 'heat_pump' }, - 'HP-03': { position: [-24, 0, 2], type: 'heat_pump' }, - 'HP-04': { position: [-24, 0, 6], type: 'heat_pump' }, - - // Meters (ground, near entrances) - 'MTR-GRID': { position: [0, 0, 14], type: 'meter' }, - 'MTR-PV': { position: [3, 0, 14], type: 'meter' }, - 'MTR-HP': { position: [26, 0, -1], type: 'meter' }, - 'MTR-PUMP': { position: [-26, 0, -1], type: 'meter' }, - - // Sensors (elevated, on buildings) - 'SENSOR-01': { position: [8, 6, 2], type: 'sensor' }, - 'SENSOR-02': { position: [16, 6, 2], type: 'sensor' }, - 'SENSOR-03': { position: [-8, 5, 2], type: 'sensor' }, - 'SENSOR-04': { position: [-16, 5, 2], type: 'sensor' }, - 'SENSOR-05': { position: [0, 4, 5], type: 'sensor' }, - - // Heat Meter - 'HM-01': { position: [26, 0, 4], type: 'heat_meter' }, -}; - -// ============ Camera ============ -export const CAMERA = { - campusPosition: [0, 35, 45] as [number, number, number], - campusTarget: [0, 0, 0] as [number, number, number], - fov: 45, - near: 0.1, - far: 500, - detailDistance: 8, - animationDuration: 1.5, -} as const; - -// ============ Energy Flow Paths ============ -export const ENERGY_FLOW_PATHS = { - pvToBuilding: { - color: '#00ff88', - waypoints: [[12, 13, -8], [12, 10, 0], [0, 6, 5]] as [number, number, number][], - }, - gridToBuilding: { - color: '#ff8c00', - waypoints: [[0, 1, 14], [0, 4, 10], [0, 6, 5]] as [number, number, number][], - }, - buildingToHeatPump: { - color: '#00d4ff', - waypoints: [[0, 6, 5], [12, 3, 4], [24, 1, 4]] as [number, number, number][], - }, -} as const; - -// ============ PV Panel Array ============ -export const PV_ARRAY = { - cols: 5, - rows: 3, - panelWidth: 2, - panelHeight: 1, - panelDepth: 0.05, - gap: 0.3, - tiltAngle: Math.PI / 6, // 30 degrees -} as const; - -// ============ Polling ============ -export const POLL_INTERVAL = 15000; // 15 seconds -export const DETAIL_POLL_INTERVAL = 5000; // 5 seconds for selected device - -// ============ Status Colors ============ -export const STATUS_COLORS: Record = { - online: '#00ff88', - offline: '#666666', - alarm: '#ff4757', - maintenance: '#ff8c00', -}; diff --git a/frontend/src/pages/BigScreen3D/hooks/useCameraAnimation.ts b/frontend/src/pages/BigScreen3D/hooks/useCameraAnimation.ts deleted file mode 100644 index 6f246be..0000000 --- a/frontend/src/pages/BigScreen3D/hooks/useCameraAnimation.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useRef, useCallback } from 'react'; -import { useThree, useFrame } from '@react-three/fiber'; -import * as THREE from 'three'; -import { CAMERA } from '../constants'; - -export function useCameraAnimation() { - const { camera } = useThree(); - - const isAnimating = useRef(false); - const startPos = useRef(new THREE.Vector3()); - const endPos = useRef(new THREE.Vector3()); - const startTarget = useRef(new THREE.Vector3()); - const endTarget = useRef(new THREE.Vector3()); - const progress = useRef(0); - const duration = useRef(CAMERA.animationDuration); - const currentTarget = useRef(new THREE.Vector3()); - - const animateTo = useCallback( - (position: [number, number, number], target: [number, number, number], dur?: number) => { - startPos.current.copy(camera.position); - endPos.current.set(...position); - - // Estimate current look-at target from camera direction - const dir = new THREE.Vector3(); - camera.getWorldDirection(dir); - startTarget.current.copy(camera.position).add(dir.multiplyScalar(10)); - - endTarget.current.set(...target); - duration.current = dur ?? CAMERA.animationDuration; - progress.current = 0; - isAnimating.current = true; - }, - [camera], - ); - - const resetToOverview = useCallback(() => { - animateTo( - CAMERA.campusPosition as [number, number, number], - CAMERA.campusTarget as [number, number, number], - ); - }, [animateTo]); - - useFrame((_, delta) => { - if (!isAnimating.current) return; - - progress.current = Math.min(progress.current + delta / duration.current, 1); - - // Smooth ease-in-out - const t = progress.current < 0.5 - ? 2 * progress.current * progress.current - : 1 - Math.pow(-2 * progress.current + 2, 2) / 2; - - camera.position.lerpVectors(startPos.current, endPos.current, t); - currentTarget.current.lerpVectors(startTarget.current, endTarget.current, t); - camera.lookAt(currentTarget.current); - - if (progress.current >= 1) { - isAnimating.current = false; - } - }); - - return { - animateTo, - resetToOverview, - get isAnimating() { - return isAnimating.current; - }, - }; -} diff --git a/frontend/src/pages/BigScreen3D/hooks/useDeviceData.ts b/frontend/src/pages/BigScreen3D/hooks/useDeviceData.ts deleted file mode 100644 index 5cfc06b..0000000 --- a/frontend/src/pages/BigScreen3D/hooks/useDeviceData.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; -import { getDevices, getDeviceStats, getDashboardOverview, getRealtimeData } from '../../../services/api'; -import type { DeviceInfo, DeviceWithPosition, OverviewData, RealtimePowerData } from '../types'; -import { DEVICE_POSITIONS, POLL_INTERVAL } from '../constants'; - -interface DeviceStats { - online: number; - offline: number; - alarm: number; - maintenance: number; - total: number; -} - -// Ordered position keys by device type for fuzzy matching -const POSITION_KEYS_BY_TYPE: Record = { - pv_inverter: ['PV-INV-01', 'PV-INV-02', 'PV-INV-03'], - heat_pump: ['HP-01', 'HP-02', 'HP-03', 'HP-04'], - meter: ['MTR-GRID', 'MTR-PV', 'MTR-HP', 'MTR-PUMP'], - sensor: ['SENSOR-01', 'SENSOR-02', 'SENSOR-03', 'SENSOR-04', 'SENSOR-05'], - heat_meter: ['HM-01'], -}; - -function matchDevicesToPositions(devices: DeviceInfo[]): DeviceWithPosition[] { - const usedPositions = new Set(); - const result: DeviceWithPosition[] = []; - - // Group devices by type - const byType: Record = {}; - for (const device of devices) { - const type = device.device_type || 'unknown'; - if (!byType[type]) byType[type] = []; - byType[type].push(device); - } - - for (const [type, typeDevices] of Object.entries(byType)) { - const positionKeys = POSITION_KEYS_BY_TYPE[type] || []; - - typeDevices.forEach((device, index) => { - // Try exact match by device code first - let matchedKey: string | undefined; - if (device.code && DEVICE_POSITIONS[device.code] && !usedPositions.has(device.code)) { - matchedKey = device.code; - } - - // Fall back to ordered assignment by type - if (!matchedKey && index < positionKeys.length) { - const key = positionKeys[index]; - if (!usedPositions.has(key)) { - matchedKey = key; - } - } - - const withPos: DeviceWithPosition = { ...device }; - if (matchedKey) { - usedPositions.add(matchedKey); - const posData = DEVICE_POSITIONS[matchedKey]; - withPos.position3D = posData.position; - withPos.rotation3D = posData.rotation; - // Override code with matched key so 3D components can look up positions by code - withPos.code = matchedKey; - } - - result.push(withPos); - }); - } - - return result; -} - -export function useDeviceData() { - const [devices, setDevices] = useState([]); - const [deviceStats, setDeviceStats] = useState(null); - const [overview, setOverview] = useState(null); - const [realtimeData, setRealtimeData] = useState(null); - const [devicesWithPositions, setDevicesWithPositions] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const intervalRef = useRef | null>(null); - - const fetchAll = useCallback(async () => { - try { - const results = await Promise.allSettled([ - getDevices({ page_size: 100 }) as Promise, - getDeviceStats() as Promise, - getDashboardOverview() as Promise, - getRealtimeData() as Promise, - ]); - - // Devices - if (results[0].status === 'fulfilled') { - const devData = results[0].value; - const items: DeviceInfo[] = devData?.items || []; - setDevices(items); - setDevicesWithPositions(matchDevicesToPositions(items)); - } - - // Stats - if (results[1].status === 'fulfilled') { - setDeviceStats(results[1].value as DeviceStats); - } - - // Overview - if (results[2].status === 'fulfilled') { - setOverview(results[2].value as OverviewData); - } - - // Realtime - if (results[3].status === 'fulfilled') { - setRealtimeData(results[3].value as RealtimePowerData); - } - - setError(null); - } catch (err: any) { - setError(err?.message || 'Failed to fetch device data'); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchAll(); - intervalRef.current = setInterval(fetchAll, POLL_INTERVAL); - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [fetchAll]); - - return { devices, deviceStats, overview, realtimeData, devicesWithPositions, loading, error }; -} diff --git a/frontend/src/pages/BigScreen3D/hooks/useEnergyFlow.ts b/frontend/src/pages/BigScreen3D/hooks/useEnergyFlow.ts deleted file mode 100644 index 4ad8df9..0000000 --- a/frontend/src/pages/BigScreen3D/hooks/useEnergyFlow.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; -import { getEnergyFlow } from '../../../services/api'; -import type { EnergyFlowNode, EnergyFlowLink } from '../types'; -import { POLL_INTERVAL } from '../constants'; - -export function useEnergyFlow() { - const [nodes, setNodes] = useState([]); - const [links, setLinks] = useState([]); - const [loading, setLoading] = useState(true); - const intervalRef = useRef | null>(null); - - const fetchFlow = useCallback(async () => { - try { - const data = (await getEnergyFlow()) as any; - setNodes(data?.nodes || []); - setLinks(data?.links || []); - } catch { - // Silently ignore — stale data is acceptable for flow visualization - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchFlow(); - intervalRef.current = setInterval(fetchFlow, POLL_INTERVAL); - return () => { - if (intervalRef.current) clearInterval(intervalRef.current); - }; - }, [fetchFlow]); - - return { nodes, links, loading }; -} diff --git a/frontend/src/pages/BigScreen3D/index.tsx b/frontend/src/pages/BigScreen3D/index.tsx deleted file mode 100644 index b9aa1fe..0000000 --- a/frontend/src/pages/BigScreen3D/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useState, useCallback, useEffect, useRef } from 'react'; -import styles from './styles.module.css'; -import type { DeviceInfo, ViewMode } from './types'; -import { useDeviceData } from './hooks/useDeviceData'; -import { useEnergyFlow } from './hooks/useEnergyFlow'; -import { getDeviceRealtime } from '../../services/api'; -import CampusScene from './components/CampusScene'; -import HUDOverlay from './components/HUDOverlay'; -import DeviceListPanel from './components/DeviceListPanel'; -import DeviceInfoPanel from './components/DeviceInfoPanel'; - -export default function BigScreen3D() { - const { devices, deviceStats, overview, realtimeData, devicesWithPositions, loading } = useDeviceData(); - const { nodes, links } = useEnergyFlow(); - - const [selectedDevice, setSelectedDevice] = useState(null); - const [hoveredDeviceId, setHoveredDeviceId] = useState(null); - const [viewMode, setViewMode] = useState('campus'); - const [detailRealtimeData, setDetailRealtimeData] = useState | null>(null); - const detailTimerRef = useRef | null>(null); - - // Poll per-device realtime data when in device-detail view - useEffect(() => { - if (!selectedDevice || viewMode !== 'device-detail') { - setDetailRealtimeData(null); - if (detailTimerRef.current) clearInterval(detailTimerRef.current); - return; - } - - const fetchDetail = async () => { - try { - const resp = await getDeviceRealtime(selectedDevice.id) as any; - const realtimeMap = resp?.data ?? resp; - setDetailRealtimeData(realtimeMap as Record); - } catch { - // ignore errors - } - }; - - fetchDetail(); - detailTimerRef.current = setInterval(fetchDetail, 5000); - - return () => { - if (detailTimerRef.current) clearInterval(detailTimerRef.current); - }; - }, [selectedDevice?.id, viewMode]); - - const handleDeviceSelect = useCallback((device: DeviceInfo) => { - setSelectedDevice(device); - }, []); - - const handleDeviceClose = useCallback(() => { - setSelectedDevice(null); - }, []); - - const handleEnterDetail = useCallback((device: DeviceInfo) => { - setSelectedDevice(device); - setViewMode('device-detail'); - }, []); - - const handleExitDetail = useCallback(() => { - setViewMode('campus'); - }, []); - - // Find 3D position of the selected device - const selectedDevicePosition = selectedDevice - ? (devicesWithPositions.find((d) => d.id === selectedDevice.id)?.position3D ?? null) - : null; - - if (loading && devicesWithPositions.length === 0) { - return ( -
-
-

天普零碳园区 3D智慧能源管理平台

-

正在加载设备数据...

-
-
- ); - } - - return ( -
- {/* 3D Canvas — fills entire screen */} -
- -
- - {/* HUD: header bar + bottom metrics — pointer-events: none */} - - - {/* Left device list panel (only in campus view) */} - {viewMode === 'campus' && ( - - )} - - {/* Right device info panel (when device selected in campus view) */} - {selectedDevice && viewMode === 'campus' && ( - - )} - - {/* Return button in detail view */} - {viewMode === 'device-detail' && ( - - )} -
- ); -} diff --git a/frontend/src/pages/BigScreen3D/styles.module.css b/frontend/src/pages/BigScreen3D/styles.module.css deleted file mode 100644 index b41fb15..0000000 --- a/frontend/src/pages/BigScreen3D/styles.module.css +++ /dev/null @@ -1,329 +0,0 @@ -/* BigScreen 3D - Dark monitoring theme */ -.container { - width: 100vw; - height: 100vh; - background: #0a1628; - position: relative; - overflow: hidden; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - color: #e0e8f0; -} - -.placeholder { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - color: #00d4ff; -} - -.placeholderTitle { - font-size: 2rem; - margin-bottom: 1rem; -} - -/* Canvas fills entire screen */ -.canvasWrapper { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 1; -} - -/* HUD overlay on top of canvas */ -.hudOverlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 10; - pointer-events: none; -} - -/* Header bar */ -.header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 24px; - background: linear-gradient(180deg, rgba(6, 30, 62, 0.95) 0%, transparent 100%); - pointer-events: none; -} - -.headerDate { - font-size: 14px; - color: #8899aa; - min-width: 200px; -} - -.headerTitle { - font-size: 24px; - font-weight: 700; - background: linear-gradient(90deg, #00d4ff, #00ff88, #00d4ff); - background-size: 200% auto; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - animation: shimmer 3s linear infinite; - text-align: center; -} - -@keyframes shimmer { - to { background-position: 200% center; } -} - -.headerClock { - font-size: 20px; - font-weight: 600; - color: #00d4ff; - font-variant-numeric: tabular-nums; - min-width: 200px; - text-align: right; -} - -/* Bottom metrics bar */ -.metricsBar { - position: absolute; - bottom: 0; - left: 0; - width: 100%; - display: flex; - justify-content: center; - gap: 24px; - padding: 16px 24px; - background: linear-gradient(0deg, rgba(6, 30, 62, 0.95) 0%, transparent 100%); - pointer-events: none; -} - -.metricCard { - display: flex; - flex-direction: column; - align-items: center; - padding: 8px 20px; - background: rgba(0, 212, 255, 0.08); - border: 1px solid rgba(0, 212, 255, 0.2); - border-radius: 8px; - min-width: 140px; -} - -.metricLabel { - font-size: 12px; - color: #8899aa; - margin-bottom: 4px; -} - -.metricValue { - font-size: 22px; - font-weight: 700; - color: #00d4ff; - font-variant-numeric: tabular-nums; -} - -.metricUnit { - font-size: 12px; - color: #8899aa; - margin-left: 4px; -} - -/* Left device list panel */ -.deviceListPanel { - position: absolute; - top: 60px; - left: 16px; - width: 240px; - max-height: calc(100vh - 160px); - overflow-y: auto; - background: rgba(6, 30, 62, 0.85); - border: 1px solid rgba(0, 212, 255, 0.25); - border-radius: 8px; - padding: 12px; - z-index: 20; - pointer-events: auto; -} - -.deviceListPanel::-webkit-scrollbar { - width: 4px; -} -.deviceListPanel::-webkit-scrollbar-thumb { - background: rgba(0, 212, 255, 0.3); - border-radius: 2px; -} - -.deviceGroupTitle { - font-size: 13px; - font-weight: 600; - color: #00d4ff; - padding: 8px 0 4px; - border-bottom: 1px solid rgba(0, 212, 255, 0.15); - margin-bottom: 4px; - cursor: pointer; - user-select: none; -} - -.deviceItem { - display: flex; - align-items: center; - padding: 6px 8px; - border-radius: 4px; - cursor: pointer; - transition: background 0.2s; - gap: 8px; -} - -.deviceItem:hover { - background: rgba(0, 212, 255, 0.1); -} - -.deviceItemActive { - background: rgba(0, 212, 255, 0.15); - border: 1px solid rgba(0, 212, 255, 0.3); -} - -.statusDot { - width: 8px; - height: 8px; - border-radius: 50%; - flex-shrink: 0; -} - -.deviceName { - font-size: 12px; - color: #e0e8f0; - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.deviceValue { - font-size: 11px; - color: #00d4ff; - font-variant-numeric: tabular-nums; -} - -/* Right device info panel */ -.deviceInfoPanel { - position: absolute; - top: 60px; - right: 16px; - width: 300px; - max-height: calc(100vh - 160px); - overflow-y: auto; - background: rgba(6, 30, 62, 0.9); - border: 1px solid rgba(0, 212, 255, 0.25); - border-radius: 8px; - padding: 16px; - z-index: 20; - pointer-events: auto; -} - -.infoPanelHeader { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; - padding-bottom: 8px; - border-bottom: 1px solid rgba(0, 212, 255, 0.2); -} - -.infoPanelTitle { - font-size: 16px; - font-weight: 600; - color: #e0e8f0; -} - -.closeBtn { - background: none; - border: 1px solid rgba(0, 212, 255, 0.3); - color: #8899aa; - font-size: 14px; - cursor: pointer; - padding: 2px 8px; - border-radius: 4px; - transition: all 0.2s; -} - -.closeBtn:hover { - color: #00d4ff; - border-color: #00d4ff; -} - -.paramRow { - display: flex; - justify-content: space-between; - align-items: center; - padding: 6px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); -} - -.paramLabel { - font-size: 13px; - color: #8899aa; -} - -.paramValue { - font-size: 15px; - font-weight: 600; - color: #00d4ff; - font-variant-numeric: tabular-nums; -} - -.detailBtn { - width: 100%; - margin-top: 12px; - padding: 8px; - background: rgba(0, 212, 255, 0.15); - border: 1px solid rgba(0, 212, 255, 0.4); - border-radius: 6px; - color: #00d4ff; - font-size: 14px; - cursor: pointer; - transition: all 0.2s; -} - -.detailBtn:hover { - background: rgba(0, 212, 255, 0.25); -} - -/* Return button (detail view) */ -.returnBtn { - position: absolute; - top: 70px; - left: 50%; - transform: translateX(-50%); - padding: 8px 24px; - background: rgba(6, 30, 62, 0.9); - border: 1px solid rgba(0, 212, 255, 0.4); - border-radius: 20px; - color: #00d4ff; - font-size: 14px; - cursor: pointer; - z-index: 25; - pointer-events: auto; - transition: all 0.2s; -} - -.returnBtn:hover { - background: rgba(0, 212, 255, 0.2); -} - -/* 3D label styles (used inside drei Html) */ -.label3d { - font-size: 11px; - color: #e0e8f0; - background: rgba(6, 30, 62, 0.8); - padding: 2px 8px; - border-radius: 4px; - border: 1px solid rgba(0, 212, 255, 0.2); - white-space: nowrap; - pointer-events: none; -} - -.label3dValue { - color: #00d4ff; - font-weight: 600; - margin-left: 4px; -} diff --git a/frontend/src/pages/BigScreen3D/types.ts b/frontend/src/pages/BigScreen3D/types.ts deleted file mode 100644 index 19b19bb..0000000 --- a/frontend/src/pages/BigScreen3D/types.ts +++ /dev/null @@ -1,73 +0,0 @@ -// 3D BigScreen shared type definitions - -export interface DeviceInfo { - id: number; - name: string; - code: string; - device_type: string; - device_type_id?: number; - status: 'online' | 'offline' | 'alarm' | 'maintenance'; - model?: string; - manufacturer?: string; - rated_power?: number; - location?: string; - serial_number?: string; - collect_interval?: number; -} - -export interface DeviceRealtimeEntry { - value: number; - unit: string; - timestamp: string; -} - -export type DeviceRealtimeData = Record; - -export interface EnergyFlowNode { - id: string; - name: string; - power: number; - unit: string; -} - -export interface EnergyFlowLink { - source: string; - target: string; - value: number; -} - -export interface DevicePosition3D { - deviceCode: string; - position: [number, number, number]; - rotation?: [number, number, number]; - type: string; -} - -export type ViewMode = 'campus' | 'device-detail'; - -export interface SceneState { - viewMode: ViewMode; - selectedDevice: DeviceInfo | null; - hoveredDeviceId: number | null; -} - -export interface DeviceWithPosition extends DeviceInfo { - position3D?: [number, number, number]; - rotation3D?: [number, number, number]; -} - -export interface OverviewData { - total_devices?: number; - online_devices?: number; - today_consumption?: number; - today_generation?: number; - carbon_reduction?: number; - active_alarms?: number; -} - -export interface RealtimePowerData { - pv_power?: number; - heatpump_power?: number; - total_load?: number; - grid_power?: number; -} diff --git a/frontend/src/pages/Carbon/index.tsx b/frontend/src/pages/Carbon/index.tsx deleted file mode 100644 index 6326794..0000000 --- a/frontend/src/pages/Carbon/index.tsx +++ /dev/null @@ -1,626 +0,0 @@ -import { useEffect, useState, useCallback } from 'react'; -import { - Card, Row, Col, Statistic, Table, Select, Tabs, Tag, Progress, - Button, Modal, Form, InputNumber, DatePicker, Input, message, Space, Badge, Empty, -} from 'antd'; -import { - CloudOutlined, FallOutlined, RiseOutlined, AimOutlined, SafetyCertificateOutlined, - FileTextOutlined, BarChartOutlined, ThunderboltOutlined, PlusOutlined, ReloadOutlined, -} from '@ant-design/icons'; -import ReactECharts from 'echarts-for-react'; -import dayjs from 'dayjs'; -import { - getCarbonOverview, getCarbonTrend, getEmissionFactors, - getCarbonDashboard, getCarbonTargets, createCarbonTarget, updateCarbonTarget, - getCarbonTargetProgress, getCarbonReductions, getCarbonReductionSummary, - calculateCarbonReductions, getGreenCertificates, createGreenCertificate, - updateGreenCertificate, getCertificatePortfolioValue, - getCarbonReports, generateCarbonReport, getCarbonReportDetail, - getCarbonBenchmarks, getCarbonBenchmarkComparison, -} from '../../services/api'; - -const SOURCE_LABELS: Record = { - pv_generation: '光伏发电', - heat_pump_cop: '热泵节能', - energy_saving: '节能措施', -}; - -const STATUS_COLOR: Record = { - on_track: 'green', warning: 'orange', exceeded: 'red', - active: 'green', used: 'blue', expired: 'default', traded: 'purple', -}; - -// ============================================================ -// Overview Tab -// ============================================================ -function OverviewTab() { - const [dashboard, setDashboard] = useState(null); - const [trend, setTrend] = useState([]); - const [days, setDays] = useState(30); - const [overview, setOverview] = useState(null); - - useEffect(() => { - getCarbonDashboard().then(setDashboard).catch(() => {}); - getCarbonOverview().then(setOverview).catch(() => {}); - }, []); - useEffect(() => { getCarbonTrend(days).then((d: any) => setTrend(d || [])).catch(() => {}); }, [days]); - - const kpi = dashboard?.kpi || {}; - const target = dashboard?.target_progress; - const greenRate = kpi.green_rate || 0; - - const trendOption = { - tooltip: { trigger: 'axis' }, - legend: { data: ['碳排放', '碳减排'] }, - grid: { top: 40, right: 20, bottom: 30, left: 60 }, - xAxis: { type: 'category', data: trend.map((d: any) => { const t = new Date(d.date); return `${t.getMonth() + 1}/${t.getDate()}`; }) }, - yAxis: { type: 'value', name: 'kgCO\u2082' }, - series: [ - { name: '碳排放', type: 'bar', data: trend.map((d: any) => d.emission), itemStyle: { color: '#f5222d' } }, - { name: '碳减排', type: 'bar', data: trend.map((d: any) => d.reduction), itemStyle: { color: '#52c41a' } }, - ], - }; - - const scopeOption = { - tooltip: { trigger: 'item' }, - legend: { bottom: 0 }, - series: [{ - type: 'pie', radius: ['40%', '65%'], center: ['50%', '45%'], - data: [ - { value: overview?.by_scope?.[1] || 0, name: 'Scope 1 (直接排放)', itemStyle: { color: '#f5222d' } }, - { value: overview?.by_scope?.[2] || 0, name: 'Scope 2 (间接排放)', itemStyle: { color: '#faad14' } }, - { value: overview?.by_scope?.[3] || 0, name: 'Scope 3 (其他排放)', itemStyle: { color: '#1890ff' } }, - ], - }], - }; - - const reductionSourceOption = { - tooltip: { trigger: 'item' }, - legend: { bottom: 0 }, - series: [{ - type: 'pie', radius: '60%', - data: (dashboard?.reduction_by_source || []).map((s: any) => ({ - value: s.reduction_tons, name: SOURCE_LABELS[s.source_type] || s.source_type, - })), - }], - }; - - return ( -
- -
- - } valueStyle={{ color: '#f5222d' }} /> - - - - - } valueStyle={{ color: '#52c41a' }} /> - - - - - } /> - - - - - } valueStyle={{ color: '#52c41a' }} /> - - - - - {target && ( - - `${target.actual_tons} / ${target.target_tons} tCO\u2082`} - /> - - {target.status === 'on_track' ? '达标' : target.status === 'warning' ? '预警' : '超标'} - - - )} - - - - - }> - - - - - - - - - - - - - - {(dashboard?.reduction_by_source || []).length > 0 - ? - : } - - - - - -

基于年度减排量折算 (1棵树 ≈ 0.02 tCO₂/年)

-
- - - - ); -} - -// ============================================================ -// Targets Tab -// ============================================================ -function TargetsTab() { - const [targets, setTargets] = useState([]); - const [progress, setProgress] = useState(null); - const [modalOpen, setModalOpen] = useState(false); - const [form] = Form.useForm(); - const year = new Date().getFullYear(); - - const load = useCallback(() => { - getCarbonTargets(year).then(setTargets).catch(() => {}); - getCarbonTargetProgress(year).then(setProgress).catch(() => {}); - }, [year]); - useEffect(() => { load(); }, [load]); - - const handleCreate = async () => { - try { - const vals = await form.validateFields(); - await createCarbonTarget(vals); - message.success('目标创建成功'); - setModalOpen(false); - form.resetFields(); - load(); - } catch { /* validation */ } - }; - - const cols = [ - { title: '年份', dataIndex: 'year', width: 80 }, - { title: '月份', dataIndex: 'month', width: 80, render: (v: any) => v || '年度' }, - { title: '目标排放(tCO₂)', dataIndex: 'target_emission_tons', width: 140 }, - { title: '实际排放(tCO₂)', dataIndex: 'actual_emission_tons', width: 140 }, - { title: '状态', dataIndex: 'status', width: 100, render: (v: string) => ( - {v === 'on_track' ? '达标' : v === 'warning' ? '预警' : '超标'} - )}, - ]; - - const progressGaugeOption = progress?.annual_target ? { - series: [{ - type: 'gauge', startAngle: 200, endAngle: -20, min: 0, max: 100, - pointer: { show: true }, - progress: { show: true, width: 18 }, - axisLine: { lineStyle: { width: 18 } }, - detail: { valueAnimation: true, formatter: '{value}%', fontSize: 20 }, - data: [{ value: Math.min(progress.annual_target.progress_pct, 150), name: '排放进度' }], - }], - } : null; - - return ( -
- -
- } onClick={() => setModalOpen(true)}>新建目标 - }> - {progressGaugeOption - ? - : } - - - - - {(progress?.monthly_targets || []).length > 0 ? ( - `${m.month}月`) }, - yAxis: { type: 'value', name: 'tCO₂' }, - series: [ - { name: '目标', type: 'bar', data: progress.monthly_targets.map((m: any) => m.target_tons), itemStyle: { color: '#1890ff' } }, - { name: '实际', type: 'bar', data: progress.monthly_targets.map((m: any) => m.actual_tons), itemStyle: { color: '#f5222d' } }, - ], - }} style={{ height: 280 }} /> - ) : } - - - - - -
- - - setModalOpen(false)} okText="创建"> - - - - - - - - ); -} - -// ============================================================ -// Reductions Tab -// ============================================================ -function ReductionsTab() { - const [reductions, setReductions] = useState([]); - const [summary, setSummary] = useState([]); - const [calculating, setCalculating] = useState(false); - - const load = useCallback(() => { - getCarbonReductions().then(setReductions).catch(() => {}); - getCarbonReductionSummary().then((d: any) => setSummary(d || [])).catch(() => {}); - }, []); - useEffect(() => { load(); }, [load]); - - const handleCalc = async () => { - setCalculating(true); - try { - const result: any = await calculateCarbonReductions(); - message.success(`计算完成,新增${result.records_created}条记录`); - load(); - } catch { message.error('计算失败'); } - setCalculating(false); - }; - - const cols = [ - { title: '日期', dataIndex: 'date', width: 120 }, - { title: '来源', dataIndex: 'source_type', width: 120, render: (v: string) => SOURCE_LABELS[v] || v }, - { title: '减排量(tCO₂)', dataIndex: 'reduction_tons', width: 130 }, - { title: '等效植树(棵)', dataIndex: 'equivalent_trees', width: 120 }, - { title: '方法学', dataIndex: 'methodology', ellipsis: true }, - { title: '已核证', dataIndex: 'verified', width: 80, render: (v: boolean) => v ? : }, - ]; - - const sourceChartOption = { - tooltip: { trigger: 'item' }, - legend: { bottom: 0 }, - series: [{ - type: 'pie', radius: ['35%', '60%'], - data: summary.map((s: any) => ({ - value: s.reduction_tons, name: SOURCE_LABELS[s.source_type] || s.source_type, - })), - }], - }; - - return ( -
- -
- } loading={calculating} onClick={handleCalc}>重新计算 - }> - {summary.length > 0 ? : } - - - - - - {summary.map((s: any) => ( - - -

等效植树 {s.equivalent_trees} 棵

- - ))} - - - - - - -
- - - ); -} - -// ============================================================ -// Certificates Tab -// ============================================================ -function CertificatesTab() { - const [certs, setCerts] = useState([]); - const [portfolio, setPortfolio] = useState(null); - const [modalOpen, setModalOpen] = useState(false); - const [form] = Form.useForm(); - - const load = useCallback(() => { - getGreenCertificates().then(setCerts).catch(() => {}); - getCertificatePortfolioValue().then(setPortfolio).catch(() => {}); - }, []); - useEffect(() => { load(); }, [load]); - - const handleCreate = async () => { - try { - const vals = await form.validateFields(); - if (vals.issue_date) vals.issue_date = vals.issue_date.format('YYYY-MM-DD'); - if (vals.expiry_date) vals.expiry_date = vals.expiry_date.format('YYYY-MM-DD'); - await createGreenCertificate(vals); - message.success('绿证登记成功'); - setModalOpen(false); - form.resetFields(); - load(); - } catch { /* validation */ } - }; - - const cols = [ - { title: '类型', dataIndex: 'certificate_type', width: 80, render: (v: string) => {v} }, - { title: '编号', dataIndex: 'certificate_number', width: 160, ellipsis: true }, - { title: '签发日期', dataIndex: 'issue_date', width: 110 }, - { title: '到期日期', dataIndex: 'expiry_date', width: 110, render: (v: any) => v || '-' }, - { title: '电量(MWh)', dataIndex: 'energy_mwh', width: 100 }, - { title: '价格(元)', dataIndex: 'price_yuan', width: 100 }, - { title: '状态', dataIndex: 'status', width: 80, render: (v: string) => {v} }, - ]; - - return ( -
- -
- - - - - - - - - - - - - - - - - - {Object.entries(portfolio?.by_status || {}).map(([k, v]: any) => ( - - {k} - - ))} - - - - - - } onClick={() => setModalOpen(true)}>登记绿证 - }> -
- - - setModalOpen(false)} okText="登记" width={520}> -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - ); -} - -// ============================================================ -// Reports Tab -// ============================================================ -function ReportsTab() { - const [reports, setReports] = useState([]); - const [detail, setDetail] = useState(null); - const [modalOpen, setModalOpen] = useState(false); - const [generating, setGenerating] = useState(false); - const [form] = Form.useForm(); - - const load = useCallback(() => { getCarbonReports().then(setReports).catch(() => {}); }, []); - useEffect(() => { load(); }, [load]); - - const handleGenerate = async () => { - try { - const vals = await form.validateFields(); - setGenerating(true); - const data = { - report_type: vals.report_type, - period_start: vals.period[0].format('YYYY-MM-DD'), - period_end: vals.period[1].format('YYYY-MM-DD'), - }; - await generateCarbonReport(data); - message.success('报告生成成功'); - setModalOpen(false); - form.resetFields(); - load(); - } catch { /* */ } - setGenerating(false); - }; - - const viewDetail = async (id: number) => { - const d = await getCarbonReportDetail(id); - setDetail(d); - }; - - const cols = [ - { title: '类型', dataIndex: 'report_type', width: 80, render: (v: string) => {v} }, - { title: '起始', dataIndex: 'period_start', width: 110 }, - { title: '截止', dataIndex: 'period_end', width: 110 }, - { title: '总排放(t)', dataIndex: 'total_tons', width: 100 }, - { title: '减排(t)', dataIndex: 'reduction_tons', width: 100 }, - { title: '净排放(t)', dataIndex: 'net_tons', width: 100 }, - { title: '生成时间', dataIndex: 'generated_at', width: 160, ellipsis: true }, - { title: '操作', key: 'action', width: 80, render: (_: any, r: any) => ( - - )}, - ]; - - return ( -
- } onClick={() => setModalOpen(true)}>生成报告 - }> -
- - - {detail && ( - setDetail(null)}>关闭}> - - - - - - - {detail.report_data?.monthly_breakdown && ( - m.month) }, - yAxis: { type: 'value', name: 'tCO₂' }, - series: [ - { name: '排放', type: 'bar', data: detail.report_data.monthly_breakdown.map((m: any) => m.emission_tons), itemStyle: { color: '#f5222d' } }, - { name: '减排', type: 'bar', data: detail.report_data.monthly_breakdown.map((m: any) => m.reduction_tons), itemStyle: { color: '#52c41a' } }, - ], - }} /> - )} - - )} - - setModalOpen(false)} - okText="生成" confirmLoading={generating}> -
- -
- - - - - ); -} - -// ============================================================ -// Main Page -// ============================================================ -export default function Carbon() { - const items = [ - { key: 'overview', label: '总览', icon: , children: }, - { key: 'targets', label: '目标管理', icon: , children: }, - { key: 'reductions', label: '减排追踪', icon: , children: }, - { key: 'certificates', label: '绿证管理', icon: , children: }, - { key: 'reports', label: '碳报告', icon: , children: }, - { key: 'benchmarks', label: '行业对标', icon: , children: }, - ]; - - return ( -
- ({ ...t, label: {t.icon} {t.label} }))} /> -
- ); -} diff --git a/frontend/src/pages/Charging/Dashboard.tsx b/frontend/src/pages/Charging/Dashboard.tsx deleted file mode 100644 index 5bd2b49..0000000 --- a/frontend/src/pages/Charging/Dashboard.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Card, Row, Col, Statistic, message } from 'antd'; -import { DollarOutlined, ThunderboltOutlined, CarOutlined, DashboardOutlined } from '@ant-design/icons'; -import { getChargingDashboard } from '../../services/api'; -import ReactECharts from 'echarts-for-react'; - -export default function ChargingDashboard() { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - loadDashboard(); - }, []); - - const loadDashboard = async () => { - setLoading(true); - try { - const res = await getChargingDashboard(); - setData(res); - } catch { - message.error('加载充电总览失败'); - } finally { - setLoading(false); - } - }; - - const pileStatusData = data ? [ - { type: '空闲', value: data.pile_status?.idle || 0 }, - { type: '充电中', value: data.pile_status?.charging || 0 }, - { type: '故障', value: data.pile_status?.fault || 0 }, - { type: '离线', value: data.pile_status?.offline || 0 }, - ].filter(d => d.value > 0) : []; - - const pileStatusColors: Record = { - '空闲': '#52c41a', - '充电中': '#1890ff', - '故障': '#ff4d4f', - '离线': '#d9d9d9', - }; - - const revenueLineOption = { - tooltip: { trigger: 'axis' as const }, - xAxis: { - type: 'category' as const, - data: (data?.revenue_trend || []).map((d: any) => d.date), - axisLabel: { rotate: 45 }, - }, - yAxis: { type: 'value' as const, name: '营收 (元)' }, - series: [{ - type: 'line', - data: (data?.revenue_trend || []).map((d: any) => d.revenue), - smooth: true, - symbolSize: 6, - }], - grid: { left: 60, right: 20, bottom: 60, top: 30 }, - }; - - const pieOption = { - tooltip: { trigger: 'item' as const }, - series: [{ - type: 'pie', - radius: ['40%', '70%'], - data: pileStatusData.map(d => ({ - name: d.type, - value: d.value, - itemStyle: { color: pileStatusColors[d.type] || '#d9d9d9' }, - })), - label: { formatter: '{b} {c}' }, - }], - }; - - const barOption = { - tooltip: { trigger: 'axis' as const }, - xAxis: { type: 'value' as const, name: '营收 (元)' }, - yAxis: { - type: 'category' as const, - data: (data?.station_ranking || []).map((d: any) => d.station), - }, - series: [{ - type: 'bar', - data: (data?.station_ranking || []).map((d: any) => d.revenue), - label: { show: true, position: 'right' }, - }], - grid: { left: 120, right: 40, bottom: 20, top: 20 }, - }; - - return ( -
- -
- - } - precision={2} - valueStyle={{ color: '#cf1322' }} - /> - - - - - } - precision={1} - valueStyle={{ color: '#1890ff' }} - /> - - - - - } - valueStyle={{ color: '#52c41a' }} - /> - - - - - } - suffix="%" - valueStyle={{ color: '#faad14' }} - /> - - - - - - - - {data?.revenue_trend?.length > 0 ? ( - - ) : ( -
暂无数据
- )} -
- - - - {pileStatusData.length > 0 ? ( - - ) : ( -
暂无数据
- )} -
- - - - - - - {data?.station_ranking?.length > 0 ? ( - - ) : ( -
暂无数据
- )} -
- - - - ); -} diff --git a/frontend/src/pages/Charging/Orders.tsx b/frontend/src/pages/Charging/Orders.tsx deleted file mode 100644 index b3492db..0000000 --- a/frontend/src/pages/Charging/Orders.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { useEffect, useState, useCallback } from 'react'; -import { Card, Table, Tag, Button, Tabs, Space, DatePicker, Select, message } from 'antd'; -import { SyncOutlined, WarningOutlined } from '@ant-design/icons'; -import { getChargingOrders, getChargingRealtimeOrders, getChargingAbnormalOrders, settleChargingOrder } from '../../services/api'; - -const { RangePicker } = DatePicker; - -const orderStatusMap: Record = { - charging: { color: 'processing', text: '充电中' }, - pending_pay: { color: 'warning', text: '待支付' }, - completed: { color: 'success', text: '已完成' }, - failed: { color: 'error', text: '失败' }, - refunded: { color: 'default', text: '已退款' }, -}; - -const formatDuration = (seconds: number | null) => { - if (!seconds) return '-'; - const h = Math.floor(seconds / 3600); - const m = Math.floor((seconds % 3600) / 60); - return h > 0 ? `${h}时${m}分` : `${m}分`; -}; - -export default function Orders() { - return ( - }, - { key: 'history', label: '历史订单', children: }, - { key: 'abnormal', label: '异常订单', children: }, - ]} - /> - ); -} - -function RealtimeOrders() { - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - - const loadData = async () => { - setLoading(true); - try { - const res = await getChargingRealtimeOrders(); - setData(res as any[]); - } catch { message.error('加载实时充电失败'); } - finally { setLoading(false); } - }; - - useEffect(() => { loadData(); }, []); - - const columns = [ - { title: '订单号', dataIndex: 'order_no', width: 160 }, - { title: '充电站', dataIndex: 'station_name', width: 150, ellipsis: true }, - { title: '充电桩', dataIndex: 'pile_name', width: 120 }, - { title: '用户', dataIndex: 'user_name', width: 100 }, - { title: '车牌', dataIndex: 'car_no', width: 100 }, - { title: '开始时间', dataIndex: 'start_time', width: 170 }, - { title: '时长', dataIndex: 'charge_duration', width: 80, render: formatDuration }, - { title: '充电量(kWh)', dataIndex: 'energy', width: 110, render: (v: number) => v != null ? v.toFixed(2) : '-' }, - { title: '起始SOC', dataIndex: 'start_soc', width: 90, render: (v: number) => v != null ? `${v}%` : '-' }, - { title: '当前SOC', dataIndex: 'end_soc', width: 90, render: (v: number) => v != null ? `${v}%` : '-' }, - { title: '状态', dataIndex: 'order_status', width: 90, render: () => ( - } color="processing">充电中 - )}, - ]; - - return ( - 刷新}> -
- - ); -} - -function HistoryOrders() { - const [data, setData] = useState({ total: 0, items: [] }); - const [loading, setLoading] = useState(true); - const [filters, setFilters] = useState>({ page: 1, page_size: 20 }); - - const loadData = useCallback(async () => { - setLoading(true); - try { - const cleanQuery: Record = {}; - Object.entries(filters).forEach(([k, v]) => { - if (v !== undefined && v !== null && v !== '') cleanQuery[k] = v; - }); - const res = await getChargingOrders(cleanQuery); - setData(res as any); - } catch { message.error('加载订单失败'); } - finally { setLoading(false); } - }, [filters]); - - useEffect(() => { loadData(); }, [filters, loadData]); - - const handleDateChange = (_: any, dates: [string, string]) => { - setFilters(prev => ({ - ...prev, - start_date: dates[0] || undefined, - end_date: dates[1] || undefined, - page: 1, - })); - }; - - const columns = [ - { title: '订单号', dataIndex: 'order_no', width: 160 }, - { title: '充电站', dataIndex: 'station_name', width: 150, ellipsis: true }, - { title: '充电桩', dataIndex: 'pile_name', width: 120 }, - { title: '用户', dataIndex: 'user_name', width: 100 }, - { title: '车牌', dataIndex: 'car_no', width: 100 }, - { title: '充电量(kWh)', dataIndex: 'energy', width: 110, render: (v: number) => v != null ? v.toFixed(2) : '-' }, - { title: '时长', dataIndex: 'charge_duration', width: 80, render: formatDuration }, - { title: '电费(元)', dataIndex: 'elec_amt', width: 90, render: (v: number) => v != null ? v.toFixed(2) : '-' }, - { title: '服务费(元)', dataIndex: 'serve_amt', width: 100, render: (v: number) => v != null ? v.toFixed(2) : '-' }, - { title: '实付(元)', dataIndex: 'paid_price', width: 90, render: (v: number) => v != null ? v.toFixed(2) : '-' }, - { title: '状态', dataIndex: 'order_status', width: 90, render: (s: string) => { - const st = orderStatusMap[s] || { color: 'default', text: s || '-' }; - return {st.text}; - }}, - { title: '创建时间', dataIndex: 'created_at', width: 170 }, - ]; - - return ( - - -
`共 ${total} 条订单`, - onChange: (page, pageSize) => setFilters(prev => ({ ...prev, page, page_size: pageSize })), - }} - /> - - ); -} - -function AbnormalOrders() { - const [data, setData] = useState({ total: 0, items: [] }); - const [loading, setLoading] = useState(true); - const [filters, setFilters] = useState>({ page: 1, page_size: 20 }); - - const loadData = useCallback(async () => { - setLoading(true); - try { - const res = await getChargingAbnormalOrders(filters); - setData(res as any); - } catch { message.error('加载异常订单失败'); } - finally { setLoading(false); } - }, [filters]); - - useEffect(() => { loadData(); }, [filters, loadData]); - - const handleSettle = async (id: number) => { - try { - await settleChargingOrder(id); - message.success('手动结算成功'); - loadData(); - } catch { message.error('结算失败'); } - }; - - const columns = [ - { title: '订单号', dataIndex: 'order_no', width: 160 }, - { title: '充电站', dataIndex: 'station_name', width: 150, ellipsis: true }, - { title: '充电桩', dataIndex: 'pile_name', width: 120 }, - { title: '用户', dataIndex: 'user_name', width: 100 }, - { title: '充电量(kWh)', dataIndex: 'energy', width: 110, render: (v: number) => v != null ? v.toFixed(2) : '-' }, - { title: '状态', dataIndex: 'order_status', width: 90, render: (s: string) => { - const st = orderStatusMap[s] || { color: 'default', text: s || '-' }; - return } color={st.color}>{st.text}; - }}, - { title: '异常原因', dataIndex: 'abno_cause', width: 200, ellipsis: true }, - { title: '创建时间', dataIndex: 'created_at', width: 170 }, - { title: '操作', key: 'action', width: 100, render: (_: any, record: any) => ( - record.order_status === 'failed' && ( - - ) - )}, - ]; - - return ( - -
`共 ${total} 条异常订单`, - onChange: (page, pageSize) => setFilters(prev => ({ ...prev, page, page_size: pageSize })), - }} - /> - - ); -} diff --git a/frontend/src/pages/Charging/Piles.tsx b/frontend/src/pages/Charging/Piles.tsx deleted file mode 100644 index d558782..0000000 --- a/frontend/src/pages/Charging/Piles.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { useEffect, useState, useCallback } from 'react'; -import { Card, Table, Tag, Button, Modal, Form, Input, Select, InputNumber, Space, message } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; -import { getChargingPiles, createChargingPile, updateChargingPile, deleteChargingPile, getChargingStations, getChargingBrands } from '../../services/api'; - -const workStatusMap: Record = { - idle: { color: 'green', text: '空闲' }, - charging: { color: 'blue', text: '充电中' }, - fault: { color: 'red', text: '故障' }, - offline: { color: 'default', text: '离线' }, -}; - -const typeOptions = [ - { label: '交流慢充', value: 'AC_slow' }, - { label: '直流快充', value: 'DC_fast' }, - { label: '直流超充', value: 'DC_superfast' }, -]; - -const connectorOptions = [ - { label: 'GB/T', value: 'GB_T' }, - { label: 'CCS', value: 'CCS' }, - { label: 'CHAdeMO', value: 'CHAdeMO' }, -]; - -export default function Piles() { - const [data, setData] = useState({ total: 0, items: [] }); - const [stations, setStations] = useState([]); - const [brands, setBrands] = useState([]); - const [loading, setLoading] = useState(true); - const [showModal, setShowModal] = useState(false); - const [editing, setEditing] = useState(null); - const [form] = Form.useForm(); - const [filters, setFilters] = useState>({ page: 1, page_size: 20 }); - - const loadData = useCallback(async () => { - setLoading(true); - try { - const cleanQuery: Record = {}; - Object.entries(filters).forEach(([k, v]) => { - if (v !== undefined && v !== null && v !== '') cleanQuery[k] = v; - }); - const res = await getChargingPiles(cleanQuery); - setData(res as any); - } catch { message.error('加载充电桩失败'); } - finally { setLoading(false); } - }, [filters]); - - const loadMeta = async () => { - try { - const [st, br] = await Promise.all([ - getChargingStations({ page_size: 100 }), - getChargingBrands(), - ]); - setStations((st as any).items || []); - setBrands(br as any[]); - } catch {} - }; - - useEffect(() => { loadMeta(); }, []); - useEffect(() => { loadData(); }, [filters, loadData]); - - const handleFilterChange = (key: string, value: any) => { - setFilters(prev => ({ ...prev, [key]: value, page: 1 })); - }; - - const openAddModal = () => { - setEditing(null); - form.resetFields(); - form.setFieldsValue({ status: 'active', work_status: 'offline' }); - setShowModal(true); - }; - - const openEditModal = (record: any) => { - setEditing(record); - form.setFieldsValue(record); - setShowModal(true); - }; - - const handleSubmit = async (values: any) => { - try { - if (editing) { - await updateChargingPile(editing.id, values); - message.success('充电桩更新成功'); - } else { - await createChargingPile(values); - message.success('充电桩创建成功'); - } - setShowModal(false); - form.resetFields(); - loadData(); - } catch (e: any) { - message.error(e?.detail || '操作失败'); - } - }; - - const handleDelete = async (id: number) => { - try { - await deleteChargingPile(id); - message.success('已停用'); - loadData(); - } catch { message.error('操作失败'); } - }; - - const columns = [ - { title: '终端编码', dataIndex: 'encoding', width: 140 }, - { title: '名称', dataIndex: 'name', width: 150, ellipsis: true }, - { title: '所属充电站', dataIndex: 'station_id', width: 150, render: (id: number) => { - const s = stations.find((st: any) => st.id === id); - return s ? s.name : id; - }}, - { title: '类型', dataIndex: 'type', width: 100, render: (v: string) => typeOptions.find(o => o.value === v)?.label || v || '-' }, - { title: '额定功率(kW)', dataIndex: 'rated_power_kw', width: 120, render: (v: number) => v != null ? v : '-' }, - { title: '品牌', dataIndex: 'brand', width: 100 }, - { title: '型号', dataIndex: 'model', width: 100 }, - { title: '接口类型', dataIndex: 'connector_type', width: 100 }, - { title: '工作状态', dataIndex: 'work_status', width: 100, render: (s: string) => { - const st = workStatusMap[s] || { color: 'default', text: s || '-' }; - return {st.text}; - }}, - { title: '状态', dataIndex: 'status', width: 80, render: (s: string) => ( - {s === 'active' ? '启用' : '停用'} - )}, - { title: '操作', key: 'action', width: 150, fixed: 'right' as const, render: (_: any, record: any) => ( - - - - - )}, - ]; - - return ( - } onClick={openAddModal}>添加充电桩 - }> - - handleFilterChange('type', v)} /> -
`共 ${total} 个充电桩`, - onChange: (page, pageSize) => setFilters(prev => ({ ...prev, page, page_size: pageSize })), - }} - /> - - { setShowModal(false); form.resetFields(); }} - onOk={() => form.submit()} - okText={editing ? '保存' : '创建'} - cancelText="取消" - width={640} - destroyOnClose - > - - - - - - - - - ({ label: b.brand_name, value: b.brand_name }))} /> - - - - - - - - -
; - }; - - return ( - } onClick={openAddModal}>新建策略 - }> -
- - { setShowModal(false); form.resetFields(); }} - onOk={() => form.submit()} - okText={editing ? '保存' : '创建'} - cancelText="取消" - width={800} - destroyOnClose - > - - - - - - - - - - - - - - - - - - handleFilterChange('type', v)} /> -
`共 ${total} 个充电站`, - onChange: (page, pageSize) => setFilters(prev => ({ ...prev, page, page_size: pageSize })), - }} - /> - - { setShowModal(false); form.resetFields(); }} - onOk={() => form.submit()} - okText={editing ? '保存' : '创建'} - cancelText="取消" - width={640} - destroyOnClose - > - - - - - - ({ label: m.name, value: m.id }))} /> - - - - - - - - - - - - - - - - - - - - - - - - - {Object.keys(chartData).length > 0 && ( - <> - - - - - -
`共 ${total} 条` }} - /> - - - )} - - - ); -} diff --git a/frontend/src/pages/DeviceDetail/index.tsx b/frontend/src/pages/DeviceDetail/index.tsx deleted file mode 100644 index 83cc59e..0000000 --- a/frontend/src/pages/DeviceDetail/index.tsx +++ /dev/null @@ -1,490 +0,0 @@ -import { useEffect, useState, useCallback } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import { - Card, Tabs, Tag, Button, Statistic, Row, Col, Table, Descriptions, Select, - DatePicker, Space, Badge, Spin, message, Empty, -} from 'antd'; -import { - ArrowLeftOutlined, ReloadOutlined, ThunderboltOutlined, - DashboardOutlined, FireOutlined, ExperimentOutlined, -} from '@ant-design/icons'; -import ReactECharts from 'echarts-for-react'; -import dayjs from 'dayjs'; -import { getDevice, getDeviceRealtime, getEnergyHistory, getAlarmEvents } from '../../services/api'; -import { getDevicePhoto } from '../../utils/devicePhoto'; - -const { RangePicker } = DatePicker; - -const statusMap: Record = { - online: { color: 'green', text: '在线' }, - offline: { color: 'default', text: '离线' }, - alarm: { color: 'red', text: '告警' }, - maintenance: { color: 'orange', text: '维护' }, -}; - -const severityMap: Record = { - critical: { color: 'red', text: '严重' }, - warning: { color: 'orange', text: '警告' }, - info: { color: 'blue', text: '信息' }, -}; - -const alarmStatusMap: Record = { - active: { color: 'red', text: '活跃' }, - acknowledged: { color: 'orange', text: '已确认' }, - resolved: { color: 'green', text: '已解决' }, -}; - -const protocolLabels: Record = { - modbus_tcp: 'Modbus TCP', - modbus_rtu: 'Modbus RTU', - opc_ua: 'OPC UA', - mqtt: 'MQTT', - http_api: 'HTTP API', - dlt645: 'DL/T 645', - image: '图像采集', -}; - -const dataTypeOptions = [ - { label: '功率 (kW)', value: 'power' }, - { label: '电量 (kWh)', value: 'energy' }, - { label: '温度 (°C)', value: 'temperature' }, - { label: 'COP', value: 'cop' }, - { label: '电流 (A)', value: 'current' }, - { label: '电压 (V)', value: 'voltage' }, - { label: '频率 (Hz)', value: 'frequency' }, - { label: '功率因数', value: 'power_factor' }, - { label: '流量 (m³/h)', value: 'flow_rate' }, - { label: '湿度 (%)', value: 'humidity' }, -]; - -const granularityOptions = [ - { label: '原始数据', value: 'raw' }, - { label: '5分钟', value: '5min' }, - { label: '小时', value: 'hour' }, - { label: '天', value: 'day' }, -]; - -const timeRangePresets = [ - { label: '24小时', value: '24h' }, - { label: '7天', value: '7d' }, - { label: '30天', value: '30d' }, -]; - -function getTimeRange(preset: string): [dayjs.Dayjs, dayjs.Dayjs] { - const now = dayjs(); - switch (preset) { - case '24h': return [now.subtract(24, 'hour'), now]; - case '7d': return [now.subtract(7, 'day'), now]; - case '30d': return [now.subtract(30, 'day'), now]; - default: return [now.subtract(24, 'hour'), now]; - } -} - -export default function DeviceDetail() { - const { id } = useParams<{ id: string }>(); - const navigate = useNavigate(); - const deviceId = Number(id); - - const [device, setDevice] = useState(null); - const [loading, setLoading] = useState(true); - const [activeTab, setActiveTab] = useState('realtime'); - - // Realtime state - const [realtimeData, setRealtimeData] = useState(null); - const [realtimeLoading, setRealtimeLoading] = useState(false); - - // History state - const [historyData, setHistoryData] = useState([]); - const [historyLoading, setHistoryLoading] = useState(false); - const [dataType, setDataType] = useState('power'); - const [granularity, setGranularity] = useState('hour'); - const [timePreset, setTimePreset] = useState('24h'); - const [timeRange, setTimeRange] = useState<[dayjs.Dayjs, dayjs.Dayjs]>(getTimeRange('24h')); - - // Alarm state - const [alarmData, setAlarmData] = useState({ total: 0, items: [] }); - const [alarmLoading, setAlarmLoading] = useState(false); - const [alarmPage, setAlarmPage] = useState(1); - - // Load device info - useEffect(() => { - if (!deviceId) return; - setLoading(true); - getDevice(deviceId) - .then((res: any) => setDevice(res)) - .catch(() => message.error('加载设备信息失败')) - .finally(() => setLoading(false)); - }, [deviceId]); - - // Load realtime data - const loadRealtime = useCallback(async () => { - if (!deviceId) return; - setRealtimeLoading(true); - try { - const res = await getDeviceRealtime(deviceId); - setRealtimeData(res); - } catch { setRealtimeData(null); } - finally { setRealtimeLoading(false); } - }, [deviceId]); - - // Auto-refresh realtime every 15s - useEffect(() => { - if (activeTab !== 'realtime') return; - loadRealtime(); - const timer = setInterval(loadRealtime, 15000); - return () => clearInterval(timer); - }, [activeTab, loadRealtime]); - - // Load history data - const loadHistory = useCallback(async () => { - if (!deviceId) return; - setHistoryLoading(true); - try { - const res = await getEnergyHistory({ - device_id: deviceId, - data_type: dataType, - granularity, - start_time: timeRange[0].format('YYYY-MM-DD HH:mm:ss'), - end_time: timeRange[1].format('YYYY-MM-DD HH:mm:ss'), - page_size: 1000, - }); - setHistoryData(res as any[]); - } catch { setHistoryData([]); } - finally { setHistoryLoading(false); } - }, [deviceId, dataType, granularity, timeRange]); - - useEffect(() => { - if (activeTab === 'history') loadHistory(); - }, [activeTab, loadHistory]); - - // Load alarm events - const loadAlarms = useCallback(async () => { - if (!deviceId) return; - setAlarmLoading(true); - try { - const res = await getAlarmEvents({ device_id: deviceId, page: alarmPage, page_size: 20 }); - setAlarmData(res as any); - } catch { setAlarmData({ total: 0, items: [] }); } - finally { setAlarmLoading(false); } - }, [deviceId, alarmPage]); - - useEffect(() => { - if (activeTab === 'alarms') loadAlarms(); - }, [activeTab, loadAlarms]); - - const handleTimePreset = (preset: string) => { - setTimePreset(preset); - setTimeRange(getTimeRange(preset)); - }; - - const handleRangeChange = (dates: any) => { - if (dates && dates[0] && dates[1]) { - setTimePreset(''); - setTimeRange([dates[0], dates[1]]); - } - }; - - // ---- Chart options ---- - const getChartOption = () => { - const isRaw = granularity === 'raw'; - const times = historyData.map(d => isRaw ? d.timestamp : d.time); - const typeLabel = dataTypeOptions.find(o => o.value === dataType)?.label || dataType; - - if (isRaw) { - return { - tooltip: { trigger: 'axis' }, - legend: { data: [typeLabel] }, - grid: { left: 60, right: 30, top: 40, bottom: 40 }, - xAxis: { type: 'category', data: times, axisLabel: { rotate: 30 } }, - yAxis: { type: 'value', name: typeLabel }, - series: [{ - name: typeLabel, - type: 'line', - data: historyData.map(d => d.value), - smooth: true, - lineStyle: { width: 2 }, - areaStyle: { opacity: 0.1 }, - }], - }; - } - - // Aggregated data with avg/max/min - const avgData = historyData.map(d => d.avg); - const maxData = historyData.map(d => d.max); - const minData = historyData.map(d => d.min); - const avgVal = avgData.length ? (avgData.reduce((a, b) => a + b, 0) / avgData.length).toFixed(2) : '-'; - const maxVal = maxData.length ? Math.max(...maxData).toFixed(2) : '-'; - const minVal = minData.length ? Math.min(...minData).toFixed(2) : '-'; - - return { - tooltip: { trigger: 'axis' }, - legend: { - data: [ - `平均 (${avgVal})`, - `最大 (${maxVal})`, - `最小 (${minVal})`, - ], - }, - grid: { left: 60, right: 30, top: 50, bottom: 40 }, - xAxis: { type: 'category', data: times, axisLabel: { rotate: 30 } }, - yAxis: { type: 'value', name: typeLabel }, - series: [ - { - name: `平均 (${avgVal})`, - type: 'line', - data: avgData, - smooth: true, - lineStyle: { width: 2, color: '#1890ff' }, - itemStyle: { color: '#1890ff' }, - areaStyle: { opacity: 0.08, color: '#1890ff' }, - }, - { - name: `最大 (${maxVal})`, - type: 'line', - data: maxData, - smooth: true, - lineStyle: { width: 1, type: 'dashed', color: '#ff4d4f' }, - itemStyle: { color: '#ff4d4f' }, - }, - { - name: `最小 (${minVal})`, - type: 'line', - data: minData, - smooth: true, - lineStyle: { width: 1, type: 'dashed', color: '#52c41a' }, - itemStyle: { color: '#52c41a' }, - }, - ], - }; - }; - - // ---- Alarm columns ---- - const alarmColumns = [ - { - title: '时间', dataIndex: 'triggered_at', width: 170, - render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-', - }, - { title: '标题', dataIndex: 'title', width: 200, ellipsis: true }, - { - title: '严重程度', dataIndex: 'severity', width: 90, - render: (v: string) => { - const s = severityMap[v] || { color: 'default', text: v }; - return {s.text}; - }, - }, - { - title: '状态', dataIndex: 'status', width: 90, - render: (v: string) => { - const s = alarmStatusMap[v] || { color: 'default', text: v }; - return {s.text}; - }, - }, - { - title: '实际值', dataIndex: 'value', width: 100, - render: (v: number) => v != null ? v.toFixed(2) : '-', - }, - { - title: '阈值', dataIndex: 'threshold', width: 100, - render: (v: number) => v != null ? v.toFixed(2) : '-', - }, - { - title: '解决时间', dataIndex: 'resolved_at', width: 170, - render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm:ss') : '-', - }, - ]; - - // ---- Render metric cards for realtime ---- - const renderRealtimeMetrics = () => { - if (!realtimeData?.data) return ; - const entries = Object.entries(realtimeData.data) as [string, any][]; - return ( - - {entries.map(([key, val]) => { - let icon = ; - let color: string | undefined; - if (key.includes('power') || key.includes('功率')) { icon = ; color = '#1890ff'; } - else if (key.includes('temp') || key.includes('温度')) { icon = ; color = '#fa541c'; } - else if (key.includes('cop') || key.includes('COP')) { icon = ; color = '#52c41a'; } - return ( - - - - - - ); - })} - - ); - }; - - if (loading) { - return
; - } - - if (!device) { - return - - ; - } - - const st = statusMap[device.status] || { color: 'default', text: device.status || '-' }; - - return ( -
- {/* Header */} - -
- {device.name} -
-
-

{device.name}

- {st.text} - {device.code} -
- -
型号:{device.model || '-'} - 厂商:{device.manufacturer || '-'} - 额定功率:{device.rated_power != null ? `${device.rated_power} kW` : '-'} - 位置:{device.location || '-'} - 协议:{protocolLabels[device.protocol] || device.protocol || '-'} - 采集间隔:{device.collect_interval ? `${device.collect_interval}s` : '-'} - 最近数据:{device.last_data_time || '-'} - - - - - - - {/* Tabs */} - - -
- - 每15秒自动刷新 -
- {renderRealtimeMetrics()} - - ), - }, - { - key: 'history', - label: '历史趋势', - children: ( -
- - - {timeRangePresets.map(p => ( - - ))} - - - - - {historyData.length > 0 ? ( - - ) : ( - - )} - -
- ), - }, - { - key: 'alarms', - label: '告警记录', - children: ( -
`共 ${total} 条告警`, - onChange: (page: number) => setAlarmPage(page), - }} - /> - ), - }, - { - key: 'info', - label: '设备信息', - children: ( - - {device.name} - {device.code} - {device.device_type || '-'} - {device.group_id || '-'} - {device.model || '-'} - {device.manufacturer || '-'} - {device.serial_number || '-'} - {device.rated_power != null ? `${device.rated_power} kW` : '-'} - {device.location || '-'} - {protocolLabels[device.protocol] || device.protocol || '-'} - {device.collect_interval ? `${device.collect_interval} 秒` : '-'} - - - - - {device.is_active ? '启用' : '停用'} - - {device.last_data_time || '-'} - - {device.connection_params ? ( -
-                      {JSON.stringify(device.connection_params, null, 2)}
-                    
- ) : '-'} -
-
- ), - }, - ]} /> - - - ); -} diff --git a/frontend/src/pages/Devices/Topology.tsx b/frontend/src/pages/Devices/Topology.tsx deleted file mode 100644 index 4d2fa55..0000000 --- a/frontend/src/pages/Devices/Topology.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Card, Tree, Table, Tag, Badge, Button, Space, Row, Col, Empty } from 'antd'; -import { ApartmentOutlined, ExpandOutlined, CompressOutlined } from '@ant-design/icons'; -import { getDeviceTopology, getDevices } from '../../services/api'; -import type { DataNode } from 'antd/es/tree'; - -const statusMap: Record = { - online: { color: 'green', text: '在线' }, - offline: { color: 'default', text: '离线' }, - alarm: { color: 'red', text: '告警' }, - maintenance: { color: 'orange', text: '维护' }, -}; - -function getStatusDot(node: any): string { - if (node.total_alarm > 0) return '#f5222d'; - if (node.total_offline > 0 && node.total_online > 0) return '#faad14'; - if (node.total_online > 0) return '#52c41a'; - if (node.total_device_count === 0) return '#d9d9d9'; - return '#999'; -} - -function buildTreeNodes(nodes: any[]): DataNode[] { - return nodes.map((node: any) => { - const dotColor = getStatusDot(node); - const title = ( - - - {node.name} - {node.location ? ({node.location}) : null} - - - ); - return { - title, - key: `group-${node.id}`, - children: buildTreeNodes(node.children || []), - isLeaf: !node.children || node.children.length === 0, - }; - }); -} - -export default function Topology() { - const [treeData, setTreeData] = useState([]); - const [topologyData, setTopologyData] = useState([]); - const [selectedGroupId, setSelectedGroupId] = useState(null); - const [selectedGroupName, setSelectedGroupName] = useState(''); - const [devices, setDevices] = useState([]); - const [loading, setLoading] = useState(false); - const [expandedKeys, setExpandedKeys] = useState([]); - - useEffect(() => { - loadTopology(); - }, []); - - const loadTopology = async () => { - try { - const data = await getDeviceTopology() as any[]; - setTopologyData(data); - setTreeData(buildTreeNodes(data)); - // Expand all by default - const allKeys = collectKeys(data); - setExpandedKeys(allKeys); - } catch (e) { - console.error(e); - } - }; - - const collectKeys = (nodes: any[]): string[] => { - const keys: string[] = []; - nodes.forEach(n => { - keys.push(`group-${n.id}`); - if (n.children) { - keys.push(...collectKeys(n.children)); - } - }); - return keys; - }; - - const findGroupName = (nodes: any[], id: number): string => { - for (const n of nodes) { - if (n.id === id) return n.name; - if (n.children) { - const found = findGroupName(n.children, id); - if (found) return found; - } - } - return ''; - }; - - const handleSelect = async (selectedKeys: React.Key[]) => { - if (selectedKeys.length === 0) return; - const key = selectedKeys[0] as string; - const groupId = parseInt(key.replace('group-', '')); - setSelectedGroupId(groupId); - setSelectedGroupName(findGroupName(topologyData, groupId)); - setLoading(true); - try { - const res = await getDevices({ group_id: groupId, page_size: 100 }) as any; - setDevices(res.items || []); - } catch (e) { - console.error(e); - } finally { - setLoading(false); - } - }; - - const handleExpandAll = () => { - setExpandedKeys(collectKeys(topologyData)); - }; - - const handleCollapseAll = () => { - setExpandedKeys([]); - }; - - const columns = [ - { title: '设备名称', dataIndex: 'name', width: 160 }, - { title: '设备编号', dataIndex: 'code', width: 130 }, - { title: '类型', dataIndex: 'device_type', width: 100 }, - { - title: '状态', dataIndex: 'status', width: 80, - render: (s: string) => { - const st = statusMap[s] || { color: 'default', text: s || '-' }; - return {st.text}; - }, - }, - { title: '位置', dataIndex: 'location', width: 120, ellipsis: true }, - { title: '额定功率(kW)', dataIndex: 'rated_power', width: 110, render: (v: number) => v != null ? v : '-' }, - { title: '最近数据时间', dataIndex: 'last_data_time', width: 170 }, - ]; - - return ( - - - 设备拓扑} - size="small" - extra={ - - - - - } - bodyStyle={{ maxHeight: 'calc(100vh - 200px)', overflow: 'auto' }} - > - setExpandedKeys(keys)} - onSelect={handleSelect} - showLine - blockNode - /> - - - - - - {selectedGroupId ? ( -
`共 ${total} 台` }} - /> - ) : ( - - )} - - - - ); -} diff --git a/frontend/src/pages/Devices/index.tsx b/frontend/src/pages/Devices/index.tsx deleted file mode 100644 index ecdacd0..0000000 --- a/frontend/src/pages/Devices/index.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import { useEffect, useState, useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Card, Table, Tag, Button, Modal, Form, Input, Select, InputNumber, Space, Row, Col, Statistic, Switch, message } from 'antd'; -import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, CheckCircleOutlined, CloseCircleOutlined, WarningOutlined, AppstoreOutlined } from '@ant-design/icons'; -import { getDevices, getDeviceTypes, getDeviceGroups, getDeviceStats, createDevice, updateDevice } from '../../services/api'; -import { getDevicePhoto } from '../../utils/devicePhoto'; - -const statusMap: Record = { - online: { color: 'green', text: '在线' }, - offline: { color: 'default', text: '离线' }, - alarm: { color: 'red', text: '告警' }, - maintenance: { color: 'orange', text: '维护' }, -}; - -const protocolOptions = [ - { label: 'Modbus TCP', value: 'modbus_tcp' }, - { label: 'Modbus RTU', value: 'modbus_rtu' }, - { label: 'OPC UA', value: 'opc_ua' }, - { label: 'MQTT', value: 'mqtt' }, - { label: 'HTTP API', value: 'http_api' }, - { label: 'DL/T 645', value: 'dlt645' }, - { label: '图像采集', value: 'image' }, -]; - -export default function Devices() { - const navigate = useNavigate(); - const [data, setData] = useState({ total: 0, items: [] }); - const [stats, setStats] = useState({ online: 0, offline: 0, alarm: 0, total: 0 }); - const [deviceTypes, setDeviceTypes] = useState([]); - const [deviceGroups, setDeviceGroups] = useState([]); - const [loading, setLoading] = useState(true); - const [showModal, setShowModal] = useState(false); - const [editingDevice, setEditingDevice] = useState(null); - const [form] = Form.useForm(); - const [filters, setFilters] = useState>({ page: 1, page_size: 20 }); - - const loadDevices = useCallback(async (params?: Record) => { - setLoading(true); - try { - const query = params || filters; - // Remove empty values - const cleanQuery: Record = {}; - Object.entries(query).forEach(([k, v]) => { - if (v !== undefined && v !== null && v !== '') cleanQuery[k] = v; - }); - const res = await getDevices(cleanQuery); - setData(res as any); - } catch (e) { console.error(e); } - finally { setLoading(false); } - }, [filters]); - - const loadMeta = async () => { - try { - const [types, groups, st] = await Promise.all([ - getDeviceTypes(), getDeviceGroups(), getDeviceStats(), - ]); - setDeviceTypes(types as any[]); - setDeviceGroups(groups as any[]); - setStats(st as any); - } catch (e) { console.error(e); } - }; - - useEffect(() => { loadMeta(); }, []); - useEffect(() => { loadDevices(); }, [filters, loadDevices]); - - const handleFilterChange = (key: string, value: any) => { - setFilters(prev => ({ ...prev, [key]: value, page: 1 })); - }; - - const handlePageChange = (page: number, pageSize: number) => { - setFilters(prev => ({ ...prev, page, page_size: pageSize })); - }; - - const openAddModal = () => { - setEditingDevice(null); - form.resetFields(); - form.setFieldsValue({ collect_interval: 15, is_active: true }); - setShowModal(true); - }; - - const openEditModal = (record: any) => { - setEditingDevice(record); - form.setFieldsValue({ - ...record, - device_type_id: record.device_type_id, - device_group_id: record.device_group_id, - connection_params: record.connection_params ? JSON.stringify(record.connection_params, null, 2) : '', - }); - setShowModal(true); - }; - - const handleSubmit = async (values: any) => { - try { - // Parse connection_params if provided as string - if (values.connection_params && typeof values.connection_params === 'string') { - try { - values.connection_params = JSON.parse(values.connection_params); - } catch { - message.error('连接参数JSON格式错误'); - return; - } - } - if (editingDevice) { - await updateDevice(editingDevice.id, values); - message.success('设备更新成功'); - } else { - await createDevice(values); - message.success('设备创建成功'); - } - setShowModal(false); - form.resetFields(); - loadDevices(); - loadMeta(); - } catch (e: any) { - message.error(e?.detail || '操作失败'); - } - }; - - const columns = [ - { title: '', dataIndex: 'device_type', width: 50, render: (_: any, record: any) => ( - - )}, - { title: '设备名称', dataIndex: 'name', width: 160, ellipsis: true, render: (name: string, record: any) => ( - navigate(`/devices/${record.id}`)}>{name} - )}, - { title: '设备编号', dataIndex: 'code', width: 130 }, - { title: '设备类型', dataIndex: 'device_type_name', width: 120, render: (v: string) => v ? } color="blue">{v} : '-' }, - { title: '设备分组', dataIndex: 'device_group_name', width: 120 }, - { title: '型号', dataIndex: 'model', width: 120, ellipsis: true }, - { title: '厂商', dataIndex: 'manufacturer', width: 120, ellipsis: true }, - { title: '额定功率(kW)', dataIndex: 'rated_power', width: 110, render: (v: number) => v != null ? v : '-' }, - { title: '位置', dataIndex: 'location', width: 120, ellipsis: true }, - { title: '协议', dataIndex: 'protocol', width: 100 }, - { title: '状态', dataIndex: 'status', width: 80, render: (s: string) => { - const st = statusMap[s] || { color: 'default', text: s || '-' }; - return {st.text}; - }}, - { title: '最近数据时间', dataIndex: 'last_data_time', width: 170 }, - { title: '操作', key: 'action', width: 120, fixed: 'right' as const, render: (_: any, record: any) => ( - - - - )}, - ]; - - return ( -
- {/* Stats Cards */} - -
- - } /> - - - - - } valueStyle={{ color: '#52c41a' }} /> - - - - - } valueStyle={{ color: '#999' }} /> - - - - - } valueStyle={{ color: '#ff4d4f' }} /> - - - - - {/* Device Table */} - } onClick={openAddModal}>添加设备 - }> - {/* Filters */} - - ({ label: g.name, value: g.id }))} - onChange={v => handleFilterChange('device_group', v)} - /> -
`共 ${total} 台设备`, - onChange: handlePageChange, - }} - /> - - - {/* Add/Edit Modal */} - { setShowModal(false); form.resetFields(); }} - onOk={() => form.submit()} - okText={editingDevice ? '保存' : '创建'} - cancelText="取消" - width={640} - destroyOnClose - > - - - - - - - - - - - - - - - - - ({ label: g.name, value: g.id }))} /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ({ label: `${y}年`, value: y }))} - /> -
( -
- - - {(record.periods || []).map((p: any, i: number) => ( -
- - {p.period_label || PERIOD_LABELS[p.period_type]} {p.start_time}-{p.end_time} ¥{p.price_yuan_per_kwh} - - - ))} - - - ), - }} - /> - - setShowCreate(false)} - onOk={() => createForm.submit()} okText="创建"> - - - - - - - - - - - - - - - - - setShowPeriods(false)} onOk={handleSavePeriods} okText="保存" - width={700}> - {periods.map((p, idx) => ( - - - - { const np = [...periods]; np[idx].start_time = e.target.value; setPeriods(np); }} - /> - - - { const np = [...periods]; np[idx].end_time = e.target.value; setPeriods(np); }} - /> - - - { const np = [...periods]; np[idx].price_yuan_per_kwh = v; setPeriods(np); }} - addonAfter="元" - /> - - - - - - ); -} diff --git a/frontend/src/pages/EnergyStrategy/SavingsReport.tsx b/frontend/src/pages/EnergyStrategy/SavingsReport.tsx deleted file mode 100644 index 5afe7a8..0000000 --- a/frontend/src/pages/EnergyStrategy/SavingsReport.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Row, Col, Card, Select, Statistic, Table, Empty, message } from 'antd'; -import { ArrowUpOutlined } from '@ant-design/icons'; -import ReactECharts from 'echarts-for-react'; -import { getStrategySavingsReport } from '../../services/api'; - -export default function SavingsReport() { - const [data, setData] = useState(null); - const [year, setYear] = useState(2026); - const [loading, setLoading] = useState(true); - - useEffect(() => { loadData(); }, [year]); - - const loadData = async () => { - setLoading(true); - try { - const d = await getStrategySavingsReport({ year }); - setData(d); - } catch { message.error('加载节约报告失败'); } - finally { setLoading(false); } - }; - - const getChartOption = () => { - if (!data?.monthly_reports?.length) return {}; - const months = data.monthly_reports.map((r: any) => r.year_month); - return { - tooltip: { trigger: 'axis' }, - legend: { data: ['基准费用', '优化费用', '节约金额'] }, - xAxis: { type: 'category', data: months }, - yAxis: { type: 'value', name: '元' }, - series: [ - { name: '基准费用', type: 'bar', stack: 'compare', data: data.monthly_reports.map((r: any) => r.baseline_cost), itemStyle: { color: '#ff7875' } }, - { name: '优化费用', type: 'bar', stack: 'optimized', data: data.monthly_reports.map((r: any) => r.optimized_cost), itemStyle: { color: '#69c0ff' } }, - { name: '节约金额', type: 'line', data: data.monthly_reports.map((r: any) => r.savings_yuan), itemStyle: { color: '#52c41a' }, lineStyle: { width: 3 } }, - ], - grid: { left: 60, right: 20, top: 40, bottom: 30 }, - }; - }; - - const columns = [ - { title: '月份', dataIndex: 'year_month' }, - { title: '总用电(kWh)', dataIndex: 'total_consumption_kwh', render: (v: number) => v?.toFixed(1) || '-' }, - { title: '总电费(元)', dataIndex: 'total_cost_yuan', render: (v: number) => v?.toFixed(2) || '-' }, - { title: '基准费用(元)', dataIndex: 'baseline_cost', render: (v: number) => v?.toFixed(2) || '-' }, - { title: '优化费用(元)', dataIndex: 'optimized_cost', render: (v: number) => v?.toFixed(2) || '-' }, - { title: '节约(元)', dataIndex: 'savings_yuan', render: (v: number) => {v?.toFixed(2) || '0.00'} }, - ]; - - return ( -
- -
-
- - - ) : ( - - - - )} - - ); -} diff --git a/frontend/src/pages/EnergyStrategy/StrategyManager.tsx b/frontend/src/pages/EnergyStrategy/StrategyManager.tsx deleted file mode 100644 index 67e7187..0000000 --- a/frontend/src/pages/EnergyStrategy/StrategyManager.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Card, Row, Col, Switch, Tag, Space, Button, Descriptions, Alert, message } from 'antd'; -import { ThunderboltOutlined, FireOutlined, BulbOutlined, SwapOutlined } from '@ant-design/icons'; -import { getStrategies, getStrategyRecommendations } from '../../services/api'; - -const STRATEGY_ICONS: Record = { - heat_storage: , - pv_priority: , - load_shift: , -}; - -const TYPE_LABELS: Record = { - heat_storage: '谷电蓄热', - pv_priority: '光伏自消纳', - load_shift: '负荷转移', -}; - -export default function StrategyManager() { - const [strategies, setStrategies] = useState([]); - const [recommendations, setRecommendations] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { loadData(); }, []); - - const loadData = async () => { - setLoading(true); - try { - const [s, r] = await Promise.all([getStrategies(), getStrategyRecommendations()]); - setStrategies(s as any[]); - setRecommendations(r as any[]); - } catch { message.error('加载策略数据失败'); } - finally { setLoading(false); } - }; - - const handleToggle = async (strategy: any) => { - // For demo: toggle locally - setStrategies(strategies.map(s => - s.strategy_type === strategy.strategy_type ? { ...s, is_enabled: !s.is_enabled } : s - )); - message.success(strategy.is_enabled ? '策略已停用' : '策略已启用'); - }; - - return ( -
- {recommendations.length > 0 && ( -
- {recommendations.map((r, i) => ( - - ))} -
- )} - - - {strategies.map((strategy, idx) => ( -
- - {STRATEGY_ICONS[strategy.strategy_type] || } - {strategy.name} - - } - extra={ - handleToggle(strategy)} - checkedChildren="启用" unCheckedChildren="停用" /> - } - > -

{strategy.description}

- - {strategy.is_enabled ? '运行中' : '未启用'} - - {TYPE_LABELS[strategy.strategy_type] || strategy.strategy_type} - {strategy.priority > 0 && 优先级: {strategy.priority}} - - {strategy.parameters && Object.keys(strategy.parameters).length > 0 && ( - - {Object.entries(strategy.parameters).map(([k, v]) => ( - {String(v)} - ))} - - )} -
- - ))} - - - ); -} diff --git a/frontend/src/pages/EnergyStrategy/StrategySimulator.tsx b/frontend/src/pages/EnergyStrategy/StrategySimulator.tsx deleted file mode 100644 index fabd0d6..0000000 --- a/frontend/src/pages/EnergyStrategy/StrategySimulator.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { useState } from 'react'; -import { Card, Row, Col, InputNumber, Checkbox, Button, Statistic, Divider, message, Space, Tag } from 'antd'; -import { ExperimentOutlined, ThunderboltOutlined } from '@ant-design/icons'; -import ReactECharts from 'echarts-for-react'; -import { simulateStrategy } from '../../services/api'; - -export default function StrategySimulator() { - const [params, setParams] = useState({ - daily_consumption_kwh: 2000, - pv_daily_kwh: 800, - strategies: ['heat_storage', 'pv_priority', 'load_shift'], - }); - const [result, setResult] = useState(null); - const [loading, setLoading] = useState(false); - - const handleSimulate = async () => { - setLoading(true); - try { - const r = await simulateStrategy(params); - setResult(r); - } catch { message.error('模拟失败'); } - finally { setLoading(false); } - }; - - const strategyOptions = [ - { label: '谷电蓄热', value: 'heat_storage' }, - { label: '光伏自消纳', value: 'pv_priority' }, - { label: '负荷转移', value: 'load_shift' }, - ]; - - const getCompareOption = () => { - if (!result) return {}; - return { - tooltip: { trigger: 'axis' }, - xAxis: { type: 'category', data: ['基准费用', '优化费用'] }, - yAxis: { type: 'value', name: '元/天' }, - series: [{ - type: 'bar', - barWidth: '40%', - data: [ - { value: result.baseline_cost_per_day, itemStyle: { color: '#ff7875' } }, - { value: result.optimized_cost_per_day, itemStyle: { color: '#52c41a' } }, - ], - label: { show: true, position: 'top', formatter: '¥{c}' }, - }], - grid: { left: 60, right: 20, top: 30, bottom: 30 }, - }; - }; - - const getSavingsBreakdownOption = () => { - if (!result?.details?.length) return {}; - return { - tooltip: { trigger: 'item', formatter: '{b}: ¥{c}/天' }, - series: [{ - type: 'pie', - radius: ['40%', '70%'], - data: result.details.map((d: any) => ({ - name: d.strategy, - value: d.savings_per_day, - })), - label: { formatter: '{b}\n¥{c}/天' }, - }], - }; - }; - - return ( -
- 策略模拟器}> - -
-
日均用电量 (kWh)
- setParams({ ...params, daily_consumption_kwh: v || 2000 })} - /> - - -
日均光伏发电 (kWh)
- setParams({ ...params, pv_daily_kwh: v || 800 })} - /> - - -
优化策略
- setParams({ ...params, strategies: v as string[] })} - /> - - - - - - - - {result && ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {result.details?.map((d: any, i: number) => ( -
- -
- {d.strategy} - {d.description} - - - - - - - - - - ))} - - - - 节约比例: {result.savings_percentage}% - - - - - )} - - ); -} diff --git a/frontend/src/pages/EnergyStrategy/WeatherPanel.tsx b/frontend/src/pages/EnergyStrategy/WeatherPanel.tsx deleted file mode 100644 index 9990b89..0000000 --- a/frontend/src/pages/EnergyStrategy/WeatherPanel.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useEffect, useState } from 'react'; -import { Row, Col, Card, Statistic, Space, Tag, message } from 'antd'; -import { CloudOutlined, FireOutlined } from '@ant-design/icons'; -import ReactECharts from 'echarts-for-react'; -import { getWeatherCurrent, getWeatherForecast, getWeatherImpact } from '../../services/api'; - -export default function WeatherPanel() { - const [current, setCurrent] = useState(null); - const [forecast, setForecast] = useState([]); - const [impact, setImpact] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { loadData(); }, []); - - const loadData = async () => { - setLoading(true); - try { - const [c, f, imp] = await Promise.all([ - getWeatherCurrent(), - getWeatherForecast(), - getWeatherImpact(), - ]); - setCurrent(c); - setForecast(f as any[]); - setImpact(imp); - } catch { message.error('加载天气数据失败'); } - finally { setLoading(false); } - }; - - const getForecastOption = () => { - if (!forecast.length) return {}; - const times = forecast.map(f => { - const d = new Date(f.timestamp); - return `${d.getMonth() + 1}/${d.getDate()} ${d.getHours()}:00`; - }); - return { - tooltip: { trigger: 'axis' }, - legend: { data: ['温度(C)', '太阳辐射(W/m2)', '风速(m/s)'] }, - xAxis: { type: 'category', data: times, axisLabel: { rotate: 45, fontSize: 10 } }, - yAxis: [ - { type: 'value', name: 'C / m/s', position: 'left' }, - { type: 'value', name: 'W/m2', position: 'right' }, - ], - series: [ - { name: '温度(C)', type: 'line', data: forecast.map(f => f.temperature), smooth: true, itemStyle: { color: '#ff7875' } }, - { name: '太阳辐射(W/m2)', type: 'area', yAxisIndex: 1, data: forecast.map(f => f.solar_radiation), smooth: true, itemStyle: { color: '#faad14' }, areaStyle: { opacity: 0.3 } }, - { name: '风速(m/s)', type: 'line', data: forecast.map(f => f.wind_speed), smooth: true, itemStyle: { color: '#1890ff' } }, - ], - grid: { left: 50, right: 50, top: 40, bottom: 60 }, - }; - }; - - const getImpactOption = () => { - if (!impact?.temperature_impact) return {}; - const data = impact.temperature_impact; - return { - tooltip: { trigger: 'axis' }, - legend: { data: ['能耗(kWh)', '光伏发电(kWh)'] }, - xAxis: { type: 'category', data: data.map((d: any) => d.range) }, - yAxis: { type: 'value', name: 'kWh' }, - series: [ - { name: '能耗(kWh)', type: 'bar', data: data.map((d: any) => d.avg_consumption), itemStyle: { color: '#ff7875' } }, - { name: '光伏发电(kWh)', type: 'bar', data: data.map((d: any) => d.pv_generation), itemStyle: { color: '#52c41a' } }, - ], - grid: { left: 50, right: 20, top: 40, bottom: 30 }, - }; - }; - - const tempColor = (t: number) => { - if (t < 0) return '#1890ff'; - if (t < 15) return '#69c0ff'; - if (t < 25) return '#52c41a'; - if (t < 35) return '#faad14'; - return '#f5222d'; - }; - - return ( -
- {/* Current weather */} - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - {current?.source && ( -
- - 数据来源: {current.source === 'api' ? '气象API' : '模拟数据'} - -
- )} - - {/* Forecast */} - - - - - {/* Weather impact */} - - - - - - - - - {impact?.key_findings?.map((f: string, i: number) => ( -
- - - {f} - -
- ))} -
- - - - ); -} diff --git a/frontend/src/pages/EnergyStrategy/index.tsx b/frontend/src/pages/EnergyStrategy/index.tsx deleted file mode 100644 index 2f67643..0000000 --- a/frontend/src/pages/EnergyStrategy/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useState } from 'react'; -import { Tabs, Card } from 'antd'; -import { - DollarOutlined, ThunderboltOutlined, BarChartOutlined, - FundOutlined, ExperimentOutlined, CloudOutlined, -} from '@ant-design/icons'; -import PricingConfig from './PricingConfig'; -import StrategyManager from './StrategyManager'; -import CostAnalysis from './CostAnalysis'; -import SavingsReport from './SavingsReport'; -import StrategySimulator from './StrategySimulator'; -import WeatherPanel from './WeatherPanel'; - -export default function EnergyStrategy() { - const [activeTab, setActiveTab] = useState('pricing'); - - const items = [ - { - key: 'pricing', - label: 电价配置, - children: , - }, - { - key: 'strategies', - label: 策略管理, - children: , - }, - { - key: 'cost', - label: 费用分析, - children: , - }, - { - key: 'savings', - label: 节约报告, - children: , - }, - { - key: 'simulator', - label: 策略模拟, - children: , - }, - { - key: 'weather', - label: 气象数据, - children: , - }, - ]; - - return ( -
- - - -
- ); -} diff --git a/frontend/src/pages/Login/index.tsx b/frontend/src/pages/Login/index.tsx deleted file mode 100644 index e204cf9..0000000 --- a/frontend/src/pages/Login/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useState } from 'react'; -import { Form, Input, Button, Card, message, Typography } from 'antd'; -import { UserOutlined, LockOutlined, ThunderboltOutlined } from '@ant-design/icons'; -import { useNavigate } from 'react-router-dom'; -import { login } from '../../services/api'; -import { setToken, setUser } from '../../utils/auth'; - -const { Title, Text } = Typography; - -export default function LoginPage() { - const [loading, setLoading] = useState(false); - const [guestLoading, setGuestLoading] = useState(false); - const navigate = useNavigate(); - - const doLogin = async (username: string, password: string) => { - const res: any = await login(username, password); - setToken(res.access_token); - setUser(res.user); - return res; - }; - - const onFinish = async (values: { username: string; password: string }) => { - setLoading(true); - try { - await doLogin(values.username, values.password); - message.success('登录成功'); - navigate('/'); - } catch { - message.error('用户名或密码错误'); - } finally { - setLoading(false); - } - }; - - const onGuestLogin = async () => { - setGuestLoading(true); - try { - await doLogin('visitor', 'visitor123'); - message.success('访客登录成功'); - navigate('/'); - } catch { - message.error('访客登录失败,请联系管理员'); - } finally { - setGuestLoading(false); - } - }; - - return ( -
- -
- - - 天普智慧能源管理平台 - - 零碳园区 · 智慧运维 -
-
- - } placeholder="用户名" /> - - - } placeholder="密码" /> - - - - - - - -
- - 访客仅可浏览数据,无管理权限 - -
- -
-
- ); -} diff --git a/frontend/src/pages/Maintenance/index.tsx b/frontend/src/pages/Maintenance/index.tsx deleted file mode 100644 index 369bca1..0000000 --- a/frontend/src/pages/Maintenance/index.tsx +++ /dev/null @@ -1,399 +0,0 @@ -import { useEffect, useState } from 'react'; -import { - Card, Table, Tag, Button, Tabs, Modal, Form, Input, Select, InputNumber, - Space, message, Row, Col, Statistic, DatePicker, Badge, -} from 'antd'; -import { - PlusOutlined, ToolOutlined, CheckOutlined, WarningOutlined, - ClockCircleOutlined, UserOutlined, -} from '@ant-design/icons'; -import dayjs from 'dayjs'; -import { - getMaintenanceDashboard, getMaintenancePlans, createMaintenancePlan, - triggerMaintenancePlan, getMaintenanceRecords, getMaintenanceOrders, - createMaintenanceOrder, assignMaintenanceOrder, completeMaintenanceOrder, - getMaintenanceDuty, createMaintenanceDuty, -} from '../../services/api'; - -const priorityMap: Record = { - critical: { color: 'red', text: '紧急' }, - high: { color: 'orange', text: '高' }, - medium: { color: 'blue', text: '中' }, - low: { color: 'default', text: '低' }, -}; - -const orderStatusMap: Record = { - open: { color: 'red', text: '待处理' }, - assigned: { color: 'orange', text: '已指派' }, - in_progress: { color: 'processing', text: '处理中' }, - completed: { color: 'green', text: '已完成' }, - verified: { color: 'cyan', text: '已验证' }, - closed: { color: 'default', text: '已关闭' }, -}; - -const recordStatusMap: Record = { - pending: { color: 'default', text: '待执行' }, - in_progress: { color: 'processing', text: '执行中' }, - completed: { color: 'green', text: '已完成' }, - issues_found: { color: 'orange', text: '发现问题' }, -}; - -const shiftMap: Record = { - day: '白班', night: '夜班', on_call: '值班', -}; - -// ── Tab 1: Dashboard ─────────────────────────────────────────────── - -function DashboardTab() { - const [dashboard, setDashboard] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - (async () => { - try { - setDashboard(await getMaintenanceDashboard()); - } catch { message.error('加载运维概览失败'); } - finally { setLoading(false); } - })(); - }, []); - - const orderColumns = [ - { title: '工单号', dataIndex: 'code', width: 160 }, - { title: '标题', dataIndex: 'title' }, - { title: '优先级', dataIndex: 'priority', width: 80, render: (v: string) => { - const p = priorityMap[v] || { color: 'default', text: v }; - return {p.text}; - }}, - { title: '状态', dataIndex: 'status', width: 90, render: (v: string) => { - const s = orderStatusMap[v] || { color: 'default', text: v }; - return ; - }}, - { title: '创建时间', dataIndex: 'created_at', width: 170, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' }, - ]; - - return ( -
- -
- } /> - - - } valueStyle={{ color: dashboard?.overdue_count > 0 ? '#f5222d' : undefined }} /> - - - } /> - - - } /> - - - -
- - - ); -} - -// ── Tab 2: Inspection Plans ──────────────────────────────────────── - -function PlansTab() { - const [plans, setPlans] = useState([]); - const [loading, setLoading] = useState(true); - const [showModal, setShowModal] = useState(false); - const [form] = Form.useForm(); - - const loadPlans = async () => { - setLoading(true); - try { setPlans(await getMaintenancePlans() as any[]); } - catch { message.error('加载巡检计划失败'); } - finally { setLoading(false); } - }; - - useEffect(() => { loadPlans(); }, []); - - const handleCreate = async (values: any) => { - try { - await createMaintenancePlan(values); - message.success('巡检计划创建成功'); - setShowModal(false); - form.resetFields(); - loadPlans(); - } catch { message.error('创建失败'); } - }; - - const handleTrigger = async (id: number) => { - try { - await triggerMaintenancePlan(id); - message.success('已触发巡检'); - } catch { message.error('触发失败'); } - }; - - const columns = [ - { title: '计划名称', dataIndex: 'name' }, - { title: '巡检周期', dataIndex: 'schedule_type', render: (v: string) => { - const map: Record = { daily: '每日', weekly: '每周', monthly: '每月', custom: '自定义' }; - return map[v] || v || '-'; - }}, - { title: '状态', dataIndex: 'is_active', width: 80, render: (v: boolean) => {v ? '启用' : '停用'} }, - { title: '下次执行', dataIndex: 'next_run_at', width: 170, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' }, - { title: '操作', key: 'action', width: 120, render: (_: any, r: any) => ( - - )}, - ]; - - return ( - } onClick={() => setShowModal(true)}>新建计划}> -
- setShowModal(false)} onOk={() => form.submit()} okText="创建" cancelText="取消"> -
- - - - - - - - - - -
- - ); -} - -// ── Tab 3: Inspection Records ────────────────────────────────────── - -function RecordsTab() { - const [records, setRecords] = useState({ total: 0, items: [] }); - const [loading, setLoading] = useState(true); - const [statusFilter, setStatusFilter] = useState(); - - const loadRecords = async () => { - setLoading(true); - try { - setRecords(await getMaintenanceRecords({ status: statusFilter })); - } catch { message.error('加载巡检记录失败'); } - finally { setLoading(false); } - }; - - useEffect(() => { loadRecords(); }, [statusFilter]); - - const columns = [ - { title: 'ID', dataIndex: 'id', width: 60 }, - { title: '计划ID', dataIndex: 'plan_id', width: 80 }, - { title: '巡检人', dataIndex: 'inspector_id', width: 80 }, - { title: '状态', dataIndex: 'status', width: 100, render: (v: string) => { - const s = recordStatusMap[v] || { color: 'default', text: v }; - return {s.text}; - }}, - { title: '开始时间', dataIndex: 'started_at', width: 170, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' }, - { title: '完成时间', dataIndex: 'completed_at', width: 170, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' }, - ]; - - return ( - ({ label: v.text, value: k }))} /> - }> -
- - ); -} - -// ── Tab 4: Repair Orders ─────────────────────────────────────────── - -function OrdersTab() { - const [orders, setOrders] = useState({ total: 0, items: [] }); - const [loading, setLoading] = useState(true); - const [showModal, setShowModal] = useState(false); - const [assignModal, setAssignModal] = useState<{ open: boolean; orderId: number | null }>({ open: false, orderId: null }); - const [form] = Form.useForm(); - - const loadOrders = async () => { - setLoading(true); - try { setOrders(await getMaintenanceOrders({})); } - catch { message.error('加载工单失败'); } - finally { setLoading(false); } - }; - - useEffect(() => { loadOrders(); }, []); - - const handleCreate = async (values: any) => { - try { - await createMaintenanceOrder(values); - message.success('工单创建成功'); - setShowModal(false); - form.resetFields(); - loadOrders(); - } catch { message.error('创建失败'); } - }; - - const handleAssign = async (userId: number) => { - if (!assignModal.orderId) return; - try { - await assignMaintenanceOrder(assignModal.orderId, userId); - message.success('已指派'); - setAssignModal({ open: false, orderId: null }); - loadOrders(); - } catch { message.error('指派失败'); } - }; - - const handleComplete = async (id: number) => { - try { - await completeMaintenanceOrder(id); - message.success('已完成'); - loadOrders(); - } catch { message.error('操作失败'); } - }; - - const columns = [ - { title: '工单号', dataIndex: 'code', width: 160 }, - { title: '标题', dataIndex: 'title' }, - { title: '优先级', dataIndex: 'priority', width: 80, render: (v: string) => { - const p = priorityMap[v] || { color: 'default', text: v }; - return {p.text}; - }}, - { title: '状态', dataIndex: 'status', width: 90, render: (v: string) => { - const s = orderStatusMap[v] || { color: 'default', text: v }; - return ; - }}, - { title: '创建时间', dataIndex: 'created_at', width: 170, render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' }, - { title: '操作', key: 'action', width: 200, render: (_: any, r: any) => ( - - {r.status === 'open' && } - {['assigned', 'in_progress'].includes(r.status) && } - - )}, - ]; - - return ( - } onClick={() => setShowModal(true)}>新建工单}> -
- - setShowModal(false)} onOk={() => form.submit()} okText="创建" cancelText="取消"> -
- - - - - - - - - - -
- - setShowModal(false)} onOk={() => form.submit()} okText="创建" cancelText="取消"> - - - - - - - - - setFilters(prev => ({ ...prev, category: v, page: 1 }))} /> -
`共 ${t} 条`, - onChange: (p, ps) => setFilters(prev => ({ ...prev, page: p, page_size: ps })), - }} /> - { setShowModal(false); form.resetFields(); }} - onOk={() => form.submit()} destroyOnClose width={600}> - - - ({ label: s.label, value: s.value }))} /> - - -