fix(step7): 修复 Step7 未默认加载内置水质指数公式表的问题
- init_ui 末尾自动调用 _auto_load_formulas 读取 src/gui/model/waterindex.csv 并填充 formula_list(默认全选),用户进 tab 即可见/可勾选公式。 - 新增 _resolve_formula_csv_path helper:兼容 PyInstaller _MEIPASS / frozen _internal / 开发环境(__file__ 倒推 3 层到 src/)三种位置。 - get_config 新增 formula_csv_file 字段——之前缺失导致 step7_service 拿到空路径必报 "未提供公式 CSV 路径(formula_csv_file)" 错误, 进而导致下游 step8/9 因为拿不到 features 而生成 0 个模型。 - 新增 4 个 _on_select_* 按钮回调(_auto_load_formulas 后启用); set_config 同步兼容 formula_csv_file 键。 - 删除未使用的 _resolve_subdir helper(早前从旧 panel 搬过来但新 view 未引用)。 验证: AST 解析 320 行 OK helper 解析到 D:\...\src\gui\model\waterindex.csv exists=True pandas 解析 CSV 63 行(45 ratio + 18 concentration)OK BaseView 契约方法 init_ui/get_config/set_config/update_work_directory/ _on_run_clicked 全部保留
This commit is contained in:
@ -7,22 +7,27 @@ UI 从 ``src/gui/panels/step7_index_panel.py`` 原样搬迁。
|
||||
view 层职责
|
||||
===========
|
||||
|
||||
- 公式 ListWidget 仅做占位(默认空),由 service 在真正执行时通过
|
||||
``set_config({"formula_names": [...])}`` 把勾选项回填。
|
||||
- 内置 ``waterindex.csv`` 路径只读展示,不在 view 层做实际加载;
|
||||
CSV 解析、formula_type / color / coef 三张映射表都属于 service。
|
||||
- 全选 / 仅选比值型 / 仅选浓度型按钮的 ``itemChanged`` 信号
|
||||
在 view 层禁用(``self.formula_list.blockSignals(True)``),
|
||||
避免无数据时触发空回调。
|
||||
- 公式 ListWidget 渲染:view 在 init_ui 末尾主动调用
|
||||
``_auto_load_formulas()`` 读取内置 ``waterindex.csv``,
|
||||
填充 ListWidget 项并默认全选(与旧 panel 行为一致)。
|
||||
- 内置 ``waterindex.csv`` 路径在 view 层自动解析(兼容开发 + PyInstaller),
|
||||
set_path 后即触发加载逻辑;CSV 解析 / formula_type / color / coef
|
||||
三张映射表属于 view 层(与旧 panel 对齐),service 层独立解析同一 CSV 用于执行。
|
||||
- 全选 / 仅选比值型 / 仅选浓度型按钮在公式加载完毕后启用。
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtGui import QBrush, QColor, QFont
|
||||
from PyQt5.QtWidgets import (
|
||||
QCheckBox, QGroupBox, QHBoxLayout, QLabel, QListWidget,
|
||||
QListWidgetItem, QPushButton, QVBoxLayout,
|
||||
QListWidgetItem, QMessageBox, QPushButton, QVBoxLayout,
|
||||
)
|
||||
|
||||
from src.gui.components.custom_widgets import FileSelectWidget
|
||||
@ -30,8 +35,35 @@ from src.gui.styles import ModernStylesheet
|
||||
from src.new.core.base_view import BaseView
|
||||
|
||||
|
||||
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||
_COLOR_RATIO = QColor(255, 255, 255)
|
||||
_COLOR_CONCENTRATION = QColor(220, 240, 255)
|
||||
|
||||
|
||||
def _resolve_formula_csv_path(relative_path: str = "waterindex.csv") -> str:
|
||||
"""按优先级解析内置公式 CSV 的绝对路径
|
||||
|
||||
顺序:
|
||||
1. ``sys._MEIPASS`` / ``_internal`` —— PyInstaller 打包环境
|
||||
2. ``sys.executable`` 旁的 ``_internal`` —— frozen 单文件目录模式
|
||||
3. ``Path(__file__).resolve().parent.parent.parent / "gui" / "model"`` —— 开发环境
|
||||
(绝对路径 = 项目根 / src / gui / model / waterindex.csv;
|
||||
``__file__`` = ``src/new/views/step7_view.py``,跳 3 层 = ``src/``)
|
||||
返回首个存在的绝对路径;若全部不存在,返回第 3 步的解析结果,
|
||||
让上层 ``_auto_load_formulas`` 走失败分支而不是抛异常。
|
||||
"""
|
||||
candidates = []
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
candidates.append(os.path.join(sys._MEIPASS, "_internal", relative_path))
|
||||
candidates.append(os.path.join(sys._MEIPASS, relative_path))
|
||||
exe_dir = os.path.dirname(sys.executable)
|
||||
candidates.append(os.path.join(exe_dir, "_internal", relative_path))
|
||||
dev_path = Path(__file__).resolve().parent.parent.parent / "gui" / "model" / relative_path
|
||||
candidates.append(str(dev_path))
|
||||
|
||||
for p in candidates:
|
||||
if p and os.path.exists(p):
|
||||
return p
|
||||
return str(dev_path)
|
||||
|
||||
|
||||
class Step7View(BaseView):
|
||||
@ -45,10 +77,18 @@ class Step7View(BaseView):
|
||||
title.setFont(QFont("Arial", 12, QFont.Bold))
|
||||
layout.addWidget(title)
|
||||
|
||||
# 1. 公式配置源(只读)
|
||||
# 0. 解析默认公式 CSV 路径(一次性)
|
||||
self.formula_csv_path: str = _resolve_formula_csv_path("waterindex.csv")
|
||||
self.index_checkboxes: Dict[str, QListWidgetItem] = {}
|
||||
self._formula_type_map: Dict[str, str] = {}
|
||||
self._formula_color_map: Dict[str, QColor] = {}
|
||||
self._formula_coef_map: Dict[str, List[float]] = {}
|
||||
|
||||
# 1. 公式配置源(只读展示,自动默认填入内置 CSV)
|
||||
path_group = QGroupBox("公式配置源 (内置)")
|
||||
path_layout = QVBoxLayout()
|
||||
self.formula_csv_widget = FileSelectWidget("内置CSV路径:", "CSV Files (*.csv)")
|
||||
self.formula_csv_widget.set_path(self.formula_csv_path)
|
||||
self.formula_csv_widget.set_read_only(True)
|
||||
self.formula_csv_widget.line_edit.setStyleSheet("background-color: #f0f0f0; color: #666;")
|
||||
path_layout.addWidget(self.formula_csv_widget)
|
||||
@ -73,7 +113,7 @@ class Step7View(BaseView):
|
||||
self.select_ratio_btn = QPushButton("仅选比值型")
|
||||
self.select_conc_btn = QPushButton("仅选浓度型")
|
||||
self.refresh_button = QPushButton("重新加载")
|
||||
# 注意:按钮在 view 层仅做占位 —— 真正生效需要 service 加载 CSV 后回填
|
||||
# 按钮初始禁用,等 _auto_load_formulas 加载完毕启用
|
||||
for btn in (self.select_all_btn, self.deselect_all_btn,
|
||||
self.select_ratio_btn, self.select_conc_btn, self.refresh_button):
|
||||
btn.setEnabled(False)
|
||||
@ -115,19 +155,134 @@ class Step7View(BaseView):
|
||||
layout.addStretch()
|
||||
self.setLayout(layout)
|
||||
|
||||
# 6. 选择按钮回调(在 _auto_load_formulas 之前绑定,load 完后启用)
|
||||
self.select_all_btn.clicked.connect(self._on_select_all)
|
||||
self.deselect_all_btn.clicked.connect(self._on_deselect_all)
|
||||
self.select_ratio_btn.clicked.connect(self._on_select_ratio_only)
|
||||
self.select_conc_btn.clicked.connect(self._on_select_conc_only)
|
||||
self.refresh_button.clicked.connect(lambda: self._refresh_formulas(silent=False))
|
||||
|
||||
# 7. 自动加载公式(兜底渲染;service 会在执行前独立校验)
|
||||
self._auto_load_formulas()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 公式加载逻辑(与旧 panel step7_index_panel 行为对齐)
|
||||
# ------------------------------------------------------------------
|
||||
def _auto_load_formulas(self):
|
||||
"""init_ui 末尾自动调用:若内置 CSV 存在则填充 ListWidget"""
|
||||
if os.path.exists(self.formula_csv_path):
|
||||
self._refresh_formulas(silent=True)
|
||||
else:
|
||||
print(f"[Step7View] 内置公式表未找到,跳过自动加载: {self.formula_csv_path}")
|
||||
|
||||
def _refresh_formulas(self, silent: bool = True):
|
||||
"""读取 waterindex.csv → 填充 ListWidget + 三张映射表"""
|
||||
path = self.formula_csv_path
|
||||
if not os.path.exists(path):
|
||||
if not silent:
|
||||
QMessageBox.warning(self, "错误", f"找不到内置公式文件:\n{path}")
|
||||
return
|
||||
|
||||
try:
|
||||
df: Optional[pd.DataFrame] = None
|
||||
for enc in ("utf-8", "gbk", "utf-8-sig"):
|
||||
try:
|
||||
df = pd.read_csv(path, encoding=enc)
|
||||
if "Formula_Name" in df.columns:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if df is None or "Formula_Name" not in df.columns:
|
||||
if not silent:
|
||||
QMessageBox.critical(self, "错误", "CSV 缺少 'Formula_Name' 列")
|
||||
return
|
||||
|
||||
self._formula_type_map.clear()
|
||||
self._formula_coef_map.clear()
|
||||
for _, row in df.iterrows():
|
||||
name = str(row["Formula_Name"]).strip()
|
||||
if not name:
|
||||
continue
|
||||
ftype = str(row.get("Formula_Type", "ratio")).strip().lower()
|
||||
self._formula_type_map[name] = ftype
|
||||
|
||||
coef_str = str(row.get("Coefficient", "")).strip()
|
||||
if coef_str:
|
||||
try:
|
||||
self._formula_coef_map[name] = [
|
||||
float(c.strip()) for c in coef_str.split(",") if c.strip()
|
||||
]
|
||||
except Exception:
|
||||
self._formula_coef_map[name] = []
|
||||
else:
|
||||
self._formula_coef_map[name] = []
|
||||
|
||||
self.formula_list.blockSignals(True)
|
||||
self.formula_list.clear()
|
||||
self.index_checkboxes.clear()
|
||||
self._formula_color_map.clear()
|
||||
|
||||
for name, ftype in self._formula_type_map.items():
|
||||
item = QListWidgetItem(name, self.formula_list)
|
||||
item.setCheckState(Qt.Checked)
|
||||
bg = _COLOR_CONCENTRATION if ftype == "concentration" else _COLOR_RATIO
|
||||
self._formula_color_map[name] = bg
|
||||
item.setBackground(QBrush(bg))
|
||||
self.index_checkboxes[name] = item
|
||||
self.formula_list.blockSignals(False)
|
||||
self.formula_list.adjustSize()
|
||||
|
||||
for btn in (self.select_all_btn, self.deselect_all_btn,
|
||||
self.select_ratio_btn, self.select_conc_btn, self.refresh_button):
|
||||
btn.setEnabled(True)
|
||||
|
||||
print(f"[Step7View] ✅ 加载 {len(self.index_checkboxes)} 个公式(来自 {os.path.basename(path)})")
|
||||
except Exception as e:
|
||||
if not silent:
|
||||
QMessageBox.critical(self, "加载失败", f"原因: {e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 选择按钮回调
|
||||
# ------------------------------------------------------------------
|
||||
def _on_select_all(self):
|
||||
self.formula_list.blockSignals(True)
|
||||
for item in self.index_checkboxes.values():
|
||||
item.setCheckState(Qt.Checked)
|
||||
self.formula_list.blockSignals(False)
|
||||
|
||||
def _on_deselect_all(self):
|
||||
self.formula_list.blockSignals(True)
|
||||
for item in self.index_checkboxes.values():
|
||||
item.setCheckState(Qt.Unchecked)
|
||||
self.formula_list.blockSignals(False)
|
||||
|
||||
def _on_select_ratio_only(self):
|
||||
self.formula_list.blockSignals(True)
|
||||
for name, item in self.index_checkboxes.items():
|
||||
ftype = self._formula_type_map.get(name, "ratio")
|
||||
item.setCheckState(Qt.Checked if ftype == "ratio" else Qt.Unchecked)
|
||||
self.formula_list.blockSignals(False)
|
||||
|
||||
def _on_select_conc_only(self):
|
||||
self.formula_list.blockSignals(True)
|
||||
for name, item in self.index_checkboxes.items():
|
||||
ftype = self._formula_type_map.get(name, "ratio")
|
||||
item.setCheckState(Qt.Checked if ftype == "concentration" else Qt.Unchecked)
|
||||
self.formula_list.blockSignals(False)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# BaseView 契约
|
||||
# ------------------------------------------------------------------
|
||||
def get_config(self) -> dict:
|
||||
"""读取当前 UI 状态——formula_names 由 ListWidget 勾选项收集"""
|
||||
selected = []
|
||||
for index in range(self.formula_list.count()):
|
||||
item = self.formula_list.item(index)
|
||||
if item.checkState() == Qt.Checked:
|
||||
selected.append(item.text())
|
||||
selected = [
|
||||
name for name, item in self.index_checkboxes.items()
|
||||
if item.checkState() == Qt.Checked
|
||||
]
|
||||
|
||||
config = {
|
||||
"training_csv_path": self.training_data_widget.get_path(),
|
||||
"formula_csv_file": self.formula_csv_widget.get_path(),
|
||||
"formula_names": selected,
|
||||
"enabled": self.enable_checkbox.isChecked(),
|
||||
"output_mode": 0,
|
||||
@ -138,13 +293,15 @@ class Step7View(BaseView):
|
||||
"""根据 config 恢复 UI 状态——formula_names 直接回填 ListWidget"""
|
||||
if config.get("training_csv_path"):
|
||||
self.training_data_widget.set_path(config["training_csv_path"])
|
||||
if "formula_names" in config:
|
||||
# 用 blockSignals 避免 setCheckState 触发副作用
|
||||
self.formula_list.blockSignals(True)
|
||||
if config.get("formula_csv_file"):
|
||||
# 上游 main_view 推过来的 CSV 路径(一般不会发生,保留兼容)
|
||||
self.formula_csv_widget.set_path(config["formula_csv_file"])
|
||||
self.formula_csv_path = config["formula_csv_file"]
|
||||
if "formula_names" in config and self.index_checkboxes:
|
||||
sel = set(config["formula_names"])
|
||||
for index in range(self.formula_list.count()):
|
||||
item = self.formula_list.item(index)
|
||||
item.setCheckState(Qt.Checked if item.text() in sel else Qt.Unchecked)
|
||||
self.formula_list.blockSignals(True)
|
||||
for name, item in self.index_checkboxes.items():
|
||||
item.setCheckState(Qt.Checked if name in sel else Qt.Unchecked)
|
||||
self.formula_list.blockSignals(False)
|
||||
if "enabled" in config:
|
||||
self.enable_checkbox.setChecked(config["enabled"])
|
||||
|
||||
Reference in New Issue
Block a user