fix:添加UI界面,完善功能
This commit is contained in:
199
UI.py
Normal file
199
UI.py
Normal 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()
|
||||
@@ -1,37 +1,71 @@
|
||||
# 尝试导入 toml 解析库 (兼容 Python 3.11+ 和旧版本)
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 尝试导入 toml 解析库
|
||||
try:
|
||||
import tomllib as toml # Python 3.11+ 内置
|
||||
import tomllib as toml # Python 3.11+
|
||||
except ImportError:
|
||||
try:
|
||||
import tomli as toml # 需要 pip install tomli
|
||||
import tomli as toml # pip install tomli
|
||||
except ImportError:
|
||||
print("错误: 缺少 TOML 解析库。请运行: pip install tomli")
|
||||
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)
|
||||
# ==========================================
|
||||
def load_config(config_path="config.toml"):
|
||||
"""读取 TOML 配置文件并转换为扁平字典"""
|
||||
def load_config(config_filename="config.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):
|
||||
print(f"错误: 找不到配置文件 {config_path}")
|
||||
print(f"错误: 在路径 {main_dir} 下找不到配置文件 {config_filename}")
|
||||
print(f"尝试寻找的完整路径是: {config_path}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(config_path, "rb") as f:
|
||||
data = toml.load(f)
|
||||
|
||||
# 将 TOML 的层级结构映射回原本的扁平结构,保持代码其余部分不用大改
|
||||
|
||||
# 将 TOML 的层级结构映射回扁平结构
|
||||
# 关键点:所有的 os.path.join 都必须基于 main_dir (项目根目录)
|
||||
config = {
|
||||
"source_file": data["paths"]["source_file"],
|
||||
"output_folder": data["paths"]["output_folder"],
|
||||
"excel_file": data["paths"]["excel_file"],
|
||||
"image_folder": data["paths"]["image_folder"],
|
||||
"fonts_dir": data["paths"]["fonts_dir"],
|
||||
"root_path": main_dir, # 方便调试,把根目录也存进去
|
||||
"source_file": os.path.join(main_dir, data["paths"]["source_file"]),
|
||||
"output_folder": os.path.join(main_dir, data["paths"]["output_folder"]),
|
||||
"excel_file": os.path.join(main_dir, data["paths"]["excel_file"]),
|
||||
"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"],
|
||||
"teachers": data["class_info"]["teachers"],
|
||||
"default_comment": data["defaults"].get("default_comment", "暂无评语"),
|
||||
@@ -41,4 +75,4 @@ def load_config(config_path="config.toml"):
|
||||
return config
|
||||
except Exception as 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 |
BIN
data/names.xlsx
BIN
data/names.xlsx
Binary file not shown.
340
main.py
340
main.py
@@ -1,285 +1,12 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
from pptx import Presentation
|
||||
from pptx.util import Pt
|
||||
from rich import box
|
||||
from rich.align import Align
|
||||
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())
|
||||
from utils.generate_utils import (
|
||||
generate_template,
|
||||
generate_comment_all,
|
||||
generate_report,
|
||||
batch_convert_folder,
|
||||
generate_zodiac,
|
||||
)
|
||||
from utils.file_utils import export_data_folder, initialize_project
|
||||
from UI import applicationUI
|
||||
|
||||
|
||||
def application():
|
||||
@@ -304,11 +31,14 @@ def application():
|
||||
table.add_column(justify="left")
|
||||
|
||||
# 3. 添加行内容
|
||||
table.add_row("1.", "📁 生成模板")
|
||||
table.add_row("2.", "🤖 生成评语")
|
||||
table.add_row("3.", "📊 生成报告")
|
||||
table.add_row("4.", "📑 格式转换") # 新增
|
||||
table.add_row("5.", "🚪 退出系统")
|
||||
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.", "🚪 退出系统")
|
||||
|
||||
# 4. 将表格放入面板,并居中显示
|
||||
panel = Panel(
|
||||
@@ -317,19 +47,25 @@ def application():
|
||||
subtitle="[dim]By 寒寒",
|
||||
width=60,
|
||||
border_style="bright_blue",
|
||||
box=box.ROUNDED # 圆角边框更柔和
|
||||
box=box.ROUNDED, # 圆角边框更柔和
|
||||
)
|
||||
|
||||
# 使用 Align.center 让整个菜单在屏幕中间显示
|
||||
console.print(Align.center(panel, vertical="middle"))
|
||||
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:
|
||||
if choice == "1":
|
||||
console.rule("[bold cyan]正在执行: 生成模板[/]")
|
||||
with console.status("[bold green]正在创建文件夹结构...[/]", spinner="dots"):
|
||||
with console.status(
|
||||
"[bold green]正在创建文件夹结构...[/]", spinner="dots"
|
||||
):
|
||||
generate_template()
|
||||
elif choice == "2":
|
||||
console.rule("[bold yellow]正在执行: AI 生成评语[/]")
|
||||
@@ -337,20 +73,38 @@ def application():
|
||||
generate_comment_all()
|
||||
elif choice == "3":
|
||||
console.rule("[bold blue]正在执行: PPT 合成[/]")
|
||||
with console.status("[bold blue]正在处理图片和文字...[/]", spinner="earth"):
|
||||
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_data_folder()
|
||||
elif choice == "7":
|
||||
console.rule("[bold magenta]正在执行: 初始化系统[/]")
|
||||
# 调用上面的批量转换函数,传入你的 output 文件夹路径
|
||||
initialize_project()
|
||||
elif choice == "8":
|
||||
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"))
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold red]❌ 发生错误:[/]\n{e}", title="Error", border_style="red"
|
||||
)
|
||||
)
|
||||
Prompt.ask("按 Enter 键继续...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
application()
|
||||
applicationUI()
|
||||
|
||||
42
start_app.bat
Normal file
42
start_app.bat
Normal 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
BIN
templates/names.xlsx
Normal file
Binary file not shown.
@@ -10,12 +10,13 @@ from config.config import load_config
|
||||
config = load_config("config.toml")
|
||||
|
||||
|
||||
def generate_comment(name, age_group, traits):
|
||||
def generate_comment(name, age_group, traits,sex):
|
||||
"""
|
||||
生成评语
|
||||
:param name: 学生姓名
|
||||
:param age_group: 所在班级
|
||||
:param traits: 表现特征
|
||||
:param sex: 性别
|
||||
:return: 评语
|
||||
"""
|
||||
|
||||
@@ -29,7 +30,7 @@ def generate_comment(name, age_group, traits):
|
||||
# 2. 构建 Prompt Template
|
||||
prompt = ChatPromptTemplate.from_messages([
|
||||
("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)
|
||||
@@ -40,11 +41,12 @@ def generate_comment(name, age_group, traits):
|
||||
comment = chain.invoke({
|
||||
"name": name,
|
||||
"age_group": age_group,
|
||||
"traits": traits
|
||||
"traits": traits,
|
||||
"sex": sex
|
||||
})
|
||||
cleaned_text = re.sub(r'\s+', '', comment)
|
||||
logger.success(f"学生:{name} =>生成评语成功: {cleaned_text}")
|
||||
return cleaned_text
|
||||
except Exception as e:
|
||||
print(f"生成评语失败: {e}")
|
||||
logger.error(f"生成评语失败: {e}")
|
||||
return "生成失败,请检查网络或Key。"
|
||||
|
||||
121
utils/file_utils.py
Normal file
121
utils/file_utils.py
Normal 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
358
utils/generate_utils.py
Normal 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
46
utils/growt_utils.py
Normal 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
42
utils/pdf_utils.py
Normal 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()
|
||||
@@ -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
30
utils/zodiac_utils.py
Normal 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' # 你的文件名
|
||||
Reference in New Issue
Block a user