fix:修复一些BUG
This commit is contained in:
12
.idea/growth_report.iml
generated
12
.idea/growth_report.iml
generated
@@ -1,12 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="uv (growth_report) (2)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<module version="4">
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
@@ -18,4 +11,7 @@
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="TestRunnerService">
|
||||
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
||||
</component>
|
||||
</module>
|
||||
4
.idea/misc.xml
generated
4
.idea/misc.xml
generated
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="uv (growth_report) (2)" />
|
||||
<option name="sdkName" value="uv (growth_report)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="uv (growth_report) (2)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="uv(growth_report)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/growth_report.iml" filepath="$PROJECT_DIR$/.idea/growth_report.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
95
.idea/workspace.xml
generated
95
.idea/workspace.xml
generated
@@ -4,19 +4,46 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="09d6a6cb-782b-4f40-bb60-5703c43250ec" name="更改" comment="fix:更新代码开源协议" />
|
||||
<list default="true" id="e258c58a-2a5f-4fad-9d39-8dc186b6b5a7" name="更改" comment="">
|
||||
<change afterPath="$PROJECT_DIR$/script/setup.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/growth_report.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/growth_report.iml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/modules.xml" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config.env.toml" beforeDir="false" afterPath="$PROJECT_DIR$/config.env.toml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/config/config.py" beforeDir="false" afterPath="$PROJECT_DIR$/config/config.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/data/names.xlsx" beforeDir="false" afterPath="$PROJECT_DIR$/data/names.xlsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/main.pyw" beforeDir="false" afterPath="$PROJECT_DIR$/main.pyw" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/old/main.py" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/public/icon.ico" beforeDir="false" afterPath="$PROJECT_DIR$/public/icon.ico" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/ui/app_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/app_window.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/utils/font_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/font_utils.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/utils/generate_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/generate_utils.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/utils/growt_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/growt_utils.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/utils/pdf_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/pdf_utils.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/uv.lock" beforeDir="false" />
|
||||
</list>
|
||||
<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="FileTemplateManagerImpl">
|
||||
<option name="RECENT_TEMPLATES">
|
||||
<list>
|
||||
<option value="Python Script" />
|
||||
</list>
|
||||
</option>
|
||||
</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="ProjectColorInfo">{
|
||||
"customColor": "",
|
||||
"associatedIndex": 0
|
||||
}</component>
|
||||
<component name="ProjectId" id="36mY8RowM4y8kcGPt6V9WQiPGN9" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
@@ -24,12 +51,12 @@
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"Python.UI.executor": "Run",
|
||||
"Python.main.pyw.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",
|
||||
"last_opened_file_path": "D:/working/tools/growth_report/public",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
@@ -39,8 +66,13 @@
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="D:\working\tools\growth_report\public" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunManager">
|
||||
<configuration name="UI" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||
<configuration name="main.pyw" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||
<module name="growth_report" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
@@ -54,7 +86,7 @@
|
||||
<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="SCRIPT_NAME" value="$PROJECT_DIR$/main.pyw" />
|
||||
<option name="PARAMETERS" value="" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
@@ -65,56 +97,27 @@
|
||||
</configuration>
|
||||
<recent_temporary>
|
||||
<list>
|
||||
<item itemvalue="Python.UI" />
|
||||
<item itemvalue="Python.main.pyw" />
|
||||
</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>
|
||||
<changelist id="e258c58a-2a5f-4fad-9d39-8dc186b6b5a7" name="更改" comment="" />
|
||||
<created>1765613055475</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1765460141811</updated>
|
||||
<workItem from="1765460142948" duration="1948000" />
|
||||
<updated>1765613055475</updated>
|
||||
<workItem from="1765613057798" duration="372000" />
|
||||
<workItem from="1765613448098" duration="48000" />
|
||||
<workItem from="1765613503892" duration="10202000" />
|
||||
</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$" />
|
||||
<SUITE FILE_PATH="coverage/growth_report$main_pyw.coverage" NAME="main.pyw 覆盖结果" MODIFIED="1765626060279" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -4,9 +4,9 @@ source_file = "大班幼儿学期发展报告.pptx"
|
||||
# 输出文件夹
|
||||
output_folder = "output"
|
||||
# Excel数据文件路径
|
||||
excel_file = "data/names.xlsx"
|
||||
excel_file = "names.xlsx"
|
||||
# 图片资源文件夹
|
||||
image_folder = "data/images"
|
||||
image_folder = "images"
|
||||
# 字体文件夹
|
||||
fonts_dir = "fonts"
|
||||
|
||||
|
||||
112
config/config.py
112
config/config.py
@@ -12,30 +12,46 @@ except ImportError:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_main_path():
|
||||
def get_base_dir():
|
||||
"""
|
||||
获取程序运行的根目录
|
||||
兼容:
|
||||
1. PyInstaller 打包后的 .exe 环境
|
||||
2. 开发环境 (假设此脚本在子文件夹中,如 utils/)
|
||||
获取程序运行的基准目录 (即 EXE 所在的目录 或 开发环境的项目根目录)
|
||||
用于确定 output_folder 等需要写入的路径。
|
||||
"""
|
||||
if getattr(sys, "frozen", False):
|
||||
# --- 情况 A: 打包后的 exe ---
|
||||
# exe 就在根目录下,直接取 exe 所在目录
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包环境: EXE 所在目录
|
||||
return os.path.dirname(sys.executable)
|
||||
else:
|
||||
# --- 情况 B: 开发环境 (.py) ---
|
||||
# 1. 获取当前脚本的绝对路径 (例如: .../MyProject/utils/config_loader.py)
|
||||
current_file_path = os.path.abspath(__file__)
|
||||
# 开发环境: 项目根目录 (假设此脚本在 utils/ 文件夹中,需要向上两级)
|
||||
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# 2. 获取当前脚本所在的文件夹 (例如: .../MyProject/utils)
|
||||
current_dir = os.path.dirname(current_file_path)
|
||||
|
||||
# 3. 【关键修改】再往上一层,获取项目根目录 (例如: .../MyProject)
|
||||
# 如果你的脚本藏得更深,就再套一层 os.path.dirname
|
||||
project_root = os.path.dirname(current_dir)
|
||||
def get_resource_path(relative_path):
|
||||
"""
|
||||
智能路径获取:
|
||||
1. 优先检查 EXE 旁边是否有该文件 (外部资源)。
|
||||
2. 如果没有,则使用 EXE 内部打包的资源 (内部资源)。
|
||||
"""
|
||||
# 1. 获取外部基准路径
|
||||
base_path = get_base_dir()
|
||||
|
||||
return project_root
|
||||
# 拼接外部路径
|
||||
external_path = os.path.join(base_path, relative_path)
|
||||
|
||||
# 如果外部文件存在,直接返回 (优先使用用户修改过的文件)
|
||||
if os.path.exists(external_path):
|
||||
return external_path
|
||||
|
||||
# 2. 如果外部不存在,且处于打包环境,则回退到内部临时目录 (sys._MEIPASS)
|
||||
if getattr(sys, 'frozen', False):
|
||||
# sys._MEIPASS 是 PyInstaller 解压临时文件的目录
|
||||
internal_path = os.path.join(sys._MEIPASS, relative_path)
|
||||
|
||||
if os.path.exists(internal_path):
|
||||
return internal_path
|
||||
|
||||
# 3. 默认返回外部路径
|
||||
# (如果都没找到,让报错信息指向外部路径,提示用户文件缺失)
|
||||
return external_path
|
||||
|
||||
|
||||
# ==========================================
|
||||
@@ -44,32 +60,58 @@ def get_main_path():
|
||||
def load_config(config_filename="config.toml"):
|
||||
"""读取 TOML 配置文件"""
|
||||
|
||||
# 1. 先获取正确的根目录
|
||||
main_dir = get_main_path()
|
||||
|
||||
# 2. 拼接配置文件的绝对路径 (防止在不同目录下运行脚本时找不到配置文件)
|
||||
config_path = os.path.join(main_dir, config_filename)
|
||||
# 1. 智能获取配置文件路径
|
||||
# (优先找 EXE 旁边的 config.toml,找不到则用打包在里面的)
|
||||
config_path = get_resource_path(config_filename)
|
||||
|
||||
if not os.path.exists(config_path):
|
||||
print(f"错误: 在路径 {main_dir} 下找不到配置文件 {config_filename}")
|
||||
print(f"尝试寻找的完整路径是: {config_path}")
|
||||
print(f"错误: 找不到配置文件 {config_filename}")
|
||||
print(f"尝试寻找的路径是: {config_path}")
|
||||
# 如果是打包环境,提示用户可能需要把 config.toml 复制出来
|
||||
if getattr(sys, 'frozen', False):
|
||||
print("提示: 请确保 config.toml 位于程序同级目录下,或已正确打包。")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(config_path, "rb") as f:
|
||||
data = toml.load(f)
|
||||
|
||||
# 获取基准目录(用于 output_folder)
|
||||
base_dir = get_base_dir()
|
||||
|
||||
# 将 TOML 的层级结构映射回扁平结构
|
||||
# 关键点:所有的 os.path.join 都必须基于 main_dir (项目根目录)
|
||||
# ⚠️ 注意:
|
||||
# - 读取类文件 (模板, Excel, 图片, 字体) 使用 get_resource_path (支持内外回退)
|
||||
# - 写入类文件夹 (output_folder) 使用 os.path.join(base_dir, ...) (必须在外部)
|
||||
|
||||
config = {
|
||||
"root_path": main_dir, # 方便调试,把根目录也存进去
|
||||
"source_file": os.path.join(
|
||||
main_dir, "templates", data["paths"]["source_file"]
|
||||
"root_path": base_dir,
|
||||
|
||||
# --- 资源文件 (使用智能路径) ---
|
||||
|
||||
# 假设 config.toml 里写的是 "report_template.pptx",文件在 templates 文件夹下
|
||||
"source_file": get_resource_path(
|
||||
os.path.join("templates", data["paths"]["source_file"])
|
||||
),
|
||||
"output_folder": os.path.join(main_dir, data["paths"]["output_folder"]),
|
||||
"excel_file": os.path.join(main_dir, data["paths"]["excel_file"]),
|
||||
"image_folder": os.path.join(main_dir, data["paths"]["image_folder"]),
|
||||
"fonts_dir": os.path.join(main_dir, data["paths"]["fonts_dir"]),
|
||||
|
||||
# 假设 config.toml 里写的是 "names.xlsx",文件在 data 文件夹下
|
||||
# 如果 config.toml 里写的是 "data/names.xlsx",则不需要 os.path.join("data", ...)
|
||||
"excel_file": get_resource_path(
|
||||
os.path.join("data", data["paths"]["excel_file"])
|
||||
),
|
||||
|
||||
"image_folder": get_resource_path(
|
||||
os.path.join("data", data["paths"]["image_folder"])
|
||||
),
|
||||
|
||||
"fonts_dir": get_resource_path(
|
||||
os.path.join(data["paths"]["fonts_dir"])
|
||||
),
|
||||
|
||||
# --- 输出文件夹 (必须强制在外部,不能指向临时目录) ---
|
||||
"output_folder": os.path.join(base_dir, data["paths"]["output_folder"]),
|
||||
|
||||
# --- 其他配置 ---
|
||||
"class_name": data["class_info"]["class_name"],
|
||||
"teachers": data["class_info"]["teachers"],
|
||||
"default_comment": data["defaults"].get("default_comment", "暂无评语"),
|
||||
@@ -77,6 +119,12 @@ def load_config(config_filename="config.toml"):
|
||||
"ai": data["ai"],
|
||||
}
|
||||
return config
|
||||
|
||||
except KeyError as e:
|
||||
print(f"配置文件格式错误,缺少键值: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"读取配置文件出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
BIN
data/names.xlsx
BIN
data/names.xlsx
Binary file not shown.
86
main.pyw
86
main.pyw
@@ -1,23 +1,81 @@
|
||||
import sys
|
||||
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("正在启动应用程序...")
|
||||
from ui.app_window import ReportApp
|
||||
from utils.log_handler import setup_logging
|
||||
|
||||
# 2. 启动 UI
|
||||
root = tk.Tk()
|
||||
# 全局变量,用于判断日志是否已初始化
|
||||
LOGGING_INITIALIZED = False
|
||||
|
||||
# 这一行可以设置图标 (如果有 icon.ico 文件)
|
||||
# root.iconbitmap("icon.ico")
|
||||
|
||||
app = ReportApp(root)
|
||||
# --- 全局错误处理 ---
|
||||
def handle_exception(exc_type, exc_value, exc_traceback):
|
||||
"""
|
||||
捕获未被 try/except 块处理的全局异常(如线程崩溃)。
|
||||
"""
|
||||
if exc_type is KeyboardInterrupt:
|
||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
return
|
||||
|
||||
# 尝试使用 loguru 记录
|
||||
if LOGGING_INITIALIZED:
|
||||
logger.error("捕获到未处理的全局异常:", exc_info=(exc_type, exc_value, exc_traceback))
|
||||
else:
|
||||
# 如果日志系统未初始化,直接打印到标准错误流,确保用户看到
|
||||
print("FATAL ERROR (Log Not Initialized):", file=sys.stderr)
|
||||
import traceback
|
||||
traceback.print_exception(exc_type, exc_value, exc_traceback, file=sys.stderr)
|
||||
|
||||
|
||||
sys.excepthook = handle_exception
|
||||
|
||||
|
||||
# --------------------
|
||||
|
||||
|
||||
def create_main_window():
|
||||
global LOGGING_INITIALIZED
|
||||
|
||||
# 顶级 try 块,捕获日志初始化阶段的错误
|
||||
try:
|
||||
# 1. 初始化日志
|
||||
setup_logging()
|
||||
LOGGING_INITIALIZED = True
|
||||
logger.info("正在启动应用程序...")
|
||||
|
||||
# 2. 启动 UI
|
||||
root = tk.Tk()
|
||||
|
||||
# 这一行可以设置图标 (如果有 icon.ico 文件)
|
||||
# root.iconbitmap(os.path.join(os.path.dirname(__file__), "public", "icon.ico"))
|
||||
|
||||
# 确保 ReportApp 实例化时不会出现路径错误
|
||||
app = ReportApp(root)
|
||||
|
||||
# 3. 进入主循环
|
||||
root.mainloop()
|
||||
|
||||
except Exception as e:
|
||||
# 如果日志系统已启动,使用 logger 记录
|
||||
if LOGGING_INITIALIZED:
|
||||
logger.error(f"应用程序启动/主循环出错: {e}", exc_info=True)
|
||||
else:
|
||||
# 如果日志系统未初始化,直接打印到控制台
|
||||
print(f"FATAL STARTUP ERROR: {e}", file=sys.stderr)
|
||||
import traceback
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
|
||||
# 确保窗口被销毁
|
||||
if 'root' in locals() and root:
|
||||
root.destroy()
|
||||
|
||||
# 非窗口模式下,在启动错误时等待用户查看
|
||||
if not getattr(sys, 'frozen', False) or not any(arg in sys.argv for arg in ('--windowed', '-w')):
|
||||
input("按任意键退出...")
|
||||
sys.exit(1)
|
||||
|
||||
# 3. 进入主循环
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
create_main_window()
|
||||
415
old/main.py
415
old/main.py
@@ -1,415 +0,0 @@
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
from pptx import Presentation
|
||||
from pptx.util import Pt
|
||||
|
||||
|
||||
def get_system_fonts():
|
||||
"""获取系统中可用的字体列表"""
|
||||
fonts = set()
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
# 读取Windows字体目录
|
||||
system_fonts_dir = Path(os.environ['WINDIR']) / 'Fonts'
|
||||
user_fonts_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts'
|
||||
|
||||
# 检查系统字体目录
|
||||
if system_fonts_dir.exists():
|
||||
for font_file in system_fonts_dir.glob('*.ttf'):
|
||||
fonts.add(font_file.stem)
|
||||
for font_file in system_fonts_dir.glob('*.ttc'):
|
||||
fonts.add(font_file.stem)
|
||||
for font_file in system_fonts_dir.glob('*.otf'):
|
||||
fonts.add(font_file.stem)
|
||||
|
||||
# 检查用户字体目录
|
||||
if user_fonts_dir.exists():
|
||||
for font_file in user_fonts_dir.glob('*.ttf'):
|
||||
fonts.add(font_file.stem)
|
||||
for font_file in user_fonts_dir.glob('*.ttc'):
|
||||
fonts.add(font_file.stem)
|
||||
for font_file in user_fonts_dir.glob('*.otf'):
|
||||
fonts.add(font_file.stem)
|
||||
|
||||
except Exception as e:
|
||||
print(f"读取系统字体时出错: {e}")
|
||||
# 备选方案:返回常见字体
|
||||
fonts = {"微软雅黑", "宋体", "黑体", "楷体", "仿宋", "Arial", "Times New Roman", "Courier New",
|
||||
"Microsoft YaHei"}
|
||||
return fonts
|
||||
|
||||
|
||||
def is_font_available(font_name):
|
||||
"""检查字体是否在系统中可用"""
|
||||
system_fonts = get_system_fonts()
|
||||
# 检查字体名称的多种可能形式
|
||||
check_names = [font_name, font_name.replace(" ", ""), font_name.replace("-", ""), font_name.lower(),
|
||||
font_name.upper()]
|
||||
for name in check_names:
|
||||
if name in system_fonts:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def install_fonts_from_directory(fonts_dir="fonts"):
|
||||
"""从指定目录安装字体到系统"""
|
||||
if platform.system() != "Windows":
|
||||
print("字体安装功能目前仅支持Windows系统")
|
||||
return False
|
||||
|
||||
# 获取系统字体目录
|
||||
user_fonts_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts'
|
||||
|
||||
# 优先使用用户字体目录(不需要管理员权限)
|
||||
target_font_dir = user_fonts_dir
|
||||
|
||||
# 创建目标目录(如果不存在)
|
||||
target_font_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 检查字体目录是否存在
|
||||
if not os.path.exists(fonts_dir):
|
||||
print(f"字体目录 {fonts_dir} 不存在,请创建该目录并将字体文件放入")
|
||||
return False
|
||||
|
||||
# 遍历字体目录中的字体文件
|
||||
font_extensions = ['.ttf', '.ttc', '.otf', '.fon']
|
||||
font_files = []
|
||||
for ext in font_extensions:
|
||||
font_files.extend(Path(fonts_dir).glob(f'*{ext}'))
|
||||
|
||||
if not font_files:
|
||||
print(f"在 {fonts_dir} 目录中未找到字体文件 (.ttf, .ttc, .otf, .fon)")
|
||||
return False
|
||||
|
||||
# 安装字体文件
|
||||
installed_fonts = []
|
||||
for font_file in font_files:
|
||||
try:
|
||||
# 复制字体文件到系统字体目录
|
||||
target_path = target_font_dir / font_file.name
|
||||
if not target_path.exists(): # 避免重复安装
|
||||
shutil.copy2(font_file, target_path)
|
||||
print(f"已安装字体: {font_file.name}")
|
||||
installed_fonts.append(font_file.name)
|
||||
else:
|
||||
pass
|
||||
# print(f"字体已存在: {font_file.name}") # 减少日志输出
|
||||
except Exception as e:
|
||||
print(f"安装字体 {font_file.name} 时出错: {str(e)}")
|
||||
continue
|
||||
|
||||
if installed_fonts:
|
||||
print(f"共安装了 {len(installed_fonts)} 个新字体文件")
|
||||
print("注意:新安装的字体可能需要重启Python环境后才能在PowerPoint中使用")
|
||||
return True
|
||||
else:
|
||||
print("没有安装新字体文件,可能已存在于系统中")
|
||||
return False
|
||||
|
||||
|
||||
def replace_text_in_slide(prs, slide_index, placeholder, text):
|
||||
"""
|
||||
在指定幻灯片中替换指定占位符的文本,并保持原有格式。
|
||||
:param prs: Presentation 对象
|
||||
:param slide_index: 要操作的幻灯片索引(从0开始)
|
||||
:param placeholder: 占位符名称 (shape.name)
|
||||
:param text: 要替换的文本
|
||||
"""
|
||||
slide = prs.slides[slide_index]
|
||||
for shape in slide.shapes:
|
||||
if shape.name == placeholder:
|
||||
if not shape.has_text_frame:
|
||||
continue
|
||||
|
||||
# --- 1. 保存原有格式信息 ---
|
||||
original_paragraph_formats = []
|
||||
|
||||
for paragraph in shape.text_frame.paragraphs:
|
||||
paragraph_format = {
|
||||
'alignment': paragraph.alignment,
|
||||
'space_before': getattr(paragraph, 'space_before', None),
|
||||
'space_after': getattr(paragraph, 'space_after', None),
|
||||
'line_spacing': getattr(paragraph, 'line_spacing', None),
|
||||
# [修复] 补充读取缩进属性,防止后面恢复时 KeyError
|
||||
'left_indent': getattr(paragraph, 'left_indent', None),
|
||||
'right_indent': getattr(paragraph, 'right_indent', None),
|
||||
'first_line_indent': getattr(paragraph, 'first_line_indent', None),
|
||||
'font_info': []
|
||||
}
|
||||
|
||||
for run in paragraph.runs:
|
||||
run_format = {
|
||||
'font_name': run.font.name,
|
||||
'font_size': run.font.size,
|
||||
'bold': run.font.bold,
|
||||
'italic': run.font.italic,
|
||||
'underline': run.font.underline,
|
||||
'color': run.font.color,
|
||||
'character_space': getattr(run.font, 'space', None),
|
||||
'all_caps': getattr(run.font, 'all_caps', None),
|
||||
'small_caps': getattr(run.font, 'small_caps', None)
|
||||
}
|
||||
paragraph_format['font_info'].append(run_format)
|
||||
|
||||
original_paragraph_formats.append(paragraph_format)
|
||||
|
||||
# --- 2. 设置新文本内容 ---
|
||||
shape.text = text
|
||||
|
||||
# --- 3. 恢复原有格式 ---
|
||||
for i, paragraph in enumerate(shape.text_frame.paragraphs):
|
||||
# 如果新文本段落数超过原格式数量,使用最后一个格式或默认格式
|
||||
orig_idx = i if i < len(original_paragraph_formats) else -1
|
||||
if not original_paragraph_formats: break # 防止空列表
|
||||
|
||||
original_para = original_paragraph_formats[orig_idx]
|
||||
|
||||
# 恢复段落格式
|
||||
if original_para['alignment'] is not None:
|
||||
paragraph.alignment = original_para['alignment']
|
||||
if original_para['space_before'] is not None:
|
||||
paragraph.space_before = original_para['space_before']
|
||||
if original_para['space_after'] is not None:
|
||||
paragraph.space_after = original_para['space_after']
|
||||
if original_para['line_spacing'] is not None:
|
||||
paragraph.line_spacing = original_para['line_spacing']
|
||||
if original_para['left_indent'] is not None:
|
||||
paragraph.left_indent = original_para['left_indent']
|
||||
if original_para['right_indent'] is not None:
|
||||
paragraph.right_indent = original_para['right_indent']
|
||||
if original_para['first_line_indent'] is not None:
|
||||
paragraph.first_line_indent = original_para['first_line_indent']
|
||||
|
||||
# 恢复字体格式 (尽量应用到所有 runs)
|
||||
# 注意:shape.text = text 会把所有内容变成一个 run,但也可能有多个
|
||||
for j, run in enumerate(paragraph.runs):
|
||||
# 通常取第一个run的格式,或者按顺序取
|
||||
font_idx = j if j < len(original_para['font_info']) else 0
|
||||
if not original_para['font_info']: break
|
||||
|
||||
original_font = original_para['font_info'][font_idx]
|
||||
|
||||
# 字体名称
|
||||
if original_font['font_name'] is not None:
|
||||
font_name = original_font['font_name']
|
||||
if is_font_available(font_name):
|
||||
run.font.name = font_name
|
||||
else:
|
||||
run.font.name = "微软雅黑"
|
||||
# print(f"警告: 字体 '{font_name}' 不可用,已替换")
|
||||
|
||||
# 其他属性
|
||||
if original_font['font_size'] is not None:
|
||||
run.font.size = original_font['font_size']
|
||||
if original_font['bold'] is not None:
|
||||
run.font.bold = original_font['bold']
|
||||
if original_font['italic'] is not None:
|
||||
run.font.italic = original_font['italic']
|
||||
if original_font['underline'] is not None:
|
||||
run.font.underline = original_font['underline']
|
||||
|
||||
if original_font['character_space'] is not None:
|
||||
try:
|
||||
run.font.space = original_font['character_space']
|
||||
except:
|
||||
pass
|
||||
|
||||
if original_font['all_caps'] is not None:
|
||||
run.font.all_caps = original_font['all_caps']
|
||||
|
||||
# 颜色处理
|
||||
if original_font['color'] is not None:
|
||||
try:
|
||||
# 仅当它是RGB类型时才复制,主题色可能导致报错
|
||||
if hasattr(original_font['color'], 'rgb'):
|
||||
run.font.color.rgb = original_font['color'].rgb
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def replace_picture(prs, slide_index, placeholder, img_path):
|
||||
"""
|
||||
在指定幻灯片中替换指定占位符的图片。
|
||||
:param prs: Presentation 对象
|
||||
:param slide_index: 要操作的幻灯片索引(从0开始)
|
||||
:param placeholder: 占位符名称
|
||||
:param img_path: 要替换的图片路径
|
||||
"""
|
||||
if not os.path.exists(img_path):
|
||||
print(f"警告: 图片路径不存在 {img_path}")
|
||||
return
|
||||
|
||||
slide = prs.slides[slide_index]
|
||||
sp_tree = slide.shapes._spTree
|
||||
|
||||
# 先找到要替换的形状及其索引位置
|
||||
for i, shape in enumerate(slide.shapes):
|
||||
if shape.name == placeholder:
|
||||
# 保存旧形状的位置信息
|
||||
left = shape.left
|
||||
top = shape.top
|
||||
width = shape.width
|
||||
height = 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)
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# --- 1. 资源准备 ---
|
||||
# 安装字体(如果存在fonts目录)
|
||||
if install_fonts_from_directory("../fonts"):
|
||||
print("等待系统识别新安装的字体...")
|
||||
time.sleep(2)
|
||||
|
||||
source_file = r"../templates/大班幼儿学期发展报告.pptx"
|
||||
output_folder = "output"
|
||||
excel_file = os.path.join("../data/names.xlsx")
|
||||
image_folder = os.path.join("../data/images")
|
||||
|
||||
# 创建输出文件夹
|
||||
os.makedirs(output_folder, exist_ok=True)
|
||||
|
||||
# 检查源文件是否存在
|
||||
if not os.path.exists(source_file):
|
||||
print(f"错误: 找不到模版文件 {source_file}")
|
||||
sys.exit(1)
|
||||
if not os.path.exists(excel_file):
|
||||
print(f"错误: 找不到数据文件 {excel_file}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# --- 2. 定义辅助函数 (显式传入 prs) ---
|
||||
def replace_one_page(current_prs, name, class_name):
|
||||
"""替换第一页信息"""
|
||||
replace_text_in_slide(current_prs, 0, "name", name)
|
||||
replace_text_in_slide(current_prs, 0, "class", class_name)
|
||||
|
||||
|
||||
def replace_two_page(current_prs, comments, teacher_name):
|
||||
"""替换第二页信息"""
|
||||
replace_text_in_slide(current_prs, 1, "comments", comments)
|
||||
replace_text_in_slide(current_prs, 1, "teacher_name", teacher_name)
|
||||
|
||||
|
||||
def replace_three_page(current_prs, name, english_name, sex, birthday, zodiac, friend, hobby, game, food, me_image):
|
||||
"""替换第三页信息
|
||||
:param current_prs: Presentation对象,当前演示文稿
|
||||
:param name: 学生姓名
|
||||
:param english_name: 学生英文名
|
||||
:param sex: 性别
|
||||
:param birthday: 生日
|
||||
:param zodiac: 生肖
|
||||
:param friend: 好朋友
|
||||
:param hobby: 爱好
|
||||
:param game: 爱玩的游戏
|
||||
:param food: 爱吃的食物
|
||||
:param me_image: 自己的照片
|
||||
"""
|
||||
replace_text_in_slide(current_prs, 2, "name", name)
|
||||
replace_text_in_slide(current_prs, 2, "english_name", english_name)
|
||||
replace_text_in_slide(current_prs, 2, "sex", sex)
|
||||
replace_text_in_slide(current_prs, 2, "birthday", birthday)
|
||||
replace_text_in_slide(current_prs, 2, "zodiac", zodiac)
|
||||
replace_text_in_slide(current_prs, 2, "friend", friend)
|
||||
replace_text_in_slide(current_prs, 2, "hobby", hobby)
|
||||
replace_text_in_slide(current_prs, 2, "game", game)
|
||||
replace_text_in_slide(current_prs, 2, "food", food)
|
||||
replace_picture(current_prs, 2, "me_image", me_image)
|
||||
|
||||
|
||||
def replace_four_page(current_prs, class_image):
|
||||
"""替换第四页信息"""
|
||||
replace_picture(current_prs, 3, "class_image", class_image)
|
||||
|
||||
|
||||
def replace_five_page(current_prs, image1, image2):
|
||||
"""替换第五页信息"""
|
||||
replace_picture(current_prs, 4, "image1", image1)
|
||||
replace_picture(current_prs, 4, "image2", image2)
|
||||
|
||||
|
||||
# --- 3. 读取数据并处理 ---
|
||||
try:
|
||||
df = pd.read_excel(excel_file, sheet_name="Sheet1")
|
||||
datas = df[["姓名", "英文名", "性别", "生日", "属相", "我的好朋友", "我的爱好", "喜欢的游戏", "喜欢吃的食物",
|
||||
"评价"]].values.tolist()
|
||||
|
||||
class_name = "K4D"
|
||||
teacher_name = ["简蜜", "王敏千", "李玉香"]
|
||||
|
||||
print(f"开始处理,共 {len(datas)} 位学生...")
|
||||
|
||||
# --- 4. 循环处理 ---
|
||||
for i, (name, english_name, sex, birthday, zodiac, friend, hobby, game, food, comments) in enumerate(datas):
|
||||
|
||||
# 班级图片
|
||||
|
||||
print(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
|
||||
|
||||
# [修复] 每次循环重新加载模版,保证文件干净
|
||||
prs = Presentation(source_file)
|
||||
|
||||
# 替换第一页内容
|
||||
replace_one_page(prs, name, class_name)
|
||||
|
||||
# 替换第二页内容
|
||||
teacher_names = " ".join(teacher_name)
|
||||
replace_two_page(prs, comments, teacher_names)
|
||||
|
||||
# 替换第三页内容
|
||||
student_image_folder = os.path.join(image_folder, name)
|
||||
if os.path.exists(student_image_folder):
|
||||
me_image = os.path.join(student_image_folder, "me_image.jpg")
|
||||
replace_three_page(prs, name, english_name, sex, birthday, zodiac, friend, hobby, game, food, me_image)
|
||||
else:
|
||||
print(f"错误: 学生图片文件夹不存在 {student_image_folder}")
|
||||
|
||||
# 替换第四页内容
|
||||
class_image = os.path.join(image_folder, class_name + ".jpg")
|
||||
if os.path.exists(class_image):
|
||||
replace_four_page(prs, class_image)
|
||||
else:
|
||||
print(f"错误: 班级图片文件不存在 {class_image}")
|
||||
continue
|
||||
|
||||
# 替换第五页内容
|
||||
if os.path.exists(student_image_folder):
|
||||
image1 = os.path.join(student_image_folder, "1.jpg")
|
||||
image2 = os.path.join(student_image_folder, "1.jpg")
|
||||
replace_five_page(prs, image1, image2)
|
||||
else:
|
||||
print(f"错误: 学生图片文件夹不存在 {student_image_folder}")
|
||||
|
||||
# 获取文件拓展名
|
||||
file_ext = os.path.splitext(source_file)[1]
|
||||
safe_name = str(name).strip() # 去除可能存在的空格
|
||||
new_filename = f"{class_name} {safe_name} 幼儿成长报告{file_ext}"
|
||||
output_path = os.path.join(output_folder, new_filename)
|
||||
|
||||
# 保存
|
||||
try:
|
||||
prs.save(output_path)
|
||||
except PermissionError:
|
||||
print(f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。")
|
||||
|
||||
print("\n所有报告生成完毕!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"程序运行出错: {str(e)}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
BIN
public/icon.ico
BIN
public/icon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 277 KiB After Width: | Height: | Size: 74 KiB |
109
script/setup.py
Normal file
109
script/setup.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# setup.py
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
|
||||
def copy_resources():
|
||||
"""
|
||||
将资源文件从项目根目录复制到 dist 文件夹中,
|
||||
以便用户可以直接在 exe 旁边修改这些文件。
|
||||
"""
|
||||
# 1. 定义路径
|
||||
# setup.py 所在的目录 (script/)
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# 项目根目录 (script/ 的上一级)
|
||||
project_root = os.path.dirname(current_dir)
|
||||
# 输出目录 (script/dist)
|
||||
dist_dir = os.path.join(current_dir, "dist")
|
||||
|
||||
print(f"\n--- 正在复制外部资源到 {dist_dir} ---")
|
||||
|
||||
if not os.path.exists(dist_dir):
|
||||
print("错误: dist 文件夹不存在,请先运行打包。")
|
||||
return
|
||||
|
||||
# 2. 定义要复制的资源清单
|
||||
# 格式: (源路径相对root, 目标文件夹相对dist)
|
||||
# 如果目标是根目录,用 "" 表示
|
||||
resources_to_copy = [
|
||||
("config.toml", ""), # 复制 config.toml 到 dist/
|
||||
("fonts", "fonts"), # 复制 fonts 文件夹 到 dist/fonts
|
||||
("data", "data"), # 复制 data 文件夹 到 dist/data
|
||||
("templates", "templates"), # 复制 templates 文件夹 到 dist/templates
|
||||
("public", "public"), # 复制 public 文件夹 到 dist/public
|
||||
]
|
||||
|
||||
for src_name, dest_name in resources_to_copy:
|
||||
src_path = os.path.join(project_root, src_name)
|
||||
dest_path = os.path.join(dist_dir, dest_name)
|
||||
|
||||
try:
|
||||
if os.path.isfile(src_path):
|
||||
# --- 复制文件 ---
|
||||
shutil.copy2(src_path, dest_path) # copy2 保留文件元数据
|
||||
print(f"✅ 已复制文件: {src_name}")
|
||||
|
||||
elif os.path.isdir(src_path):
|
||||
# --- 复制文件夹 ---
|
||||
# dirs_exist_ok=True 允许覆盖已存在的目录 (Python 3.8+)
|
||||
shutil.copytree(src_path, dest_path, dirs_exist_ok=True)
|
||||
print(f"✅ 已复制目录: {src_name}")
|
||||
|
||||
else:
|
||||
print(f"⚠️ 警告: 源文件不存在,跳过: {src_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 复制失败 {src_name}: {e}")
|
||||
|
||||
|
||||
def build_exe():
|
||||
"""使用 PyInstaller 打包 main_app.py"""
|
||||
|
||||
# --- 内部资源 (打入包内的资源) ---
|
||||
# 即使我们在外部复制了一份,为了保证 exe 独立运行(万一外部文件被删),
|
||||
# 建议依然保留这些作为“默认出厂设置”打入包内。
|
||||
resource_paths = [
|
||||
"--add-data=../config.toml:.",
|
||||
"--add-data=../fonts:fonts",
|
||||
"--add-data=../data:data",
|
||||
"--add-data=../templates:templates",
|
||||
"--add-data=../public:public",
|
||||
]
|
||||
|
||||
try:
|
||||
command = [
|
||||
sys.executable, "-m", "PyInstaller",
|
||||
"--onefile",
|
||||
"--windowed", # 调试阶段建议先注释掉,确认无误后再开启
|
||||
"--name=尚城幼儿园幼儿学期发展报告",
|
||||
"--icon=../public/icon.ico",
|
||||
"../main.pyw"
|
||||
]
|
||||
|
||||
# 添加资源参数
|
||||
command.extend(resource_paths)
|
||||
|
||||
print("--- 开始打包 (PyInstaller) ---")
|
||||
# 运行 PyInstaller
|
||||
subprocess.run(command, check=True, cwd=os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
print("\n--- PyInstaller 打包完成!---")
|
||||
|
||||
# === 执行资源复制 ===
|
||||
copy_resources()
|
||||
|
||||
print(f"\n🎉 全部完成!请查看 'dist' 文件夹。")
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"\n--- 打包失败 ---")
|
||||
print(f"命令执行出错: {e}")
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"\n--- 打包失败 ---")
|
||||
print("错误:找不到 PyInstaller。")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build_exe()
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext, messagebox, filedialog
|
||||
import threading
|
||||
@@ -6,6 +8,7 @@ import sys
|
||||
|
||||
from loguru import logger
|
||||
from config.config import load_config
|
||||
from utils.font_utils import install_fonts_from_directory
|
||||
from utils.log_handler import log_queue
|
||||
|
||||
# 导入业务逻辑
|
||||
@@ -20,6 +23,7 @@ from utils.file_utils import export_templates_folder, initialize_project, export
|
||||
|
||||
config = load_config("config.toml")
|
||||
|
||||
|
||||
class ReportApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
@@ -30,7 +34,24 @@ class ReportApp:
|
||||
self.stop_event = threading.Event()
|
||||
self.is_running = False
|
||||
|
||||
self._setup_ui()
|
||||
# 尝试初始化 UI
|
||||
try:
|
||||
self._setup_ui()
|
||||
except Exception as e:
|
||||
logger.critical(f"UI 初始化失败: {e}", exc_info=True)
|
||||
messagebox.showerror("致命错误", f"界面初始化失败,请检查日志。\n错误: {e}")
|
||||
self.root.destroy()
|
||||
sys.exit(1)
|
||||
|
||||
# 尝试初始化项目资源
|
||||
try:
|
||||
self.init_project()
|
||||
except Exception as e:
|
||||
logger.critical(f"项目资源初始化失败: {e}", exc_info=True)
|
||||
messagebox.showerror("致命错误", f"项目资源初始化失败,请检查日志。\n错误: {e}")
|
||||
self.root.destroy()
|
||||
sys.exit(1)
|
||||
|
||||
self._start_log_polling()
|
||||
|
||||
def _setup_ui(self):
|
||||
@@ -48,9 +69,21 @@ class ReportApp:
|
||||
ttk.Label(header, text="By 寒寒 | 这里的每一份评语都充满爱意", font=("微软雅黑", 9), foreground="gray").pack()
|
||||
|
||||
# 2. 功能区容器
|
||||
main_content = ttk.Frame(self.root, padding=10)
|
||||
main_content = ttk.Frame(self.root, padding="10 15 10 5")
|
||||
main_content.pack(fill=tk.X)
|
||||
|
||||
# === 进度条区域 ===
|
||||
progress_frame = ttk.Frame(self.root, padding="10 15 10 5")
|
||||
progress_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# 进度条 Label
|
||||
self.progress_label = ttk.Label(progress_frame, text="⛳ 任务进度: 待命", font=("微软雅黑", 10))
|
||||
self.progress_label.pack(fill=tk.X, pady=(0, 2))
|
||||
|
||||
# 进度条
|
||||
self.progressbar = ttk.Progressbar(progress_frame, orient="horizontal", mode="determinate")
|
||||
self.progressbar.pack(fill=tk.X, expand=True)
|
||||
|
||||
# === A组: 核心功能 ===
|
||||
self._create_btn_group(main_content, "🛠️ 核心功能", [
|
||||
("📁 生成图片路径", lambda: self.run_task(generate_template)),
|
||||
@@ -74,7 +107,7 @@ class ReportApp:
|
||||
], columns=3, special_styles={"⛔ 停止当前任务": "Stop.TButton"})
|
||||
|
||||
# 3. 日志区
|
||||
log_frame = ttk.LabelFrame(self.root, text="📝 系统实时日志", padding=10)
|
||||
log_frame = ttk.LabelFrame(self.root, text="📝 系统实时日志", padding="10 15 10 5")
|
||||
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))
|
||||
@@ -106,6 +139,15 @@ class ReportApp:
|
||||
break
|
||||
self.root.after(100, self._start_log_polling)
|
||||
|
||||
def init_project(self):
|
||||
# 1. 资源准备
|
||||
if install_fonts_from_directory(config["fonts_dir"]):
|
||||
logger.info("等待系统识别新安装的字体...")
|
||||
time.sleep(2)
|
||||
# 2. 创建输出文件夹
|
||||
os.makedirs(config["output_folder"], exist_ok=True)
|
||||
logger.success("项目初始化完成.....")
|
||||
|
||||
# --- 任务运行核心逻辑 ---
|
||||
def run_task(self, target_func, *args, **kwargs):
|
||||
if self.is_running:
|
||||
@@ -115,6 +157,9 @@ class ReportApp:
|
||||
self.stop_event.clear()
|
||||
self.is_running = True
|
||||
|
||||
# 将进度更新方法作为参数传入
|
||||
kwargs['progress_callback'] = self.update_progress
|
||||
|
||||
def thread_worker():
|
||||
try:
|
||||
# 尝试传入 stop_event
|
||||
@@ -129,7 +174,8 @@ class ReportApp:
|
||||
logger.error(traceback.format_exc())
|
||||
finally:
|
||||
self.is_running = False
|
||||
logger.info("--- 就绪 ---")
|
||||
logger.info("系统准备就绪.....")
|
||||
self.reset_progress() # 重置进度条
|
||||
|
||||
threading.Thread(target=thread_worker, daemon=True).start()
|
||||
|
||||
@@ -159,3 +205,32 @@ class ReportApp:
|
||||
return
|
||||
self.root.destroy()
|
||||
sys.exit()
|
||||
|
||||
# --- 进度条更新(实现线程安全更新) ---
|
||||
def update_progress(self, current, total, task_name="任务"):
|
||||
"""
|
||||
线程安全地更新进度条和标签
|
||||
:param current: 当前完成的项目数
|
||||
:param total: 总项目数
|
||||
:param task_name: 当前任务名称
|
||||
"""
|
||||
if total <= 0:
|
||||
# 重置进度条
|
||||
self.progressbar['value'] = 0
|
||||
self.progress_label.config(text=f"任务进度: {task_name} 完成或待命")
|
||||
return
|
||||
|
||||
percentage = int((current / total) * 100)
|
||||
display_text = f"{task_name}: {current}/{total} ({percentage}%)"
|
||||
|
||||
# 使用 after 确保在主线程中更新 UI
|
||||
self.root.after(0, self._set_progress_ui, percentage, display_text)
|
||||
|
||||
def _set_progress_ui(self, percentage, display_text):
|
||||
"""实际更新 UI 的私有方法"""
|
||||
self.progressbar['value'] = percentage
|
||||
self.progress_label.config(text=display_text)
|
||||
|
||||
def reset_progress(self):
|
||||
"""任务结束后重置进度条"""
|
||||
self.root.after(0, self._set_progress_ui, 0, "任务进度: 就绪")
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from config.config import load_config
|
||||
|
||||
config = load_config("config.toml")
|
||||
|
||||
|
||||
def get_system_fonts():
|
||||
"""获取系统中可用的字体列表"""
|
||||
|
||||
@@ -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 ---
|
||||
@@ -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())
|
||||
|
||||
@@ -11,6 +11,8 @@ console = Console()
|
||||
# 1. 配置区域 (Configuration)
|
||||
# ==========================================
|
||||
config = load_config("config.toml")
|
||||
|
||||
|
||||
def replace_one_page(prs, name, class_name):
|
||||
"""替换第一页信息"""
|
||||
replace_text_in_slide(prs, 0, "name", name)
|
||||
@@ -23,7 +25,7 @@ def replace_two_page(prs, comments, teacher_name):
|
||||
replace_text_in_slide(prs, 1, "teacher_name", teacher_name)
|
||||
|
||||
|
||||
def replace_three_page(prs, info_dict, me_image):
|
||||
def replace_three_page(prs, info_dict, me_image=None):
|
||||
"""替换第三页信息"""
|
||||
# 使用字典解包传递多个字段,减少参数数量
|
||||
fields = ["name", "english_name", "sex", "birthday", "zodiac", "friend", "hobby", "game", "food"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import os
|
||||
|
||||
import pythoncom
|
||||
import comtypes.client
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ def ppt_to_pdf_single(ppt_path, pdf_path=None):
|
||||
:param ppt_path: PPT 文件路径
|
||||
:param pdf_path: PDF 输出路径 (可选,默认同名)
|
||||
"""
|
||||
# 子线程初始化 COM 组件
|
||||
pythoncom.CoInitialize()
|
||||
ppt_path = os.path.abspath(ppt_path) # COM 接口必须使用绝对路径
|
||||
|
||||
if pdf_path is None:
|
||||
@@ -40,3 +42,4 @@ def ppt_to_pdf_single(ppt_path, pdf_path=None):
|
||||
finally:
|
||||
if powerpoint:
|
||||
powerpoint.Quit()
|
||||
pythoncom.CoUninitialize()
|
||||
|
||||
44
uv.lock
generated
44
uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
@@ -289,7 +289,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.1.3"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -301,28 +301,28 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "uuid-utils" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/0b/6f/9e959821df556cf32d57d26aa7b86905a63a91e6e9e744994f8045fe2d70/langchain_core-1.1.3.tar.gz", hash = "sha256:ff0bc5e6e701c4d6fe00c73c4feae5c993a7a8e0b91f0a1d07015277d4634275" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/6f/ae/2041e14c8781b1696bb161b78152f1523b5128bdb16c95199632eb034c6f/langchain_core-1.2.0.tar.gz", hash = "sha256:e3f6450ae88505ec509ffa6f5c7ba3fa377a35b5d73f307b3ba1fc5aeb8a95b1" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/58/41/6db768d4b208a33b4f09d5415e617d489f68167bb5dd27f87c7a49d13caf/langchain_core-1.1.3-py3-none-any.whl", hash = "sha256:e06efbf55bf7c7e4fcffc2f5b0a39a855176df16b02077add063534d7dabb740" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/dd/bb/ddac30cba0c246f7c15d81851311a23dc1455b6e908f624e71fa3b82b3d1/langchain_core-1.2.0-py3-none-any.whl", hash = "sha256:ed95ee5cbab0d1188c91ad230bb6a513427bc1e2ed5a8329075ab24412cd7727" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-openai"
|
||||
version = "1.1.1"
|
||||
version = "1.1.3"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
{ name = "openai" },
|
||||
{ name = "tiktoken" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/05/38/c6517187ea5f0db6d682083116a020409b01eef0547b4330542c117cd25d/langchain_openai-1.1.1.tar.gz", hash = "sha256:72aa7262854104e0b2794522a90c49353c79d0132caa1be27ef253852685d5e7" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/93/67/6126a1c645b34388edee917473e51b2158812af1fcc8fedc23a330478329/langchain_openai-1.1.3.tar.gz", hash = "sha256:d8be85e4d1151258e1d2ed29349179ad971499115948b01364c2a1ab0474b1bf" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/1d/95/d65d3e187cd717baeb62988ae90a995f93564657f67518fb775af4090880/langchain_openai-1.1.1-py3-none-any.whl", hash = "sha256:69b9be37e6ae3372b4d937cb9365cf55c0c59b5f7870e7507cb7d802a8b98b30" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/d1/11/2b3b4973495fc5f0456ed5c8c88a6ded7ca34c8608c72faafa87088acf5a/langchain_openai-1.1.3-py3-none-any.whl", hash = "sha256:58945d9e87c1ab3a91549c3f3744c6c9571511cdc3cf875b8842aaec5b3e32a6" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -332,9 +332,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "xxhash" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d6/3c/af87902d300c1f467165558c8966d8b1e1f896dace271d3f35a410a5c26a/langgraph-1.0.4.tar.gz", hash = "sha256:86d08e25d7244340f59c5200fa69fdd11066aa999b3164b531e2a20036fac156" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/7d/47/28f4d4d33d88f69de26f7a54065961ac0c662cec2479b36a2db081ef5cb6/langgraph-1.0.5.tar.gz", hash = "sha256:7f6ae59622386b60fe9fa0ad4c53f42016b668455ed604329e7dc7904adbf3f8" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/14/52/4eb25a3f60399da34ba34adff1b3e324cf0d87eb7a08cebf1882a9b5e0d5/langgraph-1.0.4-py3-none-any.whl", hash = "sha256:b1a835ceb0a8d69b9db48075e1939e28b1ad70ee23fa3fa8f90149904778bacf" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/23/1b/e318ee76e42d28f515d87356ac5bd7a7acc8bad3b8f54ee377bef62e1cbf/langgraph-1.0.5-py3-none-any.whl", hash = "sha256:b4cfd173dca3c389735b47228ad8b295e6f7b3df779aba3a1e0c23871f81281e" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -365,20 +365,20 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.2.15"
|
||||
version = "0.3.0"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "orjson" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/71/46/a0bc5914e4a418ad5e8558b19bccd6f0baf56d0c674d6d65a0acf4f22590/langgraph_sdk-0.2.15.tar.gz", hash = "sha256:8faaafe2c1193b89f782dd66c591060cd67862aa6aaf283749b7846f331d5334" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/2b/1b/f328afb4f24f6e18333ff357d9580a3bb5b133ff2c7aae34fef7f5b87f31/langgraph_sdk-0.3.0.tar.gz", hash = "sha256:4145bc3c34feae227ae918341f66d3ba7d1499722c1ef4a8aae5ea828897d1d4" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/6b/c9/bf2bff18f85bb7973fa5280838580049574bd7649c36e3dd346c49304997/langgraph_sdk-0.2.15-py3-none-any.whl", hash = "sha256:746566a5d89aa47160eccc17d71682a78771c754126f6c235a68353d61ed7462" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/69/48/ee4d7afb3c3d38bd2ebe51a4d37f1ed7f1058dd242f35994b562203067aa/langgraph_sdk-0.3.0-py3-none-any.whl", hash = "sha256:c1ade483fba17ae354ee920e4779042b18d5aba875f2a858ba569f62f628f26f" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.4.58"
|
||||
version = "0.4.59"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
@@ -390,9 +390,9 @@ dependencies = [
|
||||
{ name = "uuid-utils" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a8/c1/a2f04ccbd5ef44b9bd41fb0174fff539b4d41f273ad3649cd656e87b44b2/langsmith-0.4.58.tar.gz", hash = "sha256:ec72e2fcfbbf90b827212c75acac8fc58bb921726f4326633bd774b50133eea3" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/61/71/d61524c3205bde7ec90423d997cf1a228d8adf2811110ec91ed40c8e8a34/langsmith-0.4.59.tar.gz", hash = "sha256:6b143214c2303dafb29ab12dcd05ac50bdfc60dac01c6e0450e50cee1d2415e0" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/9d/69/1a48e6b6c4d1e244a272b36e029fd27a70d37c8f4f002367c3d78f6c8e1d/langsmith-0.4.58-py3-none-any.whl", hash = "sha256:f6999b2f7956f030c32a80c1d631b49677f35648560207a09973c8d5ab74279d" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/63/54/4577ef9424debea2fa08af338489d593276520d2e2f8950575d292be612c/langsmith-0.4.59-py3-none-any.whl", hash = "sha256:97c26399286441a7b7b06b912e2801420fbbf3a049787e609d49dc975ab10bc5" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -557,7 +557,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.9.0"
|
||||
version = "2.11.0"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -569,9 +569,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/09/48/516290f38745cc1e72856f50e8afed4a7f9ac396a5a18f39e892ab89dfc2/openai-2.9.0.tar.gz", hash = "sha256:b52ec65727fc8f1eed2fbc86c8eac0998900c7ef63aa2eb5c24b69717c56fa5f" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f4/8c/aa6aea6072f985ace9d6515046b9088ff00c157f9654da0c7b1e129d9506/openai-2.11.0.tar.gz", hash = "sha256:b3da01d92eda31524930b6ec9d7167c535e843918d7ba8a76b1c38f1104f321e" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/59/fd/ae2da789cd923dd033c99b8d544071a827c92046b150db01cfa5cea5b3fd/openai-2.9.0-py3-none-any.whl", hash = "sha256:0d168a490fbb45630ad508a6f3022013c155a68fd708069b6a1a01a5e8f0ffad" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/e5/f1/d9251b565fce9f8daeb45611e3e0d2f7f248429e40908dcee3b6fe1b5944/openai-2.11.0-py3-none-any.whl", hash = "sha256:21189da44d2e3d027b08c7a920ba4454b8b7d6d30ae7e64d9de11dbe946d4faa" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1260,11 +1260,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.1"
|
||||
version = "2.6.2"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/5e/1d/0f3a93cca1ac5e8287842ed4eebbd0f7a991315089b1a0b01c7788aa7b63/urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/bc/56/190ceb8cb10511b730b564fb1e0293fa468363dbad26145c34928a60cb0c/urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b" },
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user