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>
This commit is contained in:
Misaka
2026-02-24 20:31:59 +08:00
parent 3c45ef58d1
commit 24053c6a3b
9 changed files with 821 additions and 24 deletions

View File

@@ -9,12 +9,14 @@
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
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
@@ -32,6 +34,10 @@ class DataExtractionTab(ttk.Frame):
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()
@@ -116,6 +122,14 @@ class DataExtractionTab(ttk.Frame):
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")
@@ -219,15 +233,24 @@ class DataExtractionTab(ttk.Frame):
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)
"""
标准的日志更新方法(兼容接口)
通过统一的 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:

43
gui/log_config.py Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
GUI 日志配置模块
统一配置 GUI 应用和控制台的日志输出
"""
import logging
# 日志格式配置
LOG_FORMAT = '%(asctime)s [%(levelname)s] %(message)s'
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
def setup_gui_logging(level=logging.INFO):
"""
初始化 GUI 应用的日志配置
Args:
level: 日志级别,默认为 INFO
Returns:
logging.Logger: 根 logger
"""
logging.basicConfig(
level=level,
format=LOG_FORMAT,
datefmt=DATE_FORMAT,
force=True # 确保重新配置(即使之前配置过)
)
return logging.getLogger()
def get_logger(name):
"""
获取指定名称的 logger
Args:
name: logger 名称,通常使用 __name__
Returns:
logging.Logger: logger 实例
"""
return logging.getLogger(name)

View File

@@ -9,6 +9,7 @@ ERP 自动化工具的主窗口,包含多个功能标签页。
import tkinter as tk
from tkinter import ttk
from gui.config_manager import ConfigManager
from gui.log_config import setup_gui_logging
from gui.data_extraction_tab import DataExtractionTab
from gui.material_validation_tab import MaterialValidationTab
from gui.settings_tab import SettingsTab
@@ -30,6 +31,9 @@ class MainWindow:
self.session_manager = session_manager
self.shared_production_ids = [] # 共享的 Production ID 列表
# 初始化统一日志系统
setup_gui_logging()
# 设置窗口属性(包含用户信息)
user_type_display = "管理员" if session_manager.is_admin() else "用户"
self.root.title(f"ERP 自动化工具 v1.0 - {session_manager.get_username()} ({user_type_display})")

View File

@@ -11,14 +11,16 @@
import os
import threading
import logging
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, simpledialog
from pathlib import Path
from io import StringIO
from contextlib import redirect_stdout
from typing import List, Dict
from gui.widgets import FileSelector, LogText
from gui.widgets import FileSelector, LogText, GuiTextHandler
from gui.config_manager import ConfigManager
from gui.log_config import setup_gui_logging, get_logger
from gui.material_type_management_dialog import MaterialTypeManagementDialog
from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO
import pandas as pd
@@ -156,6 +158,10 @@ class MaterialValidationTab(ttk.Frame):
self.select_all_managers_var: tk.BooleanVar = tk.BooleanVar(value=True) # 全选复选框状态
self.previously_selected_managers: List[str] = [] # 保存筛选状态
# 初始化统一日志系统
self.logger = get_logger(__name__)
self._gui_handler = None # 将在 _create_log_panel 中设置
self.create_widgets()
# 稍后显示就绪消息
@@ -379,6 +385,14 @@ class MaterialValidationTab(ttk.Frame):
self.log_text = LogText(parent, height=8, 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 _on_source_mode_change(self):
"""数据源模式切换"""
# PERMISSION CHECK: 仅管理员用户可以选择数据源模式
@@ -895,10 +909,13 @@ class MaterialValidationTab(ttk.Frame):
if line.strip():
self._update_log(line, "INFO")
if result_file and results:
if result_file and results and len(results) > 0:
self._update_log(f"校验完成,结果已保存到:{output_file}", "SUCCESS")
# 加载结果显示(带删除状态)
self._load_results_with_deletion_status(result_file)
elif results is not None and len(results) == 0:
# 已经在 validator 中输出详细错误信息,这里只做简单提示
self._update_log("校验失败:未找到物料记录,请查看上方日志了解详细原因", "ERROR")
else:
self._update_log("校验失败", "ERROR")
@@ -1159,19 +1176,24 @@ class MaterialValidationTab(ttk.Frame):
self._initialize_manager_filter()
def _update_log(self, message: str, level: str = "INFO"):
"""线程安全的日志更新"""
"""
线程安全的日志更新(兼容接口)
def update():
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)
通过统一的 logging 系统输出日志,自动同时输出到控制台和 GUI。
self.after(0, update)
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 export_results(self):
"""导出结果到 Excel"""

View File

@@ -7,5 +7,6 @@ GUI 自定义组件模块
from .file_selector import FileSelector
from .log_text import LogText
from .production_id_input import ProductionIdInput
from .log_handler import GuiTextHandler
__all__ = ['FileSelector', 'LogText', 'ProductionIdInput']
__all__ = ['FileSelector', 'LogText', 'ProductionIdInput', 'GuiTextHandler']

120
gui/widgets/log_handler.py Normal file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
自定义 logging Handler将日志输出到 LogText 组件
"""
import logging
import re
from typing import Optional
from gui.widgets.log_text import LogText
class GuiTextHandler(logging.Handler):
"""
将日志输出到 GUI LogText 组件的 Handler
这个 Handler 桥接了 Python 标准 logging 模块和 GUI 的 LogText 组件,
使得使用 logging 模块的代码可以自动将日志输出到 GUI 界面。
"""
def __init__(self, log_text: Optional[LogText] = None):
"""
初始化 Handler
Args:
log_text: LogText 组件实例,可以为 None稍后通过 set_log_text 设置
"""
super().__init__()
self.log_text = log_text
# 映射 logging 级别到 LogText 级别
self.level_map = {
logging.INFO: 'INFO',
logging.WARNING: 'WARNING',
logging.ERROR: 'ERROR',
logging.DEBUG: 'DEBUG',
logging.CRITICAL: 'ERROR'
}
def set_log_text(self, log_text: LogText):
"""
设置或更新 LogText 组件引用
Args:
log_text: LogText 组件实例
"""
self.log_text = log_text
def emit(self, record: logging.LogRecord):
"""
实现日志输出
Args:
record: logging.LogRecord 对象
"""
if not self.log_text:
return
try:
# 获取日志级别
level = self.level_map.get(record.levelno, 'INFO')
# 只获取消息内容不包含时间戳和级别LogText.log() 会添加)
message = record.getMessage()
# 移除消息中可能存在的冗余级别标记(如 "[INFO] "、"[ERROR] " 等)
# 这是因为有些代码在消息中已经包含了级别标记
message = self._strip_redundant_level_prefix(message)
# 定义更新函数
def update():
"""在主线程中更新 GUI"""
try:
# LogText.log() 会自动添加时间戳和级别
self.log_text.log(message, level)
except Exception:
# 如果 log 失败,忽略错误避免递归
pass
# 尝试使用 after 确保在主线程更新
import tkinter as tk
try:
# 尝试获取主窗口
widget = self.log_text
while widget and widget.master:
if isinstance(widget.master, tk.Tk):
# 找到主窗口,使用 after 调度更新
widget.master.after(0, update)
return
widget = widget.master
# 如果找不到主窗口,直接调用(适用于非 GUI 模式或测试)
update()
except Exception:
# 如果线程调度失败,直接调用
update()
except Exception:
# 处理错误,避免影响主程序
self.handleError(record)
def _strip_redundant_level_prefix(self, message: str) -> str:
"""
移除消息开头的冗余级别标记
例如:"[INFO] 读取 ProductionID 文件" -> "读取 ProductionID 文件"
"[ERROR] 错误信息" -> "错误信息"
Args:
message: 原始消息
Returns:
清理后的消息
"""
# 常见的日志级别标记模式
level_pattern = r'^\[(?:INFO|WARNING|ERROR|DEBUG|CRITICAL|WARN|SUCCESS)\]\s*'
match = re.match(level_pattern, message)
if match:
# 移除匹配到的级别前缀
return message[match.end():]
return message