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:
DXC
2026-06-17 14:06:15 +08:00
parent b3a6855881
commit c2740c2bde

View File

@ -7,22 +7,27 @@ UI 从 ``src/gui/panels/step7_index_panel.py`` 原样搬迁。
view 层职责 view 层职责
=========== ===========
- 公式 ListWidget 仅做占位(默认空),由 service 在真正执行时通过 - 公式 ListWidget 渲染view 在 init_ui 末尾主动调用
``set_config({"formula_names": [...])}`` 把勾选项回填。 ``_auto_load_formulas()`` 读取内置 ``waterindex.csv``
- 内置 ``waterindex.csv`` 路径只读展示,不在 view 层做实际加载; 填充 ListWidget 项并默认全选(与旧 panel 行为一致)。
CSV 解析、formula_type / color / coef 三张映射表都属于 service。 - 内置 ``waterindex.csv`` 路径在 view 层自动解析(兼容开发 + PyInstaller
- 全选 / 仅选比值型 / 仅选浓度型按钮的 ``itemChanged`` 信号 set_path 后即触发加载逻辑CSV 解析 / formula_type / color / coef
在 view 层禁用(``self.formula_list.blockSignals(True)`` 三张映射表属于 view 层(与旧 panel 对齐service 层独立解析同一 CSV 用于执行。
避免无数据时触发空回调 - 全选 / 仅选比值型 / 仅选浓度型按钮在公式加载完毕后启用
""" """
import os 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.QtCore import Qt
from PyQt5.QtGui import QFont from PyQt5.QtGui import QBrush, QColor, QFont
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QCheckBox, QGroupBox, QHBoxLayout, QLabel, QListWidget, QCheckBox, QGroupBox, QHBoxLayout, QLabel, QListWidget,
QListWidgetItem, QPushButton, QVBoxLayout, QListWidgetItem, QMessageBox, QPushButton, QVBoxLayout,
) )
from src.gui.components.custom_widgets import FileSelectWidget 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 from src.new.core.base_view import BaseView
def _resolve_subdir(work_dir: str, subdir_name: str) -> str: _COLOR_RATIO = QColor(255, 255, 255)
return os.path.join(work_dir, subdir_name).replace("\\", "/") _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): class Step7View(BaseView):
@ -45,10 +77,18 @@ class Step7View(BaseView):
title.setFont(QFont("Arial", 12, QFont.Bold)) title.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(title) 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_group = QGroupBox("公式配置源 (内置)")
path_layout = QVBoxLayout() path_layout = QVBoxLayout()
self.formula_csv_widget = FileSelectWidget("内置CSV路径:", "CSV Files (*.csv)") 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.set_read_only(True)
self.formula_csv_widget.line_edit.setStyleSheet("background-color: #f0f0f0; color: #666;") self.formula_csv_widget.line_edit.setStyleSheet("background-color: #f0f0f0; color: #666;")
path_layout.addWidget(self.formula_csv_widget) path_layout.addWidget(self.formula_csv_widget)
@ -73,7 +113,7 @@ class Step7View(BaseView):
self.select_ratio_btn = QPushButton("仅选比值型") self.select_ratio_btn = QPushButton("仅选比值型")
self.select_conc_btn = QPushButton("仅选浓度型") self.select_conc_btn = QPushButton("仅选浓度型")
self.refresh_button = QPushButton("重新加载") self.refresh_button = QPushButton("重新加载")
# 注意:按钮在 view 层仅做占位 —— 真正生效需要 service 加载 CSV 后回填 # 按钮初始禁用,等 _auto_load_formulas 加载完毕启用
for btn in (self.select_all_btn, self.deselect_all_btn, for btn in (self.select_all_btn, self.deselect_all_btn,
self.select_ratio_btn, self.select_conc_btn, self.refresh_button): self.select_ratio_btn, self.select_conc_btn, self.refresh_button):
btn.setEnabled(False) btn.setEnabled(False)
@ -115,19 +155,134 @@ class Step7View(BaseView):
layout.addStretch() layout.addStretch()
self.setLayout(layout) 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 契约 # BaseView 契约
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def get_config(self) -> dict: def get_config(self) -> dict:
"""读取当前 UI 状态——formula_names 由 ListWidget 勾选项收集""" """读取当前 UI 状态——formula_names 由 ListWidget 勾选项收集"""
selected = [] selected = [
for index in range(self.formula_list.count()): name for name, item in self.index_checkboxes.items()
item = self.formula_list.item(index) if item.checkState() == Qt.Checked
if item.checkState() == Qt.Checked: ]
selected.append(item.text())
config = { config = {
"training_csv_path": self.training_data_widget.get_path(), "training_csv_path": self.training_data_widget.get_path(),
"formula_csv_file": self.formula_csv_widget.get_path(),
"formula_names": selected, "formula_names": selected,
"enabled": self.enable_checkbox.isChecked(), "enabled": self.enable_checkbox.isChecked(),
"output_mode": 0, "output_mode": 0,
@ -138,13 +293,15 @@ class Step7View(BaseView):
"""根据 config 恢复 UI 状态——formula_names 直接回填 ListWidget""" """根据 config 恢复 UI 状态——formula_names 直接回填 ListWidget"""
if config.get("training_csv_path"): if config.get("training_csv_path"):
self.training_data_widget.set_path(config["training_csv_path"]) self.training_data_widget.set_path(config["training_csv_path"])
if "formula_names" in config: if config.get("formula_csv_file"):
# 用 blockSignals 避免 setCheckState 触发副作用 # 上游 main_view 推过来的 CSV 路径(一般不会发生,保留兼容)
self.formula_list.blockSignals(True) 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"]) sel = set(config["formula_names"])
for index in range(self.formula_list.count()): self.formula_list.blockSignals(True)
item = self.formula_list.item(index) for name, item in self.index_checkboxes.items():
item.setCheckState(Qt.Checked if item.text() in sel else Qt.Unchecked) item.setCheckState(Qt.Checked if name in sel else Qt.Unchecked)
self.formula_list.blockSignals(False) self.formula_list.blockSignals(False)
if "enabled" in config: if "enabled" in config:
self.enable_checkbox.setChecked(config["enabled"]) self.enable_checkbox.setChecked(config["enabled"])