Compare commits
8 Commits
cee451914a
...
8a74a576c0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a74a576c0 | ||
|
|
0927c94a23 | ||
|
|
68328a68f1 | ||
|
|
4aa56c71d5 | ||
|
|
fd6684c884 | ||
|
|
c19e7b7631 | ||
|
|
07063ec638 | ||
|
|
a65ce23cee |
13
Makefile
13
Makefile
@@ -1,6 +1,15 @@
|
||||
target: dist build
|
||||
create-version-file metadata.yml --outfile build/file_version_info.txt
|
||||
pyinstaller -F main.py --version-file build/file_version_info.txt -n Lightening
|
||||
uv run python update_version.py
|
||||
cd webui && npm run build
|
||||
cd ..
|
||||
uv run create-version-file metadata.yml --outfile build/file_version_info.txt
|
||||
uv run pyinstaller -F main.py --version-file build/file_version_info.txt -n Lightening
|
||||
|
||||
gui: build
|
||||
uv run python update_version.py
|
||||
cd webui && npm run build
|
||||
cd ..
|
||||
uv run pyinstaller webview_app.py -n LighteningGUI --noconsole --add-data "webui/dist;webui/dist" -y
|
||||
|
||||
dist:
|
||||
mkdir dist
|
||||
|
||||
34
README.md
34
README.md
@@ -18,6 +18,7 @@
|
||||
### 环境要求
|
||||
|
||||
- Python >= 3.12
|
||||
- Node.js >= 18(图形界面开发需要)
|
||||
|
||||
### 安装依赖
|
||||
|
||||
@@ -122,10 +123,10 @@ python webview_app.py
|
||||
rated_voltage = 750 # 额定电压等级 (kV)
|
||||
h_c_sag = 14.43 # 导线弧垂 (m)
|
||||
h_g_sag = 11.67 # 地线弧垂 (m)
|
||||
insulator_c_len = 7.02 # 导线串子绝缘长度 (m)
|
||||
insulator_c_len = 7.4 # 导线串子绝缘长度 (m)
|
||||
string_c_len = 9.2 # 导线串长 (m)
|
||||
string_g_len = 0.5 # 地线串长 (m)
|
||||
h_arm = [150, 130] # 导、地线挂点垂直距离 (m),第一个值为地线挂点高度
|
||||
h_arm = [130, 100] # 导、地线挂点垂直距离 (m),第一个值为地线挂点高度
|
||||
gc_x = [17.9, 17] # 导、地线水平坐标 (m)
|
||||
ground_angels = [0] # 地面倾角 (°),向下为正,支持多个角度
|
||||
altitude = 1000 # 海拔高度 (m)
|
||||
@@ -141,7 +142,7 @@ Ip_a = -1 # 雷电流概率密度曲线系数a,大于0时使用此
|
||||
Ip_b = -1 # 雷电流概率密度曲线系数b,大于0时使用此值
|
||||
```
|
||||
|
||||
注意:当 `ng` > 0 时,不会通过雷暴日计算地闪密度;当 `Ip_a` 和 `Ip_b` > 0 时,不会使用默认雷暴日对应的概率密度。
|
||||
**注意**:当 `ng` > 0 时,不会通过雷暴日计算地闪密度;当 `Ip_a` 和 `Ip_b` > 0 时,不会使用默认雷暴日对应的概率密度。
|
||||
|
||||
### [optional] - 可选参数
|
||||
|
||||
@@ -189,25 +190,28 @@ EGM/
|
||||
├── animation.py # 动画演示模块
|
||||
├── webview_app.py # pywebview 图形界面后端
|
||||
├── article.toml # 示例配置文件
|
||||
├── default.toml # 默认配置文件
|
||||
├── Makefile # 构建脚本
|
||||
├── pyproject.toml # 项目配置
|
||||
├── README.md # 说明文档
|
||||
├── webui/ # 图形界面前端项目
|
||||
│ ├── src/
|
||||
│ │ ├── components/
|
||||
│ │ │ └── ParameterForm.vue
|
||||
│ │ │ ├── ParameterForm.vue # 参数表单组件
|
||||
│ │ │ ├── Animation.vue # 动画可视化组件
|
||||
│ │ │ └── Log.vue # 日志显示组件
|
||||
│ │ ├── types/
|
||||
│ │ │ └── index.ts
|
||||
│ │ ├── App.vue
|
||||
│ │ ├── main.ts
|
||||
│ │ └── style.css
|
||||
│ ├── package.json
|
||||
│ ├── vite.config.ts
|
||||
│ ├── tsconfig.json
|
||||
│ ├── tailwind.config.js
|
||||
│ ├── postcss.config.js
|
||||
│ └── index.html
|
||||
└── CSharp/ # C# 版本实现
|
||||
│ │ │ └── index.ts # TypeScript 类型定义
|
||||
│ │ ├── App.vue # 主应用组件
|
||||
│ │ ├── main.ts # 应用入口
|
||||
│ │ └── style.css # 全局样式
|
||||
│ ├── package.json # 前端依赖配置
|
||||
│ ├── vite.config.ts # Vite 配置
|
||||
│ ├── tsconfig.json # TypeScript 配置
|
||||
│ ├── tailwind.config.js # Tailwind CSS 配置
|
||||
│ └── index.html # HTML 入口
|
||||
├── CSharp/ # C# 版本实现
|
||||
└── 历史/ # 历史配置文件和DXF文件
|
||||
```
|
||||
|
||||
## 技术支持
|
||||
|
||||
6
core.py
6
core.py
@@ -26,6 +26,8 @@ class Parameter:
|
||||
Ip_a: float # 概率密度曲线系数a
|
||||
Ip_b: float # 概率密度曲线系数b
|
||||
ac_or_dc: str # 交流或直流标识,"AC" 或 "DC",默认 "AC"
|
||||
z_0: float # 雷电波阻抗,默认 300
|
||||
z_c: float # 导线波阻抗,默认 251
|
||||
|
||||
|
||||
def rg_line_function_factory(_rg, ground_angel): # 返回一个地面捕雷线的直线方程
|
||||
@@ -185,7 +187,7 @@ def solve_circle_line_intersection(
|
||||
return [_x, _y]
|
||||
|
||||
|
||||
def min_i(string_len, u_ph, altitude: float = 0):
|
||||
def min_i(string_len, u_ph, altitude: float = 0, z_0: float = 300, z_c: float = 251):
|
||||
# 海拔修正
|
||||
if altitude > 1000:
|
||||
k_a = math.exp((altitude - 1000) / 8150) # 气隙海拔修正
|
||||
@@ -196,8 +198,6 @@ def min_i(string_len, u_ph, altitude: float = 0):
|
||||
# u_50 = 1 / k_a * (477 * string_len + 99) # 串放电路径 2000m海拔
|
||||
# u_50 = 615 * string_len # 导线对塔身放电 1000m海拔
|
||||
# u_50= 263.32647401+533.90081562*string_len
|
||||
z_0 = 300 # 雷电波阻抗
|
||||
z_c = 251 # 导线波阻抗
|
||||
# 新版大手册公式 3-277
|
||||
r = (u_50 + 2 * z_0 / (2 * z_0 + z_c) * u_ph) * (2 * z_0 + z_c) / (z_0 * z_c)
|
||||
# r = 2 * (u_50 - u_ph) / z_c
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import math
|
||||
import os.path
|
||||
import sys
|
||||
import tomli
|
||||
import tomllib as tomli
|
||||
from loguru import logger
|
||||
from core import *
|
||||
import timeit
|
||||
|
||||
60
main.py
60
main.py
@@ -2,16 +2,16 @@ import math
|
||||
import os.path
|
||||
import sys
|
||||
import time
|
||||
import tomli
|
||||
import tomllib as tomli
|
||||
from loguru import logger
|
||||
from core import *
|
||||
import timeit
|
||||
from animation import Animation
|
||||
|
||||
|
||||
# 打印参数
|
||||
def parameter_display(para_dis: Parameter):
|
||||
logger.info(f"额定电压 kV {para_dis.rated_voltage}")
|
||||
logger.info(f"交、直流标识 {para_dis.ac_or_dc}")
|
||||
logger.info(f"导线弧垂 m {para_dis.h_c_sag}")
|
||||
logger.info(f"地线弧垂 m {para_dis.h_g_sag}")
|
||||
logger.info(f"全塔高 m {para_dis.h_arm[0]}")
|
||||
@@ -22,14 +22,18 @@ def parameter_display(para_dis: Parameter):
|
||||
logger.info(f"挂点水平坐标 m {para_dis.gc_x}")
|
||||
logger.info(f"地面倾角 ° {[an * 180 / math.pi for an in para_dis.ground_angels]}")
|
||||
logger.info(f"海拔高度 m {para_dis.altitude}")
|
||||
if para_dis.ng > 0:
|
||||
logger.info("不采用雷暴日计算地闪密度和雷电流密度")
|
||||
logger.info(f"地闪密度 次/(每平方公里·每年) {para_dis.ng}")
|
||||
logger.info(f"概率密度曲线系数a {para_dis.Ip_a}")
|
||||
logger.info(f"概率密度曲线系数b {para_dis.Ip_b}")
|
||||
pass
|
||||
else:
|
||||
logger.info(f"雷暴日 d {para_dis.td}")
|
||||
logger.info(f"雷电波阻抗 Ω {para_dis.z_0}")
|
||||
logger.info(f"导线波阻抗 Ω {para_dis.z_c}")
|
||||
logger.info(f"工作电压分份数 {para_dis.voltage_n}")
|
||||
logger.info(f"最大尝试电流 kA {para_dis.max_i}")
|
||||
# if para_dis.ng > 0:
|
||||
# logger.info("不采用雷暴日计算地闪密度和雷电流密度")
|
||||
logger.info(f"地闪密度 次/(每平方公里·每年) {para_dis.ng}")
|
||||
logger.info(f"概率密度曲线系数a {para_dis.Ip_a}")
|
||||
logger.info(f"概率密度曲线系数b {para_dis.Ip_b}")
|
||||
# pass
|
||||
# else:
|
||||
logger.info(f"雷暴日 d {para_dis.td}")
|
||||
|
||||
|
||||
def read_parameter(toml_file_path) -> Parameter:
|
||||
@@ -62,11 +66,12 @@ def read_parameter(toml_file_path) -> Parameter:
|
||||
return para
|
||||
|
||||
|
||||
def run_egm(para: Parameter) -> dict:
|
||||
def run_egm(para: Parameter, animation=None) -> dict:
|
||||
"""
|
||||
执行 EGM 计算的核心函数,可被外部调用。
|
||||
Args:
|
||||
para: 参数对象,包含所有计算所需的参数。
|
||||
animation: 可选的动画对象,用于可视化。需要实现 add_rs, add_rc, add_rg_line, add_expose_area, pause 方法。
|
||||
Returns:
|
||||
计算结果字典。
|
||||
"""
|
||||
@@ -104,9 +109,9 @@ def run_egm(para: Parameter) -> dict:
|
||||
ng = func_ng(td, para.ng)
|
||||
avr_n_sf = 0 # 考虑电压的影响计算的跳闸率
|
||||
ground_angels = para.ground_angels
|
||||
# 初始化动画
|
||||
animate = Animation()
|
||||
animate.enable(False)
|
||||
# 动画对象:如果传入了 animation 则使用,否则不启用动画
|
||||
# 注意:动画的启用由前端用户通过"启用动画"开关控制,后端不主动启用
|
||||
animate = animation
|
||||
# animate.show()
|
||||
for ground_angel in ground_angels:
|
||||
logger.info(f"地面倾角{ground_angel/math.pi*180:.3f}°")
|
||||
@@ -170,7 +175,7 @@ def run_egm(para: Parameter) -> dict:
|
||||
insulator_c_len = para.insulator_c_len
|
||||
# i_min = min_i(insulator_c_len, u_ph / 1.732)
|
||||
# TODO 需要考虑交、直流
|
||||
i_min = min_i(insulator_c_len, u_ph, para.altitude)
|
||||
i_min = min_i(insulator_c_len, u_ph, para.altitude, para.z_0, para.z_c)
|
||||
_min_i = i_min # 尝试的最小电流
|
||||
_max_i = para.max_i # 尝试的最大电流
|
||||
# cad.draw(i_min, u_ph, rs_x, rs_y, rc_x, rc_y, rg_x, rg_y, rg_type, 2)
|
||||
@@ -179,14 +184,17 @@ def run_egm(para: Parameter) -> dict:
|
||||
): # 雷电流
|
||||
logger.info(f"尝试计算电流为{i_bar:.2f}")
|
||||
rs = rs_fun(i_bar)
|
||||
animate.add_rs(rs, rs_x, rs_y)
|
||||
if animate:
|
||||
animate.add_rs(rs, rs_x, rs_y)
|
||||
rc = rc_fun(i_bar, u_ph)
|
||||
animate.add_rc(rc, rc_x, rc_y)
|
||||
if animate:
|
||||
animate.add_rc(rc, rc_x, rc_y)
|
||||
rg = rg_fun(i_bar, rc_y, u_ph, typ=rg_type)
|
||||
rg_line_func = None
|
||||
if rg_type == "g":
|
||||
rg_line_func = rg_line_function_factory(rg, ground_angel)
|
||||
animate.add_rg_line(rg_line_func)
|
||||
if animate:
|
||||
animate.add_rg_line(rg_line_func)
|
||||
rs_rc_circle_intersection = solve_circle_intersection(
|
||||
rs, rc, rs_x, rs_y, rc_x, rc_y
|
||||
)
|
||||
@@ -219,12 +227,13 @@ def run_egm(para: Parameter) -> dict:
|
||||
"上面的导地线无法保护下面的导地线,检查设置参数。"
|
||||
)
|
||||
continue
|
||||
animate.add_expose_area(
|
||||
rc_x,
|
||||
rc_y,
|
||||
*rs_rc_circle_intersection,
|
||||
*circle_rc_or_rg_line_intersection,
|
||||
)
|
||||
if animate:
|
||||
animate.add_expose_area(
|
||||
rc_x,
|
||||
rc_y,
|
||||
*rs_rc_circle_intersection,
|
||||
*circle_rc_or_rg_line_intersection,
|
||||
)
|
||||
cad = Draw()
|
||||
cad.draw(
|
||||
i_min,
|
||||
@@ -298,7 +307,8 @@ def run_egm(para: Parameter) -> dict:
|
||||
logger.info(f"电流为{i_bar}kA时,暴露弧已经完全被屏蔽")
|
||||
exposed_curve_shielded = True
|
||||
break
|
||||
animate.pause()
|
||||
if animate:
|
||||
animate.pause()
|
||||
# 判断是否导线已经被完全保护
|
||||
if abs(i_max - _max_i) < 1e-5:
|
||||
logger.info("无法找到最大电流,可能是杆塔较高。")
|
||||
|
||||
4
metadata.yml
Normal file
4
metadata.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
version: 1.0.12
|
||||
company_name: EGM
|
||||
file_description: EGM Lightning Protection Calculator
|
||||
product_name: Lightening
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "EGM",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
76
update_version.py
Normal file
76
update_version.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""更新版本号脚本"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_version():
|
||||
"""从 VERSION 文件读取版本号"""
|
||||
version_file = Path(__file__).parent / "VERSION"
|
||||
return version_file.read_text().strip()
|
||||
|
||||
|
||||
def increment_version(version: str) -> str:
|
||||
"""递增版本号的修订号"""
|
||||
parts = version.split(".")
|
||||
if len(parts) == 3:
|
||||
parts[2] = str(int(parts[2]) + 1)
|
||||
return ".".join(parts)
|
||||
return version
|
||||
|
||||
|
||||
def update_index_html(version: str):
|
||||
"""更新 webui/index.html 中的标题版本号"""
|
||||
index_file = Path(__file__).parent / "webui" / "index.html"
|
||||
content = index_file.read_text(encoding="utf-8")
|
||||
|
||||
# 替换标题中的版本号
|
||||
new_content = re.sub(
|
||||
r"<title>EGM 输电线路绕击跳闸率计算( v[\d.]+)?</title>",
|
||||
f"<title>EGM 输电线路绕击跳闸率计算 v{version}</title>",
|
||||
content
|
||||
)
|
||||
|
||||
index_file.write_text(new_content, encoding="utf-8")
|
||||
print(f"Updated version in {index_file} to v{version}")
|
||||
|
||||
|
||||
def update_version_file(version: str):
|
||||
"""更新 VERSION 文件"""
|
||||
version_file = Path(__file__).parent / "VERSION"
|
||||
version_file.write_text(version + "\n")
|
||||
|
||||
|
||||
def create_metadata(version: str):
|
||||
"""创建 metadata.yml 文件"""
|
||||
metadata_file = Path(__file__).parent / "metadata.yml"
|
||||
content = f"""version: {version}
|
||||
company_name: EGM
|
||||
file_description: EGM Lightning Protection Calculator
|
||||
product_name: Lightening
|
||||
"""
|
||||
metadata_file.write_text(content)
|
||||
print(f"Created metadata.yml with version {version}")
|
||||
|
||||
|
||||
def main():
|
||||
# 检查是否只获取版本号
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--get":
|
||||
print(get_version())
|
||||
return
|
||||
|
||||
# 获取当前版本并递增
|
||||
current_version = get_version()
|
||||
new_version = increment_version(current_version)
|
||||
|
||||
# 更新所有文件
|
||||
update_version_file(new_version)
|
||||
update_index_html(new_version)
|
||||
create_metadata(new_version)
|
||||
|
||||
print(f"Version updated: {current_version} -> {new_version}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EGM 输电线路绕击跳闸率计算</title>
|
||||
<title>EGM 输电线路绕击跳闸率计算 v1.0.12</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
130
webui/package-lock.json
generated
130
webui/package-lock.json
generated
@@ -21,7 +21,7 @@
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.3.0",
|
||||
"vite": "^5.0.0",
|
||||
"vue-tsc": "^1.8.0"
|
||||
"vue-tsc": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@@ -1218,34 +1218,32 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/language-core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.11.1.tgz",
|
||||
"integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
|
||||
"version": "2.4.15",
|
||||
"resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz",
|
||||
"integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/source-map": "1.11.1"
|
||||
"@volar/source-map": "2.4.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/source-map": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.11.1.tgz",
|
||||
"integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
|
||||
"version": "2.4.15",
|
||||
"resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz",
|
||||
"integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"muggle-string": "^0.3.1"
|
||||
}
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@volar/typescript": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-1.11.1.tgz",
|
||||
"integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
|
||||
"version": "2.4.15",
|
||||
"resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz",
|
||||
"integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "1.11.1",
|
||||
"path-browserify": "^1.0.1"
|
||||
"@volar/language-core": "2.4.15",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vscode-uri": "^3.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
@@ -1298,22 +1296,32 @@
|
||||
"@vue/shared": "3.5.29"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core": {
|
||||
"version": "1.8.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.27.tgz",
|
||||
"integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
|
||||
"node_modules/@vue/compiler-vue2": {
|
||||
"version": "2.7.16",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
|
||||
"integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "~1.11.1",
|
||||
"@volar/source-map": "~1.11.1",
|
||||
"@vue/compiler-dom": "^3.3.0",
|
||||
"@vue/shared": "^3.3.0",
|
||||
"computeds": "^0.0.1",
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core": {
|
||||
"version": "2.2.12",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz",
|
||||
"integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "2.4.15",
|
||||
"@vue/compiler-dom": "^3.5.0",
|
||||
"@vue/compiler-vue2": "^2.7.16",
|
||||
"@vue/shared": "^3.5.0",
|
||||
"alien-signals": "^1.0.3",
|
||||
"minimatch": "^9.0.3",
|
||||
"muggle-string": "^0.3.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vue-template-compiler": "^2.7.14"
|
||||
"muggle-string": "^0.4.1",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
@@ -1374,6 +1382,13 @@
|
||||
"integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/alien-signals": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz",
|
||||
"integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/any-promise": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz",
|
||||
@@ -1616,13 +1631,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/computeds": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz",
|
||||
"integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -2033,9 +2041,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/muggle-string": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.3.1.tgz",
|
||||
"integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2881,19 +2889,6 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -3235,6 +3230,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.29.tgz",
|
||||
@@ -3257,33 +3259,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-template-compiler": {
|
||||
"version": "2.7.16",
|
||||
"resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
|
||||
"integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-tsc": {
|
||||
"version": "1.8.27",
|
||||
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-1.8.27.tgz",
|
||||
"integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==",
|
||||
"version": "2.2.12",
|
||||
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz",
|
||||
"integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/typescript": "~1.11.1",
|
||||
"@vue/language-core": "1.8.27",
|
||||
"semver": "^7.5.4"
|
||||
"@volar/typescript": "2.4.15",
|
||||
"@vue/language-core": "2.2.12"
|
||||
},
|
||||
"bin": {
|
||||
"vue-tsc": "bin/vue-tsc.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"name":"egm-webui","version":"0.1.0","type":"module","scripts":{"dev":"vite","build":"vue-tsc && vite build","preview":"vite preview"},"dependencies":{"@quasar/extras":"^1.17.0","@quasar/vite-plugin":"^1.10.0","quasar":"^2.14.0","vue":"^3.4.0"},"devDependencies":{"@vitejs/plugin-vue":"^5.0.0","autoprefixer":"^10.4.0","postcss":"^8.4.0","sass-embedded":"^1.97.3","tailwindcss":"^3.4.0","typescript":"^5.3.0","vite":"^5.0.0","vue-tsc":"^1.8.0"}}
|
||||
{"name":"egm-webui","version":"0.1.0","type":"module","scripts":{"dev":"vite","build":"vue-tsc && vite build","preview":"vite preview"},"dependencies":{"@quasar/extras":"^1.17.0","@quasar/vite-plugin":"^1.10.0","quasar":"^2.14.0","vue":"^3.4.0"},"devDependencies":{"@vitejs/plugin-vue":"^5.0.0","autoprefixer":"^10.4.0","postcss":"^8.4.0","sass-embedded":"^1.97.3","tailwindcss":"^3.4.0","typescript":"^5.3.0","vite":"^5.0.0","vue-tsc":"^2.0.0"}}
|
||||
257
webui/src/components/Animation.vue
Normal file
257
webui/src/components/Animation.vue
Normal file
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<q-card class="shadow-2">
|
||||
<q-card-section class="bg-indigo-50 cursor-pointer" @click="toggleExpand">
|
||||
<div class="text-h6 text-indigo-900 flex items-center gap-2">
|
||||
<q-icon name="animation" />
|
||||
EGM 动画可视化
|
||||
<q-space />
|
||||
<q-icon :name="expanded ? 'expand_less' : 'expand_more'" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-slide-transition>
|
||||
<q-card-section v-show="expanded">
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
class="animation-canvas"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-slide-transition>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// Canvas 尺寸
|
||||
const canvasWidth = 600
|
||||
const canvasHeight = 600
|
||||
|
||||
// 坐标范围(对应 animation.py 的 [-500, 500])
|
||||
const coordRange = {
|
||||
xMin: -500,
|
||||
xMax: 500,
|
||||
yMin: -500,
|
||||
yMax: 500
|
||||
}
|
||||
|
||||
// 展开/折叠状态
|
||||
const expanded = ref(false)
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
let tick = 0
|
||||
|
||||
// 切换展开/折叠
|
||||
const toggleExpand = () => {
|
||||
expanded.value = !expanded.value
|
||||
if (expanded.value) {
|
||||
// 展开时初始化画布
|
||||
setTimeout(() => {
|
||||
if (canvasRef.value) {
|
||||
ctx = canvasRef.value.getContext('2d')
|
||||
initFig()
|
||||
}
|
||||
}, 350) // 等待动画完成
|
||||
}
|
||||
}
|
||||
|
||||
// 坐标转换:数据坐标 -> Canvas 坐标
|
||||
const toCanvasX = (x: number): number => {
|
||||
return ((x - coordRange.xMin) / (coordRange.xMax - coordRange.xMin)) * canvasWidth
|
||||
}
|
||||
|
||||
const toCanvasY = (y: number): number => {
|
||||
// Canvas Y 轴向下,需要反转
|
||||
return canvasHeight - ((y - coordRange.yMin) / (coordRange.yMax - coordRange.yMin)) * canvasHeight
|
||||
}
|
||||
|
||||
// 初始化画布
|
||||
const initFig = () => {
|
||||
if (!ctx || !expanded.value) return
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight)
|
||||
|
||||
// 绘制坐标轴
|
||||
ctx.strokeStyle = '#e0e0e0'
|
||||
ctx.lineWidth = 1
|
||||
|
||||
// X 轴
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, toCanvasY(0))
|
||||
ctx.lineTo(canvasWidth, toCanvasY(0))
|
||||
ctx.stroke()
|
||||
|
||||
// Y 轴
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(toCanvasX(0), 0)
|
||||
ctx.lineTo(toCanvasX(0), canvasHeight)
|
||||
ctx.stroke()
|
||||
|
||||
// 绘制刻度
|
||||
ctx.fillStyle = '#666'
|
||||
ctx.font = '10px Arial'
|
||||
ctx.textAlign = 'center'
|
||||
|
||||
for (let x = coordRange.xMin; x <= coordRange.xMax; x += 100) {
|
||||
if (x !== 0) {
|
||||
const canvasX = toCanvasX(x)
|
||||
ctx.fillText(x.toString(), canvasX, toCanvasY(0) + 15)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.textAlign = 'right'
|
||||
for (let y = coordRange.yMin; y <= coordRange.yMax; y += 100) {
|
||||
if (y !== 0) {
|
||||
const canvasY = toCanvasY(y)
|
||||
ctx.fillText(y.toString(), toCanvasX(0) - 5, canvasY + 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清除画布
|
||||
const clear = () => {
|
||||
if (!ctx) return
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight)
|
||||
}
|
||||
|
||||
// 添加 RG 线(地面线)
|
||||
const addRgLine = (points: [number, number][]) => {
|
||||
if (!ctx || !expanded.value || !points || points.length === 0) return
|
||||
|
||||
ctx.strokeStyle = '#2196F3'
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
|
||||
points.forEach((point, index) => {
|
||||
const canvasX = toCanvasX(point[0])
|
||||
const canvasY = toCanvasY(point[1])
|
||||
if (index === 0) {
|
||||
ctx!.moveTo(canvasX, canvasY)
|
||||
} else {
|
||||
ctx!.lineTo(canvasX, canvasY)
|
||||
}
|
||||
})
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// 添加 RS 圆(地线保护弧)- 这是每帧第一个绘制的元素,先清除画布
|
||||
const addRs = (rs: number, rsX: number, rsY: number) => {
|
||||
if (!ctx || !expanded.value) return
|
||||
|
||||
// 清除并重新初始化画布,准备绘制新的一帧
|
||||
clear()
|
||||
initFig()
|
||||
|
||||
const canvasX = toCanvasX(rsX)
|
||||
const canvasY = toCanvasY(rsY)
|
||||
const canvasRadius = rs * (canvasWidth / (coordRange.xMax - coordRange.xMin))
|
||||
|
||||
ctx.strokeStyle = '#4CAF50'
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
ctx.arc(canvasX, canvasY, canvasRadius, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// 添加 RC 圆(导线暴露弧)
|
||||
const addRc = (rc: number, rcX: number, rcY: number) => {
|
||||
if (!ctx || !expanded.value) return
|
||||
|
||||
const canvasX = toCanvasX(rcX)
|
||||
const canvasY = toCanvasY(rcY)
|
||||
const canvasRadius = rc * (canvasWidth / (coordRange.xMax - coordRange.xMin))
|
||||
|
||||
ctx.strokeStyle = '#FF9800'
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
ctx.arc(canvasX, canvasY, canvasRadius, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// 添加暴露弧区域(两条红线)
|
||||
const addExposeArea = (
|
||||
rcX: number,
|
||||
rcY: number,
|
||||
intersectionX1: number,
|
||||
intersectionY1: number,
|
||||
intersectionX2: number,
|
||||
intersectionY2: number
|
||||
) => {
|
||||
if (!ctx || !expanded.value) return
|
||||
|
||||
ctx.strokeStyle = '#F44336'
|
||||
ctx.lineWidth = 3
|
||||
|
||||
// 第一条线
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(toCanvasX(rcX), toCanvasY(rcY))
|
||||
ctx.lineTo(toCanvasX(intersectionX1), toCanvasY(intersectionY1))
|
||||
ctx.stroke()
|
||||
|
||||
// 第二条线
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(toCanvasX(rcX), toCanvasY(rcY))
|
||||
ctx.lineTo(toCanvasX(intersectionX2), toCanvasY(intersectionY2))
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// 暂停并刷新 - 用于下一帧绘制前清除
|
||||
const pause = () => {
|
||||
if (!ctx || !expanded.value) return
|
||||
|
||||
tick += 1
|
||||
// 不立即清除,等待下一次绑图时清除
|
||||
// 这样用户可以看到当前帧
|
||||
}
|
||||
|
||||
// 暴露方法给父组件或全局调用
|
||||
const animationApi = {
|
||||
enable: (enable: boolean) => {
|
||||
expanded.value = enable
|
||||
if (enable && canvasRef.value) {
|
||||
ctx = canvasRef.value.getContext('2d')
|
||||
initFig()
|
||||
}
|
||||
},
|
||||
isEnabled: () => expanded.value,
|
||||
initFig,
|
||||
clear,
|
||||
addRgLine,
|
||||
addRs,
|
||||
addRc,
|
||||
addExposeArea,
|
||||
pause
|
||||
}
|
||||
|
||||
// 注册到全局,供后端调用
|
||||
declare global {
|
||||
interface Window {
|
||||
animationApi?: typeof animationApi
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 注册全局 API
|
||||
window.animationApi = animationApi
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.animationApi = undefined
|
||||
})
|
||||
|
||||
// 导出方法供父组件使用
|
||||
defineExpose(animationApi)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animation-canvas {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -39,7 +39,7 @@ interface LogEntry {
|
||||
|
||||
const logs = ref<LogEntry[]>([])
|
||||
const logContainer = ref<HTMLElement | null>(null)
|
||||
const expanded = ref(true)
|
||||
const expanded = ref(false) // 默认折叠
|
||||
const lastTripRates = ref<number[]>([])
|
||||
|
||||
const addLog = (level: LogEntry['level'], message: string) => {
|
||||
|
||||
@@ -109,6 +109,24 @@
|
||||
<q-tooltip>一年中雷暴天数,用于计算地闪密度</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.parameter.z_0"
|
||||
type="number"
|
||||
label="雷电波阻抗 (Ω)"
|
||||
>
|
||||
<q-tooltip>雷电波阻抗,用于计算最小雷电流。</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.parameter.z_c"
|
||||
type="number"
|
||||
label="导线波阻抗 (Ω)"
|
||||
>
|
||||
<q-tooltip>导线波阻抗,用于计算最小雷电流。</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导、地线挂点高度 -->
|
||||
@@ -117,7 +135,7 @@
|
||||
导、地线挂点垂直坐标 (m)
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col" v-for="(h, index) in params.parameter.h_arm" :key="index">
|
||||
<div class="col" v-for="(_, index) in params.parameter.h_arm" :key="index">
|
||||
<q-input
|
||||
v-model="params.parameter.h_arm[index]"
|
||||
type="number"
|
||||
@@ -141,7 +159,7 @@
|
||||
导、地线挂点水平坐标 (m)
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col" v-for="(x, index) in params.parameter.gc_x" :key="index">
|
||||
<div class="col" v-for="(_, index) in params.parameter.gc_x" :key="index">
|
||||
<q-input
|
||||
v-model="params.parameter.gc_x[index]"
|
||||
type="number"
|
||||
@@ -351,6 +369,9 @@
|
||||
<!-- 运行日志 -->
|
||||
<LogComponent ref="logRef" />
|
||||
|
||||
<!-- EGM 动画可视化 -->
|
||||
<Animation ref="animationRef" class="q-mt-md" />
|
||||
|
||||
</div>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
@@ -361,6 +382,7 @@
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import type { AllParameters } from '@/types'
|
||||
import LogComponent from './Log.vue'
|
||||
import Animation from './Animation.vue'
|
||||
|
||||
// 默认参数
|
||||
const defaultParams: AllParameters = {
|
||||
@@ -376,7 +398,9 @@ const defaultParams: AllParameters = {
|
||||
gc_x: [17.9, 17],
|
||||
ground_angels: [0],
|
||||
altitude: 1000,
|
||||
td: 20
|
||||
td: 20,
|
||||
z_0: 300,
|
||||
z_c: 251
|
||||
},
|
||||
advance: {
|
||||
ng: -1,
|
||||
@@ -394,6 +418,7 @@ const calculating = ref(false)
|
||||
const result = ref<{ tripping_rate: number; n_sf_phases: number[]; message: string } | null>(null)
|
||||
const error = ref<string | null>(null)
|
||||
const logRef = ref<InstanceType<typeof LogComponent> | null>(null)
|
||||
const animationRef = ref<InstanceType<typeof Animation> | null>(null)
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
// 雷电流概率密度系数设置开关
|
||||
const showIpCoefficients = ref(false)
|
||||
@@ -532,7 +557,12 @@ const calculate = async () => {
|
||||
if (window.pywebview) {
|
||||
// 后台线程启动计算,实时日志通过 addLogFromBackend 推送
|
||||
// 结果通过 receiveResult 回调接收
|
||||
await window.pywebview.api.calculate(params)
|
||||
// 传递动画启用状态
|
||||
const paramsWithAnimation = {
|
||||
...params,
|
||||
animation_enabled: animationRef.value?.isEnabled() ?? false
|
||||
}
|
||||
await window.pywebview.api.calculate(paramsWithAnimation)
|
||||
// 不在这里设置 calculating = false,等待 receiveResult 回调
|
||||
} else {
|
||||
// 开发模式下的模拟
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// EGM 计算参数类型定义
|
||||
// Version: 1.0.11
|
||||
|
||||
export interface Parameter {
|
||||
// 基本参数
|
||||
@@ -14,6 +15,8 @@ export interface Parameter {
|
||||
ground_angels: number[] // 地面倾角 (°)
|
||||
altitude: number // 海拔高度 (m)
|
||||
td: number // 雷暴日 (d)
|
||||
z_0: number // 雷电波阻抗 (Ω),默认 300
|
||||
z_c: number // 导线波阻抗 (Ω),默认 251
|
||||
}
|
||||
|
||||
export interface AdvanceParameter {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [
|
||||
vue({
|
||||
template: { transformAssetUrls }
|
||||
|
||||
140
webview_app.py
140
webview_app.py
@@ -23,6 +23,127 @@ from core import Parameter
|
||||
from main import parameter_display, run_egm
|
||||
|
||||
|
||||
class WebAnimation:
|
||||
"""
|
||||
Web 动画类,将 Python 端的 Animation 调用映射到前端 JavaScript
|
||||
对应 Animation.vue 的功能
|
||||
|
||||
注意:动画的启用/禁用由前端用户通过"启用动画"开关控制,
|
||||
后端只负责发送绘制指令,前端根据 enabled 状态决定是否执行
|
||||
"""
|
||||
|
||||
def __init__(self, window=None):
|
||||
self._window = window
|
||||
|
||||
def set_window(self, window):
|
||||
"""设置窗口对象"""
|
||||
self._window = window
|
||||
|
||||
def enable(self, enabled: bool):
|
||||
"""
|
||||
启用/禁用动画(由前端用户控制)
|
||||
此方法保留以兼容接口,但实际启用状态由前端控制
|
||||
"""
|
||||
if self._window:
|
||||
js_code = f'if(window.animationApi){{window.animationApi.enable({str(enabled).lower()})}}'
|
||||
self._window.evaluate_js(js_code)
|
||||
|
||||
def init_fig(self):
|
||||
"""初始化画布"""
|
||||
if not self._window:
|
||||
return
|
||||
js_code = 'if(window.animationApi){window.animationApi.initFig()}'
|
||||
self._window.evaluate_js(js_code)
|
||||
|
||||
def add_rs(self, rs: float, rs_x: float, rs_y: float):
|
||||
"""
|
||||
添加地线保护弧(RS 圆)
|
||||
对应 animation.py 的 add_rs 方法
|
||||
"""
|
||||
if not self._window:
|
||||
return
|
||||
js_code = f'if(window.animationApi){{window.animationApi.addRs({rs}, {rs_x}, {rs_y})}}'
|
||||
self._window.evaluate_js(js_code)
|
||||
|
||||
def add_rc(self, rc: float, rc_x: float, rc_y: float):
|
||||
"""
|
||||
添加导线暴露弧(RC 圆)
|
||||
对应 animation.py 的 add_rc 方法
|
||||
"""
|
||||
if not self._window:
|
||||
return
|
||||
js_code = f'if(window.animationApi){{window.animationApi.addRc({rc}, {rc_x}, {rc_y})}}'
|
||||
self._window.evaluate_js(js_code)
|
||||
|
||||
def add_rg_line(self, line_func):
|
||||
"""
|
||||
添加地面线(RG 线)
|
||||
对应 animation.py 的 add_rg_line 方法
|
||||
|
||||
Args:
|
||||
line_func: 一个函数,接收 x 返回 y
|
||||
"""
|
||||
if not self._window:
|
||||
return
|
||||
# 生成线上的点,传递给前端
|
||||
# 由于无法直接传递函数,我们预先计算一些点
|
||||
import numpy as np
|
||||
x_points = np.linspace(0, 300, 50)
|
||||
y_points = [line_func(x) for x in x_points]
|
||||
points = list(zip(x_points.tolist(), y_points))
|
||||
|
||||
js_code = f'''
|
||||
if(window.animationApi){{
|
||||
window.animationApi.addRgLine({json.dumps(points)})
|
||||
}}
|
||||
'''
|
||||
self._window.evaluate_js(js_code)
|
||||
|
||||
def add_expose_area(
|
||||
self,
|
||||
rc_x: float,
|
||||
rc_y: float,
|
||||
intersection_x1: float,
|
||||
intersection_y1: float,
|
||||
intersection_x2: float,
|
||||
intersection_y2: float
|
||||
):
|
||||
"""
|
||||
添加暴露弧区域(两条红线)
|
||||
对应 animation.py 的 add_expose_area 方法
|
||||
"""
|
||||
if not self._window:
|
||||
return
|
||||
js_code = f'''if(window.animationApi){{
|
||||
window.animationApi.addExposeArea(
|
||||
{rc_x}, {rc_y},
|
||||
{intersection_x1}, {intersection_y1},
|
||||
{intersection_x2}, {intersection_y2}
|
||||
)
|
||||
}}'''
|
||||
self._window.evaluate_js(js_code)
|
||||
|
||||
def clear(self):
|
||||
"""清除画布"""
|
||||
if not self._window:
|
||||
return
|
||||
js_code = 'if(window.animationApi){window.animationApi.clear()}'
|
||||
self._window.evaluate_js(js_code)
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
暂停并刷新
|
||||
对应 animation.py 的 pause 方法
|
||||
"""
|
||||
if not self._window:
|
||||
return
|
||||
js_code = 'if(window.animationApi){window.animationApi.pause()}'
|
||||
self._window.evaluate_js(js_code)
|
||||
# 添加延迟以便动画可见
|
||||
import time
|
||||
time.sleep(0.1) # 增加延迟,让用户看清动画
|
||||
|
||||
|
||||
class WebHandler:
|
||||
"""Web日志处理器"""
|
||||
def __init__(self, callback=None):
|
||||
@@ -86,6 +207,7 @@ class EGMWebApp:
|
||||
self._loguru_handler_id = None
|
||||
self._log_queue: queue.Queue = queue.Queue()
|
||||
self._running = False
|
||||
self.animation = WebAnimation() # Web 动画实例
|
||||
|
||||
def _process_log_queue(self):
|
||||
"""处理日志队列,在主线程中定时调用"""
|
||||
@@ -210,6 +332,8 @@ class EGMWebApp:
|
||||
|
||||
para.voltage_n = int(optional_data.get('voltage_n', 3))
|
||||
para.max_i = float(optional_data.get('max_i', 200))
|
||||
para.z_0 = float(parameter_data.get('z_0', 300))
|
||||
para.z_c = float(parameter_data.get('z_c', 251))
|
||||
|
||||
# 设置 ac_or_dc 参数
|
||||
ac_or_dc_value = str(parameter_data.get('ac_or_dc', 'AC'))
|
||||
@@ -220,8 +344,12 @@ class EGMWebApp:
|
||||
|
||||
logger.info("开始执行 EGM 计算...")
|
||||
|
||||
# 根据前端动画启用状态决定是否传递 animation 对象
|
||||
animation_enabled = params.get('animation_enabled', False)
|
||||
animation_obj = self.animation if animation_enabled else None
|
||||
|
||||
# 调用 main.py 的核心计算函数
|
||||
result = run_egm(para)
|
||||
result = run_egm(para, animation_obj)
|
||||
|
||||
self.add_log("info", "EGM 计算完成")
|
||||
|
||||
@@ -434,7 +562,14 @@ def start_webview():
|
||||
# 确定前端 URL
|
||||
# 在开发环境中使用 Vite 开发服务器
|
||||
# 在生产环境中使用构建后的文件
|
||||
dev_mode = os.getenv('EGM_DEV_MODE', 'true').lower() == 'true'
|
||||
|
||||
# 检查是否在打包环境中运行
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包环境:强制使用生产模式,禁用调试
|
||||
dev_mode = False
|
||||
else:
|
||||
# 开发环境:通过环境变量控制
|
||||
dev_mode = os.getenv('EGM_DEV_MODE', 'true').lower() == 'true'
|
||||
|
||||
if dev_mode:
|
||||
# 开发模式:使用 Vite 开发服务器
|
||||
@@ -467,6 +602,7 @@ def start_webview():
|
||||
|
||||
# 将窗口对象传递给 API
|
||||
api.window = window
|
||||
api.animation.set_window(window)
|
||||
|
||||
# 启动
|
||||
logger.info("启动 EGM Web 界面...")
|
||||
|
||||
Reference in New Issue
Block a user