功能: 完善风电功率方案计算
- 新增计算方案保存与方案二筛选参数 - 支持方案二桨角规则和有效数据导出 - 优化功率曲线绘制和设计功率展示
This commit is contained in:
@@ -10,7 +10,7 @@ description: 技能建设规范。用于为 wind_power_cal 创建/改写 .agents
|
|||||||
## 规则
|
## 规则
|
||||||
- 每个 skill 一个目录,内含 `SKILL.md`。
|
- 每个 skill 一个目录,内含 `SKILL.md`。
|
||||||
- frontmatter:`name`(kebab-case)、`description`(一句话:**何时触发** + 做什么;用于检索匹配)。
|
- frontmatter:`name`(kebab-case)、`description`(一句话:**何时触发** + 做什么;用于检索匹配)。
|
||||||
- 内容贴合 **wind_power_cal 实际**:C++ Drogon 后端(:8848)、React+Vite 前端、`package.sh / run.sh / deploy.sh`、`third_party` 源码编译 Drogon、响应信封 `{status,msg,data}`。
|
- 内容贴合 **wind_power_cal 实际**:C++ Drogon 后端(:8080)、React+Vite 前端、`package.sh / run.sh / deploy.sh`、`third_party` 源码编译 Drogon、响应信封 `{status,msg,data}`。
|
||||||
- **禁止**原封照搬 edge_collector 等其它项目的 skill——其 OTA / 协议采集 / 边缘主机 / collector / configurator / cloud_server 等内容在本仓库不存在,会误导。
|
- **禁止**原封照搬 edge_collector 等其它项目的 skill——其 OTA / 协议采集 / 边缘主机 / collector / configurator / cloud_server 等内容在本仓库不存在,会误导。
|
||||||
- 用 `[[other-skill-name]]` 链接相关技能。
|
- 用 `[[other-skill-name]]` 链接相关技能。
|
||||||
- 通用工程规范(代码风格、git 提交、shell、前端调试)与项目专属规范(`wind-*`)分开维护。
|
- 通用工程规范(代码风格、git 提交、shell、前端调试)与项目专属规范(`wind-*`)分开维护。
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ SendForbidden(callback); // 无权限(code 3, 403)
|
|||||||
- `main.cpp` 必须 `using namespace drogon;`,否则 `app()` / `HttpResponse` / `CT_TEXT_HTML` 未声明
|
- `main.cpp` 必须 `using namespace drogon;`,否则 `app()` / `HttpResponse` / `CT_TEXT_HTML` 未声明
|
||||||
|
|
||||||
## 配置 / SPA
|
## 配置 / SPA
|
||||||
- `backend/config/server_config.json`:listener(默认 :8848)、CORS、`document_root ./web`
|
- `backend/config/server_config.json`:listener(默认 :8080)、CORS、`document_root ./web`
|
||||||
- SPA 回退:`app().setCustom404Page(HttpResponse::newFileResponse("./web/index.html","",CT_TEXT_HTML), false)`
|
- SPA 回退:`app().setCustom404Page(HttpResponse::newFileResponse("./web/index.html","",CT_TEXT_HTML), false)`
|
||||||
|
|
||||||
## 现有 Controller
|
## 现有 Controller
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ description: wind_power_cal 本地构建与运行规范。用于编译后端、
|
|||||||
`--publish` 额外生成 `publish/wind_power-<ver>.tar.gz`。
|
`--publish` 额外生成 `publish/wind_power-<ver>.tar.gz`。
|
||||||
|
|
||||||
## 运行
|
## 运行
|
||||||
- 一键(后端托管 SPA):`./run.sh`,访问 http://localhost:8848/
|
- 一键(后端托管 SPA):`./run.sh`,访问 http://localhost:8080/
|
||||||
- 仅后端:`bash backend/run.sh`(先 build.sh 再启动,设 `LD_LIBRARY_PATH=libs`)
|
- 仅后端:`bash backend/run.sh`(先 build.sh 再启动,设 `LD_LIBRARY_PATH=libs`)
|
||||||
- 开发(前后端分离热更):
|
- 开发(前后端分离热更):
|
||||||
- 后端 `bash backend/run.sh`(:8848)
|
- 后端 `bash backend/run.sh`(:8080)
|
||||||
- 前端 `cd frontend/web_app && npm install && npm run dev`(:5173,代理 `/api`→:8848)
|
- 前端 `cd frontend/web_app && npm install && npm run dev`(:5173,代理 `/api`→:8080)
|
||||||
|
|
||||||
## 后端构建细节
|
## 后端构建细节
|
||||||
- `backend/CMakeLists.txt`:`find_package(Drogon CONFIG REQUIRED)`,`file(GLOB_RECURSE src/*.cpp)`,链 `Drogon::Drogon`,include `src` + `third_party`。
|
- `backend/CMakeLists.txt`:`find_package(Drogon CONFIG REQUIRED)`,`file(GLOB_RECURSE src/*.cpp)`,链 `Drogon::Drogon`,include `src` + `third_party`。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: wind-deploy
|
name: wind-deploy
|
||||||
description: wind_power_cal 远端部署规范。用于把服务部署到公网主机(默认 82.157.83.226)时,按 deploy.sh 流程操作并校验 :8848。
|
description: wind_power_cal 远端部署规范。用于把服务部署到公网主机(默认 82.157.83.226)时,按 deploy.sh 流程操作并校验 :8080。
|
||||||
---
|
---
|
||||||
|
|
||||||
# 远端部署(deploy.sh)
|
# 远端部署(deploy.sh)
|
||||||
@@ -14,7 +14,7 @@ description: wind_power_cal 远端部署规范。用于把服务部署到公网
|
|||||||
- `User=ubuntu`,`ExecStart=~/wind_power/wind_server`
|
- `User=ubuntu`,`ExecStart=~/wind_power/wind_server`
|
||||||
- `Environment=LD_LIBRARY_PATH=~/wind_power/libs`
|
- `Environment=LD_LIBRARY_PATH=~/wind_power/libs`
|
||||||
- 日志 `logs/server.log`,`Restart=on-failure`
|
- 日志 `logs/server.log`,`Restart=on-failure`
|
||||||
4. 重启 `wind_power`,校验 `localhost:8848`
|
4. 重启 `wind_power`,校验 `localhost:8080`
|
||||||
|
|
||||||
## 远端运行期依赖(最小化服务器常缺)
|
## 远端运行期依赖(最小化服务器常缺)
|
||||||
`sudo apt-get install -y libjsoncpp25 libc-ares2`(deploy.sh 已内置)。
|
`sudo apt-get install -y libjsoncpp25 libc-ares2`(deploy.sh 已内置)。
|
||||||
@@ -28,7 +28,7 @@ description: wind_power_cal 远端部署规范。用于把服务部署到公网
|
|||||||
|
|
||||||
## 校验
|
## 校验
|
||||||
```
|
```
|
||||||
curl -s localhost:8848/api/system/health
|
curl -s localhost:8080/api/system/health
|
||||||
# {"status":0,"msg":"success","data":{"status":"ok"}}
|
# {"status":0,"msg":"success","data":{"status":"ok"}}
|
||||||
```
|
```
|
||||||
查看服务:`ssh ubuntu@82.157.83.226 'systemctl status wind_power'`
|
查看服务:`ssh ubuntu@82.157.83.226 'systemctl status wind_power'`
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ description: wind_power_cal 前端 React+Vite 规范。用于前端页面/接口
|
|||||||
- 后端经 `/api` 前缀;生产由后端托管 SPA(`document_root ./web`)
|
- 后端经 `/api` 前缀;生产由后端托管 SPA(`document_root ./web`)
|
||||||
|
|
||||||
## 构建配置
|
## 构建配置
|
||||||
`vite.config.js`:`base='/'`,dev server :5173,proxy `/api` → `http://localhost:8848`。
|
`vite.config.js`:`base='/'`,dev server :5173,proxy `/api` → `http://localhost:8080`。
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ wind_power_cal/
|
|||||||
## 端口
|
## 端口
|
||||||
| 服务 | 端口 |
|
| 服务 | 端口 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 后端 wind_server | 8848(监听地址见 server_config.json) |
|
| 后端 wind_server | 8080(监听地址见 server_config.json) |
|
||||||
| 前端 dev | 5173(vite proxy `/api` → :8848) |
|
| 前端 dev | 5173(vite proxy `/api` → :8080) |
|
||||||
|
|
||||||
## 脚本入口
|
## 脚本入口
|
||||||
- `./run.sh` 一键打包+前台运行(后端托管 SPA)
|
- `./run.sh` 一键打包+前台运行(后端托管 SPA)
|
||||||
|
|||||||
+20
-1
@@ -1,7 +1,26 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(grep -E \"node_modules|/build/|build_package|^runtime/|^publish/|third_party/drogon/install|\\\\.so$|\\\\.o$\")"
|
"Bash(grep -E \"node_modules|/build/|build_package|^runtime/|^publish/|third_party/drogon/install|\\\\.so$|\\\\.o$\")",
|
||||||
|
"Bash(git commit *)",
|
||||||
|
"Bash(git push *)",
|
||||||
|
"Bash(echo \"=== exit: $? ===\")",
|
||||||
|
"Bash(sshpass -p 'i7568737i~' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 'echo \"=== nginx status ===\"; systemctl is-active nginx 2>/dev/null || service nginx status 2>/dev/null | head -3; echo; echo \"=== all listen ports in nginx config ===\"; sudo grep -rhoE \"listen[^;]*;\" /etc/nginx/ 2>/dev/null | sort | uniq -c')",
|
||||||
|
"Bash(sshpass -p 'i7568737i~' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 'echo \"=== nginx 监听的端口(去重)===\"; sudo nginx -T 2>/dev/null | grep -oE \"listen[[:space:]]+[^;]+\" | sed \"s/listen[[:space:]]*//\" | sed \"s/[[:space:]]*default_server//\" | sed \"s/[[:space:]]*ssl//\" | sort -u; echo; echo \"=== proxy_pass 转发目标(去重)===\"; sudo nginx -T 2>/dev/null | grep -oE \"proxy_pass[[:space:]]+[^;]+\" | sed \"s/proxy_pass[[:space:]]*//\" | sort -u')",
|
||||||
|
"Bash(sshpass -p i7568737i~ ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 ' *)",
|
||||||
|
"Bash(sshpass -p 'i7568737i~' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 'echo \"=== 服务器上 8080 / 8082 占用情况 ===\"; sudo ss -tlnp 2>/dev/null | grep -E \":8080|:8082|:8848|:3000\" ; echo; echo \"=== nginx 对外监听 8080 的 server 块 ===\"; sudo nginx -T 2>/dev/null | grep -B2 -A20 \"listen.*8080\"')",
|
||||||
|
"Bash(sshpass -p 'i7568737i~' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 'echo \"=== IPv4 网卡 ===\"; ip -4 addr show | grep -E \"inet \" | grep -v \"127.0.0.1\"; echo; echo \"=== IPv6 全局地址 ===\"; ip -6 addr show | grep -i \"scope global\" || echo \"\\(无全局 IPv6\\)\"; echo; echo \"=== 现有 listen 10.2.0.8:8081 server 块\\(作为方案A的参照\\) ===\"; sudo nginx -T 2>/dev/null | grep -B1 -A18 \"listen[[:space:]]+10.2.0.8:8081\"; echo; echo \"=== 确认 8080 当前 default_server 标记 ===\"; sudo nginx -T 2>/dev/null | grep -nE \"listen.*8080\"')",
|
||||||
|
"Bash(sshpass -p 'i7568737i~' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 'bash -s')",
|
||||||
|
"Bash(curl -s -m 5 -o /dev/null -w \"HTTP %{http_code} 耗时%{time_total}s\\\\n\" http://82.157.83.226:8080/api/system/health)",
|
||||||
|
"Bash(curl -s -m 5 http://82.157.83.226:8080/api/system/health)",
|
||||||
|
"Bash(echo \"health body: $\\(curl -s -m 5 http://82.157.83.226:8080/api/system/health\\)\")",
|
||||||
|
"Bash(sshpass -p 'i7568737i~' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 ubuntu@82.157.83.226 'sudo nginx -t && sudo systemctl reload nginx && echo \"[nginx] reloaded -> 10.2.0.8:8080 转发 127.0.0.1:8080 \\(中断开始\\)\"')",
|
||||||
|
"Bash(./deploy.sh)",
|
||||||
|
"Bash(curl -s -m5 http://82.157.83.226:8080/api/system/health)",
|
||||||
|
"Bash(curl -s -m5 http://82.157.83.226:8080/api/system/version)",
|
||||||
|
"Bash(curl -s -m5 -o /dev/null -w \"HTTP %{http_code} 类型%{content_type}\\\\n\" http://82.157.83.226:8080/)",
|
||||||
|
"Bash(curl -s -m5 -o /dev/null -w \"8848 HTTP %{http_code} \\(000=不通/拒绝, 符合预期\\)\\\\n\" http://82.157.83.226:8848/)",
|
||||||
|
"Bash(git restore *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ description: 技能建设规范。用于为 wind_power_cal 创建/改写 .agents
|
|||||||
## 规则
|
## 规则
|
||||||
- 每个 skill 一个目录,内含 `SKILL.md`。
|
- 每个 skill 一个目录,内含 `SKILL.md`。
|
||||||
- frontmatter:`name`(kebab-case)、`description`(一句话:**何时触发** + 做什么;用于检索匹配)。
|
- frontmatter:`name`(kebab-case)、`description`(一句话:**何时触发** + 做什么;用于检索匹配)。
|
||||||
- 内容贴合 **wind_power_cal 实际**:C++ Drogon 后端(:8848)、React+Vite 前端、`package.sh / run.sh / deploy.sh`、`third_party` 源码编译 Drogon、响应信封 `{status,msg,data}`。
|
- 内容贴合 **wind_power_cal 实际**:C++ Drogon 后端(:8080)、React+Vite 前端、`package.sh / run.sh / deploy.sh`、`third_party` 源码编译 Drogon、响应信封 `{status,msg,data}`。
|
||||||
- **禁止**原封照搬 edge_collector 等其它项目的 skill——其 OTA / 协议采集 / 边缘主机 / collector / configurator / cloud_server 等内容在本仓库不存在,会误导。
|
- **禁止**原封照搬 edge_collector 等其它项目的 skill——其 OTA / 协议采集 / 边缘主机 / collector / configurator / cloud_server 等内容在本仓库不存在,会误导。
|
||||||
- 用 `[[other-skill-name]]` 链接相关技能。
|
- 用 `[[other-skill-name]]` 链接相关技能。
|
||||||
- 通用工程规范(代码风格、git 提交、shell、前端调试)与项目专属规范(`wind-*`)分开维护。
|
- 通用工程规范(代码风格、git 提交、shell、前端调试)与项目专属规范(`wind-*`)分开维护。
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ SendForbidden(callback); // 无权限(code 3, 403)
|
|||||||
- `main.cpp` 必须 `using namespace drogon;`,否则 `app()` / `HttpResponse` / `CT_TEXT_HTML` 未声明
|
- `main.cpp` 必须 `using namespace drogon;`,否则 `app()` / `HttpResponse` / `CT_TEXT_HTML` 未声明
|
||||||
|
|
||||||
## 配置 / SPA
|
## 配置 / SPA
|
||||||
- `backend/config/server_config.json`:listener(默认 :8848)、CORS、`document_root ./web`
|
- `backend/config/server_config.json`:listener(默认 :8080)、CORS、`document_root ./web`
|
||||||
- SPA 回退:`app().setCustom404Page(HttpResponse::newFileResponse("./web/index.html","",CT_TEXT_HTML), false)`
|
- SPA 回退:`app().setCustom404Page(HttpResponse::newFileResponse("./web/index.html","",CT_TEXT_HTML), false)`
|
||||||
|
|
||||||
## 现有 Controller
|
## 现有 Controller
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ description: wind_power_cal 本地构建与运行规范。用于编译后端、
|
|||||||
`--publish` 额外生成 `publish/wind_power-<ver>.tar.gz`。
|
`--publish` 额外生成 `publish/wind_power-<ver>.tar.gz`。
|
||||||
|
|
||||||
## 运行
|
## 运行
|
||||||
- 一键(后端托管 SPA):`./run.sh`,访问 http://localhost:8848/
|
- 一键(后端托管 SPA):`./run.sh`,访问 http://localhost:8080/
|
||||||
- 仅后端:`bash backend/run.sh`(先 build.sh 再启动,设 `LD_LIBRARY_PATH=libs`)
|
- 仅后端:`bash backend/run.sh`(先 build.sh 再启动,设 `LD_LIBRARY_PATH=libs`)
|
||||||
- 开发(前后端分离热更):
|
- 开发(前后端分离热更):
|
||||||
- 后端 `bash backend/run.sh`(:8848)
|
- 后端 `bash backend/run.sh`(:8080)
|
||||||
- 前端 `cd frontend/web_app && npm install && npm run dev`(:5173,代理 `/api`→:8848)
|
- 前端 `cd frontend/web_app && npm install && npm run dev`(:5173,代理 `/api`→:8080)
|
||||||
|
|
||||||
## 后端构建细节
|
## 后端构建细节
|
||||||
- `backend/CMakeLists.txt`:`find_package(Drogon CONFIG REQUIRED)`,`file(GLOB_RECURSE src/*.cpp)`,链 `Drogon::Drogon`,include `src` + `third_party`。
|
- `backend/CMakeLists.txt`:`find_package(Drogon CONFIG REQUIRED)`,`file(GLOB_RECURSE src/*.cpp)`,链 `Drogon::Drogon`,include `src` + `third_party`。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: wind-deploy
|
name: wind-deploy
|
||||||
description: wind_power_cal 远端部署规范。用于把服务部署到公网主机(默认 82.157.83.226)时,按 deploy.sh 流程操作并校验 :8848。
|
description: wind_power_cal 远端部署规范。用于把服务部署到公网主机(默认 82.157.83.226)时,按 deploy.sh 流程操作并校验 :8080。
|
||||||
---
|
---
|
||||||
|
|
||||||
# 远端部署(deploy.sh)
|
# 远端部署(deploy.sh)
|
||||||
@@ -14,7 +14,7 @@ description: wind_power_cal 远端部署规范。用于把服务部署到公网
|
|||||||
- `User=ubuntu`,`ExecStart=~/wind_power/wind_server`
|
- `User=ubuntu`,`ExecStart=~/wind_power/wind_server`
|
||||||
- `Environment=LD_LIBRARY_PATH=~/wind_power/libs`
|
- `Environment=LD_LIBRARY_PATH=~/wind_power/libs`
|
||||||
- 日志 `logs/server.log`,`Restart=on-failure`
|
- 日志 `logs/server.log`,`Restart=on-failure`
|
||||||
4. 重启 `wind_power`,校验 `localhost:8848`
|
4. 重启 `wind_power`,校验 `localhost:8080`
|
||||||
|
|
||||||
## 远端运行期依赖(最小化服务器常缺)
|
## 远端运行期依赖(最小化服务器常缺)
|
||||||
`sudo apt-get install -y libjsoncpp25 libc-ares2`(deploy.sh 已内置)。
|
`sudo apt-get install -y libjsoncpp25 libc-ares2`(deploy.sh 已内置)。
|
||||||
@@ -28,7 +28,7 @@ description: wind_power_cal 远端部署规范。用于把服务部署到公网
|
|||||||
|
|
||||||
## 校验
|
## 校验
|
||||||
```
|
```
|
||||||
curl -s localhost:8848/api/system/health
|
curl -s localhost:8080/api/system/health
|
||||||
# {"status":0,"msg":"success","data":{"status":"ok"}}
|
# {"status":0,"msg":"success","data":{"status":"ok"}}
|
||||||
```
|
```
|
||||||
查看服务:`ssh ubuntu@82.157.83.226 'systemctl status wind_power'`
|
查看服务:`ssh ubuntu@82.157.83.226 'systemctl status wind_power'`
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ description: wind_power_cal 前端 React+Vite 规范。用于前端页面/接口
|
|||||||
- 后端经 `/api` 前缀;生产由后端托管 SPA(`document_root ./web`)
|
- 后端经 `/api` 前缀;生产由后端托管 SPA(`document_root ./web`)
|
||||||
|
|
||||||
## 构建配置
|
## 构建配置
|
||||||
`vite.config.js`:`base='/'`,dev server :5173,proxy `/api` → `http://localhost:8848`。
|
`vite.config.js`:`base='/'`,dev server :5173,proxy `/api` → `http://localhost:8080`。
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ wind_power_cal/
|
|||||||
## 端口
|
## 端口
|
||||||
| 服务 | 端口 |
|
| 服务 | 端口 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 后端 wind_server | 8848(监听地址见 server_config.json) |
|
| 后端 wind_server | 8080(监听地址见 server_config.json) |
|
||||||
| 前端 dev | 5173(vite proxy `/api` → :8848) |
|
| 前端 dev | 5173(vite proxy `/api` → :8080) |
|
||||||
|
|
||||||
## 脚本入口
|
## 脚本入口
|
||||||
- `./run.sh` 一键打包+前台运行(后端托管 SPA)
|
- `./run.sh` 一键打包+前台运行(后端托管 SPA)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ wind_power_cal/
|
|||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 1. 一键启动(默认监听 :8848)
|
### 1. 一键启动(默认监听 :8080)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./run.sh
|
./run.sh
|
||||||
@@ -35,10 +35,10 @@ wind_power_cal/
|
|||||||
验证:
|
验证:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -s localhost:8848/api/system/health
|
curl -s localhost:8080/api/system/health
|
||||||
# {"data":{"status":"ok"},"msg":"success","status":0}
|
# {"data":{"status":"ok"},"msg":"success","status":0}
|
||||||
|
|
||||||
curl -s localhost:8848/api/system/version
|
curl -s localhost:8080/api/system/version
|
||||||
# {"data":{"name":"wind_power_cal","version":"0.1.0"},"msg":"success","status":0}
|
# {"data":{"name":"wind_power_cal","version":"0.1.0"},"msg":"success","status":0}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -50,17 +50,17 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
打开 http://localhost:5173 ,首页通过 Vite 代理 (`/api` → `localhost:8848`) 访问后端接口。
|
打开 http://localhost:5173 ,首页通过 Vite 代理 (`/api` → `localhost:8080`) 访问后端接口。
|
||||||
|
|
||||||
### 3. 生产联调
|
### 3. 生产联调
|
||||||
|
|
||||||
统一使用根目录 `./run.sh`。脚本会构建前端并把 `dist/` 同步到 `runtime/wind_power/web/`,由后端托管 SPA;访问 http://localhost:8848/ 直接返回前端页面。
|
统一使用根目录 `./run.sh`。脚本会构建前端并把 `dist/` 同步到 `runtime/wind_power/web/`,由后端托管 SPA;访问 http://localhost:8080/ 直接返回前端页面。
|
||||||
|
|
||||||
## 端口
|
## 端口
|
||||||
|
|
||||||
| 服务 | 端口 |
|
| 服务 | 端口 |
|
||||||
| ------ | ----- |
|
| ------ | ----- |
|
||||||
| 后端 | 8848 |
|
| 后端 | 8080 |
|
||||||
| 前端 dev | 5173 |
|
| 前端 dev | 5173 |
|
||||||
|
|
||||||
## 说明
|
## 说明
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"listeners": [
|
"listeners": [
|
||||||
{
|
{
|
||||||
"address": "0.0.0.0",
|
"address": "127.0.0.1",
|
||||||
"port": 8848,
|
"port": 8080,
|
||||||
"https": false
|
"https": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"allow_origins": [
|
"allow_origins": [
|
||||||
"http://localhost:5173",
|
"http://localhost:5173",
|
||||||
"http://localhost:8848",
|
"http://localhost:8080",
|
||||||
"http://127.0.0.1:8848"
|
"http://127.0.0.1:8080"
|
||||||
],
|
],
|
||||||
"allow_methods": [
|
"allow_methods": [
|
||||||
"GET",
|
"GET",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <deque>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
@@ -23,6 +24,13 @@ namespace {
|
|||||||
constexpr int kErrorInvalidRequest = 1001;
|
constexpr int kErrorInvalidRequest = 1001;
|
||||||
constexpr int kErrorJobNotFound = 1002;
|
constexpr int kErrorJobNotFound = 1002;
|
||||||
constexpr int kErrorServer = 1003;
|
constexpr int kErrorServer = 1003;
|
||||||
|
constexpr const char* kDefaultSchemeId = "scheme_one";
|
||||||
|
constexpr const char* kSchemeTwoId = "scheme_two";
|
||||||
|
constexpr const char* kSchemeOneName = "方案一";
|
||||||
|
constexpr const char* kSchemeTwoName = "方案二";
|
||||||
|
constexpr const char* kSchemeOneDefaultDescription = "通用方案";
|
||||||
|
constexpr const char* kSchemeTwoDefaultDescription =
|
||||||
|
"桨角筛选方案,使用三支叶片角度平均值、600s 平均风速和手填转速/功率参数";
|
||||||
|
|
||||||
struct RawRow {
|
struct RawRow {
|
||||||
std::string time;
|
std::string time;
|
||||||
@@ -40,6 +48,11 @@ struct ValidRow {
|
|||||||
double wind_speed = 0.0;
|
double wind_speed = 0.0;
|
||||||
double active_power = 0.0;
|
double active_power = 0.0;
|
||||||
double generator_speed = 0.0;
|
double generator_speed = 0.0;
|
||||||
|
double blade_pitch_1 = 0.0;
|
||||||
|
double blade_pitch_2 = 0.0;
|
||||||
|
double blade_pitch_3 = 0.0;
|
||||||
|
double pitch_angle_average = 0.0;
|
||||||
|
double wind_speed_600s_average = 0.0;
|
||||||
double tip_speed_ratio = 0.0;
|
double tip_speed_ratio = 0.0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,6 +62,7 @@ struct RemovedPoint {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct CalculationOptions {
|
struct CalculationOptions {
|
||||||
|
std::string scheme_id = kDefaultSchemeId;
|
||||||
double rated_power = 4800.0;
|
double rated_power = 4800.0;
|
||||||
double rated_wind_speed = 18.0;
|
double rated_wind_speed = 18.0;
|
||||||
double power_step = 5.0;
|
double power_step = 5.0;
|
||||||
@@ -59,6 +73,11 @@ struct CalculationOptions {
|
|||||||
double iqr_upper_multiplier = 2.0;
|
double iqr_upper_multiplier = 2.0;
|
||||||
double minimum_generator_speed = 1.0;
|
double minimum_generator_speed = 1.0;
|
||||||
double generator_speed_k = 0.9;
|
double generator_speed_k = 0.9;
|
||||||
|
double grid_connected_speed = 0.0;
|
||||||
|
double rated_generator_speed = 0.0;
|
||||||
|
bool rated_power_provided = false;
|
||||||
|
bool grid_connected_speed_provided = false;
|
||||||
|
bool rated_generator_speed_provided = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct EstimatedParams {
|
struct EstimatedParams {
|
||||||
@@ -73,6 +92,15 @@ struct CurveBin {
|
|||||||
size_t sample_count = 0;
|
size_t sample_count = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct SchemeInfo {
|
||||||
|
std::string id;
|
||||||
|
std::string name;
|
||||||
|
std::string description;
|
||||||
|
double grid_connected_speed = 1030.0;
|
||||||
|
double rated_generator_speed = 1755.0;
|
||||||
|
double rated_power = 2000.0;
|
||||||
|
};
|
||||||
|
|
||||||
std::string Trim(const std::string& value) {
|
std::string Trim(const std::string& value) {
|
||||||
const auto begin = value.find_first_not_of(" \t\r\n");
|
const auto begin = value.find_first_not_of(" \t\r\n");
|
||||||
if (begin == std::string::npos) {
|
if (begin == std::string::npos) {
|
||||||
@@ -103,6 +131,10 @@ fs::path JobRowsPath(const std::string& job_id) {
|
|||||||
return JobDir(job_id) / "rows.jsonl";
|
return JobDir(job_id) / "rows.jsonl";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fs::path SchemeConfigPath() {
|
||||||
|
return fs::path("data") / "wind_schemes.json";
|
||||||
|
}
|
||||||
|
|
||||||
std::string GenerateJobId() {
|
std::string GenerateJobId() {
|
||||||
const auto now = std::chrono::system_clock::now().time_since_epoch().count();
|
const auto now = std::chrono::system_clock::now().time_since_epoch().count();
|
||||||
std::random_device rd;
|
std::random_device rd;
|
||||||
@@ -148,6 +180,117 @@ std::optional<std::string> GetStringField(const json& body, const std::string& f
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<double> GetNumberField(const json& body, const std::string& field) {
|
||||||
|
if (!body.contains(field) || !body[field].is_number()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return body[field].get<double>();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string NormalizeSchemeId(const std::string& scheme_id) {
|
||||||
|
return scheme_id == kSchemeTwoId ? kSchemeTwoId : kDefaultSchemeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<SchemeInfo> DefaultSchemes() {
|
||||||
|
return {
|
||||||
|
{kDefaultSchemeId, kSchemeOneName, kSchemeOneDefaultDescription},
|
||||||
|
{kSchemeTwoId, kSchemeTwoName, kSchemeTwoDefaultDescription, 1030.0, 1755.0, 2000.0},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<SchemeInfo> FindScheme(const std::vector<SchemeInfo>& schemes,
|
||||||
|
const std::string& scheme_id) {
|
||||||
|
const std::string normalized_id = NormalizeSchemeId(scheme_id);
|
||||||
|
for (const auto& scheme : schemes) {
|
||||||
|
if (scheme.id == normalized_id) {
|
||||||
|
return scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
json SchemeToJson(const SchemeInfo& scheme) {
|
||||||
|
json data;
|
||||||
|
data["id"] = scheme.id;
|
||||||
|
data["name"] = scheme.name;
|
||||||
|
data["description"] = scheme.description;
|
||||||
|
if (scheme.id == kSchemeTwoId) {
|
||||||
|
data["parameters"]["grid_connected_speed"] = scheme.grid_connected_speed;
|
||||||
|
data["parameters"]["rated_generator_speed"] = scheme.rated_generator_speed;
|
||||||
|
data["parameters"]["rated_power"] = scheme.rated_power;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<SchemeInfo> LoadSchemes() {
|
||||||
|
auto schemes = DefaultSchemes();
|
||||||
|
const auto config_path = SchemeConfigPath();
|
||||||
|
if (!fs::exists(config_path)) {
|
||||||
|
return schemes;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::ifstream in(config_path);
|
||||||
|
const auto saved = json::parse(in);
|
||||||
|
if (!saved.contains("schemes") || !saved["schemes"].is_array()) {
|
||||||
|
return schemes;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& scheme : schemes) {
|
||||||
|
for (const auto& item : saved["schemes"]) {
|
||||||
|
const auto saved_id = GetStringField(item, "id");
|
||||||
|
if (!saved_id.has_value() || saved_id.value() != scheme.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto description = GetStringField(item, "description");
|
||||||
|
if (description.has_value()) {
|
||||||
|
scheme.description = description.value();
|
||||||
|
}
|
||||||
|
if (scheme.id == kSchemeTwoId &&
|
||||||
|
item.contains("parameters") &&
|
||||||
|
item["parameters"].is_object()) {
|
||||||
|
const auto& params = item["parameters"];
|
||||||
|
if (const auto value = GetNumberField(params, "grid_connected_speed");
|
||||||
|
value.has_value() && value.value() > 0.0) {
|
||||||
|
scheme.grid_connected_speed = value.value();
|
||||||
|
}
|
||||||
|
if (const auto value = GetNumberField(params, "rated_generator_speed");
|
||||||
|
value.has_value() && value.value() > 0.0) {
|
||||||
|
scheme.rated_generator_speed = value.value();
|
||||||
|
}
|
||||||
|
if (const auto value = GetNumberField(params, "rated_power");
|
||||||
|
value.has_value() && value.value() > 0.0) {
|
||||||
|
scheme.rated_power = value.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return schemes;
|
||||||
|
}
|
||||||
|
return schemes;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SaveSchemes(const std::vector<SchemeInfo>& schemes) {
|
||||||
|
try {
|
||||||
|
const auto config_path = SchemeConfigPath();
|
||||||
|
fs::create_directories(config_path.parent_path());
|
||||||
|
|
||||||
|
json data;
|
||||||
|
data["default_scheme_id"] = kDefaultSchemeId;
|
||||||
|
data["schemes"] = json::array();
|
||||||
|
for (const auto& scheme : schemes) {
|
||||||
|
data["schemes"].push_back(SchemeToJson(scheme));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream out(config_path);
|
||||||
|
out << data.dump(2);
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<double> GetDoubleField(const json& body, const std::string& field) {
|
std::optional<double> GetDoubleField(const json& body, const std::string& field) {
|
||||||
if (!body.contains(field)) {
|
if (!body.contains(field)) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -261,9 +404,13 @@ CalculationOptions ParseOptions(const json& body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto& opt = body["options"];
|
const auto& opt = body["options"];
|
||||||
|
if (const auto value = GetStringField(opt, "scheme_id"); value.has_value()) {
|
||||||
|
options.scheme_id = NormalizeSchemeId(value.value());
|
||||||
|
}
|
||||||
if (const auto value = GetDoubleField(opt, "rated_power");
|
if (const auto value = GetDoubleField(opt, "rated_power");
|
||||||
value.has_value() && value.value() > 0.0) {
|
value.has_value() && value.value() > 0.0) {
|
||||||
options.rated_power = value.value();
|
options.rated_power = value.value();
|
||||||
|
options.rated_power_provided = true;
|
||||||
}
|
}
|
||||||
if (const auto value = GetDoubleField(opt, "rated_wind_speed");
|
if (const auto value = GetDoubleField(opt, "rated_wind_speed");
|
||||||
value.has_value() && value.value() > 0.0) {
|
value.has_value() && value.value() > 0.0) {
|
||||||
@@ -301,13 +448,43 @@ CalculationOptions ParseOptions(const json& body) {
|
|||||||
value.has_value() && value.value() >= 0.0) {
|
value.has_value() && value.value() >= 0.0) {
|
||||||
options.generator_speed_k = value.value();
|
options.generator_speed_k = value.value();
|
||||||
}
|
}
|
||||||
|
if (const auto value = GetDoubleField(opt, "grid_connected_speed");
|
||||||
|
value.has_value() && value.value() > 0.0) {
|
||||||
|
options.grid_connected_speed = value.value();
|
||||||
|
options.grid_connected_speed_provided = true;
|
||||||
|
}
|
||||||
|
if (const auto value = GetDoubleField(opt, "rated_generator_speed");
|
||||||
|
value.has_value() && value.value() > 0.0) {
|
||||||
|
options.rated_generator_speed = value.value();
|
||||||
|
options.rated_generator_speed_provided = true;
|
||||||
|
}
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsSchemeTwo(const CalculationOptions& options) {
|
||||||
|
return options.scheme_id == kSchemeTwoId;
|
||||||
|
}
|
||||||
|
|
||||||
void AddInvalid(std::unordered_map<std::string, int>& counters, const std::string& reason) {
|
void AddInvalid(std::unordered_map<std::string, int>& counters, const std::string& reason) {
|
||||||
counters[reason] += 1;
|
counters[reason] += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputeWindSpeed600sAverage(std::vector<ValidRow>& rows) {
|
||||||
|
std::deque<ValidRow> window;
|
||||||
|
double wind_speed_sum = 0.0;
|
||||||
|
for (auto& row : rows) {
|
||||||
|
while (!window.empty() &&
|
||||||
|
std::difftime(row.timestamp, window.front().timestamp) > 600.0) {
|
||||||
|
wind_speed_sum -= window.front().wind_speed;
|
||||||
|
window.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.push_back(row);
|
||||||
|
wind_speed_sum += row.wind_speed;
|
||||||
|
row.wind_speed_600s_average = wind_speed_sum / static_cast<double>(window.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<ValidRow> FilterLimitPower(const std::vector<ValidRow>& rows,
|
std::vector<ValidRow> FilterLimitPower(const std::vector<ValidRow>& rows,
|
||||||
const CalculationOptions& options,
|
const CalculationOptions& options,
|
||||||
int& removed_count,
|
int& removed_count,
|
||||||
@@ -362,6 +539,68 @@ std::vector<ValidRow> FilterLimitPower(const std::vector<ValidRow>& rows,
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<ValidRow> FilterSchemeTwoGridSpeed(const std::vector<ValidRow>& rows,
|
||||||
|
const CalculationOptions& options,
|
||||||
|
int& removed_count,
|
||||||
|
std::vector<RemovedPoint>& removed_points) {
|
||||||
|
removed_count = 0;
|
||||||
|
if (!IsSchemeTwo(options) || rows.empty()) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ValidRow> result;
|
||||||
|
result.reserve(rows.size());
|
||||||
|
for (const auto& row : rows) {
|
||||||
|
if (row.generator_speed <= options.grid_connected_speed) {
|
||||||
|
++removed_count;
|
||||||
|
removed_points.push_back(RemovedPoint{row, "scheme_two_grid_speed_low"});
|
||||||
|
} else {
|
||||||
|
result.push_back(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ValidRow> FilterSchemeTwoPitchRules(
|
||||||
|
const std::vector<ValidRow>& rows,
|
||||||
|
const CalculationOptions& options,
|
||||||
|
int& low_power_pitch_wind_count,
|
||||||
|
int& low_speed_pitch_count,
|
||||||
|
int& low_power_pitch_count,
|
||||||
|
std::vector<RemovedPoint>& removed_points) {
|
||||||
|
low_power_pitch_wind_count = 0;
|
||||||
|
low_speed_pitch_count = 0;
|
||||||
|
low_power_pitch_count = 0;
|
||||||
|
if (!IsSchemeTwo(options) || rows.empty()) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ValidRow> result;
|
||||||
|
result.reserve(rows.size());
|
||||||
|
for (const auto& row : rows) {
|
||||||
|
if (row.active_power < options.rated_power &&
|
||||||
|
row.pitch_angle_average > 5.0 &&
|
||||||
|
row.wind_speed_600s_average < 15.0) {
|
||||||
|
++low_power_pitch_wind_count;
|
||||||
|
removed_points.push_back(RemovedPoint{row, "scheme_two_low_power_pitch_wind"});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (row.generator_speed < options.rated_generator_speed &&
|
||||||
|
row.pitch_angle_average > 5.0) {
|
||||||
|
++low_speed_pitch_count;
|
||||||
|
removed_points.push_back(RemovedPoint{row, "scheme_two_low_speed_pitch"});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (row.active_power < 1980.0 && row.pitch_angle_average > 2.0) {
|
||||||
|
++low_power_pitch_count;
|
||||||
|
removed_points.push_back(RemovedPoint{row, "scheme_two_low_power_pitch"});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push_back(row);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
template <typename ValueGetter>
|
template <typename ValueGetter>
|
||||||
std::vector<ValidRow> FilterByWindBinIqr(const std::vector<ValidRow>& rows,
|
std::vector<ValidRow> FilterByWindBinIqr(const std::vector<ValidRow>& rows,
|
||||||
const CalculationOptions& options,
|
const CalculationOptions& options,
|
||||||
@@ -674,6 +913,100 @@ std::string DatePart(const std::string& time_text) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
void WindPowerController::GetSchemes(
|
||||||
|
const HttpRequestPtr&,
|
||||||
|
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||||
|
json data;
|
||||||
|
data["default_scheme_id"] = kDefaultSchemeId;
|
||||||
|
data["schemes"] = json::array();
|
||||||
|
for (const auto& scheme : LoadSchemes()) {
|
||||||
|
data["schemes"].push_back(SchemeToJson(scheme));
|
||||||
|
}
|
||||||
|
SendSuccess(callback, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindPowerController::SaveSchemeDescription(
|
||||||
|
const HttpRequestPtr& req,
|
||||||
|
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||||
|
const std::string& scheme_id) {
|
||||||
|
const std::string normalized_id = NormalizeSchemeId(scheme_id);
|
||||||
|
if (scheme_id != normalized_id) {
|
||||||
|
SendError(callback, kErrorInvalidRequest, "方案不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const auto body = ParseBody(req, error);
|
||||||
|
if (!body.has_value()) {
|
||||||
|
SendError(callback, kErrorInvalidRequest, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto description = GetStringField(body.value(), "description");
|
||||||
|
if (!description.has_value()) {
|
||||||
|
SendError(callback, kErrorInvalidRequest, "方案描述不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (description.value().size() > 1000) {
|
||||||
|
SendError(callback, kErrorInvalidRequest, "方案描述不能超过 1000 个字符");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<double> grid_connected_speed;
|
||||||
|
std::optional<double> rated_generator_speed;
|
||||||
|
std::optional<double> rated_power;
|
||||||
|
if (normalized_id == kSchemeTwoId && body.value().contains("parameters")) {
|
||||||
|
if (!body.value()["parameters"].is_object()) {
|
||||||
|
SendError(callback, kErrorInvalidRequest, "方案参数格式错误");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& params = body.value()["parameters"];
|
||||||
|
grid_connected_speed = GetDoubleField(params, "grid_connected_speed");
|
||||||
|
rated_generator_speed = GetDoubleField(params, "rated_generator_speed");
|
||||||
|
rated_power = GetDoubleField(params, "rated_power");
|
||||||
|
if (!grid_connected_speed.has_value() || grid_connected_speed.value() <= 0.0 ||
|
||||||
|
!rated_generator_speed.has_value() || rated_generator_speed.value() <= 0.0 ||
|
||||||
|
!rated_power.has_value() || rated_power.value() <= 0.0) {
|
||||||
|
SendError(callback, kErrorInvalidRequest, "方案二参数必须为正数");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto schemes = LoadSchemes();
|
||||||
|
bool updated = false;
|
||||||
|
for (auto& scheme : schemes) {
|
||||||
|
if (scheme.id == normalized_id) {
|
||||||
|
scheme.description = description.value();
|
||||||
|
if (scheme.id == kSchemeTwoId) {
|
||||||
|
if (grid_connected_speed.has_value()) {
|
||||||
|
scheme.grid_connected_speed = grid_connected_speed.value();
|
||||||
|
}
|
||||||
|
if (rated_generator_speed.has_value()) {
|
||||||
|
scheme.rated_generator_speed = rated_generator_speed.value();
|
||||||
|
}
|
||||||
|
if (rated_power.has_value()) {
|
||||||
|
scheme.rated_power = rated_power.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!updated) {
|
||||||
|
SendError(callback, kErrorInvalidRequest, "方案不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!SaveSchemes(schemes)) {
|
||||||
|
SendError(callback, kErrorServer, "保存方案描述失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto saved_scheme = FindScheme(schemes, normalized_id);
|
||||||
|
json data;
|
||||||
|
data["scheme"] = SchemeToJson(saved_scheme.value());
|
||||||
|
SendSuccess(callback, data);
|
||||||
|
}
|
||||||
|
|
||||||
void WindPowerController::StartJob(
|
void WindPowerController::StartJob(
|
||||||
const HttpRequestPtr& req,
|
const HttpRequestPtr& req,
|
||||||
std::function<void(const HttpResponsePtr&)>&& callback) {
|
std::function<void(const HttpResponsePtr&)>&& callback) {
|
||||||
@@ -769,6 +1102,14 @@ void WindPowerController::FinishJob(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CalculationOptions options = ParseOptions(*body);
|
const CalculationOptions options = ParseOptions(*body);
|
||||||
|
if (IsSchemeTwo(options) &&
|
||||||
|
(!options.rated_power_provided ||
|
||||||
|
!options.grid_connected_speed_provided ||
|
||||||
|
!options.rated_generator_speed_provided)) {
|
||||||
|
SendError(callback, kErrorInvalidRequest, "方案二需要填写并网转速、额定转速和额定功率");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<ValidRow> parsed_rows;
|
std::vector<ValidRow> parsed_rows;
|
||||||
std::unordered_map<std::string, int> invalid_reasons;
|
std::unordered_map<std::string, int> invalid_reasons;
|
||||||
int raw_rows = 0;
|
int raw_rows = 0;
|
||||||
@@ -795,6 +1136,9 @@ void WindPowerController::FinishJob(
|
|||||||
const auto wind_speed = GetDoubleField(row, "wind_speed");
|
const auto wind_speed = GetDoubleField(row, "wind_speed");
|
||||||
const auto active_power = GetDoubleField(row, "active_power");
|
const auto active_power = GetDoubleField(row, "active_power");
|
||||||
const auto generator_speed = GetDoubleField(row, "generator_speed");
|
const auto generator_speed = GetDoubleField(row, "generator_speed");
|
||||||
|
const auto blade_pitch_1 = GetDoubleField(row, "blade_pitch_1");
|
||||||
|
const auto blade_pitch_2 = GetDoubleField(row, "blade_pitch_2");
|
||||||
|
const auto blade_pitch_3 = GetDoubleField(row, "blade_pitch_3");
|
||||||
|
|
||||||
if (!active_power.has_value() || !std::isfinite(active_power.value()) ||
|
if (!active_power.has_value() || !std::isfinite(active_power.value()) ||
|
||||||
active_power.value() <= 0.0) {
|
active_power.value() <= 0.0) {
|
||||||
@@ -807,6 +1151,13 @@ void WindPowerController::FinishJob(
|
|||||||
AddInvalid(invalid_reasons, "invalid_generator_speed");
|
AddInvalid(invalid_reasons, "invalid_generator_speed");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (IsSchemeTwo(options) &&
|
||||||
|
(!blade_pitch_1.has_value() || !std::isfinite(blade_pitch_1.value()) ||
|
||||||
|
!blade_pitch_2.has_value() || !std::isfinite(blade_pitch_2.value()) ||
|
||||||
|
!blade_pitch_3.has_value() || !std::isfinite(blade_pitch_3.value()))) {
|
||||||
|
AddInvalid(invalid_reasons, "invalid_pitch_angle");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!time_text.has_value()) {
|
if (!time_text.has_value()) {
|
||||||
AddInvalid(invalid_reasons, "invalid_time");
|
AddInvalid(invalid_reasons, "invalid_time");
|
||||||
continue;
|
continue;
|
||||||
@@ -834,6 +1185,15 @@ void WindPowerController::FinishJob(
|
|||||||
valid_row.wind_speed = wind_speed.value();
|
valid_row.wind_speed = wind_speed.value();
|
||||||
valid_row.active_power = active_power.value();
|
valid_row.active_power = active_power.value();
|
||||||
valid_row.generator_speed = generator_speed.value();
|
valid_row.generator_speed = generator_speed.value();
|
||||||
|
if (IsSchemeTwo(options)) {
|
||||||
|
valid_row.blade_pitch_1 = blade_pitch_1.value();
|
||||||
|
valid_row.blade_pitch_2 = blade_pitch_2.value();
|
||||||
|
valid_row.blade_pitch_3 = blade_pitch_3.value();
|
||||||
|
valid_row.pitch_angle_average =
|
||||||
|
(valid_row.blade_pitch_1 +
|
||||||
|
valid_row.blade_pitch_2 +
|
||||||
|
valid_row.blade_pitch_3) / 3.0;
|
||||||
|
}
|
||||||
parsed_rows.push_back(valid_row);
|
parsed_rows.push_back(valid_row);
|
||||||
}
|
}
|
||||||
} catch (const std::exception&) {
|
} catch (const std::exception&) {
|
||||||
@@ -889,13 +1249,27 @@ void WindPowerController::FinishJob(
|
|||||||
int high_wind_low_power_count = 0;
|
int high_wind_low_power_count = 0;
|
||||||
int curve_residual_outlier_count = 0;
|
int curve_residual_outlier_count = 0;
|
||||||
int rated_plateau_low_power_count = 0;
|
int rated_plateau_low_power_count = 0;
|
||||||
|
int scheme_two_grid_speed_low_count = 0;
|
||||||
|
int scheme_two_low_power_pitch_wind_count = 0;
|
||||||
|
int scheme_two_low_speed_pitch_count = 0;
|
||||||
|
int scheme_two_low_power_pitch_count = 0;
|
||||||
int cleaned_rows_count = 0;
|
int cleaned_rows_count = 0;
|
||||||
for (const auto& fan_id : fan_ids) {
|
for (const auto& fan_id : fan_ids) {
|
||||||
fans.push_back(fan_id);
|
fans.push_back(fan_id);
|
||||||
auto fan_rows = rows_by_fan[fan_id];
|
auto fan_rows = rows_by_fan[fan_id];
|
||||||
std::vector<RemovedPoint> fan_removed_points;
|
std::vector<RemovedPoint> fan_removed_points;
|
||||||
|
if (IsSchemeTwo(options)) {
|
||||||
|
ComputeWindSpeed600sAverage(fan_rows);
|
||||||
|
}
|
||||||
|
|
||||||
int removed = 0;
|
int removed = 0;
|
||||||
|
fan_rows = FilterSchemeTwoGridSpeed(
|
||||||
|
fan_rows,
|
||||||
|
options,
|
||||||
|
removed,
|
||||||
|
fan_removed_points);
|
||||||
|
scheme_two_grid_speed_low_count += removed;
|
||||||
|
|
||||||
fan_rows = FilterLimitPower(fan_rows, options, removed, fan_removed_points);
|
fan_rows = FilterLimitPower(fan_rows, options, removed, fan_removed_points);
|
||||||
limit_power_count += removed;
|
limit_power_count += removed;
|
||||||
|
|
||||||
@@ -951,13 +1325,31 @@ void WindPowerController::FinishJob(
|
|||||||
fan_removed_points);
|
fan_removed_points);
|
||||||
rated_plateau_low_power_count += removed;
|
rated_plateau_low_power_count += removed;
|
||||||
|
|
||||||
|
int low_power_pitch_wind_removed = 0;
|
||||||
|
int low_speed_pitch_removed = 0;
|
||||||
|
int low_power_pitch_removed = 0;
|
||||||
|
fan_rows = FilterSchemeTwoPitchRules(
|
||||||
|
fan_rows,
|
||||||
|
options,
|
||||||
|
low_power_pitch_wind_removed,
|
||||||
|
low_speed_pitch_removed,
|
||||||
|
low_power_pitch_removed,
|
||||||
|
fan_removed_points);
|
||||||
|
scheme_two_low_power_pitch_wind_count += low_power_pitch_wind_removed;
|
||||||
|
scheme_two_low_speed_pitch_count += low_speed_pitch_removed;
|
||||||
|
scheme_two_low_power_pitch_count += low_power_pitch_removed;
|
||||||
|
|
||||||
cleaned_rows_count += static_cast<int>(fan_rows.size());
|
cleaned_rows_count += static_cast<int>(fan_rows.size());
|
||||||
|
|
||||||
json fan_scatter = json::array();
|
json fan_scatter = json::array();
|
||||||
for (const auto& row : fan_rows) {
|
for (const auto& row : fan_rows) {
|
||||||
json point;
|
json point;
|
||||||
|
point["fan_id"] = row.fan_id;
|
||||||
|
point["time"] = row.time;
|
||||||
point["wind_speed"] = row.wind_speed;
|
point["wind_speed"] = row.wind_speed;
|
||||||
point["active_power"] = row.active_power;
|
point["active_power"] = row.active_power;
|
||||||
|
point["generator_speed"] = row.generator_speed;
|
||||||
|
point["pitch_angle_average"] = row.pitch_angle_average;
|
||||||
fan_scatter.push_back(point);
|
fan_scatter.push_back(point);
|
||||||
}
|
}
|
||||||
scatter_points[fan_id] = fan_scatter;
|
scatter_points[fan_id] = fan_scatter;
|
||||||
@@ -1012,6 +1404,11 @@ void WindPowerController::FinishJob(
|
|||||||
invalid_reasons["high_wind_low_power"] = high_wind_low_power_count;
|
invalid_reasons["high_wind_low_power"] = high_wind_low_power_count;
|
||||||
invalid_reasons["curve_residual_outlier"] = curve_residual_outlier_count;
|
invalid_reasons["curve_residual_outlier"] = curve_residual_outlier_count;
|
||||||
invalid_reasons["rated_plateau_low_power"] = rated_plateau_low_power_count;
|
invalid_reasons["rated_plateau_low_power"] = rated_plateau_low_power_count;
|
||||||
|
invalid_reasons["scheme_two_grid_speed_low"] = scheme_two_grid_speed_low_count;
|
||||||
|
invalid_reasons["scheme_two_low_power_pitch_wind"] =
|
||||||
|
scheme_two_low_power_pitch_wind_count;
|
||||||
|
invalid_reasons["scheme_two_low_speed_pitch"] = scheme_two_low_speed_pitch_count;
|
||||||
|
invalid_reasons["scheme_two_low_power_pitch"] = scheme_two_low_power_pitch_count;
|
||||||
|
|
||||||
const int invalid_total = raw_rows - cleaned_rows_count;
|
const int invalid_total = raw_rows - cleaned_rows_count;
|
||||||
|
|
||||||
@@ -1026,10 +1423,17 @@ void WindPowerController::FinishJob(
|
|||||||
summary["high_wind_low_power_rows"] = high_wind_low_power_count;
|
summary["high_wind_low_power_rows"] = high_wind_low_power_count;
|
||||||
summary["curve_residual_outlier_rows"] = curve_residual_outlier_count;
|
summary["curve_residual_outlier_rows"] = curve_residual_outlier_count;
|
||||||
summary["rated_plateau_low_power_rows"] = rated_plateau_low_power_count;
|
summary["rated_plateau_low_power_rows"] = rated_plateau_low_power_count;
|
||||||
|
summary["scheme_two_grid_speed_low_rows"] = scheme_two_grid_speed_low_count;
|
||||||
|
summary["scheme_two_low_power_pitch_wind_rows"] =
|
||||||
|
scheme_two_low_power_pitch_wind_count;
|
||||||
|
summary["scheme_two_low_speed_pitch_rows"] = scheme_two_low_speed_pitch_count;
|
||||||
|
summary["scheme_two_low_power_pitch_rows"] = scheme_two_low_power_pitch_count;
|
||||||
summary["invalid_reasons"] = CounterJson(invalid_reasons);
|
summary["invalid_reasons"] = CounterJson(invalid_reasons);
|
||||||
summary["fan_count"] = fans.size();
|
summary["fan_count"] = fans.size();
|
||||||
|
|
||||||
json data;
|
json data;
|
||||||
|
const auto selected_scheme = FindScheme(LoadSchemes(), options.scheme_id);
|
||||||
|
data["scheme"] = SchemeToJson(selected_scheme.value());
|
||||||
data["summary"] = summary;
|
data["summary"] = summary;
|
||||||
data["fans"] = fans;
|
data["fans"] = fans;
|
||||||
data["curves"] = curves;
|
data["curves"] = curves;
|
||||||
|
|||||||
@@ -15,8 +15,17 @@ public:
|
|||||||
ADD_METHOD_TO(WindPowerController::UploadChunk, "/api/wind/jobs/chunk", Post);
|
ADD_METHOD_TO(WindPowerController::UploadChunk, "/api/wind/jobs/chunk", Post);
|
||||||
ADD_METHOD_TO(WindPowerController::FinishJob, "/api/wind/jobs/finish", Post);
|
ADD_METHOD_TO(WindPowerController::FinishJob, "/api/wind/jobs/finish", Post);
|
||||||
ADD_METHOD_TO(WindPowerController::DeleteJob, "/api/wind/jobs/{1}", Delete);
|
ADD_METHOD_TO(WindPowerController::DeleteJob, "/api/wind/jobs/{1}", Delete);
|
||||||
|
ADD_METHOD_TO(WindPowerController::GetSchemes, "/api/wind/schemes", Get);
|
||||||
|
ADD_METHOD_TO(WindPowerController::SaveSchemeDescription,
|
||||||
|
"/api/wind/schemes/{1}/description",
|
||||||
|
Post);
|
||||||
METHOD_LIST_END
|
METHOD_LIST_END
|
||||||
|
|
||||||
|
void GetSchemes(const HttpRequestPtr& req,
|
||||||
|
std::function<void(const HttpResponsePtr&)>&& callback);
|
||||||
|
void SaveSchemeDescription(const HttpRequestPtr& req,
|
||||||
|
std::function<void(const HttpResponsePtr&)>&& callback,
|
||||||
|
const std::string& scheme_id);
|
||||||
void StartJob(const HttpRequestPtr& req,
|
void StartJob(const HttpRequestPtr& req,
|
||||||
std::function<void(const HttpResponsePtr&)>&& callback);
|
std::function<void(const HttpResponsePtr&)>&& callback);
|
||||||
void UploadChunk(const HttpRequestPtr& req,
|
void UploadChunk(const HttpRequestPtr& req,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# 1. 本地执行 package.sh 生成 runtime/wind_power/
|
# 1. 本地执行 package.sh 生成 runtime/wind_power/
|
||||||
# 2. rsync runtime/wind_power/ → ~/wind_power/
|
# 2. rsync runtime/wind_power/ → ~/wind_power/
|
||||||
# 3. 安装/更新 systemd 服务 wind_power.service
|
# 3. 安装/更新 systemd 服务 wind_power.service
|
||||||
# 4. 重启 wind_power,并验证 8848 直连接口
|
# 4. 重启 wind_power,并验证 8080 直连接口
|
||||||
#
|
#
|
||||||
# 不安装、不配置 nginx;不修改 gitea。
|
# 不安装、不配置 nginx;不修改 gitea。
|
||||||
#
|
#
|
||||||
@@ -114,17 +114,17 @@ sudo systemctl restart "${SERVICE_NAME}"
|
|||||||
echo "[remote] systemd ${SERVICE_NAME} 已安装并启动"
|
echo "[remote] systemd ${SERVICE_NAME} 已安装并启动"
|
||||||
REMOTE
|
REMOTE
|
||||||
|
|
||||||
step "5/5 校验 8848 直连服务..."
|
step "5/5 校验 8080 直连服务..."
|
||||||
sleep 2
|
sleep 2
|
||||||
ssh_cmd "SERVICE_NAME='${SERVICE_NAME}'" 'bash -s' <<'REMOTE' | sed 's/^/[remote] /'
|
ssh_cmd "SERVICE_NAME='${SERVICE_NAME}'" 'bash -s' <<'REMOTE' | sed 's/^/[remote] /'
|
||||||
set +e
|
set +e
|
||||||
echo "service: $(systemctl is-active ${SERVICE_NAME})"
|
echo "service: $(systemctl is-active ${SERVICE_NAME})"
|
||||||
echo "health: $(curl -s -m 5 localhost:8848/api/system/health)"
|
echo "health: $(curl -s -m 5 localhost:8080/api/system/health)"
|
||||||
echo "version: $(curl -s -m 5 localhost:8848/api/system/version)"
|
echo "version: $(curl -s -m 5 localhost:8080/api/system/version)"
|
||||||
echo "listen: $(ss -tlnp 2>/dev/null | grep ':8848' | head -1 | sed 's/.*users://')"
|
echo "listen: $(ss -tlnp 2>/dev/null | grep ':8080' | head -1 | sed 's/.*users://')"
|
||||||
REMOTE
|
REMOTE
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
info "✅ 部署完成"
|
info "✅ 部署完成"
|
||||||
echo -e " wind_power: ${GREEN}http://${TARGET#*@}:8848/${NC}"
|
echo -e " wind_power: ${GREEN}http://${TARGET#*@}:8080/${NC}"
|
||||||
echo " 查看服务: ssh ${TARGET} 'systemctl status ${SERVICE_NAME}'"
|
echo " 查看服务: ssh ${TARGET} 'systemctl status ${SERVICE_NAME}'"
|
||||||
|
|||||||
@@ -154,6 +154,102 @@
|
|||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.schemePanel {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(240px, 0.38fr) minmax(0, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeDescriptionControl {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 72px minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeDescriptionControl span {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeDescriptionControl input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px solid #343449;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #0f0f18;
|
||||||
|
color: #e5e7eb;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeDescriptionControl input:focus {
|
||||||
|
border-color: #6366f1;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeDescriptionControl input:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeParamGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeParamGrid input {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px solid #343449;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #0f0f18;
|
||||||
|
color: #e5e7eb;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeParamGrid input:focus {
|
||||||
|
border-color: #6366f1;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeParamGrid input:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeParamGrid input::placeholder {
|
||||||
|
color: #5f6673;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeFooter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemeFooter span {
|
||||||
|
min-height: 18px;
|
||||||
|
color: #86efac;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.designActions {
|
.designActions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -571,7 +667,9 @@ td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.homeHeader,
|
.homeHeader,
|
||||||
.workspace {
|
.workspace,
|
||||||
|
.schemeGrid,
|
||||||
|
.schemeParamGrid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import * as XLSX from 'xlsx';
|
|||||||
import {
|
import {
|
||||||
deleteWindJob,
|
deleteWindJob,
|
||||||
finishWindJob,
|
finishWindJob,
|
||||||
|
getWindSchemes,
|
||||||
|
saveWindSchemeDescription,
|
||||||
startWindJob,
|
startWindJob,
|
||||||
uploadWindChunk,
|
uploadWindChunk,
|
||||||
} from '../utils/api';
|
} from '../utils/api';
|
||||||
@@ -15,6 +17,9 @@ const REQUIRED_FIELDS = [
|
|||||||
{ key: 'wind_speed', label: '风速' },
|
{ key: 'wind_speed', label: '风速' },
|
||||||
{ key: 'active_power', label: '有功功率' },
|
{ key: 'active_power', label: '有功功率' },
|
||||||
{ key: 'generator_speed', label: '发电机转速' },
|
{ key: 'generator_speed', label: '发电机转速' },
|
||||||
|
{ key: 'blade_pitch_1', label: '1#叶片角度' },
|
||||||
|
{ key: 'blade_pitch_2', label: '2#叶片角度' },
|
||||||
|
{ key: 'blade_pitch_3', label: '3#叶片角度' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const FIELD_HINTS = {
|
const FIELD_HINTS = {
|
||||||
@@ -23,6 +28,9 @@ const FIELD_HINTS = {
|
|||||||
wind_speed: ['风速', '平均风速', 'wind speed', 'windspeed'],
|
wind_speed: ['风速', '平均风速', 'wind speed', 'windspeed'],
|
||||||
active_power: ['平均有功功率', '有功功率', 'active power', 'power'],
|
active_power: ['平均有功功率', '有功功率', 'active power', 'power'],
|
||||||
generator_speed: ['发电机转速', '平均发电机转速', 'generator speed', 'rpm'],
|
generator_speed: ['发电机转速', '平均发电机转速', 'generator speed', 'rpm'],
|
||||||
|
blade_pitch_1: ['1#叶片变桨角度', '1#叶片角度', '1叶片变桨角度', '1叶片角度', '1号叶片角度', '叶片1角度', '桨角1', '变桨角1', 'pitch 1', 'pitch angle 1', 'blade pitch 1'],
|
||||||
|
blade_pitch_2: ['2#叶片变桨角度', '2#叶片角度', '2叶片变桨角度', '2叶片角度', '2号叶片角度', '叶片2角度', '桨角2', '变桨角2', 'pitch 2', 'pitch angle 2', 'blade pitch 2'],
|
||||||
|
blade_pitch_3: ['3#叶片变桨角度', '3#叶片角度', '3叶片变桨角度', '3叶片角度', '3号叶片角度', '叶片3角度', '桨角3', '变桨角3', 'pitch 3', 'pitch angle 3', 'blade pitch 3'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const FIELD_EXCLUDES = {
|
const FIELD_EXCLUDES = {
|
||||||
@@ -41,6 +49,27 @@ const DESIGN_FIELD_HINTS = {
|
|||||||
|
|
||||||
const CHUNK_SIZE = 4000;
|
const CHUNK_SIZE = 4000;
|
||||||
const CHART_HEIGHT = 540;
|
const CHART_HEIGHT = 540;
|
||||||
|
const DEFAULT_SCHEME_ID = 'scheme_one';
|
||||||
|
const SCHEME_TWO_ID = 'scheme_two';
|
||||||
|
const DEFAULT_SCHEMES = [
|
||||||
|
{ id: 'scheme_one', name: '方案一', description: '通用方案' },
|
||||||
|
{
|
||||||
|
id: SCHEME_TWO_ID,
|
||||||
|
name: '方案二',
|
||||||
|
description: '桨角筛选方案,使用三支叶片角度平均值、600s 平均风速和手填转速/功率参数',
|
||||||
|
parameters: {
|
||||||
|
grid_connected_speed: 1030,
|
||||||
|
rated_generator_speed: 1755,
|
||||||
|
rated_power: 2000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const DEFAULT_SCHEME_TWO_PARAMS = {
|
||||||
|
grid_connected_speed: '1030',
|
||||||
|
rated_generator_speed: '1755',
|
||||||
|
rated_power: '2000',
|
||||||
|
};
|
||||||
|
|
||||||
function createDesignRows(points) {
|
function createDesignRows(points) {
|
||||||
const source = points?.length ? points : [{ wind_speed: '', design_power: '' }];
|
const source = points?.length ? points : [{ wind_speed: '', design_power: '' }];
|
||||||
@@ -289,6 +318,9 @@ function buildStandardRows(files, mapping) {
|
|||||||
wind_speed: normalizeNumber(getCell(row, localIndexes.wind_speed)),
|
wind_speed: normalizeNumber(getCell(row, localIndexes.wind_speed)),
|
||||||
active_power: normalizeNumber(getCell(row, localIndexes.active_power)),
|
active_power: normalizeNumber(getCell(row, localIndexes.active_power)),
|
||||||
generator_speed: normalizeNumber(getCell(row, localIndexes.generator_speed)),
|
generator_speed: normalizeNumber(getCell(row, localIndexes.generator_speed)),
|
||||||
|
blade_pitch_1: normalizeNumber(getCell(row, localIndexes.blade_pitch_1)),
|
||||||
|
blade_pitch_2: normalizeNumber(getCell(row, localIndexes.blade_pitch_2)),
|
||||||
|
blade_pitch_3: normalizeNumber(getCell(row, localIndexes.blade_pitch_3)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,6 +328,69 @@ function buildStandardRows(files, mapping) {
|
|||||||
return { rows, indexes };
|
return { rows, indexes };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTimestampForFile(date = new Date()) {
|
||||||
|
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`
|
||||||
|
+ `${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeFileName(value) {
|
||||||
|
return String(value || '未选择风机').replace(/[\\/:*?"<>|]/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportValidRowsToExcel(fanId, rows) {
|
||||||
|
const exportRows = (rows || []).map((row) => ({
|
||||||
|
风机编号: row.fan_id || fanId || '',
|
||||||
|
采样时间: row.time || '',
|
||||||
|
平均功率: row.active_power ?? '',
|
||||||
|
平均转速: row.generator_speed ?? '',
|
||||||
|
平均风速: row.wind_speed ?? '',
|
||||||
|
'3个叶片变桨角平均值': row.pitch_angle_average ?? '',
|
||||||
|
}));
|
||||||
|
const worksheet = XLSX.utils.json_to_sheet(exportRows);
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, '有效数据');
|
||||||
|
XLSX.writeFile(
|
||||||
|
workbook,
|
||||||
|
`有效数据_${sanitizeFileName(fanId)}_${formatTimestampForFile()}.xlsx`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpolateDesignPower(points, windSpeed) {
|
||||||
|
if (!points.length) return null;
|
||||||
|
if (windSpeed < points[0].wind_speed || windSpeed > points[points.length - 1].wind_speed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < points.length; index += 1) {
|
||||||
|
const point = points[index];
|
||||||
|
if (Math.abs(point.wind_speed - windSpeed) < 0.000001) {
|
||||||
|
return point.design_power;
|
||||||
|
}
|
||||||
|
if (point.wind_speed > windSpeed && index > 0) {
|
||||||
|
const left = points[index - 1];
|
||||||
|
const right = point;
|
||||||
|
const span = right.wind_speed - left.wind_speed;
|
||||||
|
if (span <= 0) return left.design_power;
|
||||||
|
const ratio = (windSpeed - left.wind_speed) / span;
|
||||||
|
return left.design_power + (right.design_power - left.design_power) * ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return points[points.length - 1].design_power;
|
||||||
|
}
|
||||||
|
|
||||||
|
function schemeParamsToState(parameters) {
|
||||||
|
return {
|
||||||
|
grid_connected_speed: String(
|
||||||
|
parameters?.grid_connected_speed ?? DEFAULT_SCHEME_TWO_PARAMS.grid_connected_speed,
|
||||||
|
),
|
||||||
|
rated_generator_speed: String(
|
||||||
|
parameters?.rated_generator_speed ?? DEFAULT_SCHEME_TWO_PARAMS.rated_generator_speed,
|
||||||
|
),
|
||||||
|
rated_power: String(parameters?.rated_power ?? DEFAULT_SCHEME_TWO_PARAMS.rated_power),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function SummaryCards({ summary }) {
|
function SummaryCards({ summary }) {
|
||||||
if (!summary) return null;
|
if (!summary) return null;
|
||||||
const cards = [
|
const cards = [
|
||||||
@@ -352,15 +447,12 @@ function PowerCurveChart({
|
|||||||
const actualByWindSpeed = new Map(
|
const actualByWindSpeed = new Map(
|
||||||
sortedCurve.map((point) => [point.wind_speed.toFixed(6), point.average_power]),
|
sortedCurve.map((point) => [point.wind_speed.toFixed(6), point.average_power]),
|
||||||
);
|
);
|
||||||
const designByWindSpeed = new Map(
|
|
||||||
sortedDesign.map((point) => [point.wind_speed.toFixed(6), point.design_power]),
|
|
||||||
);
|
|
||||||
const curveX = Array.from(new Set([
|
const curveX = Array.from(new Set([
|
||||||
...sortedCurve.map((point) => point.wind_speed),
|
...sortedCurve.map((point) => point.wind_speed),
|
||||||
...sortedDesign.map((point) => point.wind_speed),
|
...sortedDesign.map((point) => point.wind_speed),
|
||||||
])).sort((left, right) => left - right);
|
])).sort((left, right) => left - right);
|
||||||
const actualY = curveX.map((windSpeed) => actualByWindSpeed.get(windSpeed.toFixed(6)) ?? null);
|
const actualY = curveX.map((windSpeed) => actualByWindSpeed.get(windSpeed.toFixed(6)) ?? null);
|
||||||
const designY = curveX.map((windSpeed) => designByWindSpeed.get(windSpeed.toFixed(6)) ?? null);
|
const designY = curveX.map((windSpeed) => interpolateDesignPower(sortedDesign, windSpeed));
|
||||||
const maxScatterPower = validScatter.reduce(
|
const maxScatterPower = validScatter.reduce(
|
||||||
(max, point) => Math.max(max, point.active_power),
|
(max, point) => Math.max(max, point.active_power),
|
||||||
0,
|
0,
|
||||||
@@ -409,6 +501,7 @@ function PowerCurveChart({
|
|||||||
stroke: '#ef4444',
|
stroke: '#ef4444',
|
||||||
width: 2.5,
|
width: 2.5,
|
||||||
paths: uPlot.paths.spline(),
|
paths: uPlot.paths.spline(),
|
||||||
|
spanGaps: true,
|
||||||
points: { show: false },
|
points: { show: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -416,6 +509,7 @@ function PowerCurveChart({
|
|||||||
stroke: '#22c55e',
|
stroke: '#22c55e',
|
||||||
width: 2.2,
|
width: 2.2,
|
||||||
paths: uPlot.paths.spline(),
|
paths: uPlot.paths.spline(),
|
||||||
|
spanGaps: true,
|
||||||
points: { show: false },
|
points: { show: false },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -430,7 +524,7 @@ function PowerCurveChart({
|
|||||||
const y = u.valToPos(point.active_power, 'y', true);
|
const y = u.valToPos(point.active_power, 'y', true);
|
||||||
if (Number.isFinite(x) && Number.isFinite(y)) {
|
if (Number.isFinite(x) && Number.isFinite(y)) {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, 1.6, 0, Math.PI * 2);
|
ctx.arc(x, y, 2, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,7 +535,7 @@ function PowerCurveChart({
|
|||||||
const y = u.valToPos(point.active_power, 'y', true);
|
const y = u.valToPos(point.active_power, 'y', true);
|
||||||
if (Number.isFinite(x) && Number.isFinite(y)) {
|
if (Number.isFinite(x) && Number.isFinite(y)) {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, 1.9, 0, Math.PI * 2);
|
ctx.arc(x, y, 2, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -484,9 +578,17 @@ export default function HomePage() {
|
|||||||
const [readingDesign, setReadingDesign] = useState(false);
|
const [readingDesign, setReadingDesign] = useState(false);
|
||||||
const [designRows, setDesignRows] = useState(() => createDesignRows());
|
const [designRows, setDesignRows] = useState(() => createDesignRows());
|
||||||
const [designMeta, setDesignMeta] = useState(null);
|
const [designMeta, setDesignMeta] = useState(null);
|
||||||
|
const [schemes, setSchemes] = useState(DEFAULT_SCHEMES);
|
||||||
|
const [selectedSchemeId, setSelectedSchemeId] = useState(DEFAULT_SCHEME_ID);
|
||||||
|
const [schemeDescription, setSchemeDescription] = useState(DEFAULT_SCHEMES[0].description);
|
||||||
|
const [savingScheme, setSavingScheme] = useState(false);
|
||||||
|
const [schemeMessage, setSchemeMessage] = useState('');
|
||||||
|
const [schemeTwoParams, setSchemeTwoParams] = useState(DEFAULT_SCHEME_TWO_PARAMS);
|
||||||
|
|
||||||
const headers = files[0]?.headers || [];
|
const headers = files[0]?.headers || [];
|
||||||
const missingFields = REQUIRED_FIELDS.filter((field) => !mapping[field.key]);
|
const missingFields = REQUIRED_FIELDS.filter((field) => !mapping[field.key]);
|
||||||
|
const selectedScheme = schemes.find((scheme) => scheme.id === selectedSchemeId) || schemes[0];
|
||||||
|
const isSchemeTwo = selectedSchemeId === SCHEME_TWO_ID;
|
||||||
const selectedPoints = selectedFan && result?.curves ? result.curves[selectedFan] || [] : [];
|
const selectedPoints = selectedFan && result?.curves ? result.curves[selectedFan] || [] : [];
|
||||||
const selectedBins = selectedFan && result?.bins ? result.bins[selectedFan] || [] : [];
|
const selectedBins = selectedFan && result?.bins ? result.bins[selectedFan] || [] : [];
|
||||||
const selectedScatter = selectedFan && result?.scatter_points
|
const selectedScatter = selectedFan && result?.scatter_points
|
||||||
@@ -526,6 +628,95 @@ export default function HomePage() {
|
|||||||
.sort((left, right) => left.wind_speed - right.wind_speed);
|
.sort((left, right) => left.wind_speed - right.wind_speed);
|
||||||
}, [designRows]);
|
}, [designRows]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let canceled = false;
|
||||||
|
|
||||||
|
async function loadSchemes() {
|
||||||
|
try {
|
||||||
|
const data = await getWindSchemes();
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
const loadedSchemes = data.schemes?.length ? data.schemes : DEFAULT_SCHEMES;
|
||||||
|
const defaultSchemeId = data.default_scheme_id || DEFAULT_SCHEME_ID;
|
||||||
|
const defaultScheme = loadedSchemes.find((scheme) => scheme.id === defaultSchemeId)
|
||||||
|
|| loadedSchemes[0];
|
||||||
|
setSchemes(loadedSchemes);
|
||||||
|
setSelectedSchemeId(defaultScheme.id);
|
||||||
|
setSchemeDescription(defaultScheme.description || '');
|
||||||
|
const schemeTwo = loadedSchemes.find((scheme) => scheme.id === SCHEME_TWO_ID);
|
||||||
|
setSchemeTwoParams(schemeParamsToState(schemeTwo?.parameters));
|
||||||
|
} catch (err) {
|
||||||
|
if (!canceled) {
|
||||||
|
setSchemeMessage(err.message || '读取方案配置失败,已使用默认方案');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSchemes();
|
||||||
|
return () => {
|
||||||
|
canceled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function handleSchemeChange(value) {
|
||||||
|
const nextScheme = schemes.find((scheme) => scheme.id === value);
|
||||||
|
setSelectedSchemeId(value);
|
||||||
|
setSchemeDescription(nextScheme?.description || '');
|
||||||
|
if (value === SCHEME_TWO_ID) {
|
||||||
|
setSchemeTwoParams(schemeParamsToState(nextScheme?.parameters));
|
||||||
|
}
|
||||||
|
setSchemeMessage('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSchemeTwoParamChange(field, value) {
|
||||||
|
setSchemeTwoParams((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveSchemeDescription() {
|
||||||
|
if (!selectedScheme) return;
|
||||||
|
|
||||||
|
setSavingScheme(true);
|
||||||
|
setError('');
|
||||||
|
setSchemeMessage('');
|
||||||
|
try {
|
||||||
|
const gridConnectedSpeed = normalizeNumber(schemeTwoParams.grid_connected_speed);
|
||||||
|
const ratedGeneratorSpeed = normalizeNumber(schemeTwoParams.rated_generator_speed);
|
||||||
|
const ratedPower = normalizeNumber(schemeTwoParams.rated_power);
|
||||||
|
if (selectedScheme.id === SCHEME_TWO_ID &&
|
||||||
|
(!Number.isFinite(gridConnectedSpeed) || gridConnectedSpeed <= 0 ||
|
||||||
|
!Number.isFinite(ratedGeneratorSpeed) || ratedGeneratorSpeed <= 0 ||
|
||||||
|
!Number.isFinite(ratedPower) || ratedPower <= 0)) {
|
||||||
|
throw new Error('方案二参数必须为正数');
|
||||||
|
}
|
||||||
|
const data = await saveWindSchemeDescription(selectedScheme.id, {
|
||||||
|
description: schemeDescription,
|
||||||
|
...(selectedScheme.id === SCHEME_TWO_ID ? {
|
||||||
|
parameters: {
|
||||||
|
grid_connected_speed: gridConnectedSpeed,
|
||||||
|
rated_generator_speed: ratedGeneratorSpeed,
|
||||||
|
rated_power: ratedPower,
|
||||||
|
},
|
||||||
|
} : {}),
|
||||||
|
});
|
||||||
|
const savedScheme = data.scheme;
|
||||||
|
setSchemes((prev) => prev.map((scheme) => (
|
||||||
|
scheme.id === savedScheme.id ? { ...scheme, ...savedScheme } : scheme
|
||||||
|
)));
|
||||||
|
setSchemeDescription(savedScheme.description || '');
|
||||||
|
if (savedScheme.id === SCHEME_TWO_ID) {
|
||||||
|
setSchemeTwoParams(schemeParamsToState(savedScheme.parameters));
|
||||||
|
}
|
||||||
|
setSchemeMessage('已保存');
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || '保存失败');
|
||||||
|
} finally {
|
||||||
|
setSavingScheme(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleFilesChange(event) {
|
async function handleFilesChange(event) {
|
||||||
const selectedFiles = Array.from(event.target.files || []);
|
const selectedFiles = Array.from(event.target.files || []);
|
||||||
if (!selectedFiles.length) return;
|
if (!selectedFiles.length) return;
|
||||||
@@ -605,6 +796,16 @@ export default function HomePage() {
|
|||||||
setError('请先上传文件并完成所有字段映射');
|
setError('请先上传文件并完成所有字段映射');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const gridConnectedSpeed = normalizeNumber(schemeTwoParams.grid_connected_speed);
|
||||||
|
const ratedGeneratorSpeed = normalizeNumber(schemeTwoParams.rated_generator_speed);
|
||||||
|
const ratedPower = normalizeNumber(schemeTwoParams.rated_power);
|
||||||
|
if (isSchemeTwo &&
|
||||||
|
(!Number.isFinite(gridConnectedSpeed) || gridConnectedSpeed <= 0 ||
|
||||||
|
!Number.isFinite(ratedGeneratorSpeed) || ratedGeneratorSpeed <= 0 ||
|
||||||
|
!Number.isFinite(ratedPower) || ratedPower <= 0)) {
|
||||||
|
setError('方案二需要填写并网转速、额定转速和额定功率,且必须为正数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let jobId = '';
|
let jobId = '';
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
@@ -647,6 +848,12 @@ export default function HomePage() {
|
|||||||
iqr_upper_multiplier: 2,
|
iqr_upper_multiplier: 2,
|
||||||
minimum_generator_speed: 1,
|
minimum_generator_speed: 1,
|
||||||
generator_speed_k: 0.9,
|
generator_speed_k: 0.9,
|
||||||
|
scheme_id: selectedSchemeId,
|
||||||
|
...(isSchemeTwo ? {
|
||||||
|
grid_connected_speed: gridConnectedSpeed,
|
||||||
|
rated_generator_speed: ratedGeneratorSpeed,
|
||||||
|
rated_power: ratedPower,
|
||||||
|
} : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setResult(calculation);
|
setResult(calculation);
|
||||||
@@ -691,6 +898,102 @@ export default function HomePage() {
|
|||||||
{error && <div className="alert error">{error}</div>}
|
{error && <div className="alert error">{error}</div>}
|
||||||
{progress && <div className="alert info">{progress}</div>}
|
{progress && <div className="alert info">{progress}</div>}
|
||||||
|
|
||||||
|
<section className="panel schemePanel">
|
||||||
|
<div className="panelHeader">
|
||||||
|
<div>
|
||||||
|
<h2>计算方案</h2>
|
||||||
|
<p className="panelHint">选择本次计算使用的清洗方案,方案描述可保存为服务端共享配置。</p>
|
||||||
|
</div>
|
||||||
|
<span>{selectedScheme?.name || '方案一'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="schemeGrid">
|
||||||
|
<label className="fieldControl">
|
||||||
|
<span>方案选择</span>
|
||||||
|
<select
|
||||||
|
value={selectedSchemeId}
|
||||||
|
onChange={(event) => handleSchemeChange(event.target.value)}
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
{schemes.map((scheme) => (
|
||||||
|
<option key={scheme.id} value={scheme.id}>{scheme.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="schemeDescriptionControl">
|
||||||
|
<span>方案描述</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={schemeDescription}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSchemeDescription(event.target.value);
|
||||||
|
setSchemeMessage('');
|
||||||
|
}}
|
||||||
|
disabled={submitting || savingScheme}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{isSchemeTwo && (
|
||||||
|
<div className="schemeParamGrid">
|
||||||
|
<label className="fieldControl">
|
||||||
|
<span>并网转速 (rpm)</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
value={schemeTwoParams.grid_connected_speed}
|
||||||
|
onChange={(event) => handleSchemeTwoParamChange(
|
||||||
|
'grid_connected_speed',
|
||||||
|
event.target.value,
|
||||||
|
)}
|
||||||
|
disabled={submitting}
|
||||||
|
placeholder="请输入"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="fieldControl">
|
||||||
|
<span>额定转速 (rpm)</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
value={schemeTwoParams.rated_generator_speed}
|
||||||
|
onChange={(event) => handleSchemeTwoParamChange(
|
||||||
|
'rated_generator_speed',
|
||||||
|
event.target.value,
|
||||||
|
)}
|
||||||
|
disabled={submitting}
|
||||||
|
placeholder="请输入"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="fieldControl">
|
||||||
|
<span>额定功率 (kW)</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.1"
|
||||||
|
value={schemeTwoParams.rated_power}
|
||||||
|
onChange={(event) => handleSchemeTwoParamChange(
|
||||||
|
'rated_power',
|
||||||
|
event.target.value,
|
||||||
|
)}
|
||||||
|
disabled={submitting}
|
||||||
|
placeholder="请输入"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="schemeFooter">
|
||||||
|
<span>{schemeMessage}</span>
|
||||||
|
<button
|
||||||
|
className="secondaryButton"
|
||||||
|
type="button"
|
||||||
|
onClick={handleSaveSchemeDescription}
|
||||||
|
disabled={submitting || savingScheme}
|
||||||
|
>
|
||||||
|
{savingScheme ? '保存中' : '保存'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="panel designPowerPanel">
|
<section className="panel designPowerPanel">
|
||||||
<div className="panelHeader">
|
<div className="panelHeader">
|
||||||
<div>
|
<div>
|
||||||
@@ -860,6 +1163,12 @@ export default function HomePage() {
|
|||||||
<span>计算完成</span>
|
<span>计算完成</span>
|
||||||
</div>
|
</div>
|
||||||
<SummaryCards summary={result.summary} />
|
<SummaryCards summary={result.summary} />
|
||||||
|
{result.scheme && (
|
||||||
|
<div className="paramStrip">
|
||||||
|
<span>方案:{result.scheme.name}</span>
|
||||||
|
<span>{result.scheme.description}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="reasonList">
|
<div className="reasonList">
|
||||||
{Object.entries(result.summary?.invalid_reasons || {}).map(([reason, count]) => (
|
{Object.entries(result.summary?.invalid_reasons || {}).map(([reason, count]) => (
|
||||||
<span key={reason}>{reason}: {Number(count).toLocaleString()}</span>
|
<span key={reason}>{reason}: {Number(count).toLocaleString()}</span>
|
||||||
@@ -883,6 +1192,14 @@ export default function HomePage() {
|
|||||||
>
|
>
|
||||||
{showFiltered ? '隐藏滤除点' : '显示滤除点'}
|
{showFiltered ? '隐藏滤除点' : '显示滤除点'}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="secondaryButton"
|
||||||
|
type="button"
|
||||||
|
onClick={() => exportValidRowsToExcel(selectedFan, selectedScatter)}
|
||||||
|
disabled={!selectedScatter.length}
|
||||||
|
>
|
||||||
|
导出有效数据
|
||||||
|
</button>
|
||||||
<select
|
<select
|
||||||
className="fanSelect"
|
className="fanSelect"
|
||||||
value={selectedFan}
|
value={selectedFan}
|
||||||
|
|||||||
@@ -54,6 +54,27 @@ export function getHealth() {
|
|||||||
return request('/system/health');
|
return request('/system/health');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取计算方案列表
|
||||||
|
* @returns {Promise<{default_scheme_id: string, schemes: Array<object>}>}
|
||||||
|
*/
|
||||||
|
export function getWindSchemes() {
|
||||||
|
return request('/wind/schemes');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存计算方案描述
|
||||||
|
* @param {string} schemeId
|
||||||
|
* @param {{description: string}} payload
|
||||||
|
* @returns {Promise<{scheme: object}>}
|
||||||
|
*/
|
||||||
|
export function saveWindSchemeDescription(schemeId, payload) {
|
||||||
|
return request(`/wind/schemes/${encodeURIComponent(schemeId)}/description`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建风功率计算任务
|
* 创建风功率计算任务
|
||||||
* @param {{files: Array<{file_name: string, row_count: number}>, mapping: object}} payload
|
* @param {{files: Array<{file_name: string, row_count: number}>, mapping: object}} payload
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default defineConfig({
|
|||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:8848',
|
target: 'http://localhost:8080',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
# (后端同时提供 /api 与 SPA 静态资源,即“前后端一起跑”)
|
# (后端同时提供 /api 与 SPA 静态资源,即“前后端一起跑”)
|
||||||
#
|
#
|
||||||
# 开发模式前端热更新:
|
# 开发模式前端热更新:
|
||||||
# ./run.sh # 后端托管生产前端,监听 :8848
|
# ./run.sh # 后端托管生产前端,监听 :8080
|
||||||
# cd frontend/web_app && npm run dev # 前端 dev :5173,/api 代理到 :8848
|
# cd frontend/web_app && npm run dev # 前端 dev :5173,/api 代理到 :8080
|
||||||
#
|
#
|
||||||
# 参考: edge_collector/run_cloud.sh
|
# 参考: edge_collector/run_cloud.sh
|
||||||
# =====================================================
|
# =====================================================
|
||||||
@@ -63,7 +63,7 @@ echo "========================================"
|
|||||||
echo " wind_power 已启动"
|
echo " wind_power 已启动"
|
||||||
echo " PID: ${SERVER_PID}"
|
echo " PID: ${SERVER_PID}"
|
||||||
echo " 工作目录: ${APP_DIR}"
|
echo " 工作目录: ${APP_DIR}"
|
||||||
echo " 访问: http://localhost:8848 (Ctrl+C 停止)"
|
echo " 访问: http://localhost:8080 (Ctrl+C 停止)"
|
||||||
echo "========================================"
|
echo "========================================"
|
||||||
|
|
||||||
# 4. 等待退出信号
|
# 4. 等待退出信号
|
||||||
|
|||||||
Reference in New Issue
Block a user