初始化
This commit is contained in:
242
ui/log_viewer.py
Normal file
242
ui/log_viewer.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
日志查看器界面
|
||||
- 日志文件列表
|
||||
- 日志内容显示
|
||||
- 搜索过滤
|
||||
- 导出功能
|
||||
"""
|
||||
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]', '<span style="color: #ef4444; font-weight: bold;">[ERROR]</span>')
|
||||
text = text.replace('[WARNING]', '<span style="color: #f59e0b; font-weight: bold;">[WARNING]</span>')
|
||||
text = text.replace('[INFO]', '<span style="color: #22c55e;">[INFO]</span>')
|
||||
text = text.replace('[DEBUG]', '<span style="color: #64748b;">[DEBUG]</span>')
|
||||
|
||||
# 时间戳颜色
|
||||
import re
|
||||
text = re.sub(
|
||||
r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]',
|
||||
r'<span style="color: #94a3b8;">[\1]</span>',
|
||||
text
|
||||
)
|
||||
|
||||
return f'<pre style="margin: 0; font-family: Consolas, monospace;">{text}</pre>'
|
||||
|
||||
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}"
|
||||
|
||||
Reference in New Issue
Block a user