Compare commits

...

12 Commits

26 changed files with 1236 additions and 715 deletions

3
.gitignore vendored
View File

@@ -12,3 +12,6 @@ output/*.pptx
output/*.pdf output/*.pdf
data/images/* data/images/*
data/*.xlsx data/*.xlsx
.idea/
.trae/

131
.idea/workspace.xml generated
View File

@@ -4,19 +4,8 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="41690157-d51b-4dae-98de-6b96990d681a" name="更改" comment=""> <list default="true" id="41690157-d51b-4dae-98de-6b96990d681a" name="更改" comment="fix优化一些命名规范">
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/growth_report.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/growth_report.iml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/inspectionProfiles/Project_Default.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/config/config.py" beforeDir="false" afterPath="$PROJECT_DIR$/config/config.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/data/names.xlsx" beforeDir="false" afterPath="$PROJECT_DIR$/data/names.xlsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/main_nicegui.py" beforeDir="false" afterPath="$PROJECT_DIR$/main_nicegui.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ui/views/config_page.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/views/config_page.py" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -26,33 +15,34 @@
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component> </component>
<component name="ProjectColorInfo"><![CDATA[{ <component name="ProjectColorInfo">{
"customColor": "", &quot;customColor&quot;: &quot;&quot;,
"associatedIndex": 0 &quot;associatedIndex&quot;: 0
}]]></component> }</component>
<component name="ProjectId" id="3744WiSuPrq64wZVLisMf4zKTFq" /> <component name="ProjectId" id="3744WiSuPrq64wZVLisMf4zKTFq" />
<component name="ProjectViewState"> <component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent">{
"keyToString": { &quot;keyToString&quot;: {
"ModuleVcsDetector.initialDetectionPerformed": "true", &quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
"Python.main_nicegui.executor": "Run", &quot;Python.main_nicegui.executor&quot;: &quot;Run&quot;,
"RunOnceActivity.ShowReadmeOnStart": "true", &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", &quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
"RunOnceActivity.git.unshallow": "true", &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
"SHARE_PROJECT_CONFIGURATION_FILES": "true", &quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
"git-widget-placeholder": "master", &quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
"node.js.detected.package.eslint": "true", &quot;git-widget-placeholder&quot;: &quot;master&quot;,
"node.js.detected.package.tslint": "true", &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
"node.js.selected.package.eslint": "(autodetect)", &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
"node.js.selected.package.tslint": "(autodetect)", &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
"nodejs_package_manager_path": "npm", &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
"settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable", &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
"vue.rearranger.settings.migration": "true" &quot;settings.editor.selected.configurable&quot;: &quot;preferences.lookFeel&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
} }
}]]></component> }</component>
<component name="RunManager"> <component name="RunManager">
<configuration name="main_nicegui" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true"> <configuration name="main_nicegui" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="growth_report" /> <module name="growth_report" />
@@ -68,6 +58,7 @@
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" /> <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="RUN_TOOL" value="" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/main_nicegui.py" /> <option name="SCRIPT_NAME" value="$PROJECT_DIR$/main_nicegui.py" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" /> <option name="SHOW_COMMAND_LINE" value="false" />
@@ -83,6 +74,14 @@
</list> </list>
</recent_temporary> </recent_temporary>
</component> </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"> <component name="TaskManager">
<task active="true" id="Default" summary="默认任务"> <task active="true" id="Default" summary="默认任务">
<changelist id="41690157-d51b-4dae-98de-6b96990d681a" name="更改" comment="" /> <changelist id="41690157-d51b-4dae-98de-6b96990d681a" name="更改" comment="" />
@@ -90,14 +89,76 @@
<option name="number" value="Default" /> <option name="number" value="Default" />
<option name="presentableId" value="Default" /> <option name="presentableId" value="Default" />
<updated>1766149044347</updated> <updated>1766149044347</updated>
<workItem from="1766149046808" duration="2127000" /> <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>
<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 /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" /> <option name="version" value="3" />
</component> </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"> <component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/growth_report$main_nicegui.coverage" NAME="main_nicegui 覆盖结果" MODIFIED="1766150538951" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/growth_report$main_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> </component>
</project> </project>

20
.vscode/settings.json vendored Normal file
View 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"
}
}

View File

@@ -2,7 +2,7 @@
## 项目概述 ## 项目概述
这是一个基于Python的自动化幼儿园成长报告生成系统。该系统可以从Excel数据文件中读取幼儿信息结合AI生成个性化评语并将所有信息批量填充到PPT模板中最终生成每个学生的个性化成长报告。系统支持双界面运行命令行界面、图形界面和NiceGUI界面具备字体安装、图片替换、批量PDF转换、生肖计算功能。 基于Python的自动化幼儿园学期成长报告生成系统。该系统可以从Excel数据文件中读取幼儿信息结合AI生成个性化评语并将所有信息批量填充到PPT模板中最终生成每个学生的个性化成长报告。系统支持UI界面操作具备字体安装、图片替换、批量PDF转换、生肖计算、模板导出等完整功能。
## 技术栈 ## 技术栈
@@ -47,17 +47,23 @@
- 批量更新Excel中的生肖信息 - 批量更新Excel中的生肖信息
- 支持日期格式自动识别 - 支持日期格式自动识别
### 6. 导出数据模板 (📦 导出数据模板) ### 6. 园长一键签名 (💴 园长一键签名)
- 一键为所有生成的报告添加园长签名
- 自动识别输出文件夹中的PPT文件
- 批量替换签名占位符为实际签名图片
### 7. 导出数据模板 (📦 导出数据模板)
- 生成标准化的数据模板ZIP包 - 生成标准化的数据模板ZIP包
- 包含示例Excel文件和图片文件夹结构 - 包含示例Excel文件和图片文件夹结构
- 方便新用户快速上手 - 方便新用户快速上手
### 7. 初始化系统 (📤 初始化系统) ### 8. 初始化系统 (📤 初始化系统)
- 自动创建必要的目录结构 - 自动创建必要的目录结构
- 安装所需字体文件 - 安装所需字体文件
- 配置系统环境 - 配置系统环境
### 8. 字体安装 (🔤 字体安装) ### 9. 字体安装 (🔤 字体安装)
- 自动检测系统是否安装了指定字体 - 自动检测系统是否安装了指定字体
- 自动安装项目所需的字体文件 - 自动安装项目所需的字体文件
- 支持方正兰亭黑简体和方正少儿简体字体 - 支持方正兰亭黑简体和方正少儿简体字体
@@ -70,7 +76,7 @@ growth_report/
├── UI.py # 图形用户界面入口tkinter ├── UI.py # 图形用户界面入口tkinter
├── main_nicegui.py # NiceGUI界面入口 ├── main_nicegui.py # NiceGUI界面入口
├── main.pyw # Windows图形界面启动文件 ├── main.pyw # Windows图形界面启动文件
├── config.env.toml # 项目配置文件 ├── config.toml # 项目配置文件
├── pyproject.toml # 项目依赖配置 ├── pyproject.toml # 项目依赖配置
├── start_app.bat # Windows启动批处理文件 ├── start_app.bat # Windows启动批处理文件
├── README.md # 项目说明文档 ├── README.md # 项目说明文档
@@ -199,7 +205,7 @@ pip install -r requirements.txt
### 初始化设置 ### 初始化设置
1. 编辑`config.env.toml`配置文件设置API密钥和其他参数 1. 编辑`config.toml`配置文件设置API密钥和其他参数
2. 准备Excel数据文件按指定格式 2. 准备Excel数据文件按指定格式
3. 准备图片资源文件夹(按指定结构) 3. 准备图片资源文件夹(按指定结构)
4. 准备PPT模板文件 4. 准备PPT模板文件

108
README.md
View File

@@ -13,10 +13,11 @@
- 🤖 **AI评语**: 智能生成个性化、治愈系风格的幼儿评语 - 🤖 **AI评语**: 智能生成个性化、治愈系风格的幼儿评语
- 🖼️ **图文并茂**: 支持个人照片、活动照片、班级合影的自动替换 - 🖼️ **图文并茂**: 支持个人照片、活动照片、班级合影的自动替换
- 📄 **格式转换**: 批量PPT转PDF便于分发和存档 - 📄 **格式转换**: 批量PPT转PDF便于分发和存档
- 🎨 **界面**: 提供命令行界面、tkinter图形界面和NiceGUI现代Web界面满足不同用户需求 - 🎨 **现代界面**: 提供NiceGUI现代Web界面操作直观友好
- 🐲 **生肖计算**: 根据生日自动计算生肖信息 - 🐲 **生肖计算**: 根据生日自动计算生肖信息
- 📦 **模板导出**: 生成标准化数据模板,快速上手 - 📦 **模板导出**: 生成标准化数据模板,快速上手
- 🔤 **字体安装**: 自动检测和安装所需字体文件 - 🔤 **字体安装**: 自动检测和安装所需字体文件
- ✍️ **签名生成**: 不依赖占位符,直接在指定位置添加园长签名
## 🛠️ 技术栈 ## 🛠️ 技术栈
@@ -27,7 +28,6 @@
- **comtypes**: PowerPoint转PDF功能 - **comtypes**: PowerPoint转PDF功能
- **rich**: 美化命令行界面 - **rich**: 美化命令行界面
- **loguru**: 日志记录 - **loguru**: 日志记录
- **tkinter**: 图形用户界面
- **nicegui**: 现代Web界面 - **nicegui**: 现代Web界面
- **tomli**: 配置文件解析 - **tomli**: 配置文件解析
@@ -43,51 +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界面 #### NiceGUI界面推荐现代Web界面
```bash
python main_nicegui.py
```
#### 图形界面tkinter界面
```bash ```bash
python UI.py python main.py
``` ```
或直接运行: 或直接运行:
```bash ```bash
start_app.bat start_app.bat
``` ```
#### 命令行界面
```bash
python main.py
```
## 📖 使用指南 ## 📖 使用指南
### 功能模块 ### 功能模块
@@ -100,13 +95,14 @@ python main.py
6. **📦 导出数据模板**: 生成标准化模板 6. **📦 导出数据模板**: 生成标准化模板
7. **📤 初始化系统**: 配置系统环境 7. **📤 初始化系统**: 配置系统环境
8. **🔤 字体安装**: 自动安装和检测所需字体 8. **🔤 字体安装**: 自动安装和检测所需字体
9. **✍️ 生成签名**: 不依赖占位符,直接在指定位置添加园长签名
### Excel数据格式 ### Excel数据格式
Excel文件应包含以下列顺序必须与配置文件中一致 Excel文件应包含以下列顺序必须与配置文件中一致
| 列名 | 说明 | 示例 | | 列名 | 说明 | 示例 |
|------|------|------| | ------------ | ------------------ | ------------ |
| 姓名 | 学生姓名 | 张小明 | | 姓名 | 学生姓名 | 张小明 |
| 英文名 | 英文昵称 | Tom | | 英文名 | 英文昵称 | Tom |
| 性别 | 性别 | 男 | | 性别 | 性别 | 男 |
@@ -132,35 +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 # 图形用户界面入口tkinter ├── config.toml # 项目配置文件
├── main_nicegui.py # NiceGUI界面入口
├── main.pyw # Windows图形界面启动文件
├── 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/ ├── ui/
│ ├── app_window.py # tkinter图形界面
│ ├── main_nicegui.py # NiceGUI界面主文件
│ ├── assets/ │ ├── assets/
│ │ ├── icon.ico # 应用图标 │ │ ├── icon.ico # 应用图标
│ │ └── style.css # 样式文件 │ │ └── style.css # 样式文件
│ ├── core/ │ ├── core/
│ │ ├── logger.py # 日志处理 │ │ ├── logger.py # 日志处理
│ │ ├── state.py # 应用状态管理 │ │ ├── state.py # 应用状态管理
│ │ ── task_runner.py # 任务运行器 │ │ ── task_runner.py # 任务运行器
│ │ └── __pycache__/
│ └── views/ │ └── views/
── home_page.py # NiceGUI主页面 ── 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 # 文件操作工具
@@ -171,14 +179,14 @@ growth_report/
│ ├── log_handler.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评语生成
@@ -191,11 +199,13 @@ growth_report/
- 表现特征 - 表现特征
评语风格为"治愈系",采用三段式结构: 评语风格为"治愈系",采用三段式结构:
1. **开头**: 亲切问候 + 总体印象 1. **开头**: 亲切问候 + 总体印象
2. **正文**: 具体描述孩子的进步和优点 2. **正文**: 具体描述孩子的进步和优点
3. **结尾**: 委婉期望 + 新学期祝福 3. **结尾**: 委婉期望 + 新学期祝福
支持分龄侧重评价: 支持分龄侧重评价:
- **小班 (3-4岁)**: 适应集体生活、情绪稳定性、基本生活自理能力 - **小班 (3-4岁)**: 适应集体生活、情绪稳定性、基本生活自理能力
- **中班 (4-5岁)**: 社交互动、分享与合作、动手能力、好奇心 - **中班 (4-5岁)**: 社交互动、分享与合作、动手能力、好奇心
- **大班 (5-6岁)**: 学习习惯、逻辑思维、领导力、幼小衔接准备 - **大班 (5-6岁)**: 学习习惯、逻辑思维、领导力、幼小衔接准备
@@ -204,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) 文件了解详情。

View File

@@ -1,25 +1,26 @@
[paths] [paths]
source_file = "(横板)中班 幼儿学期发展报告.pptx" source_file = "大四班 幼儿学期发展报告.pptx"
output_folder = "output" output_folder = "output"
excel_file = "names.xlsx" excel_file = "names.xlsx"
image_folder = "images" image_folder = "images"
fonts_dir = "fonts" fonts_dir = "fonts"
signature_image = "C:\\Users\\Administrator\\Desktop\\文档资料\\code\\growth_report\\data\\" signature_image = "d:\\working\\tools\\growth_report\\data\\signature.png"
[class_info] [class_info]
class_name = "K3A" class_name = "K4D"
teachers = [ teachers = [
"丁文敏", "康璐璐",
"麦芷晴", "冯宇阳",
"徐焕奎", "孙继艳",
] ]
class_type = 2
[defaults] [defaults]
default_comment = "" default_comment = ""
age_group = "班上学期" age_group = "班上学期"
[ai] [ai]
api_key = "sk-8b0c9522df8843b4d0e7e91ecb628957" api_key = "sk-ccb6a1445126f2e56ba50d7622ff350f"
api_url = "https://apis.iflow.cn/v1/chat/completions" api_url = "https://apis.iflow.cn/v1/chat/completions"
model = "deepseek-v3.2" model = "deepseek-v3.2"
prompt = "# Role\n你是一位拥有20年经验的资深幼儿园主班老师。你的文笔温暖、细腻、充满爱意擅长发现每个孩子身上独特的闪光点。你的评语风格是“治愈系”的能让家长读完后感到欣慰并对未来充满希望。\n\n# Goal\n请根据用户提供的【幼儿姓名】、【年龄段/班级】以及【日常表现关键词/评分数据】,撰写一份高质量的学期末成长评语。\n\n# Constraints & Rules\n1. **严格的格式排版 (Strict Formatting)**:\n - **换行**:正文中间不要随意换行,保持为一段完整的段落。\n\n2. **称呼处理**:\n - 自动识别用户输入的姓名,去掉姓氏。\n - 例如:“王小明” -> 第一行输出“小明宝贝:”。\n\n3. **分龄侧重 (根据 Age_Group 调整侧重点)**:\n - **小班 (3-4岁)**:侧重于适应集体生活、情绪稳定性、基本生活自理能力、愿意与老师互动。\n - **中班 (4-5岁)**:侧重于社交互动、分享与合作、动手能力、好奇心、规则意识。\n - **大班 (5-6岁)**:侧重于学习习惯、逻辑思维、领导力、任务意识、幼小衔接准备。\n\n4. **写作结构 (固定内容)**:\n - **开头**:固定文本必须包含:“本学期开展了柏克莱主题课程(语言、社会、科学、艺术、健康);英语及特色课程(体能、舞蹈、美工、魔力猴、足球、国学)。”\n - **正文**:结合【表现关键词】和【性别】,具体描述进步和优点。\n - **结尾**:委婉地提出期望(“如果你能...老师会更为你骄傲”),并送上祝福。\n\n5. **语气风格**:\n - 积极正面,多用肯定句。\n - 字数控制在 150-250 字之间。\n\n# Input Format\n- Name {{name}}\n- Age_Group {{class_name}}\n- Traits {{traits}}\n- Sex {{sex}}\n\n# Output Example\n(假设输入:Name=张图图, Age_Group=小班, Traits=适应能力强, 爱笑, 挑食,Sex=女)\n图图宝贝你好,本学期开展了柏克莱主题课程(语言、社会、科学、艺术、健康);英语及特色课程(体能、舞蹈、美工、魔力猴、足球、国学)。你是一个爱笑的小天使,每天早上都能看到你甜甜的笑脸。从一开始的哭鼻子到现在能开心地参与游戏,你的适应能力让老师感到惊喜。不过,老师发现你在吃饭时偶尔会把不喜欢的青菜挑出来哦。如果你能和青菜宝宝做好朋友,把身体练得棒棒的,那就更完美啦!祝可爱的图图宝贝新年快乐,健康成长!\n" 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"

View File

@@ -18,24 +18,27 @@ except ImportError:
# 如果没安装,提供一个 fallback 提示 # 如果没安装,提供一个 fallback 提示
toml_write = None toml_write = None
def get_base_dir(): def get_base_dir():
if getattr(sys, 'frozen', False): if getattr(sys, "frozen", False):
return os.path.dirname(sys.executable) return os.path.dirname(sys.executable)
else: else:
# 假设当前文件在项目根目录或根目录下的某个文件夹中 # 假设当前文件在项目根目录或根目录下的某个文件夹中
return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def get_resource_path(relative_path): def get_resource_path(relative_path):
base_path = get_base_dir() base_path = get_base_dir()
external_path = os.path.join(base_path, relative_path) external_path = os.path.join(base_path, relative_path)
if os.path.exists(external_path): if os.path.exists(external_path):
return external_path return external_path
if getattr(sys, 'frozen', False): if getattr(sys, "frozen", False):
internal_path = os.path.join(sys._MEIPASS, relative_path) internal_path = os.path.join(sys._MEIPASS, relative_path)
if os.path.exists(internal_path): if os.path.exists(internal_path):
return internal_path return internal_path
return external_path return external_path
# ========================================== # ==========================================
# 1. 配置加载 (Config Loader) # 1. 配置加载 (Config Loader)
# ========================================== # ==========================================
@@ -59,19 +62,32 @@ def load_config(config_filename="config.toml"):
config = { config = {
"root_path": base_dir, "root_path": base_dir,
"data_folder": os.path.join(os.path.join("data")),
# 扁平化映射 # 扁平化映射
"source_file": get_resource_path(os.path.join("templates", paths.get("source_file", ""))), "source_file": get_resource_path(
"excel_file": get_resource_path(os.path.join("data", paths.get("excel_file", ""))), os.path.join("templates", paths.get("source_file", ""))
"image_folder": get_resource_path(os.path.join("data", paths.get("image_folder", ""))), ),
"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")), "fonts_dir": get_resource_path(paths.get("fonts_dir", "fonts")),
"output_folder": os.path.join(base_dir, paths.get("output_folder", "output")), "output_folder": os.path.join(
"signature_image": get_resource_path(os.path.join("data", paths.get("signature_image", ""))), 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", "未命名班级"), "class_name": class_info.get("class_name", "未命名班级"),
"teachers": class_info.get("teachers", []), "teachers": class_info.get("teachers", []),
"class_type": class_info.get("class_type", 0),
"default_comment": defaults.get("default_comment", "暂无评语"), "default_comment": defaults.get("default_comment", "暂无评语"),
"age_group": defaults.get("age_group", "大班上学期"), "age_group": defaults.get("age_group", "大班上学期"),
"ai": data.get("ai", {"api_key": "", "api_url": "", "model": ""}), "ai": data.get(
"ai", {"api_key": "", "api_url": "", "model": "", "prompt": ""}
),
} }
return config return config
@@ -79,6 +95,7 @@ def load_config(config_filename="config.toml"):
print(f"解析配置文件失败: {e}") print(f"解析配置文件失败: {e}")
return {} return {}
# ========================================== # ==========================================
# 2. 配置保存 (Config Saver) # 2. 配置保存 (Config Saver)
# ========================================== # ==========================================
@@ -94,21 +111,28 @@ def save_config(config_data, config_filename="config.toml"):
new_data = { new_data = {
"paths": { "paths": {
"source_file": os.path.basename(config_data.get("source_file", "")), "source_file": os.path.basename(config_data.get("source_file", "")),
"output_folder": os.path.basename(config_data.get("output_folder", "output")), "output_folder": os.path.basename(
config_data.get("output_folder", "output")
),
"excel_file": os.path.basename(config_data.get("excel_file", "")), "excel_file": os.path.basename(config_data.get("excel_file", "")),
"image_folder": os.path.basename(config_data.get("image_folder", "")), "image_folder": os.path.basename(config_data.get("image_folder", "")),
"fonts_dir": os.path.basename(config_data.get("fonts_dir", "fonts")), "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_image": get_resource_path(
os.path.join(
"data", config_data.get("signature_image", "signature.png")
)
),
}, },
"class_info": { "class_info": {
"class_name": config_data.get("class_name", ""), "class_name": config_data.get("class_name", ""),
"teachers": config_data.get("teachers", []), "teachers": config_data.get("teachers", []),
"class_type": config_data.get("class_type", 0),
}, },
"defaults": { "defaults": {
"default_comment": config_data.get("default_comment", ""), "default_comment": config_data.get("default_comment", ""),
"age_group": config_data.get("age_group", ""), "age_group": config_data.get("age_group", ""),
}, },
"ai": config_data.get("ai", {}) "ai": config_data.get("ai", {}),
} }
# 写入文件 # 写入文件

Binary file not shown.

View File

@@ -1,32 +1,36 @@
import os import os
import sys import sys
from nicegui import ui, app, run, native
from loguru import logger
from screeninfo import get_monitors
import traceback 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 ui.core.logger import setup_logger from ui.core.logger import setup_logger
from utils.font_utils import install_fonts_from_directory
from ui.views.home_page import create_page
from ui.views.config_page import create_config_page 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.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding='utf-8') sys.stderr.reconfigure(encoding="utf-8")
# 1. 初始化配置 # 1. 初始化配置
config = load_config("config.toml") config = load_config("config.toml")
setup_logger() setup_logger()
# === 关键修改:定义一个获取路径的通用函数 === # === 关键修改:定义一个获取路径的通用函数 ===
def get_path(relative_path): def get_path(relative_path):
""" """
获取资源的绝对路径 获取资源的绝对路径
兼容开发环境(直接运行) 生产环境(打包成exe后解压的临时目录) 兼容开发环境(直接运行) 生产环境(打包成exe后解压的临时目录)
""" """
if hasattr(sys, '_MEIPASS'): if hasattr(sys, "_MEIPASS"):
base_path = sys._MEIPASS base_path = sys._MEIPASS
else: else:
# 开发环境当前目录 # 开发环境当前目录
@@ -63,26 +67,40 @@ def calculate_window_size():
logger.info(f"屏幕分辨率: {screen_width}x{screen_height}") logger.info(f"屏幕分辨率: {screen_width}x{screen_height}")
logger.info(f"设置窗口大小为: {target_width}x{target_height}") logger.info(f"设置窗口大小为: {target_width}x{target_height}")
return (target_width, target_height) return target_width, target_height
except Exception as e: except Exception as e:
logger.warning(f"无法获取屏幕分辨率 ({e}),使用默认大小 (900, 900)") logger.warning(f"无法获取屏幕分辨率 ({e}),使用默认大小 (900, 900)")
return (900, 900) # 失败时的默认值 return 900, 900 # 失败时的默认值
# 1. 挂载静态资源 (CSS/图片) # 1. 挂载静态资源 (CSS/图片)
# 注意:这里使用 get_path 确保打包后能找到 # 注意:这里使用 get_path 确保打包后能找到
static_dir = get_path(os.path.join("ui", "assets")) static_dir = get_path(os.path.join("ui", "assets"))
app.add_static_files('/assets', static_dir) app.add_static_files("/assets", static_dir)
# 3. 页面路由 # 3. 页面路由
@ui.page('/') @ui.page("/")
def index_page(): def index_page():
create_page() create_home_page()
@ui.page('/config')
@ui.page("/config")
def config_page(): def config_page():
create_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. 启动时钩子 # 4. 启动时钩子
async def startup_check(): async def startup_check():
try: try:
@@ -104,5 +122,5 @@ if __name__ in {"__main__", "__mp_main__"}:
native=True, native=True,
window_size=calculated_size, window_size=calculated_size,
port=native.find_open_port(), # 自动寻找端口 port=native.find_open_port(), # 自动寻找端口
reload=False reload=True,
) )

View File

@@ -1,81 +0,0 @@
import sys
import tkinter as tk
from loguru import logger
from ui.app_window import ReportApp
from utils.log_handler import setup_logging
# 全局变量,用于判断日志是否已初始化
LOGGING_INITIALIZED = False
# --- 全局错误处理 ---
def handle_exception(exc_type, exc_value, exc_traceback):
"""
捕获未被 try/except 块处理的全局异常(如线程崩溃)。
"""
if exc_type is KeyboardInterrupt:
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
# 尝试使用 loguru 记录
if LOGGING_INITIALIZED:
logger.error("捕获到未处理的全局异常:", exc_info=(exc_type, exc_value, exc_traceback))
else:
# 如果日志系统未初始化,直接打印到标准错误流,确保用户看到
print("FATAL ERROR (Log Not Initialized):", file=sys.stderr)
import traceback
traceback.print_exception(exc_type, exc_value, exc_traceback, file=sys.stderr)
sys.excepthook = handle_exception
# --------------------
def create_main_window():
global LOGGING_INITIALIZED
# 顶级 try 块,捕获日志初始化阶段的错误
try:
# 1. 初始化日志
setup_logging()
LOGGING_INITIALIZED = True
logger.info("正在启动应用程序...")
# 2. 启动 UI
root = tk.Tk()
# 这一行可以设置图标 (如果有 icon.ico 文件)
# root.iconbitmap(os.path.join(os.path.dirname(__file__), "public", "icon.ico"))
# 确保 ReportApp 实例化时不会出现路径错误
app = ReportApp(root)
# 3. 进入主循环
root.mainloop()
except Exception as e:
# 如果日志系统已启动,使用 logger 记录
if LOGGING_INITIALIZED:
logger.error(f"应用程序启动/主循环出错: {e}", exc_info=True)
else:
# 如果日志系统未初始化,直接打印到控制台
print(f"FATAL STARTUP ERROR: {e}", file=sys.stderr)
import traceback
traceback.print_exc(file=sys.stderr)
# 确保窗口被销毁
if 'root' in locals() and root:
root.destroy()
# 非窗口模式下,在启动错误时等待用户查看
if not getattr(sys, 'frozen', False) or not any(arg in sys.argv for arg in ('--windowed', '-w')):
input("按任意键退出...")
sys.exit(1)
if __name__ == "__main__":
create_main_window()

View File

@@ -37,10 +37,10 @@ echo [INFO] 正在拉起主程序...
echo --------------------------------------------------- echo ---------------------------------------------------
:: ======================================================= :: =======================================================
:: 【关键修改】路径改为根目录的 main.pyw :: 【关键修改】路径改为根目录的 main.py
:: 使用 start 命令启动,这样黑色的 CMD 窗口可以随后立即关闭 :: 使用 start 命令启动,这样黑色的 CMD 窗口可以随后立即关闭
:: ======================================================= :: =======================================================
start "" uv run main.pyw start "" uv run main.py
:: 等待 1 秒确保启动 :: 等待 1 秒确保启动
timeout /t 1 >nul timeout /t 1 >nul

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,236 +0,0 @@
import os
import time
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog
import threading
import queue
import sys
from loguru import logger
from config.config import load_config
from utils.font_utils import install_fonts_from_directory
from utils.log_handler import log_queue
# 导入业务逻辑
from utils.generate_utils import (
generate_template,
generate_comment_all,
batch_convert_folder,
generate_report,
generate_zodiac,
)
from utils.file_utils import export_templates_folder, initialize_project, export_data
config = load_config("config.toml")
class ReportApp:
def __init__(self, root):
self.root = root
self.root.title("🌱 尚城幼儿园成长报告助手")
self.root.geometry("720x760")
# 线程控制
self.stop_event = threading.Event()
self.is_running = False
# 尝试初始化 UI
try:
self._setup_ui()
except Exception as e:
logger.critical(f"UI 初始化失败: {e}", exc_info=True)
messagebox.showerror("致命错误", f"界面初始化失败,请检查日志。\n错误: {e}")
self.root.destroy()
sys.exit(1)
# 尝试初始化项目资源
try:
self.init_project()
except Exception as e:
logger.critical(f"项目资源初始化失败: {e}", exc_info=True)
messagebox.showerror("致命错误", f"项目资源初始化失败,请检查日志。\n错误: {e}")
self.root.destroy()
sys.exit(1)
self._start_log_polling()
def _setup_ui(self):
# 样式配置
self.style = ttk.Style()
self.style.theme_use("clam")
self.style.configure("TButton", font=("微软雅黑", 10), padding=5)
self.style.configure("Title.TLabel", font=("微软雅黑", 16, "bold"), foreground="#2E8B57")
self.style.configure("Stop.TButton", foreground="red", font=("微软雅黑", 10, "bold"))
# 1. 标题
header = ttk.Frame(self.root, padding="10 15 10 5")
header.pack(fill=tk.X)
ttk.Label(header, text="🌱 尚城幼儿园成长报告助手", style="Title.TLabel").pack()
ttk.Label(header, text="By 寒寒 | 这里的每一份评语都充满爱意", font=("微软雅黑", 9), foreground="gray").pack()
# 2. 功能区容器
main_content = ttk.Frame(self.root, padding="10 15 10 5")
main_content.pack(fill=tk.X)
# === 进度条区域 ===
progress_frame = ttk.Frame(self.root, padding="10 15 10 5")
progress_frame.pack(fill=tk.X, pady=(0, 10))
# 进度条 Label
self.progress_label = ttk.Label(progress_frame, text="⛳ 任务进度: 待命", font=("微软雅黑", 10))
self.progress_label.pack(fill=tk.X, pady=(0, 2))
# 进度条
self.progressbar = ttk.Progressbar(progress_frame, orient="horizontal", mode="determinate")
self.progressbar.pack(fill=tk.X, expand=True)
# === A组: 核心功能 ===
self._create_btn_group(main_content, "🛠️ 核心功能", [
("📁 生成图片路径", lambda: self.run_task(generate_template)),
("🤖 生成评语 (AI)", lambda: self.run_task(generate_comment_all)),
("📊 生成报告 (PPT)", lambda: self.run_task(generate_report)),
("📑 格式转换 (PDF)", lambda: self.run_task(batch_convert_folder, config.get("output_folder"))),
("🐂 生肖转化 (生日)", lambda: self.run_task(generate_zodiac)),
], columns=3)
# === B组: 数据管理 ===
self._create_btn_group(main_content, "📦 数据管理", [
("📦 导出模板 (Zip)", self.run_export_template),
("📤 导出备份 (Zip)", self.run_export_data),
], columns=2)
# === C组: 系统操作 (含停止按钮) ===
self._create_btn_group(main_content, "⚙️ 系统操作", [
("⛔ 停止当前任务", self.stop_current_task),
("⚠️ 初始化系统", self.run_init),
("🚪 退出系统", self.quit_app),
], columns=3, special_styles={"⛔ 停止当前任务": "Stop.TButton"})
# 3. 日志区
log_frame = ttk.LabelFrame(self.root, text="📝 系统实时日志", padding="10 15 10 5")
log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10))
self.log_text = scrolledtext.ScrolledText(log_frame, height=10, state="disabled", font=("Consolas", 9))
self.log_text.pack(fill=tk.BOTH, expand=True)
def _create_btn_group(self, parent, title, buttons, columns=2, special_styles=None):
frame = ttk.LabelFrame(parent, text=title, padding=10)
frame.pack(fill=tk.X, pady=5)
special_styles = special_styles or {}
for i, (text, func) in enumerate(buttons):
style = special_styles.get(text, "TButton")
btn = ttk.Button(frame, text=text, command=func, style=style)
r, c = divmod(i, columns)
btn.grid(row=r, column=c, padx=5, pady=5, sticky="ew")
for i in range(columns):
frame.columnconfigure(i, weight=1)
def _start_log_polling(self):
while not log_queue.empty():
try:
msg = log_queue.get_nowait()
self.log_text.config(state="normal")
self.log_text.insert(tk.END, msg)
self.log_text.see(tk.END)
self.log_text.config(state="disabled")
except queue.Empty:
break
self.root.after(100, self._start_log_polling)
def init_project(self):
# 1. 资源准备
if install_fonts_from_directory(config["fonts_dir"]):
logger.info("等待系统识别新安装的字体...")
time.sleep(2)
# 2. 创建输出文件夹
os.makedirs(config["output_folder"], exist_ok=True)
logger.success("项目初始化完成.....")
# --- 任务运行核心逻辑 ---
def run_task(self, target_func, *args, **kwargs):
if self.is_running:
messagebox.showwarning("忙碌中", "请先等待当前任务完成或点击【停止当前任务】")
return
self.stop_event.clear()
self.is_running = True
# 将进度更新方法作为参数传入
kwargs['progress_callback'] = self.update_progress
def thread_worker():
try:
# 尝试传入 stop_event
try:
target_func(*args, stop_event=self.stop_event, **kwargs)
except TypeError:
# 如果旧函数不支持 stop_event则普通运行
target_func(*args, **kwargs)
except Exception as e:
logger.error(f"任务出错: {e}")
import traceback
logger.error(traceback.format_exc())
finally:
self.is_running = False
logger.info("系统准备就绪.....")
self.reset_progress() # 重置进度条
threading.Thread(target=thread_worker, daemon=True).start()
def stop_current_task(self):
if not self.is_running:
return
if messagebox.askyesno("确认", "确定要中断当前任务吗?"):
self.stop_event.set()
logger.warning("正在发送停止信号...")
# --- 具体按钮事件 ---
def run_export_template(self):
path = filedialog.askdirectory()
if path: self.run_task(export_templates_folder, path)
def run_export_data(self):
path = filedialog.askdirectory()
if path: self.run_task(export_data, path)
def run_init(self):
if messagebox.askokcancel("警告", "确定重置系统吗?数据将丢失!"):
self.run_task(initialize_project)
def quit_app(self):
if self.is_running:
messagebox.showwarning("提示", "请先停止任务")
return
self.root.destroy()
sys.exit()
# --- 进度条更新(实现线程安全更新) ---
def update_progress(self, current, total, task_name="任务"):
"""
线程安全地更新进度条和标签
:param current: 当前完成的项目数
:param total: 总项目数
:param task_name: 当前任务名称
"""
if total <= 0:
# 重置进度条
self.progressbar['value'] = 0
self.progress_label.config(text=f"任务进度: {task_name} 完成或待命")
return
percentage = int((current / total) * 100)
display_text = f"{task_name}: {current}/{total} ({percentage}%)"
# 使用 after 确保在主线程中更新 UI
self.root.after(0, self._set_progress_ui, percentage, display_text)
def _set_progress_ui(self, percentage, display_text):
"""实际更新 UI 的私有方法"""
self.progressbar['value'] = percentage
self.progress_label.config(text=display_text)
def reset_progress(self):
"""任务结束后重置进度条"""
self.root.after(0, self._set_progress_ui, 0, "任务进度: 就绪")

View File

@@ -2,13 +2,13 @@
/* 全局字体 */ /* 全局字体 */
body { body {
font-family: "微软雅黑", "Microsoft YaHei", sans-serif; font-family: '微软雅黑', 'Microsoft YaHei', sans-serif;
background-color: #f0f4f8; background-color: #f0f4f8;
} }
/* 标题栏 */ /* 标题栏 */
.app-header { .app-header {
background-color: #2E8B57; /* SeaGreen */ background-color: #2e8b57; /* SeaGreen */
color: white; color: white;
} }
@@ -48,12 +48,17 @@ body {
} }
/* 绿色标题 */ /* 绿色标题 */
.text-green { color: #166534; } .text-green {
color: #166534;
}
/* 蓝色标题 */ /* 蓝色标题 */
.text-blue { color: #1e40af; } .text-blue {
color: #1e40af;
}
/* 红色标题 */ /* 红色标题 */
.text-red { color: #991b1b; } .text-red {
color: #991b1b;
}
/* assets/style.css */ /* assets/style.css */
@@ -118,7 +123,8 @@ body {
height: 0px !important; height: 0px !important;
background: transparent !important; background: transparent !important;
} }
#nicegui-content, #q-app { #nicegui-content,
#q-app {
/* 确保容器内容可以滚动,但滚动条被隐藏 */ /* 确保容器内容可以滚动,但滚动条被隐藏 */
overflow: auto; overflow: auto;
@@ -128,3 +134,49 @@ body {
/* IE/Edge 隐藏滚动条 */ /* IE/Edge 隐藏滚动条 */
-ms-overflow-style: none; -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;
}

View File

@@ -1,14 +1,16 @@
from nicegui import ui from nicegui import ui
import os import os
from utils.template_utils import get_template_files from utils.template_utils import get_template_files
# 修改点 1统一导入避免与变量名 config 冲突 # 修改点 1统一导入避免与变量名 config 冲突
from config.config import load_config, save_config from config.config import load_config, save_config
def create_config_page(): def create_config_page():
# 修改点 2将加载逻辑放入页面生成函数内确保每次刷新页面获取最新值 # 修改点 2将加载逻辑放入页面生成函数内确保每次刷新页面获取最新值
conf_data = load_config("config.toml") conf_data = load_config("config.toml")
template_options = get_template_files() template_options = get_template_files()
current_filename = os.path.basename(conf_data.get('source_file', '')) current_filename = os.path.basename(conf_data.get("source_file", ""))
if current_filename and current_filename not in template_options: if current_filename and current_filename not in template_options:
template_options.append(current_filename) template_options.append(current_filename)
@@ -16,59 +18,148 @@ def create_config_page():
ui.add_head_html('<link href="/assets/style.css" rel="stylesheet" />') ui.add_head_html('<link href="/assets/style.css" rel="stylesheet" />')
# 样式修正:添加全屏且不滚动条的 CSS # 样式修正:添加全屏且不滚动条的 CSS
ui.add_head_html(''' ui.add_head_html(
"""
<style> <style>
body { overflow: hidden; } body { overflow: hidden; }
.main-card { height: calc(100vh - 100px); display: flex; flex-direction: column; } .main-card { height: calc(100vh - 100px); display: flex; flex-direction: column; }
.q-tab-panels { flex-grow: 1; overflow-y: auto !important; } .q-tab-panels { flex-grow: 1; overflow-y: auto !important; }
</style> </style>
''') """
)
with ui.header().classes('app-header items-center justify-between shadow-md'): with ui.header().classes("app-header items-center justify-between shadow-md"):
# 左侧:图标和标题 # 左侧:图标和标题
with ui.row().classes('items-center gap-2'): with ui.row().classes("items-center gap-2"):
ui.image('/assets/icon.ico').classes('w-8 h-8').props('fit=contain') ui.image("/assets/icon.ico").classes("w-8 h-8").props("fit=contain")
ui.label('尚城幼儿园成长报告助手').classes('text-xl font-bold') ui.label("尚城幼儿园成长报告助手").classes("text-xl font-bold")
# 右侧:署名 + 配置按钮 # 右侧:署名 + 配置按钮
with ui.row().classes('items-center gap-4'): with ui.row().classes("items-center gap-4"):
ui.label('By 寒寒 | 这里的每一份评语都充满爱意').classes('text-xs opacity-90') ui.label("By 寒寒 | 这里的每一份评语都充满爱意").classes(
ui.button(icon='home', on_click=lambda: ui.navigate.to('/')).props('flat round color=white') "text-xs opacity-90"
)
ui.button(icon="home", on_click=lambda: ui.navigate.to("/")).props(
"flat round color=white"
)
# 修改点 3使用 flex 布局撑满 # 修改点 3使用 flex 布局撑满
with ui.card().classes('w-full max-w-5xl mx-auto shadow-lg main-card p-0'): 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: with ui.tabs().classes("w-full") as tabs:
tab_path = ui.tab('路径设置', icon='folder') tab_path = ui.tab("路径设置", icon="folder")
tab_class = ui.tab('班级与教师', icon='school') tab_class = ui.tab("班级与教师", icon="school")
tab_ai = ui.tab('AI 接口配置', icon='psychology') 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_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.tab_panel(tab_path).classes("w-full p-0"):
with ui.column().classes('w-full p-4 gap-4'): 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') source_file = (
excel_file = ui.input('Excel 文件', value=os.path.basename(conf_data.get('excel_file', ''))).props('outlined').classes('w-full') ui.select(
image_folder = ui.input('图片目录', value=os.path.basename(conf_data.get('image_folder', ''))).props('outlined').classes('w-full') options=template_options,
output_folder = ui.input('输出目录', value=os.path.basename(conf_data.get('output_folder', 'output'))).props('outlined').classes('w-full') 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.tab_panel(tab_class).classes("w-full p-0"):
with ui.column().classes('w-full p-4 gap-4'): 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') class_name = (
age_group = ui.select( ui.input("班级名称", value=conf_data.get("class_name", ""))
options=['小班上学期', '小班下学期', '中班上学期', '中班下学期', '大班上学期', '大班下学期'], .props("outlined")
label='年龄段', value=conf_data.get('age_group', '中班上学期') .classes("w-full")
).props('outlined').classes('w-full') )
teachers_text = ui.textarea('教师名单', value='\n'.join(conf_data.get('teachers', []))).props('outlined').classes('w-full h-40') 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 配置 --- # --- AI 配置 ---
with ui.tab_panel(tab_ai).classes('w-full p-0'): with ui.tab_panel(tab_ai).classes("w-full p-0"):
with ui.column().classes('w-full p-4 gap-4'): 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_key = (
ai_url = ui.input('API URL', value=conf_data['ai'].get('api_url', '')).props('outlined').classes('w-full') ui.input("API Key", value=conf_data["ai"].get("api_key", ""))
ai_model = ui.input('Model Name', value=conf_data['ai'].get('model', '')).props('outlined').classes('w-full') .props("outlined password")
ai_prompt = ui.textarea('System Prompt', value=conf_data['ai'].get('prompt', '')).props('outlined').classes('w-full h-full') .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'): with ui.row().classes("w-full p-4"):
async def handle_save(): async def handle_save():
new_data = { new_data = {
"source_file": source_file.value, "source_file": source_file.value,
@@ -77,16 +168,22 @@ def create_config_page():
"output_folder": output_folder.value, "output_folder": output_folder.value,
"class_name": class_name.value, "class_name": class_name.value,
"age_group": age_group.value, "age_group": age_group.value,
"teachers": [t.strip() for t in teachers_text.value.split('\n') if t.strip()], "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": { "ai": {
"api_key": ai_key.value, "api_key": ai_key.value,
"api_url": ai_url.value, "api_url": ai_url.value,
"model": ai_model.value, "model": ai_model.value,
"prompt": ai_prompt.value "prompt": ai_prompt.value,
} },
} }
# 修改点 4直接调用导入的 save_config 函数名 # 修改点 4直接调用导入的 save_config 函数名
success, message = save_config(new_data) success, message = save_config(new_data)
ui.notify(message, type='positive' if success else 'negative') ui.notify("配置已保存重启生效", type="positive")
ui.button('保存配置', on_click=handle_save).classes('w-full py-4').props('outline color=primary') ui.button("保存配置", on_click=handle_save).classes("w-full py-4").props(
"outline color=primary"
)

144
ui/views/data_page.py Normal file
View 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")

View File

@@ -5,89 +5,116 @@ from ui.core.task_runner import run_task, select_folder
# 导入业务函数 # 导入业务函数
from utils.generate_utils import ( from utils.generate_utils import (
generate_template, generate_comment_all, generate_template,
batch_convert_folder, generate_report, generate_zodiac, generate_signature generate_comment_all,
generate_convert_pdf,
generate_report,
generate_zodiac,
) )
from utils.file_utils import export_templates_folder, initialize_project, export_data from utils.file_utils import open_folder
config = load_config("config.toml") config = load_config("config.toml")
def create_header(): def create_header():
with ui.header().classes('app-header items-center justify-between shadow-md'): with ui.header().classes("app-header items-center justify-between shadow-md"):
# 左侧:图标和标题 # 左侧:图标和标题
with ui.row().classes('items-center gap-2'): with ui.row().classes("items-center gap-2"):
ui.image('/assets/icon.ico').classes('w-8 h-8').props('fit=contain') ui.image("/assets/icon.ico").classes("w-8 h-8").props("fit=contain")
ui.label('尚城幼儿园成长报告助手').classes('text-xl font-bold') ui.label("尚城幼儿园成长报告助手").classes("text-xl font-bold")
# 右侧:署名 + 配置按钮 # 右侧:署名 + 配置按钮
with ui.row().classes('items-center gap-4'): with ui.row().classes("items-center gap-4"):
ui.label('By 寒寒 | 这里的每一份评语都充满爱意').classes('text-xs opacity-90') 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('系统配置') ui.button(
icon="settings", on_click=lambda: ui.navigate.to("/config")
).props("flat round color=white").tooltip("系统配置")
def create_page():
def create_home_page():
# 1. 引入外部 CSS # 1. 引入外部 CSS
ui.add_head_html('<link href="/assets/style.css" rel="stylesheet" />') ui.add_head_html('<link href="/assets/style.css" rel="stylesheet" />')
create_header() create_header()
# 主容器 # 主容器
with ui.column().classes('w-full max-w-4xl mx-auto p-4 gap-4 thin-scrollbar'): with ui.column().classes("w-full max-w-4xl mx-auto p-4 gap-4 thin-scrollbar"):
# === 进度条区域 === # === 进度条区域 ===
with ui.card().classes('func-card'): with ui.card().classes("func-card"):
app_state.progress_label = ui.label('⛳ 任务进度: 待命').classes('font-bold text-gray-700 mb-1') app_state.progress_label = ui.label("⛳ 任务进度: 待命").classes(
"font-bold text-gray-700 mb-1"
)
# 使用 NiceGUI 原生属性配合 CSS 类 # 使用 NiceGUI 原生属性配合 CSS 类
app_state.progress_bar = ui.linear_progress(value=0, show_value=False).classes('h-4 rounded') app_state.progress_bar = ui.linear_progress(
app_state.progress_bar.props('color=positive') # 使用 Quasar 颜色变量 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'): with ui.card().classes("func-card card-core"):
ui.label('🛠️ 核心功能').classes('section-title text-green') ui.label("🛠️ 核心功能").classes("section-title text-green")
with ui.grid(columns=3).classes('w-full gap-3'): with ui.grid(columns=3).classes("w-full gap-3"):
# 辅助函数:快速创建按钮 # 辅助函数:快速创建按钮
def func_btn(text, icon, func): def func_btn(text, func):
ui.button(text, on_click=lambda: run_task(func)).props(f'outline').classes('w-full') ui.button(text, on_click=lambda: run_task(func)).props(
f"outline"
).classes("w-full")
func_btn('📁 生成图片路径', 'image', generate_template) func_btn("📁 生成图片路径", generate_template)
func_btn('🤖 生成评语 (AI)', 'smart_toy', generate_comment_all) func_btn("🤖 生成评语 (AI)", generate_comment_all)
func_btn('📊 生成报告 (PPT)', 'analytics', generate_report) func_btn("📊 生成报告 (PPT)", generate_report)
# 特殊处理带参数的 func_btn("📑 格式转换 (PDF)", generate_convert_pdf)
async def run_convert(): func_btn("🐂 生肖转化 (生日)", generate_zodiac)
await run_task(batch_convert_folder, config.get("output_folder"))
ui.button('📑 格式转换 (PDF)', on_click=run_convert).props('outline') # 签名按钮
func_btn('🐂 生肖转化 (生日)', 'pets', generate_zodiac) async def on_signature_click():
func_btn('💴 园长一键签名', 'refresh', generate_signature) 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.grid(columns=2).classes('w-full gap-4'):
# 数据管理 # 数据管理
with ui.card().classes('func-card card-data'): with ui.card().classes("func-card card-data"):
ui.label('📦 数据管理').classes('section-title text-blue') ui.label("⚙️ 系统操作").classes("section-title text-blue")
with ui.row().classes('w-full'):
async def do_export(func):
path = await select_folder()
if path: await run_task(func, path)
ui.button('📦 导出模板', on_click=lambda: do_export(export_templates_folder)).props(f'outline')
ui.button('📤 导出备份', on_click=lambda: do_export(export_data)).props(f'outline')
# 系统操作
with ui.card().classes('func-card card-system'):
ui.label('⚙️ 系统操作').classes('section-title text-red')
with ui.row().classes('w-full'):
def stop_now(): def stop_now():
if app_state.is_running: if app_state.is_running:
app_state.stop_event.set() app_state.stop_event.set()
ui.notify("发送停止信号...", type="warning") ui.notify("发送停止信号...", type="warning")
ui.button('⛔ 停止', on_click=stop_now).props('color=negative').classes('flex-1') 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"
)
async def reset_sys():
await run_task(initialize_project)
ui.button('⚠️ 初始化', on_click=reset_sys).props('outline color=warning').classes('flex-1')
# === 日志区 === # === 日志区 ===
with ui.card().classes('func-card card-logging'): with ui.card().classes("func-card card-logging"):
with ui.expansion('📝 系统实时日志',value=True).classes('w-full bg-white shadow-sm rounded'): with ui.expansion("📝 系统实时日志", value=False).classes(
app_state.log_element = ui.log(max_lines=200).classes('w-full h-40 font-mono text-xs bg-gray-100 p-2') "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
View 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")

View File

@@ -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}")

View File

@@ -5,6 +5,7 @@ from loguru import logger
import zipfile import zipfile
import traceback import traceback
def export_templates_folder(output_folder, stop_event, progress_callback=None): def export_templates_folder(output_folder, stop_event, progress_callback=None):
""" """
将指定文件夹压缩为 zip 包 将指定文件夹压缩为 zip 包
@@ -121,7 +122,9 @@ def export_data(save_dir, root_dir=".", progress_callback=None):
has_files = True has_files = True
# 更新进度条 # 更新进度条
if progress_callback: if progress_callback:
progress_callback(processed_count + 1, total_files, "导出数据中...") progress_callback(
processed_count + 1, total_files, "导出数据中..."
)
if has_files: if has_files:
# 确保进度条最后能走到 100% # 确保进度条最后能走到 100%
@@ -139,6 +142,7 @@ def export_data(save_dir, root_dir=".", progress_callback=None):
except Exception as e: except Exception as e:
logger.error(f"导出过程出错: {str(e)}") logger.error(f"导出过程出错: {str(e)}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return None return None
@@ -218,6 +222,7 @@ def check_file_exists(file_path):
""" """
return file_path and isinstance(file_path, str) and os.path.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"): def get_output_pptx_files(output_dir="output"):
""" """
获取 output 文件夹下所有的 pptx 文件 获取 output 文件夹下所有的 pptx 文件
@@ -241,3 +246,17 @@ def get_output_pptx_files(output_dir="output"):
except Exception as e: except Exception as e:
logger.error(f"发生未知错误: {e}") logger.error(f"发生未知错误: {e}")
logger.error(traceback.format_exc()) 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}")

View File

@@ -22,15 +22,6 @@ from utils.growt_utils import (
replace_four_page, replace_four_page,
replace_five_page, replace_five_page,
) )
from utils.pptx_utils import replace_picture
# 如果你之前没有全局定义 console这里定义一个
console = Console()
# ==========================================
# 1. 配置区域 (Configuration)
# ==========================================
config = load_config("config.toml")
# ========================================== # ==========================================
@@ -42,6 +33,13 @@ def generate_template(stop_event: threading.Event = None, progress_callback=None
:params stop_event 任务是否停止事件监听UI的事件监听 :params stop_event 任务是否停止事件监听UI的事件监听
:params progress_callback 进度回调函数 :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. 读取数据
df = pd.read_excel(config["excel_file"], sheet_name="Sheet1") df = pd.read_excel(config["excel_file"], sheet_name="Sheet1")
@@ -89,6 +87,13 @@ def generate_comment_all(stop_event: threading.Event = None, progress_callback=N
:params stop_event 任务是否停止事件监听UI的事件监听 :params stop_event 任务是否停止事件监听UI的事件监听
:params progress_callback 进度回调函数 :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"]
@@ -109,9 +114,6 @@ def generate_comment_all(stop_event: threading.Event = None, progress_callback=N
if stop_event and stop_event.is_set(): if stop_event and stop_event.is_set():
logger.warning("任务正在停止中,正在中断中.....") logger.warning("任务正在停止中,正在中断中.....")
return # 停止任务 return # 停止任务
# 添加进度条
if progress_callback:
progress_callback(i + 1, total_count, "生成学生评语")
# 获取学生姓名 # 获取学生姓名
name = df.at[i, "姓名"] name = df.at[i, "姓名"]
@@ -135,7 +137,11 @@ def generate_comment_all(stop_event: threading.Event = None, progress_callback=N
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:
@@ -176,7 +182,13 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
根据学生姓名生成成长报告 根据学生姓名生成成长报告
:params stop_event 任务是否停止事件监听UI的事件监听 :params stop_event 任务是否停止事件监听UI的事件监听
:params progress_callback 进度回调函数 :params progress_callback 进度回调函数
""" """ # 1. 加载配置文件
try:
config = load_config("config.toml")
except Exception as e:
logger.error(f"配置文件获取失败: {str(e)}")
# 打印详细报错位置,方便调试
logger.error(traceback.format_exc())
# 1. 检查模版文件是否存在 # 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"]}")
@@ -212,9 +224,6 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
if stop_event and stop_event.is_set(): if stop_event and stop_event.is_set():
logger.warning("任务正在停止中,正在中断中.....") logger.warning("任务正在停止中,正在中断中.....")
return return
# 更新进度条
if progress_callback:
progress_callback(i + 1, total_count, "生成报告")
# 解包数据 # 解包数据
( (
name, name,
@@ -228,7 +237,13 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
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}")
# 每次循环重新加载模版 # 每次循环重新加载模版
@@ -262,11 +277,31 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
"birthday": ( "birthday": (
birthday.strftime("%Y-%m-%d") if pd.notna(birthday) else " " birthday.strftime("%Y-%m-%d") if pd.notna(birthday) else " "
), ),
"zodiac": str(zodiac).strip() if str(zodiac).strip() or not str(zodiac).strip().lower() else " ", "zodiac": (
"friend": str(friend).strip() if str(friend).strip() or not str(friend).strip().lower() else " ", str(zodiac).strip()
"hobby": str(hobby).strip() if str(hobby).strip() or not str(hobby).strip().lower() else " ", if str(zodiac).strip() or not str(zodiac).strip().lower()
"game": str(game).strip() if str(game).strip() or not str(game).strip().lower() else " ", else " "
"food": str(food).strip() if str(food).strip() or not str(food).strip().lower() else " ", ),
"friend": (
str(friend).strip()
if str(friend).strip() or not str(friend).strip().lower()
else " "
),
"hobby": (
str(hobby).strip()
if str(hobby).strip() or not str(hobby).strip().lower()
else " "
),
"game": (
str(game).strip()
if str(game).strip() or not str(game).strip().lower()
else " "
),
"food": (
str(food).strip()
if str(food).strip() or not str(food).strip().lower()
else " "
),
} }
# 获取学生个人照片路径 # 获取学生个人照片路径
me_image_path = find_image_path(student_image_folder, "me") me_image_path = find_image_path(student_image_folder, "me")
@@ -334,17 +369,24 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
# ========================================== # ==========================================
# 4. 转换格式(根据names.xlsx文件生成PPT转PDF) # 4. 转换格式(根据names.xlsx文件生成PPT转PDF)
# ========================================== # ==========================================
def batch_convert_folder(folder_path, stop_event: threading.Event = None, progress_callback=None): def generate_convert_pdf(stop_event: threading.Event = None, progress_callback=None):
""" """
批量转换文件夹下的所有 PPT 批量转换文件夹下的所有 PPT
:params folder_path 需要转换的PPT文件夹 :params folder_path 需要转换的PPT文件夹
:params stop_event 任务是否停止事件监听UI的事件监听 :params stop_event 任务是否停止事件监听UI的事件监听
:params progress_callback 进度回调函数 :params progress_callback 进度回调函数
""" """
# 1. 加载配置文件
try:
config = load_config("config.toml")
except Exception as e:
logger.error(f"配置文件获取失败: {str(e)}")
# 打印详细报错位置,方便调试
logger.error(traceback.format_exc())
# 子线程初始化 COM 组件 # 子线程初始化 COM 组件
pythoncom.CoInitialize() pythoncom.CoInitialize()
try: try:
folder_path = os.path.abspath(folder_path) 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
@@ -384,7 +426,9 @@ def batch_convert_folder(folder_path, stop_event: threading.Event = None, progre
logger.info(f"[跳过] 已存在: {filename}") logger.info(f"[跳过] 已存在: {filename}")
continue continue
logger.info(f"[{files.index(filename)}/{total_count}]正在转换: {filename} ...") logger.info(
f"[{files.index(filename)}/{total_count}]正在转换: {filename} ..."
)
try: try:
# 打开 -> 另存为 -> 关闭 # 打开 -> 另存为 -> 关闭
@@ -424,6 +468,13 @@ def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
:params stop_event 任务是否停止事件监听UI的事件监听 :params stop_event 任务是否停止事件监听UI的事件监听
:params progress_callback 进度回调函数 :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"]
@@ -444,7 +495,7 @@ def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
logger.info(f"开始生成学生属相,共 {total_count} 位学生...") logger.info(f"开始生成学生属相,共 {total_count} 位学生...")
# 3. 预处理:将“生日”列转换为 datetime 格式 # 3. 预处理:将“生日”列转换为 datetime 格式
df['temp_date'] = pd.to_datetime(df[date_column], errors="coerce") df["temp_date"] = pd.to_datetime(df[date_column], errors="coerce")
# 4. 遍历 DataFrame 并计算/更新数据 # 4. 遍历 DataFrame 并计算/更新数据
for i, row in df.iterrows(): for i, row in df.iterrows():
@@ -458,7 +509,7 @@ def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
progress_callback(i + 1, total_count, "生成属相") progress_callback(i + 1, total_count, "生成属相")
name = row.get("姓名", f"学生_{i + 1}") name = row.get("姓名", f"学生_{i + 1}")
date = row['temp_date'] date = row["temp_date"]
logger.info(f"[{i + 1}/{total_count}] 正在处理学生:{name}...") logger.info(f"[{i + 1}/{total_count}] 正在处理学生:{name}...")
@@ -479,7 +530,7 @@ def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
logger.info(f" -> 属相计算成功:{name} ,属相: {zodiac}") logger.info(f" -> 属相计算成功:{name} ,属相: {zodiac}")
# 6. 清理和保存结果 # 6. 清理和保存结果
df = df.drop(columns=['temp_date']) df = df.drop(columns=["temp_date"])
save_path = excel_path save_path = excel_path
try: try:
@@ -498,13 +549,21 @@ def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
logger.error(f"程序运行出错: {str(e)}") logger.error(f"程序运行出错: {str(e)}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
# ========================================== # ==========================================
# 6. 一键生成园长签名(根据输出文件夹生成签名) # 6. 一键生成园长签名(根据输出文件夹生成签名)
# ========================================== # ==========================================
def generate_signature(progress_callback=None) -> str: 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: try:
# 获取所有的PPT (此时返回的是文件名或路径的列表) # 获取所有的PPT (此时返回的是文件名或路径的列表)
pptx_files = get_output_pptx_files(config["output_folder"]) pptx_files = get_output_pptx_files(config["output_folder"])
@@ -521,25 +580,53 @@ def generate_signature(progress_callback=None) -> str:
logger.warning(f"⚠️ 警告: 缺少签名照片('signature'") logger.warning(f"⚠️ 警告: 缺少签名照片('signature'")
return return
logger.info(f"签名图片存在: {img_path}") 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): for i, filename in enumerate(pptx_files):
# 获取完整绝对路径 # 获取完整绝对路径
pptx_path = os.path.join(config["output_folder"], filename) pptx_path = os.path.join(config["output_folder"], filename)
# --- 关键修改点 1: 打开 PPT 对象 --- # 打开 PPT 对象
prs = Presentation(pptx_path) prs = Presentation(pptx_path)
# --- 关键修改点 2: 传递 prs 对象而不是路径字符串 --- # 获取第二张幻灯片 (索引为1)
replace_picture(prs, 1, "signature", img_path) slide = prs.slides[1]
# --- 关键修改点 3: 保存修改后的 PPT --- # 获取修正后的图片流
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) prs.save(pptx_path)
# 更新进度条 (如果有 callback) # 更新进度条 (如果有 callback)
if progress_callback: if progress_callback:
progress_callback(i + 1, len(pptx_files),f"[{i + 1}/{len(pptx_files)}] 生成签名完成: {filename}") progress_callback(
i + 1,
len(pptx_files),
f"[{i + 1}/{len(pptx_files)}] 生成签名完成: {filename}",
)
logger.success(f"[{i + 1}/{len(pptx_files)}] 生成签名完成: {filename}") logger.success(f"[{i + 1}/{len(pptx_files)}] 生成签名完成: {filename}")
if progress_callback: if progress_callback:
progress_callback(len(pptx_files), len(pptx_files), "签名生成完成") progress_callback(len(pptx_files), len(pptx_files), "签名生成完成")
except Exception as e: except Exception as e:
logger.error(f"generate_signature 发生未知错误: {e}") logger.error(f"generate_signature 发生未知错误: {e}")
# 打印详细报错位置,方便调试
logger.error(traceback.format_exc())
return str(e) return str(e)

View File

@@ -21,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. 设置新文本
@@ -51,49 +51,67 @@ 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):
@@ -124,8 +142,17 @@ def replace_picture(prs, slide_index, placeholder, img_path):
break break
if target_shape: 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 left, top, width, height = (
target_shape.left,
target_shape.top,
target_shape.width,
target_shape.height,
)
# 2. 获取修正后的图片流 # 2. 获取修正后的图片流
img_stream = get_corrected_image_stream(img_path) img_stream = get_corrected_image_stream(img_path)
@@ -138,3 +165,5 @@ def replace_picture(prs, slide_index, placeholder, img_path):
# 5. 恢复层级位置 (z-order) # 5. 恢复层级位置 (z-order)
sp_tree.insert(target_index, new_shape._element) sp_tree.insert(target_index, new_shape._element)
else:
logger.warning(f"警告: 幻灯片 {slide_index} 中未找到占位符 {placeholder}")