From 15d8f4881dbfeb729ae5bc3e5513f009fe8f0307 Mon Sep 17 00:00:00 2001 From: dmy Date: Mon, 5 Jan 2026 21:32:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=B9=E8=BF=9B=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E5=AF=B9=E8=AF=9D=E6=A1=86=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=B7=A8=E5=B9=B3=E5=8F=B0=E7=B3=BB=E7=BB=9F=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要改进: 1. 新增 save_file_with_dialog 函数 - 优先使用 PyWebview 原生模式保存对话框 - 回退到 Tkinter 对话框(本地环境) - 最终回退到浏览器下载方式 2. 优化所有导出功能 - Excel 对比表导出支持系统保存对话框 - DXF 文件导出支持系统保存对话框 - ZIP 批量导出支持系统保存对话框 - 模板导出支持系统保存对话框 3. 代码质量改进 - 统一异步函数命名规范(on_click_*) - 改进代码格式化和缩进 - 添加详细的调试日志 4. 用户体验提升 - 用户可以自由选择保存位置 - 支持文件类型过滤 - 自动处理文件名后缀 --- gui.py | 563 +++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 423 insertions(+), 140 deletions(-) diff --git a/gui.py b/gui.py index 28b2d96..9acd50b 100644 --- a/gui.py +++ b/gui.py @@ -5,7 +5,7 @@ import contextlib import tempfile import matplotlib.pyplot as plt import matplotlib.backends.backend_svg -from nicegui import ui, events +from nicegui import ui, events, app from main import ( compare_design_methods, export_to_dxf, @@ -54,13 +54,15 @@ if not os.path.exists(state["temp_dir"]): @ui.page("/") def index(): # 注入 CSS 隐藏表格自带的复选框列,并定义选中行的高亮背景色 - ui.add_head_html(""" + ui.add_head_html( + """ - """) + """ + ) ui.query("body").style( 'background-color: #f0f2f5; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;' ) @@ -71,12 +73,12 @@ def index(): "results_table": None, "plot_container": None, "export_row": None, - "export_selected_btn": None, # 新增按钮引用 + "export_selected_btn": None, # 新增按钮引用 "status_label": None, "upload_widget": None, "run_btn": None, - "current_file_container": None, # 替换 label 为 container - "info_container": None, # 新增信息展示容器 + "current_file_container": None, # 替换 label 为 container + "info_container": None, # 新增信息展示容器 } def update_info_panel(): @@ -85,64 +87,96 @@ def index(): with refs["info_container"]: # System Params - Always show with ui.row().classes("w-full items-center gap-4 mb-2"): - ui.icon('settings', color='primary').classes('text-2xl') + ui.icon("settings", color="primary").classes("text-2xl") ui.label("系统参数").classes("text-lg font-bold") - + params_text = [] - + # 获取电压 - v = 66000 # Default + v = 66000 # Default is_default_v = True - if state.get("system_params") and 'voltage' in state['system_params']: - v = state['system_params']['voltage'] + if ( + state.get("system_params") + and "voltage" in state["system_params"] + ): + v = state["system_params"]["voltage"] is_default_v = False - + v_str = f"电压: {v/1000:.1f} kV" if v >= 1000 else f"电压: {v} V" if is_default_v: v_str += " (默认)" params_text.append(v_str) # 获取功率因数 - pf = 0.95 # Default + pf = 0.95 # Default is_default_pf = True - if state.get("system_params") and 'power_factor' in state['system_params']: - pf = state['system_params']['power_factor'] + if ( + state.get("system_params") + and "power_factor" in state["system_params"] + ): + pf = state["system_params"]["power_factor"] is_default_pf = False - + pf_str = f"功率因数: {pf}" if is_default_pf: pf_str += " (默认)" params_text.append(pf_str) - - for p in params_text: - ui.chip(p, icon='bolt').props('outline color=primary') - ui.separator().classes('my-2') + for p in params_text: + ui.chip(p, icon="bolt").props("outline color=primary") + + ui.separator().classes("my-2") # Cables if state.get("cable_specs"): with ui.row().classes("w-full items-center gap-2 mb-2"): - ui.icon('cable', color='secondary').classes('text-2xl') + ui.icon("cable", color="secondary").classes("text-2xl") ui.label("电缆规格参数").classes("text-lg font-bold") columns = [ - {'name': 'section', 'label': '截面 (mm²)', 'field': 'section', 'align': 'center'}, - {'name': 'capacity', 'label': '载流量 (A)', 'field': 'capacity', 'align': 'center'}, - {'name': 'resistance', 'label': '电阻 (Ω/km)', 'field': 'resistance', 'align': 'center'}, - {'name': 'cost', 'label': '参考单价 (元/m)', 'field': 'cost', 'align': 'center'}, + { + "name": "section", + "label": "截面 (mm²)", + "field": "section", + "align": "center", + }, + { + "name": "capacity", + "label": "载流量 (A)", + "field": "capacity", + "align": "center", + }, + { + "name": "resistance", + "label": "电阻 (Ω/km)", + "field": "resistance", + "align": "center", + }, + { + "name": "cost", + "label": "参考单价 (元/m)", + "field": "cost", + "align": "center", + }, ] rows = [] for spec in state["cable_specs"]: # spec is (section, capacity, resistance, cost, is_optional) - rows.append({ - 'section': spec[0], - 'capacity': spec[1], - 'resistance': spec[2], - 'cost': spec[3] - }) - ui.table(columns=columns, rows=rows).classes('w-full').props('dense flat bordered') + rows.append( + { + "section": spec[0], + "capacity": spec[1], + "resistance": spec[2], + "cost": spec[3], + } + ) + ui.table(columns=columns, rows=rows).classes("w-full").props( + "dense flat bordered" + ) else: - ui.label("未检测到电缆数据,将使用默认参数。").classes("text-gray-500 italic") + ui.label("未检测到电缆数据,将使用默认参数。").classes( + "text-gray-500 italic" + ) async def handle_upload(e: events.UploadEventArguments): try: @@ -193,27 +227,38 @@ def index(): state["excel_path"] = path state["original_filename"] = filename ui.notify(f"文件已上传: {filename}", type="positive") - + # 更新文件显示区域 if refs["current_file_container"]: refs["current_file_container"].clear() with refs["current_file_container"]: - with ui.row().classes('items-center w-full bg-blue-50 p-2 rounded border border-blue-200'): - ui.icon('description', color='primary').classes('text-xl mr-2') - ui.label(filename).classes('font-medium text-gray-700 flex-grow') - ui.icon('check_circle', color='positive') + with ui.row().classes( + "items-center w-full bg-blue-50 p-2 rounded border border-blue-200" + ): + ui.icon("description", color="primary").classes("text-xl mr-2") + ui.label(filename).classes( + "font-medium text-gray-700 flex-grow" + ) + ui.icon("check_circle", color="positive") # 加载数据 try: # 尝试解包 4 个返回值 (新版 main.py) - state["turbines"], state["substation"], state["cable_specs"], state["system_params"] = load_data_from_excel(path) + ( + state["turbines"], + state["substation"], + state["cable_specs"], + state["system_params"], + ) = load_data_from_excel(path) except ValueError: # 兼容旧版 (如果是 3 个返回值) - state["turbines"], state["substation"], state["cable_specs"] = load_data_from_excel(path) + state["turbines"], state["substation"], state["cable_specs"] = ( + load_data_from_excel(path) + ) state["system_params"] = {} - + update_info_panel() - + # 清空上传组件列表,以便下次选择(配合 .no-list CSS 使用) if refs["upload_widget"]: refs["upload_widget"].reset() @@ -221,6 +266,146 @@ def index(): except Exception as ex: ui.notify(f"上传处理失败: {ex}", type="negative") + async def save_file_with_dialog(filename, callback, file_filter="All files (*.*)"): + """ + 跨平台文件保存助手。 + 如果是原生模式,弹出系统保存对话框。 + 如果是浏览器模式(但在本地运行),尝试使用 tkinter 弹出对话框。 + 最后回退到使用 nicegui ui.download。 + :param filename: 默认文件名 + :param callback: 接收文件路径并执行保存操作的函数 (filepath) -> None + :param file_filter: 格式如 "Excel Files (*.xlsx)" + """ + # 检测是否为原生模式 (PyWebview) + is_native = False + native_window = None + try: + # 使用 getattr 安全获取 app.native,避免属性不存在错误 + # 并在 reload=True 时 native 可能未能正确初始化 + n_obj = getattr(app, "native", None) + if n_obj and getattr(n_obj, "main_window", None): + is_native = True + native_window = n_obj.main_window + except Exception as e: + print(f"DEBUG: Native check error: {e}") + + print( + f"DEBUG: save_file_with_dialog called. is_native={is_native}, filename={filename}" + ) + + if is_native and native_window: + try: + # PyWebview 的 create_file_dialog 的 file_types 参数期望一个字符串元组 + # 格式如: ('Description (*.ext)', 'All files (*.*)') + file_types = (file_filter,) + + print(f"DEBUG: calling create_file_dialog with types={file_types}") + + # 在 Native 模式下,create_file_dialog 是同步阻塞的 + # 注意:必须使用 app.native.SAVE_DIALOG + save_path = native_window.create_file_dialog( + app.native.SAVE_DIALOG, + directory="", + save_filename=filename, + file_types=file_types, + ) + + print(f"DEBUG: save_path result: {save_path}") + + # 用户取消 + if not save_path: + return + + # 处理返回类型 (PyWebview 可能返回字符串或列表) + if isinstance(save_path, (list, tuple)): + if not save_path: + return + save_path = save_path[0] + + # 确保文件名后缀正确 + if not save_path.lower().endswith( + os.path.splitext(filename)[1].lower() + ): + save_path += os.path.splitext(filename)[1] + + await callback(save_path) + ui.notify(f"文件已保存至: {save_path}", type="positive") + return # 成功处理,退出 + + except Exception as e: + import traceback + + traceback.print_exc() + print(f"ERROR in save_file_with_dialog (native): {e}") + # ui.notify(f"原生保存失败,尝试其他方式: {e}", type="warning") + print(f"原生保存失败,尝试其他方式: {e}") + # 继续向下执行,尝试 fallback + + # 非 Native 模式 (或 Native 失败),尝试使用 Tkinter (仅限本地环境) + try: + import tkinter as tk + from tkinter import filedialog + from nicegui import run + + print("DEBUG: Attempting Tkinter dialog...") + + def get_save_path_tk(default_name, f_filter): + try: + # 创建隐藏的根窗口 + root = tk.Tk() + root.withdraw() + root.attributes("-topmost", True) # 尝试置顶 + + # 转换 filter 格式: "Excel Files (*.xlsx)" -> [("Excel Files", "*.xlsx")] + filetypes = [] + if "(" in f_filter and ")" in f_filter: + desc = f_filter.split("(")[0].strip() + ext = f_filter.split("(")[1].split(")")[0] + filetypes.append((desc, ext)) + filetypes.append(("All files", "*.*")) + + path = filedialog.asksaveasfilename( + initialfile=default_name, filetypes=filetypes, title="保存文件" + ) + root.destroy() + return path + except Exception as ex: + print(f"Tkinter inner error: {ex}") + return None + + # 在线程中运行 tkinter,避免阻塞 asyncio 事件循环 + save_path = await run.io_bound(get_save_path_tk, filename, file_filter) + + if save_path: + print(f"DEBUG: Tkinter save_path: {save_path}") + # 确保文件名后缀正确 + if not save_path.lower().endswith( + os.path.splitext(filename)[1].lower() + ): + save_path += os.path.splitext(filename)[1] + + await callback(save_path) + ui.notify(f"文件已保存至: {save_path}", type="positive") + return # 成功处理 + elif save_path is None: + print("DEBUG: Tkinter dialog cancelled or failed silently.") + # 如果是用户取消(返回空字符串),通常不需要回退到下载。 + # 但这里如果 Tkinter 彻底失败返回 None,可能需要回退。 + # askopenfilename 返回空字符串表示取消。我们假设 None 是异常。 + # 这里简化处理:只要没拿到路径且没报错,就认为是取消。 + if save_path == "": + return + + except Exception as e: + print(f"Tkinter dialog failed: {e}") + # Fallback to ui.download if tkinter fails + + # 最后的回退方案:浏览器下载 + print("DEBUG: Falling back to ui.download") + temp_path = os.path.join(state["temp_dir"], filename) + await callback(temp_path) + ui.download(temp_path) + def update_export_buttons(): if refs["export_row"]: refs["export_row"].clear() @@ -229,17 +414,20 @@ def index(): # 获取文件名基础前缀 file_prefix = "wind_farm" - download_name = "wind_farm_design_result.xlsx" - + default_excel_name = "wind_farm_design_result.xlsx" + # 确定主对比 Excel 的生成路径 (对应 main.py 中的生成逻辑) if state.get("excel_path"): file_prefix = os.path.splitext(state["original_filename"])[0] - download_name = f"{file_prefix}_result.xlsx" - main_excel_path = os.path.join(state["temp_dir"], f"{file_prefix}_design.xlsx") + default_excel_name = f"{file_prefix}_result.xlsx" + # 这里的路径是 main.py 中生成的源文件路径,用于复制 + source_excel_path = os.path.join( + state["temp_dir"], f"{file_prefix}_design.xlsx" + ) else: - main_excel_path = "wind_farm_design.xlsx" + source_excel_path = "wind_farm_design.xlsx" - # 寻找推荐方案:优先 Scenario 1 的最低成本,否则取全局最低成本 + # 寻找推荐方案 scenario1_results = [r for r in state["results"] if "Scenario 1" in r["name"]] if scenario1_results: best_res = min(scenario1_results, key=lambda x: x["cost"]) @@ -247,14 +435,31 @@ def index(): best_res = min(state["results"], key=lambda x: x["cost"]) with refs["export_row"]: + + # --- 下载 Excel --- + async def save_excel(path): + import shutil + + # 如果源文件存在,则复制到目标路径 + if os.path.exists(source_excel_path): + shutil.copy2(source_excel_path, path) + else: + # 如果不存在,重新生成 + export_all_scenarios_to_excel(state["results"], path) + + async def on_click_excel(): + await save_file_with_dialog( + default_excel_name, save_excel, "Excel Files (*.xlsx)" + ) + ui.button( "下载 Excel 对比表", - on_click=lambda: ui.download(main_excel_path, download_name), + on_click=on_click_excel, ).props("icon=download") + # --- 导出推荐方案 DXF --- def export_best_dxf(): if state["substation"] is not None: - # 生成安全的文件名 safe_name = "".join( [ c @@ -262,31 +467,68 @@ def index(): if c.isalnum() or c in (" ", "-", "_") ] ).strip() - dxf_name = os.path.join(state["temp_dir"], f"{file_prefix}_best_{safe_name}.dxf") + default_name = f"{file_prefix}_best_{safe_name}.dxf" - export_to_dxf( - best_res["turbines"], - state["substation"], - best_res["eval"]["details"], - dxf_name, + async def save_dxf(path): + export_to_dxf( + best_res["turbines"], + state["substation"], + best_res["eval"]["details"], + path, + ) + + # 包装为 async 任务,并在 NiceGUI 事件循环中执行 + async def run_save(): + await save_file_with_dialog( + default_name, save_dxf, "DXF Files (*.dxf)" + ) + + # 这里的 export_best_dxf 本身是普通函数,绑定到 on_click + # 但我们需要它执行异步操作。最简单的是让 export_best_dxf 变为 async + # 或者在这里直接调用 run_save (但这在普通函数里不行) + # 更好的方法是将 export_best_dxf 定义为 async,如下所示 + return run_save() + else: + ui.notify("缺少升压站数据,无法导出 DXF", type="negative") + + # 将 export_best_dxf 改为 async 并重命名,以便直接用作回调 + async def on_click_best_dxf(): + if state["substation"] is not None: + safe_name = "".join( + [ + c + for c in best_res["name"] + if c.isalnum() or c in (" ", "-", "_") + ] + ).strip() + default_name = f"{file_prefix}_best_{safe_name}.dxf" + + async def save_dxf(path): + export_to_dxf( + best_res["turbines"], + state["substation"], + best_res["eval"]["details"], + path, + ) + + await save_file_with_dialog( + default_name, save_dxf, "DXF Files (*.dxf)" ) - 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 + f'导出推荐方案 DXF ({best_res["name"]})', on_click=on_click_best_dxf ).props("icon=architecture color=accent") - def export_selected_dxf(): + # --- 导出选中方案 DXF --- + async def on_click_selected_dxf(): if not refs["results_table"] or not refs["results_table"].selected: ui.notify("请先在上方表格中选择一个方案", type="warning") return selected_row = refs["results_table"].selected[0] row_name = selected_row.get("original_name", selected_row.get("name")) - selected_res = next( (r for r in state["results"] if r["name"] == row_name), None ) @@ -299,62 +541,55 @@ def index(): if c.isalnum() or c in (" ", "-", "_") ] ).strip() - dxf_name = os.path.join(state["temp_dir"], f"{file_prefix}_{safe_name}.dxf") + default_name = f"{file_prefix}_{safe_name}.dxf" - export_to_dxf( - selected_res["turbines"], - state["substation"], - selected_res["eval"]["details"], - dxf_name, + async def save_dxf(path): + export_to_dxf( + selected_res["turbines"], + state["substation"], + selected_res["eval"]["details"], + path, + ) + + await save_file_with_dialog( + default_name, save_dxf, "DXF Files (*.dxf)" ) - ui.download(dxf_name) - ui.notify(f'已导出选中方案: {selected_res["name"]}', type="positive") else: - ui.notify("无法导出:未找到方案数据或缺少升压站信息", type="negative") + ui.notify( + "无法导出:未找到方案数据或缺少升压站信息", type="negative" + ) - # 记录此按钮引用,以便 handle_row_click 更新文字 refs["export_selected_btn"] = ui.button( - "导出选中方案 DXF", on_click=export_selected_dxf + "导出选中方案 DXF", on_click=on_click_selected_dxf ).props("icon=architecture color=primary") - # 初始化按钮文字 clean_name = best_res["name"].replace("(推荐) ", "") refs["export_selected_btn"].set_text(f"导出选中方案 ({clean_name})") - def export_all_dxf(): + # --- 导出全部 ZIP --- + async def on_click_all_dxf(): if not state["results"] or state["substation"] is None: ui.notify("无方案数据可导出", type="warning") return - import zipfile + default_name = f"{file_prefix}_all_results.zip" - # 1. 确定文件名 - zip_filename = os.path.join(state["temp_dir"], f"{file_prefix}_all_results.zip") - excel_result_name = f"{file_prefix}_summary.xlsx" + async def save_zip(path): + import zipfile - # 推断 main.py 生成的原始 Excel 路径 - generated_excel_path = main_excel_path + excel_result_name = f"{file_prefix}_summary.xlsx" - try: - with zipfile.ZipFile( - zip_filename, "w", zipfile.ZIP_DEFLATED - ) as zipf: - # 2. 添加 Excel 结果表 - if os.path.exists(generated_excel_path): - zipf.write(generated_excel_path, arcname=excel_result_name) - else: - # 尝试重新生成 - try: - temp_excel = os.path.join(state["temp_dir"], "temp_export.xlsx") - export_all_scenarios_to_excel( - state["results"], temp_excel - ) - zipf.write(temp_excel, arcname=excel_result_name) - os.remove(temp_excel) - except Exception as e: - print(f"生成Excel失败: {e}") + with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zipf: + # 1. Excel + temp_excel = os.path.join(state["temp_dir"], "temp_export.xlsx") + export_all_scenarios_to_excel(state["results"], temp_excel) + zipf.write(temp_excel, arcname=excel_result_name) + try: + os.remove(temp_excel) + except: + pass - # 3. 添加所有 DXF + # 2. DXFs for res in state["results"]: safe_name = "".join( [ @@ -363,25 +598,24 @@ def index(): if c.isalnum() or c in (" ", "-", "_") ] ).strip() - dxf_name = os.path.join(state["temp_dir"], f"{file_prefix}_{safe_name}.dxf") + dxf_name = os.path.join( + state["temp_dir"], f"{file_prefix}_{safe_name}.dxf" + ) export_to_dxf( res["turbines"], state["substation"], res["eval"]["details"], dxf_name, ) - zipf.write(dxf_name) + zipf.write(dxf_name, arcname=os.path.basename(dxf_name)) try: os.remove(dxf_name) except: pass - ui.download(zip_filename) - ui.notify("已导出所有方案 (含Excel)", type="positive") - except Exception as ex: - ui.notify(f"导出失败: {ex}", type="negative") + await save_file_with_dialog(default_name, save_zip, "ZIP Files (*.zip)") - ui.button("导出全部方案 DXF (ZIP)", on_click=export_all_dxf).props( + ui.button("导出全部方案 DXF (ZIP)", on_click=on_click_all_dxf).props( "icon=folder_zip color=secondary" ) @@ -507,11 +741,11 @@ def index(): # 默认推荐 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"]) + best_res = min(scenario1_results, key=lambda x: x["cost"]) else: - # 如果没有 Scenario 1,则回退到全局最优 - best_res = min(results, key=lambda x: x["cost"]) - + # 如果没有 Scenario 1,则回退到全局最优 + best_res = min(results, key=lambda x: x["cost"]) + update_plot(best_res) ui.notify( f'计算完成!已自动加载推荐方案: {best_res["name"]}', type="positive" @@ -595,48 +829,71 @@ def index(): ) processing_dialog.props("persistent") - with ui.header().classes("bg-primary text-white p-4 shadow-lg items-center no-wrap"): + with ui.header().classes( + "bg-primary text-white p-4 shadow-lg items-center no-wrap" + ): with ui.column().classes("gap-0"): - ui.label("海上风电场集电线路设计优化系统 v1.0").classes("text-2xl font-bold") + ui.label("海上风电场集电线路设计优化系统 v1.0").classes( + "text-2xl font-bold" + ) with ui.column().classes("gap-0"): ui.label("Wind Farm Collector System Design Optimizer").classes( "text-sm opacity-80" ) ui.space() - ui.label("中能建西北院海上能源业务开发部").classes( - "text-xl font-bold" - ) + ui.label("中能建西北院海上能源业务开发部").classes("text-xl font-bold") 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") - def export_template(): + async def export_template(): from generate_template import create_template - try: + import shutil + + async def save_template(path): + # 生成模板到当前目录 create_template() - ui.download("coordinates.xlsx") - ui.notify("Excel 模板导出成功", type="positive") - except Exception as ex: - ui.notify(f"模板导出失败: {ex}", type="negative") + source = "coordinates.xlsx" + if os.path.exists(source): + shutil.copy2(source, path) + else: + raise FileNotFoundError("无法生成模板文件") + + await save_file_with_dialog( + "coordinates.xlsx", + save_template, + "Excel Files (*.xlsx)" + ) ui.button("导出 Excel 模板", on_click=export_template).classes( "w-full mb-4" ).props("icon=file_download outline color=primary") + async def test_save_dialog(): + async def dummy_callback(path): + # 仅作为测试,实际不写入文件,只弹出通知 + ui.notify(f"测试成功!选定路径: {path}", type="info") + + await save_file_with_dialog( + "test_save_dialog.txt", dummy_callback, "Text Files (*.txt)" + ) + + ui.button("测试对话框", on_click=test_save_dialog).classes( + "w-full mb-4" + ).props("icon=bug_report outline color=orange") + ui.label("1. 上传坐标文件 (.xlsx)").classes("font-medium") - + # 使用 .no-list CSS 隐藏 Quasar 默认列表,完全自定义文件显示 refs["upload_widget"] = ui.upload( - label="选择Excel文件", - on_upload=handle_upload, - auto_upload=True + label="选择Excel文件", on_upload=handle_upload, auto_upload=True ).classes("w-full mb-2 no-list") - + # 自定义文件显示容器 - refs['current_file_container'] = ui.column().classes('w-full mb-4') - with refs['current_file_container']: - ui.label('未选择文件').classes('text-xs text-gray-500 italic ml-1') + refs["current_file_container"] = ui.column().classes("w-full mb-4") + with refs["current_file_container"]: + ui.label("未选择文件").classes("text-xs text-gray-500 italic ml-1") refs["run_btn"] = ( ui.button( @@ -649,9 +906,15 @@ def index(): with ui.column().classes("w-3/4 gap-4"): # 新增:信息展示卡片 - with ui.card().classes("w-full p-4 shadow-md").style("max-height: 400px; overflow-y: auto;"): - refs["info_container"] = ui.column().classes("w-full") - ui.label("请上传 Excel 文件以查看系统参数和电缆规格...").classes("text-gray-500 italic") + with ( + ui.card() + .classes("w-full p-4 shadow-md") + .style("max-height: 400px; overflow-y: auto;") + ): + refs["info_container"] = ui.column().classes("w-full") + ui.label("请上传 Excel 文件以查看系统参数和电缆规格...").classes( + "text-gray-500 italic" + ) with ui.card().classes("w-full p-4 shadow-md"): ui.label("方案对比结果 (点击行查看拓扑详情)").classes( @@ -721,13 +984,16 @@ target_port = find_available_port(8082) # 从 8082 开始,避开常用的 808 # 检测是否为打包后的exe程序 import sys -if getattr(sys, 'frozen', False): + +if getattr(sys, "frozen", False): # 修复无控制台模式下 stdout/stderr 为 None 导致的 logging 错误 class NullWriter: def write(self, text): pass + def flush(self): pass + def isatty(self): return False @@ -738,12 +1004,29 @@ if getattr(sys, 'frozen', False): # 打包环境下禁用日志配置,避免在无控制台模式下出现错误 import logging.config - logging.config.dictConfig({ - 'version': 1, - 'disable_existing_loggers': True, - }) - ui.run(title="海上风电场集电线路优化", host='127.0.0.1', port=target_port, reload=False, window_size=(1280, 800),native=True) + + logging.config.dictConfig( + { + "version": 1, + "disable_existing_loggers": True, + } + ) + ui.run( + title="海上风电场集电线路优化", + host="127.0.0.1", + port=target_port, + reload=False, + window_size=(1280, 800), + native=True, + ) else: # 普通使用环境保留日志功能 # ui.run(title="海上风电场集电线路优化", host='127.0.0.1', reload=True, port=target_port, native=False) - ui.run(title="海上风电场集电线路优化", host='127.0.0.1', port=target_port, reload=True, window_size=(1280, 800),native=True) + ui.run( + title="海上风电场集电线路优化", + host="127.0.0.1", + port=target_port, + reload=False, + window_size=(1280, 800), + native=True, + )