feat: 改进文件保存对话框,支持跨平台系统原生保存
主要改进: 1. 新增 save_file_with_dialog 函数 - 优先使用 PyWebview 原生模式保存对话框 - 回退到 Tkinter 对话框(本地环境) - 最终回退到浏览器下载方式 2. 优化所有导出功能 - Excel 对比表导出支持系统保存对话框 - DXF 文件导出支持系统保存对话框 - ZIP 批量导出支持系统保存对话框 - 模板导出支持系统保存对话框 3. 代码质量改进 - 统一异步函数命名规范(on_click_*) - 改进代码格式化和缩进 - 添加详细的调试日志 4. 用户体验提升 - 用户可以自由选择保存位置 - 支持文件类型过滤 - 自动处理文件名后缀
This commit is contained in:
497
gui.py
497
gui.py
@@ -5,7 +5,7 @@ import contextlib
|
|||||||
import tempfile
|
import tempfile
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import matplotlib.backends.backend_svg
|
import matplotlib.backends.backend_svg
|
||||||
from nicegui import ui, events
|
from nicegui import ui, events, app
|
||||||
from main import (
|
from main import (
|
||||||
compare_design_methods,
|
compare_design_methods,
|
||||||
export_to_dxf,
|
export_to_dxf,
|
||||||
@@ -54,13 +54,15 @@ if not os.path.exists(state["temp_dir"]):
|
|||||||
@ui.page("/")
|
@ui.page("/")
|
||||||
def index():
|
def index():
|
||||||
# 注入 CSS 隐藏表格自带的复选框列,并定义选中行的高亮背景色
|
# 注入 CSS 隐藏表格自带的复选框列,并定义选中行的高亮背景色
|
||||||
ui.add_head_html("""
|
ui.add_head_html(
|
||||||
|
"""
|
||||||
<style>
|
<style>
|
||||||
.hide-selection-column .q-table__selection { display: none; }
|
.hide-selection-column .q-table__selection { display: none; }
|
||||||
.hide-selection-column .q-table tr.selected { background-color: #e3f2fd !important; }
|
.hide-selection-column .q-table tr.selected { background-color: #e3f2fd !important; }
|
||||||
.no-list .q-uploader__list { display: none !important; }
|
.no-list .q-uploader__list { display: none !important; }
|
||||||
</style>
|
</style>
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
ui.query("body").style(
|
ui.query("body").style(
|
||||||
'background-color: #f0f2f5; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;'
|
'background-color: #f0f2f5; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;'
|
||||||
)
|
)
|
||||||
@@ -85,7 +87,7 @@ def index():
|
|||||||
with refs["info_container"]:
|
with refs["info_container"]:
|
||||||
# System Params - Always show
|
# System Params - Always show
|
||||||
with ui.row().classes("w-full items-center gap-4 mb-2"):
|
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")
|
ui.label("系统参数").classes("text-lg font-bold")
|
||||||
|
|
||||||
params_text = []
|
params_text = []
|
||||||
@@ -93,8 +95,11 @@ def index():
|
|||||||
# 获取电压
|
# 获取电压
|
||||||
v = 66000 # Default
|
v = 66000 # Default
|
||||||
is_default_v = True
|
is_default_v = True
|
||||||
if state.get("system_params") and 'voltage' in state['system_params']:
|
if (
|
||||||
v = state['system_params']['voltage']
|
state.get("system_params")
|
||||||
|
and "voltage" in state["system_params"]
|
||||||
|
):
|
||||||
|
v = state["system_params"]["voltage"]
|
||||||
is_default_v = False
|
is_default_v = False
|
||||||
|
|
||||||
v_str = f"电压: {v/1000:.1f} kV" if v >= 1000 else f"电压: {v} V"
|
v_str = f"电压: {v/1000:.1f} kV" if v >= 1000 else f"电压: {v} V"
|
||||||
@@ -105,8 +110,11 @@ def index():
|
|||||||
# 获取功率因数
|
# 获取功率因数
|
||||||
pf = 0.95 # Default
|
pf = 0.95 # Default
|
||||||
is_default_pf = True
|
is_default_pf = True
|
||||||
if state.get("system_params") and 'power_factor' in state['system_params']:
|
if (
|
||||||
pf = state['system_params']['power_factor']
|
state.get("system_params")
|
||||||
|
and "power_factor" in state["system_params"]
|
||||||
|
):
|
||||||
|
pf = state["system_params"]["power_factor"]
|
||||||
is_default_pf = False
|
is_default_pf = False
|
||||||
|
|
||||||
pf_str = f"功率因数: {pf}"
|
pf_str = f"功率因数: {pf}"
|
||||||
@@ -115,34 +123,60 @@ def index():
|
|||||||
params_text.append(pf_str)
|
params_text.append(pf_str)
|
||||||
|
|
||||||
for p in params_text:
|
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
|
# Cables
|
||||||
if state.get("cable_specs"):
|
if state.get("cable_specs"):
|
||||||
with ui.row().classes("w-full items-center gap-2 mb-2"):
|
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")
|
ui.label("电缆规格参数").classes("text-lg font-bold")
|
||||||
|
|
||||||
columns = [
|
columns = [
|
||||||
{'name': 'section', 'label': '截面 (mm²)', 'field': 'section', 'align': 'center'},
|
{
|
||||||
{'name': 'capacity', 'label': '载流量 (A)', 'field': 'capacity', 'align': 'center'},
|
"name": "section",
|
||||||
{'name': 'resistance', 'label': '电阻 (Ω/km)', 'field': 'resistance', 'align': 'center'},
|
"label": "截面 (mm²)",
|
||||||
{'name': 'cost', 'label': '参考单价 (元/m)', 'field': 'cost', 'align': 'center'},
|
"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 = []
|
rows = []
|
||||||
for spec in state["cable_specs"]:
|
for spec in state["cable_specs"]:
|
||||||
# spec is (section, capacity, resistance, cost, is_optional)
|
# spec is (section, capacity, resistance, cost, is_optional)
|
||||||
rows.append({
|
rows.append(
|
||||||
'section': spec[0],
|
{
|
||||||
'capacity': spec[1],
|
"section": spec[0],
|
||||||
'resistance': spec[2],
|
"capacity": spec[1],
|
||||||
'cost': spec[3]
|
"resistance": spec[2],
|
||||||
})
|
"cost": spec[3],
|
||||||
ui.table(columns=columns, rows=rows).classes('w-full').props('dense flat bordered')
|
}
|
||||||
|
)
|
||||||
|
ui.table(columns=columns, rows=rows).classes("w-full").props(
|
||||||
|
"dense flat bordered"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
ui.label("未检测到电缆数据,将使用默认参数。").classes("text-gray-500 italic")
|
ui.label("未检测到电缆数据,将使用默认参数。").classes(
|
||||||
|
"text-gray-500 italic"
|
||||||
|
)
|
||||||
|
|
||||||
async def handle_upload(e: events.UploadEventArguments):
|
async def handle_upload(e: events.UploadEventArguments):
|
||||||
try:
|
try:
|
||||||
@@ -198,18 +232,29 @@ def index():
|
|||||||
if refs["current_file_container"]:
|
if refs["current_file_container"]:
|
||||||
refs["current_file_container"].clear()
|
refs["current_file_container"].clear()
|
||||||
with refs["current_file_container"]:
|
with refs["current_file_container"]:
|
||||||
with ui.row().classes('items-center w-full bg-blue-50 p-2 rounded border border-blue-200'):
|
with ui.row().classes(
|
||||||
ui.icon('description', color='primary').classes('text-xl mr-2')
|
"items-center w-full bg-blue-50 p-2 rounded border border-blue-200"
|
||||||
ui.label(filename).classes('font-medium text-gray-700 flex-grow')
|
):
|
||||||
ui.icon('check_circle', color='positive')
|
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:
|
try:
|
||||||
# 尝试解包 4 个返回值 (新版 main.py)
|
# 尝试解包 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:
|
except ValueError:
|
||||||
# 兼容旧版 (如果是 3 个返回值)
|
# 兼容旧版 (如果是 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"] = {}
|
state["system_params"] = {}
|
||||||
|
|
||||||
update_info_panel()
|
update_info_panel()
|
||||||
@@ -221,6 +266,146 @@ def index():
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
ui.notify(f"上传处理失败: {ex}", type="negative")
|
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():
|
def update_export_buttons():
|
||||||
if refs["export_row"]:
|
if refs["export_row"]:
|
||||||
refs["export_row"].clear()
|
refs["export_row"].clear()
|
||||||
@@ -229,17 +414,20 @@ def index():
|
|||||||
|
|
||||||
# 获取文件名基础前缀
|
# 获取文件名基础前缀
|
||||||
file_prefix = "wind_farm"
|
file_prefix = "wind_farm"
|
||||||
download_name = "wind_farm_design_result.xlsx"
|
default_excel_name = "wind_farm_design_result.xlsx"
|
||||||
|
|
||||||
# 确定主对比 Excel 的生成路径 (对应 main.py 中的生成逻辑)
|
# 确定主对比 Excel 的生成路径 (对应 main.py 中的生成逻辑)
|
||||||
if state.get("excel_path"):
|
if state.get("excel_path"):
|
||||||
file_prefix = os.path.splitext(state["original_filename"])[0]
|
file_prefix = os.path.splitext(state["original_filename"])[0]
|
||||||
download_name = f"{file_prefix}_result.xlsx"
|
default_excel_name = f"{file_prefix}_result.xlsx"
|
||||||
main_excel_path = os.path.join(state["temp_dir"], f"{file_prefix}_design.xlsx")
|
# 这里的路径是 main.py 中生成的源文件路径,用于复制
|
||||||
|
source_excel_path = os.path.join(
|
||||||
|
state["temp_dir"], f"{file_prefix}_design.xlsx"
|
||||||
|
)
|
||||||
else:
|
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"]]
|
scenario1_results = [r for r in state["results"] if "Scenario 1" in r["name"]]
|
||||||
if scenario1_results:
|
if scenario1_results:
|
||||||
best_res = min(scenario1_results, key=lambda x: x["cost"])
|
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"])
|
best_res = min(state["results"], key=lambda x: x["cost"])
|
||||||
|
|
||||||
with refs["export_row"]:
|
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(
|
ui.button(
|
||||||
"下载 Excel 对比表",
|
"下载 Excel 对比表",
|
||||||
on_click=lambda: ui.download(main_excel_path, download_name),
|
on_click=on_click_excel,
|
||||||
).props("icon=download")
|
).props("icon=download")
|
||||||
|
|
||||||
|
# --- 导出推荐方案 DXF ---
|
||||||
def export_best_dxf():
|
def export_best_dxf():
|
||||||
if state["substation"] is not None:
|
if state["substation"] is not None:
|
||||||
# 生成安全的文件名
|
|
||||||
safe_name = "".join(
|
safe_name = "".join(
|
||||||
[
|
[
|
||||||
c
|
c
|
||||||
@@ -262,31 +467,68 @@ def index():
|
|||||||
if c.isalnum() or c in (" ", "-", "_")
|
if c.isalnum() or c in (" ", "-", "_")
|
||||||
]
|
]
|
||||||
).strip()
|
).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"
|
||||||
|
|
||||||
|
async def save_dxf(path):
|
||||||
export_to_dxf(
|
export_to_dxf(
|
||||||
best_res["turbines"],
|
best_res["turbines"],
|
||||||
state["substation"],
|
state["substation"],
|
||||||
best_res["eval"]["details"],
|
best_res["eval"]["details"],
|
||||||
dxf_name,
|
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:
|
else:
|
||||||
ui.notify("缺少升压站数据,无法导出 DXF", type="negative")
|
ui.notify("缺少升压站数据,无法导出 DXF", type="negative")
|
||||||
|
|
||||||
ui.button(
|
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")
|
).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:
|
if not refs["results_table"] or not refs["results_table"].selected:
|
||||||
ui.notify("请先在上方表格中选择一个方案", type="warning")
|
ui.notify("请先在上方表格中选择一个方案", type="warning")
|
||||||
return
|
return
|
||||||
|
|
||||||
selected_row = refs["results_table"].selected[0]
|
selected_row = refs["results_table"].selected[0]
|
||||||
row_name = selected_row.get("original_name", selected_row.get("name"))
|
row_name = selected_row.get("original_name", selected_row.get("name"))
|
||||||
|
|
||||||
selected_res = next(
|
selected_res = next(
|
||||||
(r for r in state["results"] if r["name"] == row_name), None
|
(r for r in state["results"] if r["name"] == row_name), None
|
||||||
)
|
)
|
||||||
@@ -299,62 +541,55 @@ def index():
|
|||||||
if c.isalnum() or c in (" ", "-", "_")
|
if c.isalnum() or c in (" ", "-", "_")
|
||||||
]
|
]
|
||||||
).strip()
|
).strip()
|
||||||
dxf_name = os.path.join(state["temp_dir"], f"{file_prefix}_{safe_name}.dxf")
|
default_name = f"{file_prefix}_{safe_name}.dxf"
|
||||||
|
|
||||||
|
async def save_dxf(path):
|
||||||
export_to_dxf(
|
export_to_dxf(
|
||||||
selected_res["turbines"],
|
selected_res["turbines"],
|
||||||
state["substation"],
|
state["substation"],
|
||||||
selected_res["eval"]["details"],
|
selected_res["eval"]["details"],
|
||||||
dxf_name,
|
path,
|
||||||
|
)
|
||||||
|
|
||||||
|
await save_file_with_dialog(
|
||||||
|
default_name, save_dxf, "DXF Files (*.dxf)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ui.notify(
|
||||||
|
"无法导出:未找到方案数据或缺少升压站信息", type="negative"
|
||||||
)
|
)
|
||||||
ui.download(dxf_name)
|
|
||||||
ui.notify(f'已导出选中方案: {selected_res["name"]}', type="positive")
|
|
||||||
else:
|
|
||||||
ui.notify("无法导出:未找到方案数据或缺少升压站信息", type="negative")
|
|
||||||
|
|
||||||
# 记录此按钮引用,以便 handle_row_click 更新文字
|
|
||||||
refs["export_selected_btn"] = ui.button(
|
refs["export_selected_btn"] = ui.button(
|
||||||
"导出选中方案 DXF", on_click=export_selected_dxf
|
"导出选中方案 DXF", on_click=on_click_selected_dxf
|
||||||
).props("icon=architecture color=primary")
|
).props("icon=architecture color=primary")
|
||||||
|
|
||||||
# 初始化按钮文字
|
|
||||||
clean_name = best_res["name"].replace("(推荐) ", "")
|
clean_name = best_res["name"].replace("(推荐) ", "")
|
||||||
refs["export_selected_btn"].set_text(f"导出选中方案 ({clean_name})")
|
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:
|
if not state["results"] or state["substation"] is None:
|
||||||
ui.notify("无方案数据可导出", type="warning")
|
ui.notify("无方案数据可导出", type="warning")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
default_name = f"{file_prefix}_all_results.zip"
|
||||||
|
|
||||||
|
async def save_zip(path):
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
# 1. 确定文件名
|
|
||||||
zip_filename = os.path.join(state["temp_dir"], f"{file_prefix}_all_results.zip")
|
|
||||||
excel_result_name = f"{file_prefix}_summary.xlsx"
|
excel_result_name = f"{file_prefix}_summary.xlsx"
|
||||||
|
|
||||||
# 推断 main.py 生成的原始 Excel 路径
|
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||||
generated_excel_path = main_excel_path
|
# 1. Excel
|
||||||
|
|
||||||
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")
|
temp_excel = os.path.join(state["temp_dir"], "temp_export.xlsx")
|
||||||
export_all_scenarios_to_excel(
|
export_all_scenarios_to_excel(state["results"], temp_excel)
|
||||||
state["results"], temp_excel
|
|
||||||
)
|
|
||||||
zipf.write(temp_excel, arcname=excel_result_name)
|
zipf.write(temp_excel, arcname=excel_result_name)
|
||||||
|
try:
|
||||||
os.remove(temp_excel)
|
os.remove(temp_excel)
|
||||||
except Exception as e:
|
except:
|
||||||
print(f"生成Excel失败: {e}")
|
pass
|
||||||
|
|
||||||
# 3. 添加所有 DXF
|
# 2. DXFs
|
||||||
for res in state["results"]:
|
for res in state["results"]:
|
||||||
safe_name = "".join(
|
safe_name = "".join(
|
||||||
[
|
[
|
||||||
@@ -363,25 +598,24 @@ def index():
|
|||||||
if c.isalnum() or c in (" ", "-", "_")
|
if c.isalnum() or c in (" ", "-", "_")
|
||||||
]
|
]
|
||||||
).strip()
|
).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(
|
export_to_dxf(
|
||||||
res["turbines"],
|
res["turbines"],
|
||||||
state["substation"],
|
state["substation"],
|
||||||
res["eval"]["details"],
|
res["eval"]["details"],
|
||||||
dxf_name,
|
dxf_name,
|
||||||
)
|
)
|
||||||
zipf.write(dxf_name)
|
zipf.write(dxf_name, arcname=os.path.basename(dxf_name))
|
||||||
try:
|
try:
|
||||||
os.remove(dxf_name)
|
os.remove(dxf_name)
|
||||||
except:
|
except:
|
||||||
pass
|
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"
|
"icon=folder_zip color=secondary"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -595,48 +829,71 @@ def index():
|
|||||||
)
|
)
|
||||||
processing_dialog.props("persistent")
|
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"):
|
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"):
|
with ui.column().classes("gap-0"):
|
||||||
ui.label("Wind Farm Collector System Design Optimizer").classes(
|
ui.label("Wind Farm Collector System Design Optimizer").classes(
|
||||||
"text-sm opacity-80"
|
"text-sm opacity-80"
|
||||||
)
|
)
|
||||||
ui.space()
|
ui.space()
|
||||||
ui.label("中能建西北院海上能源业务开发部").classes(
|
ui.label("中能建西北院海上能源业务开发部").classes("text-xl font-bold")
|
||||||
"text-xl font-bold"
|
|
||||||
)
|
|
||||||
|
|
||||||
with ui.row().classes("w-full p-4 gap-4"):
|
with ui.row().classes("w-full p-4 gap-4"):
|
||||||
with ui.card().classes("w-1/4 p-4 shadow-md"):
|
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("配置面板").classes("text-xl font-semibold mb-4 border-b pb-2")
|
||||||
|
|
||||||
def export_template():
|
async def export_template():
|
||||||
from generate_template import create_template
|
from generate_template import create_template
|
||||||
try:
|
import shutil
|
||||||
|
|
||||||
|
async def save_template(path):
|
||||||
|
# 生成模板到当前目录
|
||||||
create_template()
|
create_template()
|
||||||
ui.download("coordinates.xlsx")
|
source = "coordinates.xlsx"
|
||||||
ui.notify("Excel 模板导出成功", type="positive")
|
if os.path.exists(source):
|
||||||
except Exception as ex:
|
shutil.copy2(source, path)
|
||||||
ui.notify(f"模板导出失败: {ex}", type="negative")
|
else:
|
||||||
|
raise FileNotFoundError("无法生成模板文件")
|
||||||
|
|
||||||
|
await save_file_with_dialog(
|
||||||
|
"coordinates.xlsx",
|
||||||
|
save_template,
|
||||||
|
"Excel Files (*.xlsx)"
|
||||||
|
)
|
||||||
|
|
||||||
ui.button("导出 Excel 模板", on_click=export_template).classes(
|
ui.button("导出 Excel 模板", on_click=export_template).classes(
|
||||||
"w-full mb-4"
|
"w-full mb-4"
|
||||||
).props("icon=file_download outline color=primary")
|
).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")
|
ui.label("1. 上传坐标文件 (.xlsx)").classes("font-medium")
|
||||||
|
|
||||||
# 使用 .no-list CSS 隐藏 Quasar 默认列表,完全自定义文件显示
|
# 使用 .no-list CSS 隐藏 Quasar 默认列表,完全自定义文件显示
|
||||||
refs["upload_widget"] = ui.upload(
|
refs["upload_widget"] = ui.upload(
|
||||||
label="选择Excel文件",
|
label="选择Excel文件", on_upload=handle_upload, auto_upload=True
|
||||||
on_upload=handle_upload,
|
|
||||||
auto_upload=True
|
|
||||||
).classes("w-full mb-2 no-list")
|
).classes("w-full mb-2 no-list")
|
||||||
|
|
||||||
# 自定义文件显示容器
|
# 自定义文件显示容器
|
||||||
refs['current_file_container'] = ui.column().classes('w-full mb-4')
|
refs["current_file_container"] = ui.column().classes("w-full mb-4")
|
||||||
with refs['current_file_container']:
|
with refs["current_file_container"]:
|
||||||
ui.label('未选择文件').classes('text-xs text-gray-500 italic ml-1')
|
ui.label("未选择文件").classes("text-xs text-gray-500 italic ml-1")
|
||||||
|
|
||||||
refs["run_btn"] = (
|
refs["run_btn"] = (
|
||||||
ui.button(
|
ui.button(
|
||||||
@@ -649,9 +906,15 @@ def index():
|
|||||||
|
|
||||||
with ui.column().classes("w-3/4 gap-4"):
|
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;"):
|
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")
|
refs["info_container"] = ui.column().classes("w-full")
|
||||||
ui.label("请上传 Excel 文件以查看系统参数和电缆规格...").classes("text-gray-500 italic")
|
ui.label("请上传 Excel 文件以查看系统参数和电缆规格...").classes(
|
||||||
|
"text-gray-500 italic"
|
||||||
|
)
|
||||||
|
|
||||||
with ui.card().classes("w-full p-4 shadow-md"):
|
with ui.card().classes("w-full p-4 shadow-md"):
|
||||||
ui.label("方案对比结果 (点击行查看拓扑详情)").classes(
|
ui.label("方案对比结果 (点击行查看拓扑详情)").classes(
|
||||||
@@ -721,13 +984,16 @@ target_port = find_available_port(8082) # 从 8082 开始,避开常用的 808
|
|||||||
|
|
||||||
# 检测是否为打包后的exe程序
|
# 检测是否为打包后的exe程序
|
||||||
import sys
|
import sys
|
||||||
if getattr(sys, 'frozen', False):
|
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
# 修复无控制台模式下 stdout/stderr 为 None 导致的 logging 错误
|
# 修复无控制台模式下 stdout/stderr 为 None 导致的 logging 错误
|
||||||
class NullWriter:
|
class NullWriter:
|
||||||
def write(self, text):
|
def write(self, text):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def isatty(self):
|
def isatty(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -738,12 +1004,29 @@ if getattr(sys, 'frozen', False):
|
|||||||
|
|
||||||
# 打包环境下禁用日志配置,避免在无控制台模式下出现错误
|
# 打包环境下禁用日志配置,避免在无控制台模式下出现错误
|
||||||
import logging.config
|
import logging.config
|
||||||
logging.config.dictConfig({
|
|
||||||
'version': 1,
|
logging.config.dictConfig(
|
||||||
'disable_existing_loggers': True,
|
{
|
||||||
})
|
"version": 1,
|
||||||
ui.run(title="海上风电场集电线路优化", host='127.0.0.1', port=target_port, reload=False, window_size=(1280, 800),native=True)
|
"disable_existing_loggers": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ui.run(
|
||||||
|
title="海上风电场集电线路优化",
|
||||||
|
host="127.0.0.1",
|
||||||
|
port=target_port,
|
||||||
|
reload=False,
|
||||||
|
window_size=(1280, 800),
|
||||||
|
native=True,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# 普通使用环境保留日志功能
|
# 普通使用环境保留日志功能
|
||||||
# ui.run(title="海上风电场集电线路优化", host='127.0.0.1', reload=True, port=target_port, native=False)
|
# 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