#!/usr/bin/env python # -*- coding: utf-8 -*- """ 数据提取标签页 从 ERP 系统提取备料计划数据。 """ import os import sys import threading import queue import tkinter as tk from tkinter import ttk, filedialog, messagebox from pathlib import Path from contextlib import redirect_stdout from gui.widgets import FileSelector, LogText from gui.config_manager import ConfigManager from gui.progress import ProgressInfo, ProgressCalculator from gui.utils import RealtimeOutput class DataExtractionTab(ttk.Frame): """数据提取标签页""" def __init__(self, parent, config: ConfigManager): """ 初始化数据提取标签页 Args: parent: 父容器 config: 配置管理器 """ super().__init__(parent) self.config = config self.extracting = False self.extractor = None self.extraction_thread = None self.progress_calculator = ProgressCalculator() self.progress_queue = queue.Queue() # 进度更新队列 # 启动进度更新轮询 self._poll_progress_queue() self.create_widgets() # 稍后显示就绪消息 try: self.log_text.info("数据提取标签页已就绪") except: pass # 如果窗口还未完全就绪,忽略错误 def create_widgets(self): """创建界面组件""" # 主容器 - 使用 PanedWindow 分割上下部分 main_paned = ttk.PanedWindow(self, orient=tk.VERTICAL) main_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 上部:控制面板 control_frame = ttk.Frame(main_paned) main_paned.add(control_frame, weight=0) # 下部:日志输出 log_frame = ttk.LabelFrame(main_paned, text="日志输出", padding=5) main_paned.add(log_frame, weight=1) self._create_control_panel(control_frame) self._create_log_panel(log_frame) def _create_control_panel(self, parent): """创建控制面板""" # 输入文件选择 input_group = ttk.LabelFrame(parent, text="输入文件", padding=10) input_group.pack(fill=tk.X, pady=5) self.input_file_selector = FileSelector( input_group, label_text="ProductionID 文件:", file_type="file", file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")], initial_dir="D:/python/playwrite/", ) self.input_file_selector.pack(fill=tk.X) # 设置默认文件 default_input = self.config.get("paths.production_id_file", "ProductionID.txt") if os.path.exists(default_input): self.input_file_selector.set(default_input) # 输出文件选择 output_group = ttk.LabelFrame(parent, text="输出文件", padding=10) output_group.pack(fill=tk.X, pady=5) self.output_file_selector = FileSelector( output_group, label_text="保存为:", file_type="file", file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")], initial_dir=self.config.get("paths.data_dir", "data/"), ) self.output_file_selector.pack(fill=tk.X) # 设置默认输出文件 default_output = os.path.join( self.config.get("paths.data_dir", "data/"), self.config.get("paths.default_output", "离散备料计划维护_合并.xlsx"), ) self.output_file_selector.set(default_output) # 选项 options_group = ttk.LabelFrame(parent, text="提取选项", padding=10) options_group.pack(fill=tk.X, pady=5) self.headless_var = tk.BooleanVar(value=self.config.get("erp.headless", True)) ttk.Checkbutton( options_group, text="无头模式 (不显示浏览器)", variable=self.headless_var ).grid(row=0, column=0, sticky="w", padx=5) # 进度显示 progress_group = ttk.LabelFrame(parent, text="进度", padding=10) progress_group.pack(fill=tk.X, pady=5) self.progress_bar = ttk.Progressbar(progress_group, mode="determinate") self.progress_bar.pack(fill=tk.X, pady=5) self.status_label = ttk.Label( progress_group, text="就绪", relief=tk.SUNKEN, anchor=tk.W ) self.status_label.pack(fill=tk.X) # 控制按钮 button_frame = ttk.Frame(parent) button_frame.pack(fill=tk.X, pady=10) self.start_button = ttk.Button( button_frame, text="开始提取", command=self.start_extraction ) self.start_button.pack(side=tk.LEFT, padx=5) self.stop_button = ttk.Button( button_frame, text="停止", command=self.stop_extraction, state=tk.DISABLED ) self.stop_button.pack(side=tk.LEFT, padx=5) def _create_log_panel(self, parent): """创建日志面板""" self.log_text = LogText(parent, height=15, readonly=True) self.log_text.pack(fill=tk.BOTH, expand=True) def start_extraction(self): """开始数据提取""" # 验证输入 input_file = self.input_file_selector.get() output_file = self.output_file_selector.get() if not input_file: messagebox.showerror("错误", "请选择 ProductionID 输入文件") return if not os.path.exists(input_file): messagebox.showerror("错误", f"输入文件不存在:{input_file}") return if not output_file: messagebox.showerror("错误", "请指定输出文件路径") return # 确保输出目录存在 output_dir = os.path.dirname(output_file) if output_dir and not os.path.exists(output_dir): os.makedirs(output_dir, exist_ok=True) # 更新 UI 状态 self.extracting = True self.start_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self.progress_bar["value"] = 0 self.status_label.config(text="正在登录...") self.log_text.clear() self.log_text.info("开始数据提取...") # 在后台线程中执行提取 self.extraction_thread = threading.Thread( target=self._extraction_worker, args=(input_file, output_file), daemon=True ) self.extraction_thread.start() def stop_extraction(self): """停止数据提取""" if self.extracting: self.extracting = False self.log_text.warning("正在停止提取...") self.status_label.config(text="正在停止...") def _extraction_worker(self, input_file: str, output_file: str): """提取工作线程""" try: # 导入提取器(延迟导入以避免启动时加载 Playwright) from utils.离散备料计划维护数据提取 import DiscreteMaterialPlanExtractor # 创建提取器实例 self.extractor = DiscreteMaterialPlanExtractor( username=self.config.get("erp.username"), password=self.config.get("erp.password"), headless=self.headless_var.get(), verbose=self.config.get("extraction.verbose", True), batch_size=self.config.get("extraction.batch_size", 100), enable_db_persistence=self.config.get("extraction.enable_db_persistence", False), ) # 创建实时输出流,每次写入立即更新 GUI realtime_output = RealtimeOutput( lambda line: self._update_log(line, "INFO") ) # 创建进度回调函数 def progress_callback(progress_info: ProgressInfo): # 计算总体进度百分比 overall_percent = self.progress_calculator.calculate_overall_percent( progress_info ) self._update_progress(overall_percent, progress_info.message) # 重定向 stdout 并执行提取(带进度回调) with redirect_stdout(realtime_output): result = self.extractor.extract( production_id_file=input_file, output_file=output_file, progress_callback=progress_callback, ) if result and self.extracting: self._update_log(f"数据已保存到:{output_file}", "SUCCESS") elif not self.extracting: self._update_log("提取已取消", "WARNING") else: self._update_log("提取失败", "ERROR") except Exception as e: self._update_log(f"提取过程中发生错误:{str(e)}", "ERROR") finally: # 更新 UI 状态 self.after(0, self._extraction_complete) def _extraction_complete(self): """提取完成后的 UI 更新""" self.extracting = False self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) self.extractor = None def _poll_progress_queue(self): """轮询进度队列,处理进度更新""" try: while True: # 非阻塞地获取队列中的消息 try: progress_data = self.progress_queue.get_nowait() value, message = progress_data self.progress_bar["value"] = value self.status_label.config(text=message) except queue.Empty: break finally: # 继续轮询(每 50ms 检查一次) self.after(50, self._poll_progress_queue) def _update_progress(self, value: int, message: str): """线程安全的进度更新(通过队列)""" try: self.progress_queue.put_nowait((value, message)) except: pass # 队列满时忽略 def _update_log(self, message: str, level: str = "INFO"): """线程安全的日志更新""" def update(): if self.extracting or level in ["ERROR", "WARNING", "SUCCESS"]: if level == "INFO": self.log_text.info(message) elif level == "SUCCESS": self.log_text.success(message) elif level == "WARNING": self.log_text.warning(message) elif level == "ERROR": self.log_text.error(message) self.after(0, update)