""" 历史记录视图组件 显示任务执行历史 """ import tkinter as tk from tkinter import ttk, messagebox from typing import Callable, List, Optional from pathlib import Path from history.manager import TaskRecord, HistoryManager class HistoryView: """ 历史记录视图 显示任务执行历史列表,支持查看详情 """ def __init__( self, parent: tk.Widget, history_manager: HistoryManager, on_back: Callable[[], None] ): self.parent = parent self.history = history_manager self.on_back = on_back self._selected_record: Optional[TaskRecord] = None self._create_widgets() def _create_widgets(self): """创建 UI 组件""" self.frame = tk.Frame(self.parent, bg='#1e1e1e') # 标题栏 title_frame = tk.Frame(self.frame, bg='#1e1e1e') title_frame.pack(fill=tk.X, padx=10, pady=10) # 返回按钮 back_btn = tk.Button( title_frame, text="← 返回", font=('Microsoft YaHei UI', 10), bg='#424242', fg='white', activebackground='#616161', activeforeground='white', relief=tk.FLAT, padx=10, cursor='hand2', command=self.on_back ) back_btn.pack(side=tk.LEFT) # 标题 title_label = tk.Label( title_frame, text="📜 任务历史记录", font=('Microsoft YaHei UI', 14, 'bold'), fg='#ce93d8', bg='#1e1e1e' ) title_label.pack(side=tk.LEFT, padx=20) # 统计信息 stats = self.history.get_stats() stats_text = f"共 {stats['total']} 条 | 成功 {stats['success']} | 失败 {stats['failed']} | 成功率 {stats['success_rate']:.0%}" stats_label = tk.Label( title_frame, text=stats_text, font=('Microsoft YaHei UI', 9), fg='#888888', bg='#1e1e1e' ) stats_label.pack(side=tk.RIGHT) # 主内容区域(左右分栏) content_frame = tk.Frame(self.frame, bg='#1e1e1e') content_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) # 左侧:历史列表 list_frame = tk.LabelFrame( content_frame, text=" 任务列表 ", font=('Microsoft YaHei UI', 10, 'bold'), fg='#4fc3f7', bg='#1e1e1e', relief=tk.GROOVE ) list_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5)) # 列表框 list_container = tk.Frame(list_frame, bg='#2d2d2d') list_container.pack(fill=tk.BOTH, expand=True, padx=3, pady=3) # 使用 Treeview 显示列表 columns = ('time', 'input', 'status', 'duration') self.tree = ttk.Treeview(list_container, columns=columns, show='headings', height=15) # 配置列 self.tree.heading('time', text='时间') self.tree.heading('input', text='任务描述') self.tree.heading('status', text='状态') self.tree.heading('duration', text='耗时') self.tree.column('time', width=120, minwidth=100) self.tree.column('input', width=250, minwidth=150) self.tree.column('status', width=60, minwidth=50) self.tree.column('duration', width=70, minwidth=50) # 滚动条 scrollbar = ttk.Scrollbar(list_container, orient=tk.VERTICAL, command=self.tree.yview) self.tree.configure(yscrollcommand=scrollbar.set) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 绑定选择事件 self.tree.bind('<>', self._on_select) # 右侧:详情面板 detail_frame = tk.LabelFrame( content_frame, text=" 任务详情 ", font=('Microsoft YaHei UI', 10, 'bold'), fg='#81c784', bg='#1e1e1e', relief=tk.GROOVE ) detail_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0)) # 详情文本框 detail_container = tk.Frame(detail_frame, bg='#2d2d2d') detail_container.pack(fill=tk.BOTH, expand=True, padx=3, pady=3) self.detail_text = tk.Text( detail_container, wrap=tk.WORD, font=('Microsoft YaHei UI', 10), bg='#2d2d2d', fg='#d4d4d4', relief=tk.FLAT, padx=10, pady=10, state=tk.DISABLED ) detail_scrollbar = ttk.Scrollbar(detail_container, orient=tk.VERTICAL, command=self.detail_text.yview) self.detail_text.configure(yscrollcommand=detail_scrollbar.set) detail_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.detail_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 配置详情文本样式 self.detail_text.tag_configure('title', font=('Microsoft YaHei UI', 11, 'bold'), foreground='#ffd54f') self.detail_text.tag_configure('label', font=('Microsoft YaHei UI', 10, 'bold'), foreground='#4fc3f7') self.detail_text.tag_configure('success', foreground='#81c784') self.detail_text.tag_configure('error', foreground='#ef5350') self.detail_text.tag_configure('code', font=('Consolas', 9), foreground='#ce93d8') # 底部按钮 btn_frame = tk.Frame(self.frame, bg='#1e1e1e') btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) # 打开日志按钮 self.open_log_btn = tk.Button( btn_frame, text="📄 打开日志", font=('Microsoft YaHei UI', 10), bg='#424242', fg='white', activebackground='#616161', activeforeground='white', relief=tk.FLAT, padx=15, cursor='hand2', state=tk.DISABLED, command=self._open_log ) self.open_log_btn.pack(side=tk.LEFT) # 清空历史按钮 clear_btn = tk.Button( btn_frame, text="🗑️ 清空历史", font=('Microsoft YaHei UI', 10), bg='#d32f2f', fg='white', activebackground='#f44336', activeforeground='white', relief=tk.FLAT, padx=15, cursor='hand2', command=self._clear_history ) clear_btn.pack(side=tk.RIGHT) # 加载数据 self._load_data() def _load_data(self): """加载历史数据到列表""" # 清空现有数据 for item in self.tree.get_children(): self.tree.delete(item) # 加载历史记录 records = self.history.get_all() for record in records: # 截断过长的输入 input_text = record.user_input if len(input_text) > 30: input_text = input_text[:30] + "..." status = "✓ 成功" if record.success else "✗ 失败" duration = f"{record.duration_ms}ms" # 提取时间(只显示时分秒) time_parts = record.timestamp.split(' ') time_str = time_parts[1] if len(time_parts) > 1 else record.timestamp date_str = time_parts[0] if len(time_parts) > 0 else "" display_time = f"{date_str}\n{time_str}" self.tree.insert('', tk.END, iid=record.task_id, values=( record.timestamp, input_text, status, duration )) # 显示空状态提示 if not records: self._show_detail("暂无历史记录\n\n执行任务后,记录将显示在这里。") def _on_select(self, event): """选择记录事件""" selection = self.tree.selection() if not selection: return task_id = selection[0] record = self.history.get_by_id(task_id) if record: self._selected_record = record self._show_record_detail(record) self.open_log_btn.config(state=tk.NORMAL) def _show_record_detail(self, record: TaskRecord): """显示记录详情""" self.detail_text.config(state=tk.NORMAL) self.detail_text.delete(1.0, tk.END) # 标题 self.detail_text.insert(tk.END, f"任务 ID: {record.task_id}\n", 'title') self.detail_text.insert(tk.END, f"时间: {record.timestamp}\n\n") # 用户输入 self.detail_text.insert(tk.END, "用户输入:\n", 'label') self.detail_text.insert(tk.END, f"{record.user_input}\n\n") # 执行状态 self.detail_text.insert(tk.END, "执行状态: ", 'label') if record.success: self.detail_text.insert(tk.END, "成功 ✓\n", 'success') else: self.detail_text.insert(tk.END, "失败 ✗\n", 'error') self.detail_text.insert(tk.END, f"耗时: {record.duration_ms}ms\n\n") # 执行计划 self.detail_text.insert(tk.END, "执行计划:\n", 'label') plan_preview = record.execution_plan[:500] + "..." if len(record.execution_plan) > 500 else record.execution_plan self.detail_text.insert(tk.END, f"{plan_preview}\n\n") # 输出 if record.stdout: self.detail_text.insert(tk.END, "输出:\n", 'label') self.detail_text.insert(tk.END, f"{record.stdout}\n\n") # 错误 if record.stderr: self.detail_text.insert(tk.END, "错误:\n", 'label') self.detail_text.insert(tk.END, f"{record.stderr}\n", 'error') self.detail_text.config(state=tk.DISABLED) def _show_detail(self, text: str): """显示详情文本""" self.detail_text.config(state=tk.NORMAL) self.detail_text.delete(1.0, tk.END) self.detail_text.insert(tk.END, text) self.detail_text.config(state=tk.DISABLED) def _open_log(self): """打开日志文件""" if self._selected_record and self._selected_record.log_path: import os log_path = Path(self._selected_record.log_path) if log_path.exists(): os.startfile(str(log_path)) else: messagebox.showwarning("提示", f"日志文件不存在:\n{log_path}") def _clear_history(self): """清空历史记录""" result = messagebox.askyesno( "确认清空", "确定要清空所有历史记录吗?\n此操作不可恢复。", icon='warning' ) if result: self.history.clear() self._load_data() self._show_detail("历史记录已清空") self.open_log_btn.config(state=tk.DISABLED) def show(self): """显示视图""" self._load_data() # 刷新数据 self.frame.pack(fill=tk.BOTH, expand=True) def hide(self): """隐藏视图""" self.frame.pack_forget() def get_frame(self) -> tk.Frame: """获取主框架""" return self.frame