fix:添加UI界面,完善功能

This commit is contained in:
2025-12-11 11:16:09 +08:00
parent 81e3c40abb
commit f437842a81
17 changed files with 939 additions and 406 deletions

199
UI.py Normal file
View File

@@ -0,0 +1,199 @@
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()

View File

@@ -1,37 +1,71 @@
# 尝试导入 toml 解析库 (兼容 Python 3.11+ 和旧版本)
import os import os
import sys import sys
# 尝试导入 toml 解析库
try: try:
import tomllib as toml # Python 3.11+ 内置 import tomllib as toml # Python 3.11+
except ImportError: except ImportError:
try: try:
import tomli as toml # 需要 pip install tomli import tomli as toml # pip install tomli
except ImportError: except ImportError:
print("错误: 缺少 TOML 解析库。请运行: pip install tomli") print("错误: 缺少 TOML 解析库。请运行: pip install tomli")
sys.exit(1) sys.exit(1)
def get_main_path():
"""
获取程序运行的根目录
兼容:
1. PyInstaller 打包后的 .exe 环境
2. 开发环境 (假设此脚本在子文件夹中,如 utils/)
"""
if getattr(sys, 'frozen', False):
# --- 情况 A: 打包后的 exe ---
# exe 就在根目录下,直接取 exe 所在目录
return os.path.dirname(sys.executable)
else:
# --- 情况 B: 开发环境 (.py) ---
# 1. 获取当前脚本的绝对路径 (例如: .../MyProject/utils/config_loader.py)
current_file_path = os.path.abspath(__file__)
# 2. 获取当前脚本所在的文件夹 (例如: .../MyProject/utils)
current_dir = os.path.dirname(current_file_path)
# 3. 【关键修改】再往上一层,获取项目根目录 (例如: .../MyProject)
# 如果你的脚本藏得更深,就再套一层 os.path.dirname
project_root = os.path.dirname(current_dir)
return project_root
# ========================================== # ==========================================
# 1. 配置加载 (Config Loader) # 1. 配置加载 (Config Loader)
# ========================================== # ==========================================
def load_config(config_path="config.toml"): def load_config(config_filename="config.toml"):
"""读取 TOML 配置文件并转换为扁平字典""" """读取 TOML 配置文件"""
# 1. 先获取正确的根目录
main_dir = get_main_path()
# 2. 拼接配置文件的绝对路径 (防止在不同目录下运行脚本时找不到配置文件)
config_path = os.path.join(main_dir, config_filename)
if not os.path.exists(config_path): if not os.path.exists(config_path):
print(f"错误: 找不到配置文件 {config_path}") print(f"错误: 在路径 {main_dir}找不到配置文件 {config_filename}")
print(f"尝试寻找的完整路径是: {config_path}")
sys.exit(1) sys.exit(1)
try: try:
with open(config_path, "rb") as f: with open(config_path, "rb") as f:
data = toml.load(f) data = toml.load(f)
# 将 TOML 的层级结构映射回原本的扁平结构,保持代码其余部分不用大改 # 将 TOML 的层级结构映射回扁平结构
# 关键点:所有的 os.path.join 都必须基于 main_dir (项目根目录)
config = { config = {
"source_file": data["paths"]["source_file"], "root_path": main_dir, # 方便调试,把根目录也存进去
"output_folder": data["paths"]["output_folder"], "source_file": os.path.join(main_dir, data["paths"]["source_file"]),
"excel_file": data["paths"]["excel_file"], "output_folder": os.path.join(main_dir, data["paths"]["output_folder"]),
"image_folder": data["paths"]["image_folder"], "excel_file": os.path.join(main_dir, data["paths"]["excel_file"]),
"fonts_dir": data["paths"]["fonts_dir"], "image_folder": os.path.join(main_dir, data["paths"]["image_folder"]),
"fonts_dir": os.path.join(main_dir, data["paths"]["fonts_dir"]),
"class_name": data["class_info"]["class_name"], "class_name": data["class_info"]["class_name"],
"teachers": data["class_info"]["teachers"], "teachers": data["class_info"]["teachers"],
"default_comment": data["defaults"].get("default_comment", "暂无评语"), "default_comment": data["defaults"].get("default_comment", "暂无评语"),
@@ -41,4 +75,4 @@ def load_config(config_path="config.toml"):
return config return config
except Exception as e: except Exception as e:
print(f"读取配置文件出错: {e}") print(f"读取配置文件出错: {e}")
sys.exit(1) sys.exit(1)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

340
main.py
View File

@@ -1,285 +1,12 @@
import os from utils.generate_utils import (
import time generate_template,
generate_comment_all,
import pandas as pd generate_report,
from loguru import logger batch_convert_folder,
from pptx import Presentation generate_zodiac,
from pptx.util import Pt )
from rich import box from utils.file_utils import export_data_folder, initialize_project
from rich.align import Align from UI import applicationUI
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn
from rich.prompt import Prompt
from rich.table import Table
from config.config import load_config
from utils.agent_utils import generate_comment
from utils.font_utils import install_fonts_from_directory
from utils.image_utils import find_image_path
from utils.pef_utils import batch_convert_folder
from utils.pptx_utils import replace_text_in_slide, replace_picture
# 如果你之前没有全局定义 console这里定义一个
console = Console()
# ==========================================
# 1. 配置区域 (Configuration)
# ==========================================
config = load_config("config.toml")
# ==========================================
# 2. 业务逻辑函数 (Business Logic)
# ==========================================
def replace_one_page(prs, name, class_name):
"""替换第一页信息"""
replace_text_in_slide(prs, 0, "name", name)
replace_text_in_slide(prs, 0, "class", class_name)
def replace_two_page(prs, comments, teacher_name):
"""替换第二页信息"""
replace_text_in_slide(prs, 1, "comments", comments)
replace_text_in_slide(prs, 1, "teacher_name", teacher_name)
def replace_three_page(prs, info_dict, me_image):
"""替换第三页信息"""
# 使用字典解包传递多个字段,减少参数数量
fields = ["name", "english_name", "sex", "birthday", "zodiac", "friend", "hobby", "game", "food"]
for field in fields:
replace_text_in_slide(prs, 2, field, info_dict.get(field, ""))
replace_picture(prs, 2, "me_image", me_image)
def replace_four_page(prs, class_image):
"""替换第四页信息"""
replace_picture(prs, 3, "class_image", class_image)
def replace_five_page(prs, image1, image2):
"""替换第五页信息"""
replace_picture(prs, 4, "image1", image1)
replace_picture(prs, 4, "image2", image2)
# ==========================================
# 3. 生成成长报告(根据names.xlsx文件生成)
# ==========================================
def generate_report():
# 1. 资源准备
if install_fonts_from_directory(config["fonts_dir"]):
logger.info("等待系统识别新安装的字体...")
time.sleep(2)
os.makedirs(config["output_folder"], exist_ok=True)
if not os.path.exists(config["source_file"]):
logger.info(f"错误: 找不到模版文件 {config['source_file']}")
return
if not os.path.exists(config["excel_file"]):
logger.info(f"错误: 找不到数据文件 {config['excel_file']}")
return
try:
# 2. 读取数据
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
# 确保列名对应
columns = ["姓名", "英文名", "性别", "生日", "属相", "我的好朋友", "我的爱好", "喜欢的游戏", "喜欢吃的食物",
"评价"]
datas = df[columns].values.tolist()
teacher_names_str = " ".join(config["teachers"])
logger.info(f"开始处理,共 {len(datas)} 位学生...")
# 3. 循环处理
for i, row_data in enumerate(datas):
# 解包数据
(name, english_name, sex, birthday, zodiac, friend, hobby, game, food, comments) = row_data
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
# 每次循环重新加载模版
prs = Presentation(config["source_file"])
# --- 页面 1 ---
replace_one_page(prs, name, config["class_name"])
# --- 页面 2 ---
replace_two_page(prs, comments, teacher_names_str)
# --- 页面 3 ---
student_image_folder = os.path.join(config["image_folder"], name)
if os.path.exists(student_image_folder):
me_image_path = find_image_path(student_image_folder, "me_image")
# 构造信息字典供 helper 使用
info_dict = {
"name": name, "english_name": english_name, "sex": sex,
"birthday": birthday, "zodiac": zodiac, "friend": friend,
"hobby": hobby, "game": game, "food": food
}
replace_three_page(prs, info_dict, me_image_path)
else:
logger.warning(f"错误: 学生图片文件夹不存在 {student_image_folder}")
# --- 页面 4 ---
class_image_path = find_image_path(config["image_folder"], config["class_name"])
if os.path.exists(class_image_path):
replace_four_page(prs, class_image_path)
else:
logger.warning(f"错误: 班级图片文件不存在 {class_image_path}")
# --- 页面 5 ---
if os.path.exists(student_image_folder):
img1_path = find_image_path(student_image_folder, "1")
img2_path = find_image_path(student_image_folder, "2")
# 逻辑优化:
# 情况A: 两张都找到了 -> 正常插入
if img1_path and img2_path:
replace_five_page(prs, img1_path, img2_path)
# 情况B: 只找到了 1 -> 两张图都用 1 (避免报错)
elif img1_path and not img2_path:
replace_five_page(prs, img1_path, img1_path)
# 情况C: 一张都没找到
else:
logger.warning(f"⚠️ 警告: {name} 缺少生活照片 (1.jpg/png 或 2.jpg/png)[/]")
else:
logger.warning(f"错误: 学生图片文件夹不存在 {student_image_folder}")
# --- 保存文件 ---
file_ext = os.path.splitext(config["source_file"])[1]
safe_name = str(name).strip()
new_filename = f"{config['class_name']} {safe_name} 幼儿成长报告{file_ext}"
output_path = os.path.join(config["output_folder"], new_filename)
try:
prs.save(output_path)
logger.success(f"学生:{name},保存成功: {new_filename}")
except PermissionError:
logger.error(f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。")
logger.success("所有报告生成完毕!")
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
# 打印详细报错位置,方便调试
import traceback
logger.error(traceback.format_exc())
# ==========================================
# 4. 生成模板(根据names.xlsx文件生成名字图片文件夹)
# ==========================================
def generate_template():
try:
# 2. 读取数据
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
# --- 修改点开始 ---
# 直接读取 "姓名" 这一列,不使用列表包裹列名,这样得到的是一维数据
datas = df["姓名"].values.tolist()
# --- 修改点结束 ---
logger.info(f"开始生成学生模版文件,共 {len(datas)} 位学生...")
# 3. 循环处理
# 此时 name 就是字符串 '张三',而不是列表 ['张三']
for i, name in enumerate(datas):
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
# 确保 name 是字符串且去除了空格 (增加健壮性)
name = str(name).strip()
student_folder = os.path.join(config["image_folder"], name)
if os.path.exists(student_folder):
logger.info(f"学生图片文件夹已存在 {student_folder}")
else:
logger.info(f"正在生成学生图片文件夹 {student_folder}")
os.makedirs(student_folder, exist_ok=True)
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
# 打印详细报错位置,方便调试
import traceback
logger.error(traceback.format_exc())
# ==========================================
# 5. 生成评语(根据names.xlsx文件生成评价)
# ==========================================
def generate_comment_all():
try:
# 1. 读取数据
excel_path = config["excel_file"]
df = pd.read_excel(excel_path, sheet_name="Sheet1")
# 检查是否存在"评价"列,不存在则新建(防止报错)
if "评价" not in df.columns:
df["评价"] = ""
# --- 获取总行数,用于日志 ---
# 强制将“评价”列转换为 object 类型
total_count = len(df)
logger.info(f"开始生成学生评语,共 {total_count} 位学生...")
df["评价"] = df["评价"].astype("object")
# --- 遍历 DataFrame 的索引 (index) ---
# 这样我们可以通过索引 i 精准地把数据写回某一行
for i in df.index:
name = df.at[i, "姓名"] # 获取当前行的姓名
# 健壮性处理
if pd.isna(name): continue # 跳过空行
name = str(name).strip()
# 获取当前行的特征如果Excel里有“特征”这一列就读没有就用默认值
# 假设Excel里有一列叫 "表现特征",如果没有则用默认的 "有礼貌..."
traits = df.at[i, "表现特征"] if "表现特征" in df.columns and not pd.isna(
df.at[i, "表现特征"]) else "有礼貌、守纪律"
# 优化如果“评价”列已经有内容了跳过不生成节省API费用
current_comment = df.at[i, "评价"]
if not pd.isna(current_comment) and str(current_comment).strip() != "":
logger.info(f"[{i + 1}/{total_count}] {name} 已有评语,跳过。")
continue
logger.info(f"[{i + 1}/{total_count}] 正在生成评价: {name}")
try:
# 调用你的生成函数,并【接收返回值】
# 注意:这里假设 generate_comment 返回的是清洗后的字符串
generated_text = generate_comment(name, config["age_group"], traits)
# --- 将结果写入 DataFrame ---
df.at[i, "评价"] = generated_text
logger.success(f"学生:{name},评语生成完毕")
# 可选:每生成 5 个就保存一次,防止程序崩溃数据丢失
if (i + 1) % 5 == 0:
df.to_excel(excel_path, index=False)
logger.info("--- 阶段性保存成功 ---")
time.sleep(1) # 避免触发API速率限制
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}")
except PermissionError:
logger.error(f"保存失败!请先关闭 Excel 文件:{config['excel_file']}")
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
import traceback
logger.error(traceback.format_exc())
def application(): def application():
@@ -304,11 +31,14 @@ def application():
table.add_column(justify="left") table.add_column(justify="left")
# 3. 添加行内容 # 3. 添加行内容
table.add_row("1.", "📁 生成模板") table.add_row("1.", "📁 生成图片路径(每一个幼儿一个图片文件夹)")
table.add_row("2.", "🤖 生成评语") table.add_row("2.", "🤖 生成评语(根据姓名、学段、性别)")
table.add_row("3.", "📊 生成报告") table.add_row("3.", "📊 生成报告(根据表格生成)")
table.add_row("4.", "📑 格式转换") # 新增 table.add_row("4.", "📑 格式转换PPT转PDF")
table.add_row("5.", "🚪 退出系统") table.add_row("5.", "📑 生肖转化(根据生日)")
table.add_row("6.", "📦 导出数据模板Zip")
table.add_row("7.", "📦 初始化系统")
table.add_row("8.", "🚪 退出系统")
# 4. 将表格放入面板,并居中显示 # 4. 将表格放入面板,并居中显示
panel = Panel( panel = Panel(
@@ -317,19 +47,25 @@ def application():
subtitle="[dim]By 寒寒", subtitle="[dim]By 寒寒",
width=60, width=60,
border_style="bright_blue", border_style="bright_blue",
box=box.ROUNDED # 圆角边框更柔和 box=box.ROUNDED, # 圆角边框更柔和
) )
# 使用 Align.center 让整个菜单在屏幕中间显示 # 使用 Align.center 让整个菜单在屏幕中间显示
console.print(Align.center(panel, vertical="middle")) console.print(Align.center(panel, vertical="middle"))
console.print("\n") # 留点空隙 console.print("\n") # 留点空隙
choice = Prompt.ask("👉 请输入序号执行", choices=["1", "2", "3", "4", "5"], default="1") choice = Prompt.ask(
"👉 请输入序号执行",
choices=["1", "2", "3", "4", "5", "6", "7","8"],
default="1",
)
try: try:
if choice == "1": if choice == "1":
console.rule("[bold cyan]正在执行: 生成模板[/]") console.rule("[bold cyan]正在执行: 生成模板[/]")
with console.status("[bold green]正在创建文件夹结构...[/]", spinner="dots"): with console.status(
"[bold green]正在创建文件夹结构...[/]", spinner="dots"
):
generate_template() generate_template()
elif choice == "2": elif choice == "2":
console.rule("[bold yellow]正在执行: AI 生成评语[/]") console.rule("[bold yellow]正在执行: AI 生成评语[/]")
@@ -337,20 +73,38 @@ def application():
generate_comment_all() generate_comment_all()
elif choice == "3": elif choice == "3":
console.rule("[bold blue]正在执行: PPT 合成[/]") console.rule("[bold blue]正在执行: PPT 合成[/]")
with console.status("[bold blue]正在处理图片和文字...[/]", spinner="earth"): with console.status(
"[bold blue]正在处理图片和文字...[/]", spinner="earth"
):
generate_report() generate_report()
elif choice == "4": elif choice == "4":
console.rule("[bold magenta]正在执行: PDF 批量转换[/]") console.rule("[bold magenta]正在执行: PDF 批量转换[/]")
# 调用上面的批量转换函数,传入你的 output 文件夹路径 # 调用上面的批量转换函数,传入你的 output 文件夹路径
batch_convert_folder(config["output_folder"]) batch_convert_folder(config["output_folder"])
elif choice == "5": elif choice == "5":
console.rule("[bold magenta]正在执行: 生肖转化[/]")
# 调用上面的批量转换函数,传入你的 output 文件夹路径
generate_zodiac()
elif choice == "6":
console.rule("[bold magenta]正在执行: 导出数据模板[/]")
# 调用上面的批量转换函数,传入你的 output 文件夹路径
export_data_folder()
elif choice == "7":
console.rule("[bold magenta]正在执行: 初始化系统[/]")
# 调用上面的批量转换函数,传入你的 output 文件夹路径
initialize_project()
elif choice == "8":
console.print("[bold red]👋 再见![/]") console.print("[bold red]👋 再见![/]")
sys.exit() sys.exit()
Prompt.ask("按 [bold]Enter[/] 键返回主菜单...") Prompt.ask("按 [bold]Enter[/] 键返回主菜单...")
except Exception as e: except Exception as e:
console.print(Panel(f"[bold red]❌ 发生错误:[/]\n{e}", title="Error", border_style="red")) console.print(
Panel(
f"[bold red]❌ 发生错误:[/]\n{e}", title="Error", border_style="red"
)
)
Prompt.ask("按 Enter 键继续...") Prompt.ask("按 Enter 键继续...")
if __name__ == "__main__": if __name__ == "__main__":
application() applicationUI()

42
start_app.bat Normal file
View File

@@ -0,0 +1,42 @@
@echo off
:: ------------------------------------------------
:: 自动切换编码为 UTF-8解决中文乱码问题
chcp 65001 >nul
:: ------------------------------------------------
title 幼儿园成长报告助手
cd /d "%~dp0"
echo.
echo ==========================================
echo 正在启动 幼儿园成长报告助手
echo ==========================================
echo.
:: 检查 uv 是否安装
uv --version >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] 未检测到 uv 工具!
echo 请先运行: pip install uv
pause
exit /b
)
echo [INFO] 环境检查通过,正在运行主程序...
echo ---------------------------------------------------
:: 这里的 gui_app.py 就是你刚才保存的那个带界面的 Python 文件名
:: 如果你的文件名不一样,请修改下面这一行
uv run main.py
:: 错误捕获
if %errorlevel% neq 0 (
echo.
echo ---------------------------------------------------
echo [ERROR] 程序异常退出 (代码: %errorlevel%)
echo 请检查上方报错信息。
pause
) else (
echo.
echo [INFO] 程序已正常结束。
)

BIN
templates/names.xlsx Normal file

Binary file not shown.

View File

@@ -10,12 +10,13 @@ from config.config import load_config
config = load_config("config.toml") config = load_config("config.toml")
def generate_comment(name, age_group, traits): def generate_comment(name, age_group, traits,sex):
""" """
生成评语 生成评语
:param name: 学生姓名 :param name: 学生姓名
:param age_group: 所在班级 :param age_group: 所在班级
:param traits: 表现特征 :param traits: 表现特征
:param sex: 性别
:return: 评语 :return: 评语
""" """
@@ -29,7 +30,7 @@ def generate_comment(name, age_group, traits):
# 2. 构建 Prompt Template # 2. 构建 Prompt Template
prompt = ChatPromptTemplate.from_messages([ prompt = ChatPromptTemplate.from_messages([
("system", ai_config["prompt"]), ("system", ai_config["prompt"]),
("human", "学生姓名:{name}\n所在班级:{age_group}\n表现特征:{traits}\n\n请开始撰写评语:") ("human", "学生姓名:{name}\n所在班级:{age_group}\n性别:{sex}\n表现特征:{traits}\n\n请开始撰写评语:")
]) ])
# 3. 组装链 (Prompt -> Model -> OutputParser) # 3. 组装链 (Prompt -> Model -> OutputParser)
@@ -40,11 +41,12 @@ def generate_comment(name, age_group, traits):
comment = chain.invoke({ comment = chain.invoke({
"name": name, "name": name,
"age_group": age_group, "age_group": age_group,
"traits": traits "traits": traits,
"sex": sex
}) })
cleaned_text = re.sub(r'\s+', '', comment) cleaned_text = re.sub(r'\s+', '', comment)
logger.success(f"学生:{name} =>生成评语成功: {cleaned_text}") logger.success(f"学生:{name} =>生成评语成功: {cleaned_text}")
return cleaned_text return cleaned_text
except Exception as e: except Exception as e:
print(f"生成评语失败: {e}") logger.error(f"生成评语失败: {e}")
return "生成失败请检查网络或Key。" return "生成失败请检查网络或Key。"

121
utils/file_utils.py Normal file
View File

@@ -0,0 +1,121 @@
import shutil
import os
import time
from loguru import logger
def export_data_folder(source_folder="data", output_folder="backup"):
"""
将指定文件夹压缩为 zip 包
:param source_folder: 要压缩的文件夹路径 (默认 'data')
:param output_folder: 压缩包存放的文件夹路径 (默认 'backup')
"""
try:
# 1. 检查源文件夹是否存在
if not os.path.exists(source_folder):
logger.error(f"备份失败: 找不到源文件夹 '{source_folder}'")
return
# 2. 确保输出目录存在
if not os.path.exists(output_folder):
os.makedirs(output_folder)
logger.info(f"创建备份目录: {output_folder}")
# 3. 生成带时间戳的文件名 (例如: data_backup_20251211_103000)
timestamp = time.strftime("%Y%m%d_%H%M%S")
# 注意: make_archive 不需要写后缀 .zip它会自动加
base_name = os.path.join(output_folder, f"data_备份_{timestamp}")
logger.info(f"正在压缩 '{source_folder}' ...")
# 4. 执行压缩
# format="zip": 压缩格式
# root_dir: 要压缩的根目录的上级目录 (通常填 None)
# base_dir: 要压缩的具体目录
zip_path = shutil.make_archive(
base_name=base_name,
format="zip",
root_dir=os.path.dirname(
os.path.abspath(source_folder)
), # 这里为了安全,定位到父级
base_dir=os.path.basename(
os.path.abspath(source_folder)
), # 只压缩 data 文件夹本身
)
logger.success(f"✅ 备份成功! 文件已保存至:\n{zip_path}")
return zip_path
except Exception as e:
logger.error(f"❌ 备份出错: {str(e)}")
import traceback
logger.error(traceback.format_exc())
def initialize_project(root_dir="."):
"""
初始化项目:清空 data重建目录复制模板
:param root_dir: 项目根目录
"""
# 定义路径
data_dir = os.path.join(root_dir, "data")
images_dir = os.path.join(data_dir, "images")
output_dir = os.path.join(root_dir, "output")
# 模板源文件路径
template_dir = os.path.join(root_dir, "templates")
src_excel = os.path.join(template_dir, "names.xlsx")
dst_excel = os.path.join(data_dir, "names.xlsx")
logger.info("开始初始化/重置项目...")
# --- 1. 清空 data 文件夹 ---
if os.path.exists(data_dir):
try:
# 暴力删除整个 data 文件夹及其内容
shutil.rmtree(data_dir)
logger.info(f"已清理旧数据: {data_dir}")
except PermissionError:
logger.error(
f"❌ 删除失败!请检查 '{data_dir}' 下的文件(如 Excel是否正在被打开。"
)
return
except Exception as e:
logger.error(f"❌ 删除出错: {e}")
return
# --- 2. 清空 output_dir 文件夹 ---
if os.path.exists(output_dir):
try:
# 暴力删除整个 output 文件夹及其内容
shutil.rmtree(output_dir)
logger.info(f"已清理旧数据: {output_dir}")
except PermissionError:
logger.error(f"❌ 删除失败!请检查 '{output_dir}' 下的文件是否正在被打开。")
return
except Exception as e:
logger.error(f"❌ 删除出错: {e}")
return
# --- 2. 新建目录结构 ---
try:
# makedirs 会自动创建父级目录 (data 和 data/images 都会被创建)
os.makedirs(images_dir, exist_ok=True)
os.makedirs(output_dir, exist_ok=True)
logger.info(f"✅ 已创建目录: {images_dir}")
except Exception as e:
logger.error(f"创建目录失败: {e}")
return
# --- 3. 复制模板文件 ---
if os.path.exists(src_excel):
try:
shutil.copy(src_excel, dst_excel)
logger.success(f"✅ 成功从模板恢复: {dst_excel}")
logger.success("🎉 初始化完成!一切已恢复出厂设置。")
except Exception as e:
logger.error(f"复制模板文件失败: {e}")
else:
logger.warning(
f"⚠️ 警告: 模板文件不存在 ({src_excel})data 文件夹内将没有 Excel 文件。"
)

358
utils/generate_utils.py Normal file
View File

@@ -0,0 +1,358 @@
import os
import time
import pandas as pd
from loguru import logger
from pptx import Presentation
from rich.console import Console
import comtypes.client
from config.config import load_config
from utils.agent_utils import generate_comment
from utils.font_utils import install_fonts_from_directory
from utils.image_utils import find_image_path
from utils.zodiac_utils import calculate_zodiac
from utils.growt_utils import replace_one_page, replace_two_page, replace_three_page, replace_four_page, replace_five_page
# 如果你之前没有全局定义 console这里定义一个
console = Console()
# ==========================================
# 1. 配置区域 (Configuration)
# ==========================================
config = load_config("config.toml")
# ==========================================
# 1. 生成模板(根据names.xlsx文件生成名字图片文件夹)
# ==========================================
def generate_template():
try:
# 2. 读取数据
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
# --- 修改点开始 ---
# 直接读取 "姓名" 这一列,不使用列表包裹列名,这样得到的是一维数据
datas = df["姓名"].values.tolist()
# --- 修改点结束 ---
logger.info(f"开始生成学生模版文件,共 {len(datas)} 位学生...")
# 3. 循环处理
# 此时 name 就是字符串 '张三',而不是列表 ['张三']
for i, name in enumerate(datas):
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
# 确保 name 是字符串且去除了空格 (增加健壮性)
name = str(name).strip()
student_folder = os.path.join(config["image_folder"], name)
if os.path.exists(student_folder):
logger.info(f"学生图片文件夹已存在 {student_folder}")
else:
logger.info(f"正在生成学生图片文件夹 {student_folder}")
os.makedirs(student_folder, exist_ok=True)
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
# 打印详细报错位置,方便调试
import traceback
logger.error(traceback.format_exc())
# ==========================================
# 2. 生成评语(根据names.xlsx文件生成评价)
# ==========================================
def generate_comment_all():
try:
# 1. 读取数据
excel_path = config["excel_file"]
df = pd.read_excel(excel_path, sheet_name="Sheet1")
# 检查是否存在"评价"列,不存在则新建(防止报错)
if "评价" not in df.columns:
df["评价"] = ""
# --- 获取总行数,用于日志 ---
# 强制将“评价”列转换为 object 类型
total_count = len(df)
logger.info(f"开始生成学生评语,共 {total_count} 位学生...")
df["评价"] = df["评价"].astype("object")
# --- 遍历 DataFrame 的索引 (index) ---
# 这样我们可以通过索引 i 精准地把数据写回某一行
for i in df.index:
name = df.at[i, "姓名"] # 获取当前行的姓名
sex = df.at[i,"性别"]
if pd.isna(sex):
sex = ""
else:
sex = str(sex).strip()
# 健壮性处理
if pd.isna(name): continue # 跳过空行
name = str(name).strip()
# 获取当前行的特征如果Excel里有“特征”这一列就读没有就用默认值
# 假设Excel里有一列叫 "表现特征",如果没有则用默认的 "有礼貌..."
traits = df.at[i, "表现特征"] if "表现特征" in df.columns and not pd.isna(
df.at[i, "表现特征"]) else "有礼貌、守纪律"
# 优化如果“评价”列已经有内容了跳过不生成节省API费用
current_comment = df.at[i, "评价"]
if not pd.isna(current_comment) and str(current_comment).strip() != "":
logger.info(f"[{i + 1}/{total_count}] {name} 已有评语,跳过。")
continue
logger.info(f"[{i + 1}/{total_count}] 正在生成评价: {name}")
try:
# 调用你的生成函数,并【接收返回值】
# 注意:这里假设 generate_comment 返回的是清洗后的字符串
generated_text = generate_comment(name, config["age_group"], traits,sex)
# --- 将结果写入 DataFrame ---
df.at[i, "评价"] = generated_text
logger.success(f"学生:{name},评语生成完毕")
# 可选:每生成 5 个就保存一次,防止程序崩溃数据丢失
if (i + 1) % 5 == 0:
df.to_excel(excel_path, index=False)
logger.info("--- 阶段性保存成功 ---")
time.sleep(1) # 避免触发API速率限制
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}")
except PermissionError:
logger.error(f"保存失败!请先关闭 Excel 文件:{config['excel_file']}")
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
import traceback
logger.error(traceback.format_exc())
# ==========================================
# 3. 生成成长报告(根据names.xlsx文件生成)
# ==========================================
def generate_report():
# 1. 资源准备
if install_fonts_from_directory(config["fonts_dir"]):
logger.info("等待系统识别新安装的字体...")
time.sleep(2)
os.makedirs(config["output_folder"], exist_ok=True)
if not os.path.exists(config["source_file"]):
logger.info(f"错误: 找不到模版文件 {config['source_file']}")
return
if not os.path.exists(config["excel_file"]):
logger.info(f"错误: 找不到数据文件 {config['excel_file']}")
return
try:
# 2. 读取数据
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
# 确保列名对应
columns = ["姓名", "英文名", "性别", "生日", "属相", "我的好朋友", "我的爱好", "喜欢的游戏", "喜欢吃的食物",
"评价"]
datas = df[columns].values.tolist()
teacher_names_str = " ".join(config["teachers"])
logger.info(f"开始处理,共 {len(datas)} 位学生...")
# 3. 循环处理
for i, row_data in enumerate(datas):
# 解包数据
(name, english_name, sex, birthday, zodiac, friend, hobby, game, food, comments) = row_data
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
# 每次循环重新加载模版
template_source_file = os.path.join("templates", config["source_file"])
prs = Presentation(template_source_file)
# --- 页面 1 ---
replace_one_page(prs, name, config["class_name"])
# --- 页面 2 ---
replace_two_page(prs, comments, teacher_names_str)
# --- 页面 3 ---
student_image_folder = os.path.join(config["image_folder"], name)
logger.info(f"学生图片文件夹: {student_image_folder}")
if os.path.exists(student_image_folder):
me_image_path = find_image_path(student_image_folder, "me_image")
# 构造信息字典供 helper 使用
info_dict = {
"name": name, "english_name": english_name, "sex": sex,
"birthday": birthday, "zodiac": zodiac, "friend": friend,
"hobby": hobby, "game": game, "food": food
}
# 逻辑:必须同时满足 "不是None" 且 "是字符串" 且 "文件存在" 才能执行
if me_image_path and isinstance(me_image_path, str) and os.path.exists(me_image_path):
logger.info(f"学生:{name},图片路径有效: {me_image_path},正在替换...")
replace_three_page(prs, info_dict, me_image_path)
else:
# 只有在这里打印日志,告诉用户跳过了,但不中断程序
replace_three_page(prs, info_dict, None)
else:
logger.warning(f"错误: 学生图片文件夹不存在 {student_image_folder}")
# --- 页面 4 ---
class_image_path = find_image_path(config["image_folder"], config["class_name"])
if os.path.exists(class_image_path):
replace_four_page(prs, class_image_path)
else:
logger.warning(f"错误: 班级图片文件不存在 {class_image_path}")
# --- 页面 5 ---
if os.path.exists(student_image_folder):
img1_path = find_image_path(student_image_folder, "1")
img2_path = find_image_path(student_image_folder, "2")
# 逻辑优化:
# 情况A: 两张都找到了 -> 正常插入
if img1_path and img2_path:
replace_five_page(prs, img1_path, img2_path)
# 情况B: 只找到了 1 -> 两张图都用 1 (避免报错)
elif img1_path and not img2_path:
replace_five_page(prs, img1_path, img1_path)
# 情况C: 一张都没找到
else:
logger.warning(f"⚠️ 警告: {name} 缺少作品照片 (1.jpg/png 或 2.jpg/png)[/]")
else:
logger.warning(f"错误: 学生图片文件夹不存在 {student_image_folder}")
# --- 保存文件 ---
file_ext = os.path.splitext(config["source_file"])[1]
safe_name = str(name).strip()
new_filename = f"{config['class_name']} {safe_name} 幼儿成长报告{file_ext}"
output_path = os.path.join(config["output_folder"], new_filename)
try:
prs.save(output_path)
logger.success(f"学生:{name},保存成功: {new_filename}")
except PermissionError:
logger.error(f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。")
logger.success("所有报告生成完毕!")
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
# 打印详细报错位置,方便调试
import traceback
logger.error(traceback.format_exc())
# ==========================================
# 5. 转换格式(根据names.xlsx文件生成PPT转PDF)
# ==========================================
def batch_convert_folder(folder_path):
"""
【推荐】批量转换文件夹下的所有 PPT (只启动一次 PowerPoint速度快)
"""
folder_path = os.path.abspath(folder_path)
if not os.path.exists(folder_path):
print("文件夹不存在")
return
# 获取所有 ppt/pptx 文件
files = [f for f in os.listdir(folder_path) if f.lower().endswith(('.ppt', '.pptx'))]
if not files:
print("没有找到 PPT 文件")
return
print(f"发现 {len(files)} 个文件,准备开始转换...")
powerpoint = None
try:
# 1. 启动应用 (只启动一次)
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
# 某些环境下需要设为可见,否则无法运行
# powerpoint.Visible = 1
for filename in files:
ppt_path = os.path.join(folder_path, filename)
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
# 如果 PDF 已存在,可以选择跳过
if os.path.exists(pdf_path):
print(f"[跳过] 已存在: {filename}")
continue
print(f"正在转换: {filename} ...")
try:
# 打开 -> 另存为 -> 关闭
deck = powerpoint.Presentations.Open(ppt_path)
deck.SaveAs(pdf_path, 32)
deck.Close()
except Exception as e:
print(f"文件 {filename} 转换出错: {e}")
except Exception as e:
print(f"PowerPoint 进程出错: {e}")
finally:
# 2. 退出应用
if powerpoint:
powerpoint.Quit()
print("PowerPoint 已关闭,批量转换完成。")
# ==========================================
# 5. 生成属相(根据names.xlsx文件生成属相)
# ==========================================
def generate_zodiac():
try:
# 1. 读取数据
excel_path = config["excel_file"]
# sheet_name 根据实际情况修改,如果不确定可以用 sheet_name=0 读取第一个
df = pd.read_excel(excel_path, sheet_name="Sheet1")
# 2. 检查必要的列
date_column = '生日'
target_column = '属相'
if date_column not in df.columns:
logger.error(f"Excel中找不到列名{date_column}】,请检查表头。")
return
# 检查是否存在"属相"列,不存在则新建
if target_column not in df.columns:
df[target_column] = ""
# --- 获取总行数,用于日志 ---
total_count = len(df)
logger.info(f"开始生成学生属相,共 {total_count} 位学生...")
# 3. 数据清洗与计算
temp_dates = pd.to_datetime(df[date_column], errors='coerce')
df[target_column] = temp_dates.apply(calculate_zodiac)
# 5. 保存结果
save_path = excel_path
try:
df.to_excel(save_path, index=False)
logger.success(f"所有属相已更新并写入文件:{save_path}")
logger.warning(f"请检查文件 {save_path} 修改日期格式。")
except PermissionError:
logger.error(f"保存失败!请先关闭 Excel 文件:{save_path}")
except FileNotFoundError:
logger.error(f"找不到文件 {config.get('excel_file')}")
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
import traceback
logger.error(traceback.format_exc())

46
utils/growt_utils.py Normal file
View File

@@ -0,0 +1,46 @@
from rich.console import Console
from loguru import logger
from config.config import load_config
from utils.pptx_utils import replace_text_in_slide, replace_picture
# 如果你之前没有全局定义 console这里定义一个
console = Console()
# ==========================================
# 1. 配置区域 (Configuration)
# ==========================================
config = load_config("config.toml")
def replace_one_page(prs, name, class_name):
"""替换第一页信息"""
replace_text_in_slide(prs, 0, "name", name)
replace_text_in_slide(prs, 0, "class", class_name)
def replace_two_page(prs, comments, teacher_name):
"""替换第二页信息"""
replace_text_in_slide(prs, 1, "comments", comments)
replace_text_in_slide(prs, 1, "teacher_name", teacher_name)
def replace_three_page(prs, info_dict, me_image):
"""替换第三页信息"""
# 使用字典解包传递多个字段,减少参数数量
fields = ["name", "english_name", "sex", "birthday", "zodiac", "friend", "hobby", "game", "food"]
for field in fields:
replace_text_in_slide(prs, 2, field, info_dict.get(field, ""))
if me_image:
replace_picture(prs, 2, "me_image", me_image)
else:
logger.warning(f"⚠️ 警告: {info_dict.get('name', '未知姓名')} 缺少个人照片('me_image'")
def replace_four_page(prs, class_image):
"""替换第四页信息"""
replace_picture(prs, 3, "class_image", class_image)
def replace_five_page(prs, image1, image2):
"""替换第五页信息"""
replace_picture(prs, 4, "image1", image1)
replace_picture(prs, 4, "image2", image2)

42
utils/pdf_utils.py Normal file
View File

@@ -0,0 +1,42 @@
import os
import comtypes.client
def ppt_to_pdf_single(ppt_path, pdf_path=None):
"""
单个 PPT 转 PDF
:param ppt_path: PPT 文件路径
:param pdf_path: PDF 输出路径 (可选,默认同名)
"""
ppt_path = os.path.abspath(ppt_path) # COM 接口必须使用绝对路径
if pdf_path is None:
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
pdf_path = os.path.abspath(pdf_path)
if not os.path.exists(ppt_path):
print(f"文件不存在: {ppt_path}")
return False
powerpoint = None
try:
# 启动 PowerPoint 应用
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
powerpoint.Visible = 1 # 设为可见,否则某些版本会报错
# 打开演示文稿
deck = powerpoint.Presentations.Open(ppt_path)
# 保存为 PDF (文件格式代码 32 代表 PDF)
deck.SaveAs(pdf_path, 32)
deck.Close()
print(f"转换成功: {pdf_path}")
return True
except Exception as e:
print(f"转换失败: {str(e)}")
return False
finally:
if powerpoint:
powerpoint.Quit()

View File

@@ -1,95 +0,0 @@
import os
import comtypes.client
def ppt_to_pdf_single(ppt_path, pdf_path=None):
"""
单个 PPT 转 PDF
:param ppt_path: PPT 文件路径
:param pdf_path: PDF 输出路径 (可选,默认同名)
"""
ppt_path = os.path.abspath(ppt_path) # COM 接口必须使用绝对路径
if pdf_path is None:
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
pdf_path = os.path.abspath(pdf_path)
if not os.path.exists(ppt_path):
print(f"文件不存在: {ppt_path}")
return False
powerpoint = None
try:
# 启动 PowerPoint 应用
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
powerpoint.Visible = 1 # 设为可见,否则某些版本会报错
# 打开演示文稿
deck = powerpoint.Presentations.Open(ppt_path)
# 保存为 PDF (文件格式代码 32 代表 PDF)
deck.SaveAs(pdf_path, 32)
deck.Close()
print(f"转换成功: {pdf_path}")
return True
except Exception as e:
print(f"转换失败: {str(e)}")
return False
finally:
if powerpoint:
powerpoint.Quit()
def batch_convert_folder(folder_path):
"""
【推荐】批量转换文件夹下的所有 PPT (只启动一次 PowerPoint速度快)
"""
folder_path = os.path.abspath(folder_path)
if not os.path.exists(folder_path):
print("文件夹不存在")
return
# 获取所有 ppt/pptx 文件
files = [f for f in os.listdir(folder_path) if f.lower().endswith(('.ppt', '.pptx'))]
if not files:
print("没有找到 PPT 文件")
return
print(f"发现 {len(files)} 个文件,准备开始转换...")
powerpoint = None
try:
# 1. 启动应用 (只启动一次)
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
# 某些环境下需要设为可见,否则无法运行
# powerpoint.Visible = 1
for filename in files:
ppt_path = os.path.join(folder_path, filename)
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
# 如果 PDF 已存在,可以选择跳过
if os.path.exists(pdf_path):
print(f"[跳过] 已存在: {filename}")
continue
print(f"正在转换: {filename} ...")
try:
# 打开 -> 另存为 -> 关闭
deck = powerpoint.Presentations.Open(ppt_path)
deck.SaveAs(pdf_path, 32)
deck.Close()
except Exception as e:
print(f"文件 {filename} 转换出错: {e}")
except Exception as e:
print(f"PowerPoint 进程出错: {e}")
finally:
# 2. 退出应用
if powerpoint:
powerpoint.Quit()
print("PowerPoint 已关闭,批量转换完成。")

30
utils/zodiac_utils.py Normal file
View File

@@ -0,0 +1,30 @@
import pandas as pd
def calculate_zodiac(birth_date):
"""
根据出生日期推断生肖 (基于公历年份简单算法)
逻辑1900年是鼠年以此为基准进行模运算
"""
if pd.isna(birth_date):
return "未知"
# 确保是 datetime 对象,方便提取年份
try:
birth_date = pd.to_datetime(birth_date)
except:
return "格式错误"
year = birth_date.year
# 生肖列表,顺序必须固定:鼠、牛、虎...
zodiacs = ['', '', '', '', '', '', '', '', '', '', '', '']
# 算法核心:(年份 - 1900) % 12 得到索引
# 1900年是鼠年索引为0
index = (year - 1900) % 12
return zodiacs[index]
# --- 主程序 ---
# 1. 读取 Excel 文件
file_path = 'names.xlsx' # 你的文件名