Files
growth_report/ui/app_window.py
2025-12-13 19:44:27 +08:00

237 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, "任务进度: 就绪")