248 lines
8.3 KiB
Python
248 lines
8.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""日志查看器"""
|
|
|
|
import re
|
|
from datetime import datetime
|
|
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout,
|
|
QPlainTextEdit, QPushButton, QLineEdit, QLabel,
|
|
QComboBox, QCheckBox, QFileDialog, QMessageBox
|
|
)
|
|
from PySide6.QtCore import Qt, Signal, Slot
|
|
from PySide6.QtGui import QTextCharFormat, QColor, QFont, QTextCursor
|
|
|
|
|
|
class LogViewer(QWidget):
|
|
"""日志查看器"""
|
|
|
|
# 信号
|
|
log_cleared = Signal()
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.max_lines = 10000
|
|
self.auto_scroll = True
|
|
self.filter_text = ""
|
|
self.filter_level = "ALL"
|
|
self._all_logs = [] # 存储所有日志
|
|
|
|
self._init_ui()
|
|
self._connect_signals()
|
|
|
|
def _init_ui(self):
|
|
"""初始化界面"""
|
|
layout = QVBoxLayout(self)
|
|
layout.setContentsMargins(16, 16, 16, 16)
|
|
layout.setSpacing(8)
|
|
|
|
# 标题和工具栏
|
|
header_layout = QHBoxLayout()
|
|
|
|
title = QLabel("执行日志")
|
|
title.setProperty("heading", True)
|
|
header_layout.addWidget(title)
|
|
|
|
header_layout.addStretch()
|
|
|
|
# 日志级别过滤
|
|
header_layout.addWidget(QLabel("级别:"))
|
|
self.level_combo = QComboBox()
|
|
self.level_combo.addItems(["ALL", "INFO", "WARNING", "ERROR", "DEBUG"])
|
|
self.level_combo.setFixedWidth(100)
|
|
header_layout.addWidget(self.level_combo)
|
|
|
|
# 搜索框
|
|
header_layout.addWidget(QLabel("搜索:"))
|
|
self.search_edit = QLineEdit()
|
|
self.search_edit.setPlaceholderText("输入关键字...")
|
|
self.search_edit.setFixedWidth(200)
|
|
header_layout.addWidget(self.search_edit)
|
|
|
|
# 自动滚动
|
|
self.auto_scroll_check = QCheckBox("自动滚动")
|
|
self.auto_scroll_check.setChecked(True)
|
|
header_layout.addWidget(self.auto_scroll_check)
|
|
|
|
layout.addLayout(header_layout)
|
|
|
|
# 日志文本区域
|
|
self.log_text = QPlainTextEdit()
|
|
self.log_text.setObjectName("logViewer")
|
|
self.log_text.setReadOnly(True)
|
|
self.log_text.setFont(QFont("Consolas", 10))
|
|
self.log_text.setLineWrapMode(QPlainTextEdit.NoWrap)
|
|
layout.addWidget(self.log_text, 1)
|
|
|
|
# 底部工具栏
|
|
footer_layout = QHBoxLayout()
|
|
|
|
self.line_count_label = QLabel("0 行")
|
|
self.line_count_label.setProperty("subheading", True)
|
|
footer_layout.addWidget(self.line_count_label)
|
|
|
|
footer_layout.addStretch()
|
|
|
|
self.copy_btn = QPushButton("复制全部")
|
|
self.copy_btn.setProperty("secondary", True)
|
|
footer_layout.addWidget(self.copy_btn)
|
|
|
|
self.export_btn = QPushButton("导出")
|
|
self.export_btn.setProperty("secondary", True)
|
|
footer_layout.addWidget(self.export_btn)
|
|
|
|
self.clear_btn = QPushButton("清空")
|
|
self.clear_btn.setProperty("secondary", True)
|
|
footer_layout.addWidget(self.clear_btn)
|
|
|
|
layout.addLayout(footer_layout)
|
|
|
|
def _connect_signals(self):
|
|
"""连接信号"""
|
|
self.level_combo.currentTextChanged.connect(self._apply_filter)
|
|
self.search_edit.textChanged.connect(self._apply_filter)
|
|
self.auto_scroll_check.stateChanged.connect(self._toggle_auto_scroll)
|
|
self.copy_btn.clicked.connect(self._copy_all)
|
|
self.export_btn.clicked.connect(self._export_log)
|
|
self.clear_btn.clicked.connect(self._clear_log)
|
|
|
|
@Slot(str)
|
|
def append_log(self, text: str):
|
|
"""追加日志"""
|
|
# 添加时间戳(如果没有)
|
|
if not re.match(r'^\d{4}-\d{2}-\d{2}', text) and not text.startswith('['):
|
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
text = f"[{timestamp}] {text}"
|
|
|
|
# 存储到全部日志
|
|
self._all_logs.append(text)
|
|
|
|
# 限制日志行数
|
|
if len(self._all_logs) > self.max_lines:
|
|
self._all_logs = self._all_logs[-self.max_lines:]
|
|
|
|
# 检查是否通过过滤器
|
|
if self._matches_filter(text):
|
|
self._append_formatted_line(text)
|
|
|
|
# 更新行数
|
|
self._update_line_count()
|
|
|
|
def _append_formatted_line(self, text: str):
|
|
"""追加格式化的行"""
|
|
cursor = self.log_text.textCursor()
|
|
cursor.movePosition(QTextCursor.End)
|
|
|
|
# 设置格式
|
|
fmt = QTextCharFormat()
|
|
|
|
text_lower = text.lower()
|
|
if "[error]" in text_lower or "错误" in text or "失败" in text:
|
|
fmt.setForeground(QColor("#d93025"))
|
|
fmt.setFontWeight(QFont.Bold)
|
|
elif "[warning]" in text_lower or "警告" in text or "warn" in text_lower:
|
|
fmt.setForeground(QColor("#f9ab00"))
|
|
elif "[info]" in text_lower:
|
|
fmt.setForeground(QColor("#1a73e8"))
|
|
elif "[debug]" in text_lower:
|
|
fmt.setForeground(QColor("#9aa0a6"))
|
|
elif "[gui]" in text_lower:
|
|
fmt.setForeground(QColor("#1e8e3e"))
|
|
else:
|
|
fmt.setForeground(QColor("#333333"))
|
|
|
|
cursor.insertText(text + "\n", fmt)
|
|
|
|
# 自动滚动
|
|
if self.auto_scroll:
|
|
self.log_text.verticalScrollBar().setValue(
|
|
self.log_text.verticalScrollBar().maximum()
|
|
)
|
|
|
|
def _matches_filter(self, text: str) -> bool:
|
|
"""检查是否匹配过滤器"""
|
|
# 级别过滤
|
|
if self.filter_level != "ALL":
|
|
level_marker = f"[{self.filter_level}]"
|
|
if level_marker.lower() not in text.lower():
|
|
return False
|
|
|
|
# 文本过滤
|
|
if self.filter_text:
|
|
if self.filter_text.lower() not in text.lower():
|
|
return False
|
|
|
|
return True
|
|
|
|
def _apply_filter(self):
|
|
"""应用过滤器"""
|
|
self.filter_level = self.level_combo.currentText()
|
|
self.filter_text = self.search_edit.text().strip()
|
|
|
|
# 重新显示日志
|
|
self.log_text.clear()
|
|
for line in self._all_logs:
|
|
if self._matches_filter(line):
|
|
self._append_formatted_line(line)
|
|
|
|
self._update_line_count()
|
|
|
|
def _toggle_auto_scroll(self, state: int):
|
|
"""切换自动滚动"""
|
|
self.auto_scroll = state == Qt.Checked
|
|
|
|
def _copy_all(self):
|
|
"""复制全部日志"""
|
|
from PySide6.QtWidgets import QApplication
|
|
text = self.log_text.toPlainText()
|
|
QApplication.clipboard().setText(text)
|
|
QMessageBox.information(self, "提示", "日志已复制到剪贴板")
|
|
|
|
def _export_log(self):
|
|
"""导出日志"""
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
default_name = f"etl_log_{timestamp}.txt"
|
|
|
|
file_path, _ = QFileDialog.getSaveFileName(
|
|
self,
|
|
"导出日志",
|
|
default_name,
|
|
"文本文件 (*.txt);;日志文件 (*.log);;所有文件 (*.*)"
|
|
)
|
|
|
|
if not file_path:
|
|
return
|
|
|
|
try:
|
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
f.write(self.log_text.toPlainText())
|
|
QMessageBox.information(self, "成功", f"日志已导出到:\n{file_path}")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "错误", f"导出失败: {e}")
|
|
|
|
def _clear_log(self):
|
|
"""清空日志"""
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"确认",
|
|
"确定要清空所有日志吗?",
|
|
QMessageBox.Yes | QMessageBox.No,
|
|
QMessageBox.No
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
self._all_logs.clear()
|
|
self.log_text.clear()
|
|
self._update_line_count()
|
|
self.log_cleared.emit()
|
|
|
|
def _update_line_count(self):
|
|
"""更新行数显示"""
|
|
visible_count = self.log_text.document().blockCount() - 1
|
|
total_count = len(self._all_logs)
|
|
|
|
if visible_count < total_count:
|
|
self.line_count_label.setText(f"{visible_count} / {total_count} 行")
|
|
else:
|
|
self.line_count_label.setText(f"{total_count} 行")
|