fix:修复一些BUG
This commit is contained in:
154
UI.py
154
UI.py
@@ -2,13 +2,14 @@ import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from tkinter import scrolledtext
|
||||
from tkinter import messagebox
|
||||
from tkinter import filedialog
|
||||
import threading
|
||||
import sys
|
||||
import time
|
||||
import queue # 【新增】引入队列库
|
||||
import re # 【新增】用于去除 ANSI 颜色代码
|
||||
import queue
|
||||
import re
|
||||
|
||||
from loguru import logger # 【新增】需要导入 logger 来配置它
|
||||
from loguru import logger
|
||||
from config.config import load_config
|
||||
|
||||
# 假设你的功能函数都在这里
|
||||
@@ -19,29 +20,23 @@ from utils.generate_utils import (
|
||||
generate_report,
|
||||
generate_zodiac,
|
||||
)
|
||||
from utils.file_utils import export_data_folder, initialize_project
|
||||
from utils.file_utils import export_templates_folder, initialize_project, export_data
|
||||
|
||||
# ==========================================
|
||||
# 0. 全局配置与队列准备
|
||||
# ==========================================
|
||||
config = load_config("config.toml")
|
||||
|
||||
# 【新增】创建一个全局队列,用于存放日志消息
|
||||
log_queue = queue.Queue()
|
||||
|
||||
|
||||
def ansi_cleaner(text):
|
||||
"""【辅助函数】去除 loguru 输出中的 ANSI 颜色代码 (例如 \x1b[32m)"""
|
||||
"""【辅助函数】去除 loguru 输出中的 ANSI 颜色代码"""
|
||||
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 里会有乱码
|
||||
"""【核心】loguru sink 回调"""
|
||||
clean_msg = ansi_cleaner(message)
|
||||
log_queue.put(clean_msg)
|
||||
|
||||
@@ -52,103 +47,118 @@ def queue_sink(message):
|
||||
class ReportApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("🌱 幼儿园成长报告助手")
|
||||
self.root.geometry("700x600") # 稍微调大一点
|
||||
self.root.title("🌱 尚城幼儿园成长报告助手")
|
||||
self.root.geometry("720x680") # 高度稍微增加一点以容纳分组
|
||||
|
||||
# 设置样式
|
||||
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("Title.TLabel", font=("微软雅黑", 16, "bold"), foreground="#2E8B57")
|
||||
self.style.configure("Sub.TLabel", font=("微软雅黑", 9), foreground="gray")
|
||||
# LabelFrame 的标题样式
|
||||
self.style.configure("TLabelframe.Label", font=("微软雅黑", 10, "bold"), foreground="#0055a3")
|
||||
|
||||
# --- 1. 标题区域 ---
|
||||
header_frame = ttk.Frame(root, padding="10 20 10 10")
|
||||
header_frame = ttk.Frame(root, padding="10 15 10 5")
|
||||
header_frame.pack(fill=tk.X)
|
||||
ttk.Label(
|
||||
header_frame, text="🌱 幼儿园成长报告助手", style="Title.TLabel"
|
||||
).pack()
|
||||
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)
|
||||
# --- 2. 按钮功能区域 (使用 LabelFrame 分组) ---
|
||||
|
||||
# 容器 Frame,给四周留点白
|
||||
main_content = ttk.Frame(root, padding=10)
|
||||
main_content.pack(fill=tk.X)
|
||||
|
||||
buttons = [
|
||||
# === A组: 核心功能 ===
|
||||
func_btns = [
|
||||
("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),
|
||||
]
|
||||
self.create_btn_group(main_content, "🛠️ 核心功能", func_btns, columns=3)
|
||||
|
||||
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")
|
||||
# === B组: 数据导出 ===
|
||||
export_btns = [
|
||||
("6. 📦 导出数据模板 (Zip)", self.run_export_data_folder),
|
||||
("8. 📤 导出数据备份 (Zip)", self.run_export_data),
|
||||
]
|
||||
self.create_btn_group(main_content, "📦 数据管理", export_btns, columns=2)
|
||||
|
||||
btn_frame.columnconfigure(0, weight=1)
|
||||
btn_frame.columnconfigure(1, weight=1)
|
||||
# === C组: 系统设置 ===
|
||||
system_btns = [
|
||||
("7. ⚠️ 初始化系统 (重置)", self.run_initialize_project),
|
||||
("9. 🚪 退出系统", self.quit_app),
|
||||
]
|
||||
self.create_btn_group(main_content, "⚙️ 系统操作", system_btns, columns=2)
|
||||
|
||||
# --- 3. 日志输出区域 ---
|
||||
log_frame = ttk.LabelFrame(root, text="系统实时日志", padding=10)
|
||||
log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
log_frame = ttk.LabelFrame(root, text="📝 系统实时日志", padding=10)
|
||||
log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 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 create_btn_group(self, parent, title, buttons, columns=2):
|
||||
"""
|
||||
辅助函数:快速创建分组按钮
|
||||
:param parent: 父容器
|
||||
:param title: 分组标题
|
||||
:param buttons: 按钮列表 [(text, func), ...]
|
||||
:param columns: 每行显示几个按钮
|
||||
"""
|
||||
frame = ttk.LabelFrame(parent, text=title, padding=10)
|
||||
frame.pack(fill=tk.X, pady=5) # 垂直堆叠
|
||||
|
||||
for index, (text, func) in enumerate(buttons):
|
||||
# 特殊处理:如果是"初始化"或"退出",可以用不同的样式(可选,这里暂不做)
|
||||
btn = ttk.Button(frame, text=text, command=func)
|
||||
|
||||
# 动态计算网格位置
|
||||
r, c = divmod(index, columns)
|
||||
btn.grid(row=r, column=c, padx=8, pady=5, sticky="ew")
|
||||
|
||||
# 配置列权重,让按钮自动填满宽度
|
||||
for i in range(columns):
|
||||
frame.columnconfigure(i, weight=1)
|
||||
|
||||
# --- 核心方法:轮询队列 ---
|
||||
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.insert(tk.END, 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 输出成功
|
||||
logger.success("✅ 当前任务执行完毕。")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 发生错误: {str(e)}") # 使用 logger 输出错误
|
||||
logger.error(f"❌ 发生错误: {str(e)}")
|
||||
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)
|
||||
@@ -160,18 +170,36 @@ class ReportApp:
|
||||
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)
|
||||
target_folder = filedialog.askdirectory(
|
||||
title="请选择导出数据保存的文件夹",
|
||||
initialdir=config.get("output_folder", ".")
|
||||
)
|
||||
if not target_folder:
|
||||
logger.warning("🚫 导出操作已取消")
|
||||
return
|
||||
logger.info(f"已选择保存路径: {target_folder}")
|
||||
self.run_in_thread(export_templates_folder, target_folder)
|
||||
|
||||
def run_initialize_project(self):
|
||||
self.run_in_thread(initialize_project)
|
||||
|
||||
def run_export_data(self):
|
||||
target_folder = filedialog.askdirectory(
|
||||
title="请选择导出数据保存的文件夹",
|
||||
initialdir=config.get("output_folder", ".")
|
||||
)
|
||||
if not target_folder:
|
||||
logger.warning("🚫 导出操作已取消")
|
||||
return
|
||||
logger.info(f"已选择保存路径: {target_folder}")
|
||||
self.run_in_thread(export_data, target_folder)
|
||||
|
||||
def quit_app(self):
|
||||
if messagebox.askokcancel("退出", "确定要退出系统吗?"):
|
||||
self.root.destroy()
|
||||
@@ -182,16 +210,10 @@ class ReportApp:
|
||||
# 启动入口
|
||||
# ==========================================
|
||||
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 信息刷屏
|
||||
level="INFO",
|
||||
)
|
||||
|
||||
root = tk.Tk()
|
||||
|
||||
Reference in New Issue
Block a user