feat: 添加运行日志组件并集成到参数表单

在参数表单中添加运行日志组件,用于显示计算过程中的日志信息
后端增加日志处理功能,将计算日志返回给前端显示
优化tsconfig配置,添加路径别名支持
This commit is contained in:
dmy
2026-03-02 21:02:58 +08:00
parent 6ebfcf848d
commit 89e4cd4973
4 changed files with 235 additions and 14 deletions

View File

@@ -0,0 +1,106 @@
<template>
<q-card class="q-mt-md shadow-2">
<q-card-section class="bg-blue-grey-1">
<div class="text-h6 text-blue-grey-9 flex items-center gap-2">
<q-icon name="terminal" />
运行日志
<q-space />
<q-btn flat dense round icon="delete" @click="clearLog" size="sm" />
</div>
</q-card-section>
<q-card-section class="q-pa-none">
<div ref="logContainer" class="log-container">
<div
v-for="(log, index) in logs"
:key="index"
:class="['log-line', `log-${log.level}`]"
>
<span class="log-time">{{ log.time }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</q-card-section>
</q-card>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue'
defineOptions({
name: 'LogComponent'
})
interface LogEntry {
level: 'info' | 'warning' | 'error' | 'debug'
time: string
message: string
}
const logs = ref<LogEntry[]>([])
const logContainer = ref<HTMLElement | null>(null)
const addLog = (level: LogEntry['level'], message: string) => {
const now = new Date()
const time = now.toLocaleTimeString('zh-CN', { hour12: false })
logs.value.push({ level, time, message })
// 自动滚动到底部
nextTick(() => {
if (logContainer.value) {
logContainer.value.scrollTop = logContainer.value.scrollHeight
}
})
}
const clearLog = () => {
logs.value = []
}
// 暴露方法给父组件
defineExpose({
addLog,
clearLog
})
</script>
<style scoped>
.log-container {
max-height: 300px;
overflow-y: auto;
background-color: #1e1e1e;
color: #d4d4d4;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
padding: 8px;
user-select: text;
cursor: text;
}
.log-line {
padding: 2px 0;
display: flex;
gap: 8px;
}
.log-time {
color: #6a9955;
flex-shrink: 0;
}
.log-info .log-message {
color: #d4d4d4;
}
.log-warning .log-message {
color: #dcdcaa;
}
.log-error .log-message {
color: #f48771;
}
.log-debug .log-message {
color: #9cdcfe;
}
</style>

View File

@@ -296,6 +296,9 @@
<p class="text-negative">{{ error }}</p> <p class="text-negative">{{ error }}</p>
</q-card-section> </q-card-section>
</q-card> </q-card>
<!-- 运行日志 -->
<LogComponent ref="logRef" />
</div> </div>
</q-page> </q-page>
</q-page-container> </q-page-container>
@@ -305,6 +308,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import type { AllParameters } from '@/types' import type { AllParameters } from '@/types'
import LogComponent from './Log.vue'
// 默认参数 // 默认参数
const defaultParams: AllParameters = { const defaultParams: AllParameters = {
@@ -336,6 +340,7 @@ const params = reactive<AllParameters>(JSON.parse(JSON.stringify(defaultParams))
const calculating = ref(false) const calculating = ref(false)
const result = ref<string | null>(null) const result = ref<string | null>(null)
const error = ref<string | null>(null) const error = ref<string | null>(null)
const logRef = ref<InstanceType<typeof LogComponent> | null>(null)
// 数组操作函数 // 数组操作函数
const addHArm = () => { const addHArm = () => {
@@ -379,11 +384,32 @@ const calculate = async () => {
try { try {
// 调用 pywebview 的 Python 函数 // 调用 pywebview 的 Python 函数
if (window.pywebview) { if (window.pywebview) {
logRef.value?.addLog('info', '开始调用后端计算...')
const response = await window.pywebview.api.calculate(params) const response = await window.pywebview.api.calculate(params)
logRef.value?.addLog('info', `后端返回 keys: ${Object.keys(response).join(', ')}`)
logRef.value?.addLog('info', `DEBUG_VERSION: ${response.DEBUG_VERSION || '无'}`)
logRef.value?.addLog('info', `后端返回: ${JSON.stringify(response).substring(0, 150)}`)
// 显示日志
console.log('[DEBUG] response:', response)
const logs = response.logs
if (Array.isArray(logs) && logs.length > 0) {
logRef.value?.addLog('info', `收到 ${logs.length} 条日志`)
for (const log of logs) {
logRef.value?.addLog(log.level as any, log.message)
}
} else {
logRef.value?.addLog('warning', '后端未返回日志')
logRef.value?.addLog('warning', `response.logs = ${JSON.stringify(logs)}`)
}
result.value = JSON.stringify(response, null, 2) result.value = JSON.stringify(response, null, 2)
} else { } else {
// 开发模式下的模拟 // 开发模式下的模拟
await new Promise(resolve => setTimeout(resolve, 1000)) 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({ result.value = JSON.stringify({
success: true, success: true,
message: '计算完成(开发模式)', message: '计算完成(开发模式)',
@@ -395,6 +421,7 @@ const calculate = async () => {
} }
} catch (e: any) { } catch (e: any) {
error.value = e.message || '计算失败' error.value = e.message || '计算失败'
logRef.value?.addLog('error', e.message || '计算失败')
} finally { } finally {
calculating.value = false calculating.value = false
} }

View File

@@ -1 +1,25 @@
{"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"}]} {
"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,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -8,7 +8,8 @@ import sys
import json import json
import math import math
from pathlib import Path from pathlib import Path
from typing import Dict, Any from typing import Dict, Any, List
from datetime import datetime
import webview import webview
from loguru import logger from loguru import logger
@@ -26,11 +27,46 @@ from core import (
import numpy as np import numpy as np
class WebHandler:
"""Web日志处理器"""
def __init__(self, callback=None):
self.callback = callback
self.logs: List[Dict[str, str]] = []
def write(self, message):
if message.strip():
log_entry = {
"level": "info",
"time": datetime.now().strftime("%H:%M:%S"),
"message": message.strip()
}
self.logs.append(log_entry)
if self.callback:
self.callback(log_entry)
class EGMWebApp: class EGMWebApp:
"""EGM 计算程序的 Web 界面后端""" """EGM 计算程序的 Web 界面后端"""
def __init__(self): def __init__(self):
self.window = None self.window = None
self.web_handler = None
self.logs: List[Dict[str, str]] = []
def add_log(self, level: str, message: str):
"""添加日志"""
log_entry = {
"level": level,
"time": datetime.now().strftime("%H:%M:%S"),
"message": message
}
self.logs.append(log_entry)
def get_logs(self) -> List[Dict[str, str]]:
"""获取日志列表"""
logs = self.logs.copy()
self.logs = []
return logs
def calculate(self, params: Dict[str, Any]) -> Dict[str, Any]: def calculate(self, params: Dict[str, Any]) -> Dict[str, Any]:
""" """
@@ -42,9 +78,10 @@ class EGMWebApp:
Returns: Returns:
计算结果字典 计算结果字典
""" """
try: self.logs = [] # 清空日志
logger.info("开始 EGM 计算...") self.add_log("info", "开始 EGM 计算...")
try:
# 解析参数 # 解析参数
parameter_data = params.get('parameter', {}) parameter_data = params.get('parameter', {})
advance_data = params.get('advance', {}) advance_data = params.get('advance', {})
@@ -73,27 +110,54 @@ class EGMWebApp:
para.voltage_n = int(optional_data.get('voltage_n', 3)) para.voltage_n = int(optional_data.get('voltage_n', 3))
para.max_i = float(optional_data.get('max_i', 200)) para.max_i = float(optional_data.get('max_i', 200))
logger.info(f"参数: 额定电压={para.rated_voltage}kV, 雷暴日={para.td}d, 海拔={para.altitude}m") self.add_log("info", f"参数: 额定电压={para.rated_voltage}kV, 雷暴日={para.td}d, 海拔={para.altitude}m")
logger.info("开始执行 _do_calculate...")
# 执行实际计算 # 执行实际计算
self.add_log("info", "EGM 计算中...")
result = self._do_calculate() result = self._do_calculate()
logger.info("EGM 计算完成") # 调试输出
return result logger.info(f"_do_calculate 返回 keys: {list(result.keys())}")
logger.info(f"日志列表: {self.logs}")
# 将日志添加到结果中(在返回之前添加最后一条日志)
self.add_log("info", "EGM 计算完成")
# 创建一个新的返回值,确保 logs 字段被包含
final_result = {
"success": result.get("success", True),
"message": result.get("message", "计算完成"),
"data": result.get("data", {}),
"logs": self.logs,
"DEBUG_VERSION": "v2" # 标记版本
}
logger.info(f"最终返回结果 keys: {list(final_result.keys())}")
logger.info(f"日志数量: {len(self.logs)}")
logger.info(f"DEBUG: self.logs = {self.logs}")
return final_result
except Exception as e: except Exception as e:
logger.error(f"计算失败: {str(e)}") self.add_log("error", f"计算失败: {str(e)}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return { return {
"success": False, "success": False,
"message": f"计算失败: {str(e)}", "message": f"计算失败: {str(e)}",
"error": str(e) "error": str(e),
"logs": self.logs
} }
def _do_calculate(self) -> Dict[str, Any]: def _do_calculate(self) -> Dict[str, Any]:
"""执行实际的EGM计算""" """执行实际的EGM计算"""
try:
h_whole = para.h_arm[0] h_whole = para.h_arm[0]
except Exception as e:
self.add_log("error", f"获取参数失败: {str(e)}")
raise
string_g_len = para.string_g_len string_g_len = para.string_g_len
string_c_len = para.string_c_len string_c_len = para.string_c_len
h_g_sag = para.h_g_sag h_g_sag = para.h_g_sag
@@ -123,7 +187,7 @@ class EGMWebApp:
results = [] results = []
for ground_angel in ground_angels: for ground_angel in ground_angels:
logger.info(f"地面倾角 {ground_angel / math.pi * 180:.3f}°") self.add_log("info", f"地面倾角 {ground_angel / math.pi * 180:.3f}°")
rg_type = None rg_type = None
rg_x = None rg_x = None
rg_y = None rg_y = None
@@ -205,11 +269,11 @@ class EGMWebApp:
if min_distance_intersection < 0.1: if min_distance_intersection < 0.1:
break break
logger.info(f"最大电流为 {i_max:.2f}, 最小电流为 {i_min:.2f}") self.add_log("info", f"最大电流为 {i_max:.2f}, 最小电流为 {i_min:.2f}")
curt_fineness = 0.1 curt_fineness = 0.1
if i_min > i_max or abs(i_min - i_max) < curt_fineness: if i_min > i_max or abs(i_min - i_max) < curt_fineness:
logger.info("最大电流小于等于最小电流,没有暴露弧。") self.add_log("info", "最大电流小于等于最小电流,没有暴露弧。")
continue continue
curt_segment_n = int((i_max - i_min) / curt_fineness) curt_segment_n = int((i_max - i_min) / curt_fineness)
@@ -249,7 +313,7 @@ class EGMWebApp:
avr_n_sf += n_sf / voltage_n avr_n_sf += n_sf / voltage_n
n_sf_phases[phase_conductor_foo][u_bar] = n_sf n_sf_phases[phase_conductor_foo][u_bar] = n_sf
logger.info(f"{phase_conductor_foo + 1}, 跳闸率: {n_sf:.16f} 次/(100km·a)") self.add_log("info", f"{phase_conductor_foo + 1}, 跳闸率: {n_sf:.16f} 次/(100km·a)")
result = { result = {
"ground_angle": f"{ground_angel / math.pi * 180:.3f}°", "ground_angle": f"{ground_angel / math.pi * 180:.3f}°",