#!/usr/bin/env python # -*- coding: utf-8 -*- """ 数据提取标签页 从 ERP 系统提取生产订单数据的标签页。 继承自 BaseTab,使用统一的日志系统。 """ import os import sys import threading import logging import queue import tkinter as tk from tkinter import ttk, filedialog, messagebox from pathlib import Path from gui.base_tab import BaseTab from gui.widgets import FileSelector, LogText, ProductionIdInput, GuiTextHandler from gui.config_manager import ConfigManager from gui.log_config import setup_gui_logging, get_logger from gui.progress import ProgressInfo, ProgressCalculator from gui.utils import RealtimeOutput class DataExtractionTab(BaseTab): """数据提取标签页""" def __init__(self, parent, config: ConfigManager, main_window=None): super().__init__(parent, config, main_window) self.extracting = False self.extractor = None self.extraction_thread = None self.progress_calculator = ProgressCalculator() self.progress_queue = queue.Queue() # 初始化统一日志系统 self.logger = get_logger(__name__) self._gui_handler = None # 将在 _create_log_panel 中设置 self._poll_progress_queue() self.create_widgets() self._apply_ui_config() # 初始化日志消息 try: self.log_text.info("数据提取标签页已就绪") except Exception as e: self.logger.debug(f"初始化日志消息失败: {e}") def create_widgets(self): horizontal_paned = ttk.PanedWindow(self, orient=tk.HORIZONTAL) horizontal_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) left_panel = ttk.Frame(horizontal_paned) horizontal_paned.add(left_panel, weight=0) right_panel = ttk.Frame(horizontal_paned) horizontal_paned.add(right_panel, weight=1) self._create_left_panel(left_panel) self._create_right_panel(right_panel) self.horizontal_paned = horizontal_paned input_width = self.config.get("ui.production_id_input_width", 20) self.after(100, lambda: self._set_pane_width(input_width * 8)) def _create_left_panel(self, parent): input_group = ttk.LabelFrame(parent, text="Production ID", padding=10) input_group.pack(fill=tk.BOTH, expand=True) self.production_id_input = ProductionIdInput( input_group, placeholder="每行输入一个 Production ID\n\n示例:\n26B848\n26B849", ) self.production_id_input.pack(fill=tk.BOTH, expand=True) self.production_id_input.text_widget.bind( "", self._on_production_ids_changed ) def _create_right_panel(self, parent): main_paned = ttk.PanedWindow(parent, orient=tk.VERTICAL) main_paned.pack(fill=tk.BOTH, expand=True) 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): 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) # 设置 GUI 日志处理器,将 logging 输出桥接到 LogText 组件 self._gui_handler = GuiTextHandler(self.log_text) self._gui_handler.setFormatter( logging.Formatter( "%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) ) self.logger.addHandler(self._gui_handler) def _apply_ui_config(self): try: font_family = self.config.get("ui.font_family", "Microsoft YaHei UI") font_size = self.config.get("ui.font_size", 10) self.production_id_input.apply_font(font_family, font_size) if hasattr(self.log_text, "apply_font"): self.log_text.apply_font(font_family, font_size) except Exception as e: self.logger.debug(f"应用 UI 配置失败: {e}") def _set_pane_width(self, width: int): try: self.horizontal_paned.sashpos(0, width) except tk.TclError as e: self.logger.debug(f"设置窗格宽度失败: {e}") def start_extraction(self): production_ids = self.production_id_input.get() if not production_ids: messagebox.showerror("错误", "请输入至少一个 Production ID") return output_file = self.output_file_selector.get() if not output_file: messagebox.showerror("错误", "请指定输出文件路径") return 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.extraction_thread = threading.Thread( target=self._extraction_worker, args=(production_ids, output_file), daemon=True, ) self.extraction_thread.start() def stop_extraction(self): if self.extracting: self.extracting = False self.log_text.warning("正在停止提取...") def _extraction_worker(self, production_ids: list[str], output_file: str): import tempfile temp_file = None try: with tempfile.NamedTemporaryFile( mode="w", suffix=".txt", delete=False, encoding="utf-8" ) as f: temp_file = f.name f.write("\n".join(production_ids)) from utils.discrete_material_plan_extractor 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 ), ) # 修复:直接调用标准的 _update_log,不再传入 add_timestamp 参数 def progress_callback(progress_info: ProgressInfo): if progress_info.stage == "log": level = progress_info.detail.get("log_level", "INFO").upper() self._update_log(progress_info.message, level) else: percent = self.progress_calculator.calculate_overall_percent( progress_info ) self._update_progress(percent, progress_info.message) result = self.extractor.extract( production_id_file=temp_file, output_file=output_file, progress_callback=progress_callback, ) if result and self.extracting: self._update_log("数据处理任务圆满结束", "SUCCESS") elif not self.extracting: self._update_log("提取已取消", "WARNING") except Exception as e: self._update_log(f"运行时错误: {str(e)}", "ERROR") finally: if temp_file and os.path.exists(temp_file): try: os.unlink(temp_file) except OSError as e: self.logger.debug(f"清理临时文件失败: {e}") self.after(0, self._extraction_complete) def _extraction_complete(self): 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: value, message = self.progress_queue.get_nowait() self.progress_bar["value"] = value self.status_label.config(text=message) except queue.Empty: break except Exception as e: self.logger.debug(f"轮询进度队列失败: {e}") finally: self.after(50, self._poll_progress_queue) def _update_progress(self, value: int, message: str): try: self.progress_queue.put_nowait((value, message)) except queue.Full as e: self.logger.debug(f"进度队列已满: {e}") def _on_production_ids_changed(self, event=None): if self.main_window: self.main_window.update_shared_production_ids( self.production_id_input.get() ) def reload_config(self): self._apply_ui_config() self._on_production_ids_changed()