diff --git a/.gitignore b/.gitignore index 4139615..ad79859 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,7 @@ wheels/ .venv output/*.pptx output/*.pdf +data/images/* +data/*.xlsx config.toml \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..08ed438 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1765460141811 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UI.py b/UI.py deleted file mode 100644 index 94f8a4f..0000000 --- a/UI.py +++ /dev/null @@ -1,225 +0,0 @@ -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 - -from loguru import 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_templates_folder, initialize_project, export_data - -# ========================================== -# 0. 全局配置与队列准备 -# ========================================== -config = load_config("config.toml") -log_queue = queue.Queue() - - -def ansi_cleaner(text): - """【辅助函数】去除 loguru 输出中的 ANSI 颜色代码""" - ansi_escape = re.compile(r"\x1b\[[0-9;]*m") - return ansi_escape.sub("", text) - - -def queue_sink(message): - """【核心】loguru sink 回调""" - 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("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("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 15 10 5") - 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. 按钮功能区域 (使用 LabelFrame 分组) --- - - # 容器 Frame,给四周留点白 - main_content = ttk.Frame(root, padding=10) - main_content.pack(fill=tk.X) - - # === A组: 核心功能 === - func_btns = [ - ("📁 生成图片路径", self.run_generate_folders), - ("🤖 生成评语 (AI)", self.run_generate_comments), - ("📊 生成报告 (PPT)", self.run_generate_report), - ("📑 格式转换 (PDF)", self.run_convert_pdf), - ("🐂 生肖转化 (生日)", self.run_zodiac), - ] - self.create_btn_group(main_content, "🛠️ 核心功能", func_btns, columns=3) - - # === B组: 数据导出 === - export_btns = [ - ("📦 导出数据模板 (Zip)", self.run_export_data_folder), - ("📤 导出数据备份 (Zip)", self.run_export_data), - ] - self.create_btn_group(main_content, "📦 数据管理", export_btns, columns=2) - - # === C组: 系统设置 === - system_btns = [ - ("⚠️ 初始化系统 (重置)", self.run_initialize_project), - ("🚪 退出系统", 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=(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) - - # 启动日志轮询 - 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() - 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.poll_log_queue) - - # --- 线程包装器 --- - def run_in_thread(self, target_func, *args): - def thread_task(): - try: - target_func(*args) - logger.success("✅ 当前任务执行完毕。") - except Exception as e: - logger.error(f"❌ 发生错误: {str(e)}") - import traceback - logger.error(traceback.format_exc()) - - threading.Thread(target=thread_task, daemon=True).start() - - # ========================================== - # 按钮事件 (业务逻辑保持不变) - # ========================================== - 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): - 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): - 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() - sys.exit() - - -# ========================================== -# 启动入口 -# ========================================== -def applicationUI(): - logger.add( - queue_sink, - format="{time:HH:mm:ss} | {level: <8} | {message}", - level="INFO", - ) - - root = tk.Tk() - app = ReportApp(root) - root.mainloop() - - -if __name__ == "__main__": - applicationUI() \ No newline at end of file diff --git a/data/names.xlsx b/data/names.xlsx index f5e6904..a3c61c6 100644 Binary files a/data/names.xlsx and b/data/names.xlsx differ diff --git a/main.py b/main.py deleted file mode 100644 index f4e30c0..0000000 --- a/main.py +++ /dev/null @@ -1,117 +0,0 @@ -from config.config import load_config -from utils.generate_utils import ( - generate_template, - generate_comment_all, - generate_report, - batch_convert_folder, - generate_zodiac, -) -from utils.file_utils import export_templates_folder, initialize_project, export_data - -config = load_config("config.yaml") - - -def application(): - from rich.console import Console - from rich.panel import Panel - from rich.prompt import Prompt - from rich.table import Table - from rich.align import Align - from rich import box - import sys - - console = Console() - - while True: - console.clear() - - # 1. 创建一个表格,不显示表头,使用圆角边框 - table = Table(box=None, show_header=False, padding=(0, 2)) - - # 2. 添加两列:序号列(居右),内容列(居左) - table.add_column(justify="right", style="cyan bold") - table.add_column(justify="left") - - # 3. 添加行内容 - table.add_row("1.", "📁 生成图片路径(每一个幼儿一个图片文件夹)") - table.add_row("2.", "🤖 生成评语(根据姓名、学段、性别)") - table.add_row("3.", "📊 生成报告(根据表格生成)") - table.add_row("4.", "📑 格式转换(PPT转PDF)") - table.add_row("5.", "📑 生肖转化(根据生日)") - table.add_row("6.", "📦 导出数据模板(Zip)") - table.add_row("7.", "📦 初始化系统") - table.add_row("8.", "📤 导出数据") - table.add_row("9.", "🚪 退出系统") - - # 4. 将表格放入面板,并居中显示 - panel = Panel( - Align.center(table), - title="[bold green]🌱 幼儿园成长报告助手", - subtitle="[dim]By 寒寒", - width=60, - border_style="bright_blue", - box=box.ROUNDED, # 圆角边框更柔和 - ) - - # 使用 Align.center 让整个菜单在屏幕中间显示 - console.print(Align.center(panel, vertical="middle")) - console.print("\n") # 留点空隙 - - choice = Prompt.ask( - "👉 请输入序号执行", - choices=["1", "2", "3", "4", "5", "6", "7", "8", "9"], - default="1", - ) - - try: - if choice == "1": - console.rule("[bold cyan]正在执行: 生成模板[/]") - with console.status( - "[bold green]正在创建文件夹结构...[/]", spinner="dots" - ): - generate_template() - elif choice == "2": - console.rule("[bold yellow]正在执行: AI 生成评语[/]") - # 这里的 generate_comment_all 最好内部有进度条,或者简单的 print - generate_comment_all() - elif choice == "3": - console.rule("[bold blue]正在执行: PPT 合成[/]") - with console.status( - "[bold blue]正在处理图片和文字...[/]", spinner="earth" - ): - generate_report() - elif choice == "4": - console.rule("[bold magenta]正在执行: PDF 批量转换[/]") - # 调用上面的批量转换函数,传入你的 output 文件夹路径 - batch_convert_folder(config["output_folder"]) - elif choice == "5": - console.rule("[bold magenta]正在执行: 生肖转化[/]") - # 调用上面的批量转换函数,传入你的 output 文件夹路径 - generate_zodiac() - elif choice == "6": - console.rule("[bold magenta]正在执行: 导出数据模板[/]") - # 调用上面的批量转换函数,传入你的 output 文件夹路径 - export_templates_folder() - elif choice == "7": - console.rule("[bold magenta]正在执行: 初始化系统[/]") - # 调用上面的批量转换函数,传入你的 output 文件夹路径 - initialize_project() - elif choice == "8": - console.rule("[bold magenta]正在执行: 导出数据[/]") - # 调用上面的批量转换函数,传入你的 output 文件夹路径 - export_data() - elif choice == "9": - console.print("[bold red]👋 再见![/]") - sys.exit() - Prompt.ask("按 [bold]Enter[/] 键返回主菜单...") - except Exception as e: - console.print( - Panel( - f"[bold red]❌ 发生错误:[/]\n{e}", title="Error", border_style="red" - ) - ) - Prompt.ask("按 Enter 键继续...") - - -if __name__ == "__main__": - application() diff --git a/main.pyw b/main.pyw new file mode 100644 index 0000000..63ec4cb --- /dev/null +++ b/main.pyw @@ -0,0 +1,23 @@ +import tkinter as tk +from utils.log_handler import setup_logging +from ui.app_window import ReportApp +from loguru import logger + +def main(): + # 1. 初始化日志 + setup_logging() + logger.info("正在启动应用程序...") + + # 2. 启动 UI + root = tk.Tk() + + # 这一行可以设置图标 (如果有 icon.ico 文件) + # root.iconbitmap("icon.ico") + + app = ReportApp(root) + + # 3. 进入主循环 + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 67a4316..b10b2f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "openpyxl>=3.1.5", "pandas>=2.3.3", "pandas-stubs==2.3.3.251201", + "pillow>=12.0.0", "python-pptx>=1.0.2", "pywin32>=311", "rich>=14.2.0", diff --git a/start_app.bat b/start_app.bat index 4b114e5..1e3d2c1 100644 --- a/start_app.bat +++ b/start_app.bat @@ -4,7 +4,7 @@ chcp 65001 >nul :: ------------------------------------------------ -title 幼儿园成长报告助手 +title 幼儿园成长报告助手启动器 cd /d "%~dp0" echo. @@ -13,7 +13,7 @@ echo 正在启动 幼儿园成长报告助手 echo ========================================== echo. -:: 检查 uv 是否安装 +:: 1. 检查 uv 是否安装 uv --version >nul 2>&1 if %errorlevel% neq 0 ( echo [ERROR] 未检测到 uv 工具! @@ -22,21 +22,28 @@ if %errorlevel% neq 0 ( exit /b ) -echo [INFO] 环境检查通过,正在运行主程序... +echo [INFO] 环境检查通过... + +:: 2. 检查依赖是否安装 (可选,防止第一次运行报错) +:: 如果你有 pyproject.toml,uv run 会自动处理,这一步可以省略 +:: 这里为了保险,检查一下 loguru 是否存在,不存在则自动安装基础依赖 +uv pip show loguru >nul 2>&1 +if %errorlevel% neq 0 ( + echo [INFO] 首次运行,正在安装依赖... + uv pip install loguru toml pandas pillow openpyxl python-pptx +) + +echo [INFO] 正在拉起主程序... echo --------------------------------------------------- -:: 这里的 gui_app.py 就是你刚才保存的那个带界面的 Python 文件名 -:: 如果你的文件名不一样,请修改下面这一行 -uv run UI.py +:: ======================================================= +:: 【关键修改】路径改为根目录的 main.pyw +:: 使用 start 命令启动,这样黑色的 CMD 窗口可以随后立即关闭 +:: ======================================================= +start "" uv run main.pyw -:: 错误捕获 -if %errorlevel% neq 0 ( - echo. - echo --------------------------------------------------- - echo [ERROR] 程序异常退出 (代码: %errorlevel%) - echo 请检查上方报错信息。 - pause -) else ( - echo. - echo [INFO] 程序已正常结束。 -) \ No newline at end of file +:: 等待 1 秒确保启动 +timeout /t 1 >nul + +:: 退出 CMD 窗口 (让用户只看到 GUI) +exit \ No newline at end of file diff --git a/templates/K4A 大班幼儿学期发展报告.pptx b/templates/K4A 大班幼儿学期发展报告.pptx new file mode 100644 index 0000000..faa72e0 Binary files /dev/null and b/templates/K4A 大班幼儿学期发展报告.pptx differ diff --git a/templates/横板/中班 幼儿学期发展报告.pptx b/templates/横板/中班 幼儿学期发展报告.pptx new file mode 100644 index 0000000..ad5de19 Binary files /dev/null and b/templates/横板/中班 幼儿学期发展报告.pptx differ diff --git a/templates/横板/大班 幼儿学期发展报告.pptx b/templates/横板/大班 幼儿学期发展报告.pptx new file mode 100644 index 0000000..55da711 Binary files /dev/null and b/templates/横板/大班 幼儿学期发展报告.pptx differ diff --git a/templates/横板/小班 幼儿学期发展报告.pptx b/templates/横板/小班 幼儿学期发展报告.pptx new file mode 100644 index 0000000..f3ce794 Binary files /dev/null and b/templates/横板/小班 幼儿学期发展报告.pptx differ diff --git a/templates/竖版/中班 幼儿学期发展报告.pptx b/templates/竖版/中班 幼儿学期发展报告.pptx new file mode 100644 index 0000000..d9899a5 Binary files /dev/null and b/templates/竖版/中班 幼儿学期发展报告.pptx differ diff --git a/templates/大班幼儿学期发展报告.pptx b/templates/竖版/大班 幼儿学期发展报告.pptx similarity index 99% rename from templates/大班幼儿学期发展报告.pptx rename to templates/竖版/大班 幼儿学期发展报告.pptx index f10505b..e7d9ad2 100644 Binary files a/templates/大班幼儿学期发展报告.pptx and b/templates/竖版/大班 幼儿学期发展报告.pptx differ diff --git a/templates/竖版/小班 幼儿学期发展报告.pptx b/templates/竖版/小班 幼儿学期发展报告.pptx new file mode 100644 index 0000000..9b5853d Binary files /dev/null and b/templates/竖版/小班 幼儿学期发展报告.pptx differ diff --git a/ui/app_window.py b/ui/app_window.py new file mode 100644 index 0000000..363eae1 --- /dev/null +++ b/ui/app_window.py @@ -0,0 +1,161 @@ +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.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 + + self._setup_ui() + 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) + main_content.pack(fill=tk.X) + + # === 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) + 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 run_task(self, target_func, *args, **kwargs): + if self.is_running: + messagebox.showwarning("忙碌中", "请先等待当前任务完成或点击【停止当前任务】") + return + + self.stop_event.clear() + self.is_running = True + + 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("--- 就绪 ---") + + 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() \ No newline at end of file diff --git a/utils/font_utils.py b/utils/font_utils.py index 77c9615..e80fb86 100644 --- a/utils/font_utils.py +++ b/utils/font_utils.py @@ -6,6 +6,8 @@ import platform import shutil from pathlib import Path +from loguru import logger + def get_system_fonts(): """获取系统中可用的字体列表""" @@ -21,7 +23,7 @@ def get_system_fonts(): for font_file in folder.glob(ext): fonts.add(font_file.stem) except Exception as e: - print(f"读取系统字体时出错: {e}") + logger.error(f"读取系统字体时出错: {e}") fonts = {"微软雅黑", "宋体", "黑体", "Arial", "Microsoft YaHei"} return fonts @@ -40,14 +42,14 @@ def is_font_available(font_name): def install_fonts_from_directory(fonts_dir="fonts"): """从指定目录安装字体到系统""" if platform.system() != "Windows": - print("字体安装功能目前仅支持Windows系统") + logger.success("字体安装功能目前仅支持Windows系统") return False target_font_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts' target_font_dir.mkdir(parents=True, exist_ok=True) if not os.path.exists(fonts_dir): - print(f"字体目录 {fonts_dir} 不存在,请创建该目录并将字体文件放入") + logger.error(f"字体目录 {fonts_dir} 不存在,请创建该目录并将字体文件放入") return False font_files = [] @@ -55,7 +57,7 @@ def install_fonts_from_directory(fonts_dir="fonts"): font_files.extend(Path(fonts_dir).glob(f'*{ext}')) if not font_files: - print(f"在 {fonts_dir} 目录中未找到字体文件") + logger.error(f"在 {fonts_dir} 目录中未找到字体文件") return False installed_count = 0 @@ -64,12 +66,12 @@ def install_fonts_from_directory(fonts_dir="fonts"): target_path = target_font_dir / font_file.name if not target_path.exists(): shutil.copy2(font_file, target_path) - print(f"已安装字体: {font_file.name}") + logger.success(f"已安装字体: {font_file.name}") installed_count += 1 except Exception as e: - print(f"安装字体 {font_file.name} 时出错: {str(e)}") + logger.error(f"安装字体 {font_file.name} 时出错: {str(e)}") if installed_count > 0: - print(f"共安装了 {installed_count} 个新字体文件,建议重启Python环境") + logger.success(f"共安装了 {installed_count} 个新字体文件,建议重启Python环境") return True return False diff --git a/utils/generate_utils.py b/utils/generate_utils.py index 6f3164b..c3c6ed6 100644 --- a/utils/generate_utils.py +++ b/utils/generate_utils.py @@ -6,6 +6,7 @@ import pandas as pd from loguru import logger from pptx import Presentation from rich.console import Console +import traceback import comtypes.client from config.config import load_config @@ -65,8 +66,6 @@ def generate_template(): except Exception as e: logger.error(f"程序运行出错: {str(e)}") # 打印详细报错位置,方便调试 - import traceback - logger.error(traceback.format_exc()) @@ -83,11 +82,10 @@ def generate_comment_all(): if "评价" not in df.columns: df["评价"] = "" - # --- 获取总行数,用于日志 --- - # 强制将“评价”列转换为 object 类型 + # 获取学生数据行数 total_count = len(df) logger.info(f"开始生成学生评语,共 {total_count} 位学生...") - + # 强制将“评价”列转换为 object 类型 df["评价"] = df["评价"].astype("object") # --- 遍历 DataFrame 的索引 (index) --- # 这样我们可以通过索引 i 精准地把数据写回某一行 @@ -121,28 +119,27 @@ def generate_comment_all(): logger.info(f"[{i + 1}/{total_count}] 正在生成评价: {name}") try: - # 调用你的生成函数,并【接收返回值】 - # 注意:这里假设 generate_comment 返回的是清洗后的字符串 + # 调用AI大模型生成内容 generated_text = generate_comment( name, config["age_group"], traits, sex ) - - # --- 将结果写入 DataFrame --- - df.at[i, "评价"] = generated_text - + if generated_text: + # 赋值 + df.at[i, "评价"] = str(generated_text).strip() + else: + df.at[i, "评价"] = "" # 防空处理 logger.success(f"学生:{name},评语生成完毕") - # 可选:每生成 5 个就保存一次,防止程序崩溃数据丢失 + # 可选:每生成 5 个就保存一次 if (i + 1) % 5 == 0: df.to_excel(excel_path, index=False) - logger.info("--- 阶段性保存成功 ---") - - time.sleep(1) # 避免触发API速率限制 - + logger.success("✅ 阶段性保存成功") + # 避免触发API速率限制 + time.sleep(1) except Exception as e: logger.error(f"学生:{name},生成评语出错: {str(e)}") - # --- 修改点 4: 循环结束后最终保存文件 --- + # --- 循环结束后最终保存文件 --- # index=False 表示不把 pandas 的索引 (0,1,2...) 写到 Excel 第一列 df.to_excel(excel_path, index=False) logger.success(f"所有评语已生成并写入文件:{excel_path}") @@ -151,8 +148,6 @@ def generate_comment_all(): logger.error(f"保存失败!请先关闭 Excel 文件:{config['excel_file']}") except Exception as e: logger.error(f"程序运行出错: {str(e)}") - import traceback - logger.error(traceback.format_exc()) @@ -306,9 +301,6 @@ def generate_report(): except Exception as e: logger.error(f"程序运行出错: {str(e)}") - # 打印详细报错位置,方便调试 - import traceback - logger.error(traceback.format_exc()) @@ -320,9 +312,8 @@ def batch_convert_folder(folder_path): 【推荐】批量转换文件夹下的所有 PPT (只启动一次 PowerPoint,速度快) 已修复多线程 CoInitialize 报错,并适配 GUI 日志 """ - # 【核心修复 1】子线程初始化 COM 组件 + # 子线程初始化 COM 组件 pythoncom.CoInitialize() - try: folder_path = os.path.abspath(folder_path) if not os.path.exists(folder_path): @@ -429,6 +420,4 @@ def generate_zodiac(): logger.error(f"找不到文件 {config.get('excel_file')}") except Exception as e: logger.error(f"程序运行出错: {str(e)}") - import traceback - logger.error(traceback.format_exc()) diff --git a/utils/image_utils.py b/utils/image_utils.py index 67dff08..938e53e 100644 --- a/utils/image_utils.py +++ b/utils/image_utils.py @@ -1,4 +1,6 @@ import os +from PIL import Image, ExifTags +import io def find_image_path(folder, base_filename): @@ -26,3 +28,39 @@ def find_image_path(folder, base_filename): return full_path return None + + +def get_corrected_image_stream(img_path): + """ + 读取图片,根据 EXIF 信息修正旋转方向,并返回 BytesIO 对象。 + 这样不需要修改原文件,直接在内存中处理。 + """ + image = Image.open(img_path) + + # 获取 EXIF 数据 + try: + for orientation in ExifTags.TAGS.keys(): + if ExifTags.TAGS[orientation] == 'Orientation': + break + + exif = dict(image._getexif().items()) + + if exif[orientation] == 3: + image = image.rotate(180, expand=True) + elif exif[orientation] == 6: + image = image.rotate(270, expand=True) + elif exif[orientation] == 8: + image = image.rotate(90, expand=True) + + except (AttributeError, KeyError, IndexError): + # 如果图片没有 EXIF 数据或不需要旋转,则忽略 + pass + + # 将处理后的图片保存到内存流中 + image_stream = io.BytesIO() + # 注意:保存时保持原格式,如果是 PNG 等无 EXIF 的格式会自动处理 + img_format = image.format if image.format else 'JPEG' + image.save(image_stream, format=img_format) + image_stream.seek(0) # 指针回到开头 + + return image_stream \ No newline at end of file diff --git a/utils/log_handler.py b/utils/log_handler.py new file mode 100644 index 0000000..8d4b1d5 --- /dev/null +++ b/utils/log_handler.py @@ -0,0 +1,27 @@ +import queue +import re +from loguru import logger + +# 全局日志队列 +log_queue = queue.Queue() + +def ansi_cleaner(text): + """去除 loguru 输出中的颜色代码,防止在 UI 显示乱码""" + ansi_escape = re.compile(r"\x1b\[[0-9;]*m") + return ansi_escape.sub("", text) + +def queue_sink(message): + """Loguru 的回调函数""" + clean_msg = ansi_cleaner(message) + log_queue.put(clean_msg) + +def setup_logging(): + """配置日志系统""" + # 清除默认的控制台输出,防止干扰 + logger.remove() + + # 添加队列输出 (给 UI 用) + logger.add(queue_sink, format="{time:HH:mm:ss} | {level: <8} | {message}", level="INFO") + + # 添加文件输出 (给开发者排查用) + # logger.add("logs/app_runtime.log", rotation="1 MB", encoding="utf-8", level="DEBUG") \ No newline at end of file diff --git a/utils/pptx_utils.py b/utils/pptx_utils.py index 76c5c6c..9d1b3cf 100644 --- a/utils/pptx_utils.py +++ b/utils/pptx_utils.py @@ -3,7 +3,10 @@ # ========================================== import os +from loguru import logger + from utils.font_utils import is_font_available +from utils.image_utils import get_corrected_image_stream def replace_text_in_slide(prs, slide_index, placeholder, text): @@ -94,18 +97,36 @@ def replace_text_in_slide(prs, slide_index, placeholder, text): def replace_picture(prs, slide_index, placeholder, img_path): - """在指定幻灯片中替换指定占位符的图片""" + """在指定幻灯片中替换指定占位符的图片(包含自动旋转修复)""" if not os.path.exists(img_path): - print(f"警告: 图片路径不存在 {img_path}") + logger.warning(f"警告: 图片路径不存在 {img_path}") return slide = prs.slides[slide_index] sp_tree = slide.shapes._spTree + target_shape = None + target_index = -1 + + # 1. 先找到目标形状和它的索引 for i, shape in enumerate(slide.shapes): if shape.name == placeholder: - left, top, width, height = shape.left, shape.top, shape.width, shape.height - sp_tree.remove(shape._element) - new_shape = slide.shapes.add_picture(img_path, left, top, width, height) - sp_tree.insert(i, new_shape._element) + target_shape = shape + target_index = i break + + if target_shape: + # 获取原位置信息 + left, top, width, height = target_shape.left, target_shape.top, target_shape.width, target_shape.height + + # 2. 获取修正后的图片流 + img_stream = get_corrected_image_stream(img_path) + + # 3. 移除旧形状 + sp_tree.remove(target_shape._element) + + # 4. 插入新图片 (使用流而不是路径) + new_shape = slide.shapes.add_picture(img_stream, left, top, width, height) + + # 5. 恢复层级位置 (z-order) + sp_tree.insert(target_index, new_shape._element) diff --git a/uv.lock b/uv.lock index 7a4d3e0..1c41422 100644 --- a/uv.lock +++ b/uv.lock @@ -121,6 +121,7 @@ dependencies = [ { name = "openpyxl" }, { name = "pandas" }, { name = "pandas-stubs" }, + { name = "pillow" }, { name = "python-pptx" }, { name = "pywin32" }, { name = "rich" }, @@ -136,6 +137,7 @@ requires-dist = [ { name = "openpyxl", specifier = ">=3.1.5" }, { name = "pandas", specifier = ">=2.3.3" }, { name = "pandas-stubs", specifier = "==2.3.3.251201" }, + { name = "pillow", specifier = ">=12.0.0" }, { name = "python-pptx", specifier = ">=1.0.2" }, { name = "pywin32", specifier = ">=311" }, { name = "rich", specifier = ">=14.2.0" },