""" ui/dialogs/export_report_dialog.py — 效能報告匯出對話框。 提供 ExportReportDialog(QDialog),讓使用者選擇報告格式(PDF/CSV)與儲存路徑, 然後觸發 ReportExporter 執行匯出。 設計重點: - _collect_report_data() 從各模組收集資料,每個來源都用 try/except 保護。 - 不在此模組執行實際 benchmark,只使用 history 的最新一筆作為 parallel_result。 - chart_image_bytes 留 None(截圖整合留未來)。 """ from __future__ import annotations from typing import TYPE_CHECKING, List, Optional from PyQt5.QtWidgets import ( QDialog, QFileDialog, QHBoxLayout, QLabel, QPushButton, QRadioButton, QVBoxLayout, QWidget, QLineEdit, QGroupBox, QProgressBar, ) from PyQt5.QtCore import Qt from core.performance.report_exporter import DeviceSummary, ReportData, ReportExporter if TYPE_CHECKING: from core.performance.benchmarker import PerformanceBenchmarker from core.performance.history import PerformanceHistory class ExportReportDialog(QDialog): """ 效能報告匯出對話框。 使用者可選擇格式(PDF / CSV),指定儲存路徑後按匯出, 對話框會呼叫 ReportExporter 產出檔案並顯示結果。 """ def __init__( self, parent: Optional[QWidget], benchmarker, # PerformanceBenchmarker | None history, # PerformanceHistory | None device_manager, # DeviceManager | None dashboard, # PerformanceDashboard | None ) -> None: super().__init__(parent) self._benchmarker = benchmarker self._history = history self._device_manager = device_manager self._dashboard = dashboard self._exporter = ReportExporter() # 預設格式為 PDF self._selected_format: str = "pdf" self._setup_ui() # ------------------------------------------------------------------ # UI 建立 # ------------------------------------------------------------------ def _setup_ui(self) -> None: """建立對話框 UI。""" self.setWindowTitle("匯出效能報告") main_layout = QVBoxLayout() # 格式選擇 format_group = QGroupBox("匯出格式") format_layout = QHBoxLayout() self._pdf_radio = QRadioButton("PDF") self._csv_radio = QRadioButton("CSV") self._pdf_radio.setChecked(True) self._pdf_radio.clicked.connect(lambda: self._set_format("pdf")) self._csv_radio.clicked.connect(lambda: self._set_format("csv")) format_layout.addWidget(self._pdf_radio) format_layout.addWidget(self._csv_radio) format_group.setLayout(format_layout) main_layout.addWidget(format_group) # 儲存路徑 path_layout = QHBoxLayout() self._path_input = QLineEdit() self._path_input.setPlaceholderText("儲存路徑…") self._browse_btn = QPushButton("瀏覽") self._browse_btn.clicked.connect(self._on_browse) path_layout.addWidget(self._path_input) path_layout.addWidget(self._browse_btn) main_layout.addLayout(path_layout) # 進度條 self._progress_bar = QProgressBar() self._progress_bar.setVisible(False) main_layout.addWidget(self._progress_bar) # 匯出按鈕 self._export_btn = QPushButton("匯出") self._export_btn.clicked.connect(self._on_export) main_layout.addWidget(self._export_btn) # 狀態標籤 self._status_label = QLabel("") main_layout.addWidget(self._status_label) self.setLayout(main_layout) # ------------------------------------------------------------------ # 格式設定 # ------------------------------------------------------------------ def _set_format(self, fmt: str) -> None: """設定匯出格式('pdf' 或 'csv')。""" self._selected_format = fmt # ------------------------------------------------------------------ # 事件處理 # ------------------------------------------------------------------ def _on_browse(self) -> None: """開啟 QFileDialog 讓使用者選擇儲存路徑。""" if self._selected_format == "pdf": file_filter = "PDF 檔案 (*.pdf)" default_suffix = ".pdf" else: file_filter = "CSV 檔案 (*.csv)" default_suffix = ".csv" path, _ = QFileDialog.getSaveFileName( self, "選擇儲存位置", f"performance_report{default_suffix}", file_filter, ) if path: self._path_input.setText(path) def _on_export(self) -> None: """執行匯出:收集資料 -> 呼叫 ReportExporter。""" output_path = self._path_input.text().strip() if not output_path: self._status_label.setText("請先指定儲存路徑。") return data = self._collect_report_data() try: if self._selected_format == "pdf": result = self._exporter.export_pdf(data, output_path) else: result = self._exporter.export_csv(data, output_path) self._status_label.setText(f"匯出成功:{result}") except ImportError as e: self._status_label.setText(f"匯出失敗(缺少函式庫):{e}") except ValueError as e: self._status_label.setText(f"匯出失敗(資料不足):{e}") except Exception as e: self._status_label.setText(f"匯出失敗:{e}") # ------------------------------------------------------------------ # 資料收集 # ------------------------------------------------------------------ def _collect_report_data(self) -> ReportData: """ 從各模組收集資料,組裝 ReportData。 每個來源都用 try/except 保護,失敗時使用 None / 空值。 不實際執行 benchmark,只使用 history 的最新一筆作為 parallel_result。 """ # 歷史記錄,同時從中取最近一筆 sequential / parallel 作為 result history_records: list = [] seq_result = None par_result = None try: records = self._history.get_history(limit=20) if self._history else [] history_records = list(records) if records else [] seq_result = next((r for r in history_records if r.mode == "sequential"), None) par_result = next((r for r in history_records if r.mode == "parallel"), None) except Exception: history_records, seq_result, par_result = [], None, None # 從 benchmarker.history 取最新一筆作為 parallel_result(fallback,不執行新的 benchmark) if par_result is None: try: if self._benchmarker is not None: hist = self._benchmarker.history if hist: par_result = hist[-1] except Exception: par_result = None # 裝置資訊 devices: List[DeviceSummary] = [] try: if self._device_manager is not None: raw_devices = self._device_manager.scan_devices() or [] devices = self._convert_devices(raw_devices) except Exception: devices = [] return ReportData( sequential_result=seq_result, parallel_result=par_result, speedup=None, history_records=history_records, devices=devices, chart_image_bytes=None, # 截圖整合留未來 ) @staticmethod def _convert_devices(raw_devices: list) -> List[DeviceSummary]: """ 將 DeviceManager 回傳的裝置列表轉換為 DeviceSummary 列表。 若轉換失敗,略過該裝置。 """ result: List[DeviceSummary] = [] for dev in raw_devices: try: result.append(DeviceSummary( device_id=str(getattr(dev, "device_id", getattr(dev, "id", "unknown"))), product_name=str(getattr(dev, "product_name", getattr(dev, "model", "unknown"))), firmware_version=str(getattr(dev, "firmware_version", "unknown")), is_active=bool(getattr(dev, "is_active", True)), )) except Exception: continue return result