Compare commits

...

13 Commits

Author SHA1 Message Date
dmy
18fc8fcb0e fix: 修正保护角计算公式 2026-03-04 11:13:16 +08:00
dmy
3498650f5f feat: 添加保护角可视化绘制 2026-03-04 11:08:20 +08:00
dmy
fb3276d49d refactor: 移除地面填充绘制逻辑 2026-03-04 10:58:19 +08:00
dmy
45b5dbaab2 fix: 修复几何坐标轴绘制范围问题 2026-03-04 10:57:07 +08:00
dmy
52a1ca7c2e fix: 增加导地线挂点垂直坐标顺序验证 2026-03-04 10:50:17 +08:00
dmy
8c1e6c2068 feat: 添加绝缘子串长计算与显示 2026-03-04 10:34:02 +08:00
dmy
b7d73e61a7 fix: 修复几何数据类型转换及折叠问题 2026-03-04 10:09:46 +08:00
dmy
6665b142e2 chore: 调整窗口宽度 2026-03-04 09:37:22 +08:00
dmy
4184a53a86 refactor: 优化参数表单布局,几何可视化并列显示 2026-03-04 09:27:58 +08:00
dmy
4b75c6a521 feat: 添加杆塔几何结构可视化组件 2026-03-04 09:13:51 +08:00
dmy
7f4a6751b4 build: 更新版本号至1.0.14并调整构建配置 2026-03-04 08:36:13 +08:00
dmy
86b294baf9 feat: 添加配置文件的导入功能及文件路径显示
新增通过系统对话框导入配置文件的功能
在界面上显示当前打开的配置文件路径
添加对50%击穿电压的验证
优化开发模式下的文件导入备用方案
2026-03-03 18:58:19 +08:00
dmy
7dd466a28a feat: 添加50%击穿电压参数支持
支持用户自定义50%击穿电压值,默认-1表示自动计算
在UI中添加相关配置开关
2026-03-03 18:26:01 +08:00
12 changed files with 799 additions and 210 deletions

View File

@@ -1,15 +1,15 @@
target: 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
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

View File

@@ -1 +1 @@
1.0.12
1.0.16

10
core.py
View File

@@ -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海拔

BIN
lightening.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -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)

21
main.py
View File

@@ -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,19 +145,13 @@ 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}")
@@ -175,7 +172,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)

View File

@@ -1,4 +1,4 @@
version: 1.0.12
version: 1.0.16
company_name: EGM
file_description: EGM Lightning Protection Calculator
product_name: Lightening

View File

@@ -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 输电线路绕击跳闸率计算 v1.0.16</title>
</head>
<body>
<div id="app"></div>

View File

@@ -0,0 +1,422 @@
<template>
<q-card class="shadow-2">
<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>

View File

@@ -2,198 +2,220 @@
<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">
<!-- 左侧基本参数 -->
<div class="col-12 col-lg-6">
<q-card class="shadow-2 full-height">
<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">
<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"
/>
</div>
</div>
<!-- 高级参数 -->
<q-card class="q-mb-md shadow-2">
@@ -249,6 +271,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>
@@ -322,7 +364,7 @@
/>
</div>
<!-- 隐藏的文件输入 -->
<!-- 隐藏的文件输入开发模式备用 -->
<input
ref="fileInput"
type="file"
@@ -383,6 +425,7 @@ import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
import type { AllParameters } from '@/types'
import LogComponent from './Log.vue'
import Animation from './Animation.vue'
import Geometry from './Geometry.vue'
// 默认参数
const defaultParams: AllParameters = {
@@ -405,7 +448,8 @@ const defaultParams: AllParameters = {
advance: {
ng: -1,
Ip_a: -1,
Ip_b: -1
Ip_b: -1,
u_50: -1
},
optional: {
voltage_n: 3,
@@ -420,8 +464,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',
@@ -457,6 +505,17 @@ watch(
}
)
// 监听50%击穿电压开关
watch(
showU50,
(show) => {
if (!show) {
// 关闭时重置为 -1使用公式计算
params.advance.u_50 = -1
}
}
)
// 雷暴日与地闪密度相互转换公式ng = 0.023 * td^3
// 标志位避免循环更新
let isUpdatingFromWatch = false
@@ -548,6 +607,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
@@ -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>
}

View File

@@ -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 {

View File

@@ -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)