2026-05-07 14:23:58 +08:00
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
|
|
|
|
|
ReportGenerationPanel - Word 分析报告生成面板
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
2026-06-16 12:54:18 +08:00
|
|
|
|
import sys
|
2026-05-07 14:23:58 +08:00
|
|
|
|
import traceback
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
2026-06-16 12:54:18 +08:00
|
|
|
|
# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里)
|
|
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
if _HERE not in sys.path:
|
|
|
|
|
|
sys.path.insert(0, _HERE)
|
|
|
|
|
|
from _step_path_resolver import resolve_subdir
|
|
|
|
|
|
|
2026-06-08 14:58:16 +08:00
|
|
|
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings
|
2026-05-07 14:23:58 +08:00
|
|
|
|
from PyQt5.QtWidgets import (
|
|
|
|
|
|
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
|
2026-06-08 14:58:16 +08:00
|
|
|
|
QLabel, QCheckBox, QPushButton, QLineEdit,
|
2026-06-23 14:39:54 +08:00
|
|
|
|
QMessageBox, QFileDialog, QProgressBar,
|
2026-05-07 14:23:58 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
from src.gui.styles import ModernStylesheet
|
2026-06-08 14:58:16 +08:00
|
|
|
|
from src.gui.dialogs import AISettingsDialog, AI_SETTINGS_ORG, AI_SETTINGS_APP
|
2026-05-07 14:23:58 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-06-23 14:39:54 +08:00
|
|
|
|
class ReportWorkerThread(QThread):
|
|
|
|
|
|
"""后台生成 Word 报告(避免阻塞 UI)。
|
|
|
|
|
|
三信号协议:
|
|
|
|
|
|
- progress(int, str):报告进度百分比 + 当前文案
|
|
|
|
|
|
- finished(str):生成的报告绝对路径
|
|
|
|
|
|
- error(str):异常信息(含 traceback)
|
|
|
|
|
|
"""
|
|
|
|
|
|
progress = pyqtSignal(int, str)
|
|
|
|
|
|
finished = pyqtSignal(str)
|
|
|
|
|
|
error = pyqtSignal(str)
|
2026-05-07 14:23:58 +08:00
|
|
|
|
|
2026-06-08 14:58:16 +08:00
|
|
|
|
def __init__(self, work_dir: str, output_dir: Optional[str], report_title: str, enable_ai: bool):
|
2026-05-07 14:23:58 +08:00
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.work_dir = work_dir
|
|
|
|
|
|
self.output_dir = output_dir
|
|
|
|
|
|
self.report_title = report_title
|
2026-06-08 14:58:16 +08:00
|
|
|
|
self.enable_ai = enable_ai
|
2026-05-07 14:23:58 +08:00
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
|
try:
|
|
|
|
|
|
from src.postprocessing.report_word import WaterQualityReportGenerator, ReportGenerationConfig
|
|
|
|
|
|
|
2026-06-08 14:58:16 +08:00
|
|
|
|
s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP)
|
|
|
|
|
|
provider = s.value("ai_provider", "minimax", type=str)
|
|
|
|
|
|
timeout = int(s.value("timeout_s", 120, type=int))
|
|
|
|
|
|
|
|
|
|
|
|
if provider == "ollama":
|
|
|
|
|
|
ai_cfg = ReportGenerationConfig(
|
|
|
|
|
|
ai_provider="ollama",
|
|
|
|
|
|
ollama_base_url=s.value("api_base_url", "", type=str) or None,
|
|
|
|
|
|
ollama_vision_model=s.value("vision_model", "", type=str) or None,
|
|
|
|
|
|
ollama_text_model=s.value("text_model", "", type=str) or None,
|
|
|
|
|
|
ollama_timeout_s=timeout,
|
|
|
|
|
|
enable_ai_analysis=self.enable_ai,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
ai_cfg = ReportGenerationConfig(
|
2026-06-25 18:11:50 +08:00
|
|
|
|
ai_provider=provider,
|
2026-06-08 14:58:16 +08:00
|
|
|
|
minimax_api_key=s.value("api_key", "", type=str) or "",
|
2026-06-25 18:11:50 +08:00
|
|
|
|
minimax_base_url=s.value("api_base_url", "", type=str) or None, # <--- 新增这行,把界面上的 URL 传过去
|
2026-06-08 14:58:16 +08:00
|
|
|
|
minimax_vision_model=s.value("vision_model", "", type=str) or None,
|
|
|
|
|
|
minimax_text_model=s.value("text_model", "", type=str) or None,
|
|
|
|
|
|
minimax_timeout_s=timeout,
|
|
|
|
|
|
enable_ai_analysis=self.enable_ai,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-07 14:23:58 +08:00
|
|
|
|
gen = WaterQualityReportGenerator(
|
|
|
|
|
|
work_dir=self.work_dir,
|
|
|
|
|
|
output_dir=self.output_dir,
|
|
|
|
|
|
ai_config=ai_cfg,
|
|
|
|
|
|
)
|
|
|
|
|
|
out_path = gen.generate_report(
|
|
|
|
|
|
work_dir=self.work_dir,
|
|
|
|
|
|
report_title=self.report_title or "水质参数反演分析报告",
|
2026-06-23 14:39:54 +08:00
|
|
|
|
on_progress=lambda pct, text: self.progress.emit(int(pct), str(text)),
|
2026-05-07 14:23:58 +08:00
|
|
|
|
)
|
2026-06-23 14:39:54 +08:00
|
|
|
|
self.finished.emit(str(out_path))
|
2026-05-07 14:23:58 +08:00
|
|
|
|
except Exception as e:
|
2026-06-23 14:39:54 +08:00
|
|
|
|
self.error.emit(f"{e}\n{traceback.format_exc()}")
|
2026-05-07 14:23:58 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-06-11 17:44:24 +08:00
|
|
|
|
class Step13ReportPanel(QWidget):
|
|
|
|
|
|
"""步骤13:分析报告生成。AI 配置统一由 AISettingsDialog 管理,本面板不持有配置状态。"""
|
2026-05-07 14:23:58 +08:00
|
|
|
|
|
|
|
|
|
|
def __init__(self, main_window=None, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.main_window = main_window
|
|
|
|
|
|
self._report_thread = None
|
2026-06-08 14:58:16 +08:00
|
|
|
|
self._ai_label = None
|
2026-05-07 14:23:58 +08:00
|
|
|
|
self.init_ui()
|
|
|
|
|
|
|
|
|
|
|
|
def init_ui(self):
|
2026-06-29 15:15:09 +08:00
|
|
|
|
# 1. 确保全局主题类被导入
|
|
|
|
|
|
from src.gui.styles import ModernStylesheet
|
|
|
|
|
|
|
2026-05-07 14:23:58 +08:00
|
|
|
|
layout = QVBoxLayout()
|
2026-06-29 15:15:09 +08:00
|
|
|
|
layout.setContentsMargins(24, 24, 24, 24)
|
|
|
|
|
|
layout.setSpacing(20)
|
|
|
|
|
|
|
|
|
|
|
|
# 重新定义全局绝美样式
|
|
|
|
|
|
common_lineedit_css = "QLineEdit { padding: 4px 8px; border: 1px solid #C0C0C0; border-radius: 4px; min-height: 24px; background-color: #FFFFFF; }"
|
|
|
|
|
|
common_btn_css = """
|
|
|
|
|
|
QPushButton { background-color: #F8F9FA; border: 1px solid #D1D5DB; border-radius: 4px; padding: 4px 12px; color: #374151; }
|
|
|
|
|
|
QPushButton:hover { background-color: #F3F4F6; border-color: #9CA3AF; }
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# 【核心黑科技】:定义标准行闭包,强制统一 120px 标签和 10px 间距
|
|
|
|
|
|
def create_standard_row(label_text, widget, btn=None):
|
|
|
|
|
|
row = QHBoxLayout()
|
|
|
|
|
|
row.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
row.setSpacing(10)
|
|
|
|
|
|
label = QLabel(label_text)
|
|
|
|
|
|
label.setMinimumWidth(120)
|
|
|
|
|
|
label.setMaximumWidth(120)
|
|
|
|
|
|
row.addWidget(label)
|
|
|
|
|
|
row.addWidget(widget, 1)
|
|
|
|
|
|
if btn:
|
|
|
|
|
|
row.addWidget(btn)
|
|
|
|
|
|
return row
|
2026-05-07 14:23:58 +08:00
|
|
|
|
|
|
|
|
|
|
intro = QLabel(
|
2026-06-29 15:15:09 +08:00
|
|
|
|
"💡 提示:根据工作目录下的可视化结果(14_visualization 等)自动生成 Word 分析报告。\n"
|
2026-06-08 14:58:16 +08:00
|
|
|
|
"需已存在可视化图表;AI 分析通过 Ollama 或 Minimax 调用云端/本地服务。"
|
2026-05-07 14:23:58 +08:00
|
|
|
|
)
|
|
|
|
|
|
intro.setWordWrap(True)
|
2026-06-29 15:15:09 +08:00
|
|
|
|
intro.setStyleSheet(f"""
|
|
|
|
|
|
QLabel {{
|
|
|
|
|
|
color: {ModernStylesheet.COLORS.get('primary', '#0078D7')};
|
|
|
|
|
|
background-color: {ModernStylesheet.COLORS.get('selected', '#E5F3FF')};
|
|
|
|
|
|
border: 1px solid {ModernStylesheet.COLORS.get('border_light', '#D1D5DB')};
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
""")
|
2026-05-07 14:23:58 +08:00
|
|
|
|
layout.addWidget(intro)
|
|
|
|
|
|
|
2026-06-29 15:15:09 +08:00
|
|
|
|
# ==========================================
|
|
|
|
|
|
# 卡片 1:报告路径配置
|
|
|
|
|
|
# ==========================================
|
|
|
|
|
|
path_group = QGroupBox("📁 报告路径配置")
|
|
|
|
|
|
path_layout = QVBoxLayout()
|
|
|
|
|
|
path_layout.setSpacing(16)
|
|
|
|
|
|
path_layout.setContentsMargins(20, 24, 20, 20)
|
2026-05-07 14:23:58 +08:00
|
|
|
|
|
2026-06-29 15:15:09 +08:00
|
|
|
|
# 工作目录 (只读)
|
2026-05-07 14:23:58 +08:00
|
|
|
|
self.work_dir_edit = QLineEdit()
|
2026-06-23 14:39:54 +08:00
|
|
|
|
self.work_dir_edit.setPlaceholderText("流程工作目录(含 14_visualization)…")
|
|
|
|
|
|
self.work_dir_edit.setReadOnly(True)
|
2026-06-29 15:15:09 +08:00
|
|
|
|
self.work_dir_edit.setStyleSheet(common_lineedit_css)
|
|
|
|
|
|
path_layout.addLayout(create_standard_row("工作目录:", self.work_dir_edit))
|
|
|
|
|
|
|
|
|
|
|
|
# 报告输出目录
|
2026-05-07 14:23:58 +08:00
|
|
|
|
self.output_dir_edit = QLineEdit()
|
|
|
|
|
|
self.output_dir_edit.setPlaceholderText("留空则保存到 工作目录/14_visualization")
|
2026-06-29 15:15:09 +08:00
|
|
|
|
self.output_dir_edit.setStyleSheet(common_lineedit_css)
|
|
|
|
|
|
|
|
|
|
|
|
out_browse = QPushButton("浏览...")
|
|
|
|
|
|
out_browse.setStyleSheet(common_btn_css)
|
|
|
|
|
|
out_browse.setMaximumWidth(80)
|
2026-05-07 14:23:58 +08:00
|
|
|
|
out_browse.clicked.connect(self.browse_output_dir)
|
|
|
|
|
|
|
2026-06-29 15:15:09 +08:00
|
|
|
|
path_layout.addLayout(create_standard_row("报告输出目录:", self.output_dir_edit, out_browse))
|
|
|
|
|
|
|
|
|
|
|
|
# 报告标题
|
2026-05-07 14:23:58 +08:00
|
|
|
|
self.report_title_edit = QLineEdit()
|
|
|
|
|
|
self.report_title_edit.setText("水质参数反演分析报告")
|
2026-06-29 15:15:09 +08:00
|
|
|
|
self.report_title_edit.setStyleSheet(common_lineedit_css)
|
|
|
|
|
|
path_layout.addLayout(create_standard_row("报告标题:", self.report_title_edit))
|
2026-05-07 14:23:58 +08:00
|
|
|
|
|
2026-06-29 15:15:09 +08:00
|
|
|
|
path_group.setLayout(path_layout)
|
2026-05-07 14:23:58 +08:00
|
|
|
|
layout.addWidget(path_group)
|
|
|
|
|
|
|
2026-06-29 15:15:09 +08:00
|
|
|
|
# ==========================================
|
|
|
|
|
|
# 卡片 2:AI 分析配置
|
|
|
|
|
|
# ==========================================
|
|
|
|
|
|
ai_group = QGroupBox("🤖 AI 分析配置")
|
2026-06-08 14:58:16 +08:00
|
|
|
|
ai_layout = QVBoxLayout()
|
2026-06-29 15:15:09 +08:00
|
|
|
|
ai_layout.setSpacing(16)
|
|
|
|
|
|
ai_layout.setContentsMargins(20, 24, 20, 20)
|
|
|
|
|
|
|
|
|
|
|
|
# 启用复选框,采用占位符顶齐 120px
|
|
|
|
|
|
check_row = QHBoxLayout()
|
|
|
|
|
|
check_row.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
check_row.setSpacing(10)
|
|
|
|
|
|
placeholder = QLabel("")
|
|
|
|
|
|
placeholder.setMinimumWidth(120)
|
|
|
|
|
|
placeholder.setMaximumWidth(120)
|
|
|
|
|
|
check_row.addWidget(placeholder)
|
2026-05-07 14:23:58 +08:00
|
|
|
|
|
|
|
|
|
|
self.enable_ai_cb = QCheckBox("启用 AI 图表解读与综合总结")
|
2026-06-29 15:15:09 +08:00
|
|
|
|
self.enable_ai_cb.setChecked(os.environ.get("ENABLE_AI_ANALYSIS", "1") not in {"0", "false", "False"})
|
|
|
|
|
|
check_row.addWidget(self.enable_ai_cb)
|
|
|
|
|
|
check_row.addStretch()
|
|
|
|
|
|
ai_layout.addLayout(check_row)
|
2026-06-08 14:58:16 +08:00
|
|
|
|
|
2026-06-29 15:15:09 +08:00
|
|
|
|
# AI 引擎状态行
|
2026-06-08 14:58:16 +08:00
|
|
|
|
ai_status_row = QHBoxLayout()
|
2026-06-29 15:15:09 +08:00
|
|
|
|
ai_status_row.setContentsMargins(0, 0, 0, 0)
|
|
|
|
|
|
ai_status_row.setSpacing(10)
|
|
|
|
|
|
engine_label = QLabel("当前 AI 引擎:")
|
|
|
|
|
|
engine_label.setMinimumWidth(120)
|
|
|
|
|
|
engine_label.setMaximumWidth(120)
|
|
|
|
|
|
ai_status_row.addWidget(engine_label)
|
|
|
|
|
|
|
2026-06-08 14:58:16 +08:00
|
|
|
|
self._ai_label = QLabel()
|
2026-06-29 15:15:09 +08:00
|
|
|
|
self._ai_label.setStyleSheet("color: #0078D7; font-weight: bold; font-size: 13px;")
|
2026-06-08 14:58:16 +08:00
|
|
|
|
ai_status_row.addWidget(self._ai_label)
|
|
|
|
|
|
ai_status_row.addStretch(1)
|
2026-06-29 15:15:09 +08:00
|
|
|
|
|
2026-06-08 14:58:16 +08:00
|
|
|
|
open_settings_btn = QPushButton("高级配置...")
|
2026-06-29 15:15:09 +08:00
|
|
|
|
open_settings_btn.setStyleSheet(common_btn_css)
|
2026-06-08 14:58:16 +08:00
|
|
|
|
open_settings_btn.clicked.connect(self._open_ai_settings)
|
|
|
|
|
|
ai_status_row.addWidget(open_settings_btn)
|
2026-06-29 15:15:09 +08:00
|
|
|
|
|
2026-06-08 14:58:16 +08:00
|
|
|
|
ai_layout.addLayout(ai_status_row)
|
|
|
|
|
|
|
|
|
|
|
|
ai_group.setLayout(ai_layout)
|
2026-05-07 14:23:58 +08:00
|
|
|
|
layout.addWidget(ai_group)
|
|
|
|
|
|
|
2026-06-29 15:15:09 +08:00
|
|
|
|
# ==========================================
|
|
|
|
|
|
# 卡片 3:输出与执行
|
|
|
|
|
|
# ==========================================
|
|
|
|
|
|
execute_group = QGroupBox("🚀 输出与执行")
|
|
|
|
|
|
execute_layout = QVBoxLayout()
|
|
|
|
|
|
execute_layout.setSpacing(12)
|
|
|
|
|
|
execute_layout.setContentsMargins(20, 24, 20, 20)
|
|
|
|
|
|
|
|
|
|
|
|
# 进度状态呈现(使用独一无二的 ID 选择器,彻底击碎并无视任何全局白块背景样式!)
|
|
|
|
|
|
self.progress_label = QLabel("⏳ 准备就绪")
|
|
|
|
|
|
self.progress_label.setObjectName("step13_progress_lbl")
|
|
|
|
|
|
self.progress_label.setStyleSheet("""
|
|
|
|
|
|
QLabel#step13_progress_lbl {
|
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: #4A5568;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
""")
|
|
|
|
|
|
execute_layout.addWidget(self.progress_label)
|
|
|
|
|
|
|
|
|
|
|
|
# 现代极简细线条进度条
|
2026-06-23 14:39:54 +08:00
|
|
|
|
self.progress_bar = QProgressBar()
|
|
|
|
|
|
self.progress_bar.setRange(0, 100)
|
|
|
|
|
|
self.progress_bar.setValue(0)
|
2026-06-29 15:15:09 +08:00
|
|
|
|
self.progress_bar.setTextVisible(False) # 【核心】关闭原生文字,彻底干掉 Windows 丑陋白框!
|
|
|
|
|
|
self.progress_bar.setFixedHeight(8) # 压低高度,高级感拉满
|
|
|
|
|
|
self.progress_bar.setStyleSheet("""
|
|
|
|
|
|
QProgressBar { border: none; border-radius: 4px; background-color: #E2E8F0; }
|
|
|
|
|
|
QProgressBar::chunk { background-color: #0078D7; border-radius: 4px; }
|
|
|
|
|
|
""")
|
|
|
|
|
|
execute_layout.addWidget(self.progress_bar)
|
|
|
|
|
|
|
|
|
|
|
|
action_layout = QHBoxLayout()
|
|
|
|
|
|
action_layout.addStretch()
|
|
|
|
|
|
|
|
|
|
|
|
self.generate_btn = QPushButton("独立运行步骤")
|
|
|
|
|
|
self.generate_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet("primary"))
|
|
|
|
|
|
self.generate_btn.setMinimumWidth(140)
|
|
|
|
|
|
self.generate_btn.clicked.connect(self.on_generate_clicked)
|
|
|
|
|
|
action_layout.addWidget(self.generate_btn)
|
|
|
|
|
|
|
|
|
|
|
|
execute_layout.addLayout(action_layout)
|
|
|
|
|
|
execute_group.setLayout(execute_layout)
|
|
|
|
|
|
layout.addWidget(execute_group)
|
2026-06-23 14:39:54 +08:00
|
|
|
|
|
2026-05-07 14:23:58 +08:00
|
|
|
|
layout.addStretch()
|
|
|
|
|
|
self.setLayout(layout)
|
|
|
|
|
|
|
2026-06-08 14:58:16 +08:00
|
|
|
|
self._refresh_ai_label()
|
2026-06-23 14:39:54 +08:00
|
|
|
|
self._auto_pull_work_dir()
|
|
|
|
|
|
|
2026-06-08 14:58:16 +08:00
|
|
|
|
def _refresh_ai_label(self):
|
|
|
|
|
|
"""从 QSettings 读取当前 Provider 并更新只读标签。"""
|
|
|
|
|
|
s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP)
|
|
|
|
|
|
provider = s.value("ai_provider", "minimax", type=str)
|
|
|
|
|
|
label_map = {"ollama": "Ollama (本地)", "minimax": "Minimax (云端)"}
|
|
|
|
|
|
self._ai_label.setText(label_map.get(provider, provider))
|
2026-05-07 14:23:58 +08:00
|
|
|
|
|
2026-06-08 14:58:16 +08:00
|
|
|
|
def _open_ai_settings(self):
|
|
|
|
|
|
"""弹出全局 AI 设置对话框,保存后刷新提示标签。"""
|
|
|
|
|
|
dlg = AISettingsDialog(self)
|
2026-06-29 15:15:09 +08:00
|
|
|
|
# 【强力注入】强制将配置弹窗内的标签与编组框背景抹为透明,全面恢复高级灰度质感!
|
|
|
|
|
|
dlg.setStyleSheet("""
|
|
|
|
|
|
QLabel { background-color: transparent; background: transparent; border: none; }
|
|
|
|
|
|
QGroupBox { background-color: transparent; }
|
|
|
|
|
|
""")
|
2026-06-08 14:58:16 +08:00
|
|
|
|
if dlg.exec_() == dlg.Accepted:
|
|
|
|
|
|
self._refresh_ai_label()
|
2026-05-07 14:23:58 +08:00
|
|
|
|
|
2026-05-07 16:49:24 +08:00
|
|
|
|
def _get_default_work_dir(self):
|
|
|
|
|
|
"""获取 work_dir,优先用主窗口缓存的 work_dir"""
|
|
|
|
|
|
if self.main_window and hasattr(self.main_window, 'work_dir') and self.main_window.work_dir:
|
|
|
|
|
|
return str(self.main_window.work_dir)
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
2026-05-07 14:23:58 +08:00
|
|
|
|
def browse_work_dir(self):
|
2026-05-07 16:49:24 +08:00
|
|
|
|
default = self._get_default_work_dir()
|
|
|
|
|
|
d = QFileDialog.getExistingDirectory(self, "选择工作目录", default)
|
2026-05-07 14:23:58 +08:00
|
|
|
|
if d:
|
|
|
|
|
|
self.work_dir_edit.setText(d)
|
|
|
|
|
|
|
|
|
|
|
|
def browse_output_dir(self):
|
2026-05-07 16:49:24 +08:00
|
|
|
|
default = self._get_default_work_dir()
|
|
|
|
|
|
if default:
|
2026-06-16 12:54:18 +08:00
|
|
|
|
default = resolve_subdir(default, 'visualization')
|
2026-05-07 16:49:24 +08:00
|
|
|
|
d = QFileDialog.getExistingDirectory(self, "选择报告输出目录", default)
|
2026-05-07 14:23:58 +08:00
|
|
|
|
if d:
|
|
|
|
|
|
self.output_dir_edit.setText(d)
|
|
|
|
|
|
|
|
|
|
|
|
def sync_work_dir_from_main(self):
|
|
|
|
|
|
mw = self.main_window
|
|
|
|
|
|
if mw is not None and getattr(mw, "work_dir", None):
|
|
|
|
|
|
self.work_dir_edit.setText(str(mw.work_dir))
|
|
|
|
|
|
else:
|
|
|
|
|
|
QMessageBox.information(self, "提示", "主窗口尚未设置工作目录。")
|
|
|
|
|
|
|
|
|
|
|
|
def set_work_dir(self, work_dir):
|
|
|
|
|
|
if work_dir:
|
|
|
|
|
|
self.work_dir_edit.setText(str(work_dir))
|
|
|
|
|
|
|
2026-06-23 14:39:54 +08:00
|
|
|
|
def _auto_pull_work_dir(self):
|
|
|
|
|
|
"""从主窗口自动同步工作目录到 work_dir_edit(无需用户操作)。"""
|
|
|
|
|
|
mw = self.main_window
|
|
|
|
|
|
if mw is not None and getattr(mw, "work_dir", None):
|
|
|
|
|
|
wd = str(mw.work_dir)
|
|
|
|
|
|
cur = self.work_dir_edit.text().strip()
|
|
|
|
|
|
if wd and wd != cur:
|
|
|
|
|
|
self.work_dir_edit.setText(wd)
|
|
|
|
|
|
|
|
|
|
|
|
def update_from_config(self, work_dir=None, pipeline=None):
|
|
|
|
|
|
"""切入面板时由主窗口统一调用,把当前 work_dir 同步到本面板。
|
|
|
|
|
|
解耦手动 browse:work_dir_edit 已 ReadOnly,外部只能通过此入口更新。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if work_dir:
|
|
|
|
|
|
self.work_dir_edit.setText(str(work_dir))
|
|
|
|
|
|
self._auto_pull_work_dir()
|
|
|
|
|
|
|
|
|
|
|
|
def showEvent(self, event):
|
|
|
|
|
|
"""Tab 切换到本面板时再次兜底同步一次(应对 init_ui 时尚未绑定 main_window 的场景)。"""
|
|
|
|
|
|
super().showEvent(event)
|
|
|
|
|
|
self._auto_pull_work_dir()
|
|
|
|
|
|
|
2026-05-07 14:23:58 +08:00
|
|
|
|
def get_config(self):
|
2026-06-08 14:58:16 +08:00
|
|
|
|
"""返回路径和标题配置(AI 配置不由本面板持有)。"""
|
2026-05-07 14:23:58 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"work_dir": self.work_dir_edit.text().strip() or None,
|
|
|
|
|
|
"output_dir": self.output_dir_edit.text().strip() or None,
|
|
|
|
|
|
"report_title": self.report_title_edit.text().strip() or "水质参数反演分析报告",
|
|
|
|
|
|
"enable_ai_analysis": self.enable_ai_cb.isChecked(),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def set_config(self, config):
|
|
|
|
|
|
if not config:
|
|
|
|
|
|
return
|
|
|
|
|
|
if config.get("work_dir"):
|
|
|
|
|
|
self.work_dir_edit.setText(str(config["work_dir"]))
|
|
|
|
|
|
if "output_dir" in config:
|
|
|
|
|
|
self.output_dir_edit.setText(str(config["output_dir"] or ""))
|
|
|
|
|
|
if config.get("report_title"):
|
|
|
|
|
|
self.report_title_edit.setText(str(config["report_title"]))
|
|
|
|
|
|
if "enable_ai_analysis" in config:
|
|
|
|
|
|
self.enable_ai_cb.setChecked(bool(config["enable_ai_analysis"]))
|
|
|
|
|
|
|
|
|
|
|
|
def on_generate_clicked(self):
|
|
|
|
|
|
wd = self.work_dir_edit.text().strip()
|
|
|
|
|
|
if not wd or not os.path.isdir(wd):
|
|
|
|
|
|
QMessageBox.warning(self, "提示", "请选择有效的工作目录。")
|
|
|
|
|
|
return
|
|
|
|
|
|
viz = Path(wd) / "14_visualization"
|
|
|
|
|
|
if not viz.is_dir():
|
|
|
|
|
|
QMessageBox.warning(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"提示",
|
|
|
|
|
|
f"未找到可视化目录:\n{viz}\n请先完成流程或生成可视化。",
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
if self._report_thread and self._report_thread.isRunning():
|
|
|
|
|
|
QMessageBox.information(self, "提示", "报告正在生成中,请稍候。")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
out = self.output_dir_edit.text().strip() or None
|
|
|
|
|
|
title = self.report_title_edit.text().strip() or "水质参数反演分析报告"
|
2026-06-08 14:58:16 +08:00
|
|
|
|
enable_ai = self.enable_ai_cb.isChecked()
|
|
|
|
|
|
|
2026-06-23 14:39:54 +08:00
|
|
|
|
# 重置进度条并禁用按钮
|
2026-05-07 14:23:58 +08:00
|
|
|
|
self.generate_btn.setEnabled(False)
|
2026-06-23 14:39:54 +08:00
|
|
|
|
self.progress_bar.setValue(0)
|
|
|
|
|
|
self.progress_label.setText("正在准备生成…")
|
|
|
|
|
|
|
|
|
|
|
|
self._report_thread = ReportWorkerThread(wd, out, title, enable_ai)
|
|
|
|
|
|
self._report_thread.progress.connect(self._on_progress, Qt.QueuedConnection)
|
|
|
|
|
|
self._report_thread.finished.connect(self._on_finished, Qt.QueuedConnection)
|
|
|
|
|
|
self._report_thread.error.connect(self._on_error, Qt.QueuedConnection)
|
2026-05-07 14:23:58 +08:00
|
|
|
|
self._report_thread.start()
|
|
|
|
|
|
self._forward_log("已开始生成 Word 报告…", "info")
|
|
|
|
|
|
|
|
|
|
|
|
def _forward_log(self, msg: str, level: str):
|
|
|
|
|
|
mw = self.main_window
|
|
|
|
|
|
if mw is not None and hasattr(mw, "log_message"):
|
|
|
|
|
|
mw.log_message(msg, level)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"[{level}] {msg}")
|
|
|
|
|
|
|
2026-06-23 14:39:54 +08:00
|
|
|
|
def _on_progress(self, pct: int, text: str):
|
2026-06-29 15:15:09 +08:00
|
|
|
|
"""接收后台进度的槽函数 —— 主线程。"""
|
2026-06-23 14:39:54 +08:00
|
|
|
|
self.progress_bar.setValue(int(pct))
|
2026-06-29 15:15:09 +08:00
|
|
|
|
# 将百分比和文案完美融合进透明标签
|
|
|
|
|
|
self.progress_label.setText(f"⏳ {text} ({pct}%)" if text else f"⏳ 正在生成... ({pct}%)")
|
2026-06-23 14:39:54 +08:00
|
|
|
|
|
|
|
|
|
|
def _on_finished(self, path: str):
|
|
|
|
|
|
"""报告生成成功 —— 主线程槽。"""
|
|
|
|
|
|
self.progress_bar.setValue(100)
|
2026-06-29 15:15:09 +08:00
|
|
|
|
self.progress_label.setText("✅ 分析报告生成成功!")
|
2026-06-23 14:39:54 +08:00
|
|
|
|
self.generate_btn.setEnabled(True)
|
2026-05-07 14:23:58 +08:00
|
|
|
|
QMessageBox.information(self, "完成", f"报告已生成:\n{path}")
|
|
|
|
|
|
self._forward_log(f"Word 报告已保存: {path}", "info")
|
|
|
|
|
|
|
2026-06-23 14:39:54 +08:00
|
|
|
|
def _on_error(self, err: str):
|
|
|
|
|
|
"""报告生成异常 —— 主线程槽。"""
|
2026-06-29 15:15:09 +08:00
|
|
|
|
self.progress_label.setText("❌ 报告生成失败")
|
2026-06-23 14:39:54 +08:00
|
|
|
|
self.generate_btn.setEnabled(True)
|
2026-05-07 14:23:58 +08:00
|
|
|
|
QMessageBox.critical(self, "失败", f"报告生成失败:\n{err[:800]}")
|
2026-06-08 14:58:16 +08:00
|
|
|
|
self._forward_log(err, "error")
|