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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="PYTHON_MODULE" version="4">
|
<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>
|
|
||||||
<component name="PyDocumentationSettings">
|
<component name="PyDocumentationSettings">
|
||||||
<option name="format" value="PLAIN" />
|
<option name="format" value="PLAIN" />
|
||||||
<option name="myDocStringFormat" value="Plain" />
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
@@ -18,4 +11,7 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
<component name="TestRunnerService">
|
||||||
|
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
||||||
|
</component>
|
||||||
</module>
|
</module>
|
||||||
4
.idea/misc.xml
generated
4
.idea/misc.xml
generated
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="Black">
|
<component name="Black">
|
||||||
<option name="sdkName" value="uv (growth_report) (2)" />
|
<option name="sdkName" value="uv (growth_report)" />
|
||||||
</component>
|
</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>
|
</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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
95
.idea/workspace.xml
generated
95
.idea/workspace.xml
generated
@@ -4,19 +4,46 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<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="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="FileTemplateManagerImpl">
|
||||||
|
<option name="RECENT_TEMPLATES">
|
||||||
|
<list>
|
||||||
|
<option value="Python Script" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectColorInfo"><![CDATA[{
|
<component name="ProjectColorInfo">{
|
||||||
"associatedIndex": 0
|
"customColor": "",
|
||||||
}]]></component>
|
"associatedIndex": 0
|
||||||
<component name="ProjectId" id="36hYCM0j8RdgslpqW2LFtFj8NEK" />
|
}</component>
|
||||||
|
<component name="ProjectId" id="36mY8RowM4y8kcGPt6V9WQiPGN9" />
|
||||||
<component name="ProjectViewState">
|
<component name="ProjectViewState">
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
@@ -24,12 +51,12 @@
|
|||||||
<component name="PropertiesComponent"><![CDATA[{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
"Python.UI.executor": "Run",
|
"Python.main.pyw.executor": "Run",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"git-widget-placeholder": "master",
|
"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.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
@@ -39,8 +66,13 @@
|
|||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}]]></component>
|
}]]></component>
|
||||||
|
<component name="RecentsManager">
|
||||||
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
|
<recent name="D:\working\tools\growth_report\public" />
|
||||||
|
</key>
|
||||||
|
</component>
|
||||||
<component name="RunManager">
|
<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" />
|
<module name="growth_report" />
|
||||||
<option name="ENV_FILES" value="" />
|
<option name="ENV_FILES" value="" />
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
@@ -54,7 +86,7 @@
|
|||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
<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="PARAMETERS" value="" />
|
||||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
<option name="EMULATE_TERMINAL" value="false" />
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
@@ -65,56 +97,27 @@
|
|||||||
</configuration>
|
</configuration>
|
||||||
<recent_temporary>
|
<recent_temporary>
|
||||||
<list>
|
<list>
|
||||||
<item itemvalue="Python.UI" />
|
<item itemvalue="Python.main.pyw" />
|
||||||
</list>
|
</list>
|
||||||
</recent_temporary>
|
</recent_temporary>
|
||||||
</component>
|
</component>
|
||||||
<component name="TaskManager">
|
<component name="TaskManager">
|
||||||
<task active="true" id="Default" summary="默认任务">
|
<task active="true" id="Default" summary="默认任务">
|
||||||
<changelist id="09d6a6cb-782b-4f40-bb60-5703c43250ec" name="更改" comment="" />
|
<changelist id="e258c58a-2a5f-4fad-9d39-8dc186b6b5a7" name="更改" comment="" />
|
||||||
<created>1765460141811</created>
|
<created>1765613055475</created>
|
||||||
<option name="number" value="Default" />
|
<option name="number" value="Default" />
|
||||||
<option name="presentableId" value="Default" />
|
<option name="presentableId" value="Default" />
|
||||||
<updated>1765460141811</updated>
|
<updated>1765613055475</updated>
|
||||||
<workItem from="1765460142948" duration="1948000" />
|
<workItem from="1765613057798" duration="372000" />
|
||||||
|
<workItem from="1765613448098" duration="48000" />
|
||||||
|
<workItem from="1765613503892" duration="10202000" />
|
||||||
</task>
|
</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 />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
<option name="version" value="3" />
|
<option name="version" value="3" />
|
||||||
</component>
|
</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">
|
<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>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -4,9 +4,9 @@ source_file = "大班幼儿学期发展报告.pptx"
|
|||||||
# 输出文件夹
|
# 输出文件夹
|
||||||
output_folder = "output"
|
output_folder = "output"
|
||||||
# Excel数据文件路径
|
# Excel数据文件路径
|
||||||
excel_file = "data/names.xlsx"
|
excel_file = "names.xlsx"
|
||||||
# 图片资源文件夹
|
# 图片资源文件夹
|
||||||
image_folder = "data/images"
|
image_folder = "images"
|
||||||
# 字体文件夹
|
# 字体文件夹
|
||||||
fonts_dir = "fonts"
|
fonts_dir = "fonts"
|
||||||
|
|
||||||
|
|||||||
112
config/config.py
112
config/config.py
@@ -12,30 +12,46 @@ except ImportError:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def get_main_path():
|
def get_base_dir():
|
||||||
"""
|
"""
|
||||||
获取程序运行的根目录
|
获取程序运行的基准目录 (即 EXE 所在的目录 或 开发环境的项目根目录)
|
||||||
兼容:
|
用于确定 output_folder 等需要写入的路径。
|
||||||
1. PyInstaller 打包后的 .exe 环境
|
|
||||||
2. 开发环境 (假设此脚本在子文件夹中,如 utils/)
|
|
||||||
"""
|
"""
|
||||||
if getattr(sys, "frozen", False):
|
if getattr(sys, 'frozen', False):
|
||||||
# --- 情况 A: 打包后的 exe ---
|
# 打包环境: EXE 所在目录
|
||||||
# exe 就在根目录下,直接取 exe 所在目录
|
|
||||||
return os.path.dirname(sys.executable)
|
return os.path.dirname(sys.executable)
|
||||||
else:
|
else:
|
||||||
# --- 情况 B: 开发环境 (.py) ---
|
# 开发环境: 项目根目录 (假设此脚本在 utils/ 文件夹中,需要向上两级)
|
||||||
# 1. 获取当前脚本的绝对路径 (例如: .../MyProject/utils/config_loader.py)
|
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
current_file_path = os.path.abspath(__file__)
|
|
||||||
|
|
||||||
# 2. 获取当前脚本所在的文件夹 (例如: .../MyProject/utils)
|
|
||||||
current_dir = os.path.dirname(current_file_path)
|
|
||||||
|
|
||||||
# 3. 【关键修改】再往上一层,获取项目根目录 (例如: .../MyProject)
|
def get_resource_path(relative_path):
|
||||||
# 如果你的脚本藏得更深,就再套一层 os.path.dirname
|
"""
|
||||||
project_root = os.path.dirname(current_dir)
|
智能路径获取:
|
||||||
|
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"):
|
def load_config(config_filename="config.toml"):
|
||||||
"""读取 TOML 配置文件"""
|
"""读取 TOML 配置文件"""
|
||||||
|
|
||||||
# 1. 先获取正确的根目录
|
# 1. 智能获取配置文件路径
|
||||||
main_dir = get_main_path()
|
# (优先找 EXE 旁边的 config.toml,找不到则用打包在里面的)
|
||||||
|
config_path = get_resource_path(config_filename)
|
||||||
# 2. 拼接配置文件的绝对路径 (防止在不同目录下运行脚本时找不到配置文件)
|
|
||||||
config_path = os.path.join(main_dir, config_filename)
|
|
||||||
|
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
print(f"错误: 在路径 {main_dir} 下找不到配置文件 {config_filename}")
|
print(f"错误: 找不到配置文件 {config_filename}")
|
||||||
print(f"尝试寻找的完整路径是: {config_path}")
|
print(f"尝试寻找的路径是: {config_path}")
|
||||||
|
# 如果是打包环境,提示用户可能需要把 config.toml 复制出来
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
print("提示: 请确保 config.toml 位于程序同级目录下,或已正确打包。")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(config_path, "rb") as f:
|
with open(config_path, "rb") as f:
|
||||||
data = toml.load(f)
|
data = toml.load(f)
|
||||||
|
|
||||||
|
# 获取基准目录(用于 output_folder)
|
||||||
|
base_dir = get_base_dir()
|
||||||
|
|
||||||
# 将 TOML 的层级结构映射回扁平结构
|
# 将 TOML 的层级结构映射回扁平结构
|
||||||
# 关键点:所有的 os.path.join 都必须基于 main_dir (项目根目录)
|
# ⚠️ 注意:
|
||||||
|
# - 读取类文件 (模板, Excel, 图片, 字体) 使用 get_resource_path (支持内外回退)
|
||||||
|
# - 写入类文件夹 (output_folder) 使用 os.path.join(base_dir, ...) (必须在外部)
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"root_path": main_dir, # 方便调试,把根目录也存进去
|
"root_path": base_dir,
|
||||||
"source_file": os.path.join(
|
|
||||||
main_dir, "templates", data["paths"]["source_file"]
|
# --- 资源文件 (使用智能路径) ---
|
||||||
|
|
||||||
|
# 假设 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"]),
|
# 假设 config.toml 里写的是 "names.xlsx",文件在 data 文件夹下
|
||||||
"image_folder": os.path.join(main_dir, data["paths"]["image_folder"]),
|
# 如果 config.toml 里写的是 "data/names.xlsx",则不需要 os.path.join("data", ...)
|
||||||
"fonts_dir": os.path.join(main_dir, data["paths"]["fonts_dir"]),
|
"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"],
|
"class_name": data["class_info"]["class_name"],
|
||||||
"teachers": data["class_info"]["teachers"],
|
"teachers": data["class_info"]["teachers"],
|
||||||
"default_comment": data["defaults"].get("default_comment", "暂无评语"),
|
"default_comment": data["defaults"].get("default_comment", "暂无评语"),
|
||||||
@@ -77,6 +119,12 @@ def load_config(config_filename="config.toml"):
|
|||||||
"ai": data["ai"],
|
"ai": data["ai"],
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
except KeyError as e:
|
||||||
|
print(f"配置文件格式错误,缺少键值: {e}")
|
||||||
|
sys.exit(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"读取配置文件出错: {e}")
|
print(f"读取配置文件出错: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
BIN
data/names.xlsx
BIN
data/names.xlsx
Binary file not shown.
92
main.pyw
92
main.pyw
@@ -1,23 +1,81 @@
|
|||||||
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from utils.log_handler import setup_logging
|
|
||||||
from ui.app_window import ReportApp
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
def main():
|
from ui.app_window import ReportApp
|
||||||
# 1. 初始化日志
|
from utils.log_handler import setup_logging
|
||||||
setup_logging()
|
|
||||||
logger.info("正在启动应用程序...")
|
# 全局变量,用于判断日志是否已初始化
|
||||||
|
LOGGING_INITIALIZED = False
|
||||||
|
|
||||||
|
|
||||||
|
# --- 全局错误处理 ---
|
||||||
|
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)
|
||||||
|
|
||||||
# 2. 启动 UI
|
|
||||||
root = tk.Tk()
|
|
||||||
|
|
||||||
# 这一行可以设置图标 (如果有 icon.ico 文件)
|
|
||||||
# root.iconbitmap("icon.ico")
|
|
||||||
|
|
||||||
app = ReportApp(root)
|
|
||||||
|
|
||||||
# 3. 进入主循环
|
|
||||||
root.mainloop()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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
|
import tkinter as tk
|
||||||
from tkinter import ttk, scrolledtext, messagebox, filedialog
|
from tkinter import ttk, scrolledtext, messagebox, filedialog
|
||||||
import threading
|
import threading
|
||||||
@@ -6,6 +8,7 @@ import sys
|
|||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from config.config import load_config
|
from config.config import load_config
|
||||||
|
from utils.font_utils import install_fonts_from_directory
|
||||||
from utils.log_handler import log_queue
|
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")
|
config = load_config("config.toml")
|
||||||
|
|
||||||
|
|
||||||
class ReportApp:
|
class ReportApp:
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
@@ -30,7 +34,24 @@ class ReportApp:
|
|||||||
self.stop_event = threading.Event()
|
self.stop_event = threading.Event()
|
||||||
self.is_running = False
|
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()
|
self._start_log_polling()
|
||||||
|
|
||||||
def _setup_ui(self):
|
def _setup_ui(self):
|
||||||
@@ -48,9 +69,21 @@ class ReportApp:
|
|||||||
ttk.Label(header, text="By 寒寒 | 这里的每一份评语都充满爱意", font=("微软雅黑", 9), foreground="gray").pack()
|
ttk.Label(header, text="By 寒寒 | 这里的每一份评语都充满爱意", font=("微软雅黑", 9), foreground="gray").pack()
|
||||||
|
|
||||||
# 2. 功能区容器
|
# 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)
|
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组: 核心功能 ===
|
# === A组: 核心功能 ===
|
||||||
self._create_btn_group(main_content, "🛠️ 核心功能", [
|
self._create_btn_group(main_content, "🛠️ 核心功能", [
|
||||||
("📁 生成图片路径", lambda: self.run_task(generate_template)),
|
("📁 生成图片路径", lambda: self.run_task(generate_template)),
|
||||||
@@ -74,9 +107,9 @@ class ReportApp:
|
|||||||
], columns=3, special_styles={"⛔ 停止当前任务": "Stop.TButton"})
|
], columns=3, special_styles={"⛔ 停止当前任务": "Stop.TButton"})
|
||||||
|
|
||||||
# 3. 日志区
|
# 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))
|
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 = scrolledtext.ScrolledText(log_frame, height=10, state="disabled", font=("Consolas", 9))
|
||||||
self.log_text.pack(fill=tk.BOTH, expand=True)
|
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
@@ -90,7 +123,7 @@ class ReportApp:
|
|||||||
btn = ttk.Button(frame, text=text, command=func, style=style)
|
btn = ttk.Button(frame, text=text, command=func, style=style)
|
||||||
r, c = divmod(i, columns)
|
r, c = divmod(i, columns)
|
||||||
btn.grid(row=r, column=c, padx=5, pady=5, sticky="ew")
|
btn.grid(row=r, column=c, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
for i in range(columns):
|
for i in range(columns):
|
||||||
frame.columnconfigure(i, weight=1)
|
frame.columnconfigure(i, weight=1)
|
||||||
|
|
||||||
@@ -106,6 +139,15 @@ class ReportApp:
|
|||||||
break
|
break
|
||||||
self.root.after(100, self._start_log_polling)
|
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):
|
def run_task(self, target_func, *args, **kwargs):
|
||||||
if self.is_running:
|
if self.is_running:
|
||||||
@@ -115,6 +157,9 @@ class ReportApp:
|
|||||||
self.stop_event.clear()
|
self.stop_event.clear()
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
|
|
||||||
|
# 将进度更新方法作为参数传入
|
||||||
|
kwargs['progress_callback'] = self.update_progress
|
||||||
|
|
||||||
def thread_worker():
|
def thread_worker():
|
||||||
try:
|
try:
|
||||||
# 尝试传入 stop_event
|
# 尝试传入 stop_event
|
||||||
@@ -129,7 +174,8 @@ class ReportApp:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
finally:
|
finally:
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
logger.info("--- 就绪 ---")
|
logger.info("系统准备就绪.....")
|
||||||
|
self.reset_progress() # 重置进度条
|
||||||
|
|
||||||
threading.Thread(target=thread_worker, daemon=True).start()
|
threading.Thread(target=thread_worker, daemon=True).start()
|
||||||
|
|
||||||
@@ -158,4 +204,33 @@ class ReportApp:
|
|||||||
messagebox.showwarning("提示", "请先停止任务")
|
messagebox.showwarning("提示", "请先停止任务")
|
||||||
return
|
return
|
||||||
self.root.destroy()
|
self.root.destroy()
|
||||||
sys.exit()
|
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 os
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
from config.config import load_config
|
||||||
|
|
||||||
|
config = load_config("config.toml")
|
||||||
|
|
||||||
|
|
||||||
def get_system_fonts():
|
def get_system_fonts():
|
||||||
"""获取系统中可用的字体列表"""
|
"""获取系统中可用的字体列表"""
|
||||||
@@ -74,4 +79,4 @@ def install_fonts_from_directory(fonts_dir="fonts"):
|
|||||||
if installed_count > 0:
|
if installed_count > 0:
|
||||||
logger.success(f"共安装了 {installed_count} 个新字体文件,建议重启Python环境")
|
logger.success(f"共安装了 {installed_count} 个新字体文件,建议重启Python环境")
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import pythoncom
|
import pythoncom
|
||||||
|
|
||||||
@@ -12,7 +13,6 @@ import comtypes.client
|
|||||||
from config.config import load_config
|
from config.config import load_config
|
||||||
from utils.agent_utils import generate_comment
|
from utils.agent_utils import generate_comment
|
||||||
from utils.file_utils import check_file_exists
|
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.image_utils import find_image_path
|
||||||
from utils.zodiac_utils import calculate_zodiac
|
from utils.zodiac_utils import calculate_zodiac
|
||||||
from utils.growt_utils import (
|
from utils.growt_utils import (
|
||||||
@@ -23,7 +23,6 @@ from utils.growt_utils import (
|
|||||||
replace_five_page,
|
replace_five_page,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# 如果你之前没有全局定义 console,这里定义一个
|
# 如果你之前没有全局定义 console,这里定义一个
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
@@ -36,26 +35,33 @@ config = load_config("config.toml")
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# 1. 生成模板(根据names.xlsx文件生成名字图片文件夹)
|
# 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:
|
try:
|
||||||
# 2. 读取数据
|
# 1. 读取数据
|
||||||
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
|
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
|
||||||
|
# 2. 获取姓名数据
|
||||||
# --- 修改点开始 ---
|
|
||||||
# 直接读取 "姓名" 这一列,不使用列表包裹列名,这样得到的是一维数据
|
|
||||||
datas = df["姓名"].values.tolist()
|
datas = df["姓名"].values.tolist()
|
||||||
# --- 修改点结束 ---
|
total_count = len(datas)
|
||||||
|
logger.info(f"开始生成学生模版文件,共 {total_count} 位学生...")
|
||||||
logger.info(f"开始生成学生模版文件,共 {len(datas)} 位学生...")
|
|
||||||
|
|
||||||
# 3. 循环处理
|
# 3. 循环处理
|
||||||
# 此时 name 就是字符串 '张三',而不是列表 ['张三']
|
|
||||||
for i, name in enumerate(datas):
|
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 是字符串且去除了空格 (增加健壮性)
|
||||||
name = str(name).strip()
|
name = str(name).strip()
|
||||||
|
# 生成学生图片的文件夹
|
||||||
student_folder = os.path.join(config["image_folder"], name)
|
student_folder = os.path.join(config["image_folder"], name)
|
||||||
|
|
||||||
if os.path.exists(student_folder):
|
if os.path.exists(student_folder):
|
||||||
@@ -63,7 +69,10 @@ def generate_template():
|
|||||||
else:
|
else:
|
||||||
logger.info(f"正在生成学生图片文件夹 {student_folder}")
|
logger.info(f"正在生成学生图片文件夹 {student_folder}")
|
||||||
os.makedirs(student_folder, exist_ok=True)
|
os.makedirs(student_folder, exist_ok=True)
|
||||||
|
# 更新进度条为100%
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(total_count, total_count, "生成学生图片文件夹")
|
||||||
|
logger.success("✅ 所有学生模版文件已生成完毕")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"程序运行出错: {str(e)}")
|
logger.error(f"程序运行出错: {str(e)}")
|
||||||
# 打印详细报错位置,方便调试
|
# 打印详细报错位置,方便调试
|
||||||
@@ -73,7 +82,12 @@ def generate_template():
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# 2. 生成评语(根据names.xlsx文件生成评价)
|
# 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:
|
try:
|
||||||
# 1. 读取数据
|
# 1. 读取数据
|
||||||
excel_path = config["excel_file"]
|
excel_path = config["excel_file"]
|
||||||
@@ -85,23 +99,27 @@ def generate_comment_all():
|
|||||||
|
|
||||||
# 获取学生数据行数
|
# 获取学生数据行数
|
||||||
total_count = len(df)
|
total_count = len(df)
|
||||||
logger.info(f"开始生成学生评语,共 {total_count} 位学生...")
|
logger.info(f"开始生成学生评语,共 {len(df)} 位学生...")
|
||||||
# 强制将“评价”列转换为 object 类型
|
# 强制将“评价”列转换为 object 类型
|
||||||
df["评价"] = df["评价"].astype("object")
|
df["评价"] = df["评价"].astype("object")
|
||||||
# --- 遍历 DataFrame 的索引 (index) ---
|
# --- 遍历 DataFrame 的索引 (index) ---
|
||||||
# 这样我们可以通过索引 i 精准地把数据写回某一行
|
|
||||||
for i in df.index:
|
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):
|
if pd.isna(name):
|
||||||
continue # 跳过空行
|
continue
|
||||||
name = str(name).strip()
|
else:
|
||||||
|
name = str(name).strip()
|
||||||
|
# 获取性别
|
||||||
|
sex = pd.isna(df.at[i, "性别"]) if "男" else str(df.at[i, "性别"]).strip()
|
||||||
|
|
||||||
# 获取当前行的特征(如果Excel里有“特征”这一列就读,没有就用默认值)
|
# 获取当前行的特征(如果Excel里有“特征”这一列就读,没有就用默认值)
|
||||||
# 假设Excel里有一列叫 "表现特征",如果没有则用默认的 "有礼貌..."
|
# 假设Excel里有一列叫 "表现特征",如果没有则用默认的 "有礼貌..."
|
||||||
@@ -124,11 +142,7 @@ def generate_comment_all():
|
|||||||
generated_text = generate_comment(
|
generated_text = generate_comment(
|
||||||
name, config["age_group"], traits, sex
|
name, config["age_group"], traits, sex
|
||||||
)
|
)
|
||||||
if generated_text:
|
df.at[i, "评价"] = generated_text if str(generated_text).strip() else ""
|
||||||
# 赋值
|
|
||||||
df.at[i, "评价"] = str(generated_text).strip()
|
|
||||||
else:
|
|
||||||
df.at[i, "评价"] = "" # 防空处理
|
|
||||||
logger.success(f"学生:{name},评语生成完毕")
|
logger.success(f"学生:{name},评语生成完毕")
|
||||||
|
|
||||||
# 可选:每生成 5 个就保存一次
|
# 可选:每生成 5 个就保存一次
|
||||||
@@ -139,11 +153,12 @@ def generate_comment_all():
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"学生:{name},生成评语出错: {str(e)}")
|
logger.error(f"学生:{name},生成评语出错: {str(e)}")
|
||||||
|
|
||||||
# --- 循环结束后最终保存文件 ---
|
# --- 循环结束后最终保存文件 ---
|
||||||
# index=False 表示不把 pandas 的索引 (0,1,2...) 写到 Excel 第一列
|
# index=False 表示不把 pandas 的索引 (0,1,2...) 写到 Excel 第一列
|
||||||
df.to_excel(excel_path, index=False)
|
df.to_excel(excel_path, index=False)
|
||||||
logger.success(f"所有评语已生成并写入文件:{excel_path}")
|
logger.success(f"所有评语已生成并写入文件:{excel_path}")
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(total_count, total_count, "生成学生评语")
|
||||||
|
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
logger.error(f"保存失败!请先关闭 Excel 文件:{config['excel_file']}")
|
logger.error(f"保存失败!请先关闭 Excel 文件:{config['excel_file']}")
|
||||||
@@ -155,26 +170,23 @@ def generate_comment_all():
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# 3. 生成成长报告(根据names.xlsx文件生成)
|
# 3. 生成成长报告(根据names.xlsx文件生成)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
def generate_report():
|
def generate_report(stop_event: threading.Event = None, progress_callback=None):
|
||||||
# 1. 资源准备
|
"""
|
||||||
if install_fonts_from_directory(config["fonts_dir"]):
|
根据学生姓名生成成长报告
|
||||||
logger.info("等待系统识别新安装的字体...")
|
:params stop_event 任务是否停止事件(监听UI的事件监听)
|
||||||
time.sleep(2)
|
:params progress_callback 进度回调函数
|
||||||
|
"""
|
||||||
os.makedirs(config["output_folder"], exist_ok=True)
|
# 1. 检查模版文件是否存在
|
||||||
# 检查模版文件是否存在
|
|
||||||
if not os.path.exists(config["source_file"]):
|
if not os.path.exists(config["source_file"]):
|
||||||
logger.info(f"错误: 找不到模版文件 {config["source_file"]}")
|
logger.info(f"错误: 找不到模版文件 {config["source_file"]}")
|
||||||
return
|
# 2. 检查数据文件是否存在
|
||||||
# 检查数据文件是否存在
|
|
||||||
if not os.path.exists(config["excel_file"]):
|
if not os.path.exists(config["excel_file"]):
|
||||||
logger.info(f"错误: 找不到数据文件 {config['excel_file']}")
|
logger.info(f"错误: 找不到数据文件 {config['excel_file']}")
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 2. 读取数据
|
# 1. 读取数据
|
||||||
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
|
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
|
||||||
# 确保列名对应
|
# 2. 确保列名对应
|
||||||
columns = [
|
columns = [
|
||||||
"姓名",
|
"姓名",
|
||||||
"英文名",
|
"英文名",
|
||||||
@@ -187,14 +199,21 @@ def generate_report():
|
|||||||
"喜欢吃的食物",
|
"喜欢吃的食物",
|
||||||
"评价",
|
"评价",
|
||||||
]
|
]
|
||||||
|
# 获取数据列表
|
||||||
datas = df[columns].values.tolist()
|
datas = df[columns].values.tolist()
|
||||||
|
total_count = len(datas)
|
||||||
|
|
||||||
|
# 获取配置文件的教师签名
|
||||||
teacher_names_str = " ".join(config["teachers"])
|
teacher_names_str = " ".join(config["teachers"])
|
||||||
|
logger.info(f"开始处理,共 {total_count} 位学生...")
|
||||||
logger.info(f"开始处理,共 {len(datas)} 位学生...")
|
|
||||||
|
|
||||||
# 3. 循环处理
|
# 3. 循环处理
|
||||||
for i, row_data in enumerate(datas):
|
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,
|
name,
|
||||||
@@ -237,16 +256,16 @@ def generate_report():
|
|||||||
# 构造学生信息字典
|
# 构造学生信息字典
|
||||||
student_info_dict = {
|
student_info_dict = {
|
||||||
"name": name,
|
"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 "男",
|
"sex": sex if pd.notna(sex) else "男",
|
||||||
"birthday": (
|
"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 "",
|
"zodiac": str(zodiac).strip() if str(zodiac).strip() or not str(zodiac).strip().lower() else " ",
|
||||||
"friend": friend if pd.notna(friend) else "",
|
"friend": str(friend).strip() if str(friend).strip() or not str(friend).strip().lower() else " ",
|
||||||
"hobby": hobby if pd.notna(hobby) else "",
|
"hobby": str(hobby).strip() if str(hobby).strip() or not str(hobby).strip().lower() else " ",
|
||||||
"game": game if pd.notna(game) else "",
|
"game": str(game).strip() if str(game).strip() or not str(game).strip().lower() else " ",
|
||||||
"food": food if pd.notna(food) 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")
|
me_image_path = find_image_path(student_image_folder, "me")
|
||||||
@@ -254,6 +273,7 @@ def generate_report():
|
|||||||
if check_file_exists(me_image_path):
|
if check_file_exists(me_image_path):
|
||||||
replace_three_page(prs, student_info_dict, me_image_path)
|
replace_three_page(prs, student_info_dict, me_image_path)
|
||||||
else:
|
else:
|
||||||
|
replace_three_page(prs, student_info_dict)
|
||||||
logger.warning(f"⚠️ 警告: 学生图片文件不存在 {me_image_path}")
|
logger.warning(f"⚠️ 警告: 学生图片文件不存在 {me_image_path}")
|
||||||
|
|
||||||
# --- 页面 4 ---
|
# --- 页面 4 ---
|
||||||
@@ -266,7 +286,7 @@ def generate_report():
|
|||||||
replace_four_page(prs, class_image_path)
|
replace_four_page(prs, class_image_path)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"⚠️ 警告: 班级图片文件不存在 {class_image_path}")
|
logger.warning(f"⚠️ 警告: 班级图片文件不存在 {class_image_path}")
|
||||||
|
|
||||||
# --- 页面 5 ---
|
# --- 页面 5 ---
|
||||||
if os.path.exists(student_image_folder):
|
if os.path.exists(student_image_folder):
|
||||||
img1_path = find_image_path(student_image_folder, "1")
|
img1_path = find_image_path(student_image_folder, "1")
|
||||||
@@ -301,6 +321,8 @@ def generate_report():
|
|||||||
f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。"
|
f"保存失败: 文件 {new_filename} 可能已被打开,请关闭后重试。"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(total_count, total_count, "生成报告")
|
||||||
logger.success("所有报告生成完毕!")
|
logger.success("所有报告生成完毕!")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -311,10 +333,12 @@ def generate_report():
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# 5. 转换格式(根据names.xlsx文件生成PPT转PDF)
|
# 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,速度快)
|
批量转换文件夹下的所有 PPT
|
||||||
已修复多线程 CoInitialize 报错,并适配 GUI 日志
|
:params folder_path 需要转换的PPT文件夹
|
||||||
|
:params stop_event 任务是否停止事件(监听UI的事件监听)
|
||||||
|
:params progress_callback 进度回调函数
|
||||||
"""
|
"""
|
||||||
# 子线程初始化 COM 组件
|
# 子线程初始化 COM 组件
|
||||||
pythoncom.CoInitialize()
|
pythoncom.CoInitialize()
|
||||||
@@ -333,18 +357,24 @@ def batch_convert_folder(folder_path):
|
|||||||
logger.warning("没有找到 PPT 文件")
|
logger.warning("没有找到 PPT 文件")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(f"发现 {len(files)} 个文件,准备开始转换...")
|
total_count = len(files)
|
||||||
|
logger.info(f"发现 {total_count} 个文件,准备开始转换...")
|
||||||
|
|
||||||
powerpoint = None
|
powerpoint = None
|
||||||
try:
|
try:
|
||||||
# 1. 启动应用 (只启动一次)
|
# 1. 启动应用 (只启动一次)
|
||||||
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
|
powerpoint = comtypes.client.CreateObject("PowerPoint.Application")
|
||||||
|
|
||||||
# 【建议】在后台线程运行时,有时设置为不可见更稳定,
|
# 设置是否显示转化页面,但如果遇到转换卡死,可以尝试去掉下面这行的注释,让它显示出来
|
||||||
# 但如果遇到转换卡死,可以尝试去掉下面这行的注释,让它显示出来
|
|
||||||
# powerpoint.Visible = 1
|
# powerpoint.Visible = 1
|
||||||
|
|
||||||
for filename in files:
|
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)
|
ppt_path = os.path.join(folder_path, filename)
|
||||||
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
|
pdf_path = os.path.splitext(ppt_path)[0] + ".pdf"
|
||||||
|
|
||||||
@@ -353,7 +383,7 @@ def batch_convert_folder(folder_path):
|
|||||||
logger.info(f"[跳过] 已存在: {filename}")
|
logger.info(f"[跳过] 已存在: {filename}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info(f"正在转换: {filename} ...")
|
logger.info(f"[{files.index(filename)}/{total_count}]正在转换: {filename} ...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 打开 -> 另存为 -> 关闭
|
# 打开 -> 另存为 -> 关闭
|
||||||
@@ -363,6 +393,9 @@ def batch_convert_folder(folder_path):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"文件 {filename} 转换出错: {e}")
|
logger.error(f"文件 {filename} 转换出错: {e}")
|
||||||
|
|
||||||
|
# 添加进度条
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(total_count, total_count, "转换PDF")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"PowerPoint 进程启动出错: {e}")
|
logger.error(f"PowerPoint 进程启动出错: {e}")
|
||||||
finally:
|
finally:
|
||||||
@@ -384,11 +417,15 @@ def batch_convert_folder(folder_path):
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# 5. 生成属相(根据names.xlsx文件生成属相)
|
# 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:
|
try:
|
||||||
# 1. 读取数据
|
# 1. 读取数据
|
||||||
excel_path = config["excel_file"]
|
excel_path = config["excel_file"]
|
||||||
# sheet_name 根据实际情况修改,如果不确定可以用 sheet_name=0 读取第一个
|
|
||||||
df = pd.read_excel(excel_path, sheet_name="Sheet1")
|
df = pd.read_excel(excel_path, sheet_name="Sheet1")
|
||||||
|
|
||||||
# 2. 检查必要的列
|
# 2. 检查必要的列
|
||||||
@@ -399,30 +436,63 @@ def generate_zodiac():
|
|||||||
logger.error(f"Excel中找不到列名:【{date_column}】,请检查表头。")
|
logger.error(f"Excel中找不到列名:【{date_column}】,请检查表头。")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 检查是否存在"属相"列,不存在则新建
|
|
||||||
if target_column not in df.columns:
|
if target_column not in df.columns:
|
||||||
df[target_column] = ""
|
df[target_column] = ""
|
||||||
|
|
||||||
# --- 获取总行数,用于日志 ---
|
|
||||||
total_count = len(df)
|
total_count = len(df)
|
||||||
logger.info(f"开始生成学生属相,共 {total_count} 位学生...")
|
logger.info(f"开始生成学生属相,共 {total_count} 位学生...")
|
||||||
|
|
||||||
# 3. 数据清洗与计算
|
# 3. 预处理:将“生日”列转换为 datetime 格式
|
||||||
temp_dates = pd.to_datetime(df[date_column], errors="coerce")
|
df['temp_date'] = pd.to_datetime(df[date_column], errors="coerce")
|
||||||
df[target_column] = temp_dates.apply(calculate_zodiac)
|
|
||||||
|
# 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
|
save_path = excel_path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
df.to_excel(save_path, index=False)
|
df.to_excel(save_path, sheet_name="Sheet1", index=False)
|
||||||
logger.success(f"所有属相已更新并写入文件:{save_path}")
|
logger.success(f"所有属相已更新并写入文件:{save_path}")
|
||||||
logger.warning(f"请检查文件 {save_path} 修改日期格式。")
|
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
logger.error(f"保存失败!请先关闭 Excel 文件:{save_path}")
|
logger.error(f"保存失败!请先关闭 Excel 文件:{save_path}")
|
||||||
|
# 添加进度条
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(total_count, total_count, "生成属相")
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.error(f"找不到文件 {config.get('excel_file')}")
|
logger.error(f"找不到文件 {config.get('excel_file')}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"程序运行出错: {str(e)}")
|
logger.error(f"程序运行出错: {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ console = Console()
|
|||||||
# 1. 配置区域 (Configuration)
|
# 1. 配置区域 (Configuration)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
config = load_config("config.toml")
|
config = load_config("config.toml")
|
||||||
|
|
||||||
|
|
||||||
def replace_one_page(prs, name, class_name):
|
def replace_one_page(prs, name, class_name):
|
||||||
"""替换第一页信息"""
|
"""替换第一页信息"""
|
||||||
replace_text_in_slide(prs, 0, "name", 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)
|
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"]
|
fields = ["name", "english_name", "sex", "birthday", "zodiac", "friend", "hobby", "game", "food"]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import pythoncom
|
||||||
import comtypes.client
|
import comtypes.client
|
||||||
|
|
||||||
|
|
||||||
@@ -9,6 +9,8 @@ def ppt_to_pdf_single(ppt_path, pdf_path=None):
|
|||||||
:param ppt_path: PPT 文件路径
|
:param ppt_path: PPT 文件路径
|
||||||
:param pdf_path: PDF 输出路径 (可选,默认同名)
|
:param pdf_path: PDF 输出路径 (可选,默认同名)
|
||||||
"""
|
"""
|
||||||
|
# 子线程初始化 COM 组件
|
||||||
|
pythoncom.CoInitialize()
|
||||||
ppt_path = os.path.abspath(ppt_path) # COM 接口必须使用绝对路径
|
ppt_path = os.path.abspath(ppt_path) # COM 接口必须使用绝对路径
|
||||||
|
|
||||||
if pdf_path is None:
|
if pdf_path is None:
|
||||||
@@ -40,3 +42,4 @@ def ppt_to_pdf_single(ppt_path, pdf_path=None):
|
|||||||
finally:
|
finally:
|
||||||
if powerpoint:
|
if powerpoint:
|
||||||
powerpoint.Quit()
|
powerpoint.Quit()
|
||||||
|
pythoncom.CoUninitialize()
|
||||||
|
|||||||
44
uv.lock
generated
44
uv.lock
generated
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -289,7 +289,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langchain-core"
|
name = "langchain-core"
|
||||||
version = "1.1.3"
|
version = "1.2.0"
|
||||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "jsonpatch" },
|
{ name = "jsonpatch" },
|
||||||
@@ -301,28 +301,28 @@ dependencies = [
|
|||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "uuid-utils" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
name = "langchain-openai"
|
name = "langchain-openai"
|
||||||
version = "1.1.1"
|
version = "1.1.3"
|
||||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "langchain-core" },
|
{ name = "langchain-core" },
|
||||||
{ name = "openai" },
|
{ name = "openai" },
|
||||||
{ name = "tiktoken" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
name = "langgraph"
|
name = "langgraph"
|
||||||
version = "1.0.4"
|
version = "1.0.5"
|
||||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "langchain-core" },
|
{ name = "langchain-core" },
|
||||||
@@ -332,9 +332,9 @@ dependencies = [
|
|||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "xxhash" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
@@ -365,20 +365,20 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langgraph-sdk"
|
name = "langgraph-sdk"
|
||||||
version = "0.2.15"
|
version = "0.3.0"
|
||||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "orjson" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
name = "langsmith"
|
name = "langsmith"
|
||||||
version = "0.4.58"
|
version = "0.4.59"
|
||||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
@@ -390,9 +390,9 @@ dependencies = [
|
|||||||
{ name = "uuid-utils" },
|
{ name = "uuid-utils" },
|
||||||
{ name = "zstandard" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
@@ -557,7 +557,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openai"
|
name = "openai"
|
||||||
version = "2.9.0"
|
version = "2.11.0"
|
||||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
@@ -569,9 +569,9 @@ dependencies = [
|
|||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
{ name = "typing-extensions" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
@@ -1260,11 +1260,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.6.1"
|
version = "2.6.2"
|
||||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
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 = [
|
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]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user