532 lines
20 KiB
Python
532 lines
20 KiB
Python
# dialogs_history.py
|
||
"""操作历史对话框"""
|
||
|
||
import tkinter as tk
|
||
from tkinter import ttk, messagebox, filedialog
|
||
from datetime import datetime
|
||
from operation_history import OperationHistory, PartitionTableBackup, OperationStatus
|
||
|
||
|
||
class OperationHistoryDialog:
|
||
"""操作历史对话框"""
|
||
|
||
def __init__(self, parent):
|
||
self.parent = parent
|
||
self.history = OperationHistory()
|
||
|
||
self.dialog = tk.Toplevel(parent)
|
||
self.dialog.title("操作历史")
|
||
self.dialog.geometry("900x600")
|
||
self.dialog.transient(parent)
|
||
|
||
self._center_window()
|
||
self._create_widgets()
|
||
self._load_history()
|
||
|
||
def _center_window(self):
|
||
self.dialog.update_idletasks()
|
||
width = self.dialog.winfo_width()
|
||
height = self.dialog.winfo_height()
|
||
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
|
||
y = (self.dialog.winfo_screenheight() // 2) - (height // 2)
|
||
self.dialog.geometry(f"{width}x{height}+{x}+{y}")
|
||
|
||
def _create_widgets(self):
|
||
main_frame = ttk.Frame(self.dialog, padding="10")
|
||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# 标题
|
||
title_label = ttk.Label(
|
||
main_frame,
|
||
text="操作历史记录",
|
||
font=("Arial", 14, "bold")
|
||
)
|
||
title_label.pack(pady=(0, 10))
|
||
|
||
# 工具栏
|
||
toolbar = ttk.Frame(main_frame)
|
||
toolbar.pack(fill=tk.X, pady=(0, 10))
|
||
|
||
ttk.Button(toolbar, text="刷新", command=self._load_history).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(toolbar, text="导出", command=self._export_history).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(toolbar, text="清理旧记录", command=self._clear_old).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(toolbar, text="清空所有", command=self._clear_all).pack(side=tk.LEFT, padx=5)
|
||
|
||
# 历史记录列表
|
||
list_frame = ttk.Frame(main_frame)
|
||
list_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
columns = ("时间", "操作类型", "设备", "状态", "详情", "耗时")
|
||
self.tree = ttk.Treeview(list_frame, columns=columns, show="headings")
|
||
|
||
for col in columns:
|
||
self.tree.heading(col, text=col)
|
||
|
||
self.tree.column("时间", width=150)
|
||
self.tree.column("操作类型", width=120)
|
||
self.tree.column("设备", width=100)
|
||
self.tree.column("状态", width=80)
|
||
self.tree.column("详情", width=300)
|
||
self.tree.column("耗时", width=80)
|
||
|
||
# 滚动条
|
||
scrollbar_y = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
||
scrollbar_x = ttk.Scrollbar(list_frame, orient=tk.HORIZONTAL, command=self.tree.xview)
|
||
self.tree.configure(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set)
|
||
|
||
self.tree.grid(row=0, column=0, sticky="nsew")
|
||
scrollbar_y.grid(row=0, column=1, sticky="ns")
|
||
scrollbar_x.grid(row=1, column=0, sticky="ew")
|
||
|
||
list_frame.grid_rowconfigure(0, weight=1)
|
||
list_frame.grid_columnconfigure(0, weight=1)
|
||
|
||
# 颜色标签
|
||
self.tree.tag_configure("success", foreground="green")
|
||
self.tree.tag_configure("failed", foreground="red")
|
||
self.tree.tag_configure("cancelled", foreground="gray")
|
||
self.tree.tag_configure("pending", foreground="orange")
|
||
|
||
# 详情区域
|
||
detail_frame = ttk.LabelFrame(main_frame, text="操作详情", padding="10")
|
||
detail_frame.pack(fill=tk.X, pady=(10, 0))
|
||
|
||
self.detail_text = tk.Text(detail_frame, height=6, wrap=tk.WORD)
|
||
self.detail_text.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# 绑定选择事件
|
||
self.tree.bind("<<TreeviewSelect>>", self._on_select)
|
||
|
||
# 关闭按钮
|
||
ttk.Button(main_frame, text="关闭", command=self.dialog.destroy).pack(pady=10)
|
||
|
||
def _load_history(self):
|
||
"""加载历史记录"""
|
||
# 清空现有数据
|
||
for item in self.tree.get_children():
|
||
self.tree.delete(item)
|
||
|
||
records = self.history.get_recent_records(limit=100)
|
||
|
||
for record in records:
|
||
# 格式化时间
|
||
try:
|
||
dt = datetime.fromisoformat(record.timestamp)
|
||
time_str = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||
except:
|
||
time_str = record.timestamp
|
||
|
||
# 格式化耗时
|
||
time_cost = ""
|
||
if record.execution_time:
|
||
if record.execution_time < 1:
|
||
time_cost = f"{record.execution_time*1000:.0f}ms"
|
||
else:
|
||
time_cost = f"{record.execution_time:.1f}s"
|
||
|
||
# 格式化详情
|
||
details = str(record.details)[:50] + "..." if len(str(record.details)) > 50 else str(record.details)
|
||
|
||
values = (
|
||
time_str,
|
||
record.operation_type,
|
||
record.device_path,
|
||
record.status,
|
||
details,
|
||
time_cost
|
||
)
|
||
|
||
# 根据状态设置标签
|
||
tag = ""
|
||
status_lower = record.status.lower()
|
||
if "成功" in status_lower or "success" in status_lower:
|
||
tag = "success"
|
||
elif "失败" in status_lower or "failed" in status_lower:
|
||
tag = "failed"
|
||
elif "取消" in status_lower or "cancelled" in status_lower:
|
||
tag = "cancelled"
|
||
elif "待" in status_lower or "pending" in status_lower:
|
||
tag = "pending"
|
||
|
||
self.tree.insert("", tk.END, values=values, tags=(tag,))
|
||
|
||
def _on_select(self, event):
|
||
"""选择记录时显示详情"""
|
||
selection = self.tree.selection()
|
||
if not selection:
|
||
return
|
||
|
||
item = selection[0]
|
||
index = self.tree.index(item)
|
||
records = self.history.get_recent_records(limit=100)
|
||
|
||
if index < len(records):
|
||
record = records[index]
|
||
|
||
detail_text = f"操作ID: {record.id}\n"
|
||
detail_text += f"时间: {record.timestamp}\n"
|
||
detail_text += f"类型: {record.operation_type}\n"
|
||
detail_text += f"设备: {record.device_path}\n"
|
||
detail_text += f"状态: {record.status}\n"
|
||
|
||
if record.execution_time:
|
||
detail_text += f"耗时: {record.execution_time:.2f}秒\n"
|
||
|
||
detail_text += f"\n详情:\n{record.details}\n"
|
||
|
||
if record.error_message:
|
||
detail_text += f"\n错误信息:\n{record.error_message}\n"
|
||
|
||
if record.rollback_data:
|
||
detail_text += f"\n可撤销数据:\n{record.rollback_data}\n"
|
||
|
||
self.detail_text.delete(1.0, tk.END)
|
||
self.detail_text.insert(1.0, detail_text)
|
||
|
||
def _export_history(self):
|
||
"""导出历史记录"""
|
||
file_path = filedialog.asksaveasfilename(
|
||
defaultextension=".json",
|
||
filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
|
||
title="导出操作历史"
|
||
)
|
||
|
||
if file_path:
|
||
if self.history.export_history(file_path):
|
||
messagebox.showinfo("成功", f"历史记录已导出到:\n{file_path}")
|
||
else:
|
||
messagebox.showerror("错误", "导出失败")
|
||
|
||
def _clear_old(self):
|
||
"""清理旧记录"""
|
||
result = messagebox.askyesno(
|
||
"确认清理",
|
||
"是否清理 30 天之前的操作记录?"
|
||
)
|
||
if result:
|
||
self.history.clear_history(days=30)
|
||
self._load_history()
|
||
messagebox.showinfo("成功", "旧记录已清理")
|
||
|
||
def _clear_all(self):
|
||
"""清空所有记录"""
|
||
result = messagebox.askyesno(
|
||
"确认清空",
|
||
"是否清空所有操作记录?\n此操作不可恢复!",
|
||
icon=messagebox.WARNING
|
||
)
|
||
if result:
|
||
self.history.clear_history()
|
||
self._load_history()
|
||
messagebox.showinfo("成功", "所有记录已清空")
|
||
|
||
|
||
class PartitionBackupDialog:
|
||
"""分区表备份管理对话框"""
|
||
|
||
def __init__(self, parent):
|
||
self.parent = parent
|
||
self.backup_manager = PartitionTableBackup()
|
||
|
||
self.dialog = tk.Toplevel(parent)
|
||
self.dialog.title("分区表备份管理")
|
||
self.dialog.geometry("800x500")
|
||
self.dialog.transient(parent)
|
||
|
||
self._center_window()
|
||
self._create_widgets()
|
||
self._load_backups()
|
||
|
||
def _center_window(self):
|
||
self.dialog.update_idletasks()
|
||
width = self.dialog.winfo_width()
|
||
height = self.dialog.winfo_height()
|
||
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
|
||
y = (self.dialog.winfo_screenheight() // 2) - (height // 2)
|
||
self.dialog.geometry(f"{width}x{height}+{x}+{y}")
|
||
|
||
def _create_widgets(self):
|
||
main_frame = ttk.Frame(self.dialog, padding="10")
|
||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# 标题
|
||
title_label = ttk.Label(
|
||
main_frame,
|
||
text="分区表备份管理",
|
||
font=("Arial", 14, "bold")
|
||
)
|
||
title_label.pack(pady=(0, 10))
|
||
|
||
# 工具栏
|
||
toolbar = ttk.Frame(main_frame)
|
||
toolbar.pack(fill=tk.X, pady=(0, 10))
|
||
|
||
ttk.Button(toolbar, text="刷新", command=self._load_backups).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(toolbar, text="删除选中", command=self._delete_selected).pack(side=tk.LEFT, padx=5)
|
||
|
||
# 备份列表
|
||
list_frame = ttk.Frame(main_frame)
|
||
list_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
columns = ("设备", "备份时间", "文件大小", "文件路径")
|
||
self.tree = ttk.Treeview(list_frame, columns=columns, show="headings")
|
||
|
||
for col in columns:
|
||
self.tree.heading(col, text=col)
|
||
|
||
self.tree.column("设备", width=100)
|
||
self.tree.column("备份时间", width=150)
|
||
self.tree.column("文件大小", width=100)
|
||
self.tree.column("文件路径", width=400)
|
||
|
||
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
||
self.tree.configure(yscrollcommand=scrollbar.set)
|
||
|
||
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||
|
||
# 详情和操作按钮
|
||
detail_frame = ttk.LabelFrame(main_frame, text="操作", padding="10")
|
||
detail_frame.pack(fill=tk.X, pady=(10, 0))
|
||
|
||
ttk.Label(detail_frame, text="选中备份文件后可执行以下操作:").pack(anchor=tk.W)
|
||
|
||
btn_frame = ttk.Frame(detail_frame)
|
||
btn_frame.pack(fill=tk.X, pady=5)
|
||
|
||
ttk.Button(btn_frame, text="查看内容", command=self._view_backup).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(btn_frame, text="恢复此备份", command=self._restore_backup).pack(side=tk.LEFT, padx=5)
|
||
|
||
# 关闭按钮
|
||
ttk.Button(main_frame, text="关闭", command=self.dialog.destroy).pack(pady=10)
|
||
|
||
def _load_backups(self):
|
||
"""加载备份列表"""
|
||
for item in self.tree.get_children():
|
||
self.tree.delete(item)
|
||
|
||
backups = self.backup_manager.list_backups()
|
||
|
||
for backup in backups:
|
||
size_str = self._format_size(backup['size'])
|
||
values = (
|
||
backup['device'],
|
||
backup['created'],
|
||
size_str,
|
||
backup['file']
|
||
)
|
||
self.tree.insert("", tk.END, values=values)
|
||
|
||
def _format_size(self, size_bytes):
|
||
"""格式化文件大小"""
|
||
if size_bytes < 1024:
|
||
return f"{size_bytes} B"
|
||
elif size_bytes < 1024 * 1024:
|
||
return f"{size_bytes / 1024:.1f} KB"
|
||
else:
|
||
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
||
|
||
def _get_selected_backup(self):
|
||
"""获取选中的备份"""
|
||
selection = self.tree.selection()
|
||
if not selection:
|
||
messagebox.showwarning("警告", "请先选择一个备份")
|
||
return None
|
||
|
||
item = selection[0]
|
||
file_path = self.tree.item(item, "values")[3]
|
||
return file_path
|
||
|
||
def _view_backup(self):
|
||
"""查看备份内容"""
|
||
file_path = self._get_selected_backup()
|
||
if not file_path:
|
||
return
|
||
|
||
try:
|
||
with open(file_path, 'r') as f:
|
||
content = f.read()
|
||
|
||
# 显示内容对话框
|
||
content_dialog = tk.Toplevel(self.dialog)
|
||
content_dialog.title(f"备份内容 - {file_path}")
|
||
content_dialog.geometry("600x400")
|
||
|
||
text = tk.Text(content_dialog, wrap=tk.WORD, padx=10, pady=10)
|
||
text.pack(fill=tk.BOTH, expand=True)
|
||
text.insert(1.0, content)
|
||
text.config(state=tk.DISABLED)
|
||
|
||
ttk.Button(content_dialog, text="关闭", command=content_dialog.destroy).pack(pady=10)
|
||
|
||
except Exception as e:
|
||
messagebox.showerror("错误", f"无法读取备份文件:\n{e}")
|
||
|
||
def _restore_backup(self):
|
||
"""恢复备份"""
|
||
file_path = self._get_selected_backup()
|
||
if not file_path:
|
||
return
|
||
|
||
# 解析设备名
|
||
filename = os.path.basename(file_path)
|
||
device_name = filename.rsplit('_pt_', 1)[0]
|
||
device_path = f"/dev/{device_name}"
|
||
|
||
result = messagebox.askyesno(
|
||
"确认恢复",
|
||
f"是否将备份恢复到 {device_path}?\n\n"
|
||
f"警告:这将覆盖当前的分区表!\n"
|
||
f"请确保您了解此操作的后果。",
|
||
icon=messagebox.WARNING
|
||
)
|
||
|
||
if result:
|
||
if self.backup_manager.restore_partition_table(device_path, file_path):
|
||
messagebox.showinfo("成功", f"分区表已恢复到 {device_path}")
|
||
else:
|
||
messagebox.showerror("错误", "恢复失败")
|
||
|
||
def _delete_selected(self):
|
||
"""删除选中的备份"""
|
||
file_path = self._get_selected_backup()
|
||
if not file_path:
|
||
return
|
||
|
||
result = messagebox.askyesno(
|
||
"确认删除",
|
||
f"是否删除备份文件?\n{file_path}"
|
||
)
|
||
|
||
if result:
|
||
if self.backup_manager.delete_backup(file_path):
|
||
self._load_backups()
|
||
messagebox.showinfo("成功", "备份已删除")
|
||
else:
|
||
messagebox.showerror("错误", "删除失败")
|
||
|
||
|
||
class OperationQueueDialog:
|
||
"""操作队列对话框"""
|
||
|
||
def __init__(self, parent, operation_queue):
|
||
self.parent = parent
|
||
self.queue = operation_queue
|
||
|
||
self.dialog = tk.Toplevel(parent)
|
||
self.dialog.title("操作队列")
|
||
self.dialog.geometry("600x400")
|
||
self.dialog.transient(parent)
|
||
|
||
self._center_window()
|
||
self._create_widgets()
|
||
self._refresh_status()
|
||
|
||
def _center_window(self):
|
||
self.dialog.update_idletasks()
|
||
width = self.dialog.winfo_width()
|
||
height = self.dialog.winfo_height()
|
||
x = (self.dialog.winfo_screenwidth() // 2) - (width // 2)
|
||
y = (self.dialog.winfo_screenheight() // 2) - (height // 2)
|
||
self.dialog.geometry(f"{width}x{height}+{x}+{y}")
|
||
|
||
def _create_widgets(self):
|
||
main_frame = ttk.Frame(self.dialog, padding="10")
|
||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
# 标题
|
||
title_label = ttk.Label(
|
||
main_frame,
|
||
text="待执行操作队列",
|
||
font=("Arial", 14, "bold")
|
||
)
|
||
title_label.pack(pady=(0, 10))
|
||
|
||
# 状态显示
|
||
self.status_label = ttk.Label(main_frame, text="队列状态: 空闲")
|
||
self.status_label.pack(anchor=tk.W, pady=(0, 10))
|
||
|
||
# 操作列表
|
||
list_frame = ttk.Frame(main_frame)
|
||
list_frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
columns = ("类型", "设备", "状态")
|
||
self.tree = ttk.Treeview(list_frame, columns=columns, show="headings")
|
||
|
||
for col in columns:
|
||
self.tree.heading(col, text=col)
|
||
self.tree.column(col, width=150)
|
||
|
||
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
||
self.tree.configure(yscrollcommand=scrollbar.set)
|
||
|
||
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||
|
||
# 按钮
|
||
btn_frame = ttk.Frame(main_frame)
|
||
btn_frame.pack(fill=tk.X, pady=10)
|
||
|
||
ttk.Button(btn_frame, text="开始执行", command=self._start_execution).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(btn_frame, text="取消当前", command=self._cancel_current).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(btn_frame, text="清空队列", command=self._clear_queue).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(btn_frame, text="刷新", command=self._refresh_status).pack(side=tk.LEFT, padx=5)
|
||
ttk.Button(btn_frame, text="关闭", command=self.dialog.destroy).pack(side=tk.RIGHT, padx=5)
|
||
|
||
# 注册回调
|
||
self.queue.register_callback('on_start', self._on_operation_start)
|
||
self.queue.register_callback('on_complete', self._on_operation_complete)
|
||
self.queue.register_callback('on_error', self._on_operation_error)
|
||
|
||
def _refresh_status(self):
|
||
"""刷新队列状态"""
|
||
status = self.queue.get_queue_status()
|
||
|
||
if status['running']:
|
||
self.status_label.configure(text=f"队列状态: 执行中 ({status['pending_count']} 个待执行)")
|
||
else:
|
||
self.status_label.configure(text=f"队列状态: 空闲 ({status['pending_count']} 个待执行)")
|
||
|
||
# 刷新列表
|
||
for item in self.tree.get_children():
|
||
self.tree.delete(item)
|
||
|
||
pending = self.queue.get_pending_operations()
|
||
for op in pending:
|
||
values = (
|
||
op['type'].value if hasattr(op['type'], 'value') else str(op['type']),
|
||
op['device_path'],
|
||
op['status'].value if hasattr(op['status'], 'value') else str(op['status'])
|
||
)
|
||
self.tree.insert("", tk.END, values=values)
|
||
|
||
def _start_execution(self):
|
||
"""开始执行队列"""
|
||
self.queue.start_execution()
|
||
self._refresh_status()
|
||
|
||
def _cancel_current(self):
|
||
"""取消当前操作"""
|
||
self.queue.cancel_current()
|
||
self._refresh_status()
|
||
|
||
def _clear_queue(self):
|
||
"""清空队列"""
|
||
result = messagebox.askyesno("确认", "是否清空所有待执行操作?")
|
||
if result:
|
||
self.queue.clear_queue()
|
||
self._refresh_status()
|
||
|
||
def _on_operation_start(self, operation):
|
||
"""操作开始回调"""
|
||
self._refresh_status()
|
||
|
||
def _on_operation_complete(self, operation, success):
|
||
"""操作完成回调"""
|
||
self._refresh_status()
|
||
|
||
def _on_operation_error(self, operation, error):
|
||
"""操作错误回调"""
|
||
self._refresh_status()
|
||
messagebox.showerror("操作错误", f"操作失败:\n{error}")
|