fix:修复一些BUG
This commit is contained in:
2
IFLOW.md
2
IFLOW.md
@@ -118,7 +118,7 @@ Excel文件应包含以下列(顺序必须与配置文件中一致):
|
|||||||
data/images/
|
data/images/
|
||||||
├── 班级名称.jpg # 班级集体照片
|
├── 班级名称.jpg # 班级集体照片
|
||||||
└── 学生姓名/
|
└── 学生姓名/
|
||||||
├── me_image.jpg # 学生个人照片
|
├── me.jpg # 学生个人照片
|
||||||
├── 1.jpg # 活动照片1
|
├── 1.jpg # 活动照片1
|
||||||
├── 2.jpg # 活动照片2
|
├── 2.jpg # 活动照片2
|
||||||
```
|
```
|
||||||
|
|||||||
154
UI.py
154
UI.py
@@ -2,13 +2,14 @@ import tkinter as tk
|
|||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from tkinter import scrolledtext
|
from tkinter import scrolledtext
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
|
from tkinter import filedialog
|
||||||
import threading
|
import threading
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import queue # 【新增】引入队列库
|
import queue
|
||||||
import re # 【新增】用于去除 ANSI 颜色代码
|
import re
|
||||||
|
|
||||||
from loguru import logger # 【新增】需要导入 logger 来配置它
|
from loguru import logger
|
||||||
from config.config import load_config
|
from config.config import load_config
|
||||||
|
|
||||||
# 假设你的功能函数都在这里
|
# 假设你的功能函数都在这里
|
||||||
@@ -19,29 +20,23 @@ from utils.generate_utils import (
|
|||||||
generate_report,
|
generate_report,
|
||||||
generate_zodiac,
|
generate_zodiac,
|
||||||
)
|
)
|
||||||
from utils.file_utils import export_data_folder, initialize_project
|
from utils.file_utils import export_templates_folder, initialize_project, export_data
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 0. 全局配置与队列准备
|
# 0. 全局配置与队列准备
|
||||||
# ==========================================
|
# ==========================================
|
||||||
config = load_config("config.toml")
|
config = load_config("config.toml")
|
||||||
|
|
||||||
# 【新增】创建一个全局队列,用于存放日志消息
|
|
||||||
log_queue = queue.Queue()
|
log_queue = queue.Queue()
|
||||||
|
|
||||||
|
|
||||||
def ansi_cleaner(text):
|
def ansi_cleaner(text):
|
||||||
"""【辅助函数】去除 loguru 输出中的 ANSI 颜色代码 (例如 \x1b[32m)"""
|
"""【辅助函数】去除 loguru 输出中的 ANSI 颜色代码"""
|
||||||
ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
|
ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
|
||||||
return ansi_escape.sub("", text)
|
return ansi_escape.sub("", text)
|
||||||
|
|
||||||
|
|
||||||
def queue_sink(message):
|
def queue_sink(message):
|
||||||
"""【核心】这是一个 loguru 的 sink 回调函数
|
"""【核心】loguru sink 回调"""
|
||||||
当 logger.info() 被调用时,loguru 会把格式化好的消息传给这个函数
|
|
||||||
"""
|
|
||||||
# message 是一个对象,我们将其转换为字符串并放入队列
|
|
||||||
# 使用 ansi_cleaner 去掉颜色代码,否则 GUI 里会有乱码
|
|
||||||
clean_msg = ansi_cleaner(message)
|
clean_msg = ansi_cleaner(message)
|
||||||
log_queue.put(clean_msg)
|
log_queue.put(clean_msg)
|
||||||
|
|
||||||
@@ -52,103 +47,118 @@ def queue_sink(message):
|
|||||||
class ReportApp:
|
class ReportApp:
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("🌱 幼儿园成长报告助手")
|
self.root.title("🌱 尚城幼儿园成长报告助手")
|
||||||
self.root.geometry("700x600") # 稍微调大一点
|
self.root.geometry("720x680") # 高度稍微增加一点以容纳分组
|
||||||
|
|
||||||
# 设置样式
|
# 设置样式
|
||||||
self.style = ttk.Style()
|
self.style = ttk.Style()
|
||||||
self.style.theme_use("clam")
|
self.style.theme_use("clam")
|
||||||
self.style.configure("TButton", font=("微软雅黑", 10), padding=5)
|
self.style.configure("TButton", font=("微软雅黑", 10), padding=5)
|
||||||
self.style.configure(
|
self.style.configure("Title.TLabel", font=("微软雅黑", 16, "bold"), foreground="#2E8B57")
|
||||||
"Title.TLabel", font=("微软雅黑", 16, "bold"), foreground="#2E8B57"
|
|
||||||
)
|
|
||||||
self.style.configure("Sub.TLabel", font=("微软雅黑", 9), foreground="gray")
|
self.style.configure("Sub.TLabel", font=("微软雅黑", 9), foreground="gray")
|
||||||
|
# LabelFrame 的标题样式
|
||||||
|
self.style.configure("TLabelframe.Label", font=("微软雅黑", 10, "bold"), foreground="#0055a3")
|
||||||
|
|
||||||
# --- 1. 标题区域 ---
|
# --- 1. 标题区域 ---
|
||||||
header_frame = ttk.Frame(root, padding="10 20 10 10")
|
header_frame = ttk.Frame(root, padding="10 15 10 5")
|
||||||
header_frame.pack(fill=tk.X)
|
header_frame.pack(fill=tk.X)
|
||||||
ttk.Label(
|
ttk.Label(header_frame, text="🌱 尚城幼儿园成长报告助手", style="Title.TLabel").pack()
|
||||||
header_frame, text="🌱 幼儿园成长报告助手", style="Title.TLabel"
|
|
||||||
).pack()
|
|
||||||
ttk.Label(header_frame, text="By 寒寒", style="Sub.TLabel").pack()
|
ttk.Label(header_frame, text="By 寒寒", style="Sub.TLabel").pack()
|
||||||
|
|
||||||
# --- 2. 按钮功能区域 ---
|
# --- 2. 按钮功能区域 (使用 LabelFrame 分组) ---
|
||||||
btn_frame = ttk.Frame(root, padding=10)
|
|
||||||
btn_frame.pack(fill=tk.X)
|
|
||||||
|
|
||||||
buttons = [
|
# 容器 Frame,给四周留点白
|
||||||
|
main_content = ttk.Frame(root, padding=10)
|
||||||
|
main_content.pack(fill=tk.X)
|
||||||
|
|
||||||
|
# === A组: 核心功能 ===
|
||||||
|
func_btns = [
|
||||||
("1. 📁 生成图片路径", self.run_generate_folders),
|
("1. 📁 生成图片路径", self.run_generate_folders),
|
||||||
("2. 🤖 生成评语 (AI)", self.run_generate_comments),
|
("2. 🤖 生成评语 (AI)", self.run_generate_comments),
|
||||||
("3. 📊 生成报告 (PPT)", self.run_generate_report),
|
("3. 📊 生成报告 (PPT)", self.run_generate_report),
|
||||||
("4. 📑 格式转换 (PDF)", self.run_convert_pdf),
|
("4. 📑 格式转换 (PDF)", self.run_convert_pdf),
|
||||||
("5. 🐂 生肖转化 (生日)", self.run_zodiac),
|
("5. 🐂 生肖转化 (生日)", self.run_zodiac),
|
||||||
("6. 📦 导出数据模板 (Zip)", self.run_export_data_folder),
|
|
||||||
("7. 📤 初始化系统", self.run_initialize_project),
|
|
||||||
("8. 🚪 退出系统", self.quit_app),
|
|
||||||
]
|
]
|
||||||
|
self.create_btn_group(main_content, "🛠️ 核心功能", func_btns, columns=3)
|
||||||
|
|
||||||
for index, (text, func) in enumerate(buttons):
|
# === B组: 数据导出 ===
|
||||||
btn = ttk.Button(btn_frame, text=text, command=func)
|
export_btns = [
|
||||||
r, c = divmod(index, 2)
|
("6. 📦 导出数据模板 (Zip)", self.run_export_data_folder),
|
||||||
btn.grid(row=r, column=c, padx=10, pady=5, sticky="ew")
|
("8. 📤 导出数据备份 (Zip)", self.run_export_data),
|
||||||
|
]
|
||||||
|
self.create_btn_group(main_content, "📦 数据管理", export_btns, columns=2)
|
||||||
|
|
||||||
btn_frame.columnconfigure(0, weight=1)
|
# === C组: 系统设置 ===
|
||||||
btn_frame.columnconfigure(1, weight=1)
|
system_btns = [
|
||||||
|
("7. ⚠️ 初始化系统 (重置)", self.run_initialize_project),
|
||||||
|
("9. 🚪 退出系统", self.quit_app),
|
||||||
|
]
|
||||||
|
self.create_btn_group(main_content, "⚙️ 系统操作", system_btns, columns=2)
|
||||||
|
|
||||||
# --- 3. 日志输出区域 ---
|
# --- 3. 日志输出区域 ---
|
||||||
log_frame = ttk.LabelFrame(root, text="系统实时日志", padding=10)
|
log_frame = ttk.LabelFrame(root, text="📝 系统实时日志", padding=10)
|
||||||
log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
||||||
|
|
||||||
# 注意:这里字体设为 Consolas 或其他等宽字体,看起来更像代码日志
|
|
||||||
self.log_text = scrolledtext.ScrolledText(
|
self.log_text = scrolledtext.ScrolledText(
|
||||||
log_frame, height=10, state="disabled", font=("Consolas", 9)
|
log_frame, height=10, state="disabled", font=("Consolas", 9)
|
||||||
)
|
)
|
||||||
self.log_text.pack(fill=tk.BOTH, expand=True)
|
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# 【新增】启动日志轮询
|
# 启动日志轮询
|
||||||
# 告诉 GUI:每隔 100ms 检查一下队列里有没有新日志
|
|
||||||
self.root.after(100, self.poll_log_queue)
|
self.root.after(100, self.poll_log_queue)
|
||||||
|
|
||||||
logger.info("GUI 初始化完成,等待指令...")
|
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):
|
def poll_log_queue(self):
|
||||||
"""检查队列中是否有新日志,如果有则显示到界面上"""
|
|
||||||
while not log_queue.empty():
|
while not log_queue.empty():
|
||||||
try:
|
try:
|
||||||
# 不阻塞地获取消息
|
|
||||||
msg = log_queue.get_nowait()
|
msg = log_queue.get_nowait()
|
||||||
|
|
||||||
# 写入 GUI
|
|
||||||
self.log_text.config(state="normal")
|
self.log_text.config(state="normal")
|
||||||
self.log_text.insert(tk.END, msg) # msg 已经包含了换行符
|
self.log_text.insert(tk.END, msg)
|
||||||
self.log_text.see(tk.END) # 自动滚动到底部
|
self.log_text.see(tk.END)
|
||||||
self.log_text.config(state="disabled")
|
self.log_text.config(state="disabled")
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
break
|
break
|
||||||
|
|
||||||
# 递归调用:100ms 后再次执行自己
|
|
||||||
self.root.after(100, self.poll_log_queue)
|
self.root.after(100, self.poll_log_queue)
|
||||||
|
|
||||||
# --- 辅助方法:线程包装器 ---
|
# --- 线程包装器 ---
|
||||||
def run_in_thread(self, target_func, *args):
|
def run_in_thread(self, target_func, *args):
|
||||||
"""在单独的线程中运行函数"""
|
|
||||||
|
|
||||||
def thread_task():
|
def thread_task():
|
||||||
try:
|
try:
|
||||||
# 这里不需要手动 self.log 了,因为 target_func 内部的 logger 会自动触发 sink
|
|
||||||
target_func(*args)
|
target_func(*args)
|
||||||
logger.success("✅ 当前任务执行完毕。") # 使用 logger 输出成功
|
logger.success("✅ 当前任务执行完毕。")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ 发生错误: {str(e)}") # 使用 logger 输出错误
|
logger.error(f"❌ 发生错误: {str(e)}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
threading.Thread(target=thread_task, daemon=True).start()
|
threading.Thread(target=thread_task, daemon=True).start()
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 按钮事件 (不需要改动,逻辑都在 thread_task 里处理了)
|
# 按钮事件 (业务逻辑保持不变)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
def run_generate_folders(self):
|
def run_generate_folders(self):
|
||||||
self.run_in_thread(generate_template)
|
self.run_in_thread(generate_template)
|
||||||
@@ -160,18 +170,36 @@ class ReportApp:
|
|||||||
self.run_in_thread(generate_report)
|
self.run_in_thread(generate_report)
|
||||||
|
|
||||||
def run_convert_pdf(self):
|
def run_convert_pdf(self):
|
||||||
# 传入 output_folder
|
|
||||||
self.run_in_thread(batch_convert_folder, config["output_folder"])
|
self.run_in_thread(batch_convert_folder, config["output_folder"])
|
||||||
|
|
||||||
def run_zodiac(self):
|
def run_zodiac(self):
|
||||||
self.run_in_thread(generate_zodiac)
|
self.run_in_thread(generate_zodiac)
|
||||||
|
|
||||||
def run_export_data_folder(self):
|
def run_export_data_folder(self):
|
||||||
self.run_in_thread(export_data_folder)
|
target_folder = filedialog.askdirectory(
|
||||||
|
title="请选择导出数据保存的文件夹",
|
||||||
|
initialdir=config.get("output_folder", ".")
|
||||||
|
)
|
||||||
|
if not target_folder:
|
||||||
|
logger.warning("🚫 导出操作已取消")
|
||||||
|
return
|
||||||
|
logger.info(f"已选择保存路径: {target_folder}")
|
||||||
|
self.run_in_thread(export_templates_folder, target_folder)
|
||||||
|
|
||||||
def run_initialize_project(self):
|
def run_initialize_project(self):
|
||||||
self.run_in_thread(initialize_project)
|
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):
|
def quit_app(self):
|
||||||
if messagebox.askokcancel("退出", "确定要退出系统吗?"):
|
if messagebox.askokcancel("退出", "确定要退出系统吗?"):
|
||||||
self.root.destroy()
|
self.root.destroy()
|
||||||
@@ -182,16 +210,10 @@ class ReportApp:
|
|||||||
# 启动入口
|
# 启动入口
|
||||||
# ==========================================
|
# ==========================================
|
||||||
def applicationUI():
|
def applicationUI():
|
||||||
# 【核心配置】在启动 GUI 前,配置 loguru
|
|
||||||
# 1. remove() 移除默认的控制台输出(如果你想保留控制台黑窗口,可以注释掉这行)
|
|
||||||
# logger.remove()
|
|
||||||
|
|
||||||
# 2. 添加我们的自定义 sink (queue_sink)
|
|
||||||
# format 可以自定义,这里保持简单,或者尽量模拟 loguru 默认格式
|
|
||||||
logger.add(
|
logger.add(
|
||||||
queue_sink,
|
queue_sink,
|
||||||
format="{time:HH:mm:ss} | {level: <8} | {message}",
|
format="{time:HH:mm:ss} | {level: <8} | {message}",
|
||||||
level="INFO", # 只显示 INFO 及以上级别,避免 DEBUG 信息刷屏
|
level="INFO",
|
||||||
)
|
)
|
||||||
|
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ except ImportError:
|
|||||||
print("错误: 缺少 TOML 解析库。请运行: pip install tomli")
|
print("错误: 缺少 TOML 解析库。请运行: pip install tomli")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def get_main_path():
|
def get_main_path():
|
||||||
"""
|
"""
|
||||||
获取程序运行的根目录
|
获取程序运行的根目录
|
||||||
@@ -18,7 +19,7 @@ def get_main_path():
|
|||||||
1. PyInstaller 打包后的 .exe 环境
|
1. PyInstaller 打包后的 .exe 环境
|
||||||
2. 开发环境 (假设此脚本在子文件夹中,如 utils/)
|
2. 开发环境 (假设此脚本在子文件夹中,如 utils/)
|
||||||
"""
|
"""
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, "frozen", False):
|
||||||
# --- 情况 A: 打包后的 exe ---
|
# --- 情况 A: 打包后的 exe ---
|
||||||
# exe 就在根目录下,直接取 exe 所在目录
|
# exe 就在根目录下,直接取 exe 所在目录
|
||||||
return os.path.dirname(sys.executable)
|
return os.path.dirname(sys.executable)
|
||||||
@@ -36,6 +37,7 @@ def get_main_path():
|
|||||||
|
|
||||||
return project_root
|
return project_root
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 1. 配置加载 (Config Loader)
|
# 1. 配置加载 (Config Loader)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -60,8 +62,10 @@ def load_config(config_filename="config.toml"):
|
|||||||
# 将 TOML 的层级结构映射回扁平结构
|
# 将 TOML 的层级结构映射回扁平结构
|
||||||
# 关键点:所有的 os.path.join 都必须基于 main_dir (项目根目录)
|
# 关键点:所有的 os.path.join 都必须基于 main_dir (项目根目录)
|
||||||
config = {
|
config = {
|
||||||
"root_path": main_dir, # 方便调试,把根目录也存进去
|
"root_path": main_dir, # 方便调试,把根目录也存进去
|
||||||
"source_file": os.path.join(main_dir, data["paths"]["source_file"]),
|
"source_file": os.path.join(
|
||||||
|
main_dir, "templates", data["paths"]["source_file"]
|
||||||
|
),
|
||||||
"output_folder": os.path.join(main_dir, data["paths"]["output_folder"]),
|
"output_folder": os.path.join(main_dir, data["paths"]["output_folder"]),
|
||||||
"excel_file": os.path.join(main_dir, data["paths"]["excel_file"]),
|
"excel_file": os.path.join(main_dir, data["paths"]["excel_file"]),
|
||||||
"image_folder": os.path.join(main_dir, data["paths"]["image_folder"]),
|
"image_folder": os.path.join(main_dir, data["paths"]["image_folder"]),
|
||||||
@@ -70,7 +74,7 @@ def load_config(config_filename="config.toml"):
|
|||||||
"teachers": data["class_info"]["teachers"],
|
"teachers": data["class_info"]["teachers"],
|
||||||
"default_comment": data["defaults"].get("default_comment", "暂无评语"),
|
"default_comment": data["defaults"].get("default_comment", "暂无评语"),
|
||||||
"age_group": data["defaults"].get("age_group", "大班上学期"),
|
"age_group": data["defaults"].get("age_group", "大班上学期"),
|
||||||
"ai": data["ai"]
|
"ai": data["ai"],
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
BIN
data/names.xlsx
BIN
data/names.xlsx
Binary file not shown.
14
main.py
14
main.py
@@ -5,8 +5,7 @@ from utils.generate_utils import (
|
|||||||
batch_convert_folder,
|
batch_convert_folder,
|
||||||
generate_zodiac,
|
generate_zodiac,
|
||||||
)
|
)
|
||||||
from utils.file_utils import export_data_folder, initialize_project
|
from utils.file_utils import export_templates_folder, initialize_project, export_data
|
||||||
from UI import applicationUI
|
|
||||||
|
|
||||||
|
|
||||||
def application():
|
def application():
|
||||||
@@ -38,7 +37,8 @@ def application():
|
|||||||
table.add_row("5.", "📑 生肖转化(根据生日)")
|
table.add_row("5.", "📑 生肖转化(根据生日)")
|
||||||
table.add_row("6.", "📦 导出数据模板(Zip)")
|
table.add_row("6.", "📦 导出数据模板(Zip)")
|
||||||
table.add_row("7.", "📦 初始化系统")
|
table.add_row("7.", "📦 初始化系统")
|
||||||
table.add_row("8.", "🚪 退出系统")
|
table.add_row("8.", "📤 导出数据")
|
||||||
|
table.add_row("9.", "🚪 退出系统")
|
||||||
|
|
||||||
# 4. 将表格放入面板,并居中显示
|
# 4. 将表格放入面板,并居中显示
|
||||||
panel = Panel(
|
panel = Panel(
|
||||||
@@ -56,7 +56,7 @@ def application():
|
|||||||
|
|
||||||
choice = Prompt.ask(
|
choice = Prompt.ask(
|
||||||
"👉 请输入序号执行",
|
"👉 请输入序号执行",
|
||||||
choices=["1", "2", "3", "4", "5", "6", "7","8"],
|
choices=["1", "2", "3", "4", "5", "6", "7","8","9"],
|
||||||
default="1",
|
default="1",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,12 +88,16 @@ def application():
|
|||||||
elif choice == "6":
|
elif choice == "6":
|
||||||
console.rule("[bold magenta]正在执行: 导出数据模板[/]")
|
console.rule("[bold magenta]正在执行: 导出数据模板[/]")
|
||||||
# 调用上面的批量转换函数,传入你的 output 文件夹路径
|
# 调用上面的批量转换函数,传入你的 output 文件夹路径
|
||||||
export_data_folder()
|
export_templates_folder()
|
||||||
elif choice == "7":
|
elif choice == "7":
|
||||||
console.rule("[bold magenta]正在执行: 初始化系统[/]")
|
console.rule("[bold magenta]正在执行: 初始化系统[/]")
|
||||||
# 调用上面的批量转换函数,传入你的 output 文件夹路径
|
# 调用上面的批量转换函数,传入你的 output 文件夹路径
|
||||||
initialize_project()
|
initialize_project()
|
||||||
elif choice == "8":
|
elif choice == "8":
|
||||||
|
console.rule("[bold magenta]正在执行: 导出数据[/]")
|
||||||
|
# 调用上面的批量转换函数,传入你的 output 文件夹路径
|
||||||
|
export_data()
|
||||||
|
elif choice == "9":
|
||||||
console.print("[bold red]👋 再见![/]")
|
console.print("[bold red]👋 再见![/]")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
Prompt.ask("按 [bold]Enter[/] 键返回主菜单...")
|
Prompt.ask("按 [bold]Enter[/] 键返回主菜单...")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ dependencies = [
|
|||||||
"pandas>=2.3.3",
|
"pandas>=2.3.3",
|
||||||
"pandas-stubs==2.3.3.251201",
|
"pandas-stubs==2.3.3.251201",
|
||||||
"python-pptx>=1.0.2",
|
"python-pptx>=1.0.2",
|
||||||
|
"pywin32>=311",
|
||||||
"rich>=14.2.0",
|
"rich>=14.2.0",
|
||||||
"tomli>=2.3.0",
|
"tomli>=2.3.0",
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
templates/names K4A.xlsx
Normal file
BIN
templates/names K4A.xlsx
Normal file
Binary file not shown.
Binary file not shown.
@@ -2,14 +2,16 @@ import shutil
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
def export_data_folder(source_folder="data", output_folder="backup"):
|
def export_templates_folder(output_folder="backup"):
|
||||||
"""
|
"""
|
||||||
将指定文件夹压缩为 zip 包
|
将指定文件夹压缩为 zip 包
|
||||||
:param source_folder: 要压缩的文件夹路径 (默认 'data')
|
:param source_folder: 要压缩的文件夹路径 (默认 'data')
|
||||||
:param output_folder: 压缩包存放的文件夹路径 (默认 'backup')
|
:param output_folder: 压缩包存放的文件夹路径 (默认 'backup')
|
||||||
"""
|
"""
|
||||||
|
source_folder = "data"
|
||||||
try:
|
try:
|
||||||
# 1. 检查源文件夹是否存在
|
# 1. 检查源文件夹是否存在
|
||||||
if not os.path.exists(source_folder):
|
if not os.path.exists(source_folder):
|
||||||
@@ -53,6 +55,76 @@ def export_data_folder(source_folder="data", output_folder="backup"):
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
def export_data(save_dir, root_dir="."):
|
||||||
|
"""
|
||||||
|
导出 data 和 output 两个文件夹到同一个 zip 包中
|
||||||
|
:param save_dir: 用户在 GUI 弹窗中选择的保存目录 (例如: D:/Backup)
|
||||||
|
:param root_dir: 项目根目录 (用于找到 data 和 output)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 1. 定义要打包的目标文件夹
|
||||||
|
targets = ["data", "output"]
|
||||||
|
|
||||||
|
# 2. 检查保存目录
|
||||||
|
if not os.path.exists(save_dir):
|
||||||
|
logger.error(f"保存目录不存在: {save_dir}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. 生成压缩包路径
|
||||||
|
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||||
|
zip_filename = f"完整备份_{timestamp}.zip"
|
||||||
|
zip_path = os.path.join(save_dir, zip_filename)
|
||||||
|
|
||||||
|
logger.info(f"开始备份,目标文件: {zip_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 4. 创建压缩包 (使用 'w' 写入模式,ZIP_DEFLATED 表示压缩)
|
||||||
|
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
|
||||||
|
has_files = False # 标记是否真的压缩了文件
|
||||||
|
|
||||||
|
for target in targets:
|
||||||
|
target_abs_path = os.path.join(root_dir, target)
|
||||||
|
|
||||||
|
# 检查 data 或 output 是否存在
|
||||||
|
if not os.path.exists(target_abs_path):
|
||||||
|
logger.warning(f"⚠️ 跳过: 找不到文件夹 '{target}'")
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"正在压缩: {target} ...")
|
||||||
|
|
||||||
|
# 5. 遍历文件夹写入 ZIP
|
||||||
|
# os.walk 会递归遍历子文件夹
|
||||||
|
for root, dirs, files in os.walk(target_abs_path):
|
||||||
|
for file in files:
|
||||||
|
# 获取文件的绝对路径
|
||||||
|
file_abs_path = os.path.join(root, file)
|
||||||
|
|
||||||
|
# 【关键】计算在压缩包里的相对路径
|
||||||
|
# 例如: D:/Project/data/images/1.jpg -> data/images/1.jpg
|
||||||
|
arcname = os.path.relpath(file_abs_path, root_dir)
|
||||||
|
|
||||||
|
# 写入压缩包
|
||||||
|
zf.write(file_abs_path, arcname)
|
||||||
|
has_files = True
|
||||||
|
|
||||||
|
if has_files:
|
||||||
|
logger.success(f"✅ 备份成功! 文件已保存至:\n{zip_path}")
|
||||||
|
return zip_path
|
||||||
|
else:
|
||||||
|
logger.error("❌ 备份失败: data 和 output 文件夹均为空或不存在。")
|
||||||
|
# 如果生成了空文件,建议删除
|
||||||
|
if os.path.exists(zip_path):
|
||||||
|
os.remove(zip_path)
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"导出过程出错: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
def initialize_project(root_dir="."):
|
def initialize_project(root_dir="."):
|
||||||
"""
|
"""
|
||||||
初始化项目:清空 data,重建目录,复制模板
|
初始化项目:清空 data,重建目录,复制模板
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import pythoncom
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -12,7 +13,13 @@ from utils.agent_utils import generate_comment
|
|||||||
from utils.font_utils import install_fonts_from_directory
|
from utils.font_utils import install_fonts_from_directory
|
||||||
from utils.image_utils import find_image_path
|
from utils.image_utils import find_image_path
|
||||||
from utils.zodiac_utils import calculate_zodiac
|
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
|
from utils.growt_utils import (
|
||||||
|
replace_one_page,
|
||||||
|
replace_two_page,
|
||||||
|
replace_three_page,
|
||||||
|
replace_four_page,
|
||||||
|
replace_five_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# 如果你之前没有全局定义 console,这里定义一个
|
# 如果你之前没有全局定义 console,这里定义一个
|
||||||
@@ -23,6 +30,7 @@ console = Console()
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
config = load_config("config.toml")
|
config = load_config("config.toml")
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 1. 生成模板(根据names.xlsx文件生成名字图片文件夹)
|
# 1. 生成模板(根据names.xlsx文件生成名字图片文件夹)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -58,8 +66,10 @@ def generate_template():
|
|||||||
logger.error(f"程序运行出错: {str(e)}")
|
logger.error(f"程序运行出错: {str(e)}")
|
||||||
# 打印详细报错位置,方便调试
|
# 打印详细报错位置,方便调试
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 2. 生成评语(根据names.xlsx文件生成评价)
|
# 2. 生成评语(根据names.xlsx文件生成评价)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -83,20 +93,24 @@ def generate_comment_all():
|
|||||||
# 这样我们可以通过索引 i 精准地把数据写回某一行
|
# 这样我们可以通过索引 i 精准地把数据写回某一行
|
||||||
for i in df.index:
|
for i in df.index:
|
||||||
name = df.at[i, "姓名"] # 获取当前行的姓名
|
name = df.at[i, "姓名"] # 获取当前行的姓名
|
||||||
sex = df.at[i,"性别"]
|
sex = df.at[i, "性别"]
|
||||||
if pd.isna(sex):
|
if pd.isna(sex):
|
||||||
sex = "男"
|
sex = "男"
|
||||||
else:
|
else:
|
||||||
sex = str(sex).strip()
|
sex = str(sex).strip()
|
||||||
|
|
||||||
# 健壮性处理
|
# 健壮性处理
|
||||||
if pd.isna(name): continue # 跳过空行
|
if pd.isna(name):
|
||||||
|
continue # 跳过空行
|
||||||
name = str(name).strip()
|
name = str(name).strip()
|
||||||
|
|
||||||
# 获取当前行的特征(如果Excel里有“特征”这一列就读,没有就用默认值)
|
# 获取当前行的特征(如果Excel里有“特征”这一列就读,没有就用默认值)
|
||||||
# 假设Excel里有一列叫 "表现特征",如果没有则用默认的 "有礼貌..."
|
# 假设Excel里有一列叫 "表现特征",如果没有则用默认的 "有礼貌..."
|
||||||
traits = df.at[i, "表现特征"] if "表现特征" in df.columns and not pd.isna(
|
traits = (
|
||||||
df.at[i, "表现特征"]) else "有礼貌、守纪律"
|
df.at[i, "表现特征"]
|
||||||
|
if "表现特征" in df.columns and not pd.isna(df.at[i, "表现特征"])
|
||||||
|
else "有礼貌、守纪律"
|
||||||
|
)
|
||||||
|
|
||||||
# 优化:如果“评价”列已经有内容了,跳过不生成(节省API费用)
|
# 优化:如果“评价”列已经有内容了,跳过不生成(节省API费用)
|
||||||
current_comment = df.at[i, "评价"]
|
current_comment = df.at[i, "评价"]
|
||||||
@@ -109,7 +123,9 @@ def generate_comment_all():
|
|||||||
try:
|
try:
|
||||||
# 调用你的生成函数,并【接收返回值】
|
# 调用你的生成函数,并【接收返回值】
|
||||||
# 注意:这里假设 generate_comment 返回的是清洗后的字符串
|
# 注意:这里假设 generate_comment 返回的是清洗后的字符串
|
||||||
generated_text = generate_comment(name, config["age_group"], traits,sex)
|
generated_text = generate_comment(
|
||||||
|
name, config["age_group"], traits, sex
|
||||||
|
)
|
||||||
|
|
||||||
# --- 将结果写入 DataFrame ---
|
# --- 将结果写入 DataFrame ---
|
||||||
df.at[i, "评价"] = generated_text
|
df.at[i, "评价"] = generated_text
|
||||||
@@ -136,8 +152,10 @@ def generate_comment_all():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"程序运行出错: {str(e)}")
|
logger.error(f"程序运行出错: {str(e)}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 3. 生成成长报告(根据names.xlsx文件生成)
|
# 3. 生成成长报告(根据names.xlsx文件生成)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -148,10 +166,11 @@ def generate_report():
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
os.makedirs(config["output_folder"], exist_ok=True)
|
os.makedirs(config["output_folder"], exist_ok=True)
|
||||||
|
# 检查模版文件是否存在
|
||||||
if not os.path.exists(config["source_file"]):
|
if not os.path.exists(config["source_file"]):
|
||||||
logger.info(f"错误: 找不到模版文件 {config['source_file']}")
|
logger.info(f"错误: 找不到模版文件 {config["source_file"]}")
|
||||||
return
|
return
|
||||||
|
# 检查数据文件是否存在
|
||||||
if not os.path.exists(config["excel_file"]):
|
if not os.path.exists(config["excel_file"]):
|
||||||
logger.info(f"错误: 找不到数据文件 {config['excel_file']}")
|
logger.info(f"错误: 找不到数据文件 {config['excel_file']}")
|
||||||
return
|
return
|
||||||
@@ -160,8 +179,18 @@ def generate_report():
|
|||||||
# 2. 读取数据
|
# 2. 读取数据
|
||||||
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
|
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
|
||||||
# 确保列名对应
|
# 确保列名对应
|
||||||
columns = ["姓名", "英文名", "性别", "生日", "属相", "我的好朋友", "我的爱好", "喜欢的游戏", "喜欢吃的食物",
|
columns = [
|
||||||
"评价"]
|
"姓名",
|
||||||
|
"英文名",
|
||||||
|
"性别",
|
||||||
|
"生日",
|
||||||
|
"属相",
|
||||||
|
"我的好朋友",
|
||||||
|
"我的爱好",
|
||||||
|
"喜欢的游戏",
|
||||||
|
"喜欢吃的食物",
|
||||||
|
"评价",
|
||||||
|
]
|
||||||
datas = df[columns].values.tolist()
|
datas = df[columns].values.tolist()
|
||||||
|
|
||||||
teacher_names_str = " ".join(config["teachers"])
|
teacher_names_str = " ".join(config["teachers"])
|
||||||
@@ -171,13 +200,23 @@ def generate_report():
|
|||||||
# 3. 循环处理
|
# 3. 循环处理
|
||||||
for i, row_data in enumerate(datas):
|
for i, row_data in enumerate(datas):
|
||||||
# 解包数据
|
# 解包数据
|
||||||
(name, english_name, sex, birthday, zodiac, friend, hobby, game, food, comments) = row_data
|
(
|
||||||
|
name,
|
||||||
|
english_name,
|
||||||
|
sex,
|
||||||
|
birthday,
|
||||||
|
zodiac,
|
||||||
|
friend,
|
||||||
|
hobby,
|
||||||
|
game,
|
||||||
|
food,
|
||||||
|
comments,
|
||||||
|
) = row_data
|
||||||
|
|
||||||
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
|
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
|
||||||
|
|
||||||
# 每次循环重新加载模版
|
# 每次循环重新加载模版
|
||||||
template_source_file = os.path.join("templates", config["source_file"])
|
prs = Presentation(config["source_file"])
|
||||||
prs = Presentation(template_source_file)
|
|
||||||
|
|
||||||
# --- 页面 1 ---
|
# --- 页面 1 ---
|
||||||
replace_one_page(prs, name, config["class_name"])
|
replace_one_page(prs, name, config["class_name"])
|
||||||
@@ -187,32 +226,47 @@ def generate_report():
|
|||||||
|
|
||||||
# --- 页面 3 ---
|
# --- 页面 3 ---
|
||||||
student_image_folder = os.path.join(config["image_folder"], name)
|
student_image_folder = os.path.join(config["image_folder"], name)
|
||||||
logger.info(f"学生图片文件夹: {student_image_folder}")
|
logger.info(f"学生:{name},图片文件夹: {student_image_folder}")
|
||||||
if os.path.exists(student_image_folder):
|
if os.path.exists(student_image_folder):
|
||||||
me_image_path = find_image_path(student_image_folder, "me_image")
|
me_image_path = find_image_path(student_image_folder, "me")
|
||||||
|
|
||||||
# 构造信息字典供 helper 使用
|
# 构造信息字典供 helper 使用
|
||||||
info_dict = {
|
info_dict = {
|
||||||
"name": name, "english_name": english_name, "sex": sex,
|
"name": name,
|
||||||
"birthday": birthday, "zodiac": zodiac, "friend": friend,
|
"english_name": english_name,
|
||||||
"hobby": hobby, "game": game, "food": food
|
"sex": sex,
|
||||||
|
"birthday": birthday.strftime("%Y-%m-%d") if pd.notna(birthday) else "",
|
||||||
|
"zodiac": zodiac,
|
||||||
|
"friend": friend,
|
||||||
|
"hobby": hobby,
|
||||||
|
"game": game,
|
||||||
|
"food": food,
|
||||||
}
|
}
|
||||||
# 逻辑:必须同时满足 "不是None" 且 "是字符串" 且 "文件存在" 才能执行
|
# 逻辑:必须同时满足 "不是None" 且 "是字符串" 且 "文件存在" 才能执行
|
||||||
if me_image_path and isinstance(me_image_path, str) and os.path.exists(me_image_path):
|
if (
|
||||||
logger.info(f"学生:{name},图片路径有效: {me_image_path},正在替换...")
|
me_image_path
|
||||||
|
and isinstance(me_image_path, str)
|
||||||
|
and os.path.exists(me_image_path)
|
||||||
|
):
|
||||||
replace_three_page(prs, info_dict, me_image_path)
|
replace_three_page(prs, info_dict, me_image_path)
|
||||||
else:
|
else:
|
||||||
# 只有在这里打印日志,告诉用户跳过了,但不中断程序
|
# 只有在这里打印日志,告诉用户跳过了,但不中断程序
|
||||||
replace_three_page(prs, info_dict, None)
|
replace_three_page(prs, info_dict, None)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"错误: 学生图片文件夹不存在 {student_image_folder}")
|
logger.warning(f"⚠️ 警告: 学生:{name},学生图片文件夹不存在 {student_image_folder}")
|
||||||
|
|
||||||
# --- 页面 4 ---
|
# --- 页面 4 ---
|
||||||
class_image_path = find_image_path(config["image_folder"], config["class_name"])
|
class_image_path = find_image_path(
|
||||||
if os.path.exists(class_image_path):
|
config["image_folder"], config["class_name"]
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
class_image_path
|
||||||
|
and isinstance(class_image_path, str)
|
||||||
|
and os.path.exists(class_image_path)
|
||||||
|
):
|
||||||
replace_four_page(prs, class_image_path)
|
replace_four_page(prs, class_image_path)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"错误: 班级图片文件不存在 {class_image_path}")
|
logger.warning(f"⚠️ 警告: 班级图片文件不存在 {class_image_path}")
|
||||||
|
|
||||||
# --- 页面 5 ---
|
# --- 页面 5 ---
|
||||||
if os.path.exists(student_image_folder):
|
if os.path.exists(student_image_folder):
|
||||||
@@ -228,7 +282,9 @@ def generate_report():
|
|||||||
replace_five_page(prs, img1_path, img1_path)
|
replace_five_page(prs, img1_path, img1_path)
|
||||||
# 情况C: 一张都没找到
|
# 情况C: 一张都没找到
|
||||||
else:
|
else:
|
||||||
logger.warning(f"⚠️ 警告: {name} 缺少作品照片 (1.jpg/png 或 2.jpg/png)[/]")
|
logger.warning(
|
||||||
|
f"⚠️ 警告: {name} 缺少作品照片 (1.jpg/png 或 2.jpg/png)[/]"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"错误: 学生图片文件夹不存在 {student_image_folder}")
|
logger.warning(f"错误: 学生图片文件夹不存在 {student_image_folder}")
|
||||||
|
|
||||||
@@ -242,7 +298,9 @@ def generate_report():
|
|||||||
prs.save(output_path)
|
prs.save(output_path)
|
||||||
logger.success(f"学生:{name},保存成功: {new_filename}")
|
logger.success(f"学生:{name},保存成功: {new_filename}")
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
logger.error(f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。")
|
logger.error(
|
||||||
|
f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。"
|
||||||
|
)
|
||||||
|
|
||||||
logger.success("所有报告生成完毕!")
|
logger.success("所有报告生成完毕!")
|
||||||
|
|
||||||
@@ -250,63 +308,82 @@ def generate_report():
|
|||||||
logger.error(f"程序运行出错: {str(e)}")
|
logger.error(f"程序运行出错: {str(e)}")
|
||||||
# 打印详细报错位置,方便调试
|
# 打印详细报错位置,方便调试
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 5. 转换格式(根据names.xlsx文件生成PPT转PDF)
|
# 5. 转换格式(根据names.xlsx文件生成PPT转PDF)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
def batch_convert_folder(folder_path):
|
def batch_convert_folder(folder_path):
|
||||||
"""
|
"""
|
||||||
【推荐】批量转换文件夹下的所有 PPT (只启动一次 PowerPoint,速度快)
|
【推荐】批量转换文件夹下的所有 PPT (只启动一次 PowerPoint,速度快)
|
||||||
|
已修复多线程 CoInitialize 报错,并适配 GUI 日志
|
||||||
"""
|
"""
|
||||||
folder_path = os.path.abspath(folder_path)
|
# 【核心修复 1】子线程初始化 COM 组件
|
||||||
if not os.path.exists(folder_path):
|
pythoncom.CoInitialize()
|
||||||
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:
|
try:
|
||||||
# 1. 启动应用 (只启动一次)
|
folder_path = os.path.abspath(folder_path)
|
||||||
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
|
if not os.path.exists(folder_path):
|
||||||
# 某些环境下需要设为可见,否则无法运行
|
logger.error(f"文件夹不存在: {folder_path}")
|
||||||
# powerpoint.Visible = 1
|
return
|
||||||
|
|
||||||
for filename in files:
|
# 获取所有 ppt/pptx 文件
|
||||||
ppt_path = os.path.join(folder_path, filename)
|
files = [
|
||||||
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
|
f for f in os.listdir(folder_path) if f.lower().endswith((".ppt", ".pptx"))
|
||||||
|
]
|
||||||
|
|
||||||
# 如果 PDF 已存在,可以选择跳过
|
if not files:
|
||||||
if os.path.exists(pdf_path):
|
logger.warning("没有找到 PPT 文件")
|
||||||
print(f"[跳过] 已存在: {filename}")
|
return
|
||||||
continue
|
|
||||||
|
|
||||||
print(f"正在转换: {filename} ...")
|
logger.info(f"发现 {len(files)} 个文件,准备开始转换...")
|
||||||
|
|
||||||
try:
|
powerpoint = None
|
||||||
# 打开 -> 另存为 -> 关闭
|
try:
|
||||||
deck = powerpoint.Presentations.Open(ppt_path)
|
# 1. 启动应用 (只启动一次)
|
||||||
deck.SaveAs(pdf_path, 32)
|
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
|
||||||
deck.Close()
|
|
||||||
except Exception as e:
|
# 【建议】在后台线程运行时,有时设置为不可见更稳定,
|
||||||
print(f"文件 {filename} 转换出错: {e}")
|
# 但如果遇到转换卡死,可以尝试去掉下面这行的注释,让它显示出来
|
||||||
|
# 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):
|
||||||
|
logger.info(f"[跳过] 已存在: {filename}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"正在转换: {filename} ...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 打开 -> 另存为 -> 关闭
|
||||||
|
deck = powerpoint.Presentations.Open(ppt_path)
|
||||||
|
deck.SaveAs(pdf_path, 32) # 32 代表 PDF 格式
|
||||||
|
deck.Close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"文件 {filename} 转换出错: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"PowerPoint 进程启动出错: {e}")
|
||||||
|
finally:
|
||||||
|
# 2. 退出应用
|
||||||
|
if powerpoint:
|
||||||
|
try:
|
||||||
|
powerpoint.Quit()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
logger.success("PowerPoint 已关闭,批量转换完成。")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"PowerPoint 进程出错: {e}")
|
logger.error(f"未知错误: {e}")
|
||||||
finally:
|
finally:
|
||||||
# 2. 退出应用
|
# 【核心修复 2】释放资源
|
||||||
if powerpoint:
|
pythoncom.CoUninitialize()
|
||||||
powerpoint.Quit()
|
|
||||||
print("PowerPoint 已关闭,批量转换完成。")
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 5. 生成属相(根据names.xlsx文件生成属相)
|
# 5. 生成属相(根据names.xlsx文件生成属相)
|
||||||
@@ -319,8 +396,8 @@ def generate_zodiac():
|
|||||||
df = pd.read_excel(excel_path, sheet_name="Sheet1")
|
df = pd.read_excel(excel_path, sheet_name="Sheet1")
|
||||||
|
|
||||||
# 2. 检查必要的列
|
# 2. 检查必要的列
|
||||||
date_column = '生日'
|
date_column = "生日"
|
||||||
target_column = '属相'
|
target_column = "属相"
|
||||||
|
|
||||||
if date_column not in df.columns:
|
if date_column not in df.columns:
|
||||||
logger.error(f"Excel中找不到列名:【{date_column}】,请检查表头。")
|
logger.error(f"Excel中找不到列名:【{date_column}】,请检查表头。")
|
||||||
@@ -335,7 +412,7 @@ def generate_zodiac():
|
|||||||
logger.info(f"开始生成学生属相,共 {total_count} 位学生...")
|
logger.info(f"开始生成学生属相,共 {total_count} 位学生...")
|
||||||
|
|
||||||
# 3. 数据清洗与计算
|
# 3. 数据清洗与计算
|
||||||
temp_dates = pd.to_datetime(df[date_column], errors='coerce')
|
temp_dates = pd.to_datetime(df[date_column], errors="coerce")
|
||||||
df[target_column] = temp_dates.apply(calculate_zodiac)
|
df[target_column] = temp_dates.apply(calculate_zodiac)
|
||||||
|
|
||||||
# 5. 保存结果
|
# 5. 保存结果
|
||||||
@@ -353,6 +430,5 @@ def generate_zodiac():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"程序运行出错: {str(e)}")
|
logger.error(f"程序运行出错: {str(e)}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
17
uv.lock
generated
17
uv.lock
generated
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 2
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -122,6 +122,7 @@ dependencies = [
|
|||||||
{ name = "pandas" },
|
{ name = "pandas" },
|
||||||
{ name = "pandas-stubs" },
|
{ name = "pandas-stubs" },
|
||||||
{ name = "python-pptx" },
|
{ name = "python-pptx" },
|
||||||
|
{ name = "pywin32" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
{ name = "tomli" },
|
{ name = "tomli" },
|
||||||
]
|
]
|
||||||
@@ -136,6 +137,7 @@ requires-dist = [
|
|||||||
{ name = "pandas", specifier = ">=2.3.3" },
|
{ name = "pandas", specifier = ">=2.3.3" },
|
||||||
{ name = "pandas-stubs", specifier = "==2.3.3.251201" },
|
{ name = "pandas-stubs", specifier = "==2.3.3.251201" },
|
||||||
{ name = "python-pptx", specifier = ">=1.0.2" },
|
{ name = "python-pptx", specifier = ">=1.0.2" },
|
||||||
|
{ name = "pywin32", specifier = ">=311" },
|
||||||
{ name = "rich", specifier = ">=14.2.0" },
|
{ name = "rich", specifier = ">=14.2.0" },
|
||||||
{ name = "tomli", specifier = ">=2.3.0" },
|
{ name = "tomli", specifier = ">=2.3.0" },
|
||||||
]
|
]
|
||||||
@@ -859,6 +861,19 @@ wheels = [
|
|||||||
{ url = "https://mirrors.aliyun.com/pypi/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00" },
|
{ url = "https://mirrors.aliyun.com/pypi/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pywin32"
|
||||||
|
version = "311"
|
||||||
|
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.aliyun.com/pypi/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d" },
|
||||||
|
{ url = "https://mirrors.aliyun.com/pypi/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d" },
|
||||||
|
{ url = "https://mirrors.aliyun.com/pypi/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a" },
|
||||||
|
{ url = "https://mirrors.aliyun.com/pypi/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee" },
|
||||||
|
{ url = "https://mirrors.aliyun.com/pypi/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87" },
|
||||||
|
{ url = "https://mirrors.aliyun.com/pypi/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.3"
|
version = "6.0.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user