import os import sys import io import contextlib import tempfile 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": os.path.join(tempfile.gettempdir(), "windfarm_gui_uploads"), } # 确保临时目录存在 if not os.path.exists(state["temp_dir"]): os.makedirs(state["temp_dir"], exist_ok=True) @ui.page("/") def index(): # 注入 CSS 隐藏表格自带的复选框列,并定义选中行的高亮背景色 ui.add_head_html(""" """) 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, "run_btn": None, "current_file_label": 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 # 清理旧文件,确保目录中只有一个文件 if os.path.exists(state["temp_dir"]): for f in os.listdir(state["temp_dir"]): try: os.remove(os.path.join(state["temp_dir"], f)) except: pass 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") if refs["current_file_label"]: refs["current_file_label"].text = f"当前文件: {filename}" # 加载数据 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"]: # 使用 ui.pyplot 上下文自动管理 figure 生命周期 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,并传递给绘图函数 # 确保绘图发生在正确的 figure 上 ax = plt.gca() visualize_design( result["turbines"], state["substation"], result["eval"]["details"], title, ax=ax, ) async def handle_row_click(e): # 获取被点击行的数据 row = e.args[1] if len(e.args) > 1 else None if not row: return # 识别方案名称 row_name = row.get("original_name", row.get("name")) if not row_name: return selected_res = next( (r for r in state["results"] if r["name"] == row_name), None ) if selected_res: # 1. 更新拓扑图 update_plot(selected_res) ui.notify(f"已切换至方案: {selected_res['name']}") # 2. 通过设置 table 的 selected 属性来实现背景高亮 (监听点击事件驱动) if refs["results_table"]: refs["results_table"].selected = [row] from nicegui import run import queue async def run_analysis(): if not state["excel_path"]: ui.notify("请先上传 Excel 坐标文件!", type="warning") return 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 # 禁止 main.py 中的后台绘图,避免线程安全问题 with contextlib.redirect_stdout(QueueLogger()): return compare_design_methods( excel_path=state["excel_path"], n_clusters_override=None, 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 ) # 计算完成后,自动寻找并显示最佳方案的拓扑图 best_res = None if results: # 默认推荐 Scenario 1 中成本最低的方案 scenario1_results = [r for r in results if "Scenario 1" in r["name"]] if scenario1_results: best_res = min(scenario1_results, key=lambda x: x["cost"]) else: # 如果没有 Scenario 1,则回退到全局最优 best_res = min(results, key=lambda x: x["cost"]) update_plot(best_res) ui.notify( f'计算完成!已自动加载推荐方案: {best_res["name"]}', type="positive" ) # 更新结果表格 if refs["results_table"]: table_data = [] best_row = None for res in results: name_display = res["name"] is_best = False if best_res and res["name"] == best_res["name"]: name_display = f"(推荐) {name_display}" is_best = True row_dict = { "name": name_display, "cost_wan": round(res["cost"] / 10000, 2), "loss_kw": round(res["loss"], 2), "original_name": res["name"], } table_data.append(row_dict) if is_best: best_row = row_dict refs["results_table"].rows = table_data # 初始选中推荐方案,实现自动高亮 if best_row: refs["results_table"].selected = [best_row] update_export_buttons() if refs["status_label"]: refs["status_label"].text = "计算完成!" except Exception as ex: ui.notify(f"运行出错: {ex}", type="negative") import traceback traceback.print_exc() 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-2") # refs['current_file_label'] = ui.label('未选择文件').classes('text-xs text-gray-500 mb-4 italic') refs["run_btn"] = ( ui.button( "运行方案对比", on_click=run_analysis, ) .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' 结合行点击事件实现背景高亮 # 这样可以完全由 Python 事件逻辑控制,不依赖 CSS 伪类 refs["results_table"] = ui.table( columns=columns, rows=[], selection="single", row_key="original_name", ).classes("w-full hide-selection-column") refs["results_table"].on("row-click", 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)