fix:添加UI界面,完善功能

This commit is contained in:
2025-12-11 11:16:09 +08:00
parent 81e3c40abb
commit f437842a81
17 changed files with 939 additions and 406 deletions

340
main.py
View File

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