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

6
.gitignore vendored
View File

@@ -10,4 +10,8 @@ dist
.venv
*.toml
launch.json
settings.json
settings.json
node_modules
*.log
*.lock
*.pdf

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.12

View File

@@ -11,6 +11,7 @@
- 支持交流/直流线路计算
- 输出CAD图形DXF格式可视化击距模型
- 提供动画演示模式(可选项)
- **提供图形化界面pywebview**,支持可视化参数配置和计算
## 安装
@@ -35,6 +36,7 @@ pip install -r requirements.txt
- matplotlib - 数据可视化和动画
- numpy - 数值计算
- tomli - TOML配置文件解析
- pywebview - 图形界面框架
## 使用方法
@@ -68,6 +70,47 @@ make
生成的可执行文件位于 `dist/Lightening.exe`
### 图形界面使用
程序提供基于 pywebview 的图形界面,使用 Vue 3 + Quasar + TypeScript + Tailwind CSS 开发。
#### 安装前端依赖
```bash
cd webui
npm install
```
#### 开发模式运行
```bash
# 终端1启动前端开发服务器
cd webui
npm run dev
# 终端2启动 pywebview 窗口
python webview_app.py
```
#### 生产模式运行
```bash
# 构建前端
cd webui
npm run build
# 启动 pywebview 窗口
python webview_app.py
```
图形界面支持:
- 可视化参数配置(基本参数、高级参数、可选参数)
- 动态添加/删除数组参数
- 参数导出功能
- 计算结果展示
详细说明请参考 `webui/README.md`
## 配置文件格式
配置文件使用 TOML 格式,包含三个主要部分:
@@ -144,10 +187,26 @@ EGM/
├── main-batch.py # 批量计算程序
├── core.py # 核心计算模块
├── animation.py # 动画演示模块
├── webview_app.py # pywebview 图形界面后端
├── article.toml # 示例配置文件
├── Makefile # 构建脚本
├── pyproject.toml # 项目配置
├── README.md # 说明文档
├── webui/ # 图形界面前端项目
│ ├── src/
│ │ ├── components/
│ │ │ └── ParameterForm.vue
│ │ ├── types/
│ │ │ └── index.ts
│ │ ├── App.vue
│ │ ├── main.ts
│ │ └── style.css
│ ├── package.json
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── tailwind.config.js
│ ├── postcss.config.js
│ └── index.html
└── CSharp/ # C# 版本实现
```

View File

@@ -1,7 +0,0 @@
Version: 1.2.0.2
#CompanyName: My Imaginary Company
#FileDescription: Simple App
#InternalName: Simple App
#LegalCopyright: © My Imaginary Company. All rights reserved.
#OriginalFilename: SimpleApp.exe
ProductName: Lightening

149
webui/README.md Normal file
View File

@@ -0,0 +1,149 @@
# EGM Web 界面
基于 pywebview 的输电线路绕击跳闸率计算程序图形界面。
## 技术栈
- **后端**: Python + pywebview
- **前端**: Vue 3 + Quasar + TypeScript + Tailwind CSS
- **构建工具**: Vite
## 安装步骤
### 1. 安装 Python 依赖
```bash
# 在项目根目录
uv sync
# 或
pip install pywebview
```
### 2. 安装前端依赖
```bash
cd webui
npm install
```
## 运行方式
### 开发模式
1. 启动前端开发服务器:
```bash
cd webui
npm run dev
```
2. 在另一个终端启动 pywebview
```bash
cd ..
python webview_app.py
```
### 生产模式
1. 构建前端:
```bash
cd webui
npm run build
```
2. 启动 pywebview
```bash
cd ..
python webview_app.py
```
## 项目结构
```
webui/
├── src/
│ ├── components/
│ │ └── ParameterForm.vue # 参数表单组件
│ ├── types/
│ │ └── index.ts # TypeScript 类型定义
│ ├── App.vue # 主应用组件
│ ├── main.ts # 应用入口
│ └── style.css # 全局样式
├── package.json # 前端依赖配置
├── vite.config.ts # Vite 配置
├── tsconfig.json # TypeScript 配置
├── tailwind.config.js # Tailwind CSS 配置
└── index.html # HTML 入口
```
## 功能说明
### 参数配置
界面提供三个参数分组:
1. **基本参数**
- 额定电压等级 (kV)
- 导线弧垂 (m)
- 地线弧垂 (m)
- 导线串子绝缘长度 (m)
- 导线串长 (m)
- 地线串长 (m)
- 导、地线挂点垂直距离 (m)
- 导、地线水平坐标 (m)
- 地面倾角 (°)
- 海拔高度 (m)
- 雷暴日 (d)
2. **高级参数**
- 地闪密度 (次/(km²·a))
- 雷电流概率密度曲线系数 a
- 雷电流概率密度曲线系数 b
3. **可选参数**
- 计算时电压分成多少份
- 最大尝试雷电流 (kA)
### 操作功能
- **开始计算**: 执行 EGM 计算
- **重置参数**: 恢复默认参数值
- **导出配置**: 将当前配置导出为 JSON 文件
## 开发说明
### 添加新功能
1.`src/types/index.ts` 中添加类型定义
2.`src/components/ParameterForm.vue` 中添加 UI 组件
3.`webview_app.py` 中添加后端 API 方法
### 样式定制
- 使用 Tailwind CSS 类名进行样式定制
- Quasar 组件自带 Material Design 风格
## 常见问题
### Q: pywebview 窗口无法打开?
A: 请确保已安装 pywebview`pip install pywebview`
### Q: 前端开发服务器无法连接?
A: 请确保先运行 `npm run dev`,然后再启动 `python webview_app.py`
### Q: 计算功能不工作?
A: 当前版本仅提供界面框架,需要进一步集成 core.py 中的计算逻辑。
## 后续开发
- [ ] 集成 core.py 中的实际计算函数
- [ ] 添加计算结果可视化
- [ ] 支持导入/导出 TOML 配置文件
- [ ] 添加批量计算功能
- [ ] 添加结果导出DXF、PDF 等)

13
webui/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EGM 输电线路绕击跳闸率计算</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

3290
webui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

1
webui/package.json Normal file
View File

@@ -0,0 +1 @@
{"name":"egm-webui","version":"0.1.0","type":"module","scripts":{"dev":"vite","build":"vue-tsc && vite build","preview":"vite preview"},"dependencies":{"@quasar/extras":"^1.17.0","@quasar/vite-plugin":"^1.10.0","quasar":"^2.14.0","vue":"^3.4.0"},"devDependencies":{"@vitejs/plugin-vue":"^5.0.0","autoprefixer":"^10.4.0","postcss":"^8.4.0","sass-embedded":"^1.97.3","tailwindcss":"^3.4.0","typescript":"^5.3.0","vite":"^5.0.0","vue-tsc":"^1.8.0"}}

6
webui/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

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
}

11
webui/tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

1
webui/tsconfig.json Normal file
View File

@@ -0,0 +1 @@
{"compilerOptions": {"target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "preserve", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true}, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{"path": "./tsconfig.node.json"}]}

1
webui/tsconfig.node.json Normal file
View File

@@ -0,0 +1 @@
{"compilerOptions": {"composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true}, "include": ["vite.config.ts"]}

22
webui/vite.config.ts Normal file
View File

@@ -0,0 +1,22 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [
vue({
template: { transformAssetUrls }
}),
quasar()
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
port: 5173,
host: true
}
})

371
webview_app.py Normal file
View File

@@ -0,0 +1,371 @@
"""
EGM 输电线路绕击跳闸率计算程序 - Pywebview 界面
使用 Vue 3 + Quasar + TypeScript + Tailwind CSS 作为前端
"""
import os
import sys
import json
import math
from pathlib import Path
from typing import Dict, Any
import webview
from loguru import logger
# 添加项目根目录到路径
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
from core import (
Parameter, para,
func_ng, min_i, rs_fun, rc_fun, rg_fun,
bd_area, thunder_density, arc_possibility,
rg_line_function_factory, solve_circle_intersection,
solve_circle_line_intersection, Draw
)
import numpy as np
class EGMWebApp:
"""EGM 计算程序的 Web 界面后端"""
def __init__(self):
self.window = None
def calculate(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""
执行 EGM 计算
Args:
params: 包含 parameter, advance, optional 的字典
Returns:
计算结果字典
"""
try:
logger.info("开始 EGM 计算...")
# 解析参数
parameter_data = params.get('parameter', {})
advance_data = params.get('advance', {})
optional_data = params.get('optional', {})
# 更新全局参数对象
para.h_g_sag = float(parameter_data.get('h_g_sag', 11.67))
para.h_c_sag = float(parameter_data.get('h_c_sag', 14.43))
para.td = int(parameter_data.get('td', 20))
para.insulator_c_len = float(parameter_data.get('insulator_c_len', 7.02))
para.string_c_len = float(parameter_data.get('string_c_len', 9.2))
para.string_g_len = float(parameter_data.get('string_g_len', 0.5))
para.gc_x = list(parameter_data.get('gc_x', [17.9, 17]))
para.ground_angels = [
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.altitude = int(parameter_data.get('altitude', 1000))
para.rated_voltage = float(parameter_data.get('rated_voltage', 750))
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.voltage_n = int(optional_data.get('voltage_n', 3))
para.max_i = float(optional_data.get('max_i', 200))
logger.info(f"参数: 额定电压={para.rated_voltage}kV, 雷暴日={para.td}d, 海拔={para.altitude}m")
# 执行实际计算
result = self._do_calculate()
logger.info("EGM 计算完成")
return result
except Exception as e:
logger.error(f"计算失败: {str(e)}")
import traceback
traceback.print_exc()
return {
"success": False,
"message": f"计算失败: {str(e)}",
"error": str(e)
}
def _do_calculate(self) -> Dict[str, Any]:
"""执行实际的EGM计算"""
h_whole = para.h_arm[0]
string_g_len = para.string_g_len
string_c_len = para.string_c_len
h_g_sag = para.h_g_sag
h_c_sag = para.h_c_sag
gc_x = para.gc_x.copy()
h_arm = para.h_arm
gc_y = [
h_whole - string_g_len - h_g_sag * 2 / 3,
]
if len(h_arm) > 1:
for hoo in h_arm[1:]:
gc_y.append(hoo - string_c_len - h_c_sag * 2 / 3)
if len(gc_y) > 2:
phase_n = 3
else:
phase_n = 1
td = para.td
ng = func_ng(td)
avr_n_sf = 0
ground_angels = para.ground_angels
voltage_n = para.voltage_n
n_sf_phases = np.zeros((phase_n, voltage_n))
results = []
for ground_angel in ground_angels:
logger.info(f"地面倾角 {ground_angel / math.pi * 180:.3f}°")
rg_type = None
rg_x = None
rg_y = None
for phase_conductor_foo in range(phase_n):
rs_x = gc_x[phase_conductor_foo]
rs_y = gc_y[phase_conductor_foo]
rc_x = gc_x[phase_conductor_foo + 1]
rc_y = gc_y[phase_conductor_foo + 1]
if phase_n == 1:
rg_type = "g"
if phase_n > 1:
if phase_conductor_foo < 2:
rg_type = "c"
rg_x = gc_x[phase_conductor_foo + 2]
rg_y = gc_y[phase_conductor_foo + 2]
else:
rg_type = "g"
rated_voltage = para.rated_voltage
for u_bar in range(voltage_n):
u_ph = rated_voltage / 1.732
insulator_c_len = para.insulator_c_len
i_min = min_i(insulator_c_len, u_ph)
_min_i = i_min
_max_i = para.max_i
i_max = _min_i
for i_bar in np.linspace(_min_i, _max_i, int((_max_i - _min_i) / 1)):
rs = rs_fun(i_bar)
rc = rc_fun(i_bar, u_ph)
rg = rg_fun(i_bar, rc_y, u_ph, typ=rg_type)
rg_line_func = None
if rg_type == "g":
rg_line_func = rg_line_function_factory(rg, ground_angel)
rs_rc_circle_intersection = solve_circle_intersection(
rs, rc, rs_x, rs_y, rc_x, rc_y
)
i_max = i_bar
if not rs_rc_circle_intersection:
continue
circle_rc_or_rg_line_intersection = None
if rg_type == "g":
circle_rc_or_rg_line_intersection = solve_circle_line_intersection(
rc, rc_x, rc_y, rg_line_func
)
elif rg_type == "c":
circle_rc_or_rg_line_intersection = solve_circle_intersection(
rg, rc, rg_x, rg_y, rc_x, rc_y
)
if not circle_rc_or_rg_line_intersection:
if rg_type == "g":
if rg_line_func(rc_x) > rc_y:
i_min = i_bar
continue
else:
continue
min_distance_intersection = (
np.sum(
(
np.array(rs_rc_circle_intersection)
- np.array(circle_rc_or_rg_line_intersection)
)
** 2
)
** 0.5
)
if min_distance_intersection < 0.1:
break
logger.info(f"最大电流为 {i_max:.2f}, 最小电流为 {i_min:.2f}")
curt_fineness = 0.1
if i_min > i_max or abs(i_min - i_max) < curt_fineness:
logger.info("最大电流小于等于最小电流,没有暴露弧。")
continue
curt_segment_n = int((i_max - i_min) / curt_fineness)
i_curt_samples, d_curt = np.linspace(
i_min, i_max, curt_segment_n + 1, retstep=True
)
bd_area_vec = np.vectorize(bd_area)
ip_a = para.Ip_a
ip_b = para.Ip_b
bd_area_vec_result = bd_area_vec(
i_curt_samples,
u_ph,
rc_x,
rc_y,
rs_x,
rs_y,
rg_x,
rg_y,
ground_angel,
rg_type,
)
thunder_density_result = thunder_density(
i_curt_samples, td, ip_a, ip_b
)
cal_bd_np = bd_area_vec_result * thunder_density_result
calculus = np.sum(cal_bd_np[:-1] + cal_bd_np[1:]) / 2 * d_curt
n_sf = (
2
* ng
/ 10
* calculus
* arc_possibility(rated_voltage, insulator_c_len)
)
avr_n_sf += n_sf / voltage_n
n_sf_phases[phase_conductor_foo][u_bar] = n_sf
logger.info(f"{phase_conductor_foo + 1}, 跳闸率: {n_sf:.16f} 次/(100km·a)")
result = {
"ground_angle": f"{ground_angel / math.pi * 180:.3f}°",
"tripping_rate": avr_n_sf,
"phases": np.mean(n_sf_phases, axis=1).tolist()
}
results.append(result)
return {
"success": True,
"message": "计算完成",
"data": {
"tripping_rate": f"{avr_n_sf:.16f} 次/(100km·a)",
"results": results,
"parameters": {
"rated_voltage": para.rated_voltage,
"td": para.td,
"altitude": para.altitude,
"ground_angels": [a / math.pi * 180 for a in para.ground_angels],
"max_i": para.max_i
}
}
}
def export_config(self, params: Dict[str, Any]) -> str:
"""
导出配置为 JSON 字符串
Args:
params: 参数字典
Returns:
JSON 字符串
"""
return json.dumps(params, indent=2, ensure_ascii=False)
def get_default_config(self) -> Dict[str, Any]:
"""
获取默认配置
Returns:
默认配置字典
"""
return {
"parameter": {
"rated_voltage": 750,
"h_c_sag": 14.43,
"h_g_sag": 11.67,
"insulator_c_len": 7.02,
"string_c_len": 9.2,
"string_g_len": 0.5,
"h_arm": [150, 130],
"gc_x": [17.9, 17],
"ground_angels": [0],
"altitude": 1000,
"td": 20
},
"advance": {
"ng": -1,
"Ip_a": -1,
"Ip_b": -1
},
"optional": {
"voltage_n": 3,
"max_i": 200
}
}
def start_webview():
"""启动 pywebview 界面"""
# 确定前端 URL
# 在开发环境中使用 Vite 开发服务器
# 在生产环境中使用构建后的文件
dev_mode = os.getenv('EGM_DEV_MODE', 'true').lower() == 'true'
if dev_mode:
# 开发模式:使用 Vite 开发服务器
url = 'http://localhost:5173'
logger.info(f"开发模式:使用 Vite 开发服务器 {url}")
logger.info("请先在 webui 目录中运行: npm install && npm run dev")
else:
# 生产模式:使用构建后的文件
dist_path = project_root / 'webui' / 'dist'
if not dist_path.exists():
logger.error(f"构建目录不存在: {dist_path}")
logger.error("请先运行: cd webui && npm run build")
sys.exit(1)
url = f'file://{dist_path / "index.html"}'
logger.info(f"生产模式:使用构建文件 {url}")
# 创建 API 实例
api = EGMWebApp()
# 创建窗口
window = webview.create_window(
title='EGM 输电线路绕击跳闸率计算',
url=url,
js_api=api,
width=1200,
height=900,
resizable=True,
min_size=(800, 600)
)
# 启动
logger.info("启动 EGM Web 界面...")
webview.start(debug=dev_mode)
if __name__ == '__main__':
# 配置日志
logger.remove()
logger.add(sys.stderr, level="INFO")
logger.add("egm_webui.log", rotation="10 MB", retention="7 days")
# 启动界面
start_webview()