fix:优化启动方式

This commit is contained in:
2025-12-12 12:37:41 +08:00
parent 4d50c73ecb
commit 7275699c25
22 changed files with 449 additions and 398 deletions

2
.gitignore vendored
View File

@@ -10,5 +10,7 @@ wheels/
.venv
output/*.pptx
output/*.pdf
data/images/*
data/*.xlsx
config.toml

120
.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="09d6a6cb-782b-4f40-bb60-5703c43250ec" name="更改" comment="fix:更新代码开源协议" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 0
}]]></component>
<component name="ProjectId" id="36hYCM0j8RdgslpqW2LFtFj8NEK" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true",
"Python.UI.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "master",
"last_opened_file_path": "D:/working/tools/growth_report",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RunManager">
<configuration name="UI" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="growth_report" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/UI.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<recent_temporary>
<list>
<item itemvalue="Python.UI" />
</list>
</recent_temporary>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="09d6a6cb-782b-4f40-bb60-5703c43250ec" name="更改" comment="" />
<created>1765460141811</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1765460141811</updated>
<workItem from="1765460142948" duration="1948000" />
</task>
<task id="LOCAL-00001" summary="fix:优化一些BUG">
<option name="closed" value="true" />
<created>1765460523294</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1765460523294</updated>
</task>
<task id="LOCAL-00002" summary="fix:优化配置项目">
<option name="closed" value="true" />
<created>1765460614548</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1765460614548</updated>
</task>
<task id="LOCAL-00003" summary="fix:更新代码开源协议">
<option name="closed" value="true" />
<created>1765460996031</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1765460996031</updated>
</task>
<option name="localTasksCounter" value="4" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="fix:优化一些BUG" />
<MESSAGE value="fix:优化配置项目" />
<MESSAGE value="fix:更新代码开源协议" />
<option name="LAST_COMMIT_MESSAGE" value="fix:更新代码开源协议" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/growth_report$UI.coverage" NAME="UI 覆盖结果" MODIFIED="1765460384581" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
</component>
</project>

225
UI.py
View File

@@ -1,225 +0,0 @@
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import messagebox
from tkinter import filedialog
import threading
import sys
import time
import queue
import re
from loguru import logger
from config.config import load_config
# 假设你的功能函数都在这里
from utils.generate_utils import (
generate_template,
generate_comment_all,
batch_convert_folder,
generate_report,
generate_zodiac,
)
from utils.file_utils import export_templates_folder, initialize_project, export_data
# ==========================================
# 0. 全局配置与队列准备
# ==========================================
config = load_config("config.toml")
log_queue = queue.Queue()
def ansi_cleaner(text):
"""【辅助函数】去除 loguru 输出中的 ANSI 颜色代码"""
ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
return ansi_escape.sub("", text)
def queue_sink(message):
"""【核心】loguru sink 回调"""
clean_msg = ansi_cleaner(message)
log_queue.put(clean_msg)
# ==========================================
# GUI 主程序类
# ==========================================
class ReportApp:
def __init__(self, root):
self.root = root
self.root.title("🌱 尚城幼儿园成长报告助手")
self.root.geometry("720x680") # 高度稍微增加一点以容纳分组
# 设置样式
self.style = ttk.Style()
self.style.theme_use("clam")
self.style.configure("TButton", font=("微软雅黑", 10), padding=5)
self.style.configure("Title.TLabel", font=("微软雅黑", 16, "bold"), foreground="#2E8B57")
self.style.configure("Sub.TLabel", font=("微软雅黑", 9), foreground="gray")
# LabelFrame 的标题样式
self.style.configure("TLabelframe.Label", font=("微软雅黑", 10, "bold"), foreground="#0055a3")
# --- 1. 标题区域 ---
header_frame = ttk.Frame(root, padding="10 15 10 5")
header_frame.pack(fill=tk.X)
ttk.Label(header_frame, text="🌱 尚城幼儿园成长报告助手", style="Title.TLabel").pack()
ttk.Label(header_frame, text="By 寒寒", style="Sub.TLabel").pack()
# --- 2. 按钮功能区域 (使用 LabelFrame 分组) ---
# 容器 Frame给四周留点白
main_content = ttk.Frame(root, padding=10)
main_content.pack(fill=tk.X)
# === A组: 核心功能 ===
func_btns = [
("📁 生成图片路径", self.run_generate_folders),
("🤖 生成评语 (AI)", self.run_generate_comments),
("📊 生成报告 (PPT)", self.run_generate_report),
("📑 格式转换 (PDF)", self.run_convert_pdf),
("🐂 生肖转化 (生日)", self.run_zodiac),
]
self.create_btn_group(main_content, "🛠️ 核心功能", func_btns, columns=3)
# === B组: 数据导出 ===
export_btns = [
("📦 导出数据模板 (Zip)", self.run_export_data_folder),
("📤 导出数据备份 (Zip)", self.run_export_data),
]
self.create_btn_group(main_content, "📦 数据管理", export_btns, columns=2)
# === C组: 系统设置 ===
system_btns = [
("⚠️ 初始化系统 (重置)", self.run_initialize_project),
("🚪 退出系统", self.quit_app),
]
self.create_btn_group(main_content, "⚙️ 系统操作", system_btns, columns=2)
# --- 3. 日志输出区域 ---
log_frame = ttk.LabelFrame(root, text="📝 系统实时日志", padding=10)
log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
self.log_text = scrolledtext.ScrolledText(
log_frame, height=10, state="disabled", font=("Consolas", 9)
)
self.log_text.pack(fill=tk.BOTH, expand=True)
# 启动日志轮询
self.root.after(100, self.poll_log_queue)
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):
while not log_queue.empty():
try:
msg = log_queue.get_nowait()
self.log_text.config(state="normal")
self.log_text.insert(tk.END, msg)
self.log_text.see(tk.END)
self.log_text.config(state="disabled")
except queue.Empty:
break
self.root.after(100, self.poll_log_queue)
# --- 线程包装器 ---
def run_in_thread(self, target_func, *args):
def thread_task():
try:
target_func(*args)
logger.success("✅ 当前任务执行完毕。")
except Exception as e:
logger.error(f"❌ 发生错误: {str(e)}")
import traceback
logger.error(traceback.format_exc())
threading.Thread(target=thread_task, daemon=True).start()
# ==========================================
# 按钮事件 (业务逻辑保持不变)
# ==========================================
def run_generate_folders(self):
self.run_in_thread(generate_template)
def run_generate_comments(self):
self.run_in_thread(generate_comment_all)
def run_generate_report(self):
self.run_in_thread(generate_report)
def run_convert_pdf(self):
self.run_in_thread(batch_convert_folder, config["output_folder"])
def run_zodiac(self):
self.run_in_thread(generate_zodiac)
def run_export_data_folder(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_templates_folder, target_folder)
def run_initialize_project(self):
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):
if messagebox.askokcancel("退出", "确定要退出系统吗?"):
self.root.destroy()
sys.exit()
# ==========================================
# 启动入口
# ==========================================
def applicationUI():
logger.add(
queue_sink,
format="{time:HH:mm:ss} | {level: <8} | {message}",
level="INFO",
)
root = tk.Tk()
app = ReportApp(root)
root.mainloop()
if __name__ == "__main__":
applicationUI()

Binary file not shown.

117
main.py
View File

@@ -1,117 +0,0 @@
from config.config import load_config
from utils.generate_utils import (
generate_template,
generate_comment_all,
generate_report,
batch_convert_folder,
generate_zodiac,
)
from utils.file_utils import export_templates_folder, initialize_project, export_data
config = load_config("config.yaml")
def application():
from rich.console import Console
from rich.panel import Panel
from rich.prompt import Prompt
from rich.table import Table
from rich.align import Align
from rich import box
import sys
console = Console()
while True:
console.clear()
# 1. 创建一个表格,不显示表头,使用圆角边框
table = Table(box=None, show_header=False, padding=(0, 2))
# 2. 添加两列:序号列(居右),内容列(居左)
table.add_column(justify="right", style="cyan bold")
table.add_column(justify="left")
# 3. 添加行内容
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.", "📤 导出数据")
table.add_row("9.", "🚪 退出系统")
# 4. 将表格放入面板,并居中显示
panel = Panel(
Align.center(table),
title="[bold green]🌱 幼儿园成长报告助手",
subtitle="[dim]By 寒寒",
width=60,
border_style="bright_blue",
box=box.ROUNDED, # 圆角边框更柔和
)
# 使用 Align.center 让整个菜单在屏幕中间显示
console.print(Align.center(panel, vertical="middle"))
console.print("\n") # 留点空隙
choice = Prompt.ask(
"👉 请输入序号执行",
choices=["1", "2", "3", "4", "5", "6", "7", "8", "9"],
default="1",
)
try:
if choice == "1":
console.rule("[bold cyan]正在执行: 生成模板[/]")
with console.status(
"[bold green]正在创建文件夹结构...[/]", spinner="dots"
):
generate_template()
elif choice == "2":
console.rule("[bold yellow]正在执行: AI 生成评语[/]")
# 这里的 generate_comment_all 最好内部有进度条,或者简单的 print
generate_comment_all()
elif choice == "3":
console.rule("[bold blue]正在执行: PPT 合成[/]")
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_templates_folder()
elif choice == "7":
console.rule("[bold magenta]正在执行: 初始化系统[/]")
# 调用上面的批量转换函数,传入你的 output 文件夹路径
initialize_project()
elif choice == "8":
console.rule("[bold magenta]正在执行: 导出数据[/]")
# 调用上面的批量转换函数,传入你的 output 文件夹路径
export_data()
elif choice == "9":
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"
)
)
Prompt.ask("按 Enter 键继续...")
if __name__ == "__main__":
application()

23
main.pyw Normal file
View File

@@ -0,0 +1,23 @@
import tkinter as tk
from utils.log_handler import setup_logging
from ui.app_window import ReportApp
from loguru import logger
def main():
# 1. 初始化日志
setup_logging()
logger.info("正在启动应用程序...")
# 2. 启动 UI
root = tk.Tk()
# 这一行可以设置图标 (如果有 icon.ico 文件)
# root.iconbitmap("icon.ico")
app = ReportApp(root)
# 3. 进入主循环
root.mainloop()
if __name__ == "__main__":
main()

View File

@@ -12,6 +12,7 @@ dependencies = [
"openpyxl>=3.1.5",
"pandas>=2.3.3",
"pandas-stubs==2.3.3.251201",
"pillow>=12.0.0",
"python-pptx>=1.0.2",
"pywin32>=311",
"rich>=14.2.0",

View File

@@ -4,7 +4,7 @@
chcp 65001 >nul
:: ------------------------------------------------
title 幼儿园成长报告助手
title 幼儿园成长报告助手启动器
cd /d "%~dp0"
echo.
@@ -13,7 +13,7 @@ echo 正在启动 幼儿园成长报告助手
echo ==========================================
echo.
:: 检查 uv 是否安装
:: 1. 检查 uv 是否安装
uv --version >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] 未检测到 uv 工具!
@@ -22,21 +22,28 @@ if %errorlevel% neq 0 (
exit /b
)
echo [INFO] 环境检查通过,正在运行主程序...
echo ---------------------------------------------------
echo [INFO] 环境检查通过...
:: 这里的 gui_app.py 就是你刚才保存的那个带界面的 Python 文件名
:: 如果你的文件名不一样,请修改下面这一行
uv run UI.py
:: 错误捕获
:: 2. 检查依赖是否安装 (可选,防止第一次运行报错)
:: 如果你有 pyproject.tomluv run 会自动处理,这一步可以省略
:: 这里为了保险,检查一下 loguru 是否存在,不存在则自动安装基础依赖
uv pip show loguru >nul 2>&1
if %errorlevel% neq 0 (
echo.
echo ---------------------------------------------------
echo [ERROR] 程序异常退出 (代码: %errorlevel%)
echo 请检查上方报错信息。
pause
) else (
echo.
echo [INFO] 程序已正常结束。
echo [INFO] 首次运行,正在安装依赖...
uv pip install loguru toml pandas pillow openpyxl python-pptx
)
echo [INFO] 正在拉起主程序...
echo ---------------------------------------------------
:: =======================================================
:: 【关键修改】路径改为根目录的 main.pyw
:: 使用 start 命令启动,这样黑色的 CMD 窗口可以随后立即关闭
:: =======================================================
start "" uv run main.pyw
:: 等待 1 秒确保启动
timeout /t 1 >nul
:: 退出 CMD 窗口 (让用户只看到 GUI)
exit

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

161
ui/app_window.py Normal file
View File

@@ -0,0 +1,161 @@
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog
import threading
import queue
import sys
from loguru import logger
from config.config import load_config
from utils.log_handler import log_queue
# 导入业务逻辑
from utils.generate_utils import (
generate_template,
generate_comment_all,
batch_convert_folder,
generate_report,
generate_zodiac,
)
from utils.file_utils import export_templates_folder, initialize_project, export_data
config = load_config("config.toml")
class ReportApp:
def __init__(self, root):
self.root = root
self.root.title("🌱 尚城幼儿园成长报告助手")
self.root.geometry("720x760")
# 线程控制
self.stop_event = threading.Event()
self.is_running = False
self._setup_ui()
self._start_log_polling()
def _setup_ui(self):
# 样式配置
self.style = ttk.Style()
self.style.theme_use("clam")
self.style.configure("TButton", font=("微软雅黑", 10), padding=5)
self.style.configure("Title.TLabel", font=("微软雅黑", 16, "bold"), foreground="#2E8B57")
self.style.configure("Stop.TButton", foreground="red", font=("微软雅黑", 10, "bold"))
# 1. 标题
header = ttk.Frame(self.root, padding="10 15 10 5")
header.pack(fill=tk.X)
ttk.Label(header, text="🌱 尚城幼儿园成长报告助手", style="Title.TLabel").pack()
ttk.Label(header, text="By 寒寒 | 这里的每一份评语都充满爱意", font=("微软雅黑", 9), foreground="gray").pack()
# 2. 功能区容器
main_content = ttk.Frame(self.root, padding=10)
main_content.pack(fill=tk.X)
# === A组: 核心功能 ===
self._create_btn_group(main_content, "🛠️ 核心功能", [
("📁 生成图片路径", lambda: self.run_task(generate_template)),
("🤖 生成评语 (AI)", lambda: self.run_task(generate_comment_all)),
("📊 生成报告 (PPT)", lambda: self.run_task(generate_report)),
("📑 格式转换 (PDF)", lambda: self.run_task(batch_convert_folder, config.get("output_folder"))),
("🐂 生肖转化 (生日)", lambda: self.run_task(generate_zodiac)),
], columns=3)
# === B组: 数据管理 ===
self._create_btn_group(main_content, "📦 数据管理", [
("📦 导出模板 (Zip)", self.run_export_template),
("📤 导出备份 (Zip)", self.run_export_data),
], columns=2)
# === C组: 系统操作 (含停止按钮) ===
self._create_btn_group(main_content, "⚙️ 系统操作", [
("⛔ 停止当前任务", self.stop_current_task),
("⚠️ 初始化系统", self.run_init),
("🚪 退出系统", self.quit_app),
], columns=3, special_styles={"⛔ 停止当前任务": "Stop.TButton"})
# 3. 日志区
log_frame = ttk.LabelFrame(self.root, text="📝 系统实时日志", padding=10)
log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
self.log_text = scrolledtext.ScrolledText(log_frame, height=10, state="disabled", font=("Consolas", 9))
self.log_text.pack(fill=tk.BOTH, expand=True)
def _create_btn_group(self, parent, title, buttons, columns=2, special_styles=None):
frame = ttk.LabelFrame(parent, text=title, padding=10)
frame.pack(fill=tk.X, pady=5)
special_styles = special_styles or {}
for i, (text, func) in enumerate(buttons):
style = special_styles.get(text, "TButton")
btn = ttk.Button(frame, text=text, command=func, style=style)
r, c = divmod(i, columns)
btn.grid(row=r, column=c, padx=5, pady=5, sticky="ew")
for i in range(columns):
frame.columnconfigure(i, weight=1)
def _start_log_polling(self):
while not log_queue.empty():
try:
msg = log_queue.get_nowait()
self.log_text.config(state="normal")
self.log_text.insert(tk.END, msg)
self.log_text.see(tk.END)
self.log_text.config(state="disabled")
except queue.Empty:
break
self.root.after(100, self._start_log_polling)
# --- 任务运行核心逻辑 ---
def run_task(self, target_func, *args, **kwargs):
if self.is_running:
messagebox.showwarning("忙碌中", "请先等待当前任务完成或点击【停止当前任务】")
return
self.stop_event.clear()
self.is_running = True
def thread_worker():
try:
# 尝试传入 stop_event
try:
target_func(*args, stop_event=self.stop_event, **kwargs)
except TypeError:
# 如果旧函数不支持 stop_event则普通运行
target_func(*args, **kwargs)
except Exception as e:
logger.error(f"任务出错: {e}")
import traceback
logger.error(traceback.format_exc())
finally:
self.is_running = False
logger.info("--- 就绪 ---")
threading.Thread(target=thread_worker, daemon=True).start()
def stop_current_task(self):
if not self.is_running:
return
if messagebox.askyesno("确认", "确定要中断当前任务吗?"):
self.stop_event.set()
logger.warning("正在发送停止信号...")
# --- 具体按钮事件 ---
def run_export_template(self):
path = filedialog.askdirectory()
if path: self.run_task(export_templates_folder, path)
def run_export_data(self):
path = filedialog.askdirectory()
if path: self.run_task(export_data, path)
def run_init(self):
if messagebox.askokcancel("警告", "确定重置系统吗?数据将丢失!"):
self.run_task(initialize_project)
def quit_app(self):
if self.is_running:
messagebox.showwarning("提示", "请先停止任务")
return
self.root.destroy()
sys.exit()

View File

@@ -6,6 +6,8 @@ import platform
import shutil
from pathlib import Path
from loguru import logger
def get_system_fonts():
"""获取系统中可用的字体列表"""
@@ -21,7 +23,7 @@ def get_system_fonts():
for font_file in folder.glob(ext):
fonts.add(font_file.stem)
except Exception as e:
print(f"读取系统字体时出错: {e}")
logger.error(f"读取系统字体时出错: {e}")
fonts = {"微软雅黑", "宋体", "黑体", "Arial", "Microsoft YaHei"}
return fonts
@@ -40,14 +42,14 @@ def is_font_available(font_name):
def install_fonts_from_directory(fonts_dir="fonts"):
"""从指定目录安装字体到系统"""
if platform.system() != "Windows":
print("字体安装功能目前仅支持Windows系统")
logger.success("字体安装功能目前仅支持Windows系统")
return False
target_font_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts'
target_font_dir.mkdir(parents=True, exist_ok=True)
if not os.path.exists(fonts_dir):
print(f"字体目录 {fonts_dir} 不存在,请创建该目录并将字体文件放入")
logger.error(f"字体目录 {fonts_dir} 不存在,请创建该目录并将字体文件放入")
return False
font_files = []
@@ -55,7 +57,7 @@ def install_fonts_from_directory(fonts_dir="fonts"):
font_files.extend(Path(fonts_dir).glob(f'*{ext}'))
if not font_files:
print(f"{fonts_dir} 目录中未找到字体文件")
logger.error(f"{fonts_dir} 目录中未找到字体文件")
return False
installed_count = 0
@@ -64,12 +66,12 @@ def install_fonts_from_directory(fonts_dir="fonts"):
target_path = target_font_dir / font_file.name
if not target_path.exists():
shutil.copy2(font_file, target_path)
print(f"已安装字体: {font_file.name}")
logger.success(f"已安装字体: {font_file.name}")
installed_count += 1
except Exception as e:
print(f"安装字体 {font_file.name} 时出错: {str(e)}")
logger.error(f"安装字体 {font_file.name} 时出错: {str(e)}")
if installed_count > 0:
print(f"共安装了 {installed_count} 个新字体文件建议重启Python环境")
logger.success(f"共安装了 {installed_count} 个新字体文件建议重启Python环境")
return True
return False

View File

@@ -6,6 +6,7 @@ import pandas as pd
from loguru import logger
from pptx import Presentation
from rich.console import Console
import traceback
import comtypes.client
from config.config import load_config
@@ -65,8 +66,6 @@ def generate_template():
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
# 打印详细报错位置,方便调试
import traceback
logger.error(traceback.format_exc())
@@ -83,11 +82,10 @@ def generate_comment_all():
if "评价" not in df.columns:
df["评价"] = ""
# --- 获取总行数,用于日志 ---
# 强制将“评价”列转换为 object 类型
# 获取学生数据行数
total_count = len(df)
logger.info(f"开始生成学生评语,共 {total_count} 位学生...")
# 强制将“评价”列转换为 object 类型
df["评价"] = df["评价"].astype("object")
# --- 遍历 DataFrame 的索引 (index) ---
# 这样我们可以通过索引 i 精准地把数据写回某一行
@@ -121,28 +119,27 @@ def generate_comment_all():
logger.info(f"[{i + 1}/{total_count}] 正在生成评价: {name}")
try:
# 调用你的生成函数,并【接收返回值】
# 注意:这里假设 generate_comment 返回的是清洗后的字符串
# 调用AI大模型生成内容
generated_text = generate_comment(
name, config["age_group"], traits, sex
)
# --- 将结果写入 DataFrame ---
df.at[i, "评价"] = generated_text
if generated_text:
# 赋值
df.at[i, "评价"] = str(generated_text).strip()
else:
df.at[i, "评价"] = "" # 防空处理
logger.success(f"学生:{name},评语生成完毕")
# 可选:每生成 5 个就保存一次,防止程序崩溃数据丢失
# 可选:每生成 5 个就保存一次
if (i + 1) % 5 == 0:
df.to_excel(excel_path, index=False)
logger.info("--- 阶段性保存成功 ---")
time.sleep(1) # 避免触发API速率限制
logger.success(" 阶段性保存成功")
# 避免触发API速率限制
time.sleep(1)
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}")
@@ -151,8 +148,6 @@ def generate_comment_all():
logger.error(f"保存失败!请先关闭 Excel 文件:{config['excel_file']}")
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
import traceback
logger.error(traceback.format_exc())
@@ -306,9 +301,6 @@ def generate_report():
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
# 打印详细报错位置,方便调试
import traceback
logger.error(traceback.format_exc())
@@ -320,9 +312,8 @@ def batch_convert_folder(folder_path):
【推荐】批量转换文件夹下的所有 PPT (只启动一次 PowerPoint速度快)
已修复多线程 CoInitialize 报错,并适配 GUI 日志
"""
# 【核心修复 1】子线程初始化 COM 组件
# 子线程初始化 COM 组件
pythoncom.CoInitialize()
try:
folder_path = os.path.abspath(folder_path)
if not os.path.exists(folder_path):
@@ -429,6 +420,4 @@ def generate_zodiac():
logger.error(f"找不到文件 {config.get('excel_file')}")
except Exception as e:
logger.error(f"程序运行出错: {str(e)}")
import traceback
logger.error(traceback.format_exc())

View File

@@ -1,4 +1,6 @@
import os
from PIL import Image, ExifTags
import io
def find_image_path(folder, base_filename):
@@ -26,3 +28,39 @@ def find_image_path(folder, base_filename):
return full_path
return None
def get_corrected_image_stream(img_path):
"""
读取图片,根据 EXIF 信息修正旋转方向,并返回 BytesIO 对象。
这样不需要修改原文件,直接在内存中处理。
"""
image = Image.open(img_path)
# 获取 EXIF 数据
try:
for orientation in ExifTags.TAGS.keys():
if ExifTags.TAGS[orientation] == 'Orientation':
break
exif = dict(image._getexif().items())
if exif[orientation] == 3:
image = image.rotate(180, expand=True)
elif exif[orientation] == 6:
image = image.rotate(270, expand=True)
elif exif[orientation] == 8:
image = image.rotate(90, expand=True)
except (AttributeError, KeyError, IndexError):
# 如果图片没有 EXIF 数据或不需要旋转,则忽略
pass
# 将处理后的图片保存到内存流中
image_stream = io.BytesIO()
# 注意:保存时保持原格式,如果是 PNG 等无 EXIF 的格式会自动处理
img_format = image.format if image.format else 'JPEG'
image.save(image_stream, format=img_format)
image_stream.seek(0) # 指针回到开头
return image_stream

27
utils/log_handler.py Normal file
View File

@@ -0,0 +1,27 @@
import queue
import re
from loguru import logger
# 全局日志队列
log_queue = queue.Queue()
def ansi_cleaner(text):
"""去除 loguru 输出中的颜色代码,防止在 UI 显示乱码"""
ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
return ansi_escape.sub("", text)
def queue_sink(message):
"""Loguru 的回调函数"""
clean_msg = ansi_cleaner(message)
log_queue.put(clean_msg)
def setup_logging():
"""配置日志系统"""
# 清除默认的控制台输出,防止干扰
logger.remove()
# 添加队列输出 (给 UI 用)
logger.add(queue_sink, format="{time:HH:mm:ss} | {level: <8} | {message}", level="INFO")
# 添加文件输出 (给开发者排查用)
# logger.add("logs/app_runtime.log", rotation="1 MB", encoding="utf-8", level="DEBUG")

View File

@@ -3,7 +3,10 @@
# ==========================================
import os
from loguru import logger
from utils.font_utils import is_font_available
from utils.image_utils import get_corrected_image_stream
def replace_text_in_slide(prs, slide_index, placeholder, text):
@@ -94,18 +97,36 @@ def replace_text_in_slide(prs, slide_index, placeholder, text):
def replace_picture(prs, slide_index, placeholder, img_path):
"""在指定幻灯片中替换指定占位符的图片"""
"""在指定幻灯片中替换指定占位符的图片(包含自动旋转修复)"""
if not os.path.exists(img_path):
print(f"警告: 图片路径不存在 {img_path}")
logger.warning(f"警告: 图片路径不存在 {img_path}")
return
slide = prs.slides[slide_index]
sp_tree = slide.shapes._spTree
target_shape = None
target_index = -1
# 1. 先找到目标形状和它的索引
for i, shape in enumerate(slide.shapes):
if shape.name == placeholder:
left, top, width, height = shape.left, shape.top, shape.width, shape.height
sp_tree.remove(shape._element)
new_shape = slide.shapes.add_picture(img_path, left, top, width, height)
sp_tree.insert(i, new_shape._element)
target_shape = shape
target_index = i
break
if target_shape:
# 获取原位置信息
left, top, width, height = target_shape.left, target_shape.top, target_shape.width, target_shape.height
# 2. 获取修正后的图片流
img_stream = get_corrected_image_stream(img_path)
# 3. 移除旧形状
sp_tree.remove(target_shape._element)
# 4. 插入新图片 (使用流而不是路径)
new_shape = slide.shapes.add_picture(img_stream, left, top, width, height)
# 5. 恢复层级位置 (z-order)
sp_tree.insert(target_index, new_shape._element)

2
uv.lock generated
View File

@@ -121,6 +121,7 @@ dependencies = [
{ name = "openpyxl" },
{ name = "pandas" },
{ name = "pandas-stubs" },
{ name = "pillow" },
{ name = "python-pptx" },
{ name = "pywin32" },
{ name = "rich" },
@@ -136,6 +137,7 @@ requires-dist = [
{ name = "openpyxl", specifier = ">=3.1.5" },
{ name = "pandas", specifier = ">=2.3.3" },
{ name = "pandas-stubs", specifier = "==2.3.3.251201" },
{ name = "pillow", specifier = ">=12.0.0" },
{ name = "python-pptx", specifier = ">=1.0.2" },
{ name = "pywin32", specifier = ">=311" },
{ name = "rich", specifier = ">=14.2.0" },