Compare commits

..

2 Commits

8 changed files with 119 additions and 51 deletions

1
.gitignore vendored
View File

@@ -14,3 +14,4 @@ data/images/*
data/*.xlsx
.idea/
.trae/

7
.idea/workspace.xml generated
View File

@@ -4,7 +4,9 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="41690157-d51b-4dae-98de-6b96990d681a" name="更改" comment="fix优化一些命名规范" />
<list default="true" id="41690157-d51b-4dae-98de-6b96990d681a" name="更改" comment="fix优化一些命名规范">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@@ -95,6 +97,9 @@
<workItem from="1766256207534" duration="960000" />
<workItem from="1766287241685" duration="2135000" />
<workItem from="1766329711762" duration="741000" />
<workItem from="1768312728552" duration="228000" />
<workItem from="1768312972093" duration="486000" />
<workItem from="1768314152581" duration="7000" />
</task>
<task id="LOCAL-00001" summary="fix修复一些BUG">
<option name="closed" value="true" />

View File

@@ -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"

View File

@@ -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": {

View File

@@ -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()
],

View File

@@ -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)

View File

@@ -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 进度回调函数
@@ -87,7 +87,7 @@ 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:
@@ -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,7 +182,7 @@ 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:
@@ -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,14 +369,14 @@ 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:
@@ -360,7 +386,7 @@ def batch_convert_folder(folder_path, stop_event: threading.Event = None, progre
# 子线程初始化 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:
# 打开 -> 另存为 -> 关闭
@@ -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,14 +549,15 @@ 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:
@@ -545,28 +574,54 @@ def generate_signature(progress_callback=None) -> str:
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)
# 获取第二张幻灯片 (索引为1)
slide = prs.slides[1]
# --- 关键修改点 3: 保存修改后的 PPT ---
# 获取修正后的图片流
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), "签名生成完成")

View File

@@ -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,