Compare commits
66 Commits
7a5bb05f58
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94080e71ae | ||
|
|
395d364a8f | ||
|
|
7c2728c004 | ||
|
|
8f67a3dd0a | ||
|
|
36139d4ab5 | ||
|
|
6471c066df | ||
|
|
3465cda361 | ||
|
|
aed5c5e3cb | ||
|
|
d5a9bb8798 | ||
|
|
e4da22868d | ||
|
|
568d7d3ef6 | ||
|
|
9d69b1bad2 | ||
|
|
d1baa87ae4 | ||
|
|
195beb3520 | ||
|
|
18fc8fcb0e | ||
|
|
3498650f5f | ||
|
|
fb3276d49d | ||
|
|
45b5dbaab2 | ||
|
|
52a1ca7c2e | ||
|
|
8c1e6c2068 | ||
|
|
b7d73e61a7 | ||
|
|
6665b142e2 | ||
|
|
4184a53a86 | ||
|
|
4b75c6a521 | ||
|
|
7f4a6751b4 | ||
|
|
86b294baf9 | ||
|
|
7dd466a28a | ||
|
|
8a74a576c0 | ||
|
|
0927c94a23 | ||
|
|
68328a68f1 | ||
|
|
4aa56c71d5 | ||
|
|
fd6684c884 | ||
|
|
c19e7b7631 | ||
|
|
07063ec638 | ||
|
|
a65ce23cee | ||
|
|
cee451914a | ||
|
|
dfdf952425 | ||
|
|
8091791fde | ||
|
|
b34da837e2 | ||
|
|
355fb2d689 | ||
|
|
acf1fa1c60 | ||
|
|
e386da0e34 | ||
|
|
3b590f9a1f | ||
|
|
02bfcc18e4 | ||
|
|
9557e18fd1 | ||
|
|
2401b0b19a | ||
|
|
5a8953d1e5 | ||
|
|
bcaa4a5a9e | ||
|
|
630598d498 | ||
|
|
759b8b9a25 | ||
|
|
8aa2f600ed | ||
|
|
73681f629d | ||
|
|
6f0f8d02a8 | ||
|
|
47d3b7b6b4 | ||
|
|
3f3527f7af | ||
|
|
dfb6399073 | ||
|
|
d7ed999da6 | ||
|
|
12565e971d | ||
|
|
dbd6c4f702 | ||
|
|
3bda0260b6 | ||
|
|
73d4f0ac01 | ||
|
|
4ae2f36049 | ||
|
|
9a5e8e0076 | ||
|
|
89e4cd4973 | ||
|
|
6ebfcf848d | ||
|
|
a153e69eb7 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -11,3 +11,11 @@ dist
|
||||
*.toml
|
||||
launch.json
|
||||
settings.json
|
||||
node_modules
|
||||
*.log
|
||||
*.lock
|
||||
*.pdf
|
||||
lightening.ico
|
||||
metadata.yml
|
||||
VERSION
|
||||
生成exe图标.png
|
||||
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
||||
15
Makefile
15
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
|
||||
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 --icon lightening.ico
|
||||
|
||||
console: dist build
|
||||
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
|
||||
|
||||
dist:
|
||||
mkdir dist
|
||||
|
||||
222
README.md
Normal file
222
README.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# EGM - 输电线路绕击跳闸率计算程序
|
||||
|
||||
基于电气几何模型(Electro-Geometric Model, EGM)的架空输电线路雷电防护性能计算工具,用于评估输电线路的绕击跳闸率。
|
||||
|
||||
## 功能特点
|
||||
|
||||
- 支持单回和双回输电线路的绕击跳闸率计算
|
||||
- 考虑工作电压对雷电击距的影响
|
||||
- 支持地面倾角参数
|
||||
- 支持自定义地闪密度和雷电流概率密度曲线
|
||||
- 支持交流/直流线路计算
|
||||
- 输出CAD图形(DXF格式)可视化击距模型
|
||||
- 提供动画演示模式(可选项)
|
||||
- **提供图形化界面(pywebview)**,支持可视化参数配置和计算
|
||||
|
||||
## 安装
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Python >= 3.12
|
||||
- Node.js >= 18(图形界面开发需要)
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
# 使用 uv(推荐)
|
||||
uv sync
|
||||
|
||||
# 或使用 pip
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 依赖包
|
||||
|
||||
- ezdxf - DXF文件生成
|
||||
- loguru - 日志记录
|
||||
- numpy - 数值计算
|
||||
- tomli - TOML配置文件解析
|
||||
- pywebview - 图形界面框架
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本使用
|
||||
|
||||
```bash
|
||||
# 使用默认配置文件
|
||||
python main.py
|
||||
|
||||
# 指定配置文件
|
||||
python main.py <配置文件路径>.toml
|
||||
```
|
||||
|
||||
### 批量计算
|
||||
|
||||
批量计算不同保护角下的跳闸率:
|
||||
|
||||
```bash
|
||||
python main-batch.py <配置文件路径>.toml
|
||||
```
|
||||
|
||||
结果将输出到 `r.txt` 文件中。
|
||||
|
||||
### 打包为可执行文件
|
||||
|
||||
使用 Makefile 打包:
|
||||
|
||||
```bash
|
||||
make
|
||||
```
|
||||
|
||||
生成的可执行文件位于 `dist/Lightening.exe`
|
||||
|
||||
### 图形界面使用
|
||||
|
||||
程序提供基于 pywebview 的图形界面,使用 Vue 3 + Quasar + TypeScript + Tailwind CSS 开发。
|
||||
|
||||
#### 安装前端依赖
|
||||
|
||||
```bash
|
||||
cd webui
|
||||
npm install
|
||||
```
|
||||
|
||||
#### 开发模式运行
|
||||
|
||||
```bash
|
||||
# 终端1:启动前端开发服务器
|
||||
cd webui
|
||||
npm run dev
|
||||
|
||||
# 终端2:启动 pywebview 窗口
|
||||
python webview_app.py
|
||||
```
|
||||
|
||||
#### 生产模式运行
|
||||
|
||||
```bash
|
||||
# 构建前端
|
||||
cd webui
|
||||
npm run build
|
||||
|
||||
# 启动 pywebview 窗口
|
||||
python webview_app.py
|
||||
```
|
||||
|
||||
图形界面支持:
|
||||
- 可视化参数配置(基本参数、高级参数、可选参数)
|
||||
- 动态添加/删除数组参数
|
||||
- 参数导出功能
|
||||
- 计算结果展示
|
||||
|
||||
详细说明请参考 `webui/README.md`
|
||||
|
||||
## 配置文件格式
|
||||
|
||||
配置文件使用 TOML 格式,包含三个主要部分:
|
||||
|
||||
### [parameter] - 基本参数
|
||||
|
||||
```toml
|
||||
[parameter]
|
||||
rated_voltage = 750 # 额定电压等级 (kV)
|
||||
h_c_sag = 14.43 # 导线弧垂 (m)
|
||||
h_g_sag = 11.67 # 地线弧垂 (m)
|
||||
insulator_c_len = 7.4 # 导线串子绝缘长度 (m)
|
||||
string_c_len = 9.2 # 导线串长 (m)
|
||||
string_g_len = 0.5 # 地线串长 (m)
|
||||
h_arm = [130, 100] # 导、地线挂点垂直距离 (m),第一个值为地线挂点高度
|
||||
gc_x = [17.9, 17] # 导、地线水平坐标 (m)
|
||||
ground_angels = [0] # 地面倾角 (°),向下为正,支持多个角度
|
||||
altitude = 1000 # 海拔高度 (m)
|
||||
td = 20 # 雷暴日 (d)
|
||||
```
|
||||
|
||||
### [advance] - 高级参数
|
||||
|
||||
```toml
|
||||
[advance]
|
||||
ng = -1 # 地闪密度 (次/(km²·a)),大于0时使用此值,否则通过雷暴日计算
|
||||
Ip_a = -1 # 雷电流概率密度曲线系数a,大于0时使用此值
|
||||
Ip_b = -1 # 雷电流概率密度曲线系数b,大于0时使用此值
|
||||
```
|
||||
|
||||
**注意**:当 `ng` > 0 时,不会通过雷暴日计算地闪密度;当 `Ip_a` 和 `Ip_b` > 0 时,不会使用默认雷暴日对应的概率密度。
|
||||
|
||||
### [optional] - 可选参数
|
||||
|
||||
```toml
|
||||
[optional]
|
||||
voltage_n = 3 # 计算时电压分成多少份(考虑电压波动影响)
|
||||
max_i = 200 # 最大尝试雷电流 (kA)
|
||||
```
|
||||
|
||||
## 计算原理
|
||||
|
||||
### 击距模型
|
||||
|
||||
程序使用基于电气几何模型的方法计算绕击跳闸率,主要涉及以下击距公式:
|
||||
|
||||
1. **地线击距**:$r_s = 10 \times I^{0.65}$
|
||||
2. **导线击距**:$r_c = 1.63 \times (5.015 \times I^{0.578} - 0.001 \times U_{ph})^{1.125}$
|
||||
3. **地面击距**:
|
||||
- $h_{av} < 40m$: $r_g = (3.6 + 1.7 \ln(43 - h_{av})) \times I^{0.65}$
|
||||
- $h_{av} \ge 40m$: $r_g = 5.5 \times I^{0.65}$
|
||||
|
||||
### 地闪密度计算
|
||||
|
||||
根据 Q/GDW 11452-2015 导则:
|
||||
|
||||
$N_g = 0.023 \times T_d^{1.3}$
|
||||
|
||||
### 跳闸率计算
|
||||
|
||||
通过积分暴露弧面积与雷电流概率密度的乘积得到最终跳闸率,并考虑建弧率。
|
||||
|
||||
## 输出结果
|
||||
|
||||
1. **控制台日志**:显示详细的计算过程和中间结果
|
||||
2. **DXF文件**:egm1.dxf、egm2.dxf 等,可视化击距模型
|
||||
3. **跳闸率结果**:单位为 次/(100km·a)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
EGM/
|
||||
├── main.py # 主程序入口
|
||||
├── main-batch.py # 批量计算程序
|
||||
├── core.py # 核心计算模块
|
||||
├── animation.py # 动画演示模块
|
||||
├── webview_app.py # pywebview 图形界面后端
|
||||
├── article.toml # 示例配置文件
|
||||
├── default.toml # 默认配置文件
|
||||
├── Makefile # 构建脚本
|
||||
├── pyproject.toml # 项目配置
|
||||
├── README.md # 说明文档
|
||||
├── webui/ # 图形界面前端项目
|
||||
│ ├── src/
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── ParameterForm.vue # 参数表单组件
|
||||
│ │ │ ├── Animation.vue # 动画可视化组件
|
||||
│ │ │ └── Log.vue # 日志显示组件
|
||||
│ │ ├── types/
|
||||
│ │ │ └── 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文件
|
||||
```
|
||||
|
||||
## 技术支持
|
||||
|
||||
程序基于新版大手册公式和 Q/GDW 11452-2015《架空输电线路防雷导则》实现。
|
||||
|
||||
## 许可证
|
||||
|
||||
请参考项目许可证文件。
|
||||
94
animation.py
94
animation.py
@@ -1,94 +0,0 @@
|
||||
import matplotlib.pyplot as plt
|
||||
from functools import wraps
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Animation:
|
||||
def __init__(self) -> None:
|
||||
fig, ax = plt.subplots()
|
||||
self._fig = fig
|
||||
self._ax = ax
|
||||
self._ticks = 0
|
||||
self._disable = False
|
||||
self.init_fig()
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def switch_decorator(func):
|
||||
@wraps(func)
|
||||
def not_run(cls, *args, **kwargs):
|
||||
# print("not run")
|
||||
pass
|
||||
|
||||
@wraps(func)
|
||||
def wrapTheFunction(cls, *args, **kwargs):
|
||||
if not cls._disable:
|
||||
# print("desc")
|
||||
return func(cls, *args, **kwargs)
|
||||
return not_run(cls, *args, **kwargs)
|
||||
|
||||
return wrapTheFunction
|
||||
|
||||
def disable(self, _disable):
|
||||
self._disable = _disable
|
||||
|
||||
@switch_decorator
|
||||
def init_fig(self):
|
||||
ax = self._ax
|
||||
ax.set_aspect(1)
|
||||
ax.set_xlim([-500, 500])
|
||||
ax.set_ylim([-500, 500])
|
||||
|
||||
@switch_decorator
|
||||
def show(self):
|
||||
self._fig.show()
|
||||
|
||||
@switch_decorator
|
||||
def add_rg_line(self, line_func):
|
||||
ax = self._ax
|
||||
x = np.linspace(0, 300)
|
||||
y = line_func(x)
|
||||
ax.plot(x, y)
|
||||
|
||||
@switch_decorator
|
||||
def add_rs(self, rs, rs_x, rs_y):
|
||||
ax = self._ax
|
||||
ax.add_artist(plt.Circle((rs_x, rs_y), rs, fill=False))
|
||||
|
||||
@switch_decorator
|
||||
def add_rc(self, rc, rc_x, rc_y):
|
||||
ax = self._ax
|
||||
ax.add_artist(plt.Circle((rc_x, rc_y), rc, fill=False))
|
||||
|
||||
# 增加暴露弧范围
|
||||
@switch_decorator
|
||||
def add_expose_area(
|
||||
self,
|
||||
rc_x,
|
||||
rc_y,
|
||||
intersection_x1,
|
||||
intersection_y1,
|
||||
intersection_x2,
|
||||
intersection_y2,
|
||||
):
|
||||
ax = self._ax
|
||||
ax.plot([rc_x, intersection_x1], [rc_y, intersection_y1], color="red")
|
||||
ax.plot([rc_x, intersection_x2], [rc_y, intersection_y2], color="red")
|
||||
pass
|
||||
|
||||
@switch_decorator
|
||||
def clear(self):
|
||||
ax = self._ax
|
||||
ax.cla()
|
||||
|
||||
@switch_decorator
|
||||
def pause(self):
|
||||
ax = self._ax
|
||||
self._ticks += 1
|
||||
ticks = self._ticks
|
||||
ax.set_title(f"{ticks}")
|
||||
plt.pause(0.02)
|
||||
self.clear()
|
||||
self.init_fig()
|
||||
|
||||
pass
|
||||
@@ -3,13 +3,13 @@ title = "绕击跳闸率计算文件"
|
||||
rated_voltage = 750 # 额定电压等级
|
||||
h_c_sag = 14.43 # 导线弧垂
|
||||
h_g_sag = 11.67 # 地线弧垂
|
||||
insulator_c_len = 7.0 # 导线串子绝缘长度
|
||||
insulator_c_len = 7.02 # 导线串子绝缘长度
|
||||
string_c_len = 9.2 # 导线串长
|
||||
string_g_len = 0.5 # 地线串长
|
||||
h_arm = [100, 80] # 导、地线挂点垂直距离,计算的是中相
|
||||
h_arm = [150, 130] # 导、地线挂点垂直距离,计算的是中相
|
||||
gc_x = [17.9, 17] # 导、地线水平坐标,计算的是中相
|
||||
ground_angels = [0] # 地面倾角,向下为正,单位°
|
||||
altitude = 1500 # 海拔,单位米
|
||||
altitude = 1000 # 海拔,单位米
|
||||
td = 20 # 雷暴日
|
||||
[advance]
|
||||
# ng=29.6 #地闪密度 !!注意!! 如果地闪密度大于0,则不会通过雷暴日计算地闪密度。填-1则忽略该项数据。
|
||||
|
||||
29
core.py
29
core.py
@@ -2,7 +2,7 @@ import math
|
||||
import ezdxf
|
||||
import numpy as np
|
||||
from typing import List
|
||||
|
||||
from loguru import logger
|
||||
gCAD = None
|
||||
gMSP = None
|
||||
gCount = 1
|
||||
@@ -18,16 +18,17 @@ class Parameter:
|
||||
string_g_len: float
|
||||
gc_x: List[float] # 导、地线水平坐标
|
||||
ground_angels: List[float] # 地面倾角,向下为正
|
||||
h_arm: float # 导、地线垂直坐标
|
||||
h_arm: List[float] # 导、地线垂直坐标
|
||||
altitude: int # 海拔,单位米
|
||||
max_i: float # 最大尝试电流,单位kA
|
||||
rated_voltage: float # 额定电压
|
||||
ng: float # 地闪密度 次/(每平方公里·每年)
|
||||
Ip_a: float # 概率密度曲线系数a
|
||||
Ip_b: float # 概率密度曲线系数b
|
||||
|
||||
|
||||
para = Parameter()
|
||||
ac_or_dc: str # 交流或直流标识,"AC" 或 "DC",默认 "AC"
|
||||
z_0: float # 雷电波阻抗,默认 300
|
||||
z_c: float # 导线波阻抗,默认 251
|
||||
u_50: float # 50%击穿电压,-1表示自动计算
|
||||
|
||||
|
||||
def rg_line_function_factory(_rg, ground_angel): # 返回一个地面捕雷线的直线方程
|
||||
@@ -187,20 +188,20 @@ def solve_circle_line_intersection(
|
||||
return [_x, _y]
|
||||
|
||||
|
||||
def min_i(string_len, u_ph):
|
||||
def min_i(string_len, u_ph, altitude: float = 0, z_0: float = 300, z_c: float = 251, u_50: float = None):
|
||||
# 海拔修正
|
||||
altitude = para.altitude
|
||||
if altitude > 1000:
|
||||
k_a = math.exp((altitude - 1000) / 8150) # 气隙海拔修正
|
||||
else:
|
||||
k_a = 1
|
||||
# 只有在u_50未提供时才使用公式计算
|
||||
if u_50 is None:
|
||||
u_50 = 1 / k_a * (530 * string_len + 35) # 50045 上附录的公式,实际应该用负极性电压的公式
|
||||
logger.info(f"50%击穿电压为: {u_50}kV")
|
||||
# u_50 = 1 / k_a * (533 * string_len + 132) # 串放电路径 1000m海拔
|
||||
# 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
|
||||
@@ -218,10 +219,10 @@ def thunder_density(i, td, ip_a, ip_b): # 雷电流幅值密度函数
|
||||
)
|
||||
return r
|
||||
else:
|
||||
if td == 20:
|
||||
if td <= 20:
|
||||
r = -(10 ** (-i / 44)) * math.log(10) * (-1 / 44) # 雷暴日20d
|
||||
return r
|
||||
if td == 40:
|
||||
if td>20:
|
||||
r = -(10 ** (-i / 88)) * math.log(10) * (-1 / 88) # 雷暴日40d
|
||||
return r
|
||||
raise Exception("检查雷电参数!")
|
||||
@@ -455,9 +456,9 @@ def tangent_line_k(line_x, line_y, center_x, center_y, radius, init_k=10.0):
|
||||
return np.array(k_candidate)[np.max(k_angle) == k_angle].tolist()[-1]
|
||||
|
||||
|
||||
def func_ng(td): # 地闪密度,通过雷暴日计算
|
||||
if para.ng > 0:
|
||||
r = para.ng
|
||||
def func_ng(td, ng: float = 0): # 地闪密度,通过雷暴日计算
|
||||
if ng > 0:
|
||||
r = ng
|
||||
else:
|
||||
r = 0.023 * (td**1.3)
|
||||
return r
|
||||
|
||||
372
main-batch.py
Normal file
372
main-batch.py
Normal file
@@ -0,0 +1,372 @@
|
||||
import math
|
||||
import os.path
|
||||
import sys
|
||||
import tomllib as tomli
|
||||
from loguru import logger
|
||||
from core import *
|
||||
import timeit
|
||||
|
||||
|
||||
# 打印参数
|
||||
def parameter_display(para_dis: Parameter):
|
||||
logger.info(f"额定电压 kV {para_dis.rated_voltage}")
|
||||
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]}")
|
||||
logger.info(f"串绝缘距离 m {para_dis.insulator_c_len}")
|
||||
logger.info(f"导线串长 m {para_dis.string_c_len}")
|
||||
logger.info(f"地线串长 m {para_dis.string_g_len}")
|
||||
logger.info(f"挂点垂直坐标 m {para_dis.h_arm}")
|
||||
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}")
|
||||
|
||||
|
||||
def read_parameter(toml_file_path) -> Parameter:
|
||||
para = Parameter()
|
||||
with open(toml_file_path, "rb") as toml_fs:
|
||||
toml_dict = tomli.load(toml_fs)
|
||||
toml_parameter = toml_dict["parameter"]
|
||||
para.h_g_sag = toml_parameter["h_g_sag"] # 地线弧垂
|
||||
para.h_c_sag = toml_parameter["h_c_sag"] # 导线弧垂
|
||||
# para.h_whole = toml_parameter["h_whole"] # 杆塔全高
|
||||
para.td = toml_parameter["td"] # 雷暴日
|
||||
para.insulator_c_len = toml_parameter["insulator_c_len"] # 串子绝缘长度
|
||||
para.string_c_len = toml_parameter["string_c_len"]
|
||||
para.string_g_len = toml_parameter["string_g_len"]
|
||||
para.gc_x = toml_parameter["gc_x"] # 导、地线水平坐标
|
||||
para.ground_angels = [
|
||||
angel / 180 * math.pi for angel in toml_parameter["ground_angels"]
|
||||
] # 地面倾角,向下为正
|
||||
para.h_arm = toml_parameter["h_arm"]
|
||||
para.altitude = toml_parameter["altitude"]
|
||||
para.rated_voltage = toml_parameter["rated_voltage"]
|
||||
para.z_0 = toml_parameter.get("z_0", 300) # 雷电波阻抗
|
||||
para.z_c = toml_parameter.get("z_c", 251) # 导线波阻抗
|
||||
toml_advance = toml_dict["advance"]
|
||||
para.ng = toml_advance["ng"] # 地闪密度
|
||||
para.Ip_a = toml_advance["Ip_a"] # 概率密度曲线系数a
|
||||
para.Ip_b = toml_advance["Ip_b"] # 概率密度曲线系数b
|
||||
para.u_50 = toml_advance.get("u_50", -1) # 50%击穿电压,-1表示自动计算
|
||||
toml_optional = toml_dict["optional"]
|
||||
para.voltage_n = toml_optional["voltage_n"] # 工作电压分成多少份来计算
|
||||
para.max_i = toml_optional["max_i"]
|
||||
return para
|
||||
|
||||
|
||||
def egm():
|
||||
if len(sys.argv) < 2:
|
||||
toml_file_path = r"ZC27103B-2000m.toml"
|
||||
else:
|
||||
toml_file_path = sys.argv[1]
|
||||
if not os.path.exists(toml_file_path):
|
||||
logger.info(f"无法找到数据文件{toml_file_path},程序退出。")
|
||||
sys.exit(0)
|
||||
logger.info(f"读取文件{toml_file_path}")
|
||||
para = read_parameter(toml_file_path)
|
||||
#########################################################
|
||||
# 以上是需要设置的参数
|
||||
parameter_display(para)
|
||||
h_whole = para.h_arm[0] # 塔全高
|
||||
string_g_len = para.string_g_len
|
||||
string_c_len = para.string_c_len
|
||||
h_g_sag = para.h_g_sag
|
||||
h_c_sag = para.h_c_sag
|
||||
gc_x = para.gc_x
|
||||
shield_angle = [0, 10, 11, 12, 13, 14, 15, 16, 17, 18]
|
||||
with open('r.txt','w',encoding='utf-8') as sa_result_file:
|
||||
for sa in shield_angle:
|
||||
gc_x[0] = gc_x[1] + math.tan(sa / 180 * math.pi) * (10 + 6 - 0.5)
|
||||
h_arm = para.h_arm
|
||||
gc_y = [
|
||||
h_whole - string_g_len - h_g_sag * 2 / 3, # 地线对地平均高
|
||||
]
|
||||
if len(h_arm) > 1:
|
||||
for hoo in h_arm[1:]:
|
||||
gc_y.append(hoo - string_c_len - h_c_sag * 2 / 3) # 导线平均高
|
||||
if len(gc_y) > 2: # 双回路
|
||||
phase_n = 3 # 边相导线数量
|
||||
else:
|
||||
phase_n = 1
|
||||
# 地闪密度 利用Q╱GDW 11452-2015 架空输电线路防雷导则的公式 Ng=0.023*Td^(1.3) 20天雷暴日地闪密度为1.13
|
||||
td = para.td
|
||||
ng = func_ng(td, para.ng)
|
||||
avr_n_sf = 0 # 考虑电压的影响计算的跳闸率
|
||||
ground_angels = para.ground_angels
|
||||
for ground_angel in ground_angels:
|
||||
logger.info(f"地面倾角{ground_angel/math.pi*180:.3f}°")
|
||||
rg_type = None
|
||||
rg_x = None
|
||||
rg_y = None
|
||||
cad = Draw()
|
||||
voltage_n = para.voltage_n
|
||||
n_sf_phases = np.zeros((phase_n, voltage_n)) # 存储每一相的跳闸率
|
||||
if np.any(np.array(gc_y) < 0):
|
||||
logger.info("导线可能掉地面下了,程序退出。")
|
||||
return 0
|
||||
for phase_conductor_foo in range(phase_n):
|
||||
exposed_curve_shielded = False
|
||||
rs_x = gc_x[phase_conductor_foo]
|
||||
rs_y = gc_y[phase_conductor_foo]
|
||||
rc_x = gc_x[phase_conductor_foo + 1]
|
||||
rc_y = gc_y[phase_conductor_foo + 1]
|
||||
if phase_n == 1:
|
||||
rg_type = "g"
|
||||
if phase_n > 1: # 多回路
|
||||
if phase_conductor_foo < 2:
|
||||
rg_type = "c" # 捕捉弧有下面一相导线的击距代替
|
||||
rg_x = gc_x[phase_conductor_foo + 2]
|
||||
rg_y = gc_y[phase_conductor_foo + 2]
|
||||
else:
|
||||
rg_type = "g"
|
||||
# TODO 保护角公式可能有问题,后面改
|
||||
shield_angle_at_avg_height = (
|
||||
math.atan(
|
||||
(rc_x - rs_x)
|
||||
/ (
|
||||
(h_arm[0] - string_g_len - h_arm[phase_conductor_foo + 1])
|
||||
+ string_c_len
|
||||
)
|
||||
)
|
||||
* 180
|
||||
/ math.pi
|
||||
) # 挂点处保护角
|
||||
logger.info(f"挂点处保护角{shield_angle_at_avg_height:.3f}°")
|
||||
logger.debug(f"最低相防护标识{rg_type}")
|
||||
rated_voltage = para.rated_voltage
|
||||
for u_bar in range(voltage_n): # 计算不同工作电压下的跳闸率
|
||||
# u_ph = (
|
||||
# math.sqrt(2)
|
||||
# * 750
|
||||
# * math.cos(2 * math.pi / voltage_n * u_bar)
|
||||
# / 1.732
|
||||
# ) # 运行相电压
|
||||
u_ph = rated_voltage
|
||||
logger.info(f"计算第{phase_conductor_foo + 1}相,电压为{u_ph:.2f}kV")
|
||||
# 迭代法计算最大电流
|
||||
i_max = 0
|
||||
insulator_c_len = para.insulator_c_len
|
||||
# i_min = min_i(insulator_c_len, u_ph / 1.732)
|
||||
# u_50: -1表示自动计算,其他值表示使用提供的值
|
||||
u_50_value = para.u_50 if para.u_50 > 0 else None
|
||||
i_min = min_i(insulator_c_len, u_ph, para.altitude, para.z_0, para.z_c, u_50_value)
|
||||
_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)
|
||||
for i_bar in np.linspace(
|
||||
_min_i, _max_i, int((_max_i - _min_i) / 0.1)
|
||||
): # 雷电流
|
||||
# logger.info(f"尝试计算电流为{i_bar:.2f}")
|
||||
rs = rs_fun(i_bar)
|
||||
rc = rc_fun(i_bar, u_ph)
|
||||
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)
|
||||
#######
|
||||
# cccCount += 1
|
||||
# if cccCount % 30 == 0:
|
||||
# import core
|
||||
#
|
||||
# core.gMSP.add_circle((0, h_gav), rs)
|
||||
# core.gMSP.add_circle(
|
||||
# (dgc, h_cav), rc_fun(i_bar, -u_ph), dxfattribs={"color": 4}
|
||||
# )
|
||||
# core.gMSP.add_circle((dgc, h_cav), rc)
|
||||
#######
|
||||
rg_rc_circle_intersection = solve_circle_intersection(
|
||||
rs, rc, rs_x, rs_y, rc_x, rc_y
|
||||
)
|
||||
i_max = i_bar
|
||||
if (
|
||||
not rg_rc_circle_intersection
|
||||
): # if circle_intersection is []
|
||||
logger.debug("保护弧和暴露弧无交点,检查设置参数。")
|
||||
continue
|
||||
circle_rc_line_or_rg_intersection = None
|
||||
if rg_type == "g":
|
||||
circle_rc_line_or_rg_intersection = (
|
||||
solve_circle_line_intersection(
|
||||
rc, rc_x, rc_y, rg_line_func
|
||||
)
|
||||
)
|
||||
elif rg_type == "c":
|
||||
circle_rc_line_or_rg_intersection = (
|
||||
solve_circle_intersection(
|
||||
rg, rc, rg_x, rg_y, rc_x, rc_y
|
||||
)
|
||||
)
|
||||
if not circle_rc_line_or_rg_intersection:
|
||||
# 暴露弧和捕捉弧无交点
|
||||
if rg_type == "g":
|
||||
if rg_line_func(rc_x) > rc_y:
|
||||
i_min = i_bar # 用于后面判断最小和最大电流是否相等,相等意味着暴露弧一直被屏蔽
|
||||
logger.info(f"捕捉面在暴露弧之上,设置最小电流为{i_min:.2f}")
|
||||
else:
|
||||
logger.info("暴露弧和地面捕捉弧无交点,检查设置参数。")
|
||||
continue
|
||||
else:
|
||||
logger.info("上面的导地线无法保护下面的导地线,检查设置参数。")
|
||||
continue
|
||||
min_distance_intersection = (
|
||||
np.sum(
|
||||
(
|
||||
np.array(rg_rc_circle_intersection)
|
||||
- np.array(circle_rc_line_or_rg_intersection)
|
||||
)
|
||||
** 2
|
||||
)
|
||||
** 0.5
|
||||
) # 计算两圆交点和地面直线交点的最小距离
|
||||
if min_distance_intersection < 0.1:
|
||||
break # 已经找到了最大电流
|
||||
# 判断是否以完全被保护
|
||||
if (
|
||||
rg_rc_circle_intersection[1]
|
||||
< circle_rc_line_or_rg_intersection[1]
|
||||
):
|
||||
circle_rs_line_or_rg_intersection = None
|
||||
if rg_type == "g":
|
||||
circle_rs_line_or_rg_intersection = (
|
||||
solve_circle_line_intersection(
|
||||
rs, rs_x, rs_y, rg_line_func
|
||||
) # 保护弧和捕雷弧的交点
|
||||
)
|
||||
if rg_type == "c":
|
||||
circle_rs_line_or_rg_intersection = (
|
||||
solve_circle_intersection(
|
||||
rs, rg, rs_x, rs_y, rg_x, rg_y
|
||||
)
|
||||
)
|
||||
# 判断与保护弧的交点是否在暴露弧外面
|
||||
distance = (
|
||||
np.sum(
|
||||
(
|
||||
np.array(circle_rs_line_or_rg_intersection)
|
||||
- np.array([rc_x, rc_y])
|
||||
)
|
||||
** 2
|
||||
)
|
||||
** 0.5
|
||||
)
|
||||
if distance > rc:
|
||||
logger.info(f"电流为{i_bar}kV时,暴露弧已经完全被屏蔽")
|
||||
exposed_curve_shielded = True
|
||||
break
|
||||
# if phase_conductor_foo == 2:
|
||||
cad.draw(
|
||||
i_min,
|
||||
u_ph,
|
||||
rs_x,
|
||||
rs_y,
|
||||
rc_x,
|
||||
rc_y,
|
||||
rg_x,
|
||||
rg_y,
|
||||
rg_type,
|
||||
ground_angel,
|
||||
2,
|
||||
)
|
||||
cad.draw(
|
||||
i_max,
|
||||
u_ph,
|
||||
rs_x,
|
||||
rs_y,
|
||||
rc_x,
|
||||
rc_y,
|
||||
rg_x,
|
||||
rg_y,
|
||||
rg_type,
|
||||
ground_angel,
|
||||
6,
|
||||
)
|
||||
cad.save_as(f"egm{phase_conductor_foo + 1}.dxf")
|
||||
# 判断是否导线已经被完全保护
|
||||
if abs(i_max - _max_i) < 1e-5:
|
||||
logger.info("无法找到最大电流,可能是杆塔较高。")
|
||||
logger.info(f"最大电流设置为自然界最大电流{i_max}kA")
|
||||
logger.info(f"最大电流为{i_max:.2f}")
|
||||
logger.info(f"最小电流为{i_min:.2f}")
|
||||
if exposed_curve_shielded:
|
||||
logger.info("暴露弧已经完全被屏蔽,不会跳闸。")
|
||||
continue
|
||||
curt_fineness = 0.1 # 电流积分细度
|
||||
if i_min > i_max or abs(i_min - i_max) < curt_fineness:
|
||||
logger.info("最大电流小于等于最小电流,没有暴露弧。")
|
||||
continue
|
||||
# 开始积分
|
||||
curt_segment_n = int((i_max - i_min) / curt_fineness) # 分成多少份
|
||||
i_curt_samples, d_curt = np.linspace(
|
||||
i_min, i_max, curt_segment_n + 1, retstep=True
|
||||
)
|
||||
bd_area_vec = np.vectorize(bd_area)
|
||||
td = para.td
|
||||
ip_a = para.Ip_a
|
||||
ip_b = para.Ip_b
|
||||
cal_bd_np = bd_area_vec(
|
||||
i_curt_samples,
|
||||
u_ph,
|
||||
rc_x,
|
||||
rc_y,
|
||||
rs_x,
|
||||
rs_y,
|
||||
rg_x,
|
||||
rg_y,
|
||||
ground_angel,
|
||||
rg_type,
|
||||
) * thunder_density(i_curt_samples, td, ip_a, ip_b)
|
||||
calculus = np.sum(cal_bd_np[:-1] + cal_bd_np[1:]) / 2 * d_curt
|
||||
# for i_curt in i_curt_samples[:-1]:
|
||||
# cal_bd_first = bd_area(i_curt, u_ph, dgc, h_gav, h_cav)
|
||||
# cal_bd_second = bd_area(i_curt + d_curt, u_ph, dgc, h_gav, h_cav)
|
||||
# cal_thunder_density_first = thunder_density(i_curt)
|
||||
# cal_thunder_density_second = thunder_density(i_curt + d_curt)
|
||||
# calculus += (
|
||||
# (
|
||||
# cal_bd_first * cal_thunder_density_first
|
||||
# + cal_bd_second * cal_thunder_density_second
|
||||
# )
|
||||
# / 2
|
||||
# * d_curt
|
||||
# )
|
||||
# if abs(calculus-0.05812740052770032)<1e-5:
|
||||
# abc=123
|
||||
# pass
|
||||
rated_voltage = para.rated_voltage
|
||||
n_sf = (
|
||||
2
|
||||
* ng
|
||||
/ 10
|
||||
* calculus
|
||||
* arc_possibility(rated_voltage, insulator_c_len)
|
||||
)
|
||||
avr_n_sf += n_sf / voltage_n
|
||||
n_sf_phases[phase_conductor_foo][u_bar] = n_sf
|
||||
logger.info(f"工作电压为{u_ph:.2f}kV时,跳闸率是{n_sf:.16f}次/(100km·a)")
|
||||
logger.info(f"线路跳闸率是{avr_n_sf:.16f}次/(100km·a)")
|
||||
logger.info(
|
||||
f"不同相跳闸率是{np.array2string(np.mean(n_sf_phases,axis=1),precision=16)}次/(100km·a)"
|
||||
)
|
||||
sa_result_file.write(f'{sa},{avr_n_sf}\n')
|
||||
|
||||
|
||||
def speed():
|
||||
a = 0
|
||||
for bar in range(100000000):
|
||||
a += bar
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="DEBUG")
|
||||
run_time = timeit.timeit("egm()", globals=globals(), number=1)
|
||||
logger.info(f"运行时间:{run_time:.2f}s")
|
||||
logger.info("Finished.")
|
||||
135
main.py
135
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,17 +22,22 @@ 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.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:
|
||||
# pass
|
||||
# else:
|
||||
logger.info(f"雷暴日 d {para_dis.td}")
|
||||
|
||||
|
||||
def read_parameter(toml_file_path):
|
||||
def read_parameter(toml_file_path) -> Parameter:
|
||||
para = Parameter()
|
||||
with open(toml_file_path, "rb") as toml_fs:
|
||||
toml_dict = tomli.load(toml_fs)
|
||||
toml_parameter = toml_dict["parameter"]
|
||||
@@ -50,28 +55,41 @@ def read_parameter(toml_file_path):
|
||||
para.h_arm = toml_parameter["h_arm"]
|
||||
para.altitude = toml_parameter["altitude"]
|
||||
para.rated_voltage = toml_parameter["rated_voltage"]
|
||||
para.ac_or_dc = toml_parameter.get("ac_or_dc", "AC") # 交流或直流标识,默认AC
|
||||
para.z_0 = toml_parameter.get("z_0", 300) # 雷电波阻抗
|
||||
para.z_c = toml_parameter.get("z_c", 251) # 导线波阻抗
|
||||
toml_advance = toml_dict["advance"]
|
||||
para.ng = toml_advance["ng"] # 地闪密度
|
||||
para.Ip_a = toml_advance["Ip_a"] # 概率密度曲线系数a
|
||||
para.Ip_b = toml_advance["Ip_b"] # 概率密度曲线系数b
|
||||
para.u_50 = toml_advance.get("u_50", -1) # 50%击穿电压,-1表示自动计算
|
||||
toml_optional = toml_dict["optional"]
|
||||
para.voltage_n = toml_optional["voltage_n"] # 工作电压分成多少份来计算
|
||||
para.max_i = toml_optional["max_i"]
|
||||
return para
|
||||
|
||||
|
||||
def egm():
|
||||
if len(sys.argv) < 2:
|
||||
toml_file_path = r"内自500kV-ZCK上相.toml"
|
||||
else:
|
||||
toml_file_path = sys.argv[1]
|
||||
if not os.path.exists(toml_file_path):
|
||||
logger.info(f"无法找到数据文件{toml_file_path},程序退出。")
|
||||
sys.exit(0)
|
||||
logger.info(f"读取文件{toml_file_path}")
|
||||
read_parameter(toml_file_path)
|
||||
#########################################################
|
||||
# 以上是需要设置的参数
|
||||
def run_egm(para: Parameter, animation=None) -> dict:
|
||||
"""
|
||||
执行 EGM 计算的核心函数,可被外部调用。
|
||||
Args:
|
||||
para: 参数对象,包含所有计算所需的参数。
|
||||
animation: 可选的动画对象,用于可视化。需要实现 add_rs, add_rc, add_rg_line, add_expose_area, pause 方法。
|
||||
Returns:
|
||||
计算结果字典。
|
||||
"""
|
||||
parameter_display(para)
|
||||
|
||||
# 参数校验:gc_x 和 h_arm 长度必须一致
|
||||
if len(para.gc_x) != len(para.h_arm):
|
||||
error_msg = f"参数错误:gc_x 长度({len(para.gc_x)})与 h_arm 长度({len(para.h_arm)})不一致"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"success": False,
|
||||
"message": error_msg,
|
||||
"data": None
|
||||
}
|
||||
|
||||
h_whole = para.h_arm[0] # 挂点高
|
||||
string_g_len = para.string_g_len
|
||||
string_c_len = para.string_c_len
|
||||
@@ -91,12 +109,12 @@ def egm():
|
||||
phase_n = 1
|
||||
# 地闪密度 利用Q╱GDW 11452-2015 架空输电线路防雷导则的公式 Ng=0.023*Td^(1.3) 20天雷暴日地闪密度为1.13
|
||||
td = para.td
|
||||
ng = func_ng(td)
|
||||
ng = func_ng(td, para.ng)
|
||||
avr_n_sf = 0 # 考虑电压的影响计算的跳闸率
|
||||
ground_angels = para.ground_angels
|
||||
# 初始化动画
|
||||
animate = Animation()
|
||||
animate.disable(False)
|
||||
# 动画对象:如果传入了 animation 则使用,否则不启用动画
|
||||
# 注意:动画的启用由前端用户通过"启用动画"开关控制,后端不主动启用
|
||||
animate = animation
|
||||
# animate.show()
|
||||
for ground_angel in ground_angels:
|
||||
logger.info(f"地面倾角{ground_angel/math.pi*180:.3f}°")
|
||||
@@ -107,7 +125,11 @@ def egm():
|
||||
n_sf_phases = np.zeros((phase_n, voltage_n)) # 存储每一相的跳闸率
|
||||
if np.any(np.array(gc_y) < 0):
|
||||
logger.info("导线可能掉地面下了,程序退出。")
|
||||
return 0
|
||||
return {
|
||||
"success": False,
|
||||
"message": "导线可能掉地面下了,程序退出。",
|
||||
"data": None
|
||||
}
|
||||
for phase_conductor_foo in range(phase_n):
|
||||
exposed_curve_shielded = False
|
||||
rs_x = gc_x[phase_conductor_foo]
|
||||
@@ -123,22 +145,18 @@ def egm():
|
||||
rg_y = gc_y[phase_conductor_foo + 2]
|
||||
else:
|
||||
rg_type = "g"
|
||||
# TODO 保护角公式可能有问题,后面改
|
||||
# 使用实际高度(考虑弧垂)计算保护角
|
||||
shield_angle_at_avg_height = (
|
||||
math.atan(
|
||||
(rc_x - rs_x)
|
||||
/ (
|
||||
(h_arm[0] - string_g_len - h_arm[phase_conductor_foo + 1])
|
||||
+ string_c_len
|
||||
)
|
||||
)
|
||||
math.atan2(rc_x - rs_x, rs_y - rc_y)
|
||||
* 180
|
||||
/ math.pi
|
||||
) # 挂点处保护角
|
||||
logger.info(f"挂点处保护角{shield_angle_at_avg_height:.3f}°")
|
||||
logger.debug(f"最低相防护标识{rg_type}")
|
||||
)
|
||||
logger.info(f"地线保护角(平均高处){shield_angle_at_avg_height:.2f}°")
|
||||
logger.debug(f"最低相防护标识{rg_type}。(g表示地面,c表示下导线)")
|
||||
rated_voltage = para.rated_voltage
|
||||
logger.info(f"交、直流标识{para.ac_or_dc}")
|
||||
for u_bar in range(voltage_n): # 计算不同工作电压下的跳闸率
|
||||
if para.ac_or_dc=="AC":
|
||||
# TODO 需要区分交、直流
|
||||
# u_ph = (
|
||||
# math.sqrt(2)
|
||||
@@ -147,13 +165,17 @@ def egm():
|
||||
# / 1.732
|
||||
# ) # 运行相电压
|
||||
u_ph = rated_voltage/1.732
|
||||
else:
|
||||
u_ph = rated_voltage
|
||||
logger.info(f"计算第{phase_conductor_foo + 1}相,电压为{u_ph:.2f}kV")
|
||||
# 迭代法计算最大电流
|
||||
i_max = 0
|
||||
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)
|
||||
# u_50: -1表示自动计算,其他值表示使用提供的值
|
||||
u_50_value = para.u_50 if para.u_50 > 0 else None
|
||||
i_min = min_i(insulator_c_len, u_ph, para.altitude, para.z_0, para.z_c, u_50_value)
|
||||
_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)
|
||||
@@ -162,13 +184,16 @@ def egm():
|
||||
): # 雷电流
|
||||
logger.info(f"尝试计算电流为{i_bar:.2f}")
|
||||
rs = rs_fun(i_bar)
|
||||
if animate:
|
||||
animate.add_rs(rs, rs_x, rs_y)
|
||||
rc = rc_fun(i_bar, u_ph)
|
||||
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)
|
||||
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
|
||||
@@ -202,6 +227,7 @@ def egm():
|
||||
"上面的导地线无法保护下面的导地线,检查设置参数。"
|
||||
)
|
||||
continue
|
||||
if animate:
|
||||
animate.add_expose_area(
|
||||
rc_x,
|
||||
rc_y,
|
||||
@@ -235,7 +261,8 @@ def egm():
|
||||
ground_angel,
|
||||
6,
|
||||
) # 最大电流时
|
||||
cad.save_as(f"egm{phase_conductor_foo + 1}.dxf")
|
||||
# TODO: 暂时先不用。
|
||||
# cad.save_as(f"egm{phase_conductor_foo + 1}.dxf")
|
||||
min_distance_intersection = (
|
||||
np.sum(
|
||||
(
|
||||
@@ -278,9 +305,10 @@ def egm():
|
||||
** 0.5
|
||||
)
|
||||
if distance > rc:
|
||||
logger.info(f"电流为{i_bar}kV时,暴露弧已经完全被屏蔽")
|
||||
logger.info(f"电流为{i_bar}kA时,暴露弧已经完全被屏蔽")
|
||||
exposed_curve_shielded = True
|
||||
break
|
||||
if animate:
|
||||
animate.pause()
|
||||
# 判断是否导线已经被完全保护
|
||||
if abs(i_max - _max_i) < 1e-5:
|
||||
@@ -353,6 +381,37 @@ def egm():
|
||||
f"不同相跳闸率是{np.array2string(np.mean(n_sf_phases,axis=1),precision=16)}次/(100km·a)"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "计算完成",
|
||||
"data": {
|
||||
"tripping_rate": f"{avr_n_sf:.16f} 次/(100km·a)",
|
||||
"avr_n_sf": avr_n_sf,
|
||||
"n_sf_phases": np.mean(n_sf_phases, axis=1).tolist(),
|
||||
"parameters": {
|
||||
"rated_voltage": para.rated_voltage,
|
||||
"td": para.td,
|
||||
"altitude": para.altitude,
|
||||
"ground_angels": [a / math.pi * 180 for a in para.ground_angels],
|
||||
"max_i": para.max_i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def egm():
|
||||
"""命令行入口函数"""
|
||||
if len(sys.argv) < 2:
|
||||
toml_file_path = r"D:/code/EGM/历史/平乾750kV.toml"
|
||||
else:
|
||||
toml_file_path = sys.argv[1]
|
||||
if not os.path.exists(toml_file_path):
|
||||
logger.info(f"无法找到数据文件{toml_file_path},程序退出。")
|
||||
sys.exit(0)
|
||||
logger.info(f"读取文件{toml_file_path}")
|
||||
para = read_parameter(toml_file_path)
|
||||
run_egm(para)
|
||||
|
||||
|
||||
def speed():
|
||||
a = 0
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
Version: 1.2.0.2
|
||||
#CompanyName: My Imaginary Company
|
||||
#FileDescription: Simple App
|
||||
#InternalName: Simple App
|
||||
#LegalCopyright: © My Imaginary Company. All rights reserved.
|
||||
#OriginalFilename: SimpleApp.exe
|
||||
ProductName: 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": {}
|
||||
}
|
||||
100
plot.py
100
plot.py
@@ -1,100 +0,0 @@
|
||||
import matplotlib
|
||||
from plot_data import *
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.ticker as mticker
|
||||
|
||||
matplotlib.use("Qt5Agg")
|
||||
# 解决中文乱码
|
||||
plt.rcParams["font.sans-serif"] = ["simsun"]
|
||||
plt.rcParams["font.family"] = "sans-serif"
|
||||
# plt.rcParams["font.weight"] = "bold"
|
||||
# 解决负号无法显示的问题
|
||||
plt.rcParams["axes.unicode_minus"] = False
|
||||
plt.rcParams["savefig.dpi"] = 1200 # 图片像素
|
||||
# plt.savefig("port.png", dpi=600, bbox_inches="tight")
|
||||
fontsize = 12
|
||||
################################################
|
||||
witdh_of_bar=0.3
|
||||
color=plt.cm.BuPu(np.linspace(152/255, 251/255,152/255))
|
||||
percent1 = data_150m塔高_不同地线保护角[:, 1] / data_150m塔高_不同地线保护角[:, 0]
|
||||
# percent1 = data_66m串长_不同塔高[:, 1] / data_66m串长_不同塔高[:, 0]
|
||||
# percent2 = data_68m串长_不同塔高[:, 1] / data_68m串长_不同塔高[:, 0]
|
||||
fig, ax = plt.subplots()
|
||||
x = np.arange(len(category_names_150m塔高_不同地线保护角)) # the label locations
|
||||
p1 = ax.bar(category_names_150m塔高_不同地线保护角, percent1, witdh_of_bar, label="绕击/反击跳闸率比值",color=color,hatch='-')
|
||||
# p1 = ax.bar(x - 0.3 / 2, percent1, 0.3, label="6.6m绝缘距离")
|
||||
# p2 = ax.bar(x + 0.3 / 2, percent2, 0.3, label="6.8m绝缘距离")
|
||||
ax.xaxis.set_major_locator(mticker.FixedLocator(x))
|
||||
ax.set_xticklabels(category_names_150m塔高_不同地线保护角)
|
||||
ax.set_ylabel("比值", fontsize=fontsize)
|
||||
ax.set_xlabel("地线保护角(°)", fontsize=fontsize)
|
||||
# ax.set_xlabel("接地电阻(Ω)", fontsize=fontsize)
|
||||
plt.xticks(fontsize=fontsize)
|
||||
plt.yticks(fontsize=fontsize)
|
||||
ax.bar_label(p1, padding=0, fontsize=fontsize)
|
||||
# ax.bar_label(p2, padding=0, fontsize=fontsize)
|
||||
ax.legend(fontsize=fontsize)
|
||||
|
||||
fig.tight_layout()
|
||||
plt.show()
|
||||
|
||||
|
||||
# results = {
|
||||
# "100m": 100 * data[0, :] / np.sum(data[0, :]),
|
||||
# "110m": data[1, :] / np.sum(data[1, :]),
|
||||
# "120m": data[2, :] / np.sum(data[2, :]),
|
||||
# "130m": data[3, :] / np.sum(data[3, :]),
|
||||
# "140m": data[4, :] / np.sum(data[4, :]),
|
||||
# "150m": data[5, :] / np.sum(data[5, :]),
|
||||
# }
|
||||
|
||||
|
||||
# def survey(results, category_names):
|
||||
# """
|
||||
# Parameters
|
||||
# ----------
|
||||
# results : dict
|
||||
# A mapping from question labels to a list of answers per category.
|
||||
# It is assumed all lists contain the same number of entries and that
|
||||
# it matches the length of *category_names*.
|
||||
# category_names : list of str
|
||||
# The category labels.
|
||||
# """
|
||||
# labels = list(results.keys())
|
||||
# data = np.array(list(results.values()))
|
||||
# data_cum = data.cumsum(axis=1)
|
||||
# category_colors = plt.get_cmap("RdYlGn")(np.linspace(0.15, 0.85, data.shape[1]))
|
||||
#
|
||||
# fig, ax = plt.subplots(figsize=(9.2, 5))
|
||||
# ax.invert_yaxis()
|
||||
# ax.xaxis.set_visible(False)
|
||||
# ax.set_xlim(0, np.sum(data, axis=1).max())
|
||||
#
|
||||
# for i, (colname, color) in enumerate(zip(category_names, category_colors)):
|
||||
# widths = data[:, i]
|
||||
# starts = data_cum[:, i] - widths
|
||||
# rects = ax.barh(
|
||||
# labels, widths, left=starts, height=0.5, label=colname, color=color
|
||||
# )
|
||||
#
|
||||
# r, g, b, _ = color
|
||||
# text_color = "white" if r * g * b < 0.5 else "darkgrey"
|
||||
# ax.bar_label(rects, label_type="center", color=text_color)
|
||||
# ax.legend(
|
||||
# ncol=len(category_names),
|
||||
# bbox_to_anchor=(0, 1),
|
||||
# loc="lower left",
|
||||
# fontsize="small",
|
||||
# )
|
||||
#
|
||||
# return fig, ax
|
||||
|
||||
# percent=data/np.sum(data,axis=1)[:,None]*100
|
||||
# percent = data[:, 1] / data[:, 0]
|
||||
# plt.bar(category_names, percent, 0.3, label="黑")
|
||||
# # plt.bar(category_names, percent[:,0], 0.2, label="r")
|
||||
#
|
||||
# # plt.bar(category_names, [0.014094 / 100, 0.025094 / 100], 0.2, label="h")
|
||||
# plt.legend()
|
||||
# # survey(results, category_names)
|
||||
# plt.show()
|
||||
58
update_version.py
Normal file
58
update_version.py
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""更新版本号脚本"""
|
||||
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_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)
|
||||
create_metadata(new_version)
|
||||
|
||||
print(f"Version updated: {current_version} -> {new_version}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
149
webui/README.md
Normal file
149
webui/README.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# EGM Web 界面
|
||||
|
||||
基于 pywebview 的输电线路绕击跳闸率计算程序图形界面。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**: Python + pywebview
|
||||
- **前端**: Vue 3 + Quasar + TypeScript + Tailwind CSS
|
||||
- **构建工具**: Vite
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 安装 Python 依赖
|
||||
|
||||
```bash
|
||||
# 在项目根目录
|
||||
uv sync
|
||||
# 或
|
||||
pip install pywebview
|
||||
```
|
||||
|
||||
### 2. 安装前端依赖
|
||||
|
||||
```bash
|
||||
cd webui
|
||||
npm install
|
||||
```
|
||||
|
||||
## 运行方式
|
||||
|
||||
### 开发模式
|
||||
|
||||
1. 启动前端开发服务器:
|
||||
|
||||
```bash
|
||||
cd webui
|
||||
npm run dev
|
||||
```
|
||||
|
||||
2. 在另一个终端启动 pywebview:
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
python webview_app.py
|
||||
```
|
||||
|
||||
### 生产模式
|
||||
|
||||
1. 构建前端:
|
||||
|
||||
```bash
|
||||
cd webui
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. 启动 pywebview:
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
python webview_app.py
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
webui/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ │ └── ParameterForm.vue # 参数表单组件
|
||||
│ ├── types/
|
||||
│ │ └── 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 入口
|
||||
```
|
||||
|
||||
## 功能说明
|
||||
|
||||
### 参数配置
|
||||
|
||||
界面提供三个参数分组:
|
||||
|
||||
1. **基本参数**:
|
||||
- 额定电压等级 (kV)
|
||||
- 导线弧垂 (m)
|
||||
- 地线弧垂 (m)
|
||||
- 导线串子绝缘长度 (m)
|
||||
- 导线串长 (m)
|
||||
- 地线串长 (m)
|
||||
- 导、地线挂点垂直距离 (m)
|
||||
- 导、地线水平坐标 (m)
|
||||
- 地面倾角 (°)
|
||||
- 海拔高度 (m)
|
||||
- 雷暴日 (d)
|
||||
|
||||
2. **高级参数**:
|
||||
- 地闪密度 (次/(km²·a))
|
||||
- 雷电流概率密度曲线系数 a
|
||||
- 雷电流概率密度曲线系数 b
|
||||
|
||||
3. **可选参数**:
|
||||
- 计算时电压分成多少份
|
||||
- 最大尝试雷电流 (kA)
|
||||
|
||||
### 操作功能
|
||||
|
||||
- **开始计算**: 执行 EGM 计算
|
||||
- **重置参数**: 恢复默认参数值
|
||||
- **导出配置**: 将当前配置导出为 JSON 文件
|
||||
|
||||
## 开发说明
|
||||
|
||||
### 添加新功能
|
||||
|
||||
1. 在 `src/types/index.ts` 中添加类型定义
|
||||
2. 在 `src/components/ParameterForm.vue` 中添加 UI 组件
|
||||
3. 在 `webview_app.py` 中添加后端 API 方法
|
||||
|
||||
### 样式定制
|
||||
|
||||
- 使用 Tailwind CSS 类名进行样式定制
|
||||
- Quasar 组件自带 Material Design 风格
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: pywebview 窗口无法打开?
|
||||
|
||||
A: 请确保已安装 pywebview:`pip install pywebview`
|
||||
|
||||
### Q: 前端开发服务器无法连接?
|
||||
|
||||
A: 请确保先运行 `npm run dev`,然后再启动 `python webview_app.py`
|
||||
|
||||
### Q: 计算功能不工作?
|
||||
|
||||
A: 当前版本仅提供界面框架,需要进一步集成 core.py 中的计算逻辑。
|
||||
|
||||
## 后续开发
|
||||
|
||||
- [ ] 集成 core.py 中的实际计算函数
|
||||
- [ ] 添加计算结果可视化
|
||||
- [ ] 支持导入/导出 TOML 配置文件
|
||||
- [ ] 添加批量计算功能
|
||||
- [ ] 添加结果导出(DXF、PDF 等)
|
||||
13
webui/index.html
Normal file
13
webui/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
3280
webui/package-lock.json
generated
Normal file
3280
webui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
webui/package.json
Normal file
1
webui/package.json
Normal file
@@ -0,0 +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":"^2.0.0"}}
|
||||
6
webui/postcss.config.js
Normal file
6
webui/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
26
webui/src/App.vue
Normal file
26
webui/src/App.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<ParameterForm />
|
||||
<div class="version-footer">v{{ appVersion }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ParameterForm from '@/components/ParameterForm.vue'
|
||||
|
||||
const appVersion = __APP_VERSION__
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.version-footer {
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
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>
|
||||
422
webui/src/components/Geometry.vue
Normal file
422
webui/src/components/Geometry.vue
Normal file
@@ -0,0 +1,422 @@
|
||||
<template>
|
||||
<q-card class="full-height">
|
||||
<q-card-section class="bg-indigo-50">
|
||||
<div class="text-h6 text-indigo-900 flex items-center gap-2">
|
||||
<q-icon name="architecture" />
|
||||
杆塔几何结构
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<div class="geometry-container">
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
:width="canvasWidth"
|
||||
:height="canvasHeight"
|
||||
class="geometry-canvas"
|
||||
/>
|
||||
<!-- 图例 -->
|
||||
<div class="legend q-mt-sm">
|
||||
<div class="row q-gutter-md justify-center">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="legend-color" style="background: #4CAF50;"></div>
|
||||
<span class="text-caption">地线</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="legend-color" style="background: #FF9800;"></div>
|
||||
<span class="text-caption">导线</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="legend-color" style="background: #795548;"></div>
|
||||
<span class="text-caption">地面</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
hArm: number[] // 导、地线挂点垂直坐标 [地线, 导线1, ...]
|
||||
gcX: number[] // 导、地线水平坐标 [地线, 导线1, ...]
|
||||
hCSag: number // 导线弧垂
|
||||
hGSag: number // 地线弧垂
|
||||
stringCLen: number // 导线串长
|
||||
stringGLen: number // 地线串长
|
||||
groundAngels: number[] // 地面倾角
|
||||
}>()
|
||||
|
||||
// Canvas 尺寸
|
||||
const canvasWidth = 600
|
||||
const canvasHeight = 500
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||
let ctx: CanvasRenderingContext2D | null = null
|
||||
|
||||
// 计算参数
|
||||
const margin = { top: 40, right: 40, bottom: 60, left: 60 }
|
||||
const plotWidth = canvasWidth - margin.left - margin.right
|
||||
const plotHeight = canvasHeight - margin.top - margin.bottom
|
||||
|
||||
// 计算实际导地线高度(考虑弧垂和串长)
|
||||
const calculateActualHeights = () => {
|
||||
const hArmNums = props.hArm.map(v => Number(v))
|
||||
const hGSagNum = Number(props.hGSag)
|
||||
const hCSagNum = Number(props.hCSag)
|
||||
const stringGLenNum = Number(props.stringGLen)
|
||||
const stringCLenNum = Number(props.stringCLen)
|
||||
|
||||
// 地线实际高度 = 挂点高度 - 地线串长 - 地线弧垂 * 2/3
|
||||
// 导线实际高度 = 挂点高度 - 导线串长 - 导线弧垂 * 2/3
|
||||
return hArmNums.map((h, index) => {
|
||||
if (index === 0) {
|
||||
// 地线
|
||||
return h - stringGLenNum - hGSagNum * 2 / 3
|
||||
} else {
|
||||
// 导线
|
||||
return h - stringCLenNum - hCSagNum * 2 / 3
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 计算坐标范围
|
||||
const calculateRange = () => {
|
||||
// 确保将字符串转换为数字
|
||||
const hArmNums = props.hArm.map(v => Number(v))
|
||||
const gcXNums = props.gcX.map(v => Number(v))
|
||||
const actualHeights = calculateActualHeights()
|
||||
|
||||
// 包含挂点高度和实际高度
|
||||
const allHeights = [...hArmNums, ...actualHeights, 0]
|
||||
const allX = [...gcXNums, -gcXNums[0] * 0.5, gcXNums[0] * 1.5] // 扩展水平范围
|
||||
|
||||
const yMin = -10
|
||||
const yMax = Math.max(...allHeights) * 1.15
|
||||
const xMin = Math.min(...allX) * 1.2
|
||||
const xMax = Math.max(...allX) * 1.2
|
||||
|
||||
return { xMin, xMax, yMin, yMax }
|
||||
}
|
||||
|
||||
// 坐标转换:数据坐标 -> Canvas 坐标
|
||||
const toCanvasX = (x: number, range: ReturnType<typeof calculateRange>): number => {
|
||||
return margin.left + ((x - range.xMin) / (range.xMax - range.xMin)) * plotWidth
|
||||
}
|
||||
|
||||
const toCanvasY = (y: number, range: ReturnType<typeof calculateRange>): number => {
|
||||
// Canvas Y 轴向下,需要反转
|
||||
return margin.top + plotHeight - ((y - range.yMin) / (range.yMax - range.yMin)) * plotHeight
|
||||
}
|
||||
|
||||
// 绘制函数
|
||||
const draw = () => {
|
||||
if (!ctx) return
|
||||
|
||||
const range = calculateRange()
|
||||
|
||||
// 清除画布
|
||||
ctx.fillStyle = '#fafafa'
|
||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight)
|
||||
|
||||
// 绘制背景网格
|
||||
ctx.strokeStyle = '#e8e8e8'
|
||||
ctx.lineWidth = 1
|
||||
|
||||
// 垂直网格线
|
||||
const xStep = Math.ceil((range.xMax - range.xMin) / 10)
|
||||
for (let x = Math.ceil(range.xMin / xStep) * xStep; x <= range.xMax; x += xStep) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(toCanvasX(x, range), margin.top)
|
||||
ctx.lineTo(toCanvasX(x, range), margin.top + plotHeight)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// 水平网格线
|
||||
const yStep = Math.ceil((range.yMax - range.yMin) / 8)
|
||||
for (let y = Math.ceil(range.yMin / yStep) * yStep; y <= range.yMax; y += yStep) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(margin.left, toCanvasY(y, range))
|
||||
ctx.lineTo(margin.left + plotWidth, toCanvasY(y, range))
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// 绘制坐标轴
|
||||
ctx.strokeStyle = '#333'
|
||||
ctx.lineWidth = 1.5
|
||||
|
||||
// Y 轴
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(margin.left, margin.top)
|
||||
ctx.lineTo(margin.left, margin.top + plotHeight)
|
||||
ctx.stroke()
|
||||
|
||||
// X 轴(地面)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(margin.left, toCanvasY(range.yMin, range))
|
||||
ctx.lineTo(margin.left + plotWidth, toCanvasY(range.yMin, range))
|
||||
ctx.stroke()
|
||||
|
||||
// 绘制刻度标签
|
||||
ctx.fillStyle = '#666'
|
||||
ctx.font = '11px Arial'
|
||||
|
||||
// Y 轴刻度
|
||||
ctx.textAlign = 'right'
|
||||
for (let y = yStep; y <= range.yMax; y += yStep) {
|
||||
const canvasY = toCanvasY(y, range)
|
||||
ctx.fillText(`${y}`, margin.left - 8, canvasY + 4)
|
||||
|
||||
// 刻度线
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(margin.left - 4, canvasY)
|
||||
ctx.lineTo(margin.left, canvasY)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// X 轴刻度
|
||||
ctx.textAlign = 'center'
|
||||
for (let x = Math.ceil(range.xMin / xStep) * xStep; x <= range.xMax; x += xStep) {
|
||||
if (x !== 0) {
|
||||
const canvasX = toCanvasX(x, range)
|
||||
ctx.fillText(`${x}`, canvasX, toCanvasY(0, range) + 18)
|
||||
|
||||
// 刻度线
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(canvasX, toCanvasY(0, range))
|
||||
ctx.lineTo(canvasX, toCanvasY(0, range) + 4)
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
// 轴标签
|
||||
ctx.font = '12px Arial'
|
||||
ctx.fillStyle = '#333'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText('水平距离 (m)', margin.left + plotWidth / 2, canvasHeight - 10)
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(15, margin.top + plotHeight / 2)
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
ctx.fillText('高度 (m)', 0, 0)
|
||||
ctx.restore()
|
||||
|
||||
// 绘制地面倾角
|
||||
drawGround(range)
|
||||
|
||||
// 绘制导线和地线挂点
|
||||
drawWirePoints(range)
|
||||
|
||||
// 绘制保护角
|
||||
drawShieldingAngle(range)
|
||||
}
|
||||
|
||||
// 绘制导线和地线挂点
|
||||
const drawWirePoints = (range: ReturnType<typeof calculateRange>) => {
|
||||
if (!ctx) return
|
||||
const c = ctx
|
||||
const actualHeights = calculateActualHeights()
|
||||
|
||||
props.hArm.forEach((height, index) => {
|
||||
// 确保将字符串转换为数字
|
||||
const heightNum = Number(height)
|
||||
const actualHeight = actualHeights[index]
|
||||
const wireX = Number(props.gcX[index]) || 0
|
||||
const isGroundWire = index === 0
|
||||
|
||||
const canvasX = toCanvasX(wireX, range)
|
||||
const canvasY = toCanvasY(heightNum, range)
|
||||
const actualCanvasY = toCanvasY(actualHeight, range)
|
||||
|
||||
// 绘制从挂点到实际位置的虚线(绝缘子串 + 弧垂)
|
||||
c.strokeStyle = isGroundWire ? '#4CAF50' : '#FF9800'
|
||||
c.lineWidth = 2
|
||||
c.setLineDash([4, 4])
|
||||
c.beginPath()
|
||||
c.moveTo(canvasX, canvasY)
|
||||
c.lineTo(canvasX, actualCanvasY)
|
||||
c.stroke()
|
||||
c.setLineDash([])
|
||||
|
||||
// 绘制挂点标记(方形,表示杆塔挂点)
|
||||
c.fillStyle = '#666'
|
||||
c.fillRect(canvasX - 5, canvasY - 5, 10, 10)
|
||||
|
||||
// 绘制实际导地线位置(圆形)
|
||||
c.fillStyle = isGroundWire ? '#4CAF50' : '#FF9800'
|
||||
c.beginPath()
|
||||
c.arc(canvasX, actualCanvasY, 8, 0, Math.PI * 2)
|
||||
c.fill()
|
||||
|
||||
// 标注信息
|
||||
c.fillStyle = '#333'
|
||||
c.font = 'bold 11px Arial'
|
||||
c.textAlign = 'left'
|
||||
|
||||
const labelX = canvasX + 12
|
||||
const labelY = actualCanvasY - 8
|
||||
|
||||
const wireName = isGroundWire ? '地线' : `导线${index}`
|
||||
const heightLabel = `H=${actualHeight.toFixed(1)}m`
|
||||
const xLabel = `X=${wireX}m`
|
||||
|
||||
c.fillText(wireName, labelX, labelY)
|
||||
c.font = '10px Arial'
|
||||
c.fillStyle = '#666'
|
||||
c.fillText(heightLabel, labelX, labelY + 14)
|
||||
c.fillText(xLabel, labelX, labelY + 26)
|
||||
})
|
||||
}
|
||||
|
||||
// 绘制地面
|
||||
const drawGround = (range: ReturnType<typeof calculateRange>) => {
|
||||
if (!ctx) return
|
||||
|
||||
// 确保将字符串转换为数字
|
||||
const groundAngle = Number(props.groundAngels[0]) || 0
|
||||
const angleRad = (groundAngle * Math.PI) / 180
|
||||
|
||||
ctx.strokeStyle = '#795548'
|
||||
ctx.lineWidth = 2
|
||||
|
||||
// 地面线(考虑倾角)
|
||||
const groundLength = range.xMax - range.xMin
|
||||
const dy = Math.tan(angleRad) * groundLength
|
||||
|
||||
const leftX = range.xMin
|
||||
const rightX = range.xMax
|
||||
const leftY = groundAngle >= 0 ? dy : 0
|
||||
const rightY = groundAngle >= 0 ? 0 : -dy
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(toCanvasX(leftX, range), toCanvasY(leftY, range))
|
||||
ctx.lineTo(toCanvasX(rightX, range), toCanvasY(rightY, range))
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// 绘制保护角
|
||||
const drawShieldingAngle = (range: ReturnType<typeof calculateRange>) => {
|
||||
if (!ctx || props.hArm.length < 2) return
|
||||
|
||||
const actualHeights = calculateActualHeights()
|
||||
const gwX = Number(props.gcX[0]) || 0
|
||||
const gwY = actualHeights[0]
|
||||
const cwX = Number(props.gcX[1]) || 0
|
||||
const cwY = actualHeights[1]
|
||||
|
||||
const gwCanvasX = toCanvasX(gwX, range)
|
||||
const gwCanvasY = toCanvasY(gwY, range)
|
||||
const cwCanvasX = toCanvasX(cwX, range)
|
||||
const cwCanvasY = toCanvasY(cwY, range)
|
||||
|
||||
// 计算保护角(地线与导线连线与垂直线的夹角)
|
||||
const dx = cwX - gwX
|
||||
const dy = gwY - cwY
|
||||
const shieldingAngle = Math.atan2(dx, dy) * (180 / Math.PI)
|
||||
|
||||
// 绘制从地线到导线1的虚线
|
||||
ctx.strokeStyle = '#9C27B0'
|
||||
ctx.lineWidth = 2
|
||||
ctx.setLineDash([6, 4])
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(gwCanvasX, gwCanvasY)
|
||||
ctx.lineTo(cwCanvasX, cwCanvasY)
|
||||
ctx.stroke()
|
||||
ctx.setLineDash([])
|
||||
|
||||
// // 绘制垂直参考线(从地线向下)
|
||||
// ctx.strokeStyle = 'rgba(156, 39, 176, 0.3)'
|
||||
// ctx.lineWidth = 1
|
||||
// ctx.setLineDash([4, 4])
|
||||
// ctx.beginPath()
|
||||
// ctx.moveTo(gwCanvasX, gwCanvasY)
|
||||
// ctx.lineTo(gwCanvasX, gwCanvasY + 80)
|
||||
// ctx.stroke()
|
||||
// ctx.setLineDash([])
|
||||
|
||||
// // 绘制角度弧
|
||||
// const arcRadius = 30
|
||||
// const verticalAngle = Math.PI / 2 // 向下
|
||||
// const lineAngle = Math.atan2(cwCanvasY - gwCanvasY, cwCanvasX - gwCanvasX)
|
||||
|
||||
ctx.strokeStyle = '#9C27B0'
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.beginPath()
|
||||
// if (dx >= 0) {
|
||||
// ctx.arc(gwCanvasX, gwCanvasY, arcRadius, Math.PI / 2, lineAngle, true)
|
||||
// } else {
|
||||
// ctx.arc(gwCanvasX, gwCanvasY, arcRadius, lineAngle, Math.PI / 2, false)
|
||||
// }
|
||||
// ctx.stroke()
|
||||
|
||||
// 计算标注位置(在线的右侧)
|
||||
const midX = (gwCanvasX + cwCanvasX) / 2
|
||||
const midY = (gwCanvasY + cwCanvasY) / 2
|
||||
const labelOffsetX = dx >= 0 ? -45 : 45
|
||||
const labelOffsetY = 0
|
||||
|
||||
// 绘制引线
|
||||
ctx.strokeStyle = '#9C27B0'
|
||||
ctx.lineWidth = 1
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(midX, midY)
|
||||
ctx.lineTo(midX + labelOffsetX, midY + labelOffsetY)
|
||||
ctx.stroke()
|
||||
|
||||
// 绘制标注文字
|
||||
const labelText = `保护角(平均高处): ${Math.abs(shieldingAngle).toFixed(2)}°`
|
||||
ctx.font = 'bold 12px Arial'
|
||||
|
||||
// 绘制标注文字
|
||||
ctx.fillStyle = '#9C27B0'
|
||||
ctx.textAlign = 'left'
|
||||
ctx.fillText(labelText, midX + labelOffsetX, midY + labelOffsetY)
|
||||
}
|
||||
|
||||
// 监听参数变化
|
||||
watch(
|
||||
() => [props.hArm, props.gcX, props.hCSag, props.hGSag, props.stringCLen, props.stringGLen, props.groundAngels],
|
||||
() => {
|
||||
draw()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (canvasRef.value) {
|
||||
ctx = canvasRef.value.getContext('2d')
|
||||
draw()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.geometry-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.geometry-canvas {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 16px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
padding: 8px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
129
webui/src/components/Log.vue
Normal file
129
webui/src/components/Log.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<q-card class="q-mt-md shadow-2">
|
||||
<q-card-section class="bg-blue-grey-1 cursor-pointer" @click="expanded = !expanded">
|
||||
<div class="text-h6 text-blue-grey-9 flex items-center gap-2">
|
||||
<q-icon name="terminal" />
|
||||
运行日志
|
||||
<q-space />
|
||||
<q-icon :name="expanded ? 'expand_less' : 'expand_more'" />
|
||||
</div>
|
||||
</q-card-section>
|
||||
<div v-show="expanded" class="q-pa-none">
|
||||
<div ref="logContainer" class="log-container">
|
||||
<div
|
||||
v-for="(log, index) in logs"
|
||||
:key="index"
|
||||
:class="['log-line', `log-${log.level}`]"
|
||||
>
|
||||
<span class="log-time">{{ log.time }}</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
<div v-if="logs.length === 0" class="log-empty">暂无日志</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'LogComponent'
|
||||
})
|
||||
|
||||
interface LogEntry {
|
||||
level: 'info' | 'warning' | 'error' | 'debug'
|
||||
time: string
|
||||
message: string
|
||||
}
|
||||
|
||||
const logs = ref<LogEntry[]>([])
|
||||
const logContainer = ref<HTMLElement | null>(null)
|
||||
const expanded = ref(false) // 默认折叠
|
||||
const lastTripRates = ref<number[]>([])
|
||||
|
||||
const addLog = (level: LogEntry['level'], message: string) => {
|
||||
const now = new Date()
|
||||
const time = now.toLocaleTimeString('zh-CN', { hour12: false })
|
||||
|
||||
logs.value.push({ level, time, message })
|
||||
|
||||
// 解析跳闸率数值
|
||||
const match = message.match(/不同相跳闸率是\[([\d.\s]+)\]/)
|
||||
if (match) {
|
||||
const values = match[1].trim().split(/\s+/).map(Number)
|
||||
lastTripRates.value = values
|
||||
}
|
||||
|
||||
// 自动滚动到底部
|
||||
nextTick(() => {
|
||||
if (logContainer.value) {
|
||||
logContainer.value.scrollTop = logContainer.value.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const clearLog = () => {
|
||||
logs.value = []
|
||||
}
|
||||
|
||||
// 获取日志文本
|
||||
const getLogsText = (): string => {
|
||||
return logs.value.map(log => `[${log.time}] [${log.level.toUpperCase()}] ${log.message}`).join('\n')
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
addLog,
|
||||
clearLog,
|
||||
lastTripRates,
|
||||
getLogsText
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.log-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 12px;
|
||||
padding: 8px;
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
padding: 2px 0;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #6a9955;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.log-info .log-message {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.log-warning .log-message {
|
||||
color: #dcdcaa;
|
||||
}
|
||||
|
||||
.log-error .log-message {
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.log-debug .log-message {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.log-empty {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
971
webui/src/components/ParameterForm.vue
Normal file
971
webui/src/components/ParameterForm.vue
Normal file
@@ -0,0 +1,971 @@
|
||||
<template>
|
||||
<q-layout view="lHh lpr lFf">
|
||||
<q-header elevated class="bg-indigo-600 text-white">
|
||||
<q-toolbar>
|
||||
<q-toolbar-title class="q-py-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<q-icon name="flash_on" size="md" />
|
||||
<span class="text-xl font-bold">EGM 输电线路绕击跳闸率计算</span>
|
||||
</div>
|
||||
<div v-if="currentFilePath" class="text-sm truncate max-w-2xl bg-white text-green-700 px-2 py-0.5 rounded mt-1" :title="currentFilePath">
|
||||
{{ currentFilePath }}
|
||||
</div>
|
||||
</q-toolbar-title>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-page-container>
|
||||
|
||||
<q-page class="q-pa-md">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<!-- 基本参数 + 杆塔几何结构 并排布局 -->
|
||||
<div class="row q-col-gutter-md q-mb-md items-stretch" style="min-height: 500px;">
|
||||
<!-- 左侧:基本参数 -->
|
||||
<div class="col-12 col-lg-6" style="display: flex;">
|
||||
<q-card class="shadow-2" style="flex: 1;">
|
||||
<q-card-section class="bg-indigo-50">
|
||||
<div class="text-h6 text-indigo-900 flex items-center gap-2">
|
||||
<q-icon name="settings" />
|
||||
基本参数
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-select
|
||||
v-model="params.parameter.rated_voltage"
|
||||
:options="voltageOptions"
|
||||
label="额定电压等级 (kV)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
:model-value="currentType"
|
||||
label="电压类型 (AC/DC)"
|
||||
readonly
|
||||
>
|
||||
<q-tooltip>交流(AC)或直流(DC),由电压等级自动判断</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.parameter.h_c_sag"
|
||||
type="number"
|
||||
step="0.01"
|
||||
label="导线弧垂 (m)"
|
||||
>
|
||||
<q-tooltip>导线在最大弧垂状态下相对于挂点的垂直距离</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.parameter.h_g_sag"
|
||||
type="number"
|
||||
step="0.01"
|
||||
label="地线弧垂 (m)"
|
||||
>
|
||||
<q-tooltip>地线在最大弧垂状态下相对于挂点的垂直距离</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.parameter.insulator_c_len"
|
||||
type="number"
|
||||
step="0.01"
|
||||
label="导线串子绝缘长度 (m)"
|
||||
>
|
||||
<q-tooltip>绝缘子串的总长度</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.parameter.string_c_len"
|
||||
type="number"
|
||||
step="0.1"
|
||||
label="导线串长 (m)"
|
||||
>
|
||||
<q-tooltip>导线绝缘子串的总长度</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.parameter.string_g_len"
|
||||
type="number"
|
||||
step="0.1"
|
||||
label="地线串长 (m)"
|
||||
>
|
||||
<q-tooltip>地线绝缘子串的总长度</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.parameter.altitude"
|
||||
type="number"
|
||||
label="海拔高度 (m)"
|
||||
>
|
||||
<q-tooltip>用于修正绝缘子串的闪络电压</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.parameter.td"
|
||||
type="number"
|
||||
label="雷暴日 (d)"
|
||||
>
|
||||
<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>
|
||||
|
||||
<!-- 导、地线挂点高度 -->
|
||||
<div class="q-mt-md">
|
||||
<div class="text-subtitle2 q-mb-sm">
|
||||
导、地线挂点垂直坐标 (m)
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col" v-for="(_, index) in params.parameter.h_arm" :key="index">
|
||||
<q-input
|
||||
v-model="params.parameter.h_arm[index]"
|
||||
type="number"
|
||||
step="0.1"
|
||||
:label="index === 0 ? '地线' : `导线 ${index}`"
|
||||
dense
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat round color="primary" icon="add" @click="addHArm" :disable="params.parameter.h_arm.length >= 4" v-show="params.parameter.h_arm.length === 2" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat round color="negative" icon="remove" @click="removeHArm" :disable="params.parameter.h_arm.length <= 2" v-show="params.parameter.h_arm.length === 4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导、地线水平坐标 -->
|
||||
<div class="q-mt-md">
|
||||
<div class="text-subtitle2 q-mb-sm">
|
||||
导、地线挂点水平坐标 (m)
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col" v-for="(_, index) in params.parameter.gc_x" :key="index">
|
||||
<q-input
|
||||
v-model="params.parameter.gc_x[index]"
|
||||
type="number"
|
||||
step="0.1"
|
||||
:label="index === 0 ? '地线' : `导线 ${index}`"
|
||||
dense
|
||||
/>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat round color="primary" icon="add" @click="addGcX" :disable="params.parameter.gc_x.length >= 4" v-show="params.parameter.gc_x.length === 2" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<q-btn flat round color="negative" icon="remove" @click="removeGcX" :disable="params.parameter.gc_x.length <= 2" v-show="params.parameter.gc_x.length === 4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 地面倾角 -->
|
||||
<div class="q-mt-md">
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.parameter.ground_angels[0]"
|
||||
type="number"
|
||||
step="1"
|
||||
label="地面倾角 (°) - 向下为正"
|
||||
>
|
||||
<q-tooltip>地面倾斜角度,向下为正值</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:杆塔几何结构可视化 -->
|
||||
<div class="col-12 col-lg-6" style="display: flex;">
|
||||
<Geometry
|
||||
:h-arm="params.parameter.h_arm"
|
||||
:gc-x="params.parameter.gc_x"
|
||||
:h-c-sag="params.parameter.h_c_sag"
|
||||
:h-g-sag="params.parameter.h_g_sag"
|
||||
:string-c-len="params.parameter.string_c_len"
|
||||
:string-g-len="params.parameter.string_g_len"
|
||||
:ground-angels="params.parameter.ground_angels"
|
||||
class="shadow-2"
|
||||
style="flex: 1;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高级参数 -->
|
||||
<q-card class="q-mb-md shadow-2">
|
||||
<q-card-section class="bg-indigo-50">
|
||||
<div class="text-h6 text-indigo-900 flex items-center gap-2">
|
||||
<q-icon name="tune" />
|
||||
高级参数
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
v-model="params.advance.ng"
|
||||
type="number"
|
||||
step="0.01"
|
||||
label="地闪密度 (次/(km²·a))"
|
||||
>
|
||||
<q-tooltip>每平方公里每年的地闪次数,默认-1表示自动计算</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 雷电流概率密度系数设置开关 -->
|
||||
<div class="q-mt-md">
|
||||
<q-toggle
|
||||
v-model="showIpCoefficients"
|
||||
label="设置雷电流概率密度系数"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md q-mt-sm" v-if="showIpCoefficients">
|
||||
<div class="col-6">
|
||||
<q-input
|
||||
v-model="params.advance.Ip_a"
|
||||
type="number"
|
||||
step="0.01"
|
||||
label="雷电流概率密度曲线系数 a"
|
||||
dense
|
||||
>
|
||||
<q-tooltip>雷电流幅值概率密度函数参数,默认-1表示使用标准参数</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<q-input
|
||||
v-model="params.advance.Ip_b"
|
||||
type="number"
|
||||
step="0.01"
|
||||
label="雷电流概率密度曲线系数 b"
|
||||
dense
|
||||
>
|
||||
<q-tooltip>雷电流幅值概率密度函数参数,默认-1表示使用标准参数</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 50%击穿电压设置开关 -->
|
||||
<div class="q-mt-md">
|
||||
<q-toggle
|
||||
v-model="showU50"
|
||||
label="设置50%击穿电压 (U_50)"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md q-mt-sm" v-if="showU50">
|
||||
<div class="col-12">
|
||||
<q-input
|
||||
v-model="params.advance.u_50"
|
||||
type="number"
|
||||
step="1"
|
||||
label="50%击穿电压 U_50 (kV)"
|
||||
>
|
||||
<q-tooltip>自定义50%击穿电压值,默认-1表示使用公式计算</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- 可选参数 -->
|
||||
<q-card class="q-mb-md shadow-2">
|
||||
<q-card-section class="bg-indigo-50">
|
||||
<div class="text-h6 text-indigo-900 flex items-center gap-2">
|
||||
<q-icon name="more_horiz" />
|
||||
可选参数
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.optional.max_i"
|
||||
type="number"
|
||||
label="最大尝试雷电流 (kA)"
|
||||
>
|
||||
<q-tooltip>计算时尝试的最大雷电流幅值</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="row q-gutter-md justify-center q-mt-lg">
|
||||
<q-btn
|
||||
color="primary"
|
||||
size="lg"
|
||||
label="开始计算"
|
||||
icon="calculate"
|
||||
@click="calculate"
|
||||
:loading="calculating"
|
||||
class="px-8"
|
||||
/>
|
||||
<q-btn
|
||||
color="orange"
|
||||
size="lg"
|
||||
label="导入配置"
|
||||
icon="upload"
|
||||
@click="importConfig"
|
||||
class="px-8"
|
||||
/>
|
||||
<q-btn
|
||||
color="positive"
|
||||
size="lg"
|
||||
label="导出配置"
|
||||
icon="download"
|
||||
@click="exportConfig"
|
||||
class="px-8"
|
||||
/>
|
||||
<q-btn
|
||||
color="grey"
|
||||
size="lg"
|
||||
label="导出日志"
|
||||
icon="description"
|
||||
@click="exportLog"
|
||||
class="px-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的文件输入(开发模式备用) -->
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
accept=".toml"
|
||||
style="display: none"
|
||||
@change="handleFileSelect"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<q-card v-if="error" class="q-mt-md shadow-2 bg-red-50">
|
||||
<q-card-section>
|
||||
<div class="text-negative q-mb-sm flex items-center gap-2">
|
||||
<q-icon name="error" />
|
||||
错误信息
|
||||
</div>
|
||||
<p class="text-negative">{{ error }}</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- 计算结果 -->
|
||||
<q-card v-if="result" class="q-mt-md shadow-2 bg-green-50">
|
||||
<q-card-section class="bg-green-100">
|
||||
<div class="text-h6 text-green-900 flex items-center gap-2">
|
||||
<q-icon name="check_circle" />
|
||||
计算结果
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="text-body1 result-text">
|
||||
<div class="q-mb-sm"><strong>跳闸率:</strong>{{ result.tripping_rate.toFixed(4) }} 次/(100km·a)</div>
|
||||
<div v-if="result.n_sf_phases && result.n_sf_phases.length > 0" class="q-mb-sm">
|
||||
<strong>各导线跳闸率:</strong>
|
||||
<span v-for="(rate, index) in result.n_sf_phases" :key="index" class="q-mr-md">
|
||||
导线{{ index + 1 }}: {{ rate.toFixed(4) }} 次/(100km·a)
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-green-800 q-mt-md">{{ result.message }}</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<!-- 运行日志 -->
|
||||
<LogComponent ref="logRef" />
|
||||
|
||||
<!-- EGM 动画可视化 -->
|
||||
<Animation ref="animationRef" class="q-mt-md" />
|
||||
|
||||
</div>
|
||||
</q-page>
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, watch, toRaw } from 'vue'
|
||||
import type { AllParameters } from '@/types'
|
||||
import LogComponent from './Log.vue'
|
||||
import Animation from './Animation.vue'
|
||||
import Geometry from './Geometry.vue'
|
||||
|
||||
// 默认参数
|
||||
const defaultParams: AllParameters = {
|
||||
parameter: {
|
||||
rated_voltage: '500kV',
|
||||
ac_or_dc: 'AC',
|
||||
h_c_sag: 14.43,
|
||||
h_g_sag: 11.67,
|
||||
insulator_c_len: 7.02,
|
||||
string_c_len: 9.2,
|
||||
string_g_len: 0.5,
|
||||
h_arm: [150, 130],
|
||||
gc_x: [17.9, 17],
|
||||
ground_angels: [0],
|
||||
altitude: 1000,
|
||||
td: 20,
|
||||
z_0: 300,
|
||||
z_c: 251
|
||||
},
|
||||
advance: {
|
||||
ng: -1,
|
||||
Ip_a: -1,
|
||||
Ip_b: -1,
|
||||
u_50: -1
|
||||
},
|
||||
optional: {
|
||||
voltage_n: 1,
|
||||
max_i: 300
|
||||
}
|
||||
}
|
||||
|
||||
const params = reactive<AllParameters>(JSON.parse(JSON.stringify(defaultParams)))
|
||||
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 currentFilePath = ref<string>('')
|
||||
// 雷电流概率密度系数设置开关
|
||||
const showIpCoefficients = ref(false)
|
||||
// 50%击穿电压设置开关
|
||||
const showU50 = ref(false)
|
||||
|
||||
const voltageOptions = [
|
||||
'110kV', '220kV', '330kV', '500kV', '750kV','1000kV',
|
||||
'±500kV', '±660kV', '±800kV', '±1100kV'
|
||||
]
|
||||
|
||||
// 根据电压等级自动判断交流/直流
|
||||
const currentType = computed(() => {
|
||||
return params.parameter.rated_voltage.includes('±') ? 'DC' : 'AC'
|
||||
})
|
||||
|
||||
// 监听电压等级变化,同步更新 ac_or_dc 字段
|
||||
watch(
|
||||
() => params.parameter.rated_voltage,
|
||||
(newVoltage) => {
|
||||
const isDC = newVoltage.includes('±')
|
||||
params.parameter.ac_or_dc = isDC ? 'DC' : 'AC'
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 监听雷电流概率密度系数开关
|
||||
watch(
|
||||
showIpCoefficients,
|
||||
(show) => {
|
||||
if (!show) {
|
||||
// 关闭时重置为 -1(使用标准参数)
|
||||
params.advance.Ip_a = -1
|
||||
params.advance.Ip_b = -1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 监听50%击穿电压开关
|
||||
watch(
|
||||
showU50,
|
||||
(show) => {
|
||||
if (!show) {
|
||||
// 关闭时重置为 -1(使用公式计算)
|
||||
params.advance.u_50 = -1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 雷暴日与地闪密度相互转换,公式:ng = 0.023 * td^3
|
||||
// 标志位避免循环更新
|
||||
let isUpdatingFromWatch = false
|
||||
|
||||
watch(
|
||||
() => params.advance.ng,
|
||||
(newNg) => {
|
||||
if (isUpdatingFromWatch) return
|
||||
const ng = Number(newNg)
|
||||
if (ng > 0) {
|
||||
isUpdatingFromWatch = true
|
||||
// td = (ng / 0.023)^(1/1.3)
|
||||
params.parameter.td = Math.round(Math.pow(ng / 0.023, 1/1.3) * 100) / 100
|
||||
isUpdatingFromWatch = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => params.parameter.td,
|
||||
(newTd) => {
|
||||
if (isUpdatingFromWatch) return
|
||||
const td = Number(newTd)
|
||||
if (td > 0) {
|
||||
isUpdatingFromWatch = true
|
||||
// ng = 0.023 * td^1.3
|
||||
params.advance.ng = Math.round(0.023 * Math.pow(td, 1.3) * 100) / 100
|
||||
isUpdatingFromWatch = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 数组操作函数(导线数量只能是1或3条,即数组长度为2或4:1地线+导线)
|
||||
// 两个数组同步增减
|
||||
const addHArm = () => {
|
||||
if (params.parameter.h_arm.length === 2) {
|
||||
// 从1条导线增加到3条导线,添加2个元素
|
||||
const last = params.parameter.h_arm[params.parameter.h_arm.length - 1] || 100
|
||||
params.parameter.h_arm.push(last - 20, last - 40)
|
||||
// 同步增加 gc_x
|
||||
const lastX = params.parameter.gc_x[params.parameter.gc_x.length - 1] || 10
|
||||
params.parameter.gc_x.push(lastX, lastX)
|
||||
}
|
||||
}
|
||||
|
||||
const removeHArm = () => {
|
||||
if (params.parameter.h_arm.length === 4) {
|
||||
// 从3条导线减少到1条导线,移除2个元素
|
||||
params.parameter.h_arm.pop()
|
||||
params.parameter.h_arm.pop()
|
||||
// 同步删除 gc_x
|
||||
params.parameter.gc_x.pop()
|
||||
params.parameter.gc_x.pop()
|
||||
}
|
||||
}
|
||||
|
||||
const addGcX = () => {
|
||||
if (params.parameter.gc_x.length === 2) {
|
||||
// 从1条导线增加到3条导线,添加2个元素
|
||||
const lastX = params.parameter.gc_x[params.parameter.gc_x.length - 1] || 10
|
||||
params.parameter.gc_x.push(lastX, lastX)
|
||||
// 同步增加 h_arm
|
||||
const last = params.parameter.h_arm[params.parameter.h_arm.length - 1] || 100
|
||||
params.parameter.h_arm.push(last - 20, last - 40)
|
||||
}
|
||||
}
|
||||
|
||||
const removeGcX = () => {
|
||||
if (params.parameter.gc_x.length === 4) {
|
||||
// 从3条导线减少到1条导线,移除2个元素
|
||||
params.parameter.gc_x.pop()
|
||||
params.parameter.gc_x.pop()
|
||||
// 同步删除 h_arm
|
||||
params.parameter.h_arm.pop()
|
||||
params.parameter.h_arm.pop()
|
||||
}
|
||||
}
|
||||
|
||||
// 计算函数
|
||||
const calculate = async () => {
|
||||
// 验证雷电流概率密度系数
|
||||
if (showIpCoefficients.value) {
|
||||
const ipA = Number(params.advance.Ip_a)
|
||||
const ipB = Number(params.advance.Ip_b)
|
||||
if (ipA < 0 && ipB < 0) {
|
||||
error.value = '请检查参数:"雷电流概率密度曲线系数 a"和"雷电流概率密度曲线系数 b"的值应该大于 0'
|
||||
logRef.value?.addLog('error', '请检查参数:"雷电流概率密度曲线系数 a"和"雷电流概率密度曲线系数 b"的值应该大于 0')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 验证50%击穿电压
|
||||
if (showU50.value) {
|
||||
const u50 = Number(params.advance.u_50)
|
||||
if (u50 < 1000) {
|
||||
error.value = '请检查参数:"50%击穿电压 U_50"的值应该大于等于 1000 kV'
|
||||
logRef.value?.addLog('error', '请检查参数:"50%击穿电压 U_50"的值应该大于等于 1000 kV')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 验证导、地线挂点垂直坐标顺序:地线 > 导线1 > 导线2 > 导线3
|
||||
const hArm = params.parameter.h_arm.map(Number)
|
||||
for (let i = 0; i < hArm.length - 1; i++) {
|
||||
if (hArm[i] <= hArm[i + 1]) {
|
||||
const labels = ['地线', '导线1', '导线2', '导线3']
|
||||
error.value = `请检查参数:${labels[i]}垂直坐标应大于${labels[i + 1]}垂直坐标`
|
||||
logRef.value?.addLog('error', error.value)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
calculating.value = true
|
||||
result.value = null
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// 调用 pywebview 的 Python 函数
|
||||
if (window.pywebview) {
|
||||
// 后台线程启动计算,实时日志通过 addLogFromBackend 推送
|
||||
// 结果通过 receiveResult 回调接收
|
||||
// 传递动画启用状态
|
||||
// 使用 toRaw 解包响应式对象,确保 pywebview 能正确序列化参数
|
||||
const rawParams = toRaw(params)
|
||||
const paramsWithAnimation = {
|
||||
parameter: toRaw(rawParams.parameter),
|
||||
advance: toRaw(rawParams.advance),
|
||||
optional: {
|
||||
...toRaw(rawParams.optional),
|
||||
voltage_n: 1 // 强制将电压份数设置为1
|
||||
},
|
||||
animation_enabled: animationRef.value?.isEnabled() ?? false
|
||||
}
|
||||
await window.pywebview.api.calculate(paramsWithAnimation)
|
||||
// 不在这里设置 calculating = false,等待 receiveResult 回调
|
||||
} else {
|
||||
// 开发模式下的模拟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
logRef.value?.addLog('info', '开始 EGM 计算(开发模式)...')
|
||||
logRef.value?.addLog('info', '参数: 额定电压=750kV, 雷暴日=20d, 海拔=1000m')
|
||||
logRef.value?.addLog('info', '计算完成')
|
||||
result.value = {
|
||||
tripping_rate: 0.0581,
|
||||
n_sf_phases: [0.0421, 0.0581, 0.0392],
|
||||
message: '计算完成'
|
||||
}
|
||||
calculating.value = false
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message || '计算失败'
|
||||
logRef.value?.addLog('error', e.message || '计算失败')
|
||||
calculating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 将参数转换为 TOML 格式
|
||||
const tomlStringify = (obj: any, indent: string = ''): string => {
|
||||
let result = ''
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === null || value === undefined) continue
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
result += `${indent}${key} = [${value.join(', ')}]\n`
|
||||
} else if (typeof value === 'object') {
|
||||
result += `\n${indent}[${key}]\n`
|
||||
result += tomlStringify(value, indent)
|
||||
} else if (typeof value === 'string') {
|
||||
result += `${indent}${key} = "${value}"\n`
|
||||
} else if (typeof value === 'boolean') {
|
||||
result += `${indent}${key} = ${value}\n`
|
||||
} else {
|
||||
result += `${indent}${key} = ${value}\n`
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 解析 TOML 格式字符串
|
||||
const parseToml = (tomlStr: string): any => {
|
||||
const result: any = {}
|
||||
let currentSection: any = result
|
||||
let currentSectionName = ''
|
||||
|
||||
const lines = tomlStr.split('\n')
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim()
|
||||
|
||||
// 跳过空行和注释
|
||||
if (!line || line.startsWith('#')) continue
|
||||
|
||||
// 匹配 section [xxx]
|
||||
const sectionMatch = line.match(/^\[([^\]]+)\]$/)
|
||||
if (sectionMatch) {
|
||||
currentSectionName = sectionMatch[1]
|
||||
currentSection = {}
|
||||
result[currentSectionName] = currentSection
|
||||
continue
|
||||
}
|
||||
|
||||
// 匹配 key = value
|
||||
const kvMatch = line.match(/^([^=]+)=(.*)$/)
|
||||
if (kvMatch) {
|
||||
const key = kvMatch[1].trim()
|
||||
let value: any = kvMatch[2].trim()
|
||||
|
||||
// 解析数组 [1, 2, 3]
|
||||
if (value.startsWith('[') && value.endsWith(']')) {
|
||||
const arrStr = value.slice(1, -1).trim()
|
||||
if (arrStr) {
|
||||
value = arrStr.split(',').map((s: string) => {
|
||||
s = s.trim()
|
||||
if (s.startsWith('"') && s.endsWith('"')) {
|
||||
return s.slice(1, -1)
|
||||
}
|
||||
return isNaN(Number(s)) ? s : Number(s)
|
||||
})
|
||||
} else {
|
||||
value = []
|
||||
}
|
||||
}
|
||||
// 解析字符串 "xxx"
|
||||
else if (value.startsWith('"') && value.endsWith('"')) {
|
||||
value = value.slice(1, -1)
|
||||
}
|
||||
// 解析布尔值
|
||||
else if (value === 'true') {
|
||||
value = true
|
||||
} else if (value === 'false') {
|
||||
value = false
|
||||
}
|
||||
// 解析数字
|
||||
else if (!isNaN(Number(value))) {
|
||||
value = Number(value)
|
||||
}
|
||||
|
||||
currentSection[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 导出日志
|
||||
const exportLog = async () => {
|
||||
try {
|
||||
const logText = logRef.value?.getLogsText() || ''
|
||||
if (!logText) {
|
||||
logRef.value?.addLog('warning', '暂无日志可导出')
|
||||
return
|
||||
}
|
||||
|
||||
if (window.pywebview) {
|
||||
const response = await window.pywebview.api.export_log(logText)
|
||||
if (response.success) {
|
||||
logRef.value?.addLog('info', response.message)
|
||||
} else {
|
||||
logRef.value?.addLog('warning', response.message)
|
||||
}
|
||||
} else {
|
||||
// 开发模式下的模拟
|
||||
logRef.value?.addLog('info', '导出日志(开发模式,直接下载)')
|
||||
const blob = new Blob([logText], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')
|
||||
a.download = `egm_log_${timestamp}.txt`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message || '导出日志失败'
|
||||
logRef.value?.addLog('error', e.message || '导出日志失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出配置
|
||||
const exportConfig = async () => {
|
||||
try {
|
||||
if (window.pywebview) {
|
||||
const response = await window.pywebview.api.export_config(params)
|
||||
if (response.success) {
|
||||
// 显示导出的文件路径
|
||||
currentFilePath.value = response.file_path || ''
|
||||
logRef.value?.addLog('info', response.message)
|
||||
} else {
|
||||
logRef.value?.addLog('warning', response.message)
|
||||
}
|
||||
} else {
|
||||
// 开发模式下的模拟
|
||||
logRef.value?.addLog('info', '导出配置(开发模式,直接下载)')
|
||||
const config = tomlStringify(params)
|
||||
const blob = new Blob([config], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')
|
||||
a.download = `egm_config_${timestamp}.toml`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message || '导出失败'
|
||||
logRef.value?.addLog('error', e.message || '导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 导入配置 - 调用后端文件对话框
|
||||
const importConfig = async () => {
|
||||
try {
|
||||
if (window.pywebview) {
|
||||
const response = await window.pywebview.api.import_config()
|
||||
if (response.success && response.params) {
|
||||
// 合并导入的参数到当前参数
|
||||
if (response.params.parameter) {
|
||||
Object.assign(params.parameter, response.params.parameter)
|
||||
}
|
||||
if (response.params.advance) {
|
||||
Object.assign(params.advance, response.params.advance)
|
||||
}
|
||||
if (response.params.optional) {
|
||||
Object.assign(params.optional, response.params.optional)
|
||||
}
|
||||
|
||||
// 显示完整文件路径
|
||||
currentFilePath.value = response.file_path || ''
|
||||
|
||||
logRef.value?.addLog('info', `成功导入配置: ${response.file_path}`)
|
||||
result.value = null
|
||||
error.value = null
|
||||
} else if (!response.success && response.message !== '用户取消了选择') {
|
||||
error.value = response.message || '导入失败'
|
||||
logRef.value?.addLog('error', response.message || '导入失败')
|
||||
}
|
||||
} else {
|
||||
// 开发模式下使用 HTML 文件输入
|
||||
fileInput.value?.click()
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message || '导入失败'
|
||||
logRef.value?.addLog('error', e.message || '导入失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件选择(开发模式备用)
|
||||
const handleFileSelect = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const content = await file.text()
|
||||
const importedParams = parseToml(content)
|
||||
|
||||
// 合并导入的参数到当前参数
|
||||
if (importedParams.parameter) {
|
||||
Object.assign(params.parameter, importedParams.parameter)
|
||||
}
|
||||
if (importedParams.advance) {
|
||||
Object.assign(params.advance, importedParams.advance)
|
||||
}
|
||||
if (importedParams.optional) {
|
||||
Object.assign(params.optional, importedParams.optional)
|
||||
}
|
||||
|
||||
currentFilePath.value = file.name
|
||||
|
||||
logRef.value?.addLog('info', `成功导入配置: ${file.name}`)
|
||||
result.value = null
|
||||
error.value = null
|
||||
} catch (e: any) {
|
||||
error.value = e.message || '导入失败'
|
||||
logRef.value?.addLog('error', e.message || '导入失败')
|
||||
}
|
||||
|
||||
// 清空 input 以便可以重复选择同一个文件
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
// 声明 pywebview API 类型
|
||||
declare global {
|
||||
interface Window {
|
||||
pywebview?: {
|
||||
api: {
|
||||
calculate: (params: AllParameters) => Promise<any>
|
||||
import_config: () => Promise<any>
|
||||
export_config: (params: AllParameters) => Promise<any>
|
||||
export_log: (logText: string) => Promise<any>
|
||||
}
|
||||
}
|
||||
addLogFromBackend?: (log: { level: string; time: string; message: string }) => void
|
||||
receiveResult?: (result: { success: boolean; message: string; data?: any; error?: string }) => void
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用数字输入框的滚轮调整功能
|
||||
const preventWheelOnNumberInput = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
if (target && target.type === 'number') {
|
||||
e.preventDefault()
|
||||
;(target as HTMLElement).blur()
|
||||
}
|
||||
}
|
||||
|
||||
// 注册全局日志接收函数,供后端实时调用
|
||||
onMounted(() => {
|
||||
// 程序启动时,根据雷暴日初始化地闪密度
|
||||
if (params.parameter.td > 0 && params.advance.ng < 0) {
|
||||
params.advance.ng = Math.round(0.023 * Math.pow(params.parameter.td, 1.3) * 100) / 100
|
||||
}
|
||||
|
||||
// 禁用数字输入框的滚轮调整
|
||||
document.addEventListener('wheel', preventWheelOnNumberInput, { passive: false })
|
||||
|
||||
// 实时日志推送
|
||||
window.addLogFromBackend = (log: { level: string; time: string; message: string }) => {
|
||||
logRef.value?.addLog(log.level as any, log.message)
|
||||
}
|
||||
|
||||
// 接收计算结果
|
||||
window.receiveResult = (res: { success: boolean; message: string; data?: any; error?: string }) => {
|
||||
calculating.value = false
|
||||
if (res.success && res.data) {
|
||||
result.value = {
|
||||
tripping_rate: res.data.avr_n_sf || 0,
|
||||
n_sf_phases: res.data.n_sf_phases || [],
|
||||
message: res.message || '计算完成'
|
||||
}
|
||||
logRef.value?.addLog('info', '计算完成')
|
||||
} else {
|
||||
error.value = res.error || res.message
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('wheel', preventWheelOnNumberInput)
|
||||
window.addLogFromBackend = undefined
|
||||
window.receiveResult = undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-text {
|
||||
user-select: text;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
/* 隐藏数字输入框的上下箭头 */
|
||||
:deep(input[type="number"]) {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
:deep(input[type="number"]::-webkit-inner-spin-button),
|
||||
:deep(input[type="number"]::-webkit-outer-spin-button) {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
14
webui/src/main.ts
Normal file
14
webui/src/main.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createApp } from 'vue'
|
||||
import { Quasar } from 'quasar'
|
||||
import '@quasar/extras/material-icons/material-icons.css'
|
||||
import 'quasar/src/css/index.sass'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(Quasar, {
|
||||
plugins: {}
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
9
webui/src/quasar-variables.sass
Normal file
9
webui/src/quasar-variables.sass
Normal file
@@ -0,0 +1,9 @@
|
||||
// Quasar SCSS Variables
|
||||
$primary : #1976D2
|
||||
$secondary : #26A69A
|
||||
$accent : #9C27B0
|
||||
$dark : #1D1D1D
|
||||
$positive : #21BA45
|
||||
$negative : #C10015
|
||||
$info : #31CCEC
|
||||
$warning : #F2C037
|
||||
19
webui/src/style.css
Normal file
19
webui/src/style.css
Normal file
@@ -0,0 +1,19 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
44
webui/src/types/index.ts
Normal file
44
webui/src/types/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// EGM 计算参数类型定义
|
||||
// Version: 1.0.11
|
||||
|
||||
export interface Parameter {
|
||||
// 基本参数
|
||||
rated_voltage: string // 额定电压等级 (kV)
|
||||
ac_or_dc: string // 交流或直流标识,"AC" 或 "DC"
|
||||
h_c_sag: number // 导线弧垂 (m)
|
||||
h_g_sag: number // 地线弧垂 (m)
|
||||
insulator_c_len: number // 导线串子绝缘长度 (m)
|
||||
string_c_len: number // 导线串长 (m)
|
||||
string_g_len: number // 地线串长 (m)
|
||||
h_arm: number[] // 导、地线挂点垂直距离 (m)
|
||||
gc_x: number[] // 导、地线水平坐标 (m)
|
||||
ground_angels: number[] // 地面倾角 (°)
|
||||
altitude: number // 海拔高度 (m)
|
||||
td: number // 雷暴日 (d)
|
||||
z_0: number // 雷电波阻抗 (Ω),默认 300
|
||||
z_c: number // 导线波阻抗 (Ω),默认 251
|
||||
}
|
||||
|
||||
export interface AdvanceParameter {
|
||||
ng: number // 地闪密度 (次/(km²·a))
|
||||
Ip_a: number // 雷电流概率密度曲线系数a
|
||||
Ip_b: number // 雷电流概率密度曲线系数b
|
||||
u_50: number // 50%击穿电压 (kV),-1表示自动计算
|
||||
}
|
||||
|
||||
export interface OptionalParameter {
|
||||
voltage_n: number // 计算时电压分成多少份
|
||||
max_i: number // 最大尝试雷电流 (kA)
|
||||
}
|
||||
|
||||
export interface AllParameters {
|
||||
parameter: Parameter
|
||||
advance: AdvanceParameter
|
||||
optional: OptionalParameter
|
||||
}
|
||||
|
||||
export interface CalculationResult {
|
||||
success: boolean
|
||||
message?: string
|
||||
data?: any
|
||||
}
|
||||
3
webui/src/vite-env.d.ts
vendored
Normal file
3
webui/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __APP_VERSION__: string
|
||||
11
webui/tailwind.config.js
Normal file
11
webui/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
25
webui/tsconfig.json
Normal file
25
webui/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
1
webui/tsconfig.node.json
Normal file
1
webui/tsconfig.node.json
Normal file
@@ -0,0 +1 @@
|
||||
{"compilerOptions": {"composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true}, "include": ["vite.config.ts"]}
|
||||
38
webui/vite.config.ts
Normal file
38
webui/vite.config.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
const version = readFileSync(fileURLToPath(new URL('../VERSION', import.meta.url)), 'utf-8').trim()
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(version)
|
||||
},
|
||||
base: './',
|
||||
plugins: [
|
||||
vue({
|
||||
template: { transformAssetUrls }
|
||||
}),
|
||||
quasar(),
|
||||
{
|
||||
name: 'html-version',
|
||||
transformIndexHtml(html) {
|
||||
return html.replace(
|
||||
/<title>EGM 输电线路绕击跳闸率计算( v[\d.]+)?<\/title>/,
|
||||
`<title>EGM 输电线路绕击跳闸率计算 v${version}</title>`
|
||||
)
|
||||
}
|
||||
}
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
host: true
|
||||
}
|
||||
})
|
||||
662
webview_app.py
Normal file
662
webview_app.py
Normal file
@@ -0,0 +1,662 @@
|
||||
"""
|
||||
EGM 输电线路绕击跳闸率计算程序 - Pywebview 界面
|
||||
使用 Vue 3 + Quasar + TypeScript + Tailwind CSS 作为前端
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import math
|
||||
import threading
|
||||
import queue
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime
|
||||
import webview
|
||||
from loguru import logger
|
||||
|
||||
# 添加项目根目录到路径
|
||||
project_root = Path(__file__).parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
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):
|
||||
self.callback = callback
|
||||
self.logs: List[Dict[str, str]] = []
|
||||
|
||||
def write(self, message):
|
||||
if message.strip():
|
||||
log_entry = {
|
||||
"level": "info",
|
||||
"time": datetime.now().strftime("%H:%M:%S"),
|
||||
"message": message.strip()
|
||||
}
|
||||
self.logs.append(log_entry)
|
||||
if self.callback:
|
||||
self.callback(log_entry)
|
||||
|
||||
|
||||
class LoguruWebHandler:
|
||||
"""Loguru 日志处理器,将 loguru 日志转发到 Web 界面"""
|
||||
|
||||
def __init__(self, log_queue: queue.Queue):
|
||||
self.log_queue = log_queue
|
||||
|
||||
def write(self, message):
|
||||
"""loguru handler 的写入方法"""
|
||||
record = message.record
|
||||
level = record['level'].name.lower()
|
||||
|
||||
# 映射 loguru 级别到前端级别
|
||||
level_map = {
|
||||
'trace': 'debug',
|
||||
'debug': 'debug',
|
||||
'info': 'info',
|
||||
'success': 'info',
|
||||
'warning': 'warning',
|
||||
'error': 'error',
|
||||
'critical': 'error'
|
||||
}
|
||||
frontend_level = level_map.get(level, 'info')
|
||||
|
||||
# 提取消息文本
|
||||
msg = record['message']
|
||||
if msg.strip():
|
||||
log_entry = {
|
||||
"level": frontend_level,
|
||||
"time": datetime.now().strftime("%H:%M:%S"),
|
||||
"message": msg
|
||||
}
|
||||
# 将日志放入队列,由主线程处理
|
||||
self.log_queue.put(log_entry)
|
||||
|
||||
|
||||
class EGMWebApp:
|
||||
"""EGM 计算程序的 Web 界面后端"""
|
||||
|
||||
def __init__(self):
|
||||
self.window = None
|
||||
self.web_handler = None
|
||||
self.logs: List[Dict[str, str]] = []
|
||||
self._loguru_handler_id = None
|
||||
self._log_queue: queue.Queue = queue.Queue()
|
||||
self._running = False
|
||||
self.animation = WebAnimation() # Web 动画实例
|
||||
|
||||
def _process_log_queue(self):
|
||||
"""处理日志队列,在主线程中定时调用"""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
try:
|
||||
# 处理队列中的所有日志
|
||||
while not self._log_queue.empty():
|
||||
try:
|
||||
log_entry = self._log_queue.get_nowait()
|
||||
self._push_log_to_frontend(log_entry)
|
||||
except queue.Empty:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"处理日志队列失败: {e}")
|
||||
|
||||
# 继续定时检查
|
||||
if self._running:
|
||||
threading.Timer(0.05, self._process_log_queue).start()
|
||||
|
||||
def _push_log_to_frontend(self, log_entry: Dict[str, str]):
|
||||
"""推送单条日志到前端"""
|
||||
if self.window:
|
||||
try:
|
||||
js_code = f'if(window.addLogFromBackend){{window.addLogFromBackend({json.dumps(log_entry)})}}'
|
||||
self.window.evaluate_js(js_code)
|
||||
except Exception as e:
|
||||
print(f"推送日志到前端失败: {e}")
|
||||
|
||||
def add_log(self, level: str, message: str):
|
||||
"""添加日志并实时推送到前端"""
|
||||
log_entry = {
|
||||
"level": level,
|
||||
"time": datetime.now().strftime("%H:%M:%S"),
|
||||
"message": message
|
||||
}
|
||||
self.logs.append(log_entry)
|
||||
|
||||
# 将日志放入队列,由主线程处理
|
||||
self._log_queue.put(log_entry)
|
||||
|
||||
def get_logs(self) -> List[Dict[str, str]]:
|
||||
"""获取日志列表"""
|
||||
logs = self.logs.copy()
|
||||
self.logs = []
|
||||
return logs
|
||||
|
||||
def _setup_loguru_handler(self):
|
||||
"""设置 loguru 处理器,捕获所有 logger 调用"""
|
||||
self._loguru_handler_id = logger.add(
|
||||
LoguruWebHandler(self._log_queue),
|
||||
format="{message}",
|
||||
level="DEBUG"
|
||||
)
|
||||
|
||||
def _remove_loguru_handler(self):
|
||||
"""移除 loguru 处理器"""
|
||||
if self._loguru_handler_id is not None:
|
||||
logger.remove(self._loguru_handler_id)
|
||||
self._loguru_handler_id = None
|
||||
|
||||
def calculate(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
执行 EGM 计算(启动后台线程,立即返回)
|
||||
|
||||
Args:
|
||||
params: 包含 parameter, advance, optional 的字典
|
||||
|
||||
Returns:
|
||||
计算状态字典
|
||||
"""
|
||||
self.logs = [] # 清空日志
|
||||
self._log_queue = queue.Queue() # 重置队列
|
||||
|
||||
# 启动日志队列处理器
|
||||
self._running = True
|
||||
self._process_log_queue()
|
||||
|
||||
# 启动后台线程执行计算
|
||||
thread = threading.Thread(target=self._calculate_thread, args=(params,))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return {"status": "started", "message": "计算已启动"}
|
||||
|
||||
def _calculate_thread(self, params: Dict[str, Any]):
|
||||
"""后台线程中执行计算"""
|
||||
# 设置 loguru 处理器,捕获所有 logger.info/debug 等调用
|
||||
self._setup_loguru_handler()
|
||||
|
||||
self.add_log("info", "开始 EGM 计算...")
|
||||
|
||||
try:
|
||||
# 解析参数
|
||||
parameter_data = params.get('parameter', {})
|
||||
advance_data = params.get('advance', {})
|
||||
optional_data = params.get('optional', {})
|
||||
|
||||
# 创建局部参数对象
|
||||
para = Parameter()
|
||||
para.h_g_sag = float(parameter_data.get('h_g_sag', 11.67))
|
||||
para.h_c_sag = float(parameter_data.get('h_c_sag', 14.43))
|
||||
para.td = int(parameter_data.get('td', 20))
|
||||
para.insulator_c_len = float(parameter_data.get('insulator_c_len', 7.02))
|
||||
para.string_c_len = float(parameter_data.get('string_c_len', 9.2))
|
||||
para.string_g_len = float(parameter_data.get('string_g_len', 0.5))
|
||||
# 确保数组元素转换为数字类型
|
||||
para.gc_x = [float(x) for x in parameter_data.get('gc_x', [17.9, 17])]
|
||||
para.ground_angels = [
|
||||
float(angel) / 180 * math.pi
|
||||
for angel in parameter_data.get('ground_angels', [0])
|
||||
]
|
||||
para.h_arm = [float(h) for h in parameter_data.get('h_arm', [150, 130])]
|
||||
para.altitude = int(parameter_data.get('altitude', 1000))
|
||||
# 解析电压等级字符串,如 "500kV" -> 500
|
||||
rated_voltage_str = str(parameter_data.get('rated_voltage', '500kV'))
|
||||
para.rated_voltage = float(rated_voltage_str.replace('kV', '').replace('±', ''))
|
||||
|
||||
para.ng = float(advance_data.get('ng', -1))
|
||||
para.Ip_a = float(advance_data.get('Ip_a', -1))
|
||||
para.Ip_b = float(advance_data.get('Ip_b', -1))
|
||||
para.u_50 = float(advance_data.get('u_50', -1))
|
||||
|
||||
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'))
|
||||
para.ac_or_dc = 'DC' if 'DC' in ac_or_dc_value.upper() else 'AC'
|
||||
|
||||
# 调用 main.py 的参数显示函数,日志会被 loguru handler 捕获
|
||||
# parameter_display(para)
|
||||
|
||||
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, animation_obj)
|
||||
|
||||
self.add_log("info", "EGM 计算完成")
|
||||
|
||||
# 推送结果到前端
|
||||
self._send_result_to_frontend(result)
|
||||
|
||||
# 等待队列中的日志处理完毕
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
while not self._log_queue.empty():
|
||||
time.sleep(0.05)
|
||||
|
||||
# 停止日志队列处理器
|
||||
self._running = False
|
||||
|
||||
# 移除 loguru 处理器
|
||||
self._remove_loguru_handler()
|
||||
|
||||
except Exception as e:
|
||||
self.add_log("error", f"计算失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# 停止日志队列处理器
|
||||
self._running = False
|
||||
|
||||
# 移除 loguru 处理器
|
||||
self._remove_loguru_handler()
|
||||
|
||||
# 推送错误到前端
|
||||
self._send_result_to_frontend({
|
||||
"success": False,
|
||||
"message": f"计算失败: {str(e)}",
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
def _send_result_to_frontend(self, result: Dict[str, Any]):
|
||||
"""将计算结果推送到前端"""
|
||||
if self.window:
|
||||
try:
|
||||
js_code = f'if(window.receiveResult){{window.receiveResult({json.dumps(result)})}}'
|
||||
self.window.evaluate_js(js_code)
|
||||
except Exception as e:
|
||||
logger.error(f"推送结果到前端失败: {e}")
|
||||
|
||||
def dict_to_toml(self, obj: Dict[str, Any], indent: str = '') -> str:
|
||||
"""
|
||||
将字典转换为 TOML 格式字符串
|
||||
|
||||
Args:
|
||||
obj: 参数字典
|
||||
indent: 缩进字符串
|
||||
|
||||
Returns:
|
||||
TOML 格式字符串
|
||||
"""
|
||||
result = ''
|
||||
|
||||
for key, value in obj.items():
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
if isinstance(value, list):
|
||||
result += f'{indent}{key} = [{", ".join(str(v) for v in value)}]\n'
|
||||
elif isinstance(value, dict):
|
||||
result += f'\n{indent}[{key}]\n'
|
||||
result += self.dict_to_toml(value, indent)
|
||||
elif isinstance(value, str):
|
||||
result += f'{indent}{key} = "{value}"\n'
|
||||
elif isinstance(value, bool):
|
||||
result += f'{indent}{key} = {str(value).lower()}\n'
|
||||
else:
|
||||
result += f'{indent}{key} = {value}\n'
|
||||
|
||||
return result
|
||||
|
||||
def export_config(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
导出配置为 TOML 文件,弹出保存对话框
|
||||
|
||||
Args:
|
||||
params: 参数字典
|
||||
|
||||
Returns:
|
||||
包含保存状态和路径的字典
|
||||
"""
|
||||
try:
|
||||
# 生成默认文件名
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
default_filename = f'egm_config_{timestamp}.toml'
|
||||
|
||||
# 打开保存文件对话框
|
||||
result = self.window.create_file_dialog(
|
||||
webview.SAVE_DIALOG,
|
||||
directory='',
|
||||
save_filename=default_filename,
|
||||
file_types=('TOML Files (*.toml)', 'All files (*.*)')
|
||||
)
|
||||
|
||||
if result and len(result) > 0:
|
||||
file_path = result[0]
|
||||
|
||||
# 转换为 TOML 格式
|
||||
toml_content = self.dict_to_toml(params)
|
||||
|
||||
# 写入文件
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(toml_content)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"配置已保存到: {file_path}",
|
||||
"file_path": file_path
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "用户取消了保存操作"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"导出配置失败: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"保存失败: {str(e)}"
|
||||
}
|
||||
|
||||
def export_log(self, log_text: str) -> Dict[str, Any]:
|
||||
"""
|
||||
导出日志为 TXT 文件,弹出保存对话框
|
||||
|
||||
Args:
|
||||
log_text: 日志文本内容
|
||||
|
||||
Returns:
|
||||
包含保存状态和路径的字典
|
||||
"""
|
||||
try:
|
||||
# 生成默认文件名
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
default_filename = f'egm_log_{timestamp}.txt'
|
||||
|
||||
# 打开保存文件对话框
|
||||
result = self.window.create_file_dialog(
|
||||
webview.SAVE_DIALOG,
|
||||
directory='',
|
||||
save_filename=default_filename,
|
||||
file_types=('Text Files (*.txt)', 'All files (*.*)')
|
||||
)
|
||||
|
||||
if result and len(result) > 0:
|
||||
file_path = result[0]
|
||||
|
||||
# 写入文件
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(log_text)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"日志已保存到: {file_path}",
|
||||
"file_path": file_path
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "用户取消了保存操作"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"导出日志失败: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"保存失败: {str(e)}"
|
||||
}
|
||||
|
||||
def import_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
导入配置从 TOML 文件,弹出打开对话框
|
||||
|
||||
Returns:
|
||||
包含解析后的参数和文件路径的字典
|
||||
"""
|
||||
try:
|
||||
# 打开文件选择对话框
|
||||
result = self.window.create_file_dialog(
|
||||
webview.OPEN_DIALOG,
|
||||
directory='',
|
||||
file_types=('TOML Files (*.toml)', 'All files (*.*)')
|
||||
)
|
||||
|
||||
if result and len(result) > 0:
|
||||
file_path = result[0]
|
||||
|
||||
# 读取并解析 TOML 文件
|
||||
with open(file_path, 'rb') as f:
|
||||
toml_data = tomllib.load(f)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"成功导入配置",
|
||||
"file_path": file_path,
|
||||
"params": toml_data
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "用户取消了选择"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"导入配置失败: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"导入失败: {str(e)}"
|
||||
}
|
||||
|
||||
def get_default_config(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取默认配置
|
||||
|
||||
Returns:
|
||||
默认配置字典
|
||||
"""
|
||||
return {
|
||||
"parameter": {
|
||||
"rated_voltage": 750,
|
||||
"h_c_sag": 14.43,
|
||||
"h_g_sag": 11.67,
|
||||
"insulator_c_len": 7.02,
|
||||
"string_c_len": 9.2,
|
||||
"string_g_len": 0.5,
|
||||
"h_arm": [150, 130],
|
||||
"gc_x": [17.9, 17],
|
||||
"ground_angels": [0],
|
||||
"altitude": 1000,
|
||||
"td": 20
|
||||
},
|
||||
"advance": {
|
||||
"ng": -1,
|
||||
"Ip_a": -1,
|
||||
"Ip_b": -1
|
||||
},
|
||||
"optional": {
|
||||
"voltage_n": 3,
|
||||
"max_i": 200
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def start_webview():
|
||||
"""启动 pywebview 界面"""
|
||||
|
||||
# 确定前端 URL
|
||||
# 在开发环境中使用 Vite 开发服务器
|
||||
# 在生产环境中使用构建后的文件
|
||||
|
||||
# 检查是否在打包环境中运行
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包环境:强制使用生产模式,禁用调试
|
||||
dev_mode = False
|
||||
else:
|
||||
# 开发环境:通过环境变量控制
|
||||
dev_mode = os.getenv('EGM_DEV_MODE', 'true').lower() == 'true'
|
||||
|
||||
if dev_mode:
|
||||
# 开发模式:使用 Vite 开发服务器
|
||||
url = 'http://localhost:5173'
|
||||
logger.info(f"开发模式:使用 Vite 开发服务器 {url}")
|
||||
logger.info("请先在 webui 目录中运行: npm install && npm run dev")
|
||||
else:
|
||||
# 生产模式:使用构建后的文件
|
||||
dist_path = project_root / 'webui' / 'dist'
|
||||
if not dist_path.exists():
|
||||
logger.error(f"构建目录不存在: {dist_path}")
|
||||
logger.error("请先运行: cd webui && npm run build")
|
||||
sys.exit(1)
|
||||
url = f'file://{dist_path / "index.html"}'
|
||||
logger.info(f"生产模式:使用构建文件 {url}")
|
||||
|
||||
# 创建 API 实例
|
||||
api = EGMWebApp()
|
||||
|
||||
# 创建窗口
|
||||
window = webview.create_window(
|
||||
title='EGM 输电线路绕击跳闸率计算',
|
||||
url=url,
|
||||
js_api=api,
|
||||
width=1500,
|
||||
height=900,
|
||||
resizable=True,
|
||||
min_size=(800, 600)
|
||||
)
|
||||
|
||||
# 将窗口对象传递给 API
|
||||
api.window = window
|
||||
api.animation.set_window(window)
|
||||
|
||||
# 启动
|
||||
logger.info("启动 EGM Web 界面...")
|
||||
webview.start(debug=dev_mode)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 配置日志
|
||||
logger.remove()
|
||||
logger.add(sys.stderr, level="INFO")
|
||||
logger.add("egm_webui.log", rotation="10 MB", retention="7 days")
|
||||
|
||||
# 启动界面
|
||||
start_webview()
|
||||
Reference in New Issue
Block a user