feat: 新增Web GUI界面,支持交互式设计对比

This commit is contained in:
dmy
2026-01-04 11:53:15 +08:00
parent d563905f28
commit 6f2f851a6e
2 changed files with 300 additions and 32 deletions

238
gui.py Normal file
View File

@@ -0,0 +1,238 @@
import os
import sys
import io
import contextlib
import matplotlib.pyplot as plt
from nicegui import ui, events
from main import compare_design_methods, export_to_dxf, load_data_from_excel, generate_wind_farm_data, visualize_design
import pandas as pd
# 设置matplotlib支持中文显示
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'Arial']
plt.rcParams['axes.unicode_minus'] = False
class Logger(io.StringIO):
def __init__(self, log_element):
super().__init__()
self.log_element = log_element
def write(self, message):
if message.strip():
self.log_element.push(message.strip())
super().write(message)
# 状态变量
state = {
'excel_path': None,
'results': [],
'substation': None,
'turbines': None,
'temp_dir': '.gemini/tmp/gui_uploads'
}
# 确保临时目录存在
if not os.path.exists(state['temp_dir']):
os.makedirs(state['temp_dir'], exist_ok=True)
@ui.page('/')
def index():
ui.query('body').style('background-color: #f0f2f5; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;')
# 定义 UI 元素引用容器,方便在函数中更新
refs = {
'log_box': None,
'results_table': None,
'plot_container': None,
'export_row': None,
'status_label': None,
'upload_widget': None,
'clusters_input': None,
'run_btn': None
}
async def handle_upload(e: events.UploadEventArguments):
try:
filename = None
content = None
if hasattr(e, 'name'):
filename = e.name
if hasattr(e, 'content'):
content = e.content
if content is None and hasattr(e, 'file'):
file_obj = e.file
if not filename:
filename = getattr(file_obj, 'name', getattr(file_obj, 'filename', None))
if hasattr(file_obj, 'file') and hasattr(file_obj.file, 'read'):
content = file_obj.file
elif hasattr(file_obj, 'read'):
content = file_obj
if not filename:
filename = 'uploaded_file.xlsx'
if content is None:
ui.notify('上传失败: 无法解析文件内容', type='negative')
return
path = os.path.join(state['temp_dir'], filename)
if hasattr(content, 'seek'):
try: content.seek(0)
except Exception: pass
data = content.read()
import inspect
if inspect.iscoroutine(data):
data = await data
with open(path, 'wb') as f:
f.write(data)
state['excel_path'] = path
ui.notify(f'文件已上传: {filename}', type='positive')
state['turbines'], state['substation'], _ = load_data_from_excel(path)
except Exception as ex:
ui.notify(f'上传处理失败: {ex}', type='negative')
def update_export_buttons():
if refs['export_row']:
refs['export_row'].clear()
if not state['results'] or not refs['export_row']:
return
with refs['export_row']:
ui.button('下载 Excel 对比表', on_click=lambda: ui.download('wind_farm_design.xlsx')).props('icon=download')
best_idx = 0
for i, res in enumerate(state['results']):
if res['cost'] < state['results'][best_idx]['cost']:
best_idx = i
best_res = state['results'][best_idx]
def export_best_dxf():
dxf_name = 'best_design.dxf'
if state['substation'] is not None:
export_to_dxf(best_res['turbines'], state['substation'], best_res['eval']['details'], dxf_name)
ui.download(dxf_name)
ui.notify(f'已导出推荐方案: {best_res["name"]}', type='positive')
else:
ui.notify('缺少升压站数据,无法导出 DXF', type='negative')
ui.button(f'导出推荐方案 DXF ({best_res["name"]})', on_click=export_best_dxf).props('icon=architecture color=accent')
def update_plot(result):
if refs['plot_container']:
refs['plot_container'].clear()
with refs['plot_container']:
with ui.pyplot(figsize=(10, 8)) as plot:
title = f"{result['name']}\nCost: ¥{result['cost']/10000:.2f}万 | Loss: {result['loss']:.2f} kW"
# 获取当前 ui.pyplot 创建的 axes
ax = plt.gca()
visualize_design(result['turbines'], state['substation'], result['eval']['details'], title, ax=ax)
def handle_row_click(e):
if not e.args or 'data' not in e.args: return
row_name = e.args['data']['name']
selected_res = next((r for r in state['results'] if r['name'] == row_name), None)
if selected_res:
update_plot(selected_res)
ui.notify(f'已切换至方案: {row_name}')
from nicegui import run
import queue
async def run_analysis(n_clusters):
if not state['excel_path']:
ui.notify('请先上传 Excel 坐标文件!', type='warning')
if refs['log_box']:
refs['log_box'].clear()
log_queue = queue.Queue()
class QueueLogger(io.StringIO):
def write(self, message):
if message and message.strip():
log_queue.put(message.strip())
super().write(message)
def process_log_queue():
if refs['log_box']:
while not log_queue.empty():
try:
msg = log_queue.get_nowait()
refs['log_box'].push(msg)
if msg.startswith('--- Scenario'):
scenario_name = msg.replace('---', '').strip()
if refs['status_label']:
refs['status_label'].text = f"正在计算: {scenario_name}..."
elif '开始比较电缆方案' in msg:
if refs['status_label']: refs['status_label'].text = "准备开始计算..."
except queue.Empty: break
log_timer = ui.timer(0.1, process_log_queue)
if refs['status_label']: refs['status_label'].text = "初始化中..."
processing_dialog.open()
try:
# 2. 定义在线程中运行的任务
def task():
# 捕获 stdout 到我们的 QueueLogger
with contextlib.redirect_stdout(QueueLogger()):
return compare_design_methods(
excel_path=state['excel_path'],
n_clusters_override=n_clusters,
interactive=False,
plot_results=False # 禁止后台绘图,避免线程安全问题
)
results = await run.io_bound(task)
state['results'] = results
if not state['excel_path'] and results:
if state['substation'] is None:
_, state['substation'] = generate_wind_farm_data(n_turbines=30, layout='grid', spacing=800)
if refs['results_table']:
table_data = []
for res in results:
table_data.append({'name': res['name'], 'cost_wan': round(res['cost'] / 10000, 2), 'loss_kw': round(res['loss'], 2)})
refs['results_table'].rows = table_data
refs['results_table'].update()
# 计算完成后,自动寻找并显示最佳方案的拓扑图 (不再显示4合1大图)
if results:
best_res = min(results, key=lambda x: x['cost'])
update_plot(best_res)
ui.notify(f'计算完成!已自动加载推荐方案: {best_res["name"]}', type='positive')
update_export_buttons()
if refs['status_label']: refs['status_label'].text = "计算完成!"
except Exception as ex:
ui.notify(f'运行出错: {ex}', type='negative')
finally:
log_timer.cancel()
process_log_queue()
processing_dialog.close()
with ui.dialog() as processing_dialog:
with ui.card().classes('w-96 items-center justify-center p-6'):
ui.label('正在计算方案...').classes('text-xl font-bold text-primary mb-2')
ui.spinner(size='lg', color='primary')
refs['status_label'] = ui.label('准备中...').classes('mt-4 text-sm text-gray-700 font-medium')
with ui.expansion('查看实时日志', icon='terminal', value=True).classes('w-full mt-4 text-sm'):
refs['log_box'] = ui.log(max_lines=100).classes('w-full h-32 text-xs font-mono bg-black text-green-400')
processing_dialog.props('persistent')
with ui.header().classes('bg-primary text-white p-4 shadow-lg'):
ui.label('海上风电场集电线路设计优化系统').classes('text-2xl font-bold')
ui.label('Wind Farm Collector System Design Optimizer').classes('text-sm opacity-80')
with ui.row().classes('w-full p-4 gap-4'):
with ui.card().classes('w-1/4 p-4 shadow-md'):
ui.label('配置面板').classes('text-xl font-semibold mb-4 border-b pb-2')
ui.label('1. 上传坐标文件 (.xlsx)').classes('font-medium')
refs['upload_widget'] = ui.upload(label='选择Excel文件', on_upload=handle_upload, auto_upload=True).classes('w-full mb-4')
ui.label('2. 参数设置').classes('font-medium mt-4')
refs['clusters_input'] = ui.number('指定回路数 (可选)', value=None, format='%d', placeholder='自动计算').classes('w-full mb-4')
refs['run_btn'] = ui.button('运行方案对比', on_click=lambda: run_analysis(refs['clusters_input'].value)).classes('w-full mt-4 py-4').props('icon=play_arrow color=secondary')
with ui.column().classes('w-3/4 gap-4'):
with ui.card().classes('w-full p-4 shadow-md'):
ui.label('方案对比结果 (点击行查看拓扑详情)').classes('text-xl font-semibold mb-2')
columns = [
{'name': 'name', 'label': '方案名称', 'field': 'name', 'required': True, 'align': 'left'},
{'name': 'cost_wan', 'label': '总投资 (万元)', 'field': 'cost_wan', 'sortable': True},
{'name': 'loss_kw', 'label': '线损 (kW)', 'field': 'loss_kw', 'sortable': True},
]
# 移除 selection='single',改为纯行点击交互
refs['results_table'] = ui.table(columns=columns, rows=[]).classes('w-full')
refs['results_table'].on('rowClick', handle_row_click)
with ui.card().classes('w-full p-4 shadow-md'):
ui.label('拓扑可视化').classes('text-xl font-semibold mb-2')
refs['plot_container'] = ui.column().classes('w-full items-center')
with ui.card().classes('w-full p-4 shadow-md'):
ui.label('导出与下载').classes('text-xl font-semibold mb-2')
refs['export_row'] = ui.row().classes('gap-4')
ui.run(title='海上风电场集电线路优化', port=8080)