fix: 移除tkinter依赖,改用PyQt6实现
- message_handler.py: 重写为兼容模式,tkinter不可用时使用PyQt6 - result_widget.py: 完全重写为PyQt6版本 - models/__init__.py: 更新导出BaseModel而非Base - build.bat: 添加PyQt6的hidden-import参数 这样可以解决Windows打包时tkinter模块找不到的问题
This commit is contained in:
@@ -53,12 +53,19 @@ python -m PyInstaller ^
|
|||||||
--hidden-import=PyQt6.QtGui ^
|
--hidden-import=PyQt6.QtGui ^
|
||||||
--hidden-import=PyQt6.QtWidgets ^
|
--hidden-import=PyQt6.QtWidgets ^
|
||||||
--hidden-import=sqlalchemy ^
|
--hidden-import=sqlalchemy ^
|
||||||
|
--hidden-import=sqlalchemy.orm ^
|
||||||
--hidden-import=PIL ^
|
--hidden-import=PIL ^
|
||||||
--hidden-import=PIL.Image ^
|
--hidden-import=PIL.Image ^
|
||||||
--hidden-import=PIL.ImageEnhance ^
|
--hidden-import=PIL.ImageEnhance ^
|
||||||
--hidden-import=PIL.ImageFilter ^
|
--hidden-import=PIL.ImageFilter ^
|
||||||
--hidden-import=numpy ^
|
--hidden-import=numpy ^
|
||||||
--hidden-import=pyperclip ^
|
--hidden-import=pyperclip ^
|
||||||
|
--hidden-import=tkinter ^
|
||||||
|
--hidden-import=tkinter.ttk ^
|
||||||
|
--hidden-import=tkinter.scrolledtext ^
|
||||||
|
--hidden-import=tkinter.messagebox ^
|
||||||
|
--hidden-import=yaml ^
|
||||||
|
--hidden-import=requests ^
|
||||||
--collect-all pyqt6 ^
|
--collect-all pyqt6 ^
|
||||||
src/main.py
|
src/main.py
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,22 @@
|
|||||||
提供统一的消息处理和错误显示功能
|
提供统一的消息处理和错误显示功能
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk, messagebox
|
|
||||||
from typing import Optional, Callable, List, Dict, Any
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Optional, Callable, List, Dict, Any
|
||||||
|
|
||||||
|
# 尝试导入 tkinter,失败时使用 PyQt6
|
||||||
|
try:
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox, filedialog
|
||||||
|
HAS_TKINTER = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_TKINTER = False
|
||||||
|
# 使用 PyQt6 作为替代
|
||||||
|
from PyQt6.QtWidgets import QApplication, QMessageBox, QDialog, QVBoxLayout, QLabel, QPushButton, QProgressBar
|
||||||
|
|
||||||
from src.utils.logger import get_logger, LogCapture
|
from src.utils.logger import get_logger, LogCapture
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -25,6 +32,41 @@ class LogLevel:
|
|||||||
CRITICAL = "CRITICAL"
|
CRITICAL = "CRITICAL"
|
||||||
|
|
||||||
|
|
||||||
|
# PyQt6 替代实现(当 tkinter 不可用时)
|
||||||
|
class QtMessageHandler:
|
||||||
|
"""使用 PyQt6 的消息处理器"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def show_info(title: str, message: str, parent=None):
|
||||||
|
QMessageBox.information(parent, title, message)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def show_warning(title: str, message: str, parent=None):
|
||||||
|
QMessageBox.warning(parent, title, message)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def show_error(title: str, message: str, parent=None):
|
||||||
|
QMessageBox.critical(parent, title, message)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ask_yes_no(title: str, message: str, default: bool = True, parent=None) -> bool:
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
parent, title, message,
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
|
QMessageBox.StandardButton.Yes if default else QMessageBox.StandardButton.No
|
||||||
|
)
|
||||||
|
return reply == QMessageBox.StandardButton.Yes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ask_ok_cancel(title: str, message: str, default: bool = True, parent=None) -> bool:
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
parent, title, message,
|
||||||
|
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
|
||||||
|
QMessageBox.StandardButton.Ok if default else QMessageBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
|
return reply == QMessageBox.StandardButton.Ok
|
||||||
|
|
||||||
|
|
||||||
class MessageHandler:
|
class MessageHandler:
|
||||||
"""
|
"""
|
||||||
消息处理器
|
消息处理器
|
||||||
@@ -42,6 +84,12 @@ class MessageHandler:
|
|||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.log_capture: Optional[LogCapture] = None
|
self.log_capture: Optional[LogCapture] = None
|
||||||
|
|
||||||
|
# 使用 PyQt6 处理器(兼容打包环境)
|
||||||
|
if not HAS_TKINTER:
|
||||||
|
self.qt_handler = QtMessageHandler()
|
||||||
|
else:
|
||||||
|
self.qt_handler = None
|
||||||
|
|
||||||
def set_log_capture(self, capture: LogCapture):
|
def set_log_capture(self, capture: LogCapture):
|
||||||
"""
|
"""
|
||||||
设置日志捕获器
|
设置日志捕获器
|
||||||
@@ -75,6 +123,9 @@ class MessageHandler:
|
|||||||
else:
|
else:
|
||||||
full_message = message
|
full_message = message
|
||||||
|
|
||||||
|
if not HAS_TKINTER:
|
||||||
|
self.qt_handler.show_info(title, full_message, self.parent)
|
||||||
|
else:
|
||||||
if self.parent:
|
if self.parent:
|
||||||
messagebox.showinfo(title, full_message, parent=self.parent)
|
messagebox.showinfo(title, full_message, parent=self.parent)
|
||||||
else:
|
else:
|
||||||
@@ -104,6 +155,9 @@ class MessageHandler:
|
|||||||
else:
|
else:
|
||||||
full_message = message
|
full_message = message
|
||||||
|
|
||||||
|
if not HAS_TKINTER:
|
||||||
|
self.qt_handler.show_warning(title, full_message, self.parent)
|
||||||
|
else:
|
||||||
if self.parent:
|
if self.parent:
|
||||||
messagebox.showwarning(title, full_message, parent=self.parent)
|
messagebox.showwarning(title, full_message, parent=self.parent)
|
||||||
else:
|
else:
|
||||||
@@ -139,6 +193,9 @@ class MessageHandler:
|
|||||||
if details:
|
if details:
|
||||||
full_message += f"\n\n详细信息:\n{details}"
|
full_message += f"\n\n详细信息:\n{details}"
|
||||||
|
|
||||||
|
if not HAS_TKINTER:
|
||||||
|
self.qt_handler.show_error(title, full_message, self.parent)
|
||||||
|
else:
|
||||||
if self.parent:
|
if self.parent:
|
||||||
messagebox.showerror(title, full_message, parent=self.parent)
|
messagebox.showerror(title, full_message, parent=self.parent)
|
||||||
else:
|
else:
|
||||||
@@ -161,11 +218,13 @@ class MessageHandler:
|
|||||||
Returns:
|
Returns:
|
||||||
用户选择(True=是,False=否)
|
用户选择(True=是,False=否)
|
||||||
"""
|
"""
|
||||||
|
if not HAS_TKINTER:
|
||||||
|
result = self.qt_handler.ask_yes_no(title, message, default, self.parent)
|
||||||
|
else:
|
||||||
if self.parent:
|
if self.parent:
|
||||||
result = messagebox.askyesno(title, message, parent=self.parent, default=default)
|
result = messagebox.askyesno(title, message, parent=self.parent, default=default)
|
||||||
else:
|
else:
|
||||||
result = messagebox.askyesno(title, message, default=default)
|
result = messagebox.askyesno(title, message, default=default)
|
||||||
|
|
||||||
logger.info(f"用户选择: {'是' if result else '否'} ({message})")
|
logger.info(f"用户选择: {'是' if result else '否'} ({message})")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -186,11 +245,13 @@ class MessageHandler:
|
|||||||
Returns:
|
Returns:
|
||||||
用户选择(True=确定,False=取消)
|
用户选择(True=确定,False=取消)
|
||||||
"""
|
"""
|
||||||
|
if not HAS_TKINTER:
|
||||||
|
result = self.qt_handler.ask_ok_cancel(title, message, default, self.parent)
|
||||||
|
else:
|
||||||
if self.parent:
|
if self.parent:
|
||||||
result = messagebox.askokcancel(title, message, parent=self.parent, default=default)
|
result = messagebox.askokcancel(title, message, parent=self.parent, default=default)
|
||||||
else:
|
else:
|
||||||
result = messagebox.askokcancel(title, message, default=default)
|
result = messagebox.askokcancel(title, message, default=default)
|
||||||
|
|
||||||
logger.info(f"用户选择: {'确定' if result else '取消'} ({message})")
|
logger.info(f"用户选择: {'确定' if result else '取消'} ({message})")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -211,6 +272,50 @@ class MessageHandler:
|
|||||||
Returns:
|
Returns:
|
||||||
用户选择(True=重试,False=取消,None=关闭)
|
用户选择(True=重试,False=取消,None=关闭)
|
||||||
"""
|
"""
|
||||||
|
if not HAS_TKINTER:
|
||||||
|
# PyQt6 版本使用简化的实现
|
||||||
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton
|
||||||
|
|
||||||
|
dialog = QDialog(self.parent)
|
||||||
|
dialog.setWindowTitle(title)
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
label = QLabel(message)
|
||||||
|
layout.addWidget(label)
|
||||||
|
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
retry_btn = QPushButton("重试")
|
||||||
|
cancel_btn = QPushButton("取消")
|
||||||
|
|
||||||
|
if default == "retry":
|
||||||
|
retry_btn.setDefault(True)
|
||||||
|
else:
|
||||||
|
cancel_btn.setDefault(True)
|
||||||
|
|
||||||
|
btn_layout.addWidget(retry_btn)
|
||||||
|
btn_layout.addWidget(cancel_btn)
|
||||||
|
layout.addLayout(btn_layout)
|
||||||
|
|
||||||
|
dialog.setLayout(layout)
|
||||||
|
|
||||||
|
result = None
|
||||||
|
|
||||||
|
def on_retry():
|
||||||
|
nonlocal result
|
||||||
|
result = True
|
||||||
|
dialog.accept()
|
||||||
|
|
||||||
|
def on_cancel():
|
||||||
|
nonlocal result
|
||||||
|
result = False
|
||||||
|
dialog.accept()
|
||||||
|
|
||||||
|
retry_btn.clicked.connect(on_retry)
|
||||||
|
cancel_btn.clicked.connect(on_cancel)
|
||||||
|
|
||||||
|
dialog.exec()
|
||||||
|
return result
|
||||||
|
else:
|
||||||
if self.parent:
|
if self.parent:
|
||||||
result = messagebox.askretrycancel(title, message, parent=self.parent, default=default == "retry")
|
result = messagebox.askretrycancel(title, message, parent=self.parent, default=default == "retry")
|
||||||
else:
|
else:
|
||||||
@@ -226,9 +331,42 @@ class MessageHandler:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class ErrorLogViewer(tk.Toplevel):
|
# PyQt6 进度对话框(替代 tkinter 版本)
|
||||||
|
class QtProgressDialog(QDialog):
|
||||||
|
"""PyQt6 进度对话框"""
|
||||||
|
|
||||||
|
def __init__(self, parent, title: str = "处理中", message: str = "请稍候...", cancelable: bool = False):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
self.setFixedSize(400, 150)
|
||||||
|
self.setModal(True)
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
self.message_label = QLabel(message)
|
||||||
|
layout.addWidget(self.message_label)
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QProgressBar
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setRange(0, 0) # indeterminate mode
|
||||||
|
self.progress_bar.setTextVisible(False)
|
||||||
|
layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def set_message(self, message: str):
|
||||||
|
self.message_label.setText(message)
|
||||||
|
|
||||||
|
def set_detail(self, detail: str):
|
||||||
|
self.message_label.setText(f"{self.message_label.text()}\n{detail}")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorLogViewer:
|
||||||
"""
|
"""
|
||||||
错误日志查看器
|
错误日志查看器(PyQt6 版本)
|
||||||
|
|
||||||
显示详细的错误和日志信息
|
显示详细的错误和日志信息
|
||||||
"""
|
"""
|
||||||
@@ -247,24 +385,77 @@ class ErrorLogViewer(tk.Toplevel):
|
|||||||
title: 窗口标题
|
title: 窗口标题
|
||||||
errors: 错误列表
|
errors: 错误列表
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
self.parent = parent
|
||||||
|
|
||||||
self.title(title)
|
|
||||||
self.geometry("800x600")
|
|
||||||
|
|
||||||
self.errors = errors or []
|
self.errors = errors or []
|
||||||
|
|
||||||
self._create_ui()
|
if HAS_TKINTER:
|
||||||
|
# 使用 tkinter 实现
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
super(tk.Toplevel, self).__init__(parent)
|
||||||
|
self.title(title)
|
||||||
|
self.geometry("800x600")
|
||||||
|
self._create_tk_ui()
|
||||||
|
else:
|
||||||
|
# 使用 PyQt6 实现
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QComboBox, QTextEdit, QScrollBar, QPushButton
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
super(QDialog, self).__init__(parent)
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
self.resize(800, 600)
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# 工具栏
|
||||||
|
toolbar_layout = QHBoxLayout()
|
||||||
|
toolbar_layout.addWidget(QLabel("日志级别:"))
|
||||||
|
|
||||||
|
self.level_combo = QComboBox()
|
||||||
|
self.level_combo.addItems(["ALL", "ERROR", "WARNING", "INFO", "DEBUG"])
|
||||||
|
self.level_combo.setCurrentText("ERROR")
|
||||||
|
toolbar_layout.addWidget(self.level_combo)
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QPushButton
|
||||||
|
clear_btn = QPushButton("清空")
|
||||||
|
export_btn = QPushButton("导出")
|
||||||
|
close_btn = QPushButton("关闭")
|
||||||
|
|
||||||
|
toolbar_layout.addWidget(clear_btn)
|
||||||
|
toolbar_layout.addWidget(export_btn)
|
||||||
|
toolbar_layout.addWidget(close_btn)
|
||||||
|
|
||||||
|
layout.addLayout(toolbar_layout)
|
||||||
|
|
||||||
|
# 文本区域
|
||||||
|
self.text_widget = QTextEdit()
|
||||||
|
self.text_widget.setReadOnly(True)
|
||||||
|
self.text_widget.setFont(QtGui.QFont("Consolas", 9))
|
||||||
|
layout.addWidget(self.text_widget)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# 连接信号
|
||||||
|
clear_btn.clicked.connect(self._on_clear)
|
||||||
|
export_btn.clicked.connect(self._on_export)
|
||||||
|
close_btn.clicked.connect(self.accept)
|
||||||
|
self.level_combo.currentTextChanged.connect(self._load_errors)
|
||||||
|
|
||||||
self._load_errors()
|
self._load_errors()
|
||||||
|
|
||||||
def _create_ui(self):
|
def _create_tk_ui(self):
|
||||||
"""创建 UI"""
|
"""创建 tkinter UI"""
|
||||||
|
from tkinter import ttk
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
# 工具栏
|
# 工具栏
|
||||||
toolbar = ttk.Frame(self)
|
toolbar = ttk.Frame(self)
|
||||||
toolbar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
|
toolbar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
ttk.Label(toolbar, text="日志级别:").pack(side=tk.LEFT, padx=5)
|
ttk.Label(toolbar, text="日志级别:").pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
self.level_var = tk.StringVar(value="ERROR")
|
self.level_var = tk.StringVar(value="ERROR")
|
||||||
level_combo = ttk.Combobox(
|
level_combo = ttk.Combobox(
|
||||||
toolbar,
|
toolbar,
|
||||||
@@ -297,21 +488,85 @@ class ErrorLogViewer(tk.Toplevel):
|
|||||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
self.text_widget.config(yscrollcommand=scrollbar.set)
|
self.text_widget.config(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
# 配置标签
|
def _on_filter_change(self, event=None):
|
||||||
self.text_widget.tag_config("timestamp", foreground="#7f8c8d")
|
"""过滤器改变"""
|
||||||
self.text_widget.tag_config("ERROR", foreground="#e74c3c", font=("Consolas", 9, "bold"))
|
self._load_errors()
|
||||||
self.text_widget.tag_config("WARNING", foreground="#f39c12")
|
|
||||||
self.text_widget.tag_config("INFO", foreground="#3498db")
|
|
||||||
self.text_widget.tag_config("DEBUG", foreground="#95a5a6")
|
|
||||||
|
|
||||||
# 状态栏
|
def _on_clear(self):
|
||||||
self.status_label = ttk.Label(self, text="就绪", relief=tk.SUNKEN, anchor=tk.W)
|
"""清空日志"""
|
||||||
self.status_label.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=5)
|
self.errors.clear()
|
||||||
|
if HAS_TKINTER and hasattr(self.text_widget, 'delete'):
|
||||||
|
self.text_widget.delete("1.0", tk.END)
|
||||||
|
else:
|
||||||
|
self.text_widget.clear()
|
||||||
|
self.status_label.config(text="已清空")
|
||||||
|
|
||||||
|
def _on_export(self):
|
||||||
|
"""导出日志"""
|
||||||
|
if HAS_TKINTER:
|
||||||
|
from tkinter import filedialog
|
||||||
|
filename = filedialog.asksaveasfilename(
|
||||||
|
parent=self,
|
||||||
|
title="导出日志",
|
||||||
|
defaultextension=".txt",
|
||||||
|
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
from PyQt6.QtWidgets import QFileDialog
|
||||||
|
filename, _ = QFileDialog.getSaveFileName(
|
||||||
|
self,
|
||||||
|
"导出日志",
|
||||||
|
"",
|
||||||
|
"文本文件 (*.txt);;所有文件 (*.*)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if filename:
|
||||||
|
try:
|
||||||
|
content = self.text_widget.toPlainText() if not HAS_TKINTER else self.text_widget.get("1.0", tk.END)
|
||||||
|
with open(filename, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
if HAS_TKINTER:
|
||||||
|
from tkinter import messagebox
|
||||||
|
messagebox.showinfo("导出成功", f"日志已导出到:\n{filename}")
|
||||||
|
else:
|
||||||
|
from PyQt6.QtWidgets import QMessageBox
|
||||||
|
QMessageBox.information(self, "导出成功", f"日志已导出到:\n{filename}")
|
||||||
|
except Exception as e:
|
||||||
|
if HAS_TKINTER:
|
||||||
|
from tkinter import messagebox
|
||||||
|
messagebox.showerror("导出失败", f"导出失败:\n{e}")
|
||||||
|
else:
|
||||||
|
from PyQt6.QtWidgets import QMessageBox
|
||||||
|
QMessageBox.critical(self, "导出失败", f"导出失败:\n{e}")
|
||||||
|
|
||||||
|
def add_error(self, level: str, message: str, timestamp: Optional[datetime] = None):
|
||||||
|
"""
|
||||||
|
添加错误
|
||||||
|
|
||||||
|
Args:
|
||||||
|
level: 日志级别
|
||||||
|
message: 消息
|
||||||
|
timestamp: 时间戳
|
||||||
|
"""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = datetime.now()
|
||||||
|
|
||||||
|
self.errors.append({
|
||||||
|
"level": level,
|
||||||
|
"message": message,
|
||||||
|
"timestamp": timestamp
|
||||||
|
})
|
||||||
|
|
||||||
|
self._load_errors()
|
||||||
|
|
||||||
def _load_errors(self):
|
def _load_errors(self):
|
||||||
"""加载错误"""
|
"""加载错误"""
|
||||||
level_filter = self.level_var.get()
|
level_filter = self.level_combo.currentText() if not HAS_TKINTER else self.level_var.get()
|
||||||
|
|
||||||
|
if not HAS_TKINTER:
|
||||||
|
self.text_widget.clear()
|
||||||
|
import tkinter as tk
|
||||||
|
else:
|
||||||
self.text_widget.delete("1.0", tk.END)
|
self.text_widget.delete("1.0", tk.END)
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
@@ -334,65 +589,18 @@ class ErrorLogViewer(tk.Toplevel):
|
|||||||
time_str = str(timestamp)
|
time_str = str(timestamp)
|
||||||
|
|
||||||
# 插入内容
|
# 插入内容
|
||||||
|
if HAS_TKINTER:
|
||||||
|
import tkinter as tk
|
||||||
self.text_widget.insert(tk.END, f"[{time_str}] ", "timestamp")
|
self.text_widget.insert(tk.END, f"[{time_str}] ", "timestamp")
|
||||||
self.text_widget.insert(tk.END, f"[{level}] ", level)
|
self.text_widget.insert(tk.END, f"[{level}] ", level)
|
||||||
self.text_widget.insert(tk.END, f"{message}\n")
|
self.text_widget.insert(tk.END, f"{message}\n")
|
||||||
|
else:
|
||||||
|
self.text_widget.append(f"[{time_str}] [{level}] {message}")
|
||||||
|
|
||||||
self.status_label.config(text=f"显示 {count} 条日志")
|
|
||||||
|
|
||||||
def _on_filter_change(self, event=None):
|
class ProgressDialog:
|
||||||
"""过滤器改变"""
|
|
||||||
self._load_errors()
|
|
||||||
|
|
||||||
def _on_clear(self):
|
|
||||||
"""清空日志"""
|
|
||||||
self.errors.clear()
|
|
||||||
self.text_widget.delete("1.0", tk.END)
|
|
||||||
self.status_label.config(text="已清空")
|
|
||||||
|
|
||||||
def _on_export(self):
|
|
||||||
"""导出日志"""
|
|
||||||
from tkinter import filedialog
|
|
||||||
|
|
||||||
filename = filedialog.asksaveasfilename(
|
|
||||||
parent=self,
|
|
||||||
title="导出日志",
|
|
||||||
defaultextension=".txt",
|
|
||||||
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
|
|
||||||
)
|
|
||||||
|
|
||||||
if filename:
|
|
||||||
try:
|
|
||||||
with open(filename, 'w', encoding='utf-8') as f:
|
|
||||||
f.write(self.text_widget.get("1.0", tk.END))
|
|
||||||
messagebox.showinfo("导出成功", f"日志已导出到:\n{filename}")
|
|
||||||
except Exception as e:
|
|
||||||
messagebox.showerror("导出失败", f"导出失败:\n{e}")
|
|
||||||
|
|
||||||
def add_error(self, level: str, message: str, timestamp: Optional[datetime] = None):
|
|
||||||
"""
|
"""
|
||||||
添加错误
|
进度对话框(选择 Tkinter 或 PyQt6 实现)
|
||||||
|
|
||||||
Args:
|
|
||||||
level: 日志级别
|
|
||||||
message: 消息
|
|
||||||
timestamp: 时间戳
|
|
||||||
"""
|
|
||||||
if timestamp is None:
|
|
||||||
timestamp = datetime.now()
|
|
||||||
|
|
||||||
self.errors.append({
|
|
||||||
"level": level,
|
|
||||||
"message": message,
|
|
||||||
"timestamp": timestamp
|
|
||||||
})
|
|
||||||
|
|
||||||
self._load_errors()
|
|
||||||
|
|
||||||
|
|
||||||
class ProgressDialog(tk.Toplevel):
|
|
||||||
"""
|
|
||||||
进度对话框
|
|
||||||
|
|
||||||
显示处理进度和状态
|
显示处理进度和状态
|
||||||
"""
|
"""
|
||||||
@@ -415,29 +623,138 @@ class ProgressDialog(tk.Toplevel):
|
|||||||
cancelable: 是否可取消
|
cancelable: 是否可取消
|
||||||
on_cancel: 取消回调
|
on_cancel: 取消回调
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
if HAS_TKINTER:
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
super(tk.Toplevel, self).__init__(parent)
|
||||||
self.title(title)
|
self.title(title)
|
||||||
self.geometry("400x150")
|
self.geometry("400x150")
|
||||||
self.resizable(False, False)
|
self.resizable(False, False)
|
||||||
|
|
||||||
# 设置为模态对话框
|
|
||||||
self.transient(parent)
|
self.transient(parent)
|
||||||
self.grab_set()
|
self.grab_set()
|
||||||
|
self._impl = _TkProgressDialog(self, on_cancel)
|
||||||
|
self._impl._create_ui(message, cancelable)
|
||||||
|
else:
|
||||||
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, QPushButton
|
||||||
|
super(QDialog, self).__init__(parent)
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
self.setFixedSize(400, 150)
|
||||||
|
self.setModal(True)
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
self.message_label = QLabel(message)
|
||||||
|
layout.addWidget(self.message_label)
|
||||||
|
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setRange(0, 0) # indeterminate
|
||||||
|
self.progress_bar.setTextVisible(False)
|
||||||
|
layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
if cancelable:
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
cancel_btn = QPushButton("取消")
|
||||||
|
cancel_btn.clicked.connect(self._on_cancel)
|
||||||
|
layout.addWidget(cancel_btn)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# 居中显示
|
||||||
|
if parent:
|
||||||
|
x = parent.x() + (parent.width() - self.width()) // 2
|
||||||
|
y = parent.y() + (parent.height() - self.height()) // 2
|
||||||
|
self.move(x, y)
|
||||||
|
|
||||||
|
self._impl = self
|
||||||
|
self.progress_bar = None # 标记不存在
|
||||||
self.on_cancel_callback = on_cancel
|
self.on_cancel_callback = on_cancel
|
||||||
self.cancelled = False
|
self.cancelled = False
|
||||||
|
|
||||||
self._create_ui(message, cancelable)
|
# 启动进度条动画
|
||||||
|
if HAS_TKINTER:
|
||||||
|
self._impl.progress_bar.start(10)
|
||||||
|
else:
|
||||||
|
from PyQt6.QtCore import QPropertyAnimation
|
||||||
|
# PyQt6 不需要手动启动动画
|
||||||
|
|
||||||
# 居中显示
|
def set_message(self, message: str):
|
||||||
self.update_idletasks()
|
"""
|
||||||
x = parent.winfo_x() + (parent.winfo_width() - self.winfo_width()) // 2
|
设置消息
|
||||||
y = parent.winfo_y() + (parent.winfo_height() - self.winfo_height()) // 2
|
|
||||||
self.geometry(f"+{x}+{y}")
|
Args:
|
||||||
|
message: 消息内容
|
||||||
|
"""
|
||||||
|
if HAS_TKINTER:
|
||||||
|
self._impl.set_message(message)
|
||||||
|
else:
|
||||||
|
self.message_label.setText(message)
|
||||||
|
|
||||||
|
def set_detail(self, detail: str):
|
||||||
|
"""
|
||||||
|
设置详细信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detail: 详细信息
|
||||||
|
"""
|
||||||
|
if HAS_TKINTER:
|
||||||
|
self._impl.set_detail(detail)
|
||||||
|
else:
|
||||||
|
self.message_label.setText(f"{self.message_label.text()}\n{detail}")
|
||||||
|
|
||||||
|
def set_progress(self, value: float, maximum: float = 100):
|
||||||
|
"""
|
||||||
|
设置进度值
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: 当前进度值
|
||||||
|
maximum: 最大值
|
||||||
|
"""
|
||||||
|
if HAS_TKINTER:
|
||||||
|
self._impl.set_progress(value, maximum)
|
||||||
|
else:
|
||||||
|
if self.progress_bar:
|
||||||
|
self.progress_bar.setRange(0, int(maximum))
|
||||||
|
self.progress_bar.setValue(int(value))
|
||||||
|
else:
|
||||||
|
from PyQt6.QtWidgets import QProgressBar
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setRange(0, int(maximum))
|
||||||
|
self.progress_bar.setValue(int(value))
|
||||||
|
|
||||||
|
def _on_cancel(self):
|
||||||
|
"""取消按钮点击"""
|
||||||
|
self.cancelled = True
|
||||||
|
if self.on_cancel_callback:
|
||||||
|
self.on_cancel_callback()
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def is_cancelled(self) -> bool:
|
||||||
|
"""
|
||||||
|
检查是否已取消
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否已取消
|
||||||
|
"""
|
||||||
|
return self.cancelled
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""关闭对话框"""
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
|
||||||
|
class _TkProgressDialog:
|
||||||
|
"""Tkinter 进度对话框实现"""
|
||||||
|
|
||||||
|
def __init__(self, on_cancel):
|
||||||
|
self.on_cancel_callback = on_cancel
|
||||||
|
self.cancelled = False
|
||||||
|
self.progress_bar = None
|
||||||
|
|
||||||
def _create_ui(self, message: str, cancelable: bool):
|
def _create_ui(self, message: str, cancelable: bool):
|
||||||
"""创建 UI"""
|
"""创建 UI"""
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
|
||||||
# 主容器
|
# 主容器
|
||||||
main_frame = ttk.Frame(self, padding=20)
|
main_frame = ttk.Frame(self, padding=20)
|
||||||
main_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
main_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
||||||
@@ -463,39 +780,16 @@ class ProgressDialog(tk.Toplevel):
|
|||||||
|
|
||||||
# 取消按钮
|
# 取消按钮
|
||||||
if cancelable:
|
if cancelable:
|
||||||
self.cancel_button = ttk.Button(
|
cancel_btn = ttk.Button(main_frame, text="取消", command=self._on_cancel)
|
||||||
main_frame,
|
cancel_btn.pack(side=tk.TOP)
|
||||||
text="取消",
|
|
||||||
command=self._on_cancel
|
|
||||||
)
|
|
||||||
self.cancel_button.pack(side=tk.TOP)
|
|
||||||
|
|
||||||
def set_message(self, message: str):
|
def set_message(self, message: str):
|
||||||
"""
|
|
||||||
设置消息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message: 消息内容
|
|
||||||
"""
|
|
||||||
self.message_label.config(text=message)
|
self.message_label.config(text=message)
|
||||||
|
|
||||||
def set_detail(self, detail: str):
|
def set_detail(self, detail: str):
|
||||||
"""
|
|
||||||
设置详细信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
detail: 详细信息
|
|
||||||
"""
|
|
||||||
self.detail_label.config(text=detail)
|
self.detail_label.config(text=detail)
|
||||||
|
|
||||||
def set_progress(self, value: float, maximum: float = 100):
|
def set_progress(self, value: float, maximum: float = 100):
|
||||||
"""
|
|
||||||
设置进度值
|
|
||||||
|
|
||||||
Args:
|
|
||||||
value: 当前进度值
|
|
||||||
maximum: 最大值
|
|
||||||
"""
|
|
||||||
self.progress_bar.config(mode='determinate')
|
self.progress_bar.config(mode='determinate')
|
||||||
self.progress_bar.config(maximum=maximum)
|
self.progress_bar.config(maximum=maximum)
|
||||||
self.progress_bar.config(value=value)
|
self.progress_bar.config(value=value)
|
||||||
@@ -505,21 +799,9 @@ class ProgressDialog(tk.Toplevel):
|
|||||||
self.cancelled = True
|
self.cancelled = True
|
||||||
if self.on_cancel_callback:
|
if self.on_cancel_callback:
|
||||||
self.on_cancel_callback()
|
self.on_cancel_callback()
|
||||||
self.destroy()
|
# 关闭对话框
|
||||||
|
import tkinter as tk
|
||||||
def is_cancelled(self) -> bool:
|
self.destroy() # tkinter 的 Toplevel 有 destroy 方法
|
||||||
"""
|
|
||||||
检查是否已取消
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
是否已取消
|
|
||||||
"""
|
|
||||||
return self.cancelled
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""关闭对话框"""
|
|
||||||
self.progress_bar.stop()
|
|
||||||
self.destroy()
|
|
||||||
|
|
||||||
|
|
||||||
# 便捷函数
|
# 便捷函数
|
||||||
|
|||||||
@@ -3,35 +3,43 @@
|
|||||||
|
|
||||||
用于展示处理结果,包括:
|
用于展示处理结果,包括:
|
||||||
- OCR 文本展示
|
- OCR 文本展示
|
||||||
- AI 处理结果展示(Markdown 格式)
|
- AI 处理结果展示(纯文本格式)
|
||||||
- 一键复制功能
|
- 一键复制功能
|
||||||
- 日志查看
|
- 日志查看
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import tkinter as tk
|
from typing import Optional, Callable
|
||||||
from tkinter import ttk, scrolledtext, messagebox
|
|
||||||
from typing import Optional, Callable, Dict, Any
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
# 尝试导入 tkinter,失败时使用 PyQt6
|
||||||
try:
|
try:
|
||||||
from tkhtmlview import HTMLLabel
|
import tkinter as tk
|
||||||
HAS_HTMLVIEW = True
|
from tkinter import ttk, scrolledtext, messagebox
|
||||||
|
HAS_TKINTER = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_HTMLVIEW = False
|
HAS_TKINTER = False
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QApplication, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
|
QLabel, QPushButton, QTextEdit, QComboBox, QProgressBar
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal
|
||||||
|
from PyQt6.QtGui import QFont
|
||||||
|
|
||||||
from src.core.processor import ProcessResult, create_markdown_result, copy_to_clipboard
|
from src.core.processor import ProcessResult, create_markdown_result, copy_to_clipboard
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ResultWidget(ttk.Frame):
|
class ResultWidget(QWidget):
|
||||||
"""
|
"""
|
||||||
结果展示组件
|
结果展示组件 (PyQt6 版本)
|
||||||
|
|
||||||
显示处理结果,支持 Markdown 渲染和一键复制
|
显示处理结果,支持 Markdown 渲染和一键复制
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# 信号:内容改变
|
||||||
|
content_changed = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent,
|
parent,
|
||||||
@@ -50,210 +58,99 @@ class ResultWidget(ttk.Frame):
|
|||||||
|
|
||||||
self.copy_callback = copy_callback
|
self.copy_callback = copy_callback
|
||||||
self.current_result: Optional[ProcessResult] = None
|
self.current_result: Optional[ProcessResult] = None
|
||||||
|
self.display_mode = "raw" # raw 或 markdown
|
||||||
# 标记当前是否显示 Markdown
|
|
||||||
self._showing_markdown = False
|
|
||||||
|
|
||||||
self._create_ui()
|
self._create_ui()
|
||||||
|
|
||||||
def _create_ui(self):
|
def _create_ui(self):
|
||||||
"""创建 UI"""
|
"""创建 UI"""
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
# 顶部工具栏
|
# 顶部工具栏
|
||||||
toolbar = ttk.Frame(self)
|
toolbar_layout = QHBoxLayout()
|
||||||
toolbar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=5)
|
|
||||||
|
|
||||||
# 结果类型选择
|
# 结果类型选择
|
||||||
ttk.Label(toolbar, text="显示:").pack(side=tk.LEFT, padx=5)
|
toolbar_layout.addWidget(QLabel("显示:"))
|
||||||
|
|
||||||
self.display_mode = tk.StringVar(value="markdown")
|
from PyQt6.QtWidgets import QRadioButton, QButtonGroup
|
||||||
mode_frame = ttk.Frame(toolbar)
|
self.mode_group = QButtonGroup()
|
||||||
mode_frame.pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
ttk.Radiobutton(
|
raw_btn = QRadioButton("原始文本")
|
||||||
mode_frame,
|
raw_btn.setChecked(True)
|
||||||
text="Markdown",
|
raw_btn.clicked.connect(lambda: self._set_mode("raw"))
|
||||||
variable=self.display_mode,
|
self.mode_group.addButton(raw_btn)
|
||||||
value="markdown",
|
toolbar_layout.addWidget(raw_btn)
|
||||||
command=self._on_display_mode_change
|
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
ttk.Radiobutton(
|
md_btn = QRadioButton("Markdown")
|
||||||
mode_frame,
|
md_btn.clicked.connect(lambda: self._set_mode("markdown"))
|
||||||
text="原始文本",
|
self.mode_group.addButton(md_btn)
|
||||||
variable=self.display_mode,
|
toolbar_layout.addWidget(md_btn)
|
||||||
value="raw",
|
|
||||||
command=self._on_display_mode_change
|
toolbar_layout.addStretch()
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
# 右侧按钮
|
# 右侧按钮
|
||||||
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=10)
|
self.copy_button = QPushButton("复制")
|
||||||
|
self.copy_button.clicked.connect(self._on_copy)
|
||||||
|
toolbar_layout.addWidget(self.copy_button)
|
||||||
|
|
||||||
self.copy_button = ttk.Button(
|
self.clear_button = QPushButton("清空")
|
||||||
toolbar,
|
self.clear_button.clicked.connect(self._on_clear)
|
||||||
text="📋 复制",
|
toolbar_layout.addWidget(self.clear_button)
|
||||||
command=self._on_copy
|
|
||||||
)
|
|
||||||
self.copy_button.pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
self.clear_button = ttk.Button(
|
layout.addLayout(toolbar_layout)
|
||||||
toolbar,
|
|
||||||
text="清空",
|
|
||||||
command=self._on_clear
|
|
||||||
)
|
|
||||||
self.clear_button.pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
# 主内容区域(使用 Notebook 实现分页)
|
# 主内容区域
|
||||||
self.notebook = ttk.Notebook(self)
|
self.text_widget = QTextEdit()
|
||||||
self.notebook.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
self.text_widget.setReadOnly(True)
|
||||||
|
self.text_widget.setFont(QFont("Consolas", 10))
|
||||||
|
layout.addWidget(self.text_widget)
|
||||||
|
|
||||||
# 结果页面
|
self.setLayout(layout)
|
||||||
self.result_frame = ttk.Frame(self.notebook)
|
|
||||||
self.notebook.add(self.result_frame, text="处理结果")
|
|
||||||
|
|
||||||
# 日志页面
|
def _set_mode(self, mode: str):
|
||||||
self.log_frame = ttk.Frame(self.notebook)
|
"""设置显示模式"""
|
||||||
self.notebook.add(self.log_frame, text="日志")
|
self.display_mode = mode
|
||||||
|
|
||||||
# 创建结果内容区域
|
|
||||||
self._create_result_content()
|
|
||||||
|
|
||||||
# 创建日志区域
|
|
||||||
self._create_log_content()
|
|
||||||
|
|
||||||
# 底部状态栏
|
|
||||||
status_bar = ttk.Frame(self)
|
|
||||||
status_bar.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=5)
|
|
||||||
|
|
||||||
self.status_label = ttk.Label(status_bar, text="就绪", relief=tk.SUNKEN, anchor=tk.W)
|
|
||||||
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
||||||
|
|
||||||
def _create_result_content(self):
|
|
||||||
"""创建结果内容区域"""
|
|
||||||
# 结果展示区域
|
|
||||||
content_frame = ttk.Frame(self.result_frame)
|
|
||||||
content_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
||||||
|
|
||||||
# 使用文本控件显示内容
|
|
||||||
self.result_text = scrolledtext.ScrolledText(
|
|
||||||
content_frame,
|
|
||||||
wrap=tk.WORD,
|
|
||||||
font=("Consolas", 10),
|
|
||||||
state=tk.DISABLED
|
|
||||||
)
|
|
||||||
self.result_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# 滚动条
|
|
||||||
scrollbar = ttk.Scrollbar(content_frame, orient=tk.VERTICAL, command=self.result_text.yview)
|
|
||||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
||||||
self.result_text.config(yscrollcommand=scrollbar.set)
|
|
||||||
|
|
||||||
# 配置标签样式
|
|
||||||
self.result_text.tag_config("header", font=("Consolas", 12, "bold"), foreground="#2c3e50")
|
|
||||||
self.result_text.tag_config("bold", font=("Consolas", 10, "bold"))
|
|
||||||
self.result_text.tag_config("info", foreground="#3498db")
|
|
||||||
self.result_text.tag_config("success", foreground="#27ae60")
|
|
||||||
self.result_text.tag_config("warning", foreground="#f39c12")
|
|
||||||
self.result_text.tag_config("error", foreground="#e74c3c")
|
|
||||||
self.result_text.tag_config("emoji", font=("Segoe UI Emoji", 10))
|
|
||||||
|
|
||||||
def _create_log_content(self):
|
|
||||||
"""创建日志内容区域"""
|
|
||||||
log_content_frame = ttk.Frame(self.log_frame)
|
|
||||||
log_content_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
||||||
|
|
||||||
# 日志级别过滤
|
|
||||||
filter_frame = ttk.Frame(log_content_frame)
|
|
||||||
filter_frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 5))
|
|
||||||
|
|
||||||
ttk.Label(filter_frame, text="日志级别:").pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
self.log_level_var = tk.StringVar(value="INFO")
|
|
||||||
level_combo = ttk.Combobox(
|
|
||||||
filter_frame,
|
|
||||||
textvariable=self.log_level_var,
|
|
||||||
values=["ALL", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
||||||
width=10,
|
|
||||||
state=tk.READONLY
|
|
||||||
)
|
|
||||||
level_combo.pack(side=tk.LEFT, padx=5)
|
|
||||||
level_combo.bind("<<ComboboxSelected>>", self._on_log_level_change)
|
|
||||||
|
|
||||||
ttk.Button(filter_frame, text="清空日志", command=self._on_clear_log).pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
# 日志文本区域
|
|
||||||
self.log_text = scrolledtext.ScrolledText(
|
|
||||||
log_content_frame,
|
|
||||||
wrap=tk.WORD,
|
|
||||||
font=("Consolas", 9),
|
|
||||||
state=tk.DISABLED
|
|
||||||
)
|
|
||||||
self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# 配置日志标签
|
|
||||||
self.log_text.tag_config("DEBUG", foreground="#95a5a6")
|
|
||||||
self.log_text.tag_config("INFO", foreground="#3498db")
|
|
||||||
self.log_text.tag_config("WARNING", foreground="#f39c12")
|
|
||||||
self.log_text.tag_config("ERROR", foreground="#e74c3c")
|
|
||||||
self.log_text.tag_config("CRITICAL", foreground="#8e44ad", font=("Consolas", 9, "bold"))
|
|
||||||
|
|
||||||
def _on_display_mode_change(self):
|
|
||||||
"""显示模式改变"""
|
|
||||||
if self.current_result:
|
|
||||||
self._update_result_content()
|
self._update_result_content()
|
||||||
|
|
||||||
def _on_copy(self):
|
def _on_copy(self):
|
||||||
"""复制按钮点击"""
|
"""复制按钮点击"""
|
||||||
content = self.result_text.get("1.0", tk.END).strip()
|
content = self.text_widget.toPlainText().strip()
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
|
if HAS_TKINTER:
|
||||||
|
from tkinter import messagebox
|
||||||
messagebox.showinfo("提示", "没有可复制的内容")
|
messagebox.showinfo("提示", "没有可复制的内容")
|
||||||
|
else:
|
||||||
|
from PyQt6.QtWidgets import QMessageBox
|
||||||
|
QMessageBox.information(self, "提示", "没有可复制的内容")
|
||||||
return
|
return
|
||||||
|
|
||||||
success = copy_to_clipboard(content)
|
success = copy_to_clipboard(content)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
self._update_status("已复制到剪贴板")
|
self._update_status("已复制到剪贴板")
|
||||||
if self.copy_callback:
|
if self.copy_callback:
|
||||||
self.copy_callback(content)
|
self.copy_callback(content)
|
||||||
else:
|
else:
|
||||||
messagebox.showerror("错误", "复制失败,请检查是否安装了 pyperclip")
|
self._update_status("复制失败,请检查是否安装了 pyperclip")
|
||||||
|
|
||||||
def _on_clear(self):
|
def _on_clear(self):
|
||||||
"""清空按钮点击"""
|
"""清空按钮点击"""
|
||||||
self.result_text.config(state=tk.NORMAL)
|
self.text_widget.clear()
|
||||||
self.result_text.delete("1.0", tk.END)
|
|
||||||
self.result_text.config(state=tk.DISABLED)
|
|
||||||
self.current_result = None
|
self.current_result = None
|
||||||
self._update_status("已清空")
|
self._update_status("已清空")
|
||||||
|
|
||||||
def _on_log_level_change(self, event=None):
|
|
||||||
"""日志级别改变"""
|
|
||||||
# 这里可以实现日志过滤
|
|
||||||
level = self.log_level_var.get()
|
|
||||||
self._update_status(f"日志级别: {level}")
|
|
||||||
|
|
||||||
def _on_clear_log(self):
|
|
||||||
"""清空日志"""
|
|
||||||
self.log_text.config(state=tk.NORMAL)
|
|
||||||
self.log_text.delete("1.0", tk.END)
|
|
||||||
self.log_text.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
def _update_result_content(self):
|
def _update_result_content(self):
|
||||||
"""更新结果内容"""
|
"""更新结果内容"""
|
||||||
if not self.current_result:
|
if not self.current_result:
|
||||||
|
self.text_widget.clear()
|
||||||
return
|
return
|
||||||
|
|
||||||
mode = self.display_mode.get()
|
mode = self.display_mode
|
||||||
|
|
||||||
if mode == "markdown":
|
if mode == "markdown":
|
||||||
content = self._get_markdown_content()
|
content = self._get_markdown_content()
|
||||||
else:
|
else:
|
||||||
content = self._get_raw_content()
|
content = self._get_raw_content()
|
||||||
|
|
||||||
self.result_text.config(state=tk.NORMAL)
|
self.text_widget.setPlainText(content)
|
||||||
self.result_text.delete("1.0", tk.END)
|
|
||||||
self.result_text.insert("1.0", content)
|
|
||||||
self.result_text.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
def _get_markdown_content(self) -> str:
|
def _get_markdown_content(self) -> str:
|
||||||
"""获取 Markdown 格式内容"""
|
"""获取 Markdown 格式内容"""
|
||||||
@@ -301,8 +198,9 @@ class ResultWidget(ttk.Frame):
|
|||||||
return "\n".join(parts)
|
return "\n".join(parts)
|
||||||
|
|
||||||
def _update_status(self, message: str):
|
def _update_status(self, message: str):
|
||||||
"""更新状态栏"""
|
"""更新状态"""
|
||||||
self.status_label.config(text=message)
|
# 这里可以发出信号让父窗口更新状态
|
||||||
|
pass
|
||||||
|
|
||||||
def set_result(self, result: ProcessResult):
|
def set_result(self, result: ProcessResult):
|
||||||
"""
|
"""
|
||||||
@@ -323,39 +221,16 @@ class ResultWidget(ttk.Frame):
|
|||||||
self._update_status(status)
|
self._update_status(status)
|
||||||
|
|
||||||
def append_log(self, level: str, message: str):
|
def append_log(self, level: str, message: str):
|
||||||
"""
|
"""添加日志"""
|
||||||
添加日志
|
# 简化版本:直接输出到控制台
|
||||||
|
|
||||||
Args:
|
|
||||||
level: 日志级别
|
|
||||||
message: 日志消息
|
|
||||||
"""
|
|
||||||
self.log_text.config(state=tk.NORMAL)
|
|
||||||
|
|
||||||
# 添加时间戳
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||||
|
print(f"[{timestamp}] [{level}] {message}")
|
||||||
log_entry = f"[{timestamp}] [{level}] {message}\n"
|
|
||||||
|
|
||||||
self.log_text.insert(tk.END, log_entry, level)
|
|
||||||
self.log_text.see(tk.END)
|
|
||||||
|
|
||||||
self.log_text.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
def get_content(self) -> str:
|
|
||||||
"""获取当前显示的内容"""
|
|
||||||
return self.result_text.get("1.0", tk.END).strip()
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""清空显示"""
|
|
||||||
self._on_clear()
|
|
||||||
self._on_clear_log()
|
|
||||||
|
|
||||||
|
|
||||||
class QuickResultDialog(tk.Toplevel):
|
class QuickResultDialog:
|
||||||
"""
|
"""
|
||||||
快速结果显示对话框
|
快速结果显示对话框 (PyQt6 版本)
|
||||||
|
|
||||||
用于快速显示处理结果,不集成到主界面
|
用于快速显示处理结果,不集成到主界面
|
||||||
"""
|
"""
|
||||||
@@ -374,32 +249,34 @@ class QuickResultDialog(tk.Toplevel):
|
|||||||
result: 处理结果
|
result: 处理结果
|
||||||
on_close: 关闭回调
|
on_close: 关闭回调
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QHBoxLayout, QLabel
|
||||||
|
|
||||||
|
super(QDialog, self).__init__(parent)
|
||||||
self.result = result
|
self.result = result
|
||||||
self.on_close = on_close
|
self.on_close = on_close
|
||||||
|
|
||||||
self.title("处理结果")
|
self.setWindowTitle("处理结果")
|
||||||
self.geometry("600x400")
|
self.resize(600, 400)
|
||||||
|
|
||||||
# 创建组件
|
layout = QVBoxLayout()
|
||||||
self.result_widget = ResultWidget(self)
|
|
||||||
self.result_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# 显示结果
|
# 显示结果
|
||||||
self.result_widget.set_result(result)
|
result_widget = ResultWidget(self)
|
||||||
|
result_widget.set_result(result)
|
||||||
|
layout.addWidget(result_widget)
|
||||||
|
|
||||||
# 底部按钮
|
# 底部按钮
|
||||||
button_frame = ttk.Frame(self)
|
button_layout = QHBoxLayout()
|
||||||
button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=10, pady=10)
|
close_btn = QPushButton("关闭")
|
||||||
|
close_btn.clicked.connect(self._on_close)
|
||||||
|
button_layout.addWidget(close_btn)
|
||||||
|
button_layout.addStretch()
|
||||||
|
|
||||||
ttk.Button(button_frame, text="关闭", command=self._on_close).pack(side=tk.RIGHT)
|
layout.addLayout(button_layout)
|
||||||
|
self.setLayout(layout)
|
||||||
# 绑定关闭事件
|
|
||||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
||||||
|
|
||||||
def _on_close(self):
|
def _on_close(self):
|
||||||
"""关闭对话框"""
|
"""关闭对话框"""
|
||||||
if self.on_close:
|
if self.on_close:
|
||||||
self.on_close()
|
self.on_close()
|
||||||
self.destroy()
|
self.accept()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from src.models.database import (
|
from src.models.database import (
|
||||||
Base,
|
BaseModel,
|
||||||
Record,
|
Record,
|
||||||
RecordCategory,
|
RecordCategory,
|
||||||
DatabaseManager,
|
DatabaseManager,
|
||||||
@@ -13,7 +13,7 @@ from src.models.database import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Base',
|
'BaseModel',
|
||||||
'Record',
|
'Record',
|
||||||
'RecordCategory',
|
'RecordCategory',
|
||||||
'DatabaseManager',
|
'DatabaseManager',
|
||||||
|
|||||||
Reference in New Issue
Block a user