diff --git a/.gitignore b/.gitignore
index 295daa5..4f37f5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,4 @@ data/images/*
data/*.xlsx
.idea/
-.trea/
\ No newline at end of file
+.trae/
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index feb4403..1aa8f25 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,7 +4,9 @@
-
+
+
+
@@ -95,6 +97,9 @@
+
+
+
diff --git a/config.toml b/config.toml
index 2a136a8..b39fa29 100644
--- a/config.toml
+++ b/config.toml
@@ -4,7 +4,7 @@ output_folder = "output"
excel_file = "names.xlsx"
image_folder = "images"
fonts_dir = "fonts"
-signature_image = "C:\\Users\\Administrator\\Desktop\\文档资料\\code\\growth_report\\data\\"
+signature_image = "d:\\working\\tools\\growth_report\\data\\signature.png"
[class_info]
class_name = "K4D"
diff --git a/config/config.py b/config/config.py
index 9058cd9..b8af7ed 100644
--- a/config/config.py
+++ b/config/config.py
@@ -78,7 +78,7 @@ def load_config(config_filename="config.toml"):
base_dir, paths.get("output_folder", "output")
),
"signature_image": get_resource_path(
- os.path.join("data", paths.get("signature_image", ""))
+ os.path.join("data", paths.get("signature_image", "signature.png"))
),
"class_name": class_info.get("class_name", "未命名班级"),
"teachers": class_info.get("teachers", []),
@@ -118,7 +118,9 @@ def save_config(config_data, config_filename="config.toml"):
"image_folder": os.path.basename(config_data.get("image_folder", "")),
"fonts_dir": os.path.basename(config_data.get("fonts_dir", "fonts")),
"signature_image": get_resource_path(
- os.path.join("data", config_data.get("signature_image", ""))
+ os.path.join(
+ "data", config_data.get("signature_image", "signature.png")
+ )
),
},
"class_info": {
diff --git a/ui/views/config_page.py b/ui/views/config_page.py
index ec5905b..b73538f 100644
--- a/ui/views/config_page.py
+++ b/ui/views/config_page.py
@@ -168,6 +168,7 @@ def create_config_page():
"output_folder": output_folder.value,
"class_name": class_name.value,
"age_group": age_group.value,
+ "signature_image": conf_data.get("signature_image", ""),
"teachers": [
t.strip() for t in teachers_text.value.split("\n") if t.strip()
],
diff --git a/ui/views/home_page.py b/ui/views/home_page.py
index eb12a01..ef2f7a1 100644
--- a/ui/views/home_page.py
+++ b/ui/views/home_page.py
@@ -7,7 +7,7 @@ from ui.core.task_runner import run_task
from utils.generate_utils import (
generate_template,
generate_comment_all,
- batch_convert_folder,
+ generate_convert_pdf,
generate_report,
generate_zodiac,
generate_signature,
@@ -66,14 +66,10 @@ def create_home_page():
f"outline"
).classes("w-full")
- # 特殊处理带参数的
- async def run_convert():
- await run_task(batch_convert_folder, config.get("output_folder"))
-
func_btn("📁 生成图片路径", generate_template)
func_btn("🤖 生成评语 (AI)", generate_comment_all)
func_btn("📊 生成报告 (PPT)", generate_report)
- func_btn("📑 格式转换 (PDF)", run_convert)
+ func_btn("📑 格式转换 (PDF)", generate_convert_pdf)
func_btn("🐂 生肖转化 (生日)", generate_zodiac)
func_btn("💴 园长一键签名", generate_signature)
diff --git a/utils/generate_utils.py b/utils/generate_utils.py
index 5c3bff0..26f6fe5 100644
--- a/utils/generate_utils.py
+++ b/utils/generate_utils.py
@@ -22,13 +22,13 @@ from utils.growt_utils import (
replace_four_page,
replace_five_page,
)
-from utils.pptx_utils import replace_picture
+
# ==========================================
# 1. 生成模板(根据names.xlsx文件生成名字图片文件夹)
# ==========================================
def generate_template(stop_event: threading.Event = None, progress_callback=None):
- """"
+ """ "
根据学生姓名生成相对应的以学生姓名的存放照片的文件夹
:params stop_event 任务是否停止事件(监听UI的事件监听)
:params progress_callback 进度回调函数
@@ -36,7 +36,7 @@ def generate_template(stop_event: threading.Event = None, progress_callback=None
# 1. 加载配置文件
try:
config = load_config("config.toml")
- except Exception as e:
+ except Exception as e:
logger.error(f"配置文件获取失败: {str(e)}")
# 打印详细报错位置,方便调试
logger.error(traceback.format_exc())
@@ -87,10 +87,10 @@ def generate_comment_all(stop_event: threading.Event = None, progress_callback=N
:params stop_event 任务是否停止事件(监听UI的事件监听)
:params progress_callback 进度回调函数
"""
- # 1. 加载配置文件
+ # 1. 加载配置文件
try:
config = load_config("config.toml")
- except Exception as e:
+ except Exception as e:
logger.error(f"配置文件获取失败: {str(e)}")
# 打印详细报错位置,方便调试
logger.error(traceback.format_exc())
@@ -139,7 +139,9 @@ def generate_comment_all(stop_event: threading.Event = None, progress_callback=N
continue
# 添加进度条
if progress_callback:
- progress_callback(i + 1, total_count, f"[{i + 1}/{total_count}] 正在生成评价: {name}")
+ progress_callback(
+ i + 1, total_count, f"[{i + 1}/{total_count}] 正在生成评价: {name}"
+ )
logger.info(f"[{i + 1}/{total_count}] 正在生成评价: {name}")
try:
@@ -180,10 +182,10 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
根据学生姓名生成成长报告
:params stop_event 任务是否停止事件(监听UI的事件监听)
:params progress_callback 进度回调函数
- """ # 1. 加载配置文件
+ """ # 1. 加载配置文件
try:
config = load_config("config.toml")
- except Exception as e:
+ except Exception as e:
logger.error(f"配置文件获取失败: {str(e)}")
# 打印详细报错位置,方便调试
logger.error(traceback.format_exc())
@@ -237,7 +239,11 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
) = row_data
# 更新进度条
if progress_callback:
- progress_callback(i + 1, total_count, f"[{i + 1}/{len(datas)}] 正在生成: 【{name}】 成长报告")
+ progress_callback(
+ i + 1,
+ total_count,
+ f"[{i + 1}/{len(datas)}] 正在生成: 【{name}】 成长报告",
+ )
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
# 每次循环重新加载模版
@@ -271,11 +277,31 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
"birthday": (
birthday.strftime("%Y-%m-%d") if pd.notna(birthday) else " "
),
- "zodiac": str(zodiac).strip() if str(zodiac).strip() or not str(zodiac).strip().lower() else " ",
- "friend": str(friend).strip() if str(friend).strip() or not str(friend).strip().lower() else " ",
- "hobby": str(hobby).strip() if str(hobby).strip() or not str(hobby).strip().lower() else " ",
- "game": str(game).strip() if str(game).strip() or not str(game).strip().lower() else " ",
- "food": str(food).strip() if str(food).strip() or not str(food).strip().lower() else " ",
+ "zodiac": (
+ str(zodiac).strip()
+ if str(zodiac).strip() or not str(zodiac).strip().lower()
+ else " "
+ ),
+ "friend": (
+ str(friend).strip()
+ if str(friend).strip() or not str(friend).strip().lower()
+ else " "
+ ),
+ "hobby": (
+ str(hobby).strip()
+ if str(hobby).strip() or not str(hobby).strip().lower()
+ else " "
+ ),
+ "game": (
+ str(game).strip()
+ if str(game).strip() or not str(game).strip().lower()
+ else " "
+ ),
+ "food": (
+ str(food).strip()
+ if str(food).strip() or not str(food).strip().lower()
+ else " "
+ ),
}
# 获取学生个人照片路径
me_image_path = find_image_path(student_image_folder, "me")
@@ -343,24 +369,24 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
# ==========================================
# 4. 转换格式(根据names.xlsx文件生成PPT转PDF)
# ==========================================
-def batch_convert_folder(folder_path, stop_event: threading.Event = None, progress_callback=None):
+def generate_convert_pdf(stop_event: threading.Event = None, progress_callback=None):
"""
批量转换文件夹下的所有 PPT
:params folder_path 需要转换的PPT文件夹
:params stop_event 任务是否停止事件(监听UI的事件监听)
:params progress_callback 进度回调函数
"""
- # 1. 加载配置文件
+ # 1. 加载配置文件
try:
config = load_config("config.toml")
- except Exception as e:
+ except Exception as e:
logger.error(f"配置文件获取失败: {str(e)}")
# 打印详细报错位置,方便调试
logger.error(traceback.format_exc())
# 子线程初始化 COM 组件
pythoncom.CoInitialize()
try:
- folder_path = os.path.abspath(folder_path)
+ folder_path = os.path.abspath(config.get("output_folder"))
if not os.path.exists(folder_path):
logger.error(f"文件夹不存在: {folder_path}")
return
@@ -400,7 +426,9 @@ def batch_convert_folder(folder_path, stop_event: threading.Event = None, progre
logger.info(f"[跳过] 已存在: {filename}")
continue
- logger.info(f"[{files.index(filename)}/{total_count}]正在转换: {filename} ...")
+ logger.info(
+ f"[{files.index(filename)}/{total_count}]正在转换: {filename} ..."
+ )
try:
# 打开 -> 另存为 -> 关闭
@@ -443,7 +471,7 @@ def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
# 1. 加载配置文件
try:
config = load_config("config.toml")
- except Exception as e:
+ except Exception as e:
logger.error(f"配置文件获取失败: {str(e)}")
# 打印详细报错位置,方便调试
logger.error(traceback.format_exc())
@@ -467,7 +495,7 @@ def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
logger.info(f"开始生成学生属相,共 {total_count} 位学生...")
# 3. 预处理:将“生日”列转换为 datetime 格式
- df['temp_date'] = pd.to_datetime(df[date_column], errors="coerce")
+ df["temp_date"] = pd.to_datetime(df[date_column], errors="coerce")
# 4. 遍历 DataFrame 并计算/更新数据
for i, row in df.iterrows():
@@ -481,7 +509,7 @@ def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
progress_callback(i + 1, total_count, "生成属相")
name = row.get("姓名", f"学生_{i + 1}")
- date = row['temp_date']
+ date = row["temp_date"]
logger.info(f"[{i + 1}/{total_count}] 正在处理学生:{name}...")
@@ -502,7 +530,7 @@ def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
logger.info(f" -> 属相计算成功:{name} ,属相: {zodiac}")
# 6. 清理和保存结果
- df = df.drop(columns=['temp_date'])
+ df = df.drop(columns=["temp_date"])
save_path = excel_path
try:
@@ -521,52 +549,79 @@ def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
logger.error(f"程序运行出错: {str(e)}")
logger.error(traceback.format_exc())
+
# ==========================================
# 6. 一键生成园长签名(根据输出文件夹生成签名)
# ==========================================
def generate_signature(progress_callback=None) -> str:
"""
- 生成园长签名
+ 生成园长签名(不依赖占位符,直接在指定位置添加)
"""
- # 1. 加载配置文件
+ # 1. 加载配置文件
try:
config = load_config("config.toml")
- except Exception as e:
+ except Exception as e:
logger.error(f"配置文件获取失败: {str(e)}")
# 打印详细报错位置,方便调试
logger.error(traceback.format_exc())
try:
# 获取所有的PPT (此时返回的是文件名或路径的列表)
pptx_files = get_output_pptx_files(config["output_folder"])
-
+
if not pptx_files:
logger.warning("没有找到 PPT 文件")
return "未找到文件"
logger.info(f"开始生成签名,共 {len(pptx_files)} 个 PPT 文件...")
-
- img_path = config.get("signature_image") # 签名图片路径
+
+ img_path = config.get("signature_image") # 签名图片路径
if not img_path or not os.path.exists(img_path):
logger.error(f"签名图片不存在: {img_path}")
logger.warning(f"⚠️ 警告: 缺少签名照片('signature')")
return
logger.info(f"签名图片存在: {img_path}")
+
+ # 从配置文件获取签名位置信息,如果没有则使用默认值
+ signature_left = config.get("signature_left", 2987040) # 左位置
+ signature_top = config.get("signature_top", 8273415) # 上位置
+ signature_width = config.get("signature_width", 1800000) # 宽度
+ signature_height = config.get("signature_height", 720000) # 高度
+ # 导入必要的模块
+ from utils.image_utils import get_corrected_image_stream
+
for i, filename in enumerate(pptx_files):
# 获取完整绝对路径
pptx_path = os.path.join(config["output_folder"], filename)
-
- # --- 关键修改点 1: 打开 PPT 对象 ---
+
+ # 打开 PPT 对象
prs = Presentation(pptx_path)
-
- # --- 关键修改点 2: 传递 prs 对象而不是路径字符串 ---
- replace_picture(prs, 1, "signature", img_path)
-
- # --- 关键修改点 3: 保存修改后的 PPT ---
+
+ # 获取第二张幻灯片 (索引为1)
+ slide = prs.slides[1]
+
+ # 获取修正后的图片流
+ img_stream = get_corrected_image_stream(img_path)
+
+ # 直接在指定位置添加签名图片
+ slide.shapes.add_picture(
+ img_stream,
+ signature_left,
+ signature_top,
+ signature_width,
+ signature_height,
+ )
+ logger.info(f"在幻灯片 1 上添加签名图片")
+
+ # 保存修改后的 PPT
prs.save(pptx_path)
-
+
# 更新进度条 (如果有 callback)
if progress_callback:
- progress_callback(i + 1, len(pptx_files),f"[{i + 1}/{len(pptx_files)}] 生成签名完成: {filename}")
+ progress_callback(
+ i + 1,
+ len(pptx_files),
+ f"[{i + 1}/{len(pptx_files)}] 生成签名完成: {filename}",
+ )
logger.success(f"[{i + 1}/{len(pptx_files)}] 生成签名完成: {filename}")
if progress_callback:
progress_callback(len(pptx_files), len(pptx_files), "签名生成完成")
diff --git a/utils/pptx_utils.py b/utils/pptx_utils.py
index 4a4f57f..78ee1f5 100644
--- a/utils/pptx_utils.py
+++ b/utils/pptx_utils.py
@@ -100,14 +100,18 @@ def replace_text_in_slide(prs, slide_index, placeholder, text):
try:
run.font.space = original_font["character_space"]
except:
- logger.error(f"错误: 无法设置字体间距 {original_font['character_space']}")
+ logger.error(
+ f"错误: 无法设置字体间距 {original_font['character_space']}"
+ )
if original_font["color"]:
try:
if hasattr(original_font["color"], "rgb"):
run.font.color.rgb = original_font["color"].rgb
except:
- logger.error(f"错误: 无法设置字体颜色 {original_font['color']}")
+ logger.error(
+ f"错误: 无法设置字体颜色 {original_font['color']}"
+ )
def replace_picture(prs, slide_index, placeholder, img_path):
@@ -138,6 +142,10 @@ def replace_picture(prs, slide_index, placeholder, img_path):
break
if target_shape:
+ logger.debug(f"找到占位符 {placeholder},索引 {target_index}")
+ logger.debug(
+ f"占位符位置: 左 {target_shape.left}, 上 {target_shape.top}, 宽 {target_shape.width}, 高 {target_shape.height}"
+ )
# 获取原位置信息
left, top, width, height = (
target_shape.left,