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>
261 lines
11 KiB
Python
261 lines
11 KiB
Python
#!/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() |