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 层职责
|
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"])
|
||||||
|
|||||||
Reference in New Issue
Block a user