From 7811bc1f083159fbd89289d5364dd895d77eda0f Mon Sep 17 00:00:00 2001 From: Du Wenbo Date: Sun, 5 Apr 2026 15:44:40 +0800 Subject: [PATCH] feat: initial EMS frontend template from ems-core v1.1.0 --- .gitignore | 6 + Dockerfile | 11 + Dockerfile.prod | 20 + README.md | 43 + eslint.config.js | 23 + index.html | 13 + package-lock.json | 6012 +++++++++++++++++ package.json | 45 + public/devices/default.svg | 19 + public/devices/heat_meter.svg | 30 + public/devices/heat_pump.svg | 46 + public/devices/meter.svg | 41 + public/devices/pv_inverter.svg | 42 + public/devices/sensor.svg | 39 + public/devices/water_meter.svg | 41 + public/favicon.svg | 1 + public/icons.svg | 24 + src/App.tsx | 84 + src/assets/hero.png | Bin 0 -> 44919 bytes src/assets/react.svg | 1 + src/assets/vite.svg | 1 + src/contexts/ThemeContext.tsx | 33 + src/hooks/useRealtimeWebSocket.ts | 196 + src/i18n/index.ts | 18 + src/i18n/locales/en.json | 64 + src/i18n/locales/zh.json | 64 + src/index.css | 84 + src/layouts/MainLayout.tsx | 224 + src/main.tsx | 10 + src/pages/AIOperations/index.tsx | 859 +++ src/pages/Alarms/index.tsx | 314 + src/pages/Analysis/CostAnalysis.tsx | 245 + src/pages/Analysis/LossAnalysis.tsx | 107 + src/pages/Analysis/MomAnalysis.tsx | 130 + src/pages/Analysis/SubitemAnalysis.tsx | 222 + src/pages/Analysis/YoyAnalysis.tsx | 108 + src/pages/Analysis/index.tsx | 336 + src/pages/BigScreen/components/AlarmCard.tsx | 91 + .../BigScreen/components/AnimatedNumber.tsx | 38 + src/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 + 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 + src/pages/BigScreen3D/components/Ground.tsx | 22 + .../BigScreen3D/components/HUDOverlay.tsx | 93 + .../BigScreen3D/components/HeatPumps.tsx | 147 + src/pages/BigScreen3D/components/PVPanels.tsx | 107 + .../components/SceneEnvironment.tsx | 24 + src/pages/BigScreen3D/constants.ts | 120 + .../BigScreen3D/hooks/useCameraAnimation.ts | 69 + src/pages/BigScreen3D/hooks/useDeviceData.ts | 129 + src/pages/BigScreen3D/hooks/useEnergyFlow.ts | 33 + src/pages/BigScreen3D/index.tsx | 132 + src/pages/BigScreen3D/styles.module.css | 329 + src/pages/BigScreen3D/types.ts | 73 + src/pages/Carbon/index.tsx | 626 ++ src/pages/Charging/Dashboard.tsx | 169 + src/pages/Charging/Orders.tsx | 201 + src/pages/Charging/Piles.tsx | 203 + src/pages/Charging/Pricing.tsx | 193 + src/pages/Charging/Stations.tsx | 180 + src/pages/Charging/index.tsx | 23 + .../Dashboard/components/DeviceStatus.tsx | 26 + src/pages/Dashboard/components/EnergyFlow.tsx | 96 + .../Dashboard/components/EnergyOverview.tsx | 33 + src/pages/Dashboard/components/LoadCurve.tsx | 40 + .../Dashboard/components/PowerGeneration.tsx | 45 + src/pages/Dashboard/index.tsx | 143 + src/pages/DataQuery/index.tsx | 365 + src/pages/DeviceDetail/index.tsx | 490 ++ src/pages/Devices/Topology.tsx | 186 + src/pages/Devices/index.tsx | 312 + src/pages/EnergyStrategy/CostAnalysis.tsx | 130 + src/pages/EnergyStrategy/PricingConfig.tsx | 259 + src/pages/EnergyStrategy/SavingsReport.tsx | 98 + src/pages/EnergyStrategy/StrategyManager.tsx | 91 + .../EnergyStrategy/StrategySimulator.tsx | 157 + src/pages/EnergyStrategy/WeatherPanel.tsx | 145 + src/pages/EnergyStrategy/index.tsx | 57 + src/pages/Login/index.tsx | 88 + src/pages/Maintenance/index.tsx | 399 ++ src/pages/Management/index.tsx | 524 ++ src/pages/Monitoring/index.tsx | 79 + src/pages/Prediction/AccuracyMetrics.tsx | 151 + src/pages/Prediction/LoadForecast.tsx | 119 + src/pages/Prediction/OptimizationPanel.tsx | 212 + src/pages/Prediction/PVForecast.tsx | 132 + src/pages/Prediction/SavingsReport.tsx | 180 + src/pages/Prediction/index.tsx | 43 + src/pages/Quota/index.tsx | 263 + src/pages/Reports/index.tsx | 129 + src/pages/System/AuditLog.tsx | 174 + src/pages/System/Settings.tsx | 110 + src/pages/System/index.tsx | 142 + src/services/api.ts | 353 + src/utils/auth.ts | 9 + src/utils/devicePhoto.ts | 21 + tsconfig.app.json | 28 + tsconfig.json | 7 + tsconfig.node.json | 26 + vite.config.ts | 29 + 112 files changed, 21450 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Dockerfile.prod create mode 100644 README.md create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/devices/default.svg create mode 100644 public/devices/heat_meter.svg create mode 100644 public/devices/heat_pump.svg create mode 100644 public/devices/meter.svg create mode 100644 public/devices/pv_inverter.svg create mode 100644 public/devices/sensor.svg create mode 100644 public/devices/water_meter.svg create mode 100644 public/favicon.svg create mode 100644 public/icons.svg create mode 100644 src/App.tsx create mode 100644 src/assets/hero.png create mode 100644 src/assets/react.svg create mode 100644 src/assets/vite.svg create mode 100644 src/contexts/ThemeContext.tsx create mode 100644 src/hooks/useRealtimeWebSocket.ts create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/locales/en.json create mode 100644 src/i18n/locales/zh.json create mode 100644 src/index.css create mode 100644 src/layouts/MainLayout.tsx create mode 100644 src/main.tsx create mode 100644 src/pages/AIOperations/index.tsx create mode 100644 src/pages/Alarms/index.tsx create mode 100644 src/pages/Analysis/CostAnalysis.tsx create mode 100644 src/pages/Analysis/LossAnalysis.tsx create mode 100644 src/pages/Analysis/MomAnalysis.tsx create mode 100644 src/pages/Analysis/SubitemAnalysis.tsx create mode 100644 src/pages/Analysis/YoyAnalysis.tsx create mode 100644 src/pages/Analysis/index.tsx create mode 100644 src/pages/BigScreen/components/AlarmCard.tsx create mode 100644 src/pages/BigScreen/components/AnimatedNumber.tsx create mode 100644 src/pages/BigScreen/components/CarbonCard.tsx create mode 100644 src/pages/BigScreen/components/EnergyFlowDiagram.tsx create mode 100644 src/pages/BigScreen/components/EnergyOverviewCard.tsx create mode 100644 src/pages/BigScreen/components/HeatPumpCard.tsx create mode 100644 src/pages/BigScreen/components/LoadCurveCard.tsx create mode 100644 src/pages/BigScreen/components/PVCard.tsx create mode 100644 src/pages/BigScreen/index.tsx create mode 100644 src/pages/BigScreen/styles.module.css create mode 100644 src/pages/BigScreen3D/components/Buildings.tsx create mode 100644 src/pages/BigScreen3D/components/CampusScene.tsx create mode 100644 src/pages/BigScreen3D/components/DeviceDetailView.tsx create mode 100644 src/pages/BigScreen3D/components/DeviceInfoPanel.tsx create mode 100644 src/pages/BigScreen3D/components/DeviceListPanel.tsx create mode 100644 src/pages/BigScreen3D/components/DeviceMarkers.tsx create mode 100644 src/pages/BigScreen3D/components/EnergyParticles.tsx create mode 100644 src/pages/BigScreen3D/components/Ground.tsx create mode 100644 src/pages/BigScreen3D/components/HUDOverlay.tsx create mode 100644 src/pages/BigScreen3D/components/HeatPumps.tsx create mode 100644 src/pages/BigScreen3D/components/PVPanels.tsx create mode 100644 src/pages/BigScreen3D/components/SceneEnvironment.tsx create mode 100644 src/pages/BigScreen3D/constants.ts create mode 100644 src/pages/BigScreen3D/hooks/useCameraAnimation.ts create mode 100644 src/pages/BigScreen3D/hooks/useDeviceData.ts create mode 100644 src/pages/BigScreen3D/hooks/useEnergyFlow.ts create mode 100644 src/pages/BigScreen3D/index.tsx create mode 100644 src/pages/BigScreen3D/styles.module.css create mode 100644 src/pages/BigScreen3D/types.ts create mode 100644 src/pages/Carbon/index.tsx create mode 100644 src/pages/Charging/Dashboard.tsx create mode 100644 src/pages/Charging/Orders.tsx create mode 100644 src/pages/Charging/Piles.tsx create mode 100644 src/pages/Charging/Pricing.tsx create mode 100644 src/pages/Charging/Stations.tsx create mode 100644 src/pages/Charging/index.tsx create mode 100644 src/pages/Dashboard/components/DeviceStatus.tsx create mode 100644 src/pages/Dashboard/components/EnergyFlow.tsx create mode 100644 src/pages/Dashboard/components/EnergyOverview.tsx create mode 100644 src/pages/Dashboard/components/LoadCurve.tsx create mode 100644 src/pages/Dashboard/components/PowerGeneration.tsx create mode 100644 src/pages/Dashboard/index.tsx create mode 100644 src/pages/DataQuery/index.tsx create mode 100644 src/pages/DeviceDetail/index.tsx create mode 100644 src/pages/Devices/Topology.tsx create mode 100644 src/pages/Devices/index.tsx create mode 100644 src/pages/EnergyStrategy/CostAnalysis.tsx create mode 100644 src/pages/EnergyStrategy/PricingConfig.tsx create mode 100644 src/pages/EnergyStrategy/SavingsReport.tsx create mode 100644 src/pages/EnergyStrategy/StrategyManager.tsx create mode 100644 src/pages/EnergyStrategy/StrategySimulator.tsx create mode 100644 src/pages/EnergyStrategy/WeatherPanel.tsx create mode 100644 src/pages/EnergyStrategy/index.tsx create mode 100644 src/pages/Login/index.tsx create mode 100644 src/pages/Maintenance/index.tsx create mode 100644 src/pages/Management/index.tsx create mode 100644 src/pages/Monitoring/index.tsx create mode 100644 src/pages/Prediction/AccuracyMetrics.tsx create mode 100644 src/pages/Prediction/LoadForecast.tsx create mode 100644 src/pages/Prediction/OptimizationPanel.tsx create mode 100644 src/pages/Prediction/PVForecast.tsx create mode 100644 src/pages/Prediction/SavingsReport.tsx create mode 100644 src/pages/Prediction/index.tsx create mode 100644 src/pages/Quota/index.tsx create mode 100644 src/pages/Reports/index.tsx create mode 100644 src/pages/System/AuditLog.tsx create mode 100644 src/pages/System/Settings.tsx create mode 100644 src/pages/System/index.tsx create mode 100644 src/services/api.ts create mode 100644 src/utils/auth.ts create mode 100644 src/utils/devicePhoto.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94362eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +.env +.env.local +*.log +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..381b906 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +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/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..983f6c9 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,20 @@ +# 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/README.md b/README.md new file mode 100644 index 0000000..190c3ac --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# EMS Frontend Template + +Base React frontend template for EMS customer projects. + +## Usage + +When creating a new customer project, copy this template: + +```bash +cp -r ems-frontend-template/ /frontend/ +cd /frontend +npm install +``` + +Then customize: +1. Edit `src/App.tsx` — add/remove routes per customer needs +2. Edit `src/layouts/MainLayout.tsx` — customer branding (logo, colors, sidebar) +3. Edit `package.json` — remove unused deps (e.g., Three.js if no 3D) +4. Edit `vite.config.ts` — update proxy target if backend port differs + +## Tech Stack +- React 19 + TypeScript +- Ant Design 5 + ProComponents +- ECharts 6 +- Three.js + React Three Fiber (optional, for 3D visualization) +- i18next (zh + en) +- Vite 8 + +## Available Pages +- Dashboard, Monitoring, Devices, DeviceDetail +- Analysis (cost, loss, YoY, MoM, subitem) +- Alarms, Carbon, Reports +- BigScreen (2D), BigScreen3D (3D) +- Charging (stations, piles, orders, pricing) +- Prediction, EnergyStrategy, AIOperations +- Maintenance, DataQuery, Management, Quota +- System (settings, audit log) +- Login + +## Notes +- Not all pages are needed for every customer +- Remove unused page imports from App.tsx +- Remove corresponding dependencies from package.json diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +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/index.html b/index.html new file mode 100644 index 0000000..14bcc3c --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + 天普智慧能源管理平台 + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e26b189 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6012 @@ +{ + "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/package.json b/package.json new file mode 100644 index 0000000..b23556e --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "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/public/devices/default.svg b/public/devices/default.svg new file mode 100644 index 0000000..73a9d56 --- /dev/null +++ b/public/devices/default.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + IoT Device + 通用设备 + \ No newline at end of file diff --git a/public/devices/heat_meter.svg b/public/devices/heat_meter.svg new file mode 100644 index 0000000..b086c1f --- /dev/null +++ b/public/devices/heat_meter.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + 256.8 + GJ + 累计热量 + + 流量: 2.4 m³/h + 温差: 8.2°C + + + + 供水 + 回水 + + + + + 热量表 + Ultrasonic Heat Meter + \ No newline at end of file diff --git a/public/devices/heat_pump.svg b/public/devices/heat_pump.svg new file mode 100644 index 0000000..05dc57a --- /dev/null +++ b/public/devices/heat_pump.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 空气源热泵 + Air Source Heat Pump + \ No newline at end of file diff --git a/public/devices/meter.svg b/public/devices/meter.svg new file mode 100644 index 0000000..1253a37 --- /dev/null +++ b/public/devices/meter.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + 1847.5 + kWh + 正向有功总 + + + + + 运行 + + + + + + OK + + + + + + + + + + + + 智能电表 + Smart Power Meter + \ No newline at end of file diff --git a/public/devices/pv_inverter.svg b/public/devices/pv_inverter.svg new file mode 100644 index 0000000..48bb479 --- /dev/null +++ b/public/devices/pv_inverter.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 光伏逆变器 + Huawei SUN2000-110KTL + \ No newline at end of file diff --git a/public/devices/sensor.svg b/public/devices/sensor.svg new file mode 100644 index 0000000..df10953 --- /dev/null +++ b/public/devices/sensor.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + 23.5 + °C + + + + 湿度 64% + + + + + + + + + + + + + 温湿度传感器 + Temperature & Humidity Sensor + \ No newline at end of file diff --git a/public/devices/water_meter.svg b/public/devices/water_meter.svg new file mode 100644 index 0000000..1253a37 --- /dev/null +++ b/public/devices/water_meter.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + 1847.5 + kWh + 正向有功总 + + + + + 运行 + + + + + + OK + + + + + + + + + + + + 智能电表 + Smart Power Meter + \ No newline at end of file diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..3026007 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,84 @@ +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/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..cc51a3d20ad4bc961b596a6adfd686685cd84bb0 GIT binary patch literal 44919 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 JPDHLkV1myZcL)Fg literal 0 HcmV?d00001 diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx new file mode 100644 index 0000000..f20eb9e --- /dev/null +++ b/src/contexts/ThemeContext.tsx @@ -0,0 +1,33 @@ +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/src/hooks/useRealtimeWebSocket.ts b/src/hooks/useRealtimeWebSocket.ts new file mode 100644 index 0000000..51a14c9 --- /dev/null +++ b/src/hooks/useRealtimeWebSocket.ts @@ -0,0 +1,196 @@ +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/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..99508f2 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,18 @@ +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/src/i18n/locales/en.json b/src/i18n/locales/en.json new file mode 100644 index 0000000..ef7d2ef --- /dev/null +++ b/src/i18n/locales/en.json @@ -0,0 +1,64 @@ +{ + "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/src/i18n/locales/zh.json b/src/i18n/locales/zh.json new file mode 100644 index 0000000..d6839d3 --- /dev/null +++ b/src/i18n/locales/zh.json @@ -0,0 +1,64 @@ +{ + "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/src/index.css b/src/index.css new file mode 100644 index 0000000..ccd2059 --- /dev/null +++ b/src/index.css @@ -0,0 +1,84 @@ +* { + 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/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx new file mode 100644 index 0000000..ac1d705 --- /dev/null +++ b/src/layouts/MainLayout.tsx @@ -0,0 +1,224 @@ +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/src/pages/Alarms/index.tsx b/src/pages/Alarms/index.tsx new file mode 100644 index 0000000..b34d30a --- /dev/null +++ b/src/pages/Alarms/index.tsx @@ -0,0 +1,314 @@ +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/src/pages/Analysis/CostAnalysis.tsx b/src/pages/Analysis/CostAnalysis.tsx new file mode 100644 index 0000000..eb1135a --- /dev/null +++ b/src/pages/Analysis/CostAnalysis.tsx @@ -0,0 +1,245 @@ +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/src/pages/Analysis/MomAnalysis.tsx b/src/pages/Analysis/MomAnalysis.tsx new file mode 100644 index 0000000..236ec6c --- /dev/null +++ b/src/pages/Analysis/MomAnalysis.tsx @@ -0,0 +1,130 @@ +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/src/pages/Analysis/SubitemAnalysis.tsx b/src/pages/Analysis/SubitemAnalysis.tsx new file mode 100644 index 0000000..a01c86f --- /dev/null +++ b/src/pages/Analysis/SubitemAnalysis.tsx @@ -0,0 +1,222 @@ +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/src/pages/Analysis/YoyAnalysis.tsx b/src/pages/Analysis/YoyAnalysis.tsx new file mode 100644 index 0000000..8d16a37 --- /dev/null +++ b/src/pages/Analysis/YoyAnalysis.tsx @@ -0,0 +1,108 @@ +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/src/pages/Analysis/index.tsx b/src/pages/Analysis/index.tsx new file mode 100644 index 0000000..b24a1ca --- /dev/null +++ b/src/pages/Analysis/index.tsx @@ -0,0 +1,336 @@ +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/src/pages/BigScreen/components/AlarmCard.tsx b/src/pages/BigScreen/components/AlarmCard.tsx new file mode 100644 index 0000000..53faf4a --- /dev/null +++ b/src/pages/BigScreen/components/AlarmCard.tsx @@ -0,0 +1,91 @@ +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/src/pages/BigScreen/components/AnimatedNumber.tsx b/src/pages/BigScreen/components/AnimatedNumber.tsx new file mode 100644 index 0000000..82605a0 --- /dev/null +++ b/src/pages/BigScreen/components/AnimatedNumber.tsx @@ -0,0 +1,38 @@ +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/src/pages/BigScreen/components/CarbonCard.tsx b/src/pages/BigScreen/components/CarbonCard.tsx new file mode 100644 index 0000000..e0ef6e9 --- /dev/null +++ b/src/pages/BigScreen/components/CarbonCard.tsx @@ -0,0 +1,139 @@ +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/src/pages/BigScreen/components/EnergyFlowDiagram.tsx b/src/pages/BigScreen/components/EnergyFlowDiagram.tsx new file mode 100644 index 0000000..f7416b5 --- /dev/null +++ b/src/pages/BigScreen/components/EnergyFlowDiagram.tsx @@ -0,0 +1,190 @@ +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/src/pages/BigScreen/components/EnergyOverviewCard.tsx b/src/pages/BigScreen/components/EnergyOverviewCard.tsx new file mode 100644 index 0000000..c347514 --- /dev/null +++ b/src/pages/BigScreen/components/EnergyOverviewCard.tsx @@ -0,0 +1,101 @@ +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/src/pages/BigScreen/components/HeatPumpCard.tsx b/src/pages/BigScreen/components/HeatPumpCard.tsx new file mode 100644 index 0000000..80567a6 --- /dev/null +++ b/src/pages/BigScreen/components/HeatPumpCard.tsx @@ -0,0 +1,96 @@ +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/src/pages/BigScreen/components/LoadCurveCard.tsx b/src/pages/BigScreen/components/LoadCurveCard.tsx new file mode 100644 index 0000000..d282d78 --- /dev/null +++ b/src/pages/BigScreen/components/LoadCurveCard.tsx @@ -0,0 +1,83 @@ +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/src/pages/BigScreen/components/PVCard.tsx b/src/pages/BigScreen/components/PVCard.tsx new file mode 100644 index 0000000..05bc661 --- /dev/null +++ b/src/pages/BigScreen/components/PVCard.tsx @@ -0,0 +1,110 @@ +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/src/pages/BigScreen/index.tsx b/src/pages/BigScreen/index.tsx new file mode 100644 index 0000000..af92b77 --- /dev/null +++ b/src/pages/BigScreen/index.tsx @@ -0,0 +1,195 @@ +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/src/pages/BigScreen/styles.module.css b/src/pages/BigScreen/styles.module.css new file mode 100644 index 0000000..04697bf --- /dev/null +++ b/src/pages/BigScreen/styles.module.css @@ -0,0 +1,658 @@ +/* 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/src/pages/BigScreen3D/components/Buildings.tsx b/src/pages/BigScreen3D/components/Buildings.tsx new file mode 100644 index 0000000..1d8b3b0 --- /dev/null +++ b/src/pages/BigScreen3D/components/Buildings.tsx @@ -0,0 +1,130 @@ +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/src/pages/BigScreen3D/components/CampusScene.tsx b/src/pages/BigScreen3D/components/CampusScene.tsx new file mode 100644 index 0000000..bd87228 --- /dev/null +++ b/src/pages/BigScreen3D/components/CampusScene.tsx @@ -0,0 +1,182 @@ +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/src/pages/BigScreen3D/components/DeviceDetailView.tsx b/src/pages/BigScreen3D/components/DeviceDetailView.tsx new file mode 100644 index 0000000..353d748 --- /dev/null +++ b/src/pages/BigScreen3D/components/DeviceDetailView.tsx @@ -0,0 +1,490 @@ +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/src/pages/BigScreen3D/components/DeviceInfoPanel.tsx b/src/pages/BigScreen3D/components/DeviceInfoPanel.tsx new file mode 100644 index 0000000..8a83728 --- /dev/null +++ b/src/pages/BigScreen3D/components/DeviceInfoPanel.tsx @@ -0,0 +1,178 @@ +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/src/pages/BigScreen3D/components/DeviceListPanel.tsx b/src/pages/BigScreen3D/components/DeviceListPanel.tsx new file mode 100644 index 0000000..a38c457 --- /dev/null +++ b/src/pages/BigScreen3D/components/DeviceListPanel.tsx @@ -0,0 +1,85 @@ +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/src/pages/BigScreen3D/components/DeviceMarkers.tsx b/src/pages/BigScreen3D/components/DeviceMarkers.tsx new file mode 100644 index 0000000..2fb0bbb --- /dev/null +++ b/src/pages/BigScreen3D/components/DeviceMarkers.tsx @@ -0,0 +1,200 @@ +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/src/pages/BigScreen3D/components/EnergyParticles.tsx b/src/pages/BigScreen3D/components/EnergyParticles.tsx new file mode 100644 index 0000000..ccab272 --- /dev/null +++ b/src/pages/BigScreen3D/components/EnergyParticles.tsx @@ -0,0 +1,164 @@ +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/src/pages/BigScreen3D/components/Ground.tsx b/src/pages/BigScreen3D/components/Ground.tsx new file mode 100644 index 0000000..89728d8 --- /dev/null +++ b/src/pages/BigScreen3D/components/Ground.tsx @@ -0,0 +1,22 @@ +import { Grid } from '@react-three/drei'; + +export default function Ground() { + return ( + + + + + + + + ); +} diff --git a/src/pages/BigScreen3D/components/HUDOverlay.tsx b/src/pages/BigScreen3D/components/HUDOverlay.tsx new file mode 100644 index 0000000..862a575 --- /dev/null +++ b/src/pages/BigScreen3D/components/HUDOverlay.tsx @@ -0,0 +1,93 @@ +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/src/pages/BigScreen3D/components/HeatPumps.tsx b/src/pages/BigScreen3D/components/HeatPumps.tsx new file mode 100644 index 0000000..8a1b786 --- /dev/null +++ b/src/pages/BigScreen3D/components/HeatPumps.tsx @@ -0,0 +1,147 @@ +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/src/pages/BigScreen3D/components/PVPanels.tsx b/src/pages/BigScreen3D/components/PVPanels.tsx new file mode 100644 index 0000000..00dffe0 --- /dev/null +++ b/src/pages/BigScreen3D/components/PVPanels.tsx @@ -0,0 +1,107 @@ +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/src/pages/BigScreen3D/components/SceneEnvironment.tsx b/src/pages/BigScreen3D/components/SceneEnvironment.tsx new file mode 100644 index 0000000..eebd480 --- /dev/null +++ b/src/pages/BigScreen3D/components/SceneEnvironment.tsx @@ -0,0 +1,24 @@ +import { Stars } from '@react-three/drei'; + +export default function SceneEnvironment() { + return ( + <> + + + + + + ); +} diff --git a/src/pages/BigScreen3D/constants.ts b/src/pages/BigScreen3D/constants.ts new file mode 100644 index 0000000..b9fd874 --- /dev/null +++ b/src/pages/BigScreen3D/constants.ts @@ -0,0 +1,120 @@ +// 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/src/pages/BigScreen3D/hooks/useCameraAnimation.ts b/src/pages/BigScreen3D/hooks/useCameraAnimation.ts new file mode 100644 index 0000000..6f246be --- /dev/null +++ b/src/pages/BigScreen3D/hooks/useCameraAnimation.ts @@ -0,0 +1,69 @@ +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/src/pages/BigScreen3D/hooks/useDeviceData.ts b/src/pages/BigScreen3D/hooks/useDeviceData.ts new file mode 100644 index 0000000..5cfc06b --- /dev/null +++ b/src/pages/BigScreen3D/hooks/useDeviceData.ts @@ -0,0 +1,129 @@ +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/src/pages/BigScreen3D/hooks/useEnergyFlow.ts b/src/pages/BigScreen3D/hooks/useEnergyFlow.ts new file mode 100644 index 0000000..4ad8df9 --- /dev/null +++ b/src/pages/BigScreen3D/hooks/useEnergyFlow.ts @@ -0,0 +1,33 @@ +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/src/pages/BigScreen3D/index.tsx b/src/pages/BigScreen3D/index.tsx new file mode 100644 index 0000000..b9aa1fe --- /dev/null +++ b/src/pages/BigScreen3D/index.tsx @@ -0,0 +1,132 @@ +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/src/pages/BigScreen3D/styles.module.css b/src/pages/BigScreen3D/styles.module.css new file mode 100644 index 0000000..b41fb15 --- /dev/null +++ b/src/pages/BigScreen3D/styles.module.css @@ -0,0 +1,329 @@ +/* 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/src/pages/BigScreen3D/types.ts b/src/pages/BigScreen3D/types.ts new file mode 100644 index 0000000..19b19bb --- /dev/null +++ b/src/pages/BigScreen3D/types.ts @@ -0,0 +1,73 @@ +// 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/src/pages/Carbon/index.tsx b/src/pages/Carbon/index.tsx new file mode 100644 index 0000000..6326794 --- /dev/null +++ b/src/pages/Carbon/index.tsx @@ -0,0 +1,626 @@ +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/src/pages/Charging/Dashboard.tsx b/src/pages/Charging/Dashboard.tsx new file mode 100644 index 0000000..5bd2b49 --- /dev/null +++ b/src/pages/Charging/Dashboard.tsx @@ -0,0 +1,169 @@ +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/src/pages/Charging/Orders.tsx b/src/pages/Charging/Orders.tsx new file mode 100644 index 0000000..b3492db --- /dev/null +++ b/src/pages/Charging/Orders.tsx @@ -0,0 +1,201 @@ +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/src/pages/Charging/Piles.tsx b/src/pages/Charging/Piles.tsx new file mode 100644 index 0000000..d558782 --- /dev/null +++ b/src/pages/Charging/Piles.tsx @@ -0,0 +1,203 @@ +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/src/pages/DeviceDetail/index.tsx b/src/pages/DeviceDetail/index.tsx new file mode 100644 index 0000000..83cc59e --- /dev/null +++ b/src/pages/DeviceDetail/index.tsx @@ -0,0 +1,490 @@ +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/src/pages/Devices/Topology.tsx b/src/pages/Devices/Topology.tsx new file mode 100644 index 0000000..4d2fa55 --- /dev/null +++ b/src/pages/Devices/Topology.tsx @@ -0,0 +1,186 @@ +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/src/pages/Devices/index.tsx b/src/pages/Devices/index.tsx new file mode 100644 index 0000000..ecdacd0 --- /dev/null +++ b/src/pages/Devices/index.tsx @@ -0,0 +1,312 @@ +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/src/pages/EnergyStrategy/SavingsReport.tsx b/src/pages/EnergyStrategy/SavingsReport.tsx new file mode 100644 index 0000000..5afe7a8 --- /dev/null +++ b/src/pages/EnergyStrategy/SavingsReport.tsx @@ -0,0 +1,98 @@ +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/src/pages/EnergyStrategy/StrategyManager.tsx b/src/pages/EnergyStrategy/StrategyManager.tsx new file mode 100644 index 0000000..67e7187 --- /dev/null +++ b/src/pages/EnergyStrategy/StrategyManager.tsx @@ -0,0 +1,91 @@ +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/src/pages/EnergyStrategy/StrategySimulator.tsx b/src/pages/EnergyStrategy/StrategySimulator.tsx new file mode 100644 index 0000000..fabd0d6 --- /dev/null +++ b/src/pages/EnergyStrategy/StrategySimulator.tsx @@ -0,0 +1,157 @@ +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/src/pages/EnergyStrategy/WeatherPanel.tsx b/src/pages/EnergyStrategy/WeatherPanel.tsx new file mode 100644 index 0000000..9990b89 --- /dev/null +++ b/src/pages/EnergyStrategy/WeatherPanel.tsx @@ -0,0 +1,145 @@ +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/src/pages/EnergyStrategy/index.tsx b/src/pages/EnergyStrategy/index.tsx new file mode 100644 index 0000000..2f67643 --- /dev/null +++ b/src/pages/EnergyStrategy/index.tsx @@ -0,0 +1,57 @@ +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/src/pages/Login/index.tsx b/src/pages/Login/index.tsx new file mode 100644 index 0000000..e204cf9 --- /dev/null +++ b/src/pages/Login/index.tsx @@ -0,0 +1,88 @@ +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/src/pages/Maintenance/index.tsx b/src/pages/Maintenance/index.tsx new file mode 100644 index 0000000..369bca1 --- /dev/null +++ b/src/pages/Maintenance/index.tsx @@ -0,0 +1,399 @@ +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 }))} /> + + +