feat: 完善经济性分析功能和优化界面显示
- 新增工程运行期限、折现率、年损耗小时数参数配置 - 实现总费用计算功能(包含电缆投资NPV和电费损耗NPV) - 修复total_investment函数调用时机问题,确保GUI模式正确计算 - 优化电缆单价显示为万元/km单位 - 总长度显示单位改为公里 - 方案对比结果新增总费用列,支持全生命周期成本比选 - 代码格式化和导入顺序优化 - 添加IFLOW.md项目上下文文档
This commit is contained in:
193
gui.py
193
gui.py
@@ -1,20 +1,22 @@
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import io
|
||||
import contextlib
|
||||
import tempfile
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
import matplotlib.backends.backend_svg
|
||||
from nicegui import ui, events, app
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
from nicegui import app, events, ui
|
||||
|
||||
from main import (
|
||||
compare_design_methods,
|
||||
export_to_dxf,
|
||||
load_data_from_excel,
|
||||
generate_wind_farm_data,
|
||||
visualize_design,
|
||||
export_all_scenarios_to_excel,
|
||||
export_to_dxf,
|
||||
generate_wind_farm_data,
|
||||
load_data_from_excel,
|
||||
visualize_design,
|
||||
)
|
||||
import pandas as pd
|
||||
|
||||
# 尝试导入自动生成的版本号
|
||||
try:
|
||||
@@ -108,7 +110,7 @@ def index():
|
||||
v = state["system_params"]["voltage"]
|
||||
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"
|
||||
if is_default_v:
|
||||
v_str += " (默认)"
|
||||
params_text.append(v_str)
|
||||
@@ -143,6 +145,51 @@ def index():
|
||||
ep_str += " (默认)"
|
||||
params_text.append(ep_str)
|
||||
|
||||
# 获取工程运行期限
|
||||
lifetime = 25 # Default
|
||||
is_default_lifetime = True
|
||||
if (
|
||||
state.get("system_params")
|
||||
and "project_lifetime" in state["system_params"]
|
||||
):
|
||||
lifetime = state["system_params"]["project_lifetime"]
|
||||
is_default_lifetime = False
|
||||
|
||||
lifetime_str = f"工程运行期限: {lifetime} 年"
|
||||
if is_default_lifetime:
|
||||
lifetime_str += " (默认)"
|
||||
params_text.append(lifetime_str)
|
||||
|
||||
# 获取折现率
|
||||
discount_rate = 8 # Default
|
||||
is_default_discount = True
|
||||
if (
|
||||
state.get("system_params")
|
||||
and "discount_rate" in state["system_params"]
|
||||
):
|
||||
discount_rate = state["system_params"]["discount_rate"]
|
||||
is_default_discount = False
|
||||
|
||||
discount_str = f"折现率: {discount_rate}%"
|
||||
if is_default_discount:
|
||||
discount_str += " (默认)"
|
||||
params_text.append(discount_str)
|
||||
|
||||
# 获取年损耗小时数
|
||||
annual_hours = 1400 # Default
|
||||
is_default_hours = True
|
||||
if (
|
||||
state.get("system_params")
|
||||
and "annual_loss_hours" in state["system_params"]
|
||||
):
|
||||
annual_hours = state["system_params"]["annual_loss_hours"]
|
||||
is_default_hours = False
|
||||
|
||||
hours_str = f"年损耗小时数: {annual_hours} 小时"
|
||||
if is_default_hours:
|
||||
hours_str += " (默认)"
|
||||
params_text.append(hours_str)
|
||||
|
||||
for p in params_text:
|
||||
ui.chip(p, icon="bolt").props("outline color=primary")
|
||||
|
||||
@@ -175,7 +222,7 @@ def index():
|
||||
},
|
||||
{
|
||||
"name": "cost",
|
||||
"label": "参考单价 (元/m)",
|
||||
"label": "参考单价(万元/km)",
|
||||
"field": "cost",
|
||||
"align": "center",
|
||||
},
|
||||
@@ -194,7 +241,9 @@ def index():
|
||||
"section": spec[0],
|
||||
"capacity": spec[1],
|
||||
"resistance": spec[2],
|
||||
"cost": spec[3],
|
||||
"cost": f"{spec[3] / 10:.2f}"
|
||||
if spec[3] is not None
|
||||
else "0.00",
|
||||
"is_optional": "Y" if len(spec) > 4 and spec[4] else "",
|
||||
}
|
||||
)
|
||||
@@ -374,6 +423,7 @@ def index():
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
|
||||
from nicegui import run
|
||||
|
||||
print("DEBUG: Attempting Tkinter dialog...")
|
||||
@@ -464,7 +514,6 @@ def index():
|
||||
best_res = min(state["results"], key=lambda x: x["cost"])
|
||||
|
||||
with refs["export_row"]:
|
||||
|
||||
# --- 下载 Excel ---
|
||||
async def save_excel(path):
|
||||
import shutil
|
||||
@@ -547,7 +596,7 @@ def index():
|
||||
ui.notify("缺少升压站数据,无法导出 DXF", type="negative")
|
||||
|
||||
ui.button(
|
||||
f'导出推荐方案 DXF ({best_res["name"]})', on_click=on_click_best_dxf
|
||||
f"导出推荐方案 DXF ({best_res['name']})", on_click=on_click_best_dxf
|
||||
).props("icon=architecture color=accent")
|
||||
|
||||
# --- 导出选中方案 DXF ---
|
||||
@@ -654,7 +703,7 @@ def index():
|
||||
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"
|
||||
title = f"{result['name']}\nCost: ¥{result['cost'] / 10000:.2f}万 | Loss: {result['loss']:.2f} kW"
|
||||
# 显式获取当前 ui.pyplot 创建的 axes,并传递给绘图函数
|
||||
# 确保绘图发生在正确的 figure 上
|
||||
ax = plt.gca()
|
||||
@@ -694,9 +743,10 @@ def index():
|
||||
clean_name = row_name.replace("(推荐) ", "")
|
||||
refs["export_selected_btn"].set_text(f"导出选中方案 ({clean_name})")
|
||||
|
||||
from nicegui import run
|
||||
import queue
|
||||
|
||||
from nicegui import run
|
||||
|
||||
async def run_analysis():
|
||||
if not state["excel_path"]:
|
||||
ui.notify("请先上传 Excel 坐标文件!", type="warning")
|
||||
@@ -722,9 +772,9 @@ def index():
|
||||
if msg.startswith("--- Scenario"):
|
||||
scenario_name = msg.replace("---", "").strip()
|
||||
if refs["status_label"]:
|
||||
refs["status_label"].text = (
|
||||
f"正在计算: {scenario_name}..."
|
||||
)
|
||||
refs[
|
||||
"status_label"
|
||||
].text = f"正在计算: {scenario_name}..."
|
||||
elif "开始比较电缆方案" in msg:
|
||||
if refs["status_label"]:
|
||||
refs["status_label"].text = "准备开始计算..."
|
||||
@@ -777,7 +827,7 @@ def index():
|
||||
|
||||
update_plot(best_res)
|
||||
ui.notify(
|
||||
f'计算完成!已自动加载推荐方案: {best_res["name"]}', type="positive"
|
||||
f"计算完成!已自动加载推荐方案: {best_res['name']}", type="positive"
|
||||
)
|
||||
|
||||
# 更新结果表格
|
||||
@@ -813,14 +863,16 @@ def index():
|
||||
elif "No Max" in original_name:
|
||||
note += "不包含可选电缆型号,且可使用的最大截面电缆降一档截面。"
|
||||
|
||||
# 计算总长度
|
||||
total_length = sum(d["length"] for d in res["eval"]["details"])
|
||||
# 计算总长度(转换为公里)
|
||||
total_length_m = sum(d["length"] for d in res["eval"]["details"])
|
||||
total_length_km = total_length_m / 1000
|
||||
|
||||
row_dict = {
|
||||
"name": name_display,
|
||||
"cost_wan": f"{res['cost'] / 10000:.2f}",
|
||||
"loss_kw": f"{res['loss']:.2f}",
|
||||
"total_length": f"{total_length:.2f}",
|
||||
"total_length": f"{total_length_km:.2f}",
|
||||
"total_cost_npv_wan": f"{res.get('total_cost_npv', res['cost']) / 10000:.2f}",
|
||||
"note": note,
|
||||
"original_name": res["name"],
|
||||
}
|
||||
@@ -879,16 +931,20 @@ def index():
|
||||
with ui.row().classes("w-full p-4 gap-4"):
|
||||
with ui.card().classes("w-full p-4 shadow-md"):
|
||||
ui.label("配置面板").classes("text-xl font-semibold mb-4 border-b pb-2")
|
||||
|
||||
|
||||
# 使用 items-stretch 确保所有子元素高度一致
|
||||
with ui.row().classes('w-full items-stretch gap-4'):
|
||||
with ui.row().classes("w-full items-stretch gap-4"):
|
||||
# 1. 导出模板按钮
|
||||
async def export_template():
|
||||
from generate_template import create_template
|
||||
import shutil
|
||||
|
||||
from generate_template import create_template
|
||||
|
||||
async def save_template(path):
|
||||
# 生成模板到系统临时目录
|
||||
temp_template = os.path.join(state["temp_dir"], "coordinates_template.xlsx")
|
||||
temp_template = os.path.join(
|
||||
state["temp_dir"], "coordinates_template.xlsx"
|
||||
)
|
||||
create_template(temp_template)
|
||||
if os.path.exists(temp_template):
|
||||
shutil.copy2(temp_template, path)
|
||||
@@ -900,9 +956,7 @@ def index():
|
||||
raise FileNotFoundError("无法生成模板文件")
|
||||
|
||||
await save_file_with_dialog(
|
||||
"coordinates.xlsx",
|
||||
save_template,
|
||||
"Excel Files (*.xlsx)"
|
||||
"coordinates.xlsx", save_template, "Excel Files (*.xlsx)"
|
||||
)
|
||||
|
||||
ui.button("导出 Excel 模板", on_click=export_template).classes(
|
||||
@@ -910,12 +964,18 @@ def index():
|
||||
).props("icon=file_download outline color=primary")
|
||||
|
||||
# 2. 上传文件区域 (垂直堆叠 Label 和 Upload 组件)
|
||||
with ui.column().classes('flex-1 gap-0 justify-between'):
|
||||
with ui.column().classes("flex-1 gap-0 justify-between"):
|
||||
# 使用 .no-list CSS 隐藏 Quasar 默认列表,完全自定义文件显示
|
||||
refs["upload_widget"] = ui.upload(
|
||||
label="选择Excel文件", on_upload=handle_upload, auto_upload=True
|
||||
).classes("w-full no-list h-full").props('flat bordered color=primary')
|
||||
|
||||
refs["upload_widget"] = (
|
||||
ui.upload(
|
||||
label="选择Excel文件",
|
||||
on_upload=handle_upload,
|
||||
auto_upload=True,
|
||||
)
|
||||
.classes("w-full no-list h-full")
|
||||
.props("flat bordered color=primary")
|
||||
)
|
||||
|
||||
# 自定义文件显示容器
|
||||
refs["current_file_container"] = ui.column().classes("w-full")
|
||||
# 初始状态不显示任何内容,直到选择文件后才显示
|
||||
@@ -946,9 +1006,15 @@ def index():
|
||||
)
|
||||
|
||||
with ui.card().classes("w-full p-0 shadow-md overflow-hidden"):
|
||||
with ui.expansion(
|
||||
"方案对比结果 (点击行查看拓扑详情)", icon="analytics", value=True
|
||||
).classes("w-full").props("header-class=\"text-xl font-semibold\""):
|
||||
with (
|
||||
ui.expansion(
|
||||
"方案对比结果 (点击行查看拓扑详情)",
|
||||
icon="analytics",
|
||||
value=True,
|
||||
)
|
||||
.classes("w-full")
|
||||
.props('header-class="text-xl font-semibold"')
|
||||
):
|
||||
columns = [
|
||||
{
|
||||
"name": "name",
|
||||
@@ -963,24 +1029,31 @@ def index():
|
||||
"field": "cost_wan",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "loss_kw",
|
||||
"label": "线损 (kW)",
|
||||
"field": "loss_kw",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "total_length",
|
||||
"label": "总长度 (m)",
|
||||
"field": "total_length",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "note",
|
||||
"label": "备注",
|
||||
"field": "note",
|
||||
"align": "left",
|
||||
}, ]
|
||||
{
|
||||
"name": "loss_kw",
|
||||
"label": "线损 (kW)",
|
||||
"field": "loss_kw",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "total_length",
|
||||
"label": "总长度/km",
|
||||
"field": "total_length",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "total_cost_npv_wan",
|
||||
"label": "总费用 (万元)",
|
||||
"field": "total_cost_npv_wan",
|
||||
"sortable": True,
|
||||
},
|
||||
{
|
||||
"name": "note",
|
||||
"label": "备注",
|
||||
"field": "note",
|
||||
"align": "left",
|
||||
},
|
||||
]
|
||||
# 使用内置的 selection='single' 结合行点击事件实现背景高亮
|
||||
# 这样可以完全由 Python 事件逻辑控制,不依赖 CSS 伪类
|
||||
refs["results_table"] = ui.table(
|
||||
@@ -1055,7 +1128,13 @@ if getattr(sys, "frozen", False):
|
||||
)
|
||||
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",
|
||||
|
||||
Reference in New Issue
Block a user