import tkinter as tk from tkinter import ttk from tkinter import scrolledtext from tkinter import messagebox import threading import sys import time import queue # 【新增】引入队列库 import re # 【新增】用于去除 ANSI 颜色代码 from loguru import logger # 【新增】需要导入 logger 来配置它 from config.config import load_config # 假设你的功能函数都在这里 from utils.generate_utils import ( generate_template, generate_comment_all, batch_convert_folder, generate_report, generate_zodiac, ) from utils.file_utils import export_data_folder, initialize_project # ========================================== # 0. 全局配置与队列准备 # ========================================== config = load_config("config.toml") # 【新增】创建一个全局队列,用于存放日志消息 log_queue = queue.Queue() def ansi_cleaner(text): """【辅助函数】去除 loguru 输出中的 ANSI 颜色代码 (例如 \x1b[32m)""" ansi_escape = re.compile(r"\x1b\[[0-9;]*m") return ansi_escape.sub("", text) def queue_sink(message): """【核心】这是一个 loguru 的 sink 回调函数 当 logger.info() 被调用时,loguru 会把格式化好的消息传给这个函数 """ # message 是一个对象,我们将其转换为字符串并放入队列 # 使用 ansi_cleaner 去掉颜色代码,否则 GUI 里会有乱码 clean_msg = ansi_cleaner(message) log_queue.put(clean_msg) # ========================================== # GUI 主程序类 # ========================================== class ReportApp: def __init__(self, root): self.root = root self.root.title("🌱 幼儿园成长报告助手") self.root.geometry("700x600") # 稍微调大一点 # 设置样式 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("Sub.TLabel", font=("微软雅黑", 9), foreground="gray") # --- 1. 标题区域 --- header_frame = ttk.Frame(root, padding="10 20 10 10") header_frame.pack(fill=tk.X) ttk.Label( header_frame, text="🌱 幼儿园成长报告助手", style="Title.TLabel" ).pack() ttk.Label(header_frame, text="By 寒寒", style="Sub.TLabel").pack() # --- 2. 按钮功能区域 --- btn_frame = ttk.Frame(root, padding=10) btn_frame.pack(fill=tk.X) buttons = [ ("1. 📁 生成图片路径", self.run_generate_folders), ("2. 🤖 生成评语 (AI)", self.run_generate_comments), ("3. 📊 生成报告 (PPT)", self.run_generate_report), ("4. 📑 格式转换 (PDF)", self.run_convert_pdf), ("5. 🐂 生肖转化 (生日)", self.run_zodiac), ("6. 📦 导出数据模板 (Zip)", self.run_export_data_folder), ("7. 📤 初始化系统", self.run_initialize_project), ("8. 🚪 退出系统", self.quit_app), ] for index, (text, func) in enumerate(buttons): btn = ttk.Button(btn_frame, text=text, command=func) r, c = divmod(index, 2) btn.grid(row=r, column=c, padx=10, pady=5, sticky="ew") btn_frame.columnconfigure(0, weight=1) btn_frame.columnconfigure(1, weight=1) # --- 3. 日志输出区域 --- log_frame = ttk.LabelFrame(root, text="系统实时日志", padding=10) log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 注意:这里字体设为 Consolas 或其他等宽字体,看起来更像代码日志 self.log_text = scrolledtext.ScrolledText( log_frame, height=10, state="disabled", font=("Consolas", 9) ) self.log_text.pack(fill=tk.BOTH, expand=True) # 【新增】启动日志轮询 # 告诉 GUI:每隔 100ms 检查一下队列里有没有新日志 self.root.after(100, self.poll_log_queue) logger.info("GUI 初始化完成,等待指令...") # --- 【新增】核心方法:轮询队列 --- def poll_log_queue(self): """检查队列中是否有新日志,如果有则显示到界面上""" while not log_queue.empty(): try: # 不阻塞地获取消息 msg = log_queue.get_nowait() # 写入 GUI self.log_text.config(state="normal") self.log_text.insert(tk.END, msg) # msg 已经包含了换行符 self.log_text.see(tk.END) # 自动滚动到底部 self.log_text.config(state="disabled") except queue.Empty: break # 递归调用:100ms 后再次执行自己 self.root.after(100, self.poll_log_queue) # --- 辅助方法:线程包装器 --- def run_in_thread(self, target_func, *args): """在单独的线程中运行函数""" def thread_task(): try: # 这里不需要手动 self.log 了,因为 target_func 内部的 logger 会自动触发 sink target_func(*args) logger.success("✅ 当前任务执行完毕。") # 使用 logger 输出成功 except Exception as e: logger.error(f"❌ 发生错误: {str(e)}") # 使用 logger 输出错误 import traceback logger.error(traceback.format_exc()) threading.Thread(target=thread_task, daemon=True).start() # ========================================== # 按钮事件 (不需要改动,逻辑都在 thread_task 里处理了) # ========================================== def run_generate_folders(self): self.run_in_thread(generate_template) def run_generate_comments(self): self.run_in_thread(generate_comment_all) def run_generate_report(self): self.run_in_thread(generate_report) def run_convert_pdf(self): # 传入 output_folder self.run_in_thread(batch_convert_folder, config["output_folder"]) def run_zodiac(self): self.run_in_thread(generate_zodiac) def run_export_data_folder(self): self.run_in_thread(export_data_folder) def run_initialize_project(self): self.run_in_thread(initialize_project) def quit_app(self): if messagebox.askokcancel("退出", "确定要退出系统吗?"): self.root.destroy() sys.exit() # ========================================== # 启动入口 # ========================================== def applicationUI(): # 【核心配置】在启动 GUI 前,配置 loguru # 1. remove() 移除默认的控制台输出(如果你想保留控制台黑窗口,可以注释掉这行) # logger.remove() # 2. 添加我们的自定义 sink (queue_sink) # format 可以自定义,这里保持简单,或者尽量模拟 loguru 默认格式 logger.add( queue_sink, format="{time:HH:mm:ss} | {level: <8} | {message}", level="INFO", # 只显示 INFO 及以上级别,避免 DEBUG 信息刷屏 ) root = tk.Tk() app = ReportApp(root) root.mainloop() if __name__ == "__main__": applicationUI()