feat: 优化GUI用户体验和打包配置

主要改进:
1. GUI界面优化
   - 自定义文件上传显示组件,替换默认列表为更美观的卡片式展示
   - 支持环境变量 PROJECT_TEMP_DIR 配置临时目录路径
   - 优化文件导出路径管理,统一使用临时目录
   - 改进端口查找逻辑,从8082开始避免常用端口冲突
   - 修复打包后无控制台模式的stdout/stderr处理

2. 打包配置改进
   - 更新Makefile使用.spec文件进行打包
   - 添加nicegui-pack打包选项
   - 优化clean命令,使用Python跨平台清理

3. 代码优化
   - 注释掉main.py中的详细统计信息打印
   - 改进打包环境的日志配置方式
This commit is contained in:
dmy
2026-01-05 17:09:39 +08:00
parent 05ac7a3388
commit 751bdef245
3 changed files with 95 additions and 51 deletions

View File

@@ -1,28 +1,31 @@
.PHONY: help clean build rebuild
# 默认目标
# 默认目标
help:
@echo "海上风电场集电线路设计优化系统 - 打包脚本"
@echo "海上风电场集电线路设计优化系统 - 构建脚本"
@echo ""
@echo "可用命令:"
@echo " make build - 打包成独立的exe程序"
@echo " make rebuild - 清理重新打包"
@echo " make clean - 清理打包生成的临时文件"
@echo " make help - 显示此帮助信息"
@echo "可用命令:"
@echo " make build - 使用 .spec 文件生成单文件 exe 程序"
@echo " make rebuild - 清理重新构建"
@echo " make clean - 清理构建生成的临时文件和缓存"
@echo " make help - 显示此帮助信息"
# 打包成独立的exe程序
# 生成单文件exe程序
# 使用 --clean 清理 PyInstaller 缓存,-y 自动覆盖输出
build:
@echo "开始打包程序..."
uv run pyinstaller build.spec
@echo "打包完成!"
@echo "可执行文件位于: dist/海上风电场集电线路设计优化系统.exe"
@echo "开始打包程序..."
uv run pyinstaller --clean -y "海上风电场集电线路设计优化系统.spec"
@echo "打包完成!"
@echo "可执行文件位于: dist/海上风电场集电线路设计优化系统.exe"
# 清理打包生成的临时文件
# 清理构建生成的临时文件
clean:
@echo "清理打包临时文件..."
@if exist build rmdir /s /q build
@if exist dist rmdir /s /q dist
@echo "清理完成!"
@echo "正在清理临时文件..."
@uv run python -c "import shutil, pathlib; [shutil.rmtree(p) for p in pathlib.Path('.').rglob('__pycache__')]; shutil.rmtree('build', ignore_errors=True); shutil.rmtree('dist', ignore_errors=True)"
@echo "清理完成!"
nice:
uv run nicegui-pack --onefile --name "海上风电场集电线路设计优化系统" gui.py --onefile --windowed
# 清理后重新打包
# 清理并重新构建
rebuild: clean build

99
gui.py
View File

@@ -4,6 +4,7 @@ import io
import contextlib
import tempfile
import matplotlib.pyplot as plt
import matplotlib.backends.backend_svg
from nicegui import ui, events
from main import (
compare_design_methods,
@@ -32,6 +33,8 @@ class Logger(io.StringIO):
# 状态变量
# 优先从环境变量 PROJECT_TEMP_DIR 读取,否则使用系统默认临时目录
_base_temp = os.environ.get("PROJECT_TEMP_DIR", tempfile.gettempdir())
state = {
"excel_path": None,
"original_filename": None,
@@ -40,7 +43,7 @@ state = {
"turbines": None,
"cable_specs": None,
"system_params": None,
"temp_dir": os.path.join(tempfile.gettempdir(), "windfarm_gui_uploads"),
"temp_dir": os.path.join(_base_temp, "windfarm_gui_uploads"),
}
# 确保临时目录存在
@@ -55,6 +58,7 @@ def index():
<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(
@@ -71,7 +75,7 @@ def index():
"status_label": None,
"upload_widget": None,
"run_btn": None,
"current_file_label": None,
"current_file_container": None, # 替换 label 为 container
"info_container": None, # 新增信息展示容器
}
@@ -189,8 +193,15 @@ def index():
state["excel_path"] = path
state["original_filename"] = filename
ui.notify(f"文件已上传: {filename}", type="positive")
if refs["current_file_label"]:
refs["current_file_label"].text = f"当前文件: {filename}"
# 更新文件显示区域
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')
# 加载数据
try:
@@ -202,6 +213,10 @@ def index():
state["system_params"] = {}
update_info_panel()
# 清空上传组件列表,以便下次选择(配合 .no-list CSS 使用)
if refs["upload_widget"]:
refs["upload_widget"].reset()
except Exception as ex:
ui.notify(f"上传处理失败: {ex}", type="negative")
@@ -215,9 +230,14 @@ def index():
# 获取文件名基础前缀
file_prefix = "wind_farm"
download_name = "wind_farm_design_result.xlsx"
if state.get("original_filename"):
# 确定主对比 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")
else:
main_excel_path = "wind_farm_design.xlsx"
# 寻找推荐方案:优先 Scenario 1 的最低成本,否则取全局最低成本
scenario1_results = [r for r in state["results"] if "Scenario 1" in r["name"]]
@@ -229,7 +249,7 @@ def index():
with refs["export_row"]:
ui.button(
"下载 Excel 对比表",
on_click=lambda: ui.download("wind_farm_design.xlsx", download_name),
on_click=lambda: ui.download(main_excel_path, download_name),
).props("icon=download")
def export_best_dxf():
@@ -242,7 +262,7 @@ def index():
if c.isalnum() or c in (" ", "-", "_")
]
).strip()
dxf_name = f"{file_prefix}_best_{safe_name}.dxf"
dxf_name = os.path.join(state["temp_dir"], f"{file_prefix}_best_{safe_name}.dxf")
export_to_dxf(
best_res["turbines"],
@@ -279,7 +299,7 @@ def index():
if c.isalnum() or c in (" ", "-", "_")
]
).strip()
dxf_name = f"{file_prefix}_{safe_name}.dxf"
dxf_name = os.path.join(state["temp_dir"], f"{file_prefix}_{safe_name}.dxf")
export_to_dxf(
selected_res["turbines"],
@@ -309,18 +329,11 @@ def index():
import zipfile
# 1. 确定文件名
zip_filename = f"{file_prefix}_all_results.zip"
zip_filename = os.path.join(state["temp_dir"], f"{file_prefix}_all_results.zip")
excel_result_name = f"{file_prefix}_summary.xlsx"
# 推断 main.py 生成的原始 Excel 路径
generated_excel_path = "wind_farm_design.xlsx"
if state.get("original_filename"):
if state.get("excel_path"):
dir_name = os.path.dirname(state["excel_path"])
generated_excel_path = os.path.join(
dir_name, f"{file_prefix}_design.xlsx"
)
generated_excel_path = main_excel_path
try:
with zipfile.ZipFile(
@@ -332,7 +345,7 @@ def index():
else:
# 尝试重新生成
try:
temp_excel = "temp_export.xlsx"
temp_excel = os.path.join(state["temp_dir"], "temp_export.xlsx")
export_all_scenarios_to_excel(
state["results"], temp_excel
)
@@ -350,7 +363,7 @@ def index():
if c.isalnum() or c in (" ", "-", "_")
]
).strip()
dxf_name = 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"],
@@ -367,6 +380,7 @@ def index():
except Exception as ex:
ui.notify(f"导出失败: {ex}", type="negative")
ui.button("导出全部方案 DXF (ZIP)", on_click=export_all_dxf).props(
"icon=folder_zip color=secondary"
)
@@ -611,10 +625,18 @@ def index():
).props("icon=file_download outline color=primary")
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
).classes("w-full mb-2")
# refs['current_file_label'] = ui.label('未选择文件').classes('text-xs text-gray-500 mb-4 italic')
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["run_btn"] = (
ui.button(
@@ -679,14 +701,15 @@ def index():
refs["export_row"] = ui.row().classes("gap-4")
def find_available_port(start_port=8080, max_attempts=10):
def find_available_port(start_port=8080, max_attempts=100):
"""尝试寻找可用的端口"""
import socket
for port in range(start_port, start_port + max_attempts):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind(("0.0.0.0", port))
# 尝试绑定到 127.0.0.1,这是最常用的本地开发地址
s.bind(("127.0.0.1", port))
return port
except OSError:
continue
@@ -694,15 +717,33 @@ def find_available_port(start_port=8080, max_attempts=10):
# 自动寻找可用端口,避免端口冲突
target_port = find_available_port(8080)
target_port = find_available_port(8082) # 从 8082 开始,避开常用的 8080
# 检测是否为打包后的exe程序
import sys
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
if sys.stdout is None:
sys.stdout = NullWriter()
if sys.stderr is None:
sys.stderr = NullWriter()
# 打包环境下禁用日志配置,避免在无控制台模式下出现错误
import logging
logging.disable(logging.CRITICAL)
ui.run(title="海上风电场集电线路优化", port=target_port, log_level=None)
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)
else:
# 普通使用环境保留日志功能
ui.run(title="海上风电场集电线路优化", port=target_port)
# 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)

View File

@@ -1051,10 +1051,10 @@ def visualize_design(turbines, substation, connections, title, ax=None, show_cos
legend_handles[section] = line
# 打印统计信息
if is_detailed:
print(f"[{title.splitlines()[0]}] 电缆统计:")
for section in sorted(cable_counts.keys()):
print(f" {section}mm²: {cable_counts[section]}")
# if is_detailed:
# print(f"[{title.splitlines()[0]}] 电缆统计:")
# for section in sorted(cable_counts.keys()):
# print(f" {section}mm²: {cable_counts[section]} 条")
# 设置图形属性
ax.set_title(title, fontsize=10)