feat: 添加基于pywebview的图形界面支持

新增图形界面模块webui,使用Vue 3 + Quasar + TypeScript + Tailwind CSS开发
扩展README文档说明图形界面使用方法
更新.gitignore忽略前端相关文件
添加Python版本配置文件
This commit is contained in:
dmy
2026-03-02 19:39:28 +08:00
parent a153e69eb7
commit 6ebfcf848d
20 changed files with 4450 additions and 8 deletions

7
webui/src/App.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<ParameterForm />
</template>
<script setup lang="ts">
import ParameterForm from '@/components/ParameterForm.vue'
</script>

View File

@@ -0,0 +1,432 @@
<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-input
v-model="params.parameter.rated_voltage"
type="number"
label="额定电压等级 (kV)"
hint="输电线路的额定电压"
:rules="[val => val > 0 || '必须大于0']"
/>
</div>
<div class="col-12 col-md-6">
<q-input
v-model="params.parameter.h_c_sag"
type="number"
step="0.01"
label="导线弧垂 (m)"
hint="导线的平均弧垂"
/>
</div>
<div class="col-12 col-md-6">
<q-input
v-model="params.parameter.h_g_sag"
type="number"
step="0.01"
label="地线弧垂 (m)"
hint="地线的平均弧垂"
/>
</div>
<div class="col-12 col-md-6">
<q-input
v-model="params.parameter.insulator_c_len"
type="number"
step="0.01"
label="导线串子绝缘长度 (m)"
hint="绝缘子的有效绝缘距离"
/>
</div>
<div class="col-12 col-md-6">
<q-input
v-model="params.parameter.string_c_len"
type="number"
step="0.1"
label="导线串长 (m)"
hint="导线串的总长度"
/>
</div>
<div class="col-12 col-md-6">
<q-input
v-model="params.parameter.string_g_len"
type="number"
step="0.1"
label="地线串长 (m)"
hint="地线串的总长度"
/>
</div>
<div class="col-12 col-md-6">
<q-input
v-model="params.parameter.altitude"
type="number"
label="海拔高度 (m)"
hint="线路所在地的海拔"
/>
</div>
<div class="col-12 col-md-6">
<q-input
v-model="params.parameter.td"
type="number"
label="雷暴日 (d)"
hint="年雷暴日数"
/>
</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" />
</div>
<div class="col-auto">
<q-btn flat round color="negative" icon="remove" @click="removeHArm" :disable="params.parameter.h_arm.length <= 1" />
</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" />
</div>
<div class="col-auto">
<q-btn flat round color="negative" icon="remove" @click="removeGcX" :disable="params.parameter.gc_x.length <= 1" />
</div>
</div>
</div>
<!-- 地面倾角 -->
<div class="q-mt-md">
<div class="text-subtitle2 q-mb-sm">
地面倾角 (°) - 向下为正
</div>
<div class="row q-col-gutter-sm">
<div class="col" v-for="(angle, index) in params.parameter.ground_angels" :key="index">
<q-input
v-model="params.parameter.ground_angels[index]"
type="number"
step="1"
:label="`角度 ${index + 1}`"
dense
/>
</div>
<div class="col-auto">
<q-btn flat round color="primary" icon="add" @click="addGroundAngel" />
</div>
<div class="col-auto">
<q-btn flat round color="negative" icon="remove" @click="removeGroundAngel" :disable="params.parameter.ground_angels.length <= 1" />
</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 col-md-6">
<q-input
v-model="params.advance.ng"
type="number"
step="0.01"
label="地闪密度 (次/(km²·a))"
hint="大于0时使用此值否则通过雷暴日计算"
/>
</div>
<div class="col-12 col-md-6">
<q-input
v-model="params.advance.Ip_a"
type="number"
step="0.01"
label="雷电流概率密度曲线系数 a"
hint="大于0时使用此值"
/>
</div>
<div class="col-12 col-md-6">
<q-input
v-model="params.advance.Ip_b"
type="number"
step="0.01"
label="雷电流概率密度曲线系数 b"
hint="大于0时使用此值"
/>
</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="计算时电压分成多少份"
hint="考虑电压波动影响"
/>
</div>
<div class="col-12 col-md-6">
<q-input
v-model="params.optional.max_i"
type="number"
label="最大尝试雷电流 (kA)"
hint="迭代计算的最大电流"
/>
</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="grey-7"
size="lg"
label="重置参数"
icon="refresh"
@click="resetParams"
class="px-8"
/>
<q-btn
color="positive"
size="lg"
label="导出配置"
icon="download"
@click="exportConfig"
class="px-8"
/>
</div>
<!-- 计算结果 -->
<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>
<!-- 错误信息 -->
<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>
</div>
</q-page>
</q-page-container>
</q-layout>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import type { AllParameters } from '@/types'
// 默认参数
const defaultParams: AllParameters = {
parameter: {
rated_voltage: 750,
h_c_sag: 14.43,
h_g_sag: 11.67,
insulator_c_len: 7.02,
string_c_len: 9.2,
string_g_len: 0.5,
h_arm: [150, 130],
gc_x: [17.9, 17],
ground_angels: [0],
altitude: 1000,
td: 20
},
advance: {
ng: -1,
Ip_a: -1,
Ip_b: -1
},
optional: {
voltage_n: 3,
max_i: 200
}
}
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 addHArm = () => {
const last = params.parameter.h_arm[params.parameter.h_arm.length - 1] || 100
params.parameter.h_arm.push(last - 20)
}
const removeHArm = () => {
if (params.parameter.h_arm.length > 1) {
params.parameter.h_arm.pop()
}
}
const addGcX = () => {
const last = params.parameter.gc_x[params.parameter.gc_x.length - 1] || 10
params.parameter.gc_x.push(last)
}
const removeGcX = () => {
if (params.parameter.gc_x.length > 1) {
params.parameter.gc_x.pop()
}
}
const addGroundAngel = () => {
params.parameter.ground_angels.push(0)
}
const removeGroundAngel = () => {
if (params.parameter.ground_angels.length > 1) {
params.parameter.ground_angels.pop()
}
}
// 计算函数
const calculate = async () => {
calculating.value = true
result.value = null
error.value = null
try {
// 调用 pywebview 的 Python 函数
if (window.pywebview) {
const response = await window.pywebview.api.calculate(params)
result.value = JSON.stringify(response, null, 2)
} else {
// 开发模式下的模拟
await new Promise(resolve => setTimeout(resolve, 1000))
result.value = JSON.stringify({
success: true,
message: '计算完成(开发模式)',
data: {
tripping_rate: '0.0581 次/(100km·a)',
parameters: params
}
}, null, 2)
}
} catch (e: any) {
error.value = e.message || '计算失败'
} finally {
calculating.value = false
}
}
// 重置参数
const resetParams = () => {
Object.assign(params, JSON.parse(JSON.stringify(defaultParams)))
result.value = null
error.value = null
}
// 导出配置
const exportConfig = () => {
const config = JSON.stringify(params, null, 2)
const blob = new Blob([config], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'egm_config.json'
a.click()
URL.revokeObjectURL(url)
}
// 声明 pywebview API 类型
declare global {
interface Window {
pywebview?: {
api: {
calculate: (params: AllParameters) => Promise<any>
}
}
}
}
</script>

14
webui/src/main.ts Normal file
View File

@@ -0,0 +1,14 @@
import { createApp } from 'vue'
import { Quasar } from 'quasar'
import '@quasar/extras/material-icons/material-icons.css'
import 'quasar/src/css/index.sass'
import './style.css'
import App from './App.vue'
const app = createApp(App)
app.use(Quasar, {
plugins: {}
})
app.mount('#app')

View File

@@ -0,0 +1,9 @@
// Quasar SCSS Variables
$primary : #1976D2
$secondary : #26A69A
$accent : #9C27B0
$dark : #1D1D1D
$positive : #21BA45
$negative : #C10015
$info : #31CCEC
$warning : #F2C037

19
webui/src/style.css Normal file
View File

@@ -0,0 +1,19 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
width: 100%;
height: 100vh;
overflow: auto;
}

39
webui/src/types/index.ts Normal file
View File

@@ -0,0 +1,39 @@
// EGM 计算参数类型定义
export interface Parameter {
// 基本参数
rated_voltage: number // 额定电压等级 (kV)
h_c_sag: number // 导线弧垂 (m)
h_g_sag: number // 地线弧垂 (m)
insulator_c_len: number // 导线串子绝缘长度 (m)
string_c_len: number // 导线串长 (m)
string_g_len: number // 地线串长 (m)
h_arm: number[] // 导、地线挂点垂直距离 (m)
gc_x: number[] // 导、地线水平坐标 (m)
ground_angels: number[] // 地面倾角 (°)
altitude: number // 海拔高度 (m)
td: number // 雷暴日 (d)
}
export interface AdvanceParameter {
ng: number // 地闪密度 (次/(km²·a))
Ip_a: number // 雷电流概率密度曲线系数a
Ip_b: number // 雷电流概率密度曲线系数b
}
export interface OptionalParameter {
voltage_n: number // 计算时电压分成多少份
max_i: number // 最大尝试雷电流 (kA)
}
export interface AllParameters {
parameter: Parameter
advance: AdvanceParameter
optional: OptionalParameter
}
export interface CalculationResult {
success: boolean
message?: string
data?: any
}