From c2740c2bded64e5c7a0163f49bb984b981020bbf Mon Sep 17 00:00:00 2001 From: DXC Date: Wed, 17 Jun 2026 14:06:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(step7):=20=E4=BF=AE=E5=A4=8D=20Step7=20?= =?UTF-8?q?=E6=9C=AA=E9=BB=98=E8=AE=A4=E5=8A=A0=E8=BD=BD=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E6=B0=B4=E8=B4=A8=E6=8C=87=E6=95=B0=E5=85=AC=E5=BC=8F=E8=A1=A8?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 全部保留 --- src/new/views/step7_view.py | 205 +++++++++++++++++++++++++++++++----- 1 file changed, 181 insertions(+), 24 deletions(-) diff --git a/src/new/views/step7_view.py b/src/new/views/step7_view.py index 48c69ff..5e5230f 100644 --- a/src/new/views/step7_view.py +++ b/src/new/views/step7_view.py @@ -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"])