Compare commits

..

8 Commits

Author SHA1 Message Date
dmy
8a74a576c0 feat: 添加版本管理功能并更新依赖
添加版本管理脚本和文件,更新webui依赖至最新版本,优化参数表单显示
2026-03-03 18:06:01 +08:00
dmy
0927c94a23 feat: 添加GUI构建目标并更新文档
添加Makefile中的GUI构建目标,更新README文档结构,调整默认参数值,优化webview应用的生产环境检测逻辑
2026-03-03 17:36:19 +08:00
dmy
68328a68f1 feat: 在参数显示中添加更多字段并简化条件判断 2026-03-03 16:56:10 +08:00
dmy
4aa56c71d5 feat: 添加雷电波阻抗和导线波阻抗参数支持
在参数类中添加z_0和z_c字段,并在计算最小雷电流时使用这些参数
更新前端表单和类型定义以支持新参数
修改webview应用以接收并处理新参数
2026-03-03 16:51:28 +08:00
dmy
fd6684c884 fix: 默认折叠日志面板 2026-03-03 16:35:28 +08:00
dmy
c19e7b7631 feat: 将动画组件改为可折叠式设计 2026-03-03 16:17:40 +08:00
dmy
07063ec638 feat: 将动画启用控制权交给前端用户
后端不再主动启用动画,改为由前端通过开关控制动画状态
移除后端冗余的动画禁用逻辑,仅根据前端状态传递动画对象
2026-03-03 16:09:03 +08:00
dmy
a65ce23cee feat: 添加 EGM 计算动画可视化功能
在 web 界面中实现 EGM 计算过程的动画展示,包括地线保护弧、导线暴露弧和地面线的动态绘制。重构 main.py 以支持可选的动画参数传递,并新增 Animation.vue 组件和 WebAnimation 类实现前后端交互。
2026-03-03 15:58:57 +08:00
19 changed files with 653 additions and 126 deletions

View File

@@ -1,6 +1,15 @@
target: dist build target: dist build
create-version-file metadata.yml --outfile build/file_version_info.txt uv run python update_version.py
pyinstaller -F main.py --version-file build/file_version_info.txt -n Lightening 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: dist:
mkdir dist mkdir dist

View File

@@ -18,6 +18,7 @@
### 环境要求 ### 环境要求
- Python >= 3.12 - Python >= 3.12
- Node.js >= 18图形界面开发需要
### 安装依赖 ### 安装依赖
@@ -122,10 +123,10 @@ python webview_app.py
rated_voltage = 750 # 额定电压等级 (kV) rated_voltage = 750 # 额定电压等级 (kV)
h_c_sag = 14.43 # 导线弧垂 (m) h_c_sag = 14.43 # 导线弧垂 (m)
h_g_sag = 11.67 # 地线弧垂 (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_c_len = 9.2 # 导线串长 (m)
string_g_len = 0.5 # 地线串长 (m) string_g_len = 0.5 # 地线串长 (m)
h_arm = [150, 130] # 导、地线挂点垂直距离 (m),第一个值为地线挂点高度 h_arm = [130, 100] # 导、地线挂点垂直距离 (m),第一个值为地线挂点高度
gc_x = [17.9, 17] # 导、地线水平坐标 (m) gc_x = [17.9, 17] # 导、地线水平坐标 (m)
ground_angels = [0] # 地面倾角 (°),向下为正,支持多个角度 ground_angels = [0] # 地面倾角 (°),向下为正,支持多个角度
altitude = 1000 # 海拔高度 (m) altitude = 1000 # 海拔高度 (m)
@@ -141,7 +142,7 @@ Ip_a = -1 # 雷电流概率密度曲线系数a大于0时使用此
Ip_b = -1 # 雷电流概率密度曲线系数b大于0时使用此值 Ip_b = -1 # 雷电流概率密度曲线系数b大于0时使用此值
``` ```
注意:当 `ng` > 0 时,不会通过雷暴日计算地闪密度;当 `Ip_a``Ip_b` > 0 时,不会使用默认雷暴日对应的概率密度。 **注意**:当 `ng` > 0 时,不会通过雷暴日计算地闪密度;当 `Ip_a``Ip_b` > 0 时,不会使用默认雷暴日对应的概率密度。
### [optional] - 可选参数 ### [optional] - 可选参数
@@ -189,25 +190,28 @@ EGM/
├── animation.py # 动画演示模块 ├── animation.py # 动画演示模块
├── webview_app.py # pywebview 图形界面后端 ├── webview_app.py # pywebview 图形界面后端
├── article.toml # 示例配置文件 ├── article.toml # 示例配置文件
├── default.toml # 默认配置文件
├── Makefile # 构建脚本 ├── Makefile # 构建脚本
├── pyproject.toml # 项目配置 ├── pyproject.toml # 项目配置
├── README.md # 说明文档 ├── README.md # 说明文档
├── webui/ # 图形界面前端项目 ├── webui/ # 图形界面前端项目
│ ├── src/ │ ├── src/
│ │ ├── components/ │ │ ├── components/
│ │ │ ── ParameterForm.vue │ │ │ ── ParameterForm.vue # 参数表单组件
│ │ │ ├── Animation.vue # 动画可视化组件
│ │ │ └── Log.vue # 日志显示组件
│ │ ├── types/ │ │ ├── types/
│ │ │ └── index.ts │ │ │ └── index.ts # TypeScript 类型定义
│ │ ├── App.vue │ │ ├── App.vue # 主应用组件
│ │ ├── main.ts │ │ ├── main.ts # 应用入口
│ │ └── style.css │ │ └── style.css # 全局样式
│ ├── package.json │ ├── package.json # 前端依赖配置
│ ├── vite.config.ts │ ├── vite.config.ts # Vite 配置
│ ├── tsconfig.json │ ├── tsconfig.json # TypeScript 配置
│ ├── tailwind.config.js │ ├── tailwind.config.js # Tailwind CSS 配置
── postcss.config.js ── index.html # HTML 入口
│ └── index.html ├── CSharp/ # C# 版本实现
└── CSharp/ # C# 版本实现 └── 历史/ # 历史配置文件和DXF文件
``` ```
## 技术支持 ## 技术支持

1
VERSION Normal file
View File

@@ -0,0 +1 @@
1.0.12

View File

@@ -26,6 +26,8 @@ class Parameter:
Ip_a: float # 概率密度曲线系数a Ip_a: float # 概率密度曲线系数a
Ip_b: float # 概率密度曲线系数b Ip_b: float # 概率密度曲线系数b
ac_or_dc: str # 交流或直流标识,"AC" 或 "DC",默认 "AC" ac_or_dc: str # 交流或直流标识,"AC" 或 "DC",默认 "AC"
z_0: float # 雷电波阻抗,默认 300
z_c: float # 导线波阻抗,默认 251
def rg_line_function_factory(_rg, ground_angel): # 返回一个地面捕雷线的直线方程 def rg_line_function_factory(_rg, ground_angel): # 返回一个地面捕雷线的直线方程
@@ -185,7 +187,7 @@ def solve_circle_line_intersection(
return [_x, _y] 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: if altitude > 1000:
k_a = math.exp((altitude - 1000) / 8150) # 气隙海拔修正 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 = 1 / k_a * (477 * string_len + 99) # 串放电路径 2000m海拔
# u_50 = 615 * string_len # 导线对塔身放电 1000m海拔 # u_50 = 615 * string_len # 导线对塔身放电 1000m海拔
# u_50= 263.32647401+533.90081562*string_len # u_50= 263.32647401+533.90081562*string_len
z_0 = 300 # 雷电波阻抗
z_c = 251 # 导线波阻抗
# 新版大手册公式 3-277 # 新版大手册公式 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 = (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 # r = 2 * (u_50 - u_ph) / z_c

View File

@@ -1,7 +1,7 @@
import math import math
import os.path import os.path
import sys import sys
import tomli import tomllib as tomli
from loguru import logger from loguru import logger
from core import * from core import *
import timeit import timeit

32
main.py
View File

@@ -2,16 +2,16 @@ import math
import os.path import os.path
import sys import sys
import time import time
import tomli import tomllib as tomli
from loguru import logger from loguru import logger
from core import * from core import *
import timeit import timeit
from animation import Animation
# 打印参数 # 打印参数
def parameter_display(para_dis: Parameter): def parameter_display(para_dis: Parameter):
logger.info(f"额定电压 kV {para_dis.rated_voltage}") 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_c_sag}")
logger.info(f"地线弧垂 m {para_dis.h_g_sag}") logger.info(f"地线弧垂 m {para_dis.h_g_sag}")
logger.info(f"全塔高 m {para_dis.h_arm[0]}") logger.info(f"全塔高 m {para_dis.h_arm[0]}")
@@ -22,13 +22,17 @@ def parameter_display(para_dis: Parameter):
logger.info(f"挂点水平坐标 m {para_dis.gc_x}") 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"地面倾角 ° {[an * 180 / math.pi for an in para_dis.ground_angels]}")
logger.info(f"海拔高度 m {para_dis.altitude}") logger.info(f"海拔高度 m {para_dis.altitude}")
if para_dis.ng > 0: logger.info(f"雷电波阻抗 Ω {para_dis.z_0}")
logger.info("不采用雷暴日计算地闪密度和雷电流密度") 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"地闪密度 次/(每平方公里·每年) {para_dis.ng}")
logger.info(f"概率密度曲线系数a {para_dis.Ip_a}") logger.info(f"概率密度曲线系数a {para_dis.Ip_a}")
logger.info(f"概率密度曲线系数b {para_dis.Ip_b}") logger.info(f"概率密度曲线系数b {para_dis.Ip_b}")
pass # pass
else: # else:
logger.info(f"雷暴日 d {para_dis.td}") logger.info(f"雷暴日 d {para_dis.td}")
@@ -62,11 +66,12 @@ def read_parameter(toml_file_path) -> Parameter:
return para return para
def run_egm(para: Parameter) -> dict: def run_egm(para: Parameter, animation=None) -> dict:
""" """
执行 EGM 计算的核心函数,可被外部调用。 执行 EGM 计算的核心函数,可被外部调用。
Args: Args:
para: 参数对象,包含所有计算所需的参数。 para: 参数对象,包含所有计算所需的参数。
animation: 可选的动画对象,用于可视化。需要实现 add_rs, add_rc, add_rg_line, add_expose_area, pause 方法。
Returns: Returns:
计算结果字典。 计算结果字典。
""" """
@@ -104,9 +109,9 @@ def run_egm(para: Parameter) -> dict:
ng = func_ng(td, para.ng) ng = func_ng(td, para.ng)
avr_n_sf = 0 # 考虑电压的影响计算的跳闸率 avr_n_sf = 0 # 考虑电压的影响计算的跳闸率
ground_angels = para.ground_angels ground_angels = para.ground_angels
# 初始化动画 # 动画对象:如果传入了 animation 则使用,否则不启用动画
animate = Animation() # 注意:动画的启用由前端用户通过"启用动画"开关控制,后端不主动启用
animate.enable(False) animate = animation
# animate.show() # animate.show()
for ground_angel in ground_angels: for ground_angel in ground_angels:
logger.info(f"地面倾角{ground_angel/math.pi*180:.3f}°") 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 insulator_c_len = para.insulator_c_len
# i_min = min_i(insulator_c_len, u_ph / 1.732) # i_min = min_i(insulator_c_len, u_ph / 1.732)
# TODO 需要考虑交、直流 # 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 # 尝试的最小电流 _min_i = i_min # 尝试的最小电流
_max_i = para.max_i # 尝试的最大电流 _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) # cad.draw(i_min, u_ph, rs_x, rs_y, rc_x, rc_y, rg_x, rg_y, rg_type, 2)
@@ -179,13 +184,16 @@ def run_egm(para: Parameter) -> dict:
): # 雷电流 ): # 雷电流
logger.info(f"尝试计算电流为{i_bar:.2f}") logger.info(f"尝试计算电流为{i_bar:.2f}")
rs = rs_fun(i_bar) rs = rs_fun(i_bar)
if animate:
animate.add_rs(rs, rs_x, rs_y) animate.add_rs(rs, rs_x, rs_y)
rc = rc_fun(i_bar, u_ph) rc = rc_fun(i_bar, u_ph)
if animate:
animate.add_rc(rc, rc_x, rc_y) animate.add_rc(rc, rc_x, rc_y)
rg = rg_fun(i_bar, rc_y, u_ph, typ=rg_type) rg = rg_fun(i_bar, rc_y, u_ph, typ=rg_type)
rg_line_func = None rg_line_func = None
if rg_type == "g": if rg_type == "g":
rg_line_func = rg_line_function_factory(rg, ground_angel) rg_line_func = rg_line_function_factory(rg, ground_angel)
if animate:
animate.add_rg_line(rg_line_func) animate.add_rg_line(rg_line_func)
rs_rc_circle_intersection = solve_circle_intersection( rs_rc_circle_intersection = solve_circle_intersection(
rs, rc, rs_x, rs_y, rc_x, rc_y rs, rc, rs_x, rs_y, rc_x, rc_y
@@ -219,6 +227,7 @@ def run_egm(para: Parameter) -> dict:
"上面的导地线无法保护下面的导地线,检查设置参数。" "上面的导地线无法保护下面的导地线,检查设置参数。"
) )
continue continue
if animate:
animate.add_expose_area( animate.add_expose_area(
rc_x, rc_x,
rc_y, rc_y,
@@ -298,6 +307,7 @@ def run_egm(para: Parameter) -> dict:
logger.info(f"电流为{i_bar}kA时暴露弧已经完全被屏蔽") logger.info(f"电流为{i_bar}kA时暴露弧已经完全被屏蔽")
exposed_curve_shielded = True exposed_curve_shielded = True
break break
if animate:
animate.pause() animate.pause()
# 判断是否导线已经被完全保护 # 判断是否导线已经被完全保护
if abs(i_max - _max_i) < 1e-5: if abs(i_max - _max_i) < 1e-5:

4
metadata.yml Normal file
View 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
View File

@@ -0,0 +1,6 @@
{
"name": "EGM",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

76
update_version.py Normal file
View 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()

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EGM 输电线路绕击跳闸率计算</title> <title>EGM 输电线路绕击跳闸率计算 v1.0.12</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

130
webui/package-lock.json generated
View File

@@ -21,7 +21,7 @@
"tailwindcss": "^3.4.0", "tailwindcss": "^3.4.0",
"typescript": "^5.3.0", "typescript": "^5.3.0",
"vite": "^5.0.0", "vite": "^5.0.0",
"vue-tsc": "^1.8.0" "vue-tsc": "^2.0.0"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@@ -1218,34 +1218,32 @@
} }
}, },
"node_modules/@volar/language-core": { "node_modules/@volar/language-core": {
"version": "1.11.1", "version": "2.4.15",
"resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.11.1.tgz", "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz",
"integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/source-map": "1.11.1" "@volar/source-map": "2.4.15"
} }
}, },
"node_modules/@volar/source-map": { "node_modules/@volar/source-map": {
"version": "1.11.1", "version": "2.4.15",
"resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.11.1.tgz", "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz",
"integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"dependencies": {
"muggle-string": "^0.3.1"
}
}, },
"node_modules/@volar/typescript": { "node_modules/@volar/typescript": {
"version": "1.11.1", "version": "2.4.15",
"resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-1.11.1.tgz", "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz",
"integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/language-core": "1.11.1", "@volar/language-core": "2.4.15",
"path-browserify": "^1.0.1" "path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8"
} }
}, },
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
@@ -1298,22 +1296,32 @@
"@vue/shared": "3.5.29" "@vue/shared": "3.5.29"
} }
}, },
"node_modules/@vue/language-core": { "node_modules/@vue/compiler-vue2": {
"version": "1.8.27", "version": "2.7.16",
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.27.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
"integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/language-core": "~1.11.1", "de-indent": "^1.0.2",
"@volar/source-map": "~1.11.1", "he": "^1.2.0"
"@vue/compiler-dom": "^3.3.0", }
"@vue/shared": "^3.3.0", },
"computeds": "^0.0.1", "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", "minimatch": "^9.0.3",
"muggle-string": "^0.3.1", "muggle-string": "^0.4.1",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1"
"vue-template-compiler": "^2.7.14"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": "*"
@@ -1374,6 +1382,13 @@
"integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==",
"license": "MIT" "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": { "node_modules/any-promise": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz",
@@ -1616,13 +1631,6 @@
"node": ">= 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": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
@@ -2033,9 +2041,9 @@
} }
}, },
"node_modules/muggle-string": { "node_modules/muggle-string": {
"version": "0.3.1", "version": "0.4.1",
"resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.3.1.tgz", "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
"integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -2881,19 +2889,6 @@
"url": "https://paulmillr.com/funding/" "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": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", "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": { "node_modules/vue": {
"version": "3.5.29", "version": "3.5.29",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.29.tgz", "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": { "node_modules/vue-tsc": {
"version": "1.8.27", "version": "2.2.12",
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-1.8.27.tgz", "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz",
"integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/typescript": "~1.11.1", "@volar/typescript": "2.4.15",
"@vue/language-core": "1.8.27", "@vue/language-core": "2.2.12"
"semver": "^7.5.4"
}, },
"bin": { "bin": {
"vue-tsc": "bin/vue-tsc.js" "vue-tsc": "bin/vue-tsc.js"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": ">=5.0.0"
} }
} }
} }

View File

@@ -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"}}

View 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>

View File

@@ -39,7 +39,7 @@ interface LogEntry {
const logs = ref<LogEntry[]>([]) const logs = ref<LogEntry[]>([])
const logContainer = ref<HTMLElement | null>(null) const logContainer = ref<HTMLElement | null>(null)
const expanded = ref(true) const expanded = ref(false) // 默认折叠
const lastTripRates = ref<number[]>([]) const lastTripRates = ref<number[]>([])
const addLog = (level: LogEntry['level'], message: string) => { const addLog = (level: LogEntry['level'], message: string) => {

View File

@@ -109,6 +109,24 @@
<q-tooltip>一年中雷暴天数用于计算地闪密度</q-tooltip> <q-tooltip>一年中雷暴天数用于计算地闪密度</q-tooltip>
</q-input> </q-input>
</div> </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> </div>
<!-- 地线挂点高度 --> <!-- 地线挂点高度 -->
@@ -117,7 +135,7 @@
地线挂点垂直坐标 (m) 地线挂点垂直坐标 (m)
</div> </div>
<div class="row q-col-gutter-sm"> <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 <q-input
v-model="params.parameter.h_arm[index]" v-model="params.parameter.h_arm[index]"
type="number" type="number"
@@ -141,7 +159,7 @@
地线挂点水平坐标 (m) 地线挂点水平坐标 (m)
</div> </div>
<div class="row q-col-gutter-sm"> <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 <q-input
v-model="params.parameter.gc_x[index]" v-model="params.parameter.gc_x[index]"
type="number" type="number"
@@ -351,6 +369,9 @@
<!-- 运行日志 --> <!-- 运行日志 -->
<LogComponent ref="logRef" /> <LogComponent ref="logRef" />
<!-- EGM 动画可视化 -->
<Animation ref="animationRef" class="q-mt-md" />
</div> </div>
</q-page> </q-page>
</q-page-container> </q-page-container>
@@ -361,6 +382,7 @@
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue' import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
import type { AllParameters } from '@/types' import type { AllParameters } from '@/types'
import LogComponent from './Log.vue' import LogComponent from './Log.vue'
import Animation from './Animation.vue'
// 默认参数 // 默认参数
const defaultParams: AllParameters = { const defaultParams: AllParameters = {
@@ -376,7 +398,9 @@ const defaultParams: AllParameters = {
gc_x: [17.9, 17], gc_x: [17.9, 17],
ground_angels: [0], ground_angels: [0],
altitude: 1000, altitude: 1000,
td: 20 td: 20,
z_0: 300,
z_c: 251
}, },
advance: { advance: {
ng: -1, 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 result = ref<{ tripping_rate: number; n_sf_phases: number[]; message: string } | null>(null)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const logRef = ref<InstanceType<typeof LogComponent> | 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 fileInput = ref<HTMLInputElement | null>(null)
// 雷电流概率密度系数设置开关 // 雷电流概率密度系数设置开关
const showIpCoefficients = ref(false) const showIpCoefficients = ref(false)
@@ -532,7 +557,12 @@ const calculate = async () => {
if (window.pywebview) { if (window.pywebview) {
// 后台线程启动计算,实时日志通过 addLogFromBackend 推送 // 后台线程启动计算,实时日志通过 addLogFromBackend 推送
// 结果通过 receiveResult 回调接收 // 结果通过 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 回调 // 不在这里设置 calculating = false等待 receiveResult 回调
} else { } else {
// 开发模式下的模拟 // 开发模式下的模拟

View File

@@ -1,4 +1,5 @@
// EGM 计算参数类型定义 // EGM 计算参数类型定义
// Version: 1.0.11
export interface Parameter { export interface Parameter {
// 基本参数 // 基本参数
@@ -14,6 +15,8 @@ export interface Parameter {
ground_angels: number[] // 地面倾角 (°) ground_angels: number[] // 地面倾角 (°)
altitude: number // 海拔高度 (m) altitude: number // 海拔高度 (m)
td: number // 雷暴日 (d) td: number // 雷暴日 (d)
z_0: number // 雷电波阻抗 (Ω),默认 300
z_c: number // 导线波阻抗 (Ω),默认 251
} }
export interface AdvanceParameter { export interface AdvanceParameter {

View File

@@ -4,6 +4,7 @@ import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
export default defineConfig({ export default defineConfig({
base: './',
plugins: [ plugins: [
vue({ vue({
template: { transformAssetUrls } template: { transformAssetUrls }

View File

@@ -23,6 +23,127 @@ from core import Parameter
from main import parameter_display, run_egm 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: class WebHandler:
"""Web日志处理器""" """Web日志处理器"""
def __init__(self, callback=None): def __init__(self, callback=None):
@@ -86,6 +207,7 @@ class EGMWebApp:
self._loguru_handler_id = None self._loguru_handler_id = None
self._log_queue: queue.Queue = queue.Queue() self._log_queue: queue.Queue = queue.Queue()
self._running = False self._running = False
self.animation = WebAnimation() # Web 动画实例
def _process_log_queue(self): def _process_log_queue(self):
"""处理日志队列,在主线程中定时调用""" """处理日志队列,在主线程中定时调用"""
@@ -210,6 +332,8 @@ class EGMWebApp:
para.voltage_n = int(optional_data.get('voltage_n', 3)) para.voltage_n = int(optional_data.get('voltage_n', 3))
para.max_i = float(optional_data.get('max_i', 200)) 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 参数
ac_or_dc_value = str(parameter_data.get('ac_or_dc', 'AC')) ac_or_dc_value = str(parameter_data.get('ac_or_dc', 'AC'))
@@ -220,8 +344,12 @@ class EGMWebApp:
logger.info("开始执行 EGM 计算...") logger.info("开始执行 EGM 计算...")
# 根据前端动画启用状态决定是否传递 animation 对象
animation_enabled = params.get('animation_enabled', False)
animation_obj = self.animation if animation_enabled else None
# 调用 main.py 的核心计算函数 # 调用 main.py 的核心计算函数
result = run_egm(para) result = run_egm(para, animation_obj)
self.add_log("info", "EGM 计算完成") self.add_log("info", "EGM 计算完成")
@@ -434,6 +562,13 @@ def start_webview():
# 确定前端 URL # 确定前端 URL
# 在开发环境中使用 Vite 开发服务器 # 在开发环境中使用 Vite 开发服务器
# 在生产环境中使用构建后的文件 # 在生产环境中使用构建后的文件
# 检查是否在打包环境中运行
if getattr(sys, 'frozen', False):
# 打包环境:强制使用生产模式,禁用调试
dev_mode = False
else:
# 开发环境:通过环境变量控制
dev_mode = os.getenv('EGM_DEV_MODE', 'true').lower() == 'true' dev_mode = os.getenv('EGM_DEV_MODE', 'true').lower() == 'true'
if dev_mode: if dev_mode:
@@ -467,6 +602,7 @@ def start_webview():
# 将窗口对象传递给 API # 将窗口对象传递给 API
api.window = window api.window = window
api.animation.set_window(window)
# 启动 # 启动
logger.info("启动 EGM Web 界面...") logger.info("启动 EGM Web 界面...")