Compare commits
22 Commits
4d50c73ecb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 60a78ed1e3 | |||
| 016e392524 | |||
| 463c1c8b8f | |||
| de71594812 | |||
| f3d16ec1f9 | |||
| 1c2d5db393 | |||
| d1f6d7da7d | |||
| 1c9aa24202 | |||
| 54398e2cbe | |||
| d3c0121632 | |||
| 3c60b3e7ca | |||
| ba7dd09037 | |||
| f64f005292 | |||
| 842a7cce64 | |||
| 14b8c19dfe | |||
| 0e47603d23 | |||
| 6809c6f2c6 | |||
| 3a4a9df751 | |||
| 93d1e8687a | |||
| 9d347f9bc9 | |||
| cbf87d2569 | |||
| 7275699c25 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -10,5 +10,8 @@ wheels/
|
|||||||
.venv
|
.venv
|
||||||
output/*.pptx
|
output/*.pptx
|
||||||
output/*.pdf
|
output/*.pdf
|
||||||
|
data/images/*
|
||||||
|
data/*.xlsx
|
||||||
|
|
||||||
config.toml
|
.idea/
|
||||||
|
.trae/
|
||||||
8
.idea/growth_report.iml
generated
8
.idea/growth_report.iml
generated
@@ -1,10 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="PYTHON_MODULE" version="4">
|
<module type="PYTHON_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
<orderEntry type="jdk" jdkName="uv (growth_report)" jdkType="Python SDK" />
|
||||||
</content>
|
|
||||||
<orderEntry type="jdk" jdkName="uv (growth_report) (2)" jdkType="Python SDK" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PyDocumentationSettings">
|
<component name="PyDocumentationSettings">
|
||||||
@@ -14,7 +12,7 @@
|
|||||||
<component name="TemplatesService">
|
<component name="TemplatesService">
|
||||||
<option name="TEMPLATE_FOLDERS">
|
<option name="TEMPLATE_FOLDERS">
|
||||||
<list>
|
<list>
|
||||||
<option value="$MODULE_DIR$/templates" />
|
<option value="$MODULE_DIR$/script/dist/尚城幼儿园幼儿学期发展报告/templates" />
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
56
.idea/inspectionProfiles/Project_Default.xml
generated
56
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,56 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
|
||||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="ignoredPackages">
|
|
||||||
<list>
|
|
||||||
<option value="loguru" />
|
|
||||||
<option value="APScheduler" />
|
|
||||||
<option value="watchdog" />
|
|
||||||
<option value="aiohttp" />
|
|
||||||
<option value="aiofiles" />
|
|
||||||
<option value="pydantic" />
|
|
||||||
<option value="SQLAlchemy" />
|
|
||||||
<option value="aiosqlite" />
|
|
||||||
<option value="fastapi" />
|
|
||||||
<option value="uvicorn" />
|
|
||||||
<option value="python-multipart" />
|
|
||||||
<option value="jinja2" />
|
|
||||||
<option value="itsdangerous" />
|
|
||||||
<option value="pillow" />
|
|
||||||
<option value="filetype" />
|
|
||||||
<option value="pydub" />
|
|
||||||
<option value="pysilk-mod" />
|
|
||||||
<option value="pymediainfo" />
|
|
||||||
<option value="py7zr" />
|
|
||||||
<option value="requests" />
|
|
||||||
<option value="httpx" />
|
|
||||||
<option value="tabulate" />
|
|
||||||
<option value="qrcode" />
|
|
||||||
<option value="psutil" />
|
|
||||||
<option value="tomli_w" />
|
|
||||||
<option value="websockets" />
|
|
||||||
<option value="redis" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
|
||||||
<option name="ignoredErrors">
|
|
||||||
<list>
|
|
||||||
<option value="N806" />
|
|
||||||
<option value="N802" />
|
|
||||||
<option value="N803" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="ignoredIdentifiers">
|
|
||||||
<list>
|
|
||||||
<option value="type.*" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</inspection_tool>
|
|
||||||
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
||||||
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>
|
||||||
164
.idea/workspace.xml
generated
Normal file
164
.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="41690157-d51b-4dae-98de-6b96990d681a" name="更改" comment="fix:优化一些命名规范">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="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="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo">{
|
||||||
|
"customColor": "",
|
||||||
|
"associatedIndex": 0
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectId" id="3744WiSuPrq64wZVLisMf4zKTFq" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">{
|
||||||
|
"keyToString": {
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"Python.main_nicegui.executor": "Run",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
|
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
|
"git-widget-placeholder": "master",
|
||||||
|
"node.js.detected.package.eslint": "true",
|
||||||
|
"node.js.detected.package.tslint": "true",
|
||||||
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"settings.editor.selected.configurable": "preferences.lookFeel",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
}
|
||||||
|
}</component>
|
||||||
|
<component name="RunManager">
|
||||||
|
<configuration name="main_nicegui" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||||
|
<module name="growth_report" />
|
||||||
|
<option name="ENV_FILES" value="" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
|
<option name="RUN_TOOL" value="" />
|
||||||
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/main_nicegui.py" />
|
||||||
|
<option name="PARAMETERS" value="" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="false" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
<recent_temporary>
|
||||||
|
<list>
|
||||||
|
<item itemvalue="Python.main_nicegui" />
|
||||||
|
</list>
|
||||||
|
</recent_temporary>
|
||||||
|
</component>
|
||||||
|
<component name="SharedIndexes">
|
||||||
|
<attachedChunks>
|
||||||
|
<set>
|
||||||
|
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-PY-253.29346.142" />
|
||||||
|
<option value="bundled-python-sdk-f2b7a9f6281b-6e1f45a539f7-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.29346.142" />
|
||||||
|
</set>
|
||||||
|
</attachedChunks>
|
||||||
|
</component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="默认任务">
|
||||||
|
<changelist id="41690157-d51b-4dae-98de-6b96990d681a" name="更改" comment="" />
|
||||||
|
<created>1766149044347</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1766149044347</updated>
|
||||||
|
<workItem from="1766149046808" duration="4371000" />
|
||||||
|
<workItem from="1766155304645" duration="34000" />
|
||||||
|
<workItem from="1766155493490" duration="387000" />
|
||||||
|
<workItem from="1766155896565" duration="429000" />
|
||||||
|
<workItem from="1766160535280" duration="21000" />
|
||||||
|
<workItem from="1766256207534" duration="960000" />
|
||||||
|
<workItem from="1766287241685" duration="2135000" />
|
||||||
|
<workItem from="1766329711762" duration="741000" />
|
||||||
|
<workItem from="1768312728552" duration="228000" />
|
||||||
|
<workItem from="1768312972093" duration="486000" />
|
||||||
|
<workItem from="1768314152581" duration="7000" />
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00001" summary="fix:修复一些BUG">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1766151464355</created>
|
||||||
|
<option name="number" value="00001" />
|
||||||
|
<option name="presentableId" value="LOCAL-00001" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1766151464355</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00002" summary="fix:修复一些BUG">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1766151466883</created>
|
||||||
|
<option name="number" value="00002" />
|
||||||
|
<option name="presentableId" value="LOCAL-00002" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1766151466883</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00003" summary="fix:优化一些命名规范">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1766152994686</created>
|
||||||
|
<option name="number" value="00003" />
|
||||||
|
<option name="presentableId" value="LOCAL-00003" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1766152994686</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00004" summary="fix:优化一些命名规范">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1766330259490</created>
|
||||||
|
<option name="number" value="00004" />
|
||||||
|
<option name="presentableId" value="LOCAL-00004" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1766330259490</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="5" />
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
<component name="Vcs.Log.Tabs.Properties">
|
||||||
|
<option name="TAB_STATES">
|
||||||
|
<map>
|
||||||
|
<entry key="MAIN">
|
||||||
|
<value>
|
||||||
|
<State />
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="VcsManagerConfiguration">
|
||||||
|
<MESSAGE value="fix:修复一些BUG" />
|
||||||
|
<MESSAGE value="fix:优化一些命名规范" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="fix:优化一些命名规范" />
|
||||||
|
<option name="OPTIMIZE_IMPORTS_BEFORE_PROJECT_COMMIT" value="true" />
|
||||||
|
<option name="REFORMAT_BEFORE_PROJECT_COMMIT" value="true" />
|
||||||
|
<option name="REARRANGE_BEFORE_PROJECT_COMMIT" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
||||||
|
<SUITE FILE_PATH="coverage/growth_report$main_nicegui.coverage" NAME="main_nicegui 覆盖结果" MODIFIED="1766329725535" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
20
.vscode/settings.json
vendored
Normal file
20
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"prettier.singleQuote": true,
|
||||||
|
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.svn": true,
|
||||||
|
"**/.hg": true,
|
||||||
|
"**/CVS": true,
|
||||||
|
"**/.DS_Store": true,
|
||||||
|
"**/node_modules": true,
|
||||||
|
"**/dist": true,
|
||||||
|
".idea/**": true,
|
||||||
|
"**/__pycache__": true
|
||||||
|
},
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||||
|
}
|
||||||
|
}
|
||||||
50
IFLOW.md
50
IFLOW.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 项目概述
|
## 项目概述
|
||||||
|
|
||||||
这是一个基于Python的自动化幼儿园成长报告生成系统。该系统可以从Excel数据文件中读取幼儿信息,结合AI生成个性化评语,并将所有信息批量填充到PPT模板中,最终生成每个学生的个性化成长报告。系统支持双界面运行(命令行界面和图形界面),具备字体安装、图片替换、批量PDF转换、生肖计算等功能。
|
基于Python的自动化幼儿园学期成长报告生成系统。该系统可以从Excel数据文件中读取幼儿信息,结合AI生成个性化评语,并将所有信息批量填充到PPT模板中,最终生成每个学生的个性化成长报告。系统支持UI界面操作,具备字体安装、图片替换、批量PDF转换、生肖计算、模板导出等完整功能。
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
- **loguru**: 日志记录
|
- **loguru**: 日志记录
|
||||||
- **tomli**: 配置文件解析
|
- **tomli**: 配置文件解析
|
||||||
- **tkinter**: 图形用户界面
|
- **tkinter**: 图形用户界面
|
||||||
|
- **nicegui**: 现代Web界面
|
||||||
|
|
||||||
## 核心功能
|
## 核心功能
|
||||||
|
|
||||||
@@ -46,17 +47,23 @@
|
|||||||
- 批量更新Excel中的生肖信息
|
- 批量更新Excel中的生肖信息
|
||||||
- 支持日期格式自动识别
|
- 支持日期格式自动识别
|
||||||
|
|
||||||
### 6. 导出数据模板 (📦 导出数据模板)
|
### 6. 园长一键签名 (💴 园长一键签名)
|
||||||
|
|
||||||
|
- 一键为所有生成的报告添加园长签名
|
||||||
|
- 自动识别输出文件夹中的PPT文件
|
||||||
|
- 批量替换签名占位符为实际签名图片
|
||||||
|
|
||||||
|
### 7. 导出数据模板 (📦 导出数据模板)
|
||||||
- 生成标准化的数据模板ZIP包
|
- 生成标准化的数据模板ZIP包
|
||||||
- 包含示例Excel文件和图片文件夹结构
|
- 包含示例Excel文件和图片文件夹结构
|
||||||
- 方便新用户快速上手
|
- 方便新用户快速上手
|
||||||
|
|
||||||
### 7. 初始化系统 (📤 初始化系统)
|
### 8. 初始化系统 (📤 初始化系统)
|
||||||
- 自动创建必要的目录结构
|
- 自动创建必要的目录结构
|
||||||
- 安装所需字体文件
|
- 安装所需字体文件
|
||||||
- 配置系统环境
|
- 配置系统环境
|
||||||
|
|
||||||
### 8. 字体安装 (🔤 字体安装)
|
### 9. 字体安装 (🔤 字体安装)
|
||||||
- 自动检测系统是否安装了指定字体
|
- 自动检测系统是否安装了指定字体
|
||||||
- 自动安装项目所需的字体文件
|
- 自动安装项目所需的字体文件
|
||||||
- 支持方正兰亭黑简体和方正少儿简体字体
|
- 支持方正兰亭黑简体和方正少儿简体字体
|
||||||
@@ -66,8 +73,10 @@
|
|||||||
```
|
```
|
||||||
growth_report/
|
growth_report/
|
||||||
├── main.py # 主程序入口(命令行界面)
|
├── main.py # 主程序入口(命令行界面)
|
||||||
├── UI.py # 图形用户界面入口
|
├── UI.py # 图形用户界面入口(tkinter)
|
||||||
├── config.env.toml # 项目配置文件
|
├── main_nicegui.py # NiceGUI界面入口
|
||||||
|
├── main.pyw # Windows图形界面启动文件
|
||||||
|
├── config.toml # 项目配置文件
|
||||||
├── pyproject.toml # 项目依赖配置
|
├── pyproject.toml # 项目依赖配置
|
||||||
├── start_app.bat # Windows启动批处理文件
|
├── start_app.bat # Windows启动批处理文件
|
||||||
├── README.md # 项目说明文档
|
├── README.md # 项目说明文档
|
||||||
@@ -75,6 +84,19 @@ growth_report/
|
|||||||
├── config/
|
├── config/
|
||||||
│ ├── config.py # 配置加载工具
|
│ ├── config.py # 配置加载工具
|
||||||
│ └── output/ # 配置输出目录
|
│ └── output/ # 配置输出目录
|
||||||
|
├── ui/
|
||||||
|
│ ├── app_window.py # tkinter图形界面
|
||||||
|
│ ├── main_nicegui.py # NiceGUI界面主文件
|
||||||
|
│ ├── assets/
|
||||||
|
│ │ ├── icon.ico # 应用图标
|
||||||
|
│ │ └── style.css # 样式文件
|
||||||
|
│ ├── core/
|
||||||
|
│ │ ├── logger.py # 日志处理
|
||||||
|
│ │ ├── state.py # 应用状态管理
|
||||||
|
│ │ ├── task_runner.py # 任务运行器
|
||||||
|
│ │ └── __pycache__/
|
||||||
|
│ └── views/
|
||||||
|
│ └── home_page.py # NiceGUI主页面
|
||||||
├── utils/
|
├── utils/
|
||||||
│ ├── agent_utils.py # AI评语生成工具
|
│ ├── agent_utils.py # AI评语生成工具
|
||||||
│ ├── file_utils.py # 文件操作工具
|
│ ├── file_utils.py # 文件操作工具
|
||||||
@@ -82,6 +104,7 @@ growth_report/
|
|||||||
│ ├── generate_utils.py # 核心生成功能
|
│ ├── generate_utils.py # 核心生成功能
|
||||||
│ ├── growt_utils.py # PPT模板替换工具
|
│ ├── growt_utils.py # PPT模板替换工具
|
||||||
│ ├── image_utils.py # 图片处理工具
|
│ ├── image_utils.py # 图片处理工具
|
||||||
|
│ ├── log_handler.py # 日志处理器
|
||||||
│ ├── pdf_utils.py # PDF转换工具
|
│ ├── pdf_utils.py # PDF转换工具
|
||||||
│ ├── pptx_utils.py # PPT文本和图片替换工具
|
│ ├── pptx_utils.py # PPT文本和图片替换工具
|
||||||
│ └── zodiac_utils.py # 生肖计算工具
|
│ └── zodiac_utils.py # 生肖计算工具
|
||||||
@@ -153,11 +176,16 @@ data/images/
|
|||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 图形界面
|
#### 图形界面 (tkinter)
|
||||||
```bash
|
```bash
|
||||||
python UI.py
|
python UI.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### NiceGUI界面 (现代Web界面)
|
||||||
|
```bash
|
||||||
|
python main_nicegui.py
|
||||||
|
```
|
||||||
|
|
||||||
或直接运行批处理文件:
|
或直接运行批处理文件:
|
||||||
```bash
|
```bash
|
||||||
start_app.bat
|
start_app.bat
|
||||||
@@ -177,18 +205,18 @@ pip install -r requirements.txt
|
|||||||
|
|
||||||
### 初始化设置
|
### 初始化设置
|
||||||
|
|
||||||
1. 编辑`config.env.toml`配置文件,设置API密钥和其他参数
|
1. 编辑`config.toml`配置文件,设置API密钥和其他参数
|
||||||
2. 准备Excel数据文件(按指定格式)
|
2. 准备Excel数据文件(按指定格式)
|
||||||
3. 准备图片资源文件夹(按指定结构)
|
3. 准备图片资源文件夹(按指定结构)
|
||||||
4. 准备PPT模板文件
|
4. 准备PPT模板文件
|
||||||
|
|
||||||
## 系统特点
|
## 系统特点
|
||||||
|
|
||||||
- **双界面支持**: 提供命令行界面和图形界面两种操作方式
|
- **三界面支持**: 提供命令行界面、tkinter图形界面和NiceGUI现代Web界面三种操作方式
|
||||||
- **自动化流程**: 从数据到成品报告的全流程自动化
|
- **自动化流程**: 从数据到成品报告的全流程自动化
|
||||||
- **AI集成**: 智能生成个性化评语
|
- **AI集成**: 智能生成个性化评语
|
||||||
- **格式保持**: 替换文本时保持原有格式
|
- **格式保持**: 替换文本时保持原有格式
|
||||||
- **用户友好**: 丰富的命令行界面和图形界面,实时日志显示
|
- **用户友好**: 丰富的命令行界面和多种图形界面,实时日志显示
|
||||||
- **批量处理**: 支持批量生成和转换
|
- **批量处理**: 支持批量生成和转换
|
||||||
- **错误处理**: 完善的异常处理和日志记录
|
- **错误处理**: 完善的异常处理和日志记录
|
||||||
- **生肖计算**: 自动根据生日计算生肖
|
- **生肖计算**: 自动根据生日计算生肖
|
||||||
@@ -200,7 +228,7 @@ pip install -r requirements.txt
|
|||||||
|
|
||||||
- 使用`loguru`进行日志记录
|
- 使用`loguru`进行日志记录
|
||||||
- 使用`rich`美化命令行输出
|
- 使用`rich`美化命令行输出
|
||||||
- 使用`tkinter`构建图形界面
|
- 使用`tkinter`和`nicegui`构建图形界面
|
||||||
- 配置文件使用TOML格式
|
- 配置文件使用TOML格式
|
||||||
- 图片和文本替换使用占位符机制
|
- 图片和文本替换使用占位符机制
|
||||||
- 遵循Python代码规范
|
- 遵循Python代码规范
|
||||||
|
|||||||
136
README.md
136
README.md
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
## 项目概述
|
## 项目概述
|
||||||
|
|
||||||
基于Python的自动化幼儿园学期成长报告生成系统。该系统可以从Excel数据文件中读取幼儿信息,结合AI生成个性化评语,并将所有信息批量填充到PPT模板中,最终生成每个学生的个性化成长报告。系统支持双界面操作(命令行界面和图形界面),具备字体安装、图片替换、批量PDF转换、生肖计算、模板导出等完整功能。
|
基于Python的自动化幼儿园学期成长报告生成系统。该系统可以从Excel数据文件中读取幼儿信息,结合AI生成个性化评语,并将所有信息批量填充到PPT模板中,最终生成每个学生的个性化成长报告。系统支持UI界面操作,具备字体安装、图片替换、批量PDF转换、生肖计算、模板导出等完整功能。
|
||||||
|
|
||||||
## ✨ 主要特性
|
## ✨ 主要特性
|
||||||
|
|
||||||
@@ -13,10 +13,11 @@
|
|||||||
- 🤖 **AI评语**: 智能生成个性化、治愈系风格的幼儿评语
|
- 🤖 **AI评语**: 智能生成个性化、治愈系风格的幼儿评语
|
||||||
- 🖼️ **图文并茂**: 支持个人照片、活动照片、班级合影的自动替换
|
- 🖼️ **图文并茂**: 支持个人照片、活动照片、班级合影的自动替换
|
||||||
- 📄 **格式转换**: 批量PPT转PDF,便于分发和存档
|
- 📄 **格式转换**: 批量PPT转PDF,便于分发和存档
|
||||||
- 🎨 **双界面**: 提供命令行界面和图形界面,满足不同用户需求
|
- 🎨 **现代界面**: 提供NiceGUI现代Web界面,操作直观友好
|
||||||
- 🐲 **生肖计算**: 根据生日自动计算生肖信息
|
- 🐲 **生肖计算**: 根据生日自动计算生肖信息
|
||||||
- 📦 **模板导出**: 生成标准化数据模板,快速上手
|
- 📦 **模板导出**: 生成标准化数据模板,快速上手
|
||||||
- 🔤 **字体安装**: 自动检测和安装所需字体文件
|
- 🔤 **字体安装**: 自动检测和安装所需字体文件
|
||||||
|
- ✍️ **签名生成**: 不依赖占位符,直接在指定位置添加园长签名
|
||||||
|
|
||||||
## 🛠️ 技术栈
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
- **comtypes**: PowerPoint转PDF功能
|
- **comtypes**: PowerPoint转PDF功能
|
||||||
- **rich**: 美化命令行界面
|
- **rich**: 美化命令行界面
|
||||||
- **loguru**: 日志记录
|
- **loguru**: 日志记录
|
||||||
- **tkinter**: 图形用户界面
|
- **nicegui**: 现代Web界面
|
||||||
- **tomli**: 配置文件解析
|
- **tomli**: 配置文件解析
|
||||||
|
|
||||||
## 📋 系统要求
|
## 📋 系统要求
|
||||||
@@ -42,46 +43,46 @@
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://gitee.com/hanhanshibaobei/growthreport.git
|
git clone https://gitee.com/hanhanshibaobei/growthreport.git
|
||||||
cd growthreport
|
cd growth_report
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 安装依赖
|
### 2. 安装依赖
|
||||||
|
|
||||||
#### 使用uv(推荐)
|
#### 使用uv(推荐)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv sync
|
uv sync
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 使用pip
|
#### 使用pip
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 配置系统
|
### 3. 配置系统
|
||||||
|
|
||||||
编辑 `config.env.toml` 文件,设置以下信息:
|
编辑 `config.toml` 文件,设置以下信息:
|
||||||
|
|
||||||
- AI API密钥和配置
|
- AI API密钥和配置
|
||||||
- 班级信息和教师名单
|
- 班级信息和教师名单
|
||||||
- 文件路径配置
|
- 文件路径配置
|
||||||
|
- 签名位置配置(可选)
|
||||||
|
|
||||||
### 4. 运行程序
|
### 4. 运行程序
|
||||||
|
|
||||||
#### 图形界面(推荐)
|
#### NiceGUI界面(推荐,现代Web界面)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python UI.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
或直接运行:
|
或直接运行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
start_app.bat
|
start_app.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 命令行界面
|
|
||||||
```bash
|
|
||||||
python main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📖 使用指南
|
## 📖 使用指南
|
||||||
|
|
||||||
### 功能模块
|
### 功能模块
|
||||||
@@ -94,24 +95,25 @@ python main.py
|
|||||||
6. **📦 导出数据模板**: 生成标准化模板
|
6. **📦 导出数据模板**: 生成标准化模板
|
||||||
7. **📤 初始化系统**: 配置系统环境
|
7. **📤 初始化系统**: 配置系统环境
|
||||||
8. **🔤 字体安装**: 自动安装和检测所需字体
|
8. **🔤 字体安装**: 自动安装和检测所需字体
|
||||||
|
9. **✍️ 生成签名**: 不依赖占位符,直接在指定位置添加园长签名
|
||||||
|
|
||||||
### Excel数据格式
|
### Excel数据格式
|
||||||
|
|
||||||
Excel文件应包含以下列(顺序必须与配置文件中一致):
|
Excel文件应包含以下列(顺序必须与配置文件中一致):
|
||||||
|
|
||||||
| 列名 | 说明 | 示例 |
|
| 列名 | 说明 | 示例 |
|
||||||
|------|------|------|
|
| ------------ | ------------------ | ------------ |
|
||||||
| 姓名 | 学生姓名 | 张小明 |
|
| 姓名 | 学生姓名 | 张小明 |
|
||||||
| 英文名 | 英文昵称 | Tom |
|
| 英文名 | 英文昵称 | Tom |
|
||||||
| 性别 | 性别 | 男 |
|
| 性别 | 性别 | 男 |
|
||||||
| 生日 | 出生日期 | 2019-03-15 |
|
| 生日 | 出生日期 | 2019-03-15 |
|
||||||
| 属相 | 生肖(自动计算) | 猪 |
|
| 属相 | 生肖(自动计算) | 猪 |
|
||||||
| 我的好朋友 | 好朋友姓名 | 李小红 |
|
| 我的好朋友 | 好朋友姓名 | 李小红 |
|
||||||
| 我的爱好 | 个人爱好 | 画画、唱歌 |
|
| 我的爱好 | 个人爱好 | 画画、唱歌 |
|
||||||
| 喜欢的游戏 | 喜欢的游戏 | 积木、捉迷藏 |
|
| 喜欢的游戏 | 喜欢的游戏 | 积木、捉迷藏 |
|
||||||
| 喜欢吃的食物 | 喜欢的食物 | 苹果、饼干 |
|
| 喜欢吃的食物 | 喜欢的食物 | 苹果、饼干 |
|
||||||
| 评价 | AI生成的评语 | 自动填充 |
|
| 评价 | AI生成的评语 | 自动填充 |
|
||||||
| 表现特征 | 表现关键词(可选) | 活泼、聪明 |
|
| 表现特征 | 表现关键词(可选) | 活泼、聪明 |
|
||||||
|
|
||||||
### 图片文件结构
|
### 图片文件结构
|
||||||
|
|
||||||
@@ -126,20 +128,47 @@ data/images/
|
|||||||
|
|
||||||
支持多种图片格式:.jpg, .jpeg, .png
|
支持多种图片格式:.jpg, .jpeg, .png
|
||||||
|
|
||||||
|
### 签名配置
|
||||||
|
|
||||||
|
在 `config.toml` 文件中,你可以配置签名图片的位置和大小:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[paths]
|
||||||
|
signature_image = "data/signature.png" # 签名图片路径
|
||||||
|
|
||||||
|
# 签名位置配置(可选)
|
||||||
|
signature_left = 2987040 # 左位置
|
||||||
|
signature_top = 8273415 # 上位置
|
||||||
|
signature_width = 1800000 # 宽度
|
||||||
|
signature_height = 720000 # 高度
|
||||||
|
```
|
||||||
|
|
||||||
|
如果不配置签名位置,系统会使用默认值。
|
||||||
|
|
||||||
## 📁 项目结构
|
## 📁 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
growth_report/
|
growth_report/
|
||||||
├── main.py # 主程序入口(命令行界面)
|
├── main.py # NiceGUI界面入口
|
||||||
├── UI.py # 图形用户界面入口
|
├── config.toml # 项目配置文件
|
||||||
├── config.env.toml # 项目配置文件
|
|
||||||
├── pyproject.toml # 项目依赖配置
|
├── pyproject.toml # 项目依赖配置
|
||||||
├── start_app.bat # 启动脚本
|
├── start_app.bat # 启动脚本
|
||||||
├── README.md # 项目说明文档
|
├── README.md # 项目说明文档
|
||||||
├── IFLOW.md # 项目详细说明文档
|
├── IFLOW.md # 项目详细说明文档
|
||||||
├── config/
|
├── config/
|
||||||
│ ├── config.py # 配置加载工具
|
│ ├── config.py # 配置加载工具
|
||||||
│ └── output/ # 配置输出目录
|
├── ui/
|
||||||
|
│ ├── assets/
|
||||||
|
│ │ ├── icon.ico # 应用图标
|
||||||
|
│ │ └── style.css # 样式文件
|
||||||
|
│ ├── core/
|
||||||
|
│ │ ├── logger.py # 日志处理
|
||||||
|
│ │ ├── state.py # 应用状态管理
|
||||||
|
│ │ └── task_runner.py # 任务运行器
|
||||||
|
│ └── views/
|
||||||
|
│ ├── config_page.py # 配置页面
|
||||||
|
│ ├── data_page.py # 数据页面
|
||||||
|
│ └── home_page.py # 主页面
|
||||||
├── utils/
|
├── utils/
|
||||||
│ ├── agent_utils.py # AI评语生成工具
|
│ ├── agent_utils.py # AI评语生成工具
|
||||||
│ ├── file_utils.py # 文件操作工具
|
│ ├── file_utils.py # 文件操作工具
|
||||||
@@ -147,16 +176,17 @@ growth_report/
|
|||||||
│ ├── generate_utils.py # 核心生成功能
|
│ ├── generate_utils.py # 核心生成功能
|
||||||
│ ├── growt_utils.py # PPT模板替换工具
|
│ ├── growt_utils.py # PPT模板替换工具
|
||||||
│ ├── image_utils.py # 图片处理工具
|
│ ├── image_utils.py # 图片处理工具
|
||||||
|
│ ├── log_handler.py # 日志处理器
|
||||||
│ ├── pdf_utils.py # PDF转换工具
|
│ ├── pdf_utils.py # PDF转换工具
|
||||||
│ ├── pptx_utils.py # PPT文本和图片替换工具
|
│ ├── pptx_utils.py # PPT文本和图片替换工具
|
||||||
|
│ ├── template_utils.py # 模板处理工具
|
||||||
│ └── zodiac_utils.py # 生肖计算工具
|
│ └── zodiac_utils.py # 生肖计算工具
|
||||||
├── data/
|
├── data/
|
||||||
│ ├── names.xlsx # 学生数据Excel文件
|
│ ├── signature.png # 签名图片
|
||||||
│ └── images/ # 学生图片资源文件夹
|
|
||||||
├── fonts/ # 字体文件目录
|
├── fonts/ # 字体文件目录
|
||||||
├── templates/ # PPT模板文件
|
├── templates/ # PPT模板文件
|
||||||
├── output/ # 生成的报告输出目录
|
├── output/ # 生成的报告输出目录
|
||||||
└── old/ # 旧版本文件备份
|
└── public/ # 公共资源文件
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🤖 AI评语生成
|
## 🤖 AI评语生成
|
||||||
@@ -169,11 +199,13 @@ growth_report/
|
|||||||
- 表现特征
|
- 表现特征
|
||||||
|
|
||||||
评语风格为"治愈系",采用三段式结构:
|
评语风格为"治愈系",采用三段式结构:
|
||||||
|
|
||||||
1. **开头**: 亲切问候 + 总体印象
|
1. **开头**: 亲切问候 + 总体印象
|
||||||
2. **正文**: 具体描述孩子的进步和优点
|
2. **正文**: 具体描述孩子的进步和优点
|
||||||
3. **结尾**: 委婉期望 + 新学期祝福
|
3. **结尾**: 委婉期望 + 新学期祝福
|
||||||
|
|
||||||
支持分龄侧重评价:
|
支持分龄侧重评价:
|
||||||
|
|
||||||
- **小班 (3-4岁)**: 适应集体生活、情绪稳定性、基本生活自理能力
|
- **小班 (3-4岁)**: 适应集体生活、情绪稳定性、基本生活自理能力
|
||||||
- **中班 (4-5岁)**: 社交互动、分享与合作、动手能力、好奇心
|
- **中班 (4-5岁)**: 社交互动、分享与合作、动手能力、好奇心
|
||||||
- **大班 (5-6岁)**: 学习习惯、逻辑思维、领导力、幼小衔接准备
|
- **大班 (5-6岁)**: 学习习惯、逻辑思维、领导力、幼小衔接准备
|
||||||
@@ -182,39 +214,55 @@ growth_report/
|
|||||||
|
|
||||||
### AI配置
|
### AI配置
|
||||||
|
|
||||||
在 `config.env.toml` 中配置AI模型:
|
在 `config.toml` 中配置AI模型:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[ai]
|
[ai]
|
||||||
api_key = "your-api-key"
|
api_key = "your-api-key"
|
||||||
api_url = "https://api.openai.com/v1"
|
api_url = "https://apis.iflow.cn/v1/chat/completions"
|
||||||
model = "gpt-3.5-turbo"
|
model = "deepseek-v3.2"
|
||||||
prompt = """
|
prompt = """
|
||||||
你的评语风格是"治愈系"的,能让家长读完后感到欣慰...
|
# Role
|
||||||
|
你是一位拥有20年经验的资深幼儿园主班老师。你的文笔温暖、细腻、充满爱意,擅长发现每个孩子身上独特的闪光点。你的评语风格是“治愈系”的,能让家长读完后感到欣慰并对未来充满希望。
|
||||||
|
|
||||||
|
# Goal
|
||||||
|
请根据用户提供的【幼儿姓名】、【年龄段/班级】以及【日常表现关键词/评分数据】,撰写一份高质量的学期末成长评语。
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
### 自定义PPT模板
|
### 签名配置
|
||||||
|
|
||||||
1. 在 `templates/` 目录放置PPT模板
|
在 `config.toml` 中配置签名位置:
|
||||||
2. 使用占位符格式:`{{变量名}}`
|
|
||||||
3. 支持的占位符:
|
```toml
|
||||||
- `{{name}}`: 学生姓名
|
[paths]
|
||||||
- `{{class_name}}`: 班级名称
|
signature_image = "data/signature.png"
|
||||||
- `{{comments}}`: 评语内容
|
|
||||||
- 等等...
|
# 签名位置配置
|
||||||
|
signature_left = 2987040 # 左位置
|
||||||
|
signature_top = 8273415 # 上位置
|
||||||
|
signature_width = 1800000 # 宽度
|
||||||
|
signature_height = 720000 # 高度
|
||||||
|
```
|
||||||
|
|
||||||
## 🐛 常见问题
|
## 🐛 常见问题
|
||||||
|
|
||||||
### Q: PDF转换失败怎么办?
|
### Q: PDF转换失败怎么办?
|
||||||
|
|
||||||
A: 请确保已安装Microsoft PowerPoint,并且没有其他程序占用PPT文件。
|
A: 请确保已安装Microsoft PowerPoint,并且没有其他程序占用PPT文件。
|
||||||
|
|
||||||
### Q: AI评语生成失败?
|
### Q: AI评语生成失败?
|
||||||
|
|
||||||
A: 检查API密钥配置是否正确,网络连接是否正常。
|
A: 检查API密钥配置是否正确,网络连接是否正常。
|
||||||
|
|
||||||
### Q: 字体显示异常?
|
### Q: 字体显示异常?
|
||||||
|
|
||||||
A: 系统会自动安装所需字体,如仍有问题请手动安装 `fonts/` 目录下的字体文件。
|
A: 系统会自动安装所需字体,如仍有问题请手动安装 `fonts/` 目录下的字体文件。
|
||||||
|
|
||||||
|
### Q: 签名生成失败?
|
||||||
|
|
||||||
|
A: 检查签名图片路径是否正确,以及签名位置配置是否合适。
|
||||||
|
|
||||||
## 📄 许可证
|
## 📄 许可证
|
||||||
|
|
||||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||||
|
|||||||
225
UI.py
225
UI.py
@@ -1,225 +0,0 @@
|
|||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk
|
|
||||||
from tkinter import scrolledtext
|
|
||||||
from tkinter import messagebox
|
|
||||||
from tkinter import filedialog
|
|
||||||
import threading
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import queue
|
|
||||||
import re
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
from config.config import load_config
|
|
||||||
|
|
||||||
# 假设你的功能函数都在这里
|
|
||||||
from utils.generate_utils import (
|
|
||||||
generate_template,
|
|
||||||
generate_comment_all,
|
|
||||||
batch_convert_folder,
|
|
||||||
generate_report,
|
|
||||||
generate_zodiac,
|
|
||||||
)
|
|
||||||
from utils.file_utils import export_templates_folder, initialize_project, export_data
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# 0. 全局配置与队列准备
|
|
||||||
# ==========================================
|
|
||||||
config = load_config("config.toml")
|
|
||||||
log_queue = queue.Queue()
|
|
||||||
|
|
||||||
|
|
||||||
def ansi_cleaner(text):
|
|
||||||
"""【辅助函数】去除 loguru 输出中的 ANSI 颜色代码"""
|
|
||||||
ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
|
|
||||||
return ansi_escape.sub("", text)
|
|
||||||
|
|
||||||
|
|
||||||
def queue_sink(message):
|
|
||||||
"""【核心】loguru sink 回调"""
|
|
||||||
clean_msg = ansi_cleaner(message)
|
|
||||||
log_queue.put(clean_msg)
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# GUI 主程序类
|
|
||||||
# ==========================================
|
|
||||||
class ReportApp:
|
|
||||||
def __init__(self, root):
|
|
||||||
self.root = root
|
|
||||||
self.root.title("🌱 尚城幼儿园成长报告助手")
|
|
||||||
self.root.geometry("720x680") # 高度稍微增加一点以容纳分组
|
|
||||||
|
|
||||||
# 设置样式
|
|
||||||
self.style = ttk.Style()
|
|
||||||
self.style.theme_use("clam")
|
|
||||||
self.style.configure("TButton", font=("微软雅黑", 10), padding=5)
|
|
||||||
self.style.configure("Title.TLabel", font=("微软雅黑", 16, "bold"), foreground="#2E8B57")
|
|
||||||
self.style.configure("Sub.TLabel", font=("微软雅黑", 9), foreground="gray")
|
|
||||||
# LabelFrame 的标题样式
|
|
||||||
self.style.configure("TLabelframe.Label", font=("微软雅黑", 10, "bold"), foreground="#0055a3")
|
|
||||||
|
|
||||||
# --- 1. 标题区域 ---
|
|
||||||
header_frame = ttk.Frame(root, padding="10 15 10 5")
|
|
||||||
header_frame.pack(fill=tk.X)
|
|
||||||
ttk.Label(header_frame, text="🌱 尚城幼儿园成长报告助手", style="Title.TLabel").pack()
|
|
||||||
ttk.Label(header_frame, text="By 寒寒", style="Sub.TLabel").pack()
|
|
||||||
|
|
||||||
# --- 2. 按钮功能区域 (使用 LabelFrame 分组) ---
|
|
||||||
|
|
||||||
# 容器 Frame,给四周留点白
|
|
||||||
main_content = ttk.Frame(root, padding=10)
|
|
||||||
main_content.pack(fill=tk.X)
|
|
||||||
|
|
||||||
# === A组: 核心功能 ===
|
|
||||||
func_btns = [
|
|
||||||
("📁 生成图片路径", self.run_generate_folders),
|
|
||||||
("🤖 生成评语 (AI)", self.run_generate_comments),
|
|
||||||
("📊 生成报告 (PPT)", self.run_generate_report),
|
|
||||||
("📑 格式转换 (PDF)", self.run_convert_pdf),
|
|
||||||
("🐂 生肖转化 (生日)", self.run_zodiac),
|
|
||||||
]
|
|
||||||
self.create_btn_group(main_content, "🛠️ 核心功能", func_btns, columns=3)
|
|
||||||
|
|
||||||
# === B组: 数据导出 ===
|
|
||||||
export_btns = [
|
|
||||||
("📦 导出数据模板 (Zip)", self.run_export_data_folder),
|
|
||||||
("📤 导出数据备份 (Zip)", self.run_export_data),
|
|
||||||
]
|
|
||||||
self.create_btn_group(main_content, "📦 数据管理", export_btns, columns=2)
|
|
||||||
|
|
||||||
# === C组: 系统设置 ===
|
|
||||||
system_btns = [
|
|
||||||
("⚠️ 初始化系统 (重置)", self.run_initialize_project),
|
|
||||||
("🚪 退出系统", self.quit_app),
|
|
||||||
]
|
|
||||||
self.create_btn_group(main_content, "⚙️ 系统操作", system_btns, columns=2)
|
|
||||||
|
|
||||||
# --- 3. 日志输出区域 ---
|
|
||||||
log_frame = ttk.LabelFrame(root, text="📝 系统实时日志", padding=10)
|
|
||||||
log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
|
|
||||||
|
|
||||||
self.log_text = scrolledtext.ScrolledText(
|
|
||||||
log_frame, height=10, state="disabled", font=("Consolas", 9)
|
|
||||||
)
|
|
||||||
self.log_text.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# 启动日志轮询
|
|
||||||
self.root.after(100, self.poll_log_queue)
|
|
||||||
logger.info("GUI 初始化完成,等待指令...")
|
|
||||||
|
|
||||||
def create_btn_group(self, parent, title, buttons, columns=2):
|
|
||||||
"""
|
|
||||||
辅助函数:快速创建分组按钮
|
|
||||||
:param parent: 父容器
|
|
||||||
:param title: 分组标题
|
|
||||||
:param buttons: 按钮列表 [(text, func), ...]
|
|
||||||
:param columns: 每行显示几个按钮
|
|
||||||
"""
|
|
||||||
frame = ttk.LabelFrame(parent, text=title, padding=10)
|
|
||||||
frame.pack(fill=tk.X, pady=5) # 垂直堆叠
|
|
||||||
|
|
||||||
for index, (text, func) in enumerate(buttons):
|
|
||||||
# 特殊处理:如果是"初始化"或"退出",可以用不同的样式(可选,这里暂不做)
|
|
||||||
btn = ttk.Button(frame, text=text, command=func)
|
|
||||||
|
|
||||||
# 动态计算网格位置
|
|
||||||
r, c = divmod(index, columns)
|
|
||||||
btn.grid(row=r, column=c, padx=8, pady=5, sticky="ew")
|
|
||||||
|
|
||||||
# 配置列权重,让按钮自动填满宽度
|
|
||||||
for i in range(columns):
|
|
||||||
frame.columnconfigure(i, weight=1)
|
|
||||||
|
|
||||||
# --- 核心方法:轮询队列 ---
|
|
||||||
def poll_log_queue(self):
|
|
||||||
while not log_queue.empty():
|
|
||||||
try:
|
|
||||||
msg = log_queue.get_nowait()
|
|
||||||
self.log_text.config(state="normal")
|
|
||||||
self.log_text.insert(tk.END, msg)
|
|
||||||
self.log_text.see(tk.END)
|
|
||||||
self.log_text.config(state="disabled")
|
|
||||||
except queue.Empty:
|
|
||||||
break
|
|
||||||
self.root.after(100, self.poll_log_queue)
|
|
||||||
|
|
||||||
# --- 线程包装器 ---
|
|
||||||
def run_in_thread(self, target_func, *args):
|
|
||||||
def thread_task():
|
|
||||||
try:
|
|
||||||
target_func(*args)
|
|
||||||
logger.success("✅ 当前任务执行完毕。")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ 发生错误: {str(e)}")
|
|
||||||
import traceback
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
|
|
||||||
threading.Thread(target=thread_task, daemon=True).start()
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# 按钮事件 (业务逻辑保持不变)
|
|
||||||
# ==========================================
|
|
||||||
def run_generate_folders(self):
|
|
||||||
self.run_in_thread(generate_template)
|
|
||||||
|
|
||||||
def run_generate_comments(self):
|
|
||||||
self.run_in_thread(generate_comment_all)
|
|
||||||
|
|
||||||
def run_generate_report(self):
|
|
||||||
self.run_in_thread(generate_report)
|
|
||||||
|
|
||||||
def run_convert_pdf(self):
|
|
||||||
self.run_in_thread(batch_convert_folder, config["output_folder"])
|
|
||||||
|
|
||||||
def run_zodiac(self):
|
|
||||||
self.run_in_thread(generate_zodiac)
|
|
||||||
|
|
||||||
def run_export_data_folder(self):
|
|
||||||
target_folder = filedialog.askdirectory(
|
|
||||||
title="请选择导出数据保存的文件夹",
|
|
||||||
initialdir=config.get("output_folder", ".")
|
|
||||||
)
|
|
||||||
if not target_folder:
|
|
||||||
logger.warning("🚫 导出操作已取消")
|
|
||||||
return
|
|
||||||
logger.info(f"已选择保存路径: {target_folder}")
|
|
||||||
self.run_in_thread(export_templates_folder, target_folder)
|
|
||||||
|
|
||||||
def run_initialize_project(self):
|
|
||||||
self.run_in_thread(initialize_project)
|
|
||||||
|
|
||||||
def run_export_data(self):
|
|
||||||
target_folder = filedialog.askdirectory(
|
|
||||||
title="请选择导出数据保存的文件夹",
|
|
||||||
initialdir=config.get("output_folder", ".")
|
|
||||||
)
|
|
||||||
if not target_folder:
|
|
||||||
logger.warning("🚫 导出操作已取消")
|
|
||||||
return
|
|
||||||
logger.info(f"已选择保存路径: {target_folder}")
|
|
||||||
self.run_in_thread(export_data, target_folder)
|
|
||||||
|
|
||||||
def quit_app(self):
|
|
||||||
if messagebox.askokcancel("退出", "确定要退出系统吗?"):
|
|
||||||
self.root.destroy()
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# 启动入口
|
|
||||||
# ==========================================
|
|
||||||
def applicationUI():
|
|
||||||
logger.add(
|
|
||||||
queue_sink,
|
|
||||||
format="{time:HH:mm:ss} | {level: <8} | {message}",
|
|
||||||
level="INFO",
|
|
||||||
)
|
|
||||||
|
|
||||||
root = tk.Tk()
|
|
||||||
app = ReportApp(root)
|
|
||||||
root.mainloop()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
applicationUI()
|
|
||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
26
config.toml
Normal file
26
config.toml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[paths]
|
||||||
|
source_file = "大四班 幼儿学期发展报告.pptx"
|
||||||
|
output_folder = "output"
|
||||||
|
excel_file = "names.xlsx"
|
||||||
|
image_folder = "images"
|
||||||
|
fonts_dir = "fonts"
|
||||||
|
signature_image = "d:\\working\\tools\\growth_report\\data\\signature.png"
|
||||||
|
|
||||||
|
[class_info]
|
||||||
|
class_name = "K4D"
|
||||||
|
teachers = [
|
||||||
|
"康璐璐",
|
||||||
|
"冯宇阳",
|
||||||
|
"孙继艳",
|
||||||
|
]
|
||||||
|
class_type = 2
|
||||||
|
|
||||||
|
[defaults]
|
||||||
|
default_comment = ""
|
||||||
|
age_group = "大班上学期"
|
||||||
|
|
||||||
|
[ai]
|
||||||
|
api_key = "sk-ccb6a1445126f2e56ba50d7622ff350f"
|
||||||
|
api_url = "https://apis.iflow.cn/v1/chat/completions"
|
||||||
|
model = "deepseek-v3.2"
|
||||||
|
prompt = "# Role \n你是一位拥有20年经验的资深幼儿园主班老师。你的文笔温暖、细腻、充满爱意,擅长发现每个孩子身上独特的闪光点。你的评语风格是“治愈系”的,能让家长读完后感到欣慰并对未来充满希望。\n\n# Goal\n请根据用户提供的【幼儿姓名】、【年龄段/班级】以及【日常表现关键词/评分数据】,撰写一份高质量的学期末成长评语。\n# Constraints & Rules\n1. **严格的格式排版 (Strict Formatting)**:\n- **换行**:正文中间不要随意换行,保持为一段完整的段落。\n2. **称呼处理**:\n- 自动识别用户输入的姓名,去掉姓氏。\n- 例如:“王小明” -> 第一行输出“小明宝贝:”。\n3. **分龄侧重 (根据 Age_Group 调整侧重点)**:\n- **小班 (3-4岁)**:侧重于适应集体生活、情绪稳定性、基本生活自理能力、愿意与老师互动。\n- **中班 (4-5岁)**:侧重于社交互动、分享与合作、动手能力、好奇心、规则意识。\n- **大班 (5-6岁)**:侧重于学习习惯、逻辑思维、领导力、任务意识、幼小衔接准备。\n4. **写作结构 (固定内容)**:\n- **开头**:固定文本必须包含:“{class_type}”\n- **正文**:结合【表现关键词】和【性别】,具体描述进步和优点。\n- **结尾**:委婉地提出期望(“如果你能...老师会更为你骄傲”),并送上祝福。\n5. **语气风格**:\n- 积极正面,多用肯定句。\n- 字数控制在 150-250 字之间。\n# Input Format\n- Name {{name}}\n- Age_Group {{class_name}}\n- Traits {{traits}}\n- Sex {{sex}}\n# Output Example\n(假设输入:Name=张图图, Age_Group=小班, Traits=适应能力强, 爱笑, 挑食,Sex=女)\n亲爱的图图宝贝:{class_type}\n你是一个爱笑的小天使,每天早上都能看到你甜甜的笑脸。从一开始的哭鼻子到现在能开心地参与游戏,你的适应能力让老师感到惊喜。不过,老师发现你在吃饭时偶尔会把不喜欢的青菜挑出来哦。如果你能和青菜宝宝做好朋友,把身体练得棒棒的,那就更完美啦!祝可爱的图图宝贝新年快乐,健康成长!\n"
|
||||||
163
config/config.py
163
config/config.py
@@ -1,82 +1,145 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# 尝试导入 toml 解析库
|
# 1. 处理读取库
|
||||||
try:
|
try:
|
||||||
import tomllib as toml # Python 3.11+
|
import tomllib as toml_read # Python 3.11+
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
import tomli as toml # pip install tomli
|
import tomli as toml_read
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("错误: 缺少 TOML 解析库。请运行: pip install tomli")
|
print("错误: 缺少 TOML 读取库。请运行: pip install tomli")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 2. 处理写入库 (必须安装 pip install tomli-w)
|
||||||
|
try:
|
||||||
|
import tomli_w as toml_write
|
||||||
|
except ImportError:
|
||||||
|
# 如果没安装,提供一个 fallback 提示
|
||||||
|
toml_write = None
|
||||||
|
|
||||||
def get_main_path():
|
|
||||||
"""
|
def get_base_dir():
|
||||||
获取程序运行的根目录
|
|
||||||
兼容:
|
|
||||||
1. PyInstaller 打包后的 .exe 环境
|
|
||||||
2. 开发环境 (假设此脚本在子文件夹中,如 utils/)
|
|
||||||
"""
|
|
||||||
if getattr(sys, "frozen", False):
|
if getattr(sys, "frozen", False):
|
||||||
# --- 情况 A: 打包后的 exe ---
|
|
||||||
# exe 就在根目录下,直接取 exe 所在目录
|
|
||||||
return os.path.dirname(sys.executable)
|
return os.path.dirname(sys.executable)
|
||||||
else:
|
else:
|
||||||
# --- 情况 B: 开发环境 (.py) ---
|
# 假设当前文件在项目根目录或根目录下的某个文件夹中
|
||||||
# 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
|
base_path = get_base_dir()
|
||||||
project_root = os.path.dirname(current_dir)
|
external_path = os.path.join(base_path, relative_path)
|
||||||
|
if os.path.exists(external_path):
|
||||||
return project_root
|
return external_path
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
internal_path = os.path.join(sys._MEIPASS, relative_path)
|
||||||
|
if os.path.exists(internal_path):
|
||||||
|
return internal_path
|
||||||
|
return external_path
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 1. 配置加载 (Config Loader)
|
# 1. 配置加载 (Config Loader)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
def load_config(config_filename="config.toml"):
|
def load_config(config_filename="config.toml"):
|
||||||
"""读取 TOML 配置文件"""
|
config_path = get_resource_path(config_filename)
|
||||||
|
|
||||||
# 1. 先获取正确的根目录
|
|
||||||
main_dir = get_main_path()
|
|
||||||
|
|
||||||
# 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_path}")
|
return {"source_file": "", "ai": {"api_key": ""}, "teachers": []}
|
||||||
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_read.load(f)
|
||||||
|
|
||||||
|
base_dir = get_base_dir()
|
||||||
|
|
||||||
|
# 使用 .get() 安全获取,防止 KeyError: 'paths'
|
||||||
|
paths = data.get("paths", {})
|
||||||
|
class_info = data.get("class_info", {})
|
||||||
|
defaults = data.get("defaults", {})
|
||||||
|
|
||||||
# 将 TOML 的层级结构映射回扁平结构
|
|
||||||
# 关键点:所有的 os.path.join 都必须基于 main_dir (项目根目录)
|
|
||||||
config = {
|
config = {
|
||||||
"root_path": main_dir, # 方便调试,把根目录也存进去
|
"root_path": base_dir,
|
||||||
"source_file": os.path.join(
|
"data_folder": os.path.join(os.path.join("data")),
|
||||||
main_dir, "templates", data["paths"]["source_file"]
|
# 扁平化映射
|
||||||
|
"source_file": get_resource_path(
|
||||||
|
os.path.join("templates", paths.get("source_file", ""))
|
||||||
|
),
|
||||||
|
"excel_file": get_resource_path(
|
||||||
|
os.path.join("data", paths.get("excel_file", ""))
|
||||||
|
),
|
||||||
|
"image_folder": get_resource_path(
|
||||||
|
os.path.join("data", paths.get("image_folder", ""))
|
||||||
|
),
|
||||||
|
"fonts_dir": get_resource_path(paths.get("fonts_dir", "fonts")),
|
||||||
|
"output_folder": os.path.join(
|
||||||
|
base_dir, paths.get("output_folder", "output")
|
||||||
|
),
|
||||||
|
"signature_image": get_resource_path(
|
||||||
|
os.path.join("data", paths.get("signature_image", "signature.png"))
|
||||||
|
),
|
||||||
|
"class_name": class_info.get("class_name", "未命名班级"),
|
||||||
|
"teachers": class_info.get("teachers", []),
|
||||||
|
"class_type": class_info.get("class_type", 0),
|
||||||
|
"default_comment": defaults.get("default_comment", "暂无评语"),
|
||||||
|
"age_group": defaults.get("age_group", "大班上学期"),
|
||||||
|
"ai": data.get(
|
||||||
|
"ai", {"api_key": "", "api_url": "", "model": "", "prompt": ""}
|
||||||
),
|
),
|
||||||
"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"]),
|
|
||||||
"class_name": data["class_info"]["class_name"],
|
|
||||||
"teachers": data["class_info"]["teachers"],
|
|
||||||
"default_comment": data["defaults"].get("default_comment", "暂无评语"),
|
|
||||||
"age_group": data["defaults"].get("age_group", "大班上学期"),
|
|
||||||
"ai": data["ai"],
|
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"读取配置文件出错: {e}")
|
print(f"解析配置文件失败: {e}")
|
||||||
sys.exit(1)
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 2. 配置保存 (Config Saver)
|
||||||
|
# ==========================================
|
||||||
|
def save_config(config_data, config_filename="config.toml"):
|
||||||
|
if not toml_write:
|
||||||
|
return False, "未安装 tomli-w 库,无法保存。请运行 pip install tomli-w"
|
||||||
|
|
||||||
|
base_path = get_base_dir()
|
||||||
|
save_path = os.path.join(base_path, config_filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 将扁平化的数据重新打包成嵌套结构,以适配 load_config 的读取逻辑
|
||||||
|
new_data = {
|
||||||
|
"paths": {
|
||||||
|
"source_file": os.path.basename(config_data.get("source_file", "")),
|
||||||
|
"output_folder": os.path.basename(
|
||||||
|
config_data.get("output_folder", "output")
|
||||||
|
),
|
||||||
|
"excel_file": os.path.basename(config_data.get("excel_file", "")),
|
||||||
|
"image_folder": os.path.basename(config_data.get("image_folder", "")),
|
||||||
|
"fonts_dir": os.path.basename(config_data.get("fonts_dir", "fonts")),
|
||||||
|
"signature_image": get_resource_path(
|
||||||
|
os.path.join(
|
||||||
|
"data", config_data.get("signature_image", "signature.png")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"class_info": {
|
||||||
|
"class_name": config_data.get("class_name", ""),
|
||||||
|
"teachers": config_data.get("teachers", []),
|
||||||
|
"class_type": config_data.get("class_type", 0),
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
|
"default_comment": config_data.get("default_comment", ""),
|
||||||
|
"age_group": config_data.get("age_group", ""),
|
||||||
|
},
|
||||||
|
"ai": config_data.get("ai", {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 写入文件
|
||||||
|
with open(save_path, "wb") as f:
|
||||||
|
f.write(toml_write.dumps(new_data).encode("utf-8"))
|
||||||
|
|
||||||
|
return True, f"成功保存到: {save_path}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"写入失败: {str(e)}"
|
||||||
|
|||||||
BIN
data/names.xlsx
BIN
data/names.xlsx
Binary file not shown.
BIN
data/signature.png
Normal file
BIN
data/signature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
229
main.py
229
main.py
@@ -1,117 +1,126 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from nicegui import ui, app, run, native
|
||||||
|
from screeninfo import get_monitors
|
||||||
|
|
||||||
from config.config import load_config
|
from config.config import load_config
|
||||||
from utils.generate_utils import (
|
|
||||||
generate_template,
|
|
||||||
generate_comment_all,
|
|
||||||
generate_report,
|
|
||||||
batch_convert_folder,
|
|
||||||
generate_zodiac,
|
|
||||||
)
|
|
||||||
from utils.file_utils import export_templates_folder, initialize_project, export_data
|
|
||||||
|
|
||||||
config = load_config("config.yaml")
|
# 导入我们的模块
|
||||||
|
from ui.core.logger import setup_logger
|
||||||
|
from ui.views.config_page import create_config_page
|
||||||
|
from ui.views.home_page import create_home_page
|
||||||
|
from ui.views.data_page import create_data_page
|
||||||
|
from ui.views.signature_page import create_signature_page
|
||||||
|
from utils.font_utils import install_fonts_from_directory
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
# 1. 初始化配置
|
||||||
|
config = load_config("config.toml")
|
||||||
|
|
||||||
|
setup_logger()
|
||||||
|
|
||||||
|
|
||||||
def application():
|
# === 关键修改:定义一个获取路径的通用函数 ===
|
||||||
from rich.console import Console
|
def get_path(relative_path):
|
||||||
from rich.panel import Panel
|
"""
|
||||||
from rich.prompt import Prompt
|
获取资源的绝对路径。
|
||||||
from rich.table import Table
|
兼容:开发环境(直接运行) 和 生产环境(打包成exe后解压的临时目录)
|
||||||
from rich.align import Align
|
"""
|
||||||
from rich import box
|
if hasattr(sys, "_MEIPASS"):
|
||||||
import sys
|
base_path = sys._MEIPASS
|
||||||
|
else:
|
||||||
|
# 开发环境当前目录
|
||||||
|
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
console = Console()
|
return os.path.join(base_path, relative_path)
|
||||||
|
|
||||||
while True:
|
|
||||||
console.clear()
|
|
||||||
|
|
||||||
# 1. 创建一个表格,不显示表头,使用圆角边框
|
|
||||||
table = Table(box=None, show_header=False, padding=(0, 2))
|
|
||||||
|
|
||||||
# 2. 添加两列:序号列(居右),内容列(居左)
|
|
||||||
table.add_column(justify="right", style="cyan bold")
|
|
||||||
table.add_column(justify="left")
|
|
||||||
|
|
||||||
# 3. 添加行内容
|
|
||||||
table.add_row("1.", "📁 生成图片路径(每一个幼儿一个图片文件夹)")
|
|
||||||
table.add_row("2.", "🤖 生成评语(根据姓名、学段、性别)")
|
|
||||||
table.add_row("3.", "📊 生成报告(根据表格生成)")
|
|
||||||
table.add_row("4.", "📑 格式转换(PPT转PDF)")
|
|
||||||
table.add_row("5.", "📑 生肖转化(根据生日)")
|
|
||||||
table.add_row("6.", "📦 导出数据模板(Zip)")
|
|
||||||
table.add_row("7.", "📦 初始化系统")
|
|
||||||
table.add_row("8.", "📤 导出数据")
|
|
||||||
table.add_row("9.", "🚪 退出系统")
|
|
||||||
|
|
||||||
# 4. 将表格放入面板,并居中显示
|
|
||||||
panel = Panel(
|
|
||||||
Align.center(table),
|
|
||||||
title="[bold green]🌱 幼儿园成长报告助手",
|
|
||||||
subtitle="[dim]By 寒寒",
|
|
||||||
width=60,
|
|
||||||
border_style="bright_blue",
|
|
||||||
box=box.ROUNDED, # 圆角边框更柔和
|
|
||||||
)
|
|
||||||
|
|
||||||
# 使用 Align.center 让整个菜单在屏幕中间显示
|
|
||||||
console.print(Align.center(panel, vertical="middle"))
|
|
||||||
console.print("\n") # 留点空隙
|
|
||||||
|
|
||||||
choice = Prompt.ask(
|
|
||||||
"👉 请输入序号执行",
|
|
||||||
choices=["1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
|
||||||
default="1",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if choice == "1":
|
|
||||||
console.rule("[bold cyan]正在执行: 生成模板[/]")
|
|
||||||
with console.status(
|
|
||||||
"[bold green]正在创建文件夹结构...[/]", spinner="dots"
|
|
||||||
):
|
|
||||||
generate_template()
|
|
||||||
elif choice == "2":
|
|
||||||
console.rule("[bold yellow]正在执行: AI 生成评语[/]")
|
|
||||||
# 这里的 generate_comment_all 最好内部有进度条,或者简单的 print
|
|
||||||
generate_comment_all()
|
|
||||||
elif choice == "3":
|
|
||||||
console.rule("[bold blue]正在执行: PPT 合成[/]")
|
|
||||||
with console.status(
|
|
||||||
"[bold blue]正在处理图片和文字...[/]", spinner="earth"
|
|
||||||
):
|
|
||||||
generate_report()
|
|
||||||
elif choice == "4":
|
|
||||||
console.rule("[bold magenta]正在执行: PDF 批量转换[/]")
|
|
||||||
# 调用上面的批量转换函数,传入你的 output 文件夹路径
|
|
||||||
batch_convert_folder(config["output_folder"])
|
|
||||||
elif choice == "5":
|
|
||||||
console.rule("[bold magenta]正在执行: 生肖转化[/]")
|
|
||||||
# 调用上面的批量转换函数,传入你的 output 文件夹路径
|
|
||||||
generate_zodiac()
|
|
||||||
elif choice == "6":
|
|
||||||
console.rule("[bold magenta]正在执行: 导出数据模板[/]")
|
|
||||||
# 调用上面的批量转换函数,传入你的 output 文件夹路径
|
|
||||||
export_templates_folder()
|
|
||||||
elif choice == "7":
|
|
||||||
console.rule("[bold magenta]正在执行: 初始化系统[/]")
|
|
||||||
# 调用上面的批量转换函数,传入你的 output 文件夹路径
|
|
||||||
initialize_project()
|
|
||||||
elif choice == "8":
|
|
||||||
console.rule("[bold magenta]正在执行: 导出数据[/]")
|
|
||||||
# 调用上面的批量转换函数,传入你的 output 文件夹路径
|
|
||||||
export_data()
|
|
||||||
elif choice == "9":
|
|
||||||
console.print("[bold red]👋 再见![/]")
|
|
||||||
sys.exit()
|
|
||||||
Prompt.ask("按 [bold]Enter[/] 键返回主菜单...")
|
|
||||||
except Exception as e:
|
|
||||||
console.print(
|
|
||||||
Panel(
|
|
||||||
f"[bold red]❌ 发生错误:[/]\n{e}", title="Error", border_style="red"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Prompt.ask("按 Enter 键继续...")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def calculate_window_size():
|
||||||
application()
|
"""
|
||||||
|
获取主屏幕分辨率,并计算一个基于百分比的 NiceGUI 窗口大小。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 尝试获取所有显示器信息
|
||||||
|
monitors = get_monitors()
|
||||||
|
if monitors:
|
||||||
|
# 假设第一个是主显示器
|
||||||
|
m = monitors[0]
|
||||||
|
screen_width = m.width
|
||||||
|
screen_height = m.height
|
||||||
|
|
||||||
|
# 设置窗口宽度为屏幕宽度的 30%
|
||||||
|
target_width = int(screen_width * 0.30)
|
||||||
|
# 设置窗口高度为屏幕高度的 60%
|
||||||
|
target_height = int(screen_height * 0.60)
|
||||||
|
|
||||||
|
# 确保窗口有一个合理的最小值 (例如 800x600)
|
||||||
|
min_width = 800
|
||||||
|
min_height = 700
|
||||||
|
|
||||||
|
target_width = max(target_width, min_width)
|
||||||
|
target_height = max(target_height, min_height)
|
||||||
|
|
||||||
|
logger.info(f"屏幕分辨率: {screen_width}x{screen_height}")
|
||||||
|
logger.info(f"设置窗口大小为: {target_width}x{target_height}")
|
||||||
|
|
||||||
|
return target_width, target_height
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"无法获取屏幕分辨率 ({e}),使用默认大小 (900, 900)")
|
||||||
|
return 900, 900 # 失败时的默认值
|
||||||
|
|
||||||
|
|
||||||
|
# 1. 挂载静态资源 (CSS/图片)
|
||||||
|
# 注意:这里使用 get_path 确保打包后能找到
|
||||||
|
static_dir = get_path(os.path.join("ui", "assets"))
|
||||||
|
app.add_static_files("/assets", static_dir)
|
||||||
|
|
||||||
|
|
||||||
|
# 3. 页面路由
|
||||||
|
@ui.page("/")
|
||||||
|
def index_page():
|
||||||
|
create_home_page()
|
||||||
|
|
||||||
|
|
||||||
|
@ui.page("/config")
|
||||||
|
def config_page():
|
||||||
|
create_config_page()
|
||||||
|
|
||||||
|
|
||||||
|
@ui.page("/data")
|
||||||
|
def data_page():
|
||||||
|
create_data_page()
|
||||||
|
|
||||||
|
|
||||||
|
@ui.page("/signature")
|
||||||
|
def signature_page(folder: str = ""):
|
||||||
|
create_signature_page(folder)
|
||||||
|
|
||||||
|
|
||||||
|
# 4. 启动时钩子
|
||||||
|
async def startup_check():
|
||||||
|
try:
|
||||||
|
logger.info("系统启动: 初始化资源...")
|
||||||
|
await run.io_bound(install_fonts_from_directory, config["fonts_dir"])
|
||||||
|
os.makedirs(config["output_folder"], exist_ok=True)
|
||||||
|
logger.success("资源初始化完成")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"初始化失败: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
app.on_startup(startup_check)
|
||||||
|
|
||||||
|
if __name__ in {"__main__", "__mp_main__"}:
|
||||||
|
calculated_size = calculate_window_size()
|
||||||
|
ui.run(
|
||||||
|
title="尚城幼儿园成长报告助手",
|
||||||
|
native=True,
|
||||||
|
window_size=calculated_size,
|
||||||
|
port=native.find_open_port(), # 自动寻找端口
|
||||||
|
reload=True,
|
||||||
|
)
|
||||||
|
|||||||
39
main.spec
Normal file
39
main.spec
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['main.pyw'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='main',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=['public\\icon.ico'],
|
||||||
|
)
|
||||||
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
Normal file
BIN
public/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -9,13 +9,19 @@ dependencies = [
|
|||||||
"langchain>=1.1.3",
|
"langchain>=1.1.3",
|
||||||
"langchain-openai>=1.1.1",
|
"langchain-openai>=1.1.1",
|
||||||
"loguru>=0.7.3",
|
"loguru>=0.7.3",
|
||||||
|
"nicegui>=3.4.0",
|
||||||
"openpyxl>=3.1.5",
|
"openpyxl>=3.1.5",
|
||||||
"pandas>=2.3.3",
|
"pandas>=2.3.3",
|
||||||
"pandas-stubs==2.3.3.251201",
|
"pandas-stubs==2.3.3.251201",
|
||||||
|
"pillow>=12.0.0",
|
||||||
|
"pyinstaller>=6.17.0",
|
||||||
"python-pptx>=1.0.2",
|
"python-pptx>=1.0.2",
|
||||||
|
"pywebview>=6.1",
|
||||||
"pywin32>=311",
|
"pywin32>=311",
|
||||||
"rich>=14.2.0",
|
"rich>=14.2.0",
|
||||||
|
"screeninfo>=0.8.1",
|
||||||
"tomli>=2.3.0",
|
"tomli>=2.3.0",
|
||||||
|
"tomli-w>=1.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[tool.uv.index]]
|
[[tool.uv.index]]
|
||||||
|
|||||||
112
script/setup.py
Normal file
112
script/setup.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# setup.py
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
MAIN_FILE = "main_nicegui.py"
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
('ui/assets', 'ui/assets'), # 复制 ui/assets 文件夹 到 dist/ui/assets
|
||||||
|
]
|
||||||
|
|
||||||
|
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_FILE
|
||||||
|
]
|
||||||
|
|
||||||
|
# 添加资源参数
|
||||||
|
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()
|
||||||
107
script/setup_nicegui.py
Normal file
107
script/setup_nicegui.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import platform
|
||||||
|
|
||||||
|
MAIN_FILE = "main_nicegui.py"
|
||||||
|
# 【关键修改】提取应用名称为变量,确保打包目录和复制目录一致
|
||||||
|
APP_NAME = "尚城幼儿园幼儿学期发展报告"
|
||||||
|
|
||||||
|
|
||||||
|
def copy_resources():
|
||||||
|
"""
|
||||||
|
将资源文件从项目根目录复制到 dist/APP_NAME 文件夹中 (与 exe 同级),
|
||||||
|
以便用户可以直接在 exe 旁边修改这些文件。
|
||||||
|
"""
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
project_root = os.path.dirname(current_dir)
|
||||||
|
|
||||||
|
# 【关键修改】目标目录改为 dist/APP_NAME
|
||||||
|
# 这样资源文件才会出现在 exe 旁边,而不是 dist 根目录
|
||||||
|
dist_dir = os.path.join(current_dir, "dist", APP_NAME)
|
||||||
|
|
||||||
|
print(f"\n--- 正在复制外部资源到 {dist_dir} ---")
|
||||||
|
|
||||||
|
if not os.path.exists(dist_dir):
|
||||||
|
print(f"错误: 目标文件夹不存在: {dist_dir}")
|
||||||
|
print("请检查是否打包成功,或者是否使用了 --onedir 模式。")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 这里的列表是【给用户看/改的】
|
||||||
|
resources_to_copy = [
|
||||||
|
("config.toml", ""),
|
||||||
|
("fonts", "fonts"),
|
||||||
|
("data", "data"),
|
||||||
|
("templates", "templates"),
|
||||||
|
("public", "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)
|
||||||
|
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 打包"""
|
||||||
|
|
||||||
|
sep = ';' if platform.system() == "Windows" else ':'
|
||||||
|
|
||||||
|
# 内部资源 (打入 exe 内部的)
|
||||||
|
resource_paths = [
|
||||||
|
f"../config.toml{sep}.",
|
||||||
|
f"../fonts{sep}fonts",
|
||||||
|
f"../templates{sep}templates",
|
||||||
|
f"../ui/assets{sep}ui/assets",
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
command = [
|
||||||
|
sys.executable, "-m", "PyInstaller",
|
||||||
|
"--onedir", # 文件夹模式
|
||||||
|
"--windowed", # 隐藏控制台 (调试时建议先注释掉)
|
||||||
|
f"--name={APP_NAME}", # 【关键修改】使用变量
|
||||||
|
"--clean",
|
||||||
|
"--distpath=./dist",
|
||||||
|
"--workpath=./build",
|
||||||
|
"--icon=../public/icon.ico",
|
||||||
|
"../" + MAIN_FILE
|
||||||
|
]
|
||||||
|
|
||||||
|
for res in resource_paths:
|
||||||
|
command.append(f"--add-data={res}")
|
||||||
|
|
||||||
|
command.extend(["--hidden-import=nicegui", "--hidden-import=uvicorn"])
|
||||||
|
|
||||||
|
print("--- 开始打包 (PyInstaller) ---")
|
||||||
|
subprocess.run(command, check=True, cwd=os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
print("\n--- PyInstaller 打包完成!---")
|
||||||
|
|
||||||
|
# 打包完成后执行复制
|
||||||
|
copy_resources()
|
||||||
|
|
||||||
|
# 打印最终 exe 的位置提示
|
||||||
|
exe_path = os.path.join("dist", APP_NAME, f"{APP_NAME}.exe")
|
||||||
|
print(f"\n🎉 全部完成!可执行文件位于: {exe_path}")
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"\n❌ 打包失败: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ 发生错误: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
build_exe()
|
||||||
45
script/尚城幼儿园幼儿学期发展报告.spec
Normal file
45
script/尚城幼儿园幼儿学期发展报告.spec
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['..\\main_nicegui.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[('../config.toml', '.'), ('../fonts', 'fonts'), ('../templates', 'templates'), ('../ui/assets', 'ui/assets')],
|
||||||
|
hiddenimports=['nicegui', 'uvicorn'],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='尚城幼儿园幼儿学期发展报告',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon=['..\\public\\icon.ico'],
|
||||||
|
)
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name='尚城幼儿园幼儿学期发展报告',
|
||||||
|
)
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
chcp 65001 >nul
|
chcp 65001 >nul
|
||||||
:: ------------------------------------------------
|
:: ------------------------------------------------
|
||||||
|
|
||||||
title 幼儿园成长报告助手
|
title 幼儿园成长报告助手启动器
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
@@ -13,7 +13,7 @@ echo 正在启动 幼儿园成长报告助手
|
|||||||
echo ==========================================
|
echo ==========================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
:: 检查 uv 是否安装
|
:: 1. 检查 uv 是否安装
|
||||||
uv --version >nul 2>&1
|
uv --version >nul 2>&1
|
||||||
if %errorlevel% neq 0 (
|
if %errorlevel% neq 0 (
|
||||||
echo [ERROR] 未检测到 uv 工具!
|
echo [ERROR] 未检测到 uv 工具!
|
||||||
@@ -22,21 +22,28 @@ if %errorlevel% neq 0 (
|
|||||||
exit /b
|
exit /b
|
||||||
)
|
)
|
||||||
|
|
||||||
echo [INFO] 环境检查通过,正在运行主程序...
|
echo [INFO] 环境检查通过...
|
||||||
|
|
||||||
|
:: 2. 检查依赖是否安装 (可选,防止第一次运行报错)
|
||||||
|
:: 如果你有 pyproject.toml,uv run 会自动处理,这一步可以省略
|
||||||
|
:: 这里为了保险,检查一下 loguru 是否存在,不存在则自动安装基础依赖
|
||||||
|
uv pip show loguru >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo [INFO] 首次运行,正在安装依赖...
|
||||||
|
uv pip install loguru toml pandas pillow openpyxl python-pptx
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [INFO] 正在拉起主程序...
|
||||||
echo ---------------------------------------------------
|
echo ---------------------------------------------------
|
||||||
|
|
||||||
:: 这里的 gui_app.py 就是你刚才保存的那个带界面的 Python 文件名
|
:: =======================================================
|
||||||
:: 如果你的文件名不一样,请修改下面这一行
|
:: 【关键修改】路径改为根目录的 main.py
|
||||||
uv run UI.py
|
:: 使用 start 命令启动,这样黑色的 CMD 窗口可以随后立即关闭
|
||||||
|
:: =======================================================
|
||||||
|
start "" uv run main.py
|
||||||
|
|
||||||
:: 错误捕获
|
:: 等待 1 秒确保启动
|
||||||
if %errorlevel% neq 0 (
|
timeout /t 1 >nul
|
||||||
echo.
|
|
||||||
echo ---------------------------------------------------
|
:: 退出 CMD 窗口 (让用户只看到 GUI)
|
||||||
echo [ERROR] 程序异常退出 (代码: %errorlevel%)
|
exit
|
||||||
echo 请检查上方报错信息。
|
|
||||||
pause
|
|
||||||
) else (
|
|
||||||
echo.
|
|
||||||
echo [INFO] 程序已正常结束。
|
|
||||||
)
|
|
||||||
Binary file not shown.
Binary file not shown.
BIN
templates/中一班 幼儿学期发展报告.pptx
Normal file
BIN
templates/中一班 幼儿学期发展报告.pptx
Normal file
Binary file not shown.
BIN
templates/大二班 幼儿学期发展报告.pptx
Normal file
BIN
templates/大二班 幼儿学期发展报告.pptx
Normal file
Binary file not shown.
BIN
templates/大四班 幼儿学期发展报告.pptx
Normal file
BIN
templates/大四班 幼儿学期发展报告.pptx
Normal file
Binary file not shown.
BIN
templates/小三班 幼儿学期发展报告.pptx
Normal file
BIN
templates/小三班 幼儿学期发展报告.pptx
Normal file
Binary file not shown.
BIN
templates/(定制款 横板)中班 幼儿学期发展报告.pptx
Normal file
BIN
templates/(定制款 横板)中班 幼儿学期发展报告.pptx
Normal file
Binary file not shown.
BIN
templates/(定制款)大班幼儿学期发展报告.pptx
Normal file
BIN
templates/(定制款)大班幼儿学期发展报告.pptx
Normal file
Binary file not shown.
BIN
templates/(横板)中班 幼儿学期发展报告.pptx
Normal file
BIN
templates/(横板)中班 幼儿学期发展报告.pptx
Normal file
Binary file not shown.
BIN
templates/(横板)大班 幼儿学期发展报告.pptx
Normal file
BIN
templates/(横板)大班 幼儿学期发展报告.pptx
Normal file
Binary file not shown.
BIN
templates/(横板)小班 幼儿学期发展报告.pptx
Normal file
BIN
templates/(横板)小班 幼儿学期发展报告.pptx
Normal file
Binary file not shown.
BIN
templates/(竖版)中班 幼儿学期发展报告.pptx
Normal file
BIN
templates/(竖版)中班 幼儿学期发展报告.pptx
Normal file
Binary file not shown.
Binary file not shown.
BIN
templates/(竖版)小班 幼儿学期发展报告.pptx
Normal file
BIN
templates/(竖版)小班 幼儿学期发展报告.pptx
Normal file
Binary file not shown.
BIN
ui/assets/icon.ico
Normal file
BIN
ui/assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
182
ui/assets/style.css
Normal file
182
ui/assets/style.css
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
/* assets/style.css */
|
||||||
|
|
||||||
|
/* 全局字体 */
|
||||||
|
body {
|
||||||
|
font-family: '微软雅黑', 'Microsoft YaHei', sans-serif;
|
||||||
|
background-color: #f0f4f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题栏 */
|
||||||
|
.app-header {
|
||||||
|
background-color: #2e8b57; /* SeaGreen */
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片通用样式 */
|
||||||
|
.func-card {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 核心功能区顶部边框 */
|
||||||
|
.card-core {
|
||||||
|
border-top: 4px solid #16a34a; /* green-600 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 数据管理区顶部边框 */
|
||||||
|
.card-data {
|
||||||
|
border-top: 4px solid #3b82f6; /* blue-500 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 系统操作区顶部边框 */
|
||||||
|
.card-system {
|
||||||
|
border-top: 4px solid #ef4444; /* red-500 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-logging {
|
||||||
|
border-top: 4px solid #9c1be0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题文字 */
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 绿色标题 */
|
||||||
|
.text-green {
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
/* 蓝色标题 */
|
||||||
|
.text-blue {
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
/* 红色标题 */
|
||||||
|
.text-red {
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* assets/style.css */
|
||||||
|
|
||||||
|
/* ... (原有的样式) ... */
|
||||||
|
|
||||||
|
/* ---------------------------------- */
|
||||||
|
/* 滚动条隐藏样式 */
|
||||||
|
/* ---------------------------------- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 1. 隐藏 Webkit 内核浏览器 (Chrome, Safari, Edge) 的滚动条
|
||||||
|
* 适用于 NiceGUI 默认的 Chromium 浏览器
|
||||||
|
*/
|
||||||
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
|
/* 完全隐藏滚动条 */
|
||||||
|
width: 0px;
|
||||||
|
background: transparent; /* 使滚动条轨道透明 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. 隐藏 Firefox 浏览器的滚动条 */
|
||||||
|
.hide-scrollbar {
|
||||||
|
/* 设置滚动条宽度为 thin (细),比 auto (默认) 要窄 */
|
||||||
|
scrollbar-width: none; /* 'none' 是最新且更彻底的隐藏方式 */
|
||||||
|
|
||||||
|
/* 确保容器内容溢出时可以滚动 */
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 示例:如果你只想隐藏日志区的滚动条 */
|
||||||
|
.card-logging .q-expansion-item__content .nicegui-log .q-scrollarea__content {
|
||||||
|
/* 如果 nicegui-log 内部使用了 q-scrollarea,可能需要针对其内容应用样式 */
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------- */
|
||||||
|
/* 滚动条美化(可选 - 不隐藏,但变细变淡) */
|
||||||
|
/* 如果完全隐藏不好,可以试试这个更温和的方案 */
|
||||||
|
/* ---------------------------------- */
|
||||||
|
.thin-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 6px; /* 调整宽度 */
|
||||||
|
height: 6px; /* 调整高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.thin-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #a0a0a0; /* 拇指颜色 */
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid #f0f4f8; /* 边框颜色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.thin-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thin-scrollbar {
|
||||||
|
scrollbar-width: thin; /* Firefox 细滚动条 */
|
||||||
|
scrollbar-color: #a0a0a0 transparent; /* Firefox 颜色设置 */
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
/* 完全隐藏滚动条 */
|
||||||
|
width: 0px !important;
|
||||||
|
height: 0px !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
#nicegui-content,
|
||||||
|
#q-app {
|
||||||
|
/* 确保容器内容可以滚动,但滚动条被隐藏 */
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
/* Firefox 隐藏滚动条 */
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
/* IE/Edge 隐藏滚动条 */
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
/* 档案卡片容器 */
|
||||||
|
.profile-dialog {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
/* 档案条目样式 */
|
||||||
|
.info-item {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.info-item:hover {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.08);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
/* 标签与内容的对比度 */
|
||||||
|
.info-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #94a3b8; /* 浅灰色标签 */
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
width: 100px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.info-value {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1e293b; /* 深色内容 */
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
/* 自定义滚动条 */
|
||||||
|
.custom-scroll::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
.custom-scroll::-webkit-scrollbar-thumb {
|
||||||
|
background: #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
15
ui/core/logger.py
Normal file
15
ui/core/logger.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import sys
|
||||||
|
from loguru import logger
|
||||||
|
from ui.core.state import app_state
|
||||||
|
|
||||||
|
class GuiLogger:
|
||||||
|
def write(self, message):
|
||||||
|
if app_state.log_element:
|
||||||
|
app_state.log_element.push(message.strip())
|
||||||
|
|
||||||
|
def setup_logger():
|
||||||
|
logger.remove()
|
||||||
|
# 控制台输出
|
||||||
|
logger.add(sys.stderr, format="{time:HH:mm:ss} | {level} | {message}")
|
||||||
|
# GUI 输出
|
||||||
|
logger.add(GuiLogger(), format="{time:HH:mm:ss} | {level} | {message}", level="INFO")
|
||||||
13
ui/core/state.py
Normal file
13
ui/core/state.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import threading
|
||||||
|
|
||||||
|
class AppState:
|
||||||
|
def __init__(self):
|
||||||
|
self.stop_event = threading.Event()
|
||||||
|
self.is_running = False
|
||||||
|
# 这些 UI 元素的引用将在 UI 初始化时被赋值
|
||||||
|
self.progress_bar = None
|
||||||
|
self.progress_label = None
|
||||||
|
self.log_element = None
|
||||||
|
|
||||||
|
# 创建全局单例
|
||||||
|
app_state = AppState()
|
||||||
73
ui/core/task_runner.py
Normal file
73
ui/core/task_runner.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import tkinter as tk
|
||||||
|
from tkinter import filedialog
|
||||||
|
from nicegui import ui, run
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from ui.core.state import app_state
|
||||||
|
|
||||||
|
|
||||||
|
async def select_folder():
|
||||||
|
"""在 Native 模式下弹窗选择文件夹"""
|
||||||
|
|
||||||
|
def _pick():
|
||||||
|
root = tk.Tk()
|
||||||
|
root.withdraw()
|
||||||
|
root.attributes("-topmost", True)
|
||||||
|
path = filedialog.askdirectory()
|
||||||
|
root.destroy()
|
||||||
|
return path
|
||||||
|
|
||||||
|
return await run.io_bound(_pick)
|
||||||
|
|
||||||
|
|
||||||
|
async def run_task(func, *args, **kwargs):
|
||||||
|
"""通用任务执行器"""
|
||||||
|
if app_state.is_running:
|
||||||
|
ui.notify("当前有任务正在运行,请稍候...", type="warning")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1. 状态重置
|
||||||
|
app_state.is_running = True
|
||||||
|
app_state.stop_event.clear()
|
||||||
|
if app_state.progress_bar: app_state.progress_bar.set_value(0)
|
||||||
|
if app_state.progress_label: app_state.progress_label.set_text("🚀 正在启动任务...")
|
||||||
|
|
||||||
|
# 2. 定义进度条回调
|
||||||
|
def progress_callback(current, total, task_name="任务"):
|
||||||
|
if total <= 0:
|
||||||
|
pct = 0
|
||||||
|
text = f"{task_name}: 准备中..."
|
||||||
|
else:
|
||||||
|
pct = current / total
|
||||||
|
text = f"{task_name}: {current}/{total} ({int(pct * 100)}%)"
|
||||||
|
|
||||||
|
# 更新 UI
|
||||||
|
if app_state.progress_bar: app_state.progress_bar.set_value(pct)
|
||||||
|
if app_state.progress_label: app_state.progress_label.set_text(text)
|
||||||
|
|
||||||
|
# 3. 组装参数
|
||||||
|
kwargs['progress_callback'] = progress_callback
|
||||||
|
kwargs['stop_event'] = app_state.stop_event
|
||||||
|
|
||||||
|
# 4. 执行
|
||||||
|
try:
|
||||||
|
# 适配器:检查函数是否接受 stop_event
|
||||||
|
def _exec():
|
||||||
|
try:
|
||||||
|
func(*args, **kwargs)
|
||||||
|
except TypeError:
|
||||||
|
kwargs.pop('stop_event', None)
|
||||||
|
func(*args, **kwargs)
|
||||||
|
|
||||||
|
await run.io_bound(_exec)
|
||||||
|
|
||||||
|
if app_state.progress_label: app_state.progress_label.set_text("✅ 任务完成")
|
||||||
|
ui.notify("任务执行成功!", type="positive")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"任务出错: {e}")
|
||||||
|
if app_state.progress_label: app_state.progress_label.set_text(f"❌ 错误: {str(e)}")
|
||||||
|
ui.notify(f"任务失败: {e}", type="negative")
|
||||||
|
finally:
|
||||||
|
app_state.is_running = False
|
||||||
|
if app_state.progress_bar: app_state.progress_bar.set_value(0)
|
||||||
189
ui/views/config_page.py
Normal file
189
ui/views/config_page.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
from nicegui import ui
|
||||||
|
import os
|
||||||
|
from utils.template_utils import get_template_files
|
||||||
|
|
||||||
|
# 修改点 1:统一导入,避免与变量名 config 冲突
|
||||||
|
from config.config import load_config, save_config
|
||||||
|
|
||||||
|
|
||||||
|
def create_config_page():
|
||||||
|
# 修改点 2:将加载逻辑放入页面生成函数内,确保每次刷新页面获取最新值
|
||||||
|
conf_data = load_config("config.toml")
|
||||||
|
template_options = get_template_files()
|
||||||
|
current_filename = os.path.basename(conf_data.get("source_file", ""))
|
||||||
|
|
||||||
|
if current_filename and current_filename not in template_options:
|
||||||
|
template_options.append(current_filename)
|
||||||
|
|
||||||
|
ui.add_head_html('<link href="/assets/style.css" rel="stylesheet" />')
|
||||||
|
|
||||||
|
# 样式修正:添加全屏且不滚动条的 CSS
|
||||||
|
ui.add_head_html(
|
||||||
|
"""
|
||||||
|
<style>
|
||||||
|
body { overflow: hidden; }
|
||||||
|
.main-card { height: calc(100vh - 100px); display: flex; flex-direction: column; }
|
||||||
|
.q-tab-panels { flex-grow: 1; overflow-y: auto !important; }
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
with ui.header().classes("app-header items-center justify-between shadow-md"):
|
||||||
|
# 左侧:图标和标题
|
||||||
|
with ui.row().classes("items-center gap-2"):
|
||||||
|
ui.image("/assets/icon.ico").classes("w-8 h-8").props("fit=contain")
|
||||||
|
ui.label("尚城幼儿园成长报告助手").classes("text-xl font-bold")
|
||||||
|
# 右侧:署名 + 配置按钮
|
||||||
|
with ui.row().classes("items-center gap-4"):
|
||||||
|
ui.label("By 寒寒 | 这里的每一份评语都充满爱意").classes(
|
||||||
|
"text-xs opacity-90"
|
||||||
|
)
|
||||||
|
ui.button(icon="home", on_click=lambda: ui.navigate.to("/")).props(
|
||||||
|
"flat round color=white"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 修改点 3:使用 flex 布局撑满
|
||||||
|
with ui.card().classes("w-full max-w-5xl mx-auto shadow-lg main-card p-0"):
|
||||||
|
with ui.tabs().classes("w-full") as tabs:
|
||||||
|
tab_path = ui.tab("路径设置", icon="folder")
|
||||||
|
tab_class = ui.tab("班级与教师", icon="school")
|
||||||
|
tab_ai = ui.tab("AI 接口配置", icon="psychology")
|
||||||
|
|
||||||
|
with ui.tab_panels(tabs, value=tab_path).classes(
|
||||||
|
"w-full flex-grow bg-transparent"
|
||||||
|
):
|
||||||
|
# --- 路径设置 ---
|
||||||
|
with ui.tab_panel(tab_path).classes("w-full p-0"):
|
||||||
|
with ui.column().classes("w-full p-4 gap-4"):
|
||||||
|
source_file = (
|
||||||
|
ui.select(
|
||||||
|
options=template_options,
|
||||||
|
label="PPT 模板",
|
||||||
|
value=current_filename,
|
||||||
|
)
|
||||||
|
.props("outlined fill-input")
|
||||||
|
.classes("w-full")
|
||||||
|
)
|
||||||
|
excel_file = (
|
||||||
|
ui.input(
|
||||||
|
"Excel 文件",
|
||||||
|
value=os.path.basename(conf_data.get("excel_file", "")),
|
||||||
|
)
|
||||||
|
.props("outlined")
|
||||||
|
.classes("w-full")
|
||||||
|
)
|
||||||
|
image_folder = (
|
||||||
|
ui.input(
|
||||||
|
"图片目录",
|
||||||
|
value=os.path.basename(conf_data.get("image_folder", "")),
|
||||||
|
)
|
||||||
|
.props("outlined")
|
||||||
|
.classes("w-full")
|
||||||
|
)
|
||||||
|
output_folder = (
|
||||||
|
ui.input(
|
||||||
|
"输出目录",
|
||||||
|
value=os.path.basename(
|
||||||
|
conf_data.get("output_folder", "output")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.props("outlined")
|
||||||
|
.classes("w-full")
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- 班级信息 ---
|
||||||
|
with ui.tab_panel(tab_class).classes("w-full p-0"):
|
||||||
|
with ui.column().classes("w-full p-4 gap-4"):
|
||||||
|
class_name = (
|
||||||
|
ui.input("班级名称", value=conf_data.get("class_name", ""))
|
||||||
|
.props("outlined")
|
||||||
|
.classes("w-full")
|
||||||
|
)
|
||||||
|
age_group = (
|
||||||
|
ui.select(
|
||||||
|
options=[
|
||||||
|
"小班上学期",
|
||||||
|
"小班下学期",
|
||||||
|
"中班上学期",
|
||||||
|
"中班下学期",
|
||||||
|
"大班上学期",
|
||||||
|
"大班下学期",
|
||||||
|
],
|
||||||
|
label="年龄段",
|
||||||
|
value=conf_data.get("age_group", "中班上学期"),
|
||||||
|
)
|
||||||
|
.props("outlined")
|
||||||
|
.classes("w-full")
|
||||||
|
)
|
||||||
|
teachers_text = (
|
||||||
|
ui.textarea(
|
||||||
|
"教师名单", value="\n".join(conf_data.get("teachers", []))
|
||||||
|
)
|
||||||
|
.props("outlined")
|
||||||
|
.classes("w-full h-40")
|
||||||
|
)
|
||||||
|
class_type = (
|
||||||
|
ui.select(
|
||||||
|
options={0: "便宜班", 1: "昂贵班", 2: "昂贵的双木桥班"},
|
||||||
|
label="班级类型",
|
||||||
|
value=conf_data.get("class_type", 0),
|
||||||
|
)
|
||||||
|
.props("outlined")
|
||||||
|
.classes("w-full")
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- AI 配置 ---
|
||||||
|
with ui.tab_panel(tab_ai).classes("w-full p-0"):
|
||||||
|
with ui.column().classes("w-full p-4 gap-4"):
|
||||||
|
ai_key = (
|
||||||
|
ui.input("API Key", value=conf_data["ai"].get("api_key", ""))
|
||||||
|
.props("outlined password")
|
||||||
|
.classes("w-full")
|
||||||
|
)
|
||||||
|
ai_url = (
|
||||||
|
ui.input("API URL", value=conf_data["ai"].get("api_url", ""))
|
||||||
|
.props("outlined")
|
||||||
|
.classes("w-full")
|
||||||
|
)
|
||||||
|
ai_model = (
|
||||||
|
ui.input("Model Name", value=conf_data["ai"].get("model", ""))
|
||||||
|
.props("outlined")
|
||||||
|
.classes("w-full")
|
||||||
|
)
|
||||||
|
ai_prompt = (
|
||||||
|
ui.textarea(
|
||||||
|
"System Prompt", value=conf_data["ai"].get("prompt", "")
|
||||||
|
)
|
||||||
|
.props("outlined")
|
||||||
|
.classes("w-full h-full")
|
||||||
|
)
|
||||||
|
# 底部固定按钮
|
||||||
|
with ui.row().classes("w-full p-4"):
|
||||||
|
|
||||||
|
async def handle_save():
|
||||||
|
new_data = {
|
||||||
|
"source_file": source_file.value,
|
||||||
|
"excel_file": excel_file.value,
|
||||||
|
"image_folder": image_folder.value,
|
||||||
|
"output_folder": output_folder.value,
|
||||||
|
"class_name": class_name.value,
|
||||||
|
"age_group": age_group.value,
|
||||||
|
"signature_image": conf_data.get("signature_image", ""),
|
||||||
|
"teachers": [
|
||||||
|
t.strip() for t in teachers_text.value.split("\n") if t.strip()
|
||||||
|
],
|
||||||
|
"class_type": class_type.value,
|
||||||
|
"ai": {
|
||||||
|
"api_key": ai_key.value,
|
||||||
|
"api_url": ai_url.value,
|
||||||
|
"model": ai_model.value,
|
||||||
|
"prompt": ai_prompt.value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
# 修改点 4:直接调用导入的 save_config 函数名
|
||||||
|
success, message = save_config(new_data)
|
||||||
|
ui.notify("配置已保存重启生效", type="positive")
|
||||||
|
|
||||||
|
ui.button("保存配置", on_click=handle_save).classes("w-full py-4").props(
|
||||||
|
"outline color=primary"
|
||||||
|
)
|
||||||
144
ui/views/data_page.py
Normal file
144
ui/views/data_page.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from nicegui import ui
|
||||||
|
from config.config import load_config
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def create_header():
|
||||||
|
with ui.header().classes("app-header items-center justify-between shadow-md"):
|
||||||
|
with ui.row().classes("items-center gap-2"):
|
||||||
|
ui.image("/assets/icon.ico").classes("w-8 h-8").props("fit=contain")
|
||||||
|
ui.label("尚城幼儿园成长报告助手").classes("text-xl font-bold")
|
||||||
|
|
||||||
|
with ui.row().classes("items-center gap-4"):
|
||||||
|
ui.label("By 寒寒 | 这里的每一份评语都充满爱意").classes(
|
||||||
|
"text-xs opacity-90"
|
||||||
|
)
|
||||||
|
ui.button(icon="home", on_click=lambda: ui.navigate.to("/")).props(
|
||||||
|
"flat round color=white"
|
||||||
|
).tooltip("回到首页")
|
||||||
|
|
||||||
|
|
||||||
|
def create_data_page():
|
||||||
|
ui.add_head_html('<link href="/assets/style.css" rel="stylesheet" />')
|
||||||
|
|
||||||
|
create_header()
|
||||||
|
|
||||||
|
with ui.column().classes("w-full max-w-6xl mx-auto p-4 gap-4 thin-scrollbar"):
|
||||||
|
with ui.card().classes("func-card"):
|
||||||
|
with ui.row().classes("items-center justify-between w-full"):
|
||||||
|
ui.label("📊 班级幼儿数据").classes("section-title text-blue")
|
||||||
|
ui.button(
|
||||||
|
"刷新表格", icon="sync", on_click=lambda: ui.navigate.to("/data")
|
||||||
|
).props("outline color=primary")
|
||||||
|
|
||||||
|
with ui.card().classes("func-card"):
|
||||||
|
load_data()
|
||||||
|
|
||||||
|
|
||||||
|
def load_data():
|
||||||
|
conf_data = load_config("config.toml")
|
||||||
|
excel_path = conf_data.get("excel_file")
|
||||||
|
|
||||||
|
with (
|
||||||
|
ui.dialog() as detail_dialog,
|
||||||
|
ui.card().classes(
|
||||||
|
"w-[600px] p-0 profile-dialog rounded-3xl overflow-hidden shadow-2xl"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
with ui.row().classes(
|
||||||
|
"w-full bg-gradient-to-r from-blue-50 to-indigo-50 items-center justify-between"
|
||||||
|
):
|
||||||
|
with ui.column().classes("gap-0"):
|
||||||
|
ui.label("幼儿成长档案").classes(
|
||||||
|
"text-xl p-4 font-black text-slate-800"
|
||||||
|
)
|
||||||
|
ui.button(icon="close", on_click=detail_dialog.close).props(
|
||||||
|
"flat round color=primary"
|
||||||
|
).classes("bg-white/50")
|
||||||
|
|
||||||
|
with ui.column().classes("w-full p-6 h-[450px] overflow-auto gap-0"):
|
||||||
|
content_container = ui.column().classes("w-full")
|
||||||
|
|
||||||
|
with ui.row().classes(
|
||||||
|
"w-full p-5 bg-slate-50/80 backdrop-blur-md border-t justify-end gap-3"
|
||||||
|
):
|
||||||
|
ui.button("确认", on_click=detail_dialog.close).props(
|
||||||
|
"unelevated color=blue-6"
|
||||||
|
).classes("px-10 rounded-xl shadow-lg shadow-blue-200 font-bold")
|
||||||
|
|
||||||
|
def handle_cell_click(e):
|
||||||
|
row_data = e.args["data"]
|
||||||
|
content_container.clear()
|
||||||
|
|
||||||
|
student_name = row_data.get("姓名", "详细数据")
|
||||||
|
|
||||||
|
with content_container:
|
||||||
|
with ui.row().classes(
|
||||||
|
"w-full items-center p-4 bg-blue-600 rounded-2xl shadow-md shadow-blue-100"
|
||||||
|
):
|
||||||
|
ui.avatar("person", color="white", text_color="blue-6").props(
|
||||||
|
"size=48px"
|
||||||
|
)
|
||||||
|
with ui.column().classes("gap-0 text-white"):
|
||||||
|
ui.label(student_name).classes("text-lg font-bold")
|
||||||
|
|
||||||
|
for key, value in row_data.items():
|
||||||
|
if key == "姓名":
|
||||||
|
continue
|
||||||
|
with ui.element("div").classes("info-item w-full flex flex-col gap-1"):
|
||||||
|
ui.label(key).classes("font-bold text-blue-800")
|
||||||
|
ui.label(str(value)).classes("info-value flex-1")
|
||||||
|
|
||||||
|
detail_dialog.open()
|
||||||
|
|
||||||
|
if not excel_path or not os.path.exists(excel_path):
|
||||||
|
with ui.column().classes("w-full items-center p-12 text-slate-400"):
|
||||||
|
ui.icon("folder_off", size="64px")
|
||||||
|
ui.label("数据文件未找到,请检查配置路径").classes("mt-4")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
df = pd.read_excel(excel_path)
|
||||||
|
for col in df.select_dtypes(include=["datetime"]):
|
||||||
|
df[col] = df[col].dt.strftime("%Y-%m-%d")
|
||||||
|
df = df.fillna("-")
|
||||||
|
|
||||||
|
with ui.row().classes(
|
||||||
|
"bg-blue-50 w-full p-3 px-6 items-center rounded-t-xl border-b border-blue-100"
|
||||||
|
):
|
||||||
|
ui.icon("fact_check", color="primary", size="20px")
|
||||||
|
ui.label(f"班级:{conf_data.get('class_name', '未设定')}").classes(
|
||||||
|
"text-sm font-bold text-blue-800"
|
||||||
|
)
|
||||||
|
ui.separator().props("vertical").classes("mx-2")
|
||||||
|
ui.label(f"共加载 {len(df)} 条幼儿记录").classes("text-xs text-slate-500")
|
||||||
|
ui.space()
|
||||||
|
ui.label("💡 提示:点击行可展开完整评语详情").classes(
|
||||||
|
"text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded"
|
||||||
|
)
|
||||||
|
|
||||||
|
ui.aggrid(
|
||||||
|
{
|
||||||
|
"columnDefs": [
|
||||||
|
{
|
||||||
|
"headerName": col,
|
||||||
|
"field": col,
|
||||||
|
"sortable": True,
|
||||||
|
"filter": True,
|
||||||
|
"cellClass": "text-slate-600",
|
||||||
|
"suppressMovable": True,
|
||||||
|
}
|
||||||
|
for col in df.columns
|
||||||
|
],
|
||||||
|
"rowData": df.to_dict("records"),
|
||||||
|
"pagination": True,
|
||||||
|
"paginationPageSize": 20,
|
||||||
|
"theme": "balham",
|
||||||
|
}
|
||||||
|
).classes("w-full flex-grow h-[550px] border-none").on(
|
||||||
|
"cellClicked", handle_cell_click
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
ui.notify(f"加载数据时发生错误: {e}", type="negative", position="top")
|
||||||
120
ui/views/home_page.py
Normal file
120
ui/views/home_page.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
from nicegui import ui
|
||||||
|
from config.config import load_config
|
||||||
|
from ui.core.state import app_state
|
||||||
|
from ui.core.task_runner import run_task, select_folder
|
||||||
|
|
||||||
|
# 导入业务函数
|
||||||
|
from utils.generate_utils import (
|
||||||
|
generate_template,
|
||||||
|
generate_comment_all,
|
||||||
|
generate_convert_pdf,
|
||||||
|
generate_report,
|
||||||
|
generate_zodiac,
|
||||||
|
)
|
||||||
|
from utils.file_utils import open_folder
|
||||||
|
|
||||||
|
config = load_config("config.toml")
|
||||||
|
|
||||||
|
|
||||||
|
def create_header():
|
||||||
|
with ui.header().classes("app-header items-center justify-between shadow-md"):
|
||||||
|
# 左侧:图标和标题
|
||||||
|
with ui.row().classes("items-center gap-2"):
|
||||||
|
ui.image("/assets/icon.ico").classes("w-8 h-8").props("fit=contain")
|
||||||
|
ui.label("尚城幼儿园成长报告助手").classes("text-xl font-bold")
|
||||||
|
|
||||||
|
# 右侧:署名 + 配置按钮
|
||||||
|
with ui.row().classes("items-center gap-4"):
|
||||||
|
ui.label("By 寒寒 | 这里的每一份评语都充满爱意").classes(
|
||||||
|
"text-xs opacity-90"
|
||||||
|
)
|
||||||
|
# 添加配置按钮
|
||||||
|
ui.button(
|
||||||
|
icon="settings", on_click=lambda: ui.navigate.to("/config")
|
||||||
|
).props("flat round color=white").tooltip("系统配置")
|
||||||
|
|
||||||
|
|
||||||
|
def create_home_page():
|
||||||
|
# 1. 引入外部 CSS
|
||||||
|
ui.add_head_html('<link href="/assets/style.css" rel="stylesheet" />')
|
||||||
|
|
||||||
|
create_header()
|
||||||
|
|
||||||
|
# 主容器
|
||||||
|
with ui.column().classes("w-full max-w-4xl mx-auto p-4 gap-4 thin-scrollbar"):
|
||||||
|
|
||||||
|
# === 进度条区域 ===
|
||||||
|
with ui.card().classes("func-card"):
|
||||||
|
app_state.progress_label = ui.label("⛳ 任务进度: 待命").classes(
|
||||||
|
"font-bold text-gray-700 mb-1"
|
||||||
|
)
|
||||||
|
# 使用 NiceGUI 原生属性配合 CSS 类
|
||||||
|
app_state.progress_bar = ui.linear_progress(
|
||||||
|
value=0, show_value=False
|
||||||
|
).classes("h-4 rounded")
|
||||||
|
app_state.progress_bar.props("color=positive") # 使用 Quasar 颜色变量
|
||||||
|
|
||||||
|
# === 核心功能区 ===
|
||||||
|
with ui.card().classes("func-card card-core"):
|
||||||
|
ui.label("🛠️ 核心功能").classes("section-title text-green")
|
||||||
|
|
||||||
|
with ui.grid(columns=3).classes("w-full gap-3"):
|
||||||
|
# 辅助函数:快速创建按钮
|
||||||
|
def func_btn(text, func):
|
||||||
|
ui.button(text, on_click=lambda: run_task(func)).props(
|
||||||
|
f"outline"
|
||||||
|
).classes("w-full")
|
||||||
|
|
||||||
|
func_btn("📁 生成图片路径", generate_template)
|
||||||
|
func_btn("🤖 生成评语 (AI)", generate_comment_all)
|
||||||
|
func_btn("📊 生成报告 (PPT)", generate_report)
|
||||||
|
func_btn("📑 格式转换 (PDF)", generate_convert_pdf)
|
||||||
|
func_btn("🐂 生肖转化 (生日)", generate_zodiac)
|
||||||
|
|
||||||
|
# 签名按钮
|
||||||
|
async def on_signature_click():
|
||||||
|
selected_folder = await select_folder()
|
||||||
|
if selected_folder:
|
||||||
|
ui.navigate.to(f"/signature?folder={selected_folder}")
|
||||||
|
else:
|
||||||
|
ui.notify("未选择目录", type="warning")
|
||||||
|
|
||||||
|
ui.button("💴 园长签名", on_click=on_signature_click).props(
|
||||||
|
f"outline"
|
||||||
|
).classes("w-full")
|
||||||
|
|
||||||
|
# === 下方双栏布局 ===
|
||||||
|
# 数据管理
|
||||||
|
with ui.card().classes("func-card card-data"):
|
||||||
|
ui.label("⚙️ 系统操作").classes("section-title text-blue")
|
||||||
|
|
||||||
|
def stop_now():
|
||||||
|
if app_state.is_running:
|
||||||
|
app_state.stop_event.set()
|
||||||
|
ui.notify("发送停止信号...", type="warning")
|
||||||
|
|
||||||
|
with ui.row().classes("w-full"):
|
||||||
|
ui.button(
|
||||||
|
"📦 打开输出文件夹",
|
||||||
|
on_click=lambda: open_folder(config.get("output_folder")),
|
||||||
|
).props(f"outline")
|
||||||
|
ui.button(
|
||||||
|
"📤 打开数据文件夹",
|
||||||
|
on_click=lambda: open_folder(config.get("data_folder")),
|
||||||
|
).props(f"outline")
|
||||||
|
ui.button(
|
||||||
|
"🔍 查看数据",
|
||||||
|
on_click=lambda: ui.navigate.to("/data"),
|
||||||
|
).props(f"outline")
|
||||||
|
ui.button("⛔ 停止", on_click=stop_now).props("color=negative").classes(
|
||||||
|
"flex-1"
|
||||||
|
)
|
||||||
|
|
||||||
|
# === 日志区 ===
|
||||||
|
with ui.card().classes("func-card card-logging"):
|
||||||
|
with ui.expansion("📝 系统实时日志", value=False).classes(
|
||||||
|
"w-full bg-white shadow-sm rounded"
|
||||||
|
):
|
||||||
|
app_state.log_element = ui.log(max_lines=200).classes(
|
||||||
|
"w-full h-40 font-mono text-xs bg-gray-100 p-2"
|
||||||
|
)
|
||||||
213
ui/views/signature_page.py
Normal file
213
ui/views/signature_page.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
from nicegui import ui
|
||||||
|
import os
|
||||||
|
from config.config import load_config
|
||||||
|
from utils.file_utils import open_folder
|
||||||
|
from loguru import logger
|
||||||
|
import traceback
|
||||||
|
from pptx import Presentation
|
||||||
|
|
||||||
|
|
||||||
|
def create_signature_page(folder: str = ""):
|
||||||
|
ui.add_head_html('<link href="/assets/style.css" rel="stylesheet" />')
|
||||||
|
|
||||||
|
# 添加样式
|
||||||
|
ui.add_head_html(
|
||||||
|
"""
|
||||||
|
<style>
|
||||||
|
.file-list { max-height: 450px; overflow-y: auto; }
|
||||||
|
.file-list::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.file-list::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.file-list::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.file-list::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a1a1a1;
|
||||||
|
}
|
||||||
|
.file-item { transition: background-color 0.2s ease; }
|
||||||
|
.file-item:hover { background-color: #f8f9fa; }
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
with ui.header().classes("app-header items-center justify-between shadow-md"):
|
||||||
|
# 左侧:图标和标题
|
||||||
|
with ui.row().classes("items-center gap-2"):
|
||||||
|
ui.image("/assets/icon.ico").classes("w-8 h-8").props("fit=contain")
|
||||||
|
ui.label("尚城幼儿园成长报告助手").classes("text-xl font-bold")
|
||||||
|
# 右侧:署名 + 配置按钮
|
||||||
|
with ui.row().classes("items-center gap-4"):
|
||||||
|
ui.label("By 寒寒 | 这里的每一份评语都充满爱意").classes(
|
||||||
|
"text-xs opacity-90"
|
||||||
|
)
|
||||||
|
ui.button(icon="home", on_click=lambda: ui.navigate.to("/")).props(
|
||||||
|
"flat round color=white"
|
||||||
|
)
|
||||||
|
|
||||||
|
with ui.card().classes("w-full"):
|
||||||
|
ui.label("💴 园长签名").classes("section-title")
|
||||||
|
with ui.row().classes("w-full items-center justify-between"):
|
||||||
|
with ui.row().classes("flex-1 items-center"):
|
||||||
|
ui.label("📁 当前目录:").classes(
|
||||||
|
"text-sm text-white border bg-[#2e8b57] p-2 rounded"
|
||||||
|
)
|
||||||
|
ui.label(f"{folder}").classes("text-sm text-gray-600")
|
||||||
|
with ui.row().classes("items-center"):
|
||||||
|
ui.button(
|
||||||
|
"💴 一键签名",
|
||||||
|
on_click=lambda: sign_all_files(folder),
|
||||||
|
).props().classes()
|
||||||
|
ui.button(
|
||||||
|
"📂 打开文件夹",
|
||||||
|
on_click=lambda: open_folder(folder),
|
||||||
|
).props("outline").classes()
|
||||||
|
ui.button(
|
||||||
|
"🔄 刷新数据",
|
||||||
|
on_click=lambda: for_file_list(list_card, folder),
|
||||||
|
).props("outline").classes()
|
||||||
|
|
||||||
|
list_card = ui.card().classes("w-full p-4 gap-4")
|
||||||
|
for_file_list(list_card, folder)
|
||||||
|
|
||||||
|
|
||||||
|
# 遍历目录
|
||||||
|
def for_file_list(list_card, folder):
|
||||||
|
# 清空旧内容
|
||||||
|
list_card.clear()
|
||||||
|
files = get_signature_files(folder)
|
||||||
|
with list_card:
|
||||||
|
ui.label(f"📄 找到 {len(files)} 个 PPT 文件").classes(
|
||||||
|
"text-sm text-gray-600 mb-4"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 存储选中的文件
|
||||||
|
selected_files = []
|
||||||
|
|
||||||
|
def toggle_file(file_name):
|
||||||
|
if file_name in selected_files:
|
||||||
|
selected_files.remove(file_name)
|
||||||
|
else:
|
||||||
|
selected_files.append(file_name)
|
||||||
|
|
||||||
|
# 创建可滚动的文件列表
|
||||||
|
with ui.grid(columns=2).classes("file-list"):
|
||||||
|
for ppt_file in files:
|
||||||
|
with ui.row().classes(
|
||||||
|
"w-full justify-between items-center file-item p-3 rounded mb-2"
|
||||||
|
):
|
||||||
|
with ui.row().classes("flex-1 items-center"):
|
||||||
|
ui.checkbox(
|
||||||
|
on_change=lambda e, f=ppt_file: toggle_file(f)
|
||||||
|
).classes("mr-3")
|
||||||
|
ui.label(ppt_file).classes("flex-1 text-sm")
|
||||||
|
with ui.row().classes("items-center gap-2"):
|
||||||
|
# 打开文件按钮
|
||||||
|
ui.button(
|
||||||
|
"📂 打开",
|
||||||
|
on_click=lambda f=ppt_file: open_folder(
|
||||||
|
os.path.join(folder, f)
|
||||||
|
),
|
||||||
|
).props("outline").classes("text-xs")
|
||||||
|
# 签名按钮
|
||||||
|
ui.button(
|
||||||
|
"💴 签名",
|
||||||
|
on_click=lambda f=ppt_file: (
|
||||||
|
sign_file(folder, f),
|
||||||
|
ui.notify(f"签名完成: {f}", type="positive"),
|
||||||
|
),
|
||||||
|
).props("outline").classes("text-xs")
|
||||||
|
|
||||||
|
|
||||||
|
def get_signature_files(folder: str) -> list:
|
||||||
|
"""获取目录下所有 PPT 文件"""
|
||||||
|
if not os.path.exists(folder):
|
||||||
|
return []
|
||||||
|
files = []
|
||||||
|
for filename in os.listdir(folder):
|
||||||
|
if not filename.startswith(".") and filename.endswith(".pptx"):
|
||||||
|
files.append(filename)
|
||||||
|
return sorted(files)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_file(folder, file_name: str):
|
||||||
|
"""
|
||||||
|
生成园长签名(不依赖占位符,直接在指定位置添加)
|
||||||
|
:param folder: PPT 文件所在目录
|
||||||
|
:param file_name: PPT 文件名称
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 1. 加载配置文件
|
||||||
|
config = load_config("config.toml")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"配置文件获取失败: {str(e)}")
|
||||||
|
# 打印详细报错位置,方便调试
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
try:
|
||||||
|
# 2. 等待签名文件路径
|
||||||
|
file_path = os.path.join(folder, file_name)
|
||||||
|
logger.info(f"开始生成签名,PPT文件:{file_name}")
|
||||||
|
# 加载签名图片路径
|
||||||
|
img_path = config.get("signature_image")
|
||||||
|
if not img_path or not os.path.exists(img_path):
|
||||||
|
logger.error(f"签名图片不存在: {img_path}")
|
||||||
|
logger.warning(f"⚠️ 警告: 缺少签名照片('signature')")
|
||||||
|
return
|
||||||
|
logger.info(f"签名图片存在: {img_path}")
|
||||||
|
|
||||||
|
# 从配置文件获取签名位置信息,如果没有则使用默认值
|
||||||
|
signature_left = config.get("signature_left", 2987040) # 左位置
|
||||||
|
signature_top = config.get("signature_top", 8273415) # 上位置
|
||||||
|
signature_width = config.get("signature_width", 1800000) # 宽度
|
||||||
|
signature_height = config.get("signature_height", 720000) # 高度
|
||||||
|
# 导入必要的模块
|
||||||
|
from utils.image_utils import get_corrected_image_stream
|
||||||
|
|
||||||
|
# 打开 PPT 对象
|
||||||
|
prs = Presentation(file_path)
|
||||||
|
|
||||||
|
# 获取第二张幻灯片 (索引为1)
|
||||||
|
slide = prs.slides[1]
|
||||||
|
|
||||||
|
# 获取修正后的图片流
|
||||||
|
img_stream = get_corrected_image_stream(img_path)
|
||||||
|
|
||||||
|
# 直接在指定位置添加签名图片
|
||||||
|
slide.shapes.add_picture(
|
||||||
|
img_stream,
|
||||||
|
signature_left,
|
||||||
|
signature_top,
|
||||||
|
signature_width,
|
||||||
|
signature_height,
|
||||||
|
)
|
||||||
|
# 保存修改后的 PPT
|
||||||
|
prs.save(file_path)
|
||||||
|
logger.info(f"签名完成,PPT文件:{file_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"generate_signature 发生未知错误: {e}")
|
||||||
|
# 打印详细报错位置,方便调试
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def sign_selected_files(folder: str, files: list):
|
||||||
|
"""为选中的文件进行签名"""
|
||||||
|
total_files = len(files)
|
||||||
|
logger.info(f"开始生成签名,共 {total_files} 个文件")
|
||||||
|
for i, file_name in enumerate(files):
|
||||||
|
sign_file(folder, file_name)
|
||||||
|
logger.info(f"已为 {total_files} 个文件签名")
|
||||||
|
ui.notify(f"签名完成: {total_files} 个文件", type="positive")
|
||||||
|
|
||||||
|
|
||||||
|
def sign_all_files(folder: str):
|
||||||
|
"""为目录下所有文件进行签名"""
|
||||||
|
files = get_signature_files(folder)
|
||||||
|
if files:
|
||||||
|
sign_selected_files(folder, files)
|
||||||
|
total_files = len(files)
|
||||||
|
ui.notify(f"签名完成: {total_files} 个文件", type="positive")
|
||||||
@@ -4,11 +4,14 @@ from langchain_core.output_parsers import StrOutputParser
|
|||||||
from langchain_core.prompts import ChatPromptTemplate
|
from langchain_core.prompts import ChatPromptTemplate
|
||||||
from langchain_openai import ChatOpenAI
|
from langchain_openai import ChatOpenAI
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
import traceback
|
||||||
from config.config import load_config
|
from config.config import load_config
|
||||||
|
|
||||||
config = load_config("config.toml")
|
class_type_config =[
|
||||||
|
"本期开展了小袋鼠整合主题课程:(语言、社会、科学、健康、艺术)、生活数学;特色课程(英语、体能、美工、篮球)。",
|
||||||
|
"本学期开展了柏克莱主题课程(语言、社会、科学、艺术、健康);英语及特色课程(体能、舞蹈、美工、魔力猴、足球、国学)。",
|
||||||
|
"本学期开展了双木桥主题课程(图说汉字、妙趣汉音、情智阅读、麦斯思维、专注力训练);英语及特色课程(体能、舞蹈、美工、魔力猴、足球、国学)。"
|
||||||
|
]
|
||||||
|
|
||||||
def generate_comment(name, age_group, traits,sex):
|
def generate_comment(name, age_group, traits,sex):
|
||||||
"""
|
"""
|
||||||
@@ -19,7 +22,14 @@ def generate_comment(name, age_group, traits,sex):
|
|||||||
:param sex: 性别
|
:param sex: 性别
|
||||||
:return: 评语
|
:return: 评语
|
||||||
"""
|
"""
|
||||||
|
# 1. 加载配置文件
|
||||||
|
try:
|
||||||
|
config = load_config("config.toml")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"配置文件获取失败: {str(e)}")
|
||||||
|
# 打印详细报错位置,方便调试
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return "配置文件加载失败,请检查文件路径和内容。"
|
||||||
ai_config = config["ai"]
|
ai_config = config["ai"]
|
||||||
llm = ChatOpenAI(
|
llm = ChatOpenAI(
|
||||||
base_url=ai_config["api_url"],
|
base_url=ai_config["api_url"],
|
||||||
@@ -42,7 +52,8 @@ def generate_comment(name, age_group, traits,sex):
|
|||||||
"name": name,
|
"name": name,
|
||||||
"age_group": age_group,
|
"age_group": age_group,
|
||||||
"traits": traits,
|
"traits": traits,
|
||||||
"sex": sex
|
"sex": sex,
|
||||||
|
"class_type": class_type_config[(config.get("class_type", 0))],
|
||||||
})
|
})
|
||||||
cleaned_text = re.sub(r'\s+', '', comment)
|
cleaned_text = re.sub(r'\s+', '', comment)
|
||||||
logger.success(f"学生:{name} =>生成评语成功: {cleaned_text}")
|
logger.success(f"学生:{name} =>生成评语成功: {cleaned_text}")
|
||||||
|
|||||||
@@ -3,15 +3,18 @@ import os
|
|||||||
import time
|
import time
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
def export_templates_folder(output_folder="backup"):
|
def export_templates_folder(output_folder, stop_event, progress_callback=None):
|
||||||
"""
|
"""
|
||||||
将指定文件夹压缩为 zip 包
|
将指定文件夹压缩为 zip 包
|
||||||
:param source_folder: 要压缩的文件夹路径 (默认 'data')
|
|
||||||
:param output_folder: 压缩包存放的文件夹路径 (默认 'backup')
|
:param output_folder: 压缩包存放的文件夹路径 (默认 'backup')
|
||||||
|
:param stop_event: 停止事件
|
||||||
|
:param progress_callback : 进度条回调
|
||||||
"""
|
"""
|
||||||
source_folder = "data"
|
source_folder = "data"
|
||||||
|
output_folder = output_folder if output_folder else "backup"
|
||||||
try:
|
try:
|
||||||
# 1. 检查源文件夹是否存在
|
# 1. 检查源文件夹是否存在
|
||||||
if not os.path.exists(source_folder):
|
if not os.path.exists(source_folder):
|
||||||
@@ -55,11 +58,12 @@ def export_templates_folder(output_folder="backup"):
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
def export_data(save_dir, root_dir="."):
|
def export_data(save_dir, root_dir=".", progress_callback=None):
|
||||||
"""
|
"""
|
||||||
导出 data 和 output 两个文件夹到同一个 zip 包中
|
导出 data 和 output 两个文件夹到同一个 zip 包中
|
||||||
:param save_dir: 用户在 GUI 弹窗中选择的保存目录 (例如: D:/Backup)
|
:param save_dir: 用户在 GUI 弹窗中选择的保存目录 (例如: D:/Backup)
|
||||||
:param root_dir: 项目根目录 (用于找到 data 和 output)
|
:param root_dir: 项目根目录 (用于找到 data 和 output)
|
||||||
|
:param progress_callback: 进度条回调函数,接收一个 float (0.0~1.0)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 1. 定义要打包的目标文件夹
|
# 1. 定义要打包的目标文件夹
|
||||||
@@ -68,7 +72,18 @@ def export_data(save_dir, root_dir="."):
|
|||||||
# 2. 检查保存目录
|
# 2. 检查保存目录
|
||||||
if not os.path.exists(save_dir):
|
if not os.path.exists(save_dir):
|
||||||
logger.error(f"保存目录不存在: {save_dir}")
|
logger.error(f"保存目录不存在: {save_dir}")
|
||||||
return
|
return None
|
||||||
|
|
||||||
|
# --- 【新增步骤 A】预先计算文件总数 ---
|
||||||
|
total_files = 0
|
||||||
|
for target in targets:
|
||||||
|
target_abs_path = os.path.join(root_dir, target)
|
||||||
|
if os.path.exists(target_abs_path):
|
||||||
|
for _, _, files in os.walk(target_abs_path):
|
||||||
|
total_files += len(files)
|
||||||
|
|
||||||
|
logger.info(f"待压缩文件总数: {total_files}")
|
||||||
|
# ------------------------------------
|
||||||
|
|
||||||
# 3. 生成压缩包路径
|
# 3. 生成压缩包路径
|
||||||
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
||||||
@@ -78,15 +93,15 @@ def export_data(save_dir, root_dir="."):
|
|||||||
logger.info(f"开始备份,目标文件: {zip_path}")
|
logger.info(f"开始备份,目标文件: {zip_path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 4. 创建压缩包 (使用 'w' 写入模式,ZIP_DEFLATED 表示压缩)
|
# 4. 创建压缩包
|
||||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
|
||||||
|
processed_count = 0 # 当前处理的文件数
|
||||||
has_files = False # 标记是否真的压缩了文件
|
has_files = False # 标记是否真的压缩了文件
|
||||||
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
target_abs_path = os.path.join(root_dir, target)
|
target_abs_path = os.path.join(root_dir, target)
|
||||||
|
|
||||||
# 检查 data 或 output 是否存在
|
|
||||||
if not os.path.exists(target_abs_path):
|
if not os.path.exists(target_abs_path):
|
||||||
logger.warning(f"⚠️ 跳过: 找不到文件夹 '{target}'")
|
logger.warning(f"⚠️ 跳过: 找不到文件夹 '{target}'")
|
||||||
continue
|
continue
|
||||||
@@ -94,26 +109,32 @@ def export_data(save_dir, root_dir="."):
|
|||||||
logger.info(f"正在压缩: {target} ...")
|
logger.info(f"正在压缩: {target} ...")
|
||||||
|
|
||||||
# 5. 遍历文件夹写入 ZIP
|
# 5. 遍历文件夹写入 ZIP
|
||||||
# os.walk 会递归遍历子文件夹
|
|
||||||
for root, dirs, files in os.walk(target_abs_path):
|
for root, dirs, files in os.walk(target_abs_path):
|
||||||
for file in files:
|
for file in files:
|
||||||
# 获取文件的绝对路径
|
# 获取文件的绝对路径
|
||||||
file_abs_path = os.path.join(root, file)
|
file_abs_path = os.path.join(root, file)
|
||||||
|
|
||||||
# 【关键】计算在压缩包里的相对路径
|
# 计算相对路径
|
||||||
# 例如: D:/Project/data/images/1.jpg -> data/images/1.jpg
|
|
||||||
arcname = os.path.relpath(file_abs_path, root_dir)
|
arcname = os.path.relpath(file_abs_path, root_dir)
|
||||||
|
|
||||||
# 写入压缩包
|
# 写入压缩包
|
||||||
zf.write(file_abs_path, arcname)
|
zf.write(file_abs_path, arcname)
|
||||||
has_files = True
|
has_files = True
|
||||||
|
# 更新进度条
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(
|
||||||
|
processed_count + 1, total_files, "导出数据中..."
|
||||||
|
)
|
||||||
|
|
||||||
if has_files:
|
if has_files:
|
||||||
|
# 确保进度条最后能走到 100%
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(total_files, total_files, "导出数据成功")
|
||||||
|
|
||||||
logger.success(f"✅ 备份成功! 文件已保存至:\n{zip_path}")
|
logger.success(f"✅ 备份成功! 文件已保存至:\n{zip_path}")
|
||||||
return zip_path
|
return zip_path
|
||||||
else:
|
else:
|
||||||
logger.error("❌ 备份失败: data 和 output 文件夹均为空或不存在。")
|
logger.error("❌ 备份失败: data 和 output 文件夹均为空或不存在。")
|
||||||
# 如果生成了空文件,建议删除
|
|
||||||
if os.path.exists(zip_path):
|
if os.path.exists(zip_path):
|
||||||
os.remove(zip_path)
|
os.remove(zip_path)
|
||||||
return None
|
return None
|
||||||
@@ -123,12 +144,14 @@ def export_data(save_dir, root_dir="."):
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def initialize_project(root_dir="."):
|
def initialize_project(root_dir=".", progress_callback=None):
|
||||||
"""
|
"""
|
||||||
初始化项目:清空 data,重建目录,复制模板
|
初始化项目:清空 data,重建目录,复制模板
|
||||||
:param root_dir: 项目根目录
|
:param root_dir: 项目根目录
|
||||||
|
:param progress_callback : 进度条回调
|
||||||
"""
|
"""
|
||||||
# 定义路径
|
# 定义路径
|
||||||
data_dir = os.path.join(root_dir, "data")
|
data_dir = os.path.join(root_dir, "data")
|
||||||
@@ -191,3 +214,49 @@ def initialize_project(root_dir="."):
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
f"⚠️ 警告: 模板文件不存在 ({src_excel}),data 文件夹内将没有 Excel 文件。"
|
f"⚠️ 警告: 模板文件不存在 ({src_excel}),data 文件夹内将没有 Excel 文件。"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_file_exists(file_path):
|
||||||
|
"""
|
||||||
|
判断文件是否存在
|
||||||
|
"""
|
||||||
|
return file_path and isinstance(file_path, str) and os.path.exists(file_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_output_pptx_files(output_dir="output"):
|
||||||
|
"""
|
||||||
|
获取 output 文件夹下所有的 pptx 文件
|
||||||
|
:param output_dir: output 文件夹路径
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
folder_path = os.path.abspath(output_dir)
|
||||||
|
if not os.path.exists(folder_path):
|
||||||
|
logger.error(f"文件夹不存在: {folder_path}")
|
||||||
|
return
|
||||||
|
# 获取所有 ppt/pptx 文件
|
||||||
|
files = [
|
||||||
|
f for f in os.listdir(folder_path) if f.lower().endswith((".ppt", ".pptx"))
|
||||||
|
]
|
||||||
|
if not files:
|
||||||
|
logger.warning("没有找到 PPT 文件")
|
||||||
|
return
|
||||||
|
total_count = len(files)
|
||||||
|
logger.info(f"发现 {total_count} 个文件,准备开始转换...")
|
||||||
|
return files
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发生未知错误: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
def open_folder(folder_path):
|
||||||
|
"""
|
||||||
|
打开指定文件夹
|
||||||
|
:param folder_path: 文件夹路径
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if os.path.exists(folder_path):
|
||||||
|
os.startfile(folder_path)
|
||||||
|
else:
|
||||||
|
logger.error(f"文件夹不存在: {folder_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"打开文件夹失败: {e}")
|
||||||
|
|||||||
@@ -4,8 +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 config.config import load_config
|
||||||
|
|
||||||
|
config = load_config("config.toml")
|
||||||
|
|
||||||
|
|
||||||
def get_system_fonts():
|
def get_system_fonts():
|
||||||
"""获取系统中可用的字体列表"""
|
"""获取系统中可用的字体列表"""
|
||||||
@@ -21,7 +28,7 @@ def get_system_fonts():
|
|||||||
for font_file in folder.glob(ext):
|
for font_file in folder.glob(ext):
|
||||||
fonts.add(font_file.stem)
|
fonts.add(font_file.stem)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"读取系统字体时出错: {e}")
|
logger.error(f"读取系统字体时出错: {e}")
|
||||||
fonts = {"微软雅黑", "宋体", "黑体", "Arial", "Microsoft YaHei"}
|
fonts = {"微软雅黑", "宋体", "黑体", "Arial", "Microsoft YaHei"}
|
||||||
return fonts
|
return fonts
|
||||||
|
|
||||||
@@ -40,14 +47,14 @@ def is_font_available(font_name):
|
|||||||
def install_fonts_from_directory(fonts_dir="fonts"):
|
def install_fonts_from_directory(fonts_dir="fonts"):
|
||||||
"""从指定目录安装字体到系统"""
|
"""从指定目录安装字体到系统"""
|
||||||
if platform.system() != "Windows":
|
if platform.system() != "Windows":
|
||||||
print("字体安装功能目前仅支持Windows系统")
|
logger.success("字体安装功能目前仅支持Windows系统")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
target_font_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts'
|
target_font_dir = Path.home() / 'AppData' / 'Local' / 'Microsoft' / 'Windows' / 'Fonts'
|
||||||
target_font_dir.mkdir(parents=True, exist_ok=True)
|
target_font_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if not os.path.exists(fonts_dir):
|
if not os.path.exists(fonts_dir):
|
||||||
print(f"字体目录 {fonts_dir} 不存在,请创建该目录并将字体文件放入")
|
logger.error(f"字体目录 {fonts_dir} 不存在,请创建该目录并将字体文件放入")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
font_files = []
|
font_files = []
|
||||||
@@ -55,7 +62,7 @@ def install_fonts_from_directory(fonts_dir="fonts"):
|
|||||||
font_files.extend(Path(fonts_dir).glob(f'*{ext}'))
|
font_files.extend(Path(fonts_dir).glob(f'*{ext}'))
|
||||||
|
|
||||||
if not font_files:
|
if not font_files:
|
||||||
print(f"在 {fonts_dir} 目录中未找到字体文件")
|
logger.error(f"在 {fonts_dir} 目录中未找到字体文件")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
installed_count = 0
|
installed_count = 0
|
||||||
@@ -64,12 +71,12 @@ def install_fonts_from_directory(fonts_dir="fonts"):
|
|||||||
target_path = target_font_dir / font_file.name
|
target_path = target_font_dir / font_file.name
|
||||||
if not target_path.exists():
|
if not target_path.exists():
|
||||||
shutil.copy2(font_file, target_path)
|
shutil.copy2(font_file, target_path)
|
||||||
print(f"已安装字体: {font_file.name}")
|
logger.success(f"已安装字体: {font_file.name}")
|
||||||
installed_count += 1
|
installed_count += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"安装字体 {font_file.name} 时出错: {str(e)}")
|
logger.error(f"安装字体 {font_file.name} 时出错: {str(e)}")
|
||||||
|
|
||||||
if installed_count > 0:
|
if installed_count > 0:
|
||||||
print(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
|
||||||
|
|
||||||
@@ -6,11 +7,12 @@ import pandas as pd
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pptx import Presentation
|
from pptx import Presentation
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
import traceback
|
||||||
|
|
||||||
import comtypes.client
|
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.font_utils import install_fonts_from_directory
|
from utils.file_utils import check_file_exists, get_output_pptx_files
|
||||||
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 (
|
||||||
@@ -22,38 +24,43 @@ from utils.growt_utils import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# 如果你之前没有全局定义 console,这里定义一个
|
|
||||||
console = Console()
|
|
||||||
|
|
||||||
# ==========================================
|
|
||||||
# 1. 配置区域 (Configuration)
|
|
||||||
# ==========================================
|
|
||||||
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 进度回调函数
|
||||||
|
"""
|
||||||
|
# 1. 加载配置文件
|
||||||
try:
|
try:
|
||||||
# 2. 读取数据
|
config = load_config("config.toml")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"配置文件获取失败: {str(e)}")
|
||||||
|
# 打印详细报错位置,方便调试
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
try:
|
||||||
|
# 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):
|
||||||
@@ -61,19 +68,32 @@ 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)}")
|
||||||
# 打印详细报错位置,方便调试
|
# 打印详细报错位置,方便调试
|
||||||
import traceback
|
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 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 进度回调函数
|
||||||
|
"""
|
||||||
|
# 1. 加载配置文件
|
||||||
|
try:
|
||||||
|
config = load_config("config.toml")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"配置文件获取失败: {str(e)}")
|
||||||
|
# 打印详细报错位置,方便调试
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
try:
|
try:
|
||||||
# 1. 读取数据
|
# 1. 读取数据
|
||||||
excel_path = config["excel_file"]
|
excel_path = config["excel_file"]
|
||||||
@@ -83,26 +103,26 @@ def generate_comment_all():
|
|||||||
if "评价" not in df.columns:
|
if "评价" not in df.columns:
|
||||||
df["评价"] = ""
|
df["评价"] = ""
|
||||||
|
|
||||||
# --- 获取总行数,用于日志 ---
|
# 获取学生数据行数
|
||||||
# 强制将“评价”列转换为 object 类型
|
|
||||||
total_count = len(df)
|
total_count = len(df)
|
||||||
logger.info(f"开始生成学生评语,共 {total_count} 位学生...")
|
logger.info(f"开始生成学生评语,共 {len(df)} 位学生...")
|
||||||
|
# 强制将“评价”列转换为 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 # 停止任务
|
||||||
|
|
||||||
|
# 获取学生姓名
|
||||||
|
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里有一列叫 "表现特征",如果没有则用默认的 "有礼貌..."
|
||||||
@@ -117,68 +137,69 @@ def generate_comment_all():
|
|||||||
if not pd.isna(current_comment) and str(current_comment).strip() != "":
|
if not pd.isna(current_comment) and str(current_comment).strip() != "":
|
||||||
logger.info(f"[{i + 1}/{total_count}] {name} 已有评语,跳过。")
|
logger.info(f"[{i + 1}/{total_count}] {name} 已有评语,跳过。")
|
||||||
continue
|
continue
|
||||||
|
# 添加进度条
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(
|
||||||
|
i + 1, total_count, f"[{i + 1}/{total_count}] 正在生成评价: {name}"
|
||||||
|
)
|
||||||
logger.info(f"[{i + 1}/{total_count}] 正在生成评价: {name}")
|
logger.info(f"[{i + 1}/{total_count}] 正在生成评价: {name}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 调用你的生成函数,并【接收返回值】
|
# 调用AI大模型生成内容
|
||||||
# 注意:这里假设 generate_comment 返回的是清洗后的字符串
|
|
||||||
generated_text = generate_comment(
|
generated_text = generate_comment(
|
||||||
name, config["age_group"], traits, sex
|
name, config["age_group"], traits, sex
|
||||||
)
|
)
|
||||||
|
df.at[i, "评价"] = generated_text if str(generated_text).strip() else ""
|
||||||
# --- 将结果写入 DataFrame ---
|
|
||||||
df.at[i, "评价"] = generated_text
|
|
||||||
|
|
||||||
logger.success(f"学生:{name},评语生成完毕")
|
logger.success(f"学生:{name},评语生成完毕")
|
||||||
|
|
||||||
# 可选:每生成 5 个就保存一次,防止程序崩溃数据丢失
|
# 可选:每生成 5 个就保存一次
|
||||||
if (i + 1) % 5 == 0:
|
if (i + 1) % 5 == 0:
|
||||||
df.to_excel(excel_path, index=False)
|
df.to_excel(excel_path, index=False)
|
||||||
logger.info("--- 阶段性保存成功 ---")
|
logger.success("✅ 阶段性保存成功")
|
||||||
|
# 避免触发API速率限制
|
||||||
time.sleep(1) # 避免触发API速率限制
|
time.sleep(1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"学生:{name},生成评语出错: {str(e)}")
|
logger.error(f"学生:{name},生成评语出错: {str(e)}")
|
||||||
|
# --- 循环结束后最终保存文件 ---
|
||||||
# --- 修改点 4: 循环结束后最终保存文件 ---
|
|
||||||
# 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']}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"程序运行出错: {str(e)}")
|
logger.error(f"程序运行出错: {str(e)}")
|
||||||
import traceback
|
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 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 进度回调函数
|
||||||
|
""" # 1. 加载配置文件
|
||||||
os.makedirs(config["output_folder"], exist_ok=True)
|
try:
|
||||||
# 检查模版文件是否存在
|
config = load_config("config.toml")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"配置文件获取失败: {str(e)}")
|
||||||
|
# 打印详细报错位置,方便调试
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
# 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 = [
|
||||||
"姓名",
|
"姓名",
|
||||||
"英文名",
|
"英文名",
|
||||||
@@ -191,14 +212,18 @@ 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
|
||||||
# 解包数据
|
# 解包数据
|
||||||
(
|
(
|
||||||
name,
|
name,
|
||||||
@@ -212,7 +237,13 @@ def generate_report():
|
|||||||
food,
|
food,
|
||||||
comments,
|
comments,
|
||||||
) = row_data
|
) = row_data
|
||||||
|
# 更新进度条
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(
|
||||||
|
i + 1,
|
||||||
|
total_count,
|
||||||
|
f"[{i + 1}/{len(datas)}] 正在生成: 【{name}】 成长报告",
|
||||||
|
)
|
||||||
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
|
logger.info(f"[{i + 1}/{len(datas)}] 正在生成: {name}")
|
||||||
|
|
||||||
# 每次循环重新加载模版
|
# 每次循环重新加载模版
|
||||||
@@ -227,43 +258,67 @@ def generate_report():
|
|||||||
# --- 页面 3 ---
|
# --- 页面 3 ---
|
||||||
student_image_folder = os.path.join(config["image_folder"], name)
|
student_image_folder = os.path.join(config["image_folder"], name)
|
||||||
logger.info(f"学生:{name},图片文件夹: {student_image_folder}")
|
logger.info(f"学生:{name},图片文件夹: {student_image_folder}")
|
||||||
if os.path.exists(student_image_folder):
|
# 判断学生图片文件夹是否存在
|
||||||
me_image_path = find_image_path(student_image_folder, "me")
|
if not os.path.exists(student_image_folder):
|
||||||
|
logger.warning(
|
||||||
|
f"⚠️ 警告: 学生:{name},学生图片文件夹不存在 {student_image_folder}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
# 构造信息字典供 helper 使用
|
# 检查姓名是否为空
|
||||||
info_dict = {
|
if not name:
|
||||||
"name": name,
|
logger.error(f"⚠️ 警告: 学生:{name},姓名为空,跳过")
|
||||||
"english_name": english_name,
|
break
|
||||||
"sex": sex,
|
# 构造学生信息字典
|
||||||
"birthday": birthday.strftime("%Y-%m-%d") if pd.notna(birthday) else "",
|
student_info_dict = {
|
||||||
"zodiac": zodiac,
|
"name": name,
|
||||||
"friend": friend,
|
"english_name": english_name if pd.notna(english_name) else " ",
|
||||||
"hobby": hobby,
|
"sex": sex if pd.notna(sex) else "男",
|
||||||
"game": game,
|
"birthday": (
|
||||||
"food": food,
|
birthday.strftime("%Y-%m-%d") if pd.notna(birthday) else " "
|
||||||
}
|
),
|
||||||
# 逻辑:必须同时满足 "不是None" 且 "是字符串" 且 "文件存在" 才能执行
|
"zodiac": (
|
||||||
if (
|
str(zodiac).strip()
|
||||||
me_image_path
|
if str(zodiac).strip() or not str(zodiac).strip().lower()
|
||||||
and isinstance(me_image_path, str)
|
else " "
|
||||||
and os.path.exists(me_image_path)
|
),
|
||||||
):
|
"friend": (
|
||||||
replace_three_page(prs, info_dict, me_image_path)
|
str(friend).strip()
|
||||||
else:
|
if str(friend).strip() or not str(friend).strip().lower()
|
||||||
# 只有在这里打印日志,告诉用户跳过了,但不中断程序
|
else " "
|
||||||
replace_three_page(prs, info_dict, None)
|
),
|
||||||
|
"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")
|
||||||
|
# 检查学生图片是否存在,若不存在则跳过
|
||||||
|
if check_file_exists(me_image_path):
|
||||||
|
replace_three_page(prs, student_info_dict, me_image_path)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"⚠️ 警告: 学生:{name},学生图片文件夹不存在 {student_image_folder}")
|
replace_three_page(prs, student_info_dict)
|
||||||
|
logger.warning(f"⚠️ 警告: 学生图片文件不存在 {me_image_path}")
|
||||||
|
|
||||||
# --- 页面 4 ---
|
# --- 页面 4 ---
|
||||||
class_image_path = find_image_path(
|
class_image_path = find_image_path(
|
||||||
config["image_folder"], config["class_name"]
|
config["image_folder"], config["class_name"]
|
||||||
)
|
)
|
||||||
if (
|
|
||||||
class_image_path
|
# 添加检查班级图片是否存在,若不存在则跳过
|
||||||
and isinstance(class_image_path, str)
|
if check_file_exists(class_image_path):
|
||||||
and os.path.exists(class_image_path)
|
|
||||||
):
|
|
||||||
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}")
|
||||||
@@ -302,29 +357,36 @@ 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:
|
||||||
logger.error(f"程序运行出错: {str(e)}")
|
logger.error(f"程序运行出错: {str(e)}")
|
||||||
# 打印详细报错位置,方便调试
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 5. 转换格式(根据names.xlsx文件生成PPT转PDF)
|
# 4. 转换格式(根据names.xlsx文件生成PPT转PDF)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
def batch_convert_folder(folder_path):
|
def generate_convert_pdf(stop_event: threading.Event = None, progress_callback=None):
|
||||||
"""
|
"""
|
||||||
【推荐】批量转换文件夹下的所有 PPT (只启动一次 PowerPoint,速度快)
|
批量转换文件夹下的所有 PPT
|
||||||
已修复多线程 CoInitialize 报错,并适配 GUI 日志
|
:params folder_path 需要转换的PPT文件夹
|
||||||
|
:params stop_event 任务是否停止事件(监听UI的事件监听)
|
||||||
|
:params progress_callback 进度回调函数
|
||||||
"""
|
"""
|
||||||
# 【核心修复 1】子线程初始化 COM 组件
|
# 1. 加载配置文件
|
||||||
pythoncom.CoInitialize()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
folder_path = os.path.abspath(folder_path)
|
config = load_config("config.toml")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"配置文件获取失败: {str(e)}")
|
||||||
|
# 打印详细报错位置,方便调试
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
# 子线程初始化 COM 组件
|
||||||
|
pythoncom.CoInitialize()
|
||||||
|
try:
|
||||||
|
folder_path = os.path.abspath(config.get("output_folder"))
|
||||||
if not os.path.exists(folder_path):
|
if not os.path.exists(folder_path):
|
||||||
logger.error(f"文件夹不存在: {folder_path}")
|
logger.error(f"文件夹不存在: {folder_path}")
|
||||||
return
|
return
|
||||||
@@ -338,18 +400,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"
|
||||||
|
|
||||||
@@ -358,16 +426,21 @@ 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:
|
||||||
# 打开 -> 另存为 -> 关闭
|
# 打开 -> 另存为 -> 关闭
|
||||||
deck = powerpoint.Presentations.Open(ppt_path)
|
deck = powerpoint.Presentations.Open(ppt_path)
|
||||||
deck.SaveAs(pdf_path, 32) # 32 代表 PDF 格式
|
deck.SaveAs(pdf_path, 32) # 32 代表 PDF 格式
|
||||||
deck.Close()
|
deck.Close()
|
||||||
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:
|
||||||
@@ -385,14 +458,26 @@ def batch_convert_folder(folder_path):
|
|||||||
# 【核心修复 2】释放资源
|
# 【核心修复 2】释放资源
|
||||||
pythoncom.CoUninitialize()
|
pythoncom.CoUninitialize()
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 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 进度回调函数
|
||||||
|
"""
|
||||||
|
# 1. 加载配置文件
|
||||||
|
try:
|
||||||
|
config = load_config("config.toml")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"配置文件获取失败: {str(e)}")
|
||||||
|
# 打印详细报错位置,方便调试
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
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. 检查必要的列
|
||||||
@@ -403,32 +488,145 @@ 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)}")
|
||||||
import traceback
|
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 6. 一键生成园长签名(根据输出文件夹生成签名)
|
||||||
|
# ==========================================
|
||||||
|
def generate_signature(progress_callback=None) -> str:
|
||||||
|
"""
|
||||||
|
生成园长签名(不依赖占位符,直接在指定位置添加)
|
||||||
|
"""
|
||||||
|
# 1. 加载配置文件
|
||||||
|
try:
|
||||||
|
config = load_config("config.toml")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"配置文件获取失败: {str(e)}")
|
||||||
|
# 打印详细报错位置,方便调试
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
try:
|
||||||
|
# 获取所有的PPT (此时返回的是文件名或路径的列表)
|
||||||
|
pptx_files = get_output_pptx_files(config["output_folder"])
|
||||||
|
|
||||||
|
if not pptx_files:
|
||||||
|
logger.warning("没有找到 PPT 文件")
|
||||||
|
return "未找到文件"
|
||||||
|
|
||||||
|
logger.info(f"开始生成签名,共 {len(pptx_files)} 个 PPT 文件...")
|
||||||
|
|
||||||
|
img_path = config.get("signature_image") # 签名图片路径
|
||||||
|
if not img_path or not os.path.exists(img_path):
|
||||||
|
logger.error(f"签名图片不存在: {img_path}")
|
||||||
|
logger.warning(f"⚠️ 警告: 缺少签名照片('signature')")
|
||||||
|
return
|
||||||
|
logger.info(f"签名图片存在: {img_path}")
|
||||||
|
|
||||||
|
# 从配置文件获取签名位置信息,如果没有则使用默认值
|
||||||
|
signature_left = config.get("signature_left", 2987040) # 左位置
|
||||||
|
signature_top = config.get("signature_top", 8273415) # 上位置
|
||||||
|
signature_width = config.get("signature_width", 1800000) # 宽度
|
||||||
|
signature_height = config.get("signature_height", 720000) # 高度
|
||||||
|
# 导入必要的模块
|
||||||
|
from utils.image_utils import get_corrected_image_stream
|
||||||
|
|
||||||
|
for i, filename in enumerate(pptx_files):
|
||||||
|
# 获取完整绝对路径
|
||||||
|
pptx_path = os.path.join(config["output_folder"], filename)
|
||||||
|
|
||||||
|
# 打开 PPT 对象
|
||||||
|
prs = Presentation(pptx_path)
|
||||||
|
|
||||||
|
# 获取第二张幻灯片 (索引为1)
|
||||||
|
slide = prs.slides[1]
|
||||||
|
|
||||||
|
# 获取修正后的图片流
|
||||||
|
img_stream = get_corrected_image_stream(img_path)
|
||||||
|
|
||||||
|
# 直接在指定位置添加签名图片
|
||||||
|
slide.shapes.add_picture(
|
||||||
|
img_stream,
|
||||||
|
signature_left,
|
||||||
|
signature_top,
|
||||||
|
signature_width,
|
||||||
|
signature_height,
|
||||||
|
)
|
||||||
|
logger.info(f"在幻灯片 1 上添加签名图片")
|
||||||
|
|
||||||
|
# 保存修改后的 PPT
|
||||||
|
prs.save(pptx_path)
|
||||||
|
|
||||||
|
# 更新进度条 (如果有 callback)
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(
|
||||||
|
i + 1,
|
||||||
|
len(pptx_files),
|
||||||
|
f"[{i + 1}/{len(pptx_files)}] 生成签名完成: {filename}",
|
||||||
|
)
|
||||||
|
logger.success(f"[{i + 1}/{len(pptx_files)}] 生成签名完成: {filename}")
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(len(pptx_files), len(pptx_files), "签名生成完成")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"generate_signature 发生未知错误: {e}")
|
||||||
|
# 打印详细报错位置,方便调试
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return str(e)
|
||||||
|
|||||||
@@ -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,4 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
from PIL import Image, ExifTags
|
||||||
|
import io
|
||||||
|
|
||||||
|
|
||||||
def find_image_path(folder, base_filename):
|
def find_image_path(folder, base_filename):
|
||||||
@@ -26,3 +28,39 @@ def find_image_path(folder, base_filename):
|
|||||||
return full_path
|
return full_path
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_corrected_image_stream(img_path):
|
||||||
|
"""
|
||||||
|
读取图片,根据 EXIF 信息修正旋转方向,并返回 BytesIO 对象。
|
||||||
|
这样不需要修改原文件,直接在内存中处理。
|
||||||
|
"""
|
||||||
|
image = Image.open(img_path)
|
||||||
|
|
||||||
|
# 获取 EXIF 数据
|
||||||
|
try:
|
||||||
|
for orientation in ExifTags.TAGS.keys():
|
||||||
|
if ExifTags.TAGS[orientation] == 'Orientation':
|
||||||
|
break
|
||||||
|
|
||||||
|
exif = dict(image._getexif().items())
|
||||||
|
|
||||||
|
if exif[orientation] == 3:
|
||||||
|
image = image.rotate(180, expand=True)
|
||||||
|
elif exif[orientation] == 6:
|
||||||
|
image = image.rotate(270, expand=True)
|
||||||
|
elif exif[orientation] == 8:
|
||||||
|
image = image.rotate(90, expand=True)
|
||||||
|
|
||||||
|
except (AttributeError, KeyError, IndexError):
|
||||||
|
# 如果图片没有 EXIF 数据或不需要旋转,则忽略
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 将处理后的图片保存到内存流中
|
||||||
|
image_stream = io.BytesIO()
|
||||||
|
# 注意:保存时保持原格式,如果是 PNG 等无 EXIF 的格式会自动处理
|
||||||
|
img_format = image.format if image.format else 'JPEG'
|
||||||
|
image.save(image_stream, format=img_format)
|
||||||
|
image_stream.seek(0) # 指针回到开头
|
||||||
|
|
||||||
|
return image_stream
|
||||||
27
utils/log_handler.py
Normal file
27
utils/log_handler.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import queue
|
||||||
|
import re
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
# 全局日志队列
|
||||||
|
log_queue = queue.Queue()
|
||||||
|
|
||||||
|
def ansi_cleaner(text):
|
||||||
|
"""去除 loguru 输出中的颜色代码,防止在 UI 显示乱码"""
|
||||||
|
ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
|
||||||
|
return ansi_escape.sub("", text)
|
||||||
|
|
||||||
|
def queue_sink(message):
|
||||||
|
"""Loguru 的回调函数"""
|
||||||
|
clean_msg = ansi_cleaner(message)
|
||||||
|
log_queue.put(clean_msg)
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
"""配置日志系统"""
|
||||||
|
# 清除默认的控制台输出,防止干扰
|
||||||
|
logger.remove()
|
||||||
|
|
||||||
|
# 添加队列输出 (给 UI 用)
|
||||||
|
logger.add(queue_sink, format="{time:HH:mm:ss} | {level: <8} | {message}", level="INFO")
|
||||||
|
|
||||||
|
# 添加文件输出 (给开发者排查用)
|
||||||
|
# logger.add("logs/app_runtime.log", rotation="1 MB", encoding="utf-8", level="DEBUG")
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
from utils.font_utils import is_font_available
|
from utils.font_utils import is_font_available
|
||||||
|
from utils.image_utils import get_corrected_image_stream
|
||||||
|
|
||||||
|
|
||||||
def replace_text_in_slide(prs, slide_index, placeholder, text):
|
def replace_text_in_slide(prs, slide_index, placeholder, text):
|
||||||
@@ -18,28 +21,28 @@ def replace_text_in_slide(prs, slide_index, placeholder, text):
|
|||||||
original_paragraph_formats = []
|
original_paragraph_formats = []
|
||||||
for paragraph in shape.text_frame.paragraphs:
|
for paragraph in shape.text_frame.paragraphs:
|
||||||
paragraph_format = {
|
paragraph_format = {
|
||||||
'alignment': paragraph.alignment,
|
"alignment": paragraph.alignment,
|
||||||
'space_before': getattr(paragraph, 'space_before', None),
|
"space_before": getattr(paragraph, "space_before", None),
|
||||||
'space_after': getattr(paragraph, 'space_after', None),
|
"space_after": getattr(paragraph, "space_after", None),
|
||||||
'line_spacing': getattr(paragraph, 'line_spacing', None),
|
"line_spacing": getattr(paragraph, "line_spacing", None),
|
||||||
'left_indent': getattr(paragraph, 'left_indent', None),
|
"left_indent": getattr(paragraph, "left_indent", None),
|
||||||
'right_indent': getattr(paragraph, 'right_indent', None),
|
"right_indent": getattr(paragraph, "right_indent", None),
|
||||||
'first_line_indent': getattr(paragraph, 'first_line_indent', None),
|
"first_line_indent": getattr(paragraph, "first_line_indent", None),
|
||||||
'font_info': []
|
"font_info": [],
|
||||||
}
|
}
|
||||||
for run in paragraph.runs:
|
for run in paragraph.runs:
|
||||||
run_format = {
|
run_format = {
|
||||||
'font_name': run.font.name,
|
"font_name": run.font.name,
|
||||||
'font_size': run.font.size,
|
"font_size": run.font.size,
|
||||||
'bold': run.font.bold,
|
"bold": run.font.bold,
|
||||||
'italic': run.font.italic,
|
"italic": run.font.italic,
|
||||||
'underline': run.font.underline,
|
"underline": run.font.underline,
|
||||||
'color': run.font.color,
|
"color": run.font.color,
|
||||||
'character_space': getattr(run.font, 'space', None),
|
"character_space": getattr(run.font, "space", None),
|
||||||
'all_caps': getattr(run.font, 'all_caps', None),
|
"all_caps": getattr(run.font, "all_caps", None),
|
||||||
'small_caps': getattr(run.font, 'small_caps', None)
|
"small_caps": getattr(run.font, "small_caps", None),
|
||||||
}
|
}
|
||||||
paragraph_format['font_info'].append(run_format)
|
paragraph_format["font_info"].append(run_format)
|
||||||
original_paragraph_formats.append(paragraph_format)
|
original_paragraph_formats.append(paragraph_format)
|
||||||
|
|
||||||
# 2. 设置新文本
|
# 2. 设置新文本
|
||||||
@@ -48,64 +51,119 @@ def replace_text_in_slide(prs, slide_index, placeholder, text):
|
|||||||
# 3. 恢复格式
|
# 3. 恢复格式
|
||||||
for i, paragraph in enumerate(shape.text_frame.paragraphs):
|
for i, paragraph in enumerate(shape.text_frame.paragraphs):
|
||||||
orig_idx = i if i < len(original_paragraph_formats) else -1
|
orig_idx = i if i < len(original_paragraph_formats) else -1
|
||||||
if not original_paragraph_formats: break
|
if not original_paragraph_formats:
|
||||||
|
break
|
||||||
|
|
||||||
original_para = original_paragraph_formats[orig_idx]
|
original_para = original_paragraph_formats[orig_idx]
|
||||||
|
|
||||||
# 恢复段落属性
|
# 恢复段落属性
|
||||||
for attr in ['alignment', 'space_before', 'space_after', 'line_spacing',
|
for attr in [
|
||||||
'left_indent', 'right_indent', 'first_line_indent']:
|
"alignment",
|
||||||
|
"space_before",
|
||||||
|
"space_after",
|
||||||
|
"line_spacing",
|
||||||
|
"left_indent",
|
||||||
|
"right_indent",
|
||||||
|
"first_line_indent",
|
||||||
|
]:
|
||||||
if original_para[attr] is not None:
|
if original_para[attr] is not None:
|
||||||
setattr(paragraph, attr, original_para[attr])
|
setattr(paragraph, attr, original_para[attr])
|
||||||
|
|
||||||
# 恢复字体属性
|
# 恢复字体属性
|
||||||
for j, run in enumerate(paragraph.runs):
|
for j, run in enumerate(paragraph.runs):
|
||||||
font_idx = j if j < len(original_para['font_info']) else 0
|
font_idx = j if j < len(original_para["font_info"]) else 0
|
||||||
if not original_para['font_info']: break
|
if not original_para["font_info"]:
|
||||||
|
break
|
||||||
|
|
||||||
original_font = original_para['font_info'][font_idx]
|
original_font = original_para["font_info"][font_idx]
|
||||||
|
|
||||||
# 字体名称检查与回退
|
# 字体名称检查与回退
|
||||||
if original_font['font_name']:
|
if original_font["font_name"]:
|
||||||
if is_font_available(original_font['font_name']):
|
if is_font_available(original_font["font_name"]):
|
||||||
run.font.name = original_font['font_name']
|
run.font.name = original_font["font_name"]
|
||||||
else:
|
else:
|
||||||
run.font.name = "微软雅黑"
|
run.font.name = "微软雅黑"
|
||||||
|
|
||||||
# 恢复其他字体属性
|
# 恢复其他字体属性
|
||||||
if original_font['font_size']: run.font.size = original_font['font_size']
|
if original_font["font_size"]:
|
||||||
if original_font['bold']: run.font.bold = original_font['bold']
|
run.font.size = original_font["font_size"]
|
||||||
if original_font['italic']: run.font.italic = original_font['italic']
|
if original_font["bold"]:
|
||||||
if original_font['underline']: run.font.underline = original_font['underline']
|
run.font.bold = original_font["bold"]
|
||||||
if original_font['all_caps']: run.font.all_caps = original_font['all_caps']
|
if original_font["italic"]:
|
||||||
|
run.font.italic = original_font["italic"]
|
||||||
|
if original_font["underline"]:
|
||||||
|
run.font.underline = original_font["underline"]
|
||||||
|
if original_font["all_caps"]:
|
||||||
|
run.font.all_caps = original_font["all_caps"]
|
||||||
|
|
||||||
if original_font['character_space']:
|
if original_font["character_space"]:
|
||||||
try:
|
try:
|
||||||
run.font.space = original_font['character_space']
|
run.font.space = original_font["character_space"]
|
||||||
except:
|
except:
|
||||||
pass
|
logger.error(
|
||||||
|
f"错误: 无法设置字体间距 {original_font['character_space']}"
|
||||||
|
)
|
||||||
|
|
||||||
if original_font['color']:
|
if original_font["color"]:
|
||||||
try:
|
try:
|
||||||
if hasattr(original_font['color'], 'rgb'):
|
if hasattr(original_font["color"], "rgb"):
|
||||||
run.font.color.rgb = original_font['color'].rgb
|
run.font.color.rgb = original_font["color"].rgb
|
||||||
except:
|
except:
|
||||||
pass
|
logger.error(
|
||||||
|
f"错误: 无法设置字体颜色 {original_font['color']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def replace_picture(prs, slide_index, placeholder, img_path):
|
def replace_picture(prs, slide_index, placeholder, img_path):
|
||||||
"""在指定幻灯片中替换指定占位符的图片"""
|
"""
|
||||||
|
在指定幻灯片中替换指定占位符的图片(包含自动旋转修复)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
prs: Presentation 对象
|
||||||
|
slide_index: 幻灯片索引 (从0开始)
|
||||||
|
placeholder: 占位符名称 (例如 "signature")
|
||||||
|
img_path: 图片路径
|
||||||
|
"""
|
||||||
if not os.path.exists(img_path):
|
if not os.path.exists(img_path):
|
||||||
print(f"警告: 图片路径不存在 {img_path}")
|
logger.warning(f"警告: 图片路径不存在 {img_path}")
|
||||||
return
|
return
|
||||||
|
|
||||||
slide = prs.slides[slide_index]
|
slide = prs.slides[slide_index]
|
||||||
sp_tree = slide.shapes._spTree
|
sp_tree = slide.shapes._spTree
|
||||||
|
|
||||||
|
target_shape = None
|
||||||
|
target_index = -1
|
||||||
|
|
||||||
|
# 1. 先找到目标形状和它的索引
|
||||||
for i, shape in enumerate(slide.shapes):
|
for i, shape in enumerate(slide.shapes):
|
||||||
if shape.name == placeholder:
|
if shape.name == placeholder:
|
||||||
left, top, width, height = shape.left, shape.top, shape.width, shape.height
|
target_shape = shape
|
||||||
sp_tree.remove(shape._element)
|
target_index = i
|
||||||
new_shape = slide.shapes.add_picture(img_path, left, top, width, height)
|
|
||||||
sp_tree.insert(i, new_shape._element)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if target_shape:
|
||||||
|
logger.debug(f"找到占位符 {placeholder},索引 {target_index}")
|
||||||
|
logger.debug(
|
||||||
|
f"占位符位置: 左 {target_shape.left}, 上 {target_shape.top}, 宽 {target_shape.width}, 高 {target_shape.height}"
|
||||||
|
)
|
||||||
|
# 获取原位置信息
|
||||||
|
left, top, width, height = (
|
||||||
|
target_shape.left,
|
||||||
|
target_shape.top,
|
||||||
|
target_shape.width,
|
||||||
|
target_shape.height,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 获取修正后的图片流
|
||||||
|
img_stream = get_corrected_image_stream(img_path)
|
||||||
|
|
||||||
|
# 3. 移除旧形状
|
||||||
|
sp_tree.remove(target_shape._element)
|
||||||
|
|
||||||
|
# 4. 插入新图片 (使用流而不是路径)
|
||||||
|
new_shape = slide.shapes.add_picture(img_stream, left, top, width, height)
|
||||||
|
|
||||||
|
# 5. 恢复层级位置 (z-order)
|
||||||
|
sp_tree.insert(target_index, new_shape._element)
|
||||||
|
else:
|
||||||
|
logger.warning(f"警告: 幻灯片 {slide_index} 中未找到占位符 {placeholder}")
|
||||||
|
|||||||
24
utils/template_utils.py
Normal file
24
utils/template_utils.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import os
|
||||||
|
from config.config import get_base_dir
|
||||||
|
|
||||||
|
def get_template_files():
|
||||||
|
"""
|
||||||
|
遍历 templates 目录,返回所有 PPTX 文件的文件名列表
|
||||||
|
"""
|
||||||
|
# 获取 templates 文件夹的绝对路径
|
||||||
|
# 这里的 get_base_dir() 是你之前定义的函数
|
||||||
|
base_dir = get_base_dir()
|
||||||
|
templates_dir = os.path.join(base_dir, 'templates')
|
||||||
|
|
||||||
|
# 检查目录是否存在,不存在则返回空列表
|
||||||
|
if not os.path.exists(templates_dir):
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 遍历目录
|
||||||
|
files = []
|
||||||
|
for filename in os.listdir(templates_dir):
|
||||||
|
# 过滤掉隐藏文件,并只保留 .pptx 结尾的文件
|
||||||
|
if not filename.startswith('.') and filename.endswith('.pptx'):
|
||||||
|
files.append(filename)
|
||||||
|
|
||||||
|
return sorted(files)
|
||||||
Reference in New Issue
Block a user