Files
egm/webui/src/components/ParameterForm.vue
dmy 9557e18fd1 fix: 修正雷电流密度计算条件并修复单位显示错误
修正雷暴日判断条件从等于改为小于等于,并添加中间范围判断
修复日志中电流单位显示错误(kV改为kA)
初始化时根据雷暴日自动计算地闪密度
2026-03-03 10:39:53 +08:00

699 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<q-layout view="lHh lpr lFf">
<q-header elevated class="bg-indigo-600 text-white">
<q-toolbar>
<q-toolbar-title>
<div class="flex items-center gap-2">
<q-icon name="flash_on" size="md" />
<span class="text-xl font-bold">EGM 输电线路绕击跳闸率计算</span>
</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>
<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="电流类型"
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)"
:disable="isTdDisabled"
>
<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="(h, 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="(x, 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>
<!-- 高级参数 -->
<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="row q-col-gutter-md q-mt-sm">
<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>
</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.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"
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"
/>
</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>
<!-- 运行日志 -->
<LogComponent ref="logRef" />
<!-- 计算结果 -->
<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>
<pre class="text-caption bg-white q-pa-md rounded">{{ result }}</pre>
</q-card-section>
</q-card>
</div>
</q-page>
</q-page-container>
</q-layout>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
import type { AllParameters } from '@/types'
import LogComponent from './Log.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
},
advance: {
ng: -1,
Ip_a: -1,
Ip_b: -1
},
optional: {
voltage_n: 3,
max_i: 200
}
}
const params = reactive<AllParameters>(JSON.parse(JSON.stringify(defaultParams)))
const calculating = ref(false)
const result = ref<string | null>(null)
const error = ref<string | null>(null)
const logRef = ref<InstanceType<typeof LogComponent> | null>(null)
const fileInput = ref<HTMLInputElement | null>(null)
const voltageOptions = [
'110kV', '220kV', '330kV', '500kV', '750kV','1000kV',
'±500kV', '±660kV', '±800kV', '±1100kV'
]
// 根据电压等级自动判断交流/直流
const currentType = computed(() => {
return params.parameter.rated_voltage.includes('±') ? 'DC' : 'AC'
})
// 雷暴日与地闪密度相互转换公式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或41地线+导线)
// 两个数组同步增减
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 () => {
calculating.value = true
result.value = null
error.value = null
try {
// 调用 pywebview 的 Python 函数
if (window.pywebview) {
// 后台线程启动计算,实时日志通过 addLogFromBackend 推送
// 结果通过 receiveResult 回调接收
await window.pywebview.api.calculate(params)
// 不在这里设置 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 = JSON.stringify({
success: true,
message: '计算完成(开发模式)',
data: {
tripping_rate: '0.0581 次/(100km·a)',
parameters: params
}
}, null, 2)
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 exportConfig = async () => {
try {
if (window.pywebview) {
const response = await window.pywebview.api.export_config(params)
if (response.success) {
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 = () => {
fileInput.value?.click()
}
// 处理文件选择
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)
}
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>
export_config: (params: AllParameters) => Promise<any>
}
}
addLogFromBackend?: (log: { level: string; time: string; message: string }) => void
receiveResult?: (result: { success: boolean; message: string; data?: any; error?: string }) => void
}
}
// 注册全局日志接收函数,供后端实时调用
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
}
// 实时日志推送
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) {
result.value = JSON.stringify(res, null, 2)
} else {
error.value = res.error || res.message
}
}
})
onUnmounted(() => {
window.addLogFromBackend = undefined
window.receiveResult = undefined
})
</script>
<style scoped>
</style>