""" 日志查看器界面 - 日志文件列表 - 日志内容显示 - 搜索过滤 - 导出功能 """ from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem, QTextEdit, QSplitter, QFrame, QFileDialog, QMessageBox ) from PySide6.QtCore import Qt from PySide6.QtGui import QFont from pathlib import Path import logging from core.logger import get_all_log_files, read_log_file class LogViewer(QWidget): """日志查看器 - 可嵌入设置页面""" def __init__(self, parent=None): super().__init__(parent) self.current_log_content = "" self.setup_ui() self.load_log_files() def setup_ui(self): layout = QVBoxLayout(self) layout.setContentsMargins(16, 16, 16, 16) layout.setSpacing(12) # 主内容区 - 分割器 splitter = QSplitter(Qt.Orientation.Horizontal) splitter.setStyleSheet("QSplitter::handle { background: #334155; }") # 左侧 - 日志文件列表 left_panel = QFrame() left_panel.setStyleSheet("background: rgba(30, 41, 59, 0.5); border-radius: 12px;") left_layout = QVBoxLayout(left_panel) left_layout.setContentsMargins(12, 12, 12, 12) left_layout.setSpacing(8) list_title = QLabel("日志文件") list_title.setStyleSheet("color: #94a3b8; font-size: 12px; font-weight: 600;") left_layout.addWidget(list_title) self.file_list = QListWidget() self.file_list.setMinimumWidth(180) self.file_list.currentItemChanged.connect(self.on_file_selected) left_layout.addWidget(self.file_list) # 刷新按钮 refresh_btn = QPushButton("🔄 刷新") refresh_btn.setObjectName("secondary_btn") refresh_btn.clicked.connect(self.load_log_files) left_layout.addWidget(refresh_btn) splitter.addWidget(left_panel) # 右侧 - 日志内容 right_panel = QFrame() right_panel.setStyleSheet("background: rgba(30, 41, 59, 0.5); border-radius: 12px;") right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(12, 12, 12, 12) right_layout.setSpacing(8) # 搜索和操作栏 toolbar = QHBoxLayout() self.search_input = QLineEdit() self.search_input.setPlaceholderText("🔍 搜索日志内容...") self.search_input.textChanged.connect(self.filter_log) toolbar.addWidget(self.search_input, 1) # 级别过滤 self.level_buttons = {} for level, color in [("ERROR", "#ef4444"), ("WARNING", "#f59e0b"), ("INFO", "#22c55e"), ("DEBUG", "#64748b")]: btn = QPushButton(level) btn.setCheckable(True) btn.setChecked(True) btn.setStyleSheet(f""" QPushButton {{ background: rgba({self.hex_to_rgb(color)}, 0.2); border: 1px solid {color}; color: {color}; padding: 4px 8px; border-radius: 4px; font-size: 11px; }} QPushButton:checked {{ background: {color}; color: white; }} """) btn.clicked.connect(self.filter_log) toolbar.addWidget(btn) self.level_buttons[level] = btn right_layout.addLayout(toolbar) # 日志内容显示 self.log_text = QTextEdit() self.log_text.setReadOnly(True) self.log_text.setFont(QFont("Consolas", 11)) self.log_text.setStyleSheet(""" QTextEdit { background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 8px; color: #e2e8f0; } """) right_layout.addWidget(self.log_text, 1) # 底部操作 bottom_bar = QHBoxLayout() self.status_label = QLabel("选择左侧日志文件查看") self.status_label.setStyleSheet("color: #64748b; font-size: 12px;") bottom_bar.addWidget(self.status_label) bottom_bar.addStretch() export_btn = QPushButton("📤 导出日志") export_btn.setObjectName("primary_btn") export_btn.clicked.connect(self.export_log) bottom_bar.addWidget(export_btn) right_layout.addLayout(bottom_bar) splitter.addWidget(right_panel) splitter.setSizes([200, 600]) layout.addWidget(splitter, 1) def load_log_files(self): """加载日志文件列表""" self.file_list.clear() log_files = get_all_log_files() for log_file in log_files: item = QListWidgetItem(log_file.name) item.setData(Qt.ItemDataRole.UserRole, log_file) self.file_list.addItem(item) # 自动选择第一个(最新) if self.file_list.count() > 0: self.file_list.setCurrentRow(0) logging.info(f"加载了 {len(log_files)} 个日志文件") def on_file_selected(self, current: QListWidgetItem, previous: QListWidgetItem): """日志文件选择变化""" if current: file_path = current.data(Qt.ItemDataRole.UserRole) self.current_log_content = read_log_file(file_path) self.filter_log() self.status_label.setText(f"文件: {file_path.name}") def filter_log(self): """过滤日志内容""" if not self.current_log_content: return search_text = self.search_input.text().lower() active_levels = [level for level, btn in self.level_buttons.items() if btn.isChecked()] lines = self.current_log_content.split('\n') filtered_lines = [] for line in lines: # 检查级别过滤 level_match = any(f"[{level}]" in line for level in active_levels) if not level_match and line.strip(): # 如果不是日志行(没有级别标记),检查是否是错误堆栈的一部分 if filtered_lines and not line.startswith('['): level_match = True else: continue # 检查搜索过滤 if search_text and search_text not in line.lower(): continue filtered_lines.append(line) # 语法高亮 highlighted = self.highlight_log('\n'.join(filtered_lines)) self.log_text.setHtml(highlighted) def highlight_log(self, text: str) -> str: """日志语法高亮""" import html text = html.escape(text) # 替换颜色 text = text.replace('[ERROR]', '[ERROR]') text = text.replace('[WARNING]', '[WARNING]') text = text.replace('[INFO]', '[INFO]') text = text.replace('[DEBUG]', '[DEBUG]') # 时间戳颜色 import re text = re.sub( r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]', r'[\1]', text ) return f'
{text}'
def export_log(self):
"""导出日志文件"""
if not self.current_log_content:
QMessageBox.warning(self, "提示", "请先选择一个日志文件")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "导出日志", "log_export.txt", "文本文件 (*.txt)"
)
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(self.log_text.toPlainText())
QMessageBox.information(self, "成功", f"日志已导出到:\n{file_path}")
logging.info(f"日志已导出到: {file_path}")
except Exception as e:
QMessageBox.critical(self, "错误", f"导出失败: {e}")
logging.error(f"导出日志失败: {e}")
@staticmethod
def hex_to_rgb(hex_color: str) -> str:
"""十六进制颜色转RGB"""
hex_color = hex_color.lstrip('#')
r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
return f"{r}, {g}, {b}"