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:
@@ -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
43
gui/log_config.py
Normal 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)
|
||||
@@ -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})")
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
120
gui/widgets/log_handler.py
Normal 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
|
||||
Reference in New Issue
Block a user