Files
playwrite/gui/data_extraction_tab.py
Misaka 24053c6a3b feat: implement unified logging system for GUI components
Add centralized logging mechanism that simultaneously outputs to console
and GUI log components, improving code maintainability and consistency.

Changes:
- Add gui/log_config.py for centralized logging configuration
- Add gui/widgets/log_handler.py as bridge between logging and LogText
- Integrate unified logging into DataExtractionTab and MaterialValidationTab
- Initialize logging system in MainWindow on startup
- Improve error messages in material_status_validator for empty results
- Add documentation for logging mechanism and refactoring

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 20:31:59 +08:00

261 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
数据提取标签页 - 稳定性修复版
修复了 LogText.info 不支持 add_timestamp 参数导致的 TypeError。
"""
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.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(ttk.Frame):
"""数据提取标签页"""
def __init__(self, parent, config: ConfigManager, main_window=None):
super().__init__(parent)
self.config = config
self.main_window = 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:
pass
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("<FocusOut>", 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: pass
def _set_pane_width(self, width: int):
try: self.horizontal_paned.sashpos(0, width)
except: pass
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.离散备料计划维护数据提取 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: pass
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
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: pass
def _update_log(self, message: str, level: str = "INFO"):
"""
标准的日志更新方法(兼容接口)
通过统一的 logging 系统输出日志,自动同时输出到控制台和 GUI。
Args:
message: 日志消息
level: 日志级别 (INFO, SUCCESS, WARNING, ERROR, DEBUG)
"""
# 将自定义级别映射到 logging 级别
level_upper = level.upper()
if level_upper == "SUCCESS":
# SUCCESS 映射到 INFO但在 UI 中仍显示为 SUCCESS
self.logger.info(message)
else:
# 其他级别直接映射
log_level = getattr(logging, level_upper, logging.INFO)
self.logger.log(log_level, message)
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()