Compare commits
27 Commits
8a74a576c0
...
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 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -14,4 +14,8 @@ settings.json
|
||||
node_modules
|
||||
*.log
|
||||
*.lock
|
||||
*.pdf
|
||||
*.pdf
|
||||
lightening.ico
|
||||
metadata.yml
|
||||
VERSION
|
||||
生成exe图标.png
|
||||
|
||||
14
Makefile
14
Makefile
@@ -1,15 +1,15 @@
|
||||
target: dist build
|
||||
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
|
||||
|
||||
gui: build
|
||||
uv run python update_version.py
|
||||
cd webui && npm run build
|
||||
cd ..
|
||||
uv run pyinstaller webview_app.py -n LighteningGUI --noconsole --add-data "webui/dist;webui/dist" -y
|
||||
|
||||
dist:
|
||||
mkdir dist
|
||||
|
||||
@@ -34,7 +34,6 @@ pip install -r requirements.txt
|
||||
|
||||
- ezdxf - DXF文件生成
|
||||
- loguru - 日志记录
|
||||
- matplotlib - 数据可视化和动画
|
||||
- numpy - 数值计算
|
||||
- tomli - TOML配置文件解析
|
||||
- pywebview - 图形界面框架
|
||||
|
||||
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 enable(self, _enable):
|
||||
self._disable = not _enable
|
||||
|
||||
@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
|
||||
10
core.py
10
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
|
||||
@@ -28,6 +28,7 @@ class 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,13 +188,16 @@ def solve_circle_line_intersection(
|
||||
return [_x, _y]
|
||||
|
||||
|
||||
def min_i(string_len, u_ph, altitude: float = 0, z_0: float = 300, z_c: float = 251):
|
||||
def min_i(string_len, u_ph, altitude: float = 0, z_0: float = 300, z_c: float = 251, u_50: float = None):
|
||||
# 海拔修正
|
||||
if altitude > 1000:
|
||||
k_a = math.exp((altitude - 1000) / 8150) # 气隙海拔修正
|
||||
else:
|
||||
k_a = 1
|
||||
u_50 = 1 / k_a * (530 * string_len + 35) # 50045 上附录的公式,实际应该用负极性电压的公式
|
||||
# 只有在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海拔
|
||||
|
||||
@@ -49,10 +49,13 @@ def read_parameter(toml_file_path) -> Parameter:
|
||||
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"]
|
||||
@@ -152,7 +155,9 @@ def egm():
|
||||
i_max = 0
|
||||
insulator_c_len = para.insulator_c_len
|
||||
# i_min = min_i(insulator_c_len, u_ph / 1.732)
|
||||
i_min = min_i(insulator_c_len, u_ph, para.altitude)
|
||||
# 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)
|
||||
|
||||
37
main.py
37
main.py
@@ -56,10 +56,13 @@ def read_parameter(toml_file_path) -> Parameter:
|
||||
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"]
|
||||
@@ -142,31 +145,26 @@ def run_egm(para: Parameter, animation=None) -> dict:
|
||||
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.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)
|
||||
* rated_voltage
|
||||
* math.cos(2 * math.pi / voltage_n * u_bar)
|
||||
/ 1.732
|
||||
) # 运行相电压
|
||||
# u_ph = (
|
||||
# math.sqrt(2)
|
||||
# * rated_voltage
|
||||
# * math.cos(2 * math.pi / voltage_n * u_bar)
|
||||
# / 1.732
|
||||
# ) # 运行相电压
|
||||
u_ph = rated_voltage/1.732
|
||||
else:
|
||||
u_ph = rated_voltage
|
||||
logger.info(f"计算第{phase_conductor_foo + 1}相,电压为{u_ph:.2f}kV")
|
||||
@@ -175,7 +173,9 @@ def run_egm(para: Parameter, animation=None) -> dict:
|
||||
insulator_c_len = para.insulator_c_len
|
||||
# i_min = min_i(insulator_c_len, u_ph / 1.732)
|
||||
# TODO 需要考虑交、直流
|
||||
i_min = min_i(insulator_c_len, u_ph, para.altitude, para.z_0, para.z_c)
|
||||
# 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)
|
||||
@@ -261,7 +261,8 @@ def run_egm(para: Parameter, animation=None) -> dict:
|
||||
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(
|
||||
(
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
version: 1.0.12
|
||||
company_name: EGM
|
||||
file_description: EGM Lightning Protection Calculator
|
||||
product_name: Lightening
|
||||
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()
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""更新版本号脚本"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -20,22 +19,6 @@ def increment_version(version: str) -> str:
|
||||
return version
|
||||
|
||||
|
||||
def update_index_html(version: str):
|
||||
"""更新 webui/index.html 中的标题版本号"""
|
||||
index_file = Path(__file__).parent / "webui" / "index.html"
|
||||
content = index_file.read_text(encoding="utf-8")
|
||||
|
||||
# 替换标题中的版本号
|
||||
new_content = re.sub(
|
||||
r"<title>EGM 输电线路绕击跳闸率计算( v[\d.]+)?</title>",
|
||||
f"<title>EGM 输电线路绕击跳闸率计算 v{version}</title>",
|
||||
content
|
||||
)
|
||||
|
||||
index_file.write_text(new_content, encoding="utf-8")
|
||||
print(f"Updated version in {index_file} to v{version}")
|
||||
|
||||
|
||||
def update_version_file(version: str):
|
||||
"""更新 VERSION 文件"""
|
||||
version_file = Path(__file__).parent / "VERSION"
|
||||
@@ -66,7 +49,6 @@ def main():
|
||||
|
||||
# 更新所有文件
|
||||
update_version_file(new_version)
|
||||
update_index_html(new_version)
|
||||
create_metadata(new_version)
|
||||
|
||||
print(f"Version updated: {current_version} -> {new_version}")
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EGM 输电线路绕击跳闸率计算 v1.0.12</title>
|
||||
<title>EGM 输电线路绕击跳闸率计算</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
<template>
|
||||
<ParameterForm />
|
||||
<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>
|
||||
|
||||
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>
|
||||
@@ -2,198 +2,222 @@
|
||||
<q-layout view="lHh lpr lFf">
|
||||
<q-header elevated class="bg-indigo-600 text-white">
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>
|
||||
<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-4xl mx-auto">
|
||||
<!-- 基本参数 -->
|
||||
<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="settings" />
|
||||
基本参数
|
||||
</div>
|
||||
</q-card-section>
|
||||
<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>
|
||||
<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 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="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 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="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 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>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</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">
|
||||
@@ -249,6 +273,26 @@
|
||||
</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>
|
||||
|
||||
@@ -263,15 +307,6 @@
|
||||
|
||||
<q-card-section>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.optional.voltage_n"
|
||||
type="number"
|
||||
label="计算时电压分成多少份"
|
||||
>
|
||||
<q-tooltip>将电压波形离散化的份数,即将交流电压在一个周期内的不同值进行计算。</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
v-model="params.optional.max_i"
|
||||
@@ -322,7 +357,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<!-- 隐藏的文件输入(开发模式备用) -->
|
||||
<input
|
||||
ref="fileInput"
|
||||
type="file"
|
||||
@@ -379,10 +414,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
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 = {
|
||||
@@ -405,10 +441,11 @@ const defaultParams: AllParameters = {
|
||||
advance: {
|
||||
ng: -1,
|
||||
Ip_a: -1,
|
||||
Ip_b: -1
|
||||
Ip_b: -1,
|
||||
u_50: -1
|
||||
},
|
||||
optional: {
|
||||
voltage_n: 3,
|
||||
voltage_n: 1,
|
||||
max_i: 300
|
||||
}
|
||||
}
|
||||
@@ -420,8 +457,12 @@ 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',
|
||||
@@ -433,14 +474,12 @@ const currentType = computed(() => {
|
||||
return params.parameter.rated_voltage.includes('±') ? 'DC' : 'AC'
|
||||
})
|
||||
|
||||
// 监听电压等级变化,同步更新 ac_or_dc 和 voltage_n 字段
|
||||
// 监听电压等级变化,同步更新 ac_or_dc 字段
|
||||
watch(
|
||||
() => params.parameter.rated_voltage,
|
||||
(newVoltage) => {
|
||||
const isDC = newVoltage.includes('±')
|
||||
params.parameter.ac_or_dc = isDC ? 'DC' : 'AC'
|
||||
// DC 时电压份数为 1,AC 时为 3
|
||||
params.optional.voltage_n = isDC ? 1 : 3
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
@@ -457,6 +496,17 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// 监听50%击穿电压开关
|
||||
watch(
|
||||
showU50,
|
||||
(show) => {
|
||||
if (!show) {
|
||||
// 关闭时重置为 -1(使用公式计算)
|
||||
params.advance.u_50 = -1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 雷暴日与地闪密度相互转换,公式:ng = 0.023 * td^3
|
||||
// 标志位避免循环更新
|
||||
let isUpdatingFromWatch = false
|
||||
@@ -548,6 +598,27 @@ const calculate = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 验证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
|
||||
@@ -558,8 +629,15 @@ const calculate = async () => {
|
||||
// 后台线程启动计算,实时日志通过 addLogFromBackend 推送
|
||||
// 结果通过 receiveResult 回调接收
|
||||
// 传递动画启用状态
|
||||
// 使用 toRaw 解包响应式对象,确保 pywebview 能正确序列化参数
|
||||
const rawParams = toRaw(params)
|
||||
const paramsWithAnimation = {
|
||||
...params,
|
||||
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)
|
||||
@@ -714,6 +792,8 @@ const exportConfig = async () => {
|
||||
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)
|
||||
@@ -737,12 +817,44 @@ const exportConfig = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 导入配置 - 触发文件选择
|
||||
const importConfig = () => {
|
||||
fileInput.value?.click()
|
||||
// 导入配置 - 调用后端文件对话框
|
||||
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]
|
||||
@@ -763,6 +875,8 @@ const handleFileSelect = async (event: Event) => {
|
||||
Object.assign(params.optional, importedParams.optional)
|
||||
}
|
||||
|
||||
currentFilePath.value = file.name
|
||||
|
||||
logRef.value?.addLog('info', `成功导入配置: ${file.name}`)
|
||||
result.value = null
|
||||
error.value = null
|
||||
@@ -781,6 +895,7 @@ declare global {
|
||||
pywebview?: {
|
||||
api: {
|
||||
calculate: (params: AllParameters) => Promise<any>
|
||||
import_config: () => Promise<any>
|
||||
export_config: (params: AllParameters) => Promise<any>
|
||||
export_log: (logText: string) => Promise<any>
|
||||
}
|
||||
@@ -790,6 +905,15 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用数字输入框的滚轮调整功能
|
||||
const preventWheelOnNumberInput = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
if (target && target.type === 'number') {
|
||||
e.preventDefault()
|
||||
;(target as HTMLElement).blur()
|
||||
}
|
||||
}
|
||||
|
||||
// 注册全局日志接收函数,供后端实时调用
|
||||
onMounted(() => {
|
||||
// 程序启动时,根据雷暴日初始化地闪密度
|
||||
@@ -797,6 +921,9 @@ onMounted(() => {
|
||||
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)
|
||||
@@ -819,6 +946,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('wheel', preventWheelOnNumberInput)
|
||||
window.addLogFromBackend = undefined
|
||||
window.receiveResult = undefined
|
||||
})
|
||||
@@ -829,4 +957,15 @@ onUnmounted(() => {
|
||||
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>
|
||||
@@ -23,6 +23,7 @@ export interface AdvanceParameter {
|
||||
ng: number // 地闪密度 (次/(km²·a))
|
||||
Ip_a: number // 雷电流概率密度曲线系数a
|
||||
Ip_b: number // 雷电流概率密度曲线系数b
|
||||
u_50: number // 50%击穿电压 (kV),-1表示自动计算
|
||||
}
|
||||
|
||||
export interface OptionalParameter {
|
||||
|
||||
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
|
||||
@@ -2,14 +2,29 @@ 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()
|
||||
quasar(),
|
||||
{
|
||||
name: 'html-version',
|
||||
transformIndexHtml(html) {
|
||||
return html.replace(
|
||||
/<title>EGM 输电线路绕击跳闸率计算( v[\d.]+)?<\/title>/,
|
||||
`<title>EGM 输电线路绕击跳闸率计算 v${version}</title>`
|
||||
)
|
||||
}
|
||||
}
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import json
|
||||
import math
|
||||
import threading
|
||||
import queue
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime
|
||||
@@ -315,12 +316,13 @@ class EGMWebApp:
|
||||
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 = list(parameter_data.get('gc_x', [17.9, 17]))
|
||||
# 确保数组元素转换为数字类型
|
||||
para.gc_x = [float(x) for x in parameter_data.get('gc_x', [17.9, 17])]
|
||||
para.ground_angels = [
|
||||
angel / 180 * math.pi
|
||||
float(angel) / 180 * math.pi
|
||||
for angel in parameter_data.get('ground_angels', [0])
|
||||
]
|
||||
para.h_arm = list(parameter_data.get('h_arm', [150, 130]))
|
||||
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'))
|
||||
@@ -329,6 +331,7 @@ class EGMWebApp:
|
||||
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))
|
||||
@@ -523,6 +526,46 @@ class EGMWebApp:
|
||||
"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]:
|
||||
"""
|
||||
获取默认配置
|
||||
@@ -594,7 +637,7 @@ def start_webview():
|
||||
title='EGM 输电线路绕击跳闸率计算',
|
||||
url=url,
|
||||
js_api=api,
|
||||
width=1200,
|
||||
width=1500,
|
||||
height=900,
|
||||
resizable=True,
|
||||
min_size=(800, 600)
|
||||
|
||||
Reference in New Issue
Block a user