feat: 改进文件保存对话框,支持跨平台系统原生保存
主要改进: 1. 新增 save_file_with_dialog 函数 - 优先使用 PyWebview 原生模式保存对话框 - 回退到 Tkinter 对话框(本地环境) - 最终回退到浏览器下载方式 2. 优化所有导出功能 - Excel 对比表导出支持系统保存对话框 - DXF 文件导出支持系统保存对话框 - ZIP 批量导出支持系统保存对话框 - 模板导出支持系统保存对话框 3. 代码质量改进 - 统一异步函数命名规范(on_click_*) - 改进代码格式化和缩进 - 添加详细的调试日志 4. 用户体验提升 - 用户可以自由选择保存位置 - 支持文件类型过滤 - 自动处理文件名后缀
This commit is contained in:
537
gui.py
537
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(
|
||||
"""
|
||||
<style>
|
||||
.hide-selection-column .q-table__selection { display: none; }
|
||||
.hide-selection-column .q-table tr.selected { background-color: #e3f2fd !important; }
|
||||
.no-list .q-uploader__list { display: none !important; }
|
||||
</style>
|
||||
""")
|
||||
"""
|
||||
)
|
||||
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,16 +87,19 @@ 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"
|
||||
@@ -103,10 +108,13 @@ def index():
|
||||
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}"
|
||||
@@ -115,34 +123,60 @@ def index():
|
||||
params_text.append(pf_str)
|
||||
|
||||
for p in params_text:
|
||||
ui.chip(p, icon='bolt').props('outline color=primary')
|
||||
ui.chip(p, icon="bolt").props("outline color=primary")
|
||||
|
||||
ui.separator().classes('my-2')
|
||||
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:
|
||||
@@ -198,18 +232,29 @@ def index():
|
||||
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()
|
||||
@@ -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,10 +741,10 @@ 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(
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user