@@ -1,4 +1,5 @@
import os
import threading
import time
import pythoncom
@@ -12,7 +13,6 @@ import comtypes.client
from config . config import load_config
from utils . agent_utils import generate_comment
from utils . file_utils import check_file_exists
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 (
@@ -23,7 +23,6 @@ from utils.growt_utils import (
replace_five_page ,
)
# 如果你之前没有全局定义 console, 这里定义一个
console = Console ( )
@@ -36,26 +35,33 @@ config = load_config("config.toml")
# ==========================================
# 1. 生成模板(根据names.xlsx文件生成名字图片文件夹)
# ==========================================
def generate_template ( ) :
def generate_template ( stop_event : threading . Event = None , progress_callback = None ) :
""" "
根据学生姓名生成相对应的以学生姓名的存放照片的文件夹
:params stop_event 任务是否停止事件( 监听UI的事件监听)
:params progress_callback 进度回调函数
"""
try :
# 2 . 读取数据
# 1 . 读取数据
df = pd . read_excel ( config [ " excel_file " ] , sheet_name = " Sheet1 " )
# --- 修改点开始 ---
# 直接读取 "姓名" 这一列,不使用列表包裹列名,这样得到的是一维数据
# 2. 获取姓名数据
datas = df [ " 姓名 " ] . values . tolist ( )
# --- 修改点结束 ---
logger . info ( f " 开始生成学生模版文件,共 { len ( datas ) } 位学生... " )
total_count = len ( datas )
logger . info ( f " 开始生成学生模版文件,共 { total_count } 位学生... " )
# 3. 循环处理
# 此时 name 就是字符串 '张三',而不是列表 ['张三']
for i , name in enumerate ( datas ) :
logger . info ( f " [ { i + 1 } / { len ( datas ) } ] 正在生成: { name } " )
# 判断是否有停止事件
if stop_event and stop_event . is_set ( ) :
logger . warning ( " 任务正在停止中,正在中断中..... " )
return # 停止任务
# 添加进度条
if progress_callback :
progress_callback ( i + 1 , total_count , " 生成学生图片文件夹 " )
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 ) :
@@ -63,7 +69,10 @@ def generate_template():
else :
logger . info ( f " 正在生成学生图片文件夹 { student_folder } " )
os . makedirs ( student_folder , exist_ok = True )
# 更新进度条为100%
if progress_callback :
progress_callback ( total_count , total_count , " 生成学生图片文件夹 " )
logger . success ( " ✅ 所有学生模版文件已生成完毕 " )
except Exception as e :
logger . error ( f " 程序运行出错: { str ( e ) } " )
# 打印详细报错位置,方便调试
@@ -73,7 +82,12 @@ def generate_template():
# ==========================================
# 2. 生成评语(根据names.xlsx文件生成评价)
# ==========================================
def generate_comment_all ( ) :
def generate_comment_all ( stop_event : threading . Event = None , progress_callback = None ) :
"""
根据学生姓名生成评价
:params stop_event 任务是否停止事件( 监听UI的事件监听)
:params progress_callback 进度回调函数
"""
try :
# 1. 读取数据
excel_path = config [ " excel_file " ]
@@ -85,23 +99,27 @@ def generate_comment_all():
# 获取学生数据行数
total_count = len ( df )
logger . info ( f " 开始生成学生评语,共 { total_count } 位学生... " )
logger . info ( f " 开始生成学生评语,共 { len ( df ) } 位学生... " )
# 强制将“评价”列转换为 object 类型
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 stop_event and stop_event . is_set ( ) :
logger . warning ( " 任务正在停止中,正在中断中..... " )
return # 停止任务
# 添加进度条
if progress_callback :
progress_callback ( i + 1 , total_count , " 生成学生评语 " )
# 获取学生姓名
name = df . at [ i , " 姓名 " ]
if pd . isna ( name ) :
continue # 跳过空行
name = str ( name ) . strip ( )
continue
else :
name = str ( name ) . strip ( )
# 获取性别
sex = pd . isna ( df . at [ i , " 性别 " ] ) if " 男 " else str ( df . at [ i , " 性别 " ] ) . strip ( )
# 获取当前行的特征( 如果Excel里有“特征”这一列就读, 没有就用默认值)
# 假设Excel里有一列叫 "表现特征",如果没有则用默认的 "有礼貌..."
@@ -124,11 +142,7 @@ def generate_comment_all():
generated_text = generate_comment (
name , config [ " age_group " ] , traits , sex
)
if generated_text :
# 赋值
df . at [ i , " 评价 " ] = str ( generated_text ) . strip ( )
else :
df . at [ i , " 评价 " ] = " " # 防空处理
df . at [ i , " 评价 " ] = generated_text if str ( generated_text ) . strip ( ) else " "
logger . success ( f " 学生: { name } ,评语生成完毕 " )
# 可选:每生成 5 个就保存一次
@@ -139,11 +153,12 @@ def generate_comment_all():
time . sleep ( 1 )
except Exception as e :
logger . error ( f " 学生: { name } ,生成评语出错: { str ( e ) } " )
# --- 循环结束后最终保存文件 ---
# index=False 表示不把 pandas 的索引 (0,1,2...) 写到 Excel 第一列
df . to_excel ( excel_path , index = False )
logger . success ( f " 所有评语已生成并写入文件: { excel_path } " )
if progress_callback :
progress_callback ( total_count , total_count , " 生成学生评语 " )
except PermissionError :
logger . error ( f " 保存失败!请先关闭 Excel 文件: { config [ ' excel_file ' ] } " )
@@ -155,26 +170,23 @@ def generate_comment_all():
# ==========================================
# 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 )
# 检查模版文件是否存在
def generate_report ( stop_event : threading . Event = None , progress_callback = None ) :
"""
根据学生姓名生成成长报告
:params stop_event 任务是否停止事件( 监听UI的事件监听)
:params progress_callback 进度回调函数
"""
# 1. 检查模版文件是否存在
if not os . path . exists ( config [ " source_file " ] ) :
logger . info ( f " 错误: 找不到模版文件 { config [ " source_file " ] } " )
return
# 检查数据文件是否存在
# 2. 检查数据文件是否存在
if not os . path . exists ( config [ " excel_file " ] ) :
logger . info ( f " 错误: 找不到数据文件 { config [ ' excel_file ' ] } " )
return
try :
# 2 . 读取数据
# 1 . 读取数据
df = pd . read_excel ( config [ " excel_file " ] , sheet_name = " Sheet1 " )
# 确保列名对应
# 2. 确保列名对应
columns = [
" 姓名 " ,
" 英文名 " ,
@@ -187,14 +199,21 @@ def generate_report():
" 喜欢吃的食物 " ,
" 评价 " ,
]
# 获取数据列表
datas = df [ columns ] . values . tolist ( )
total_count = len ( datas )
# 获取配置文件的教师签名
teacher_names_str = " " . join ( config [ " teachers " ] )
logger . info ( f " 开始处理,共 { len ( datas ) } 位学生... " )
logger . info ( f " 开始处理,共 { total_count } 位学生... " )
# 3. 循环处理
for i , row_data in enumerate ( datas ) :
if stop_event and stop_event . is_set ( ) :
logger . warning ( " 任务正在停止中,正在中断中..... " )
return
# 更新进度条
if progress_callback :
progress_callback ( i + 1 , total_count , " 生成报告 " )
# 解包数据
(
name ,
@@ -237,16 +256,16 @@ def generate_report():
# 构造学生信息字典
student_info_dict = {
" name " : name ,
" english_name " : english_name if pd . notna ( english_name ) else " " ,
" english_name " : english_name if pd . notna ( english_name ) else " ",
" sex " : sex if pd . notna ( sex ) else " 男 " ,
" birthday " : (
birthday . strftime ( " % Y- % m- %d " ) if pd . notna ( birthday ) else " "
birthday . strftime ( " % Y- % m- %d " ) if pd . notna ( birthday ) else " "
) ,
" zodiac " : zodiac if pd . notna ( zodiac ) else " " ,
" friend " : friend if pd . notna ( friend ) else " " ,
" hobby " : hobby if pd . notna ( hobby ) else " " ,
" game " : game if pd . notna ( game ) else " " ,
" food " : food if pd . notna ( food ) 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 " )
@@ -254,6 +273,7 @@ def generate_report():
if check_file_exists ( me_image_path ) :
replace_three_page ( prs , student_info_dict , me_image_path )
else :
replace_three_page ( prs , student_info_dict )
logger . warning ( f " ⚠️ 警告: 学生图片文件不存在 { me_image_path } " )
# --- 页面 4 ---
@@ -266,7 +286,7 @@ def generate_report():
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 " )
@@ -301,6 +321,8 @@ def generate_report():
f " 保存失败: 文件 { new_filename } 可能已被打开,请关闭后重试。 "
)
if progress_callback :
progress_callback ( total_count , total_count , " 生成报告 " )
logger . success ( " 所有报告生成完毕! " )
except Exception as e :
@@ -311,10 +333,12 @@ def generate_report():
# ==========================================
# 5. 转换格式(根据names.xlsx文件生成PPT转PDF)
# ==========================================
def batch_convert_folder ( folder_path ) :
def batch_convert_folder ( folder_path , stop_event : threading . Event = None , progress_callback = None ):
"""
【推荐】 批量转换文件夹下的所有 PPT (只启动一次 PowerPoint, 速度快)
已修复多线程 CoInitialize 报错,并适配 GUI 日志
批量转换文件夹下的所有 PPT
:params folder_path 需要转换的PPT文件夹
:params stop_event 任务是否停止事件( 监听UI的事件监听)
:params progress_callback 进度回调函数
"""
# 子线程初始化 COM 组件
pythoncom . CoInitialize ( )
@@ -333,18 +357,24 @@ def batch_convert_folder(folder_path):
logger . warning ( " 没有找到 PPT 文件 " )
return
logger . info ( f " 发现 { len ( files ) } 个文件,准备开始转换... " )
total_count = len ( files )
logger . info ( f " 发现 { total_count } 个文件,准备开始转换... " )
powerpoint = None
try :
# 1. 启动应用 (只启动一次)
powerpoint = comtypes . client . CreateObject ( " PowerPoint.Application " )
# 【建议】在后台线程运行时,有时设置为不可见更稳定,
# 但如果遇到转换卡死,可以尝试去掉下面这行的注释,让它显示出来
# 设置是否显示转化页面,但如果遇到转换卡死,可以尝试去掉下面这行的注释,让它显示出来
# powerpoint.Visible = 1
for filename in files :
if stop_event and stop_event . is_set ( ) :
logger . warning ( " 任务正在停止中,正在中断中..... " )
return
# 添加进度条
if progress_callback :
progress_callback ( files . index ( filename ) , total_count , " 转换PDF " )
ppt_path = os . path . join ( folder_path , filename )
pdf_path = os . path . splitext ( ppt_path ) [ 0 ] + " .pdf "
@@ -353,7 +383,7 @@ def batch_convert_folder(folder_path):
logger . info ( f " [跳过] 已存在: { filename } " )
continue
logger . info ( f " 正在转换: { filename } ... " )
logger . info ( f " [ { files . index ( filename ) } / { total_count } ] 正在转换: { filename } ... " )
try :
# 打开 -> 另存为 -> 关闭
@@ -363,6 +393,9 @@ def batch_convert_folder(folder_path):
except Exception as e :
logger . error ( f " 文件 { filename } 转换出错: { e } " )
# 添加进度条
if progress_callback :
progress_callback ( total_count , total_count , " 转换PDF " )
except Exception as e :
logger . error ( f " PowerPoint 进程启动出错: { e } " )
finally :
@@ -384,11 +417,15 @@ def batch_convert_folder(folder_path):
# ==========================================
# 5. 生成属相(根据names.xlsx文件生成属相)
# ==========================================
def generate_zodiac ( ) :
def generate_zodiac ( stop_event : threading . Event = None , progress_callback = None ) :
"""
生成学生属相,如果“生日”列为空,则跳过该学生。
:params stop_event 任务是否停止事件( 监听UI的事件监听)
:params progress_callback 进度回调函数
"""
try :
# 1. 读取数据
excel_path = config [ " excel_file " ]
# sheet_name 根据实际情况修改,如果不确定可以用 sheet_name=0 读取第一个
df = pd . read_excel ( excel_path , sheet_name = " Sheet1 " )
# 2. 检查必要的列
@@ -399,30 +436,63 @@ def generate_zodiac():
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 )
# 3. 预处理:将“生日”列转换为 datetime 格式
df [ ' temp_date' ] = pd . to_datetime ( df [ date_column ] , errors = " coerce " )
# 4. 遍历 DataFrame 并计算/更新数据
for i , row in df . iterrows ( ) :
# 关键点 1: 检查停止信号
if stop_event and stop_event . is_set ( ) :
logger . warning ( " 任务已接收到停止信号,正在中断... " )
return
# 添加进度条
if progress_callback :
progress_callback ( i + 1 , total_count , " 生成属相 " )
name = row . get ( " 姓名 " , f " 学生_ { i + 1 } " )
date = row [ ' temp_date ' ]
logger . info ( f " [ { i + 1 } / { total_count } ] 正在处理学生: { name } ... " )
# === 关键点 2: 检查生日是否为空 ===
if pd . isna ( date ) :
# 记录警告日志并跳过当前循环迭代
logger . warning ( f " 跳过:学生【 { name } 】的生日数据为空或格式错误。 " )
# 可以选择将属相字段清空或设置为特定值,此处设置为“待补充”
df . loc [ i , target_column ] = " 待补充 "
continue # 跳到下一个学生
# =================================
# 5. 计算并赋值
zodiac = calculate_zodiac ( date )
df . loc [ i , target_column ] = zodiac
logger . info ( f " -> 属相计算成功: { name } ,属相: { zodiac } " )
# 6. 清理和保存结果
df = df . drop ( columns = [ ' temp_date ' ] )
# 5. 保存结果
save_path = excel_path
try :
df . to_excel ( save_path , index = False )
df . to_excel ( save_path , sheet_name = " Sheet1 " , index = False )
logger . success ( f " 所有属相已更新并写入文件: { save_path } " )
logger . warning ( f " 请检查文件 { save_path } 修改日期格式。 " )
except PermissionError :
logger . error ( f " 保存失败!请先关闭 Excel 文件: { save_path } " )
# 添加进度条
if progress_callback :
progress_callback ( total_count , total_count , " 生成属相 " )
except FileNotFoundError :
logger . error ( f " 找不到文件 { config . get ( ' excel_file ' ) } " )
logger . error ( traceback . format_exc ( ) )
except Exception as e :
logger . error ( f " 程序运行出错: { str ( e ) } " )
logger . error ( traceback . format_exc ( ) )