import os import time import tkinter as tk from tkinter import ttk, scrolledtext, messagebox, filedialog import threading import queue import sys from loguru import logger from config.config import load_config from utils.font_utils import install_fonts_from_directory from utils.log_handler import log_queue # 导入业务逻辑 from utils.generate_utils import ( generate_template, generate_comment_all, batch_convert_folder, generate_report, generate_zodiac, ) from utils.file_utils import export_templates_folder, initialize_project, export_data config = load_config("config.toml") class ReportApp: def __init__(self, root): self.root = root self.root.title("🌱 尚城幼儿园成长报告助手") self.root.geometry("720x760") # 线程控制 self.stop_event = threading.Event() self.is_running = False # 尝试初始化 UI try: self._setup_ui() except Exception as e: logger.critical(f"UI 初始化失败: {e}", exc_info=True) messagebox.showerror("致命错误", f"界面初始化失败,请检查日志。\n错误: {e}") self.root.destroy() sys.exit(1) # 尝试初始化项目资源 try: self.init_project() except Exception as e: logger.critical(f"项目资源初始化失败: {e}", exc_info=True) messagebox.showerror("致命错误", f"项目资源初始化失败,请检查日志。\n错误: {e}") self.root.destroy() sys.exit(1) self._start_log_polling() def _setup_ui(self): # 样式配置 self.style = ttk.Style() self.style.theme_use("clam") self.style.configure("TButton", font=("微软雅黑", 10), padding=5) self.style.configure("Title.TLabel", font=("微软雅黑", 16, "bold"), foreground="#2E8B57") self.style.configure("Stop.TButton", foreground="red", font=("微软雅黑", 10, "bold")) # 1. 标题 header = ttk.Frame(self.root, padding="10 15 10 5") header.pack(fill=tk.X) ttk.Label(header, text="🌱 尚城幼儿园成长报告助手", style="Title.TLabel").pack() ttk.Label(header, text="By 寒寒 | 这里的每一份评语都充满爱意", font=("微软雅黑", 9), foreground="gray").pack() # 2. 功能区容器 main_content = ttk.Frame(self.root, padding="10 15 10 5") main_content.pack(fill=tk.X) # === 进度条区域 === progress_frame = ttk.Frame(self.root, padding="10 15 10 5") progress_frame.pack(fill=tk.X, pady=(0, 10)) # 进度条 Label self.progress_label = ttk.Label(progress_frame, text="⛳ 任务进度: 待命", font=("微软雅黑", 10)) self.progress_label.pack(fill=tk.X, pady=(0, 2)) # 进度条 self.progressbar = ttk.Progressbar(progress_frame, orient="horizontal", mode="determinate") self.progressbar.pack(fill=tk.X, expand=True) # === A组: 核心功能 === self._create_btn_group(main_content, "🛠️ 核心功能", [ ("📁 生成图片路径", lambda: self.run_task(generate_template)), ("🤖 生成评语 (AI)", lambda: self.run_task(generate_comment_all)), ("📊 生成报告 (PPT)", lambda: self.run_task(generate_report)), ("📑 格式转换 (PDF)", lambda: self.run_task(batch_convert_folder, config.get("output_folder"))), ("🐂 生肖转化 (生日)", lambda: self.run_task(generate_zodiac)), ], columns=3) # === B组: 数据管理 === self._create_btn_group(main_content, "📦 数据管理", [ ("📦 导出模板 (Zip)", self.run_export_template), ("📤 导出备份 (Zip)", self.run_export_data), ], columns=2) # === C组: 系统操作 (含停止按钮) === self._create_btn_group(main_content, "⚙️ 系统操作", [ ("⛔ 停止当前任务", self.stop_current_task), ("⚠️ 初始化系统", self.run_init), ("🚪 退出系统", self.quit_app), ], columns=3, special_styles={"⛔ 停止当前任务": "Stop.TButton"}) # 3. 日志区 log_frame = ttk.LabelFrame(self.root, text="📝 系统实时日志", padding="10 15 10 5") log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) self.log_text = scrolledtext.ScrolledText(log_frame, height=10, state="disabled", font=("Consolas", 9)) self.log_text.pack(fill=tk.BOTH, expand=True) def _create_btn_group(self, parent, title, buttons, columns=2, special_styles=None): frame = ttk.LabelFrame(parent, text=title, padding=10) frame.pack(fill=tk.X, pady=5) special_styles = special_styles or {} for i, (text, func) in enumerate(buttons): style = special_styles.get(text, "TButton") btn = ttk.Button(frame, text=text, command=func, style=style) r, c = divmod(i, columns) btn.grid(row=r, column=c, padx=5, pady=5, sticky="ew") for i in range(columns): frame.columnconfigure(i, weight=1) def _start_log_polling(self): while not log_queue.empty(): try: msg = log_queue.get_nowait() self.log_text.config(state="normal") self.log_text.insert(tk.END, msg) self.log_text.see(tk.END) self.log_text.config(state="disabled") except queue.Empty: break self.root.after(100, self._start_log_polling) def init_project(self): # 1. 资源准备 if install_fonts_from_directory(config["fonts_dir"]): logger.info("等待系统识别新安装的字体...") time.sleep(2) # 2. 创建输出文件夹 os.makedirs(config["output_folder"], exist_ok=True) logger.success("项目初始化完成.....") # --- 任务运行核心逻辑 --- def run_task(self, target_func, *args, **kwargs): if self.is_running: messagebox.showwarning("忙碌中", "请先等待当前任务完成或点击【停止当前任务】") return self.stop_event.clear() self.is_running = True # 将进度更新方法作为参数传入 kwargs['progress_callback'] = self.update_progress def thread_worker(): try: # 尝试传入 stop_event try: target_func(*args, stop_event=self.stop_event, **kwargs) except TypeError: # 如果旧函数不支持 stop_event,则普通运行 target_func(*args, **kwargs) except Exception as e: logger.error(f"任务出错: {e}") import traceback logger.error(traceback.format_exc()) finally: self.is_running = False logger.info("系统准备就绪.....") self.reset_progress() # 重置进度条 threading.Thread(target=thread_worker, daemon=True).start() def stop_current_task(self): if not self.is_running: return if messagebox.askyesno("确认", "确定要中断当前任务吗?"): self.stop_event.set() logger.warning("正在发送停止信号...") # --- 具体按钮事件 --- def run_export_template(self): path = filedialog.askdirectory() if path: self.run_task(export_templates_folder, path) def run_export_data(self): path = filedialog.askdirectory() if path: self.run_task(export_data, path) def run_init(self): if messagebox.askokcancel("警告", "确定重置系统吗?数据将丢失!"): self.run_task(initialize_project) def quit_app(self): if self.is_running: messagebox.showwarning("提示", "请先停止任务") return self.root.destroy() sys.exit() # --- 进度条更新(实现线程安全更新) --- def update_progress(self, current, total, task_name="任务"): """ 线程安全地更新进度条和标签 :param current: 当前完成的项目数 :param total: 总项目数 :param task_name: 当前任务名称 """ if total <= 0: # 重置进度条 self.progressbar['value'] = 0 self.progress_label.config(text=f"任务进度: {task_name} 完成或待命") return percentage = int((current / total) * 100) display_text = f"{task_name}: {current}/{total} ({percentage}%)" # 使用 after 确保在主线程中更新 UI self.root.after(0, self._set_progress_ui, percentage, display_text) def _set_progress_ui(self, percentage, display_text): """实际更新 UI 的私有方法""" self.progressbar['value'] = percentage self.progress_label.config(text=display_text) def reset_progress(self): """任务结束后重置进度条""" self.root.after(0, self._set_progress_ui, 0, "任务进度: 就绪")