Compare commits
12 Commits
f64f005292
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 60a78ed1e3 | |||
| 016e392524 | |||
| 463c1c8b8f | |||
| de71594812 | |||
| f3d16ec1f9 | |||
| 1c2d5db393 | |||
| d1f6d7da7d | |||
| 1c9aa24202 | |||
| 54398e2cbe | |||
| d3c0121632 | |||
| 3c60b3e7ca | |||
| ba7dd09037 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,3 +12,6 @@ output/*.pptx
|
||||
output/*.pdf
|
||||
data/images/*
|
||||
data/*.xlsx
|
||||
|
||||
.idea/
|
||||
.trae/
|
||||
131
.idea/workspace.xml
generated
131
.idea/workspace.xml
generated
@@ -4,19 +4,8 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="41690157-d51b-4dae-98de-6b96990d681a" name="更改" comment="">
|
||||
<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" />
|
||||
<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" />
|
||||
<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>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -26,33 +15,34 @@
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo"><![CDATA[{
|
||||
"customColor": "",
|
||||
"associatedIndex": 0
|
||||
}]]></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"><![CDATA[{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"Python.main_nicegui.executor": "Run",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "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": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<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>
|
||||
<component name="RunManager">
|
||||
<configuration name="main_nicegui" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||
<module name="growth_report" />
|
||||
@@ -68,6 +58,7 @@
|
||||
<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" />
|
||||
@@ -83,6 +74,14 @@
|
||||
</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="" />
|
||||
@@ -90,14 +89,76 @@
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<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 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="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>
|
||||
</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"
|
||||
}
|
||||
}
|
||||
18
IFLOW.md
18
IFLOW.md
@@ -2,7 +2,7 @@
|
||||
|
||||
## 项目概述
|
||||
|
||||
这是一个基于Python的自动化幼儿园成长报告生成系统。该系统可以从Excel数据文件中读取幼儿信息,结合AI生成个性化评语,并将所有信息批量填充到PPT模板中,最终生成每个学生的个性化成长报告。系统支持双界面运行(命令行界面、图形界面和NiceGUI界面),具备字体安装、图片替换、批量PDF转换、生肖计算等功能。
|
||||
基于Python的自动化幼儿园学期成长报告生成系统。该系统可以从Excel数据文件中读取幼儿信息,结合AI生成个性化评语,并将所有信息批量填充到PPT模板中,最终生成每个学生的个性化成长报告。系统支持UI界面操作,具备字体安装、图片替换、批量PDF转换、生肖计算、模板导出等完整功能。
|
||||
|
||||
## 技术栈
|
||||
|
||||
@@ -47,17 +47,23 @@
|
||||
- 批量更新Excel中的生肖信息
|
||||
- 支持日期格式自动识别
|
||||
|
||||
### 6. 导出数据模板 (📦 导出数据模板)
|
||||
### 6. 园长一键签名 (💴 园长一键签名)
|
||||
|
||||
- 一键为所有生成的报告添加园长签名
|
||||
- 自动识别输出文件夹中的PPT文件
|
||||
- 批量替换签名占位符为实际签名图片
|
||||
|
||||
### 7. 导出数据模板 (📦 导出数据模板)
|
||||
- 生成标准化的数据模板ZIP包
|
||||
- 包含示例Excel文件和图片文件夹结构
|
||||
- 方便新用户快速上手
|
||||
|
||||
### 7. 初始化系统 (📤 初始化系统)
|
||||
### 8. 初始化系统 (📤 初始化系统)
|
||||
- 自动创建必要的目录结构
|
||||
- 安装所需字体文件
|
||||
- 配置系统环境
|
||||
|
||||
### 8. 字体安装 (🔤 字体安装)
|
||||
### 9. 字体安装 (🔤 字体安装)
|
||||
- 自动检测系统是否安装了指定字体
|
||||
- 自动安装项目所需的字体文件
|
||||
- 支持方正兰亭黑简体和方正少儿简体字体
|
||||
@@ -70,7 +76,7 @@ growth_report/
|
||||
├── UI.py # 图形用户界面入口(tkinter)
|
||||
├── main_nicegui.py # NiceGUI界面入口
|
||||
├── main.pyw # Windows图形界面启动文件
|
||||
├── config.env.toml # 项目配置文件
|
||||
├── config.toml # 项目配置文件
|
||||
├── pyproject.toml # 项目依赖配置
|
||||
├── start_app.bat # Windows启动批处理文件
|
||||
├── README.md # 项目说明文档
|
||||
@@ -199,7 +205,7 @@ pip install -r requirements.txt
|
||||
|
||||
### 初始化设置
|
||||
|
||||
1. 编辑`config.env.toml`配置文件,设置API密钥和其他参数
|
||||
1. 编辑`config.toml`配置文件,设置API密钥和其他参数
|
||||
2. 准备Excel数据文件(按指定格式)
|
||||
3. 准备图片资源文件夹(按指定结构)
|
||||
4. 准备PPT模板文件
|
||||
|
||||
132
README.md
132
README.md
@@ -13,10 +13,11 @@
|
||||
- 🤖 **AI评语**: 智能生成个性化、治愈系风格的幼儿评语
|
||||
- 🖼️ **图文并茂**: 支持个人照片、活动照片、班级合影的自动替换
|
||||
- 📄 **格式转换**: 批量PPT转PDF,便于分发和存档
|
||||
- 🎨 **多界面**: 提供命令行界面、tkinter图形界面和NiceGUI现代Web界面,满足不同用户需求
|
||||
- 🎨 **现代界面**: 提供NiceGUI现代Web界面,操作直观友好
|
||||
- 🐲 **生肖计算**: 根据生日自动计算生肖信息
|
||||
- 📦 **模板导出**: 生成标准化数据模板,快速上手
|
||||
- 🔤 **字体安装**: 自动检测和安装所需字体文件
|
||||
- ✍️ **签名生成**: 不依赖占位符,直接在指定位置添加园长签名
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
@@ -27,7 +28,6 @@
|
||||
- **comtypes**: PowerPoint转PDF功能
|
||||
- **rich**: 美化命令行界面
|
||||
- **loguru**: 日志记录
|
||||
- **tkinter**: 图形用户界面
|
||||
- **nicegui**: 现代Web界面
|
||||
- **tomli**: 配置文件解析
|
||||
|
||||
@@ -43,51 +43,46 @@
|
||||
|
||||
```bash
|
||||
git clone https://gitee.com/hanhanshibaobei/growthreport.git
|
||||
cd growthreport
|
||||
cd growth_report
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
#### 使用uv(推荐)
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
#### 使用pip
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. 配置系统
|
||||
|
||||
编辑 `config.env.toml` 文件,设置以下信息:
|
||||
编辑 `config.toml` 文件,设置以下信息:
|
||||
|
||||
- AI API密钥和配置
|
||||
- 班级信息和教师名单
|
||||
- 文件路径配置
|
||||
- 签名位置配置(可选)
|
||||
|
||||
### 4. 运行程序
|
||||
|
||||
#### NiceGUI界面(推荐,现代Web界面)
|
||||
```bash
|
||||
python main_nicegui.py
|
||||
```
|
||||
|
||||
#### 图形界面(tkinter界面)
|
||||
```bash
|
||||
python UI.py
|
||||
python main.py
|
||||
```
|
||||
|
||||
或直接运行:
|
||||
|
||||
```bash
|
||||
start_app.bat
|
||||
```
|
||||
|
||||
#### 命令行界面
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## 📖 使用指南
|
||||
|
||||
### 功能模块
|
||||
@@ -100,24 +95,25 @@ python main.py
|
||||
6. **📦 导出数据模板**: 生成标准化模板
|
||||
7. **📤 初始化系统**: 配置系统环境
|
||||
8. **🔤 字体安装**: 自动安装和检测所需字体
|
||||
9. **✍️ 生成签名**: 不依赖占位符,直接在指定位置添加园长签名
|
||||
|
||||
### Excel数据格式
|
||||
|
||||
Excel文件应包含以下列(顺序必须与配置文件中一致):
|
||||
|
||||
| 列名 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| 姓名 | 学生姓名 | 张小明 |
|
||||
| 英文名 | 英文昵称 | Tom |
|
||||
| 性别 | 性别 | 男 |
|
||||
| 生日 | 出生日期 | 2019-03-15 |
|
||||
| 属相 | 生肖(自动计算) | 猪 |
|
||||
| 我的好朋友 | 好朋友姓名 | 李小红 |
|
||||
| 我的爱好 | 个人爱好 | 画画、唱歌 |
|
||||
| 喜欢的游戏 | 喜欢的游戏 | 积木、捉迷藏 |
|
||||
| 喜欢吃的食物 | 喜欢的食物 | 苹果、饼干 |
|
||||
| 评价 | AI生成的评语 | 自动填充 |
|
||||
| 表现特征 | 表现关键词(可选) | 活泼、聪明 |
|
||||
| 列名 | 说明 | 示例 |
|
||||
| ------------ | ------------------ | ------------ |
|
||||
| 姓名 | 学生姓名 | 张小明 |
|
||||
| 英文名 | 英文昵称 | Tom |
|
||||
| 性别 | 性别 | 男 |
|
||||
| 生日 | 出生日期 | 2019-03-15 |
|
||||
| 属相 | 生肖(自动计算) | 猪 |
|
||||
| 我的好朋友 | 好朋友姓名 | 李小红 |
|
||||
| 我的爱好 | 个人爱好 | 画画、唱歌 |
|
||||
| 喜欢的游戏 | 喜欢的游戏 | 积木、捉迷藏 |
|
||||
| 喜欢吃的食物 | 喜欢的食物 | 苹果、饼干 |
|
||||
| 评价 | AI生成的评语 | 自动填充 |
|
||||
| 表现特征 | 表现关键词(可选) | 活泼、聪明 |
|
||||
|
||||
### 图片文件结构
|
||||
|
||||
@@ -132,35 +128,47 @@ data/images/
|
||||
|
||||
支持多种图片格式:.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/
|
||||
├── main.py # 主程序入口(命令行界面)
|
||||
├── UI.py # 图形用户界面入口(tkinter)
|
||||
├── main_nicegui.py # NiceGUI界面入口
|
||||
├── main.pyw # Windows图形界面启动文件
|
||||
├── config.env.toml # 项目配置文件
|
||||
├── main.py # NiceGUI界面入口
|
||||
├── config.toml # 项目配置文件
|
||||
├── pyproject.toml # 项目依赖配置
|
||||
├── start_app.bat # 启动脚本
|
||||
├── README.md # 项目说明文档
|
||||
├── IFLOW.md # 项目详细说明文档
|
||||
├── config/
|
||||
│ ├── config.py # 配置加载工具
|
||||
│ └── output/ # 配置输出目录
|
||||
├── ui/
|
||||
│ ├── app_window.py # tkinter图形界面
|
||||
│ ├── main_nicegui.py # NiceGUI界面主文件
|
||||
│ ├── assets/
|
||||
│ │ ├── icon.ico # 应用图标
|
||||
│ │ └── style.css # 样式文件
|
||||
│ ├── core/
|
||||
│ │ ├── logger.py # 日志处理
|
||||
│ │ ├── state.py # 应用状态管理
|
||||
│ │ ├── task_runner.py # 任务运行器
|
||||
│ │ └── __pycache__/
|
||||
│ │ └── task_runner.py # 任务运行器
|
||||
│ └── views/
|
||||
│ └── home_page.py # NiceGUI主页面
|
||||
│ ├── config_page.py # 配置页面
|
||||
│ ├── data_page.py # 数据页面
|
||||
│ └── home_page.py # 主页面
|
||||
├── utils/
|
||||
│ ├── agent_utils.py # AI评语生成工具
|
||||
│ ├── file_utils.py # 文件操作工具
|
||||
@@ -171,14 +179,14 @@ growth_report/
|
||||
│ ├── log_handler.py # 日志处理器
|
||||
│ ├── pdf_utils.py # PDF转换工具
|
||||
│ ├── pptx_utils.py # PPT文本和图片替换工具
|
||||
│ ├── template_utils.py # 模板处理工具
|
||||
│ └── zodiac_utils.py # 生肖计算工具
|
||||
├── data/
|
||||
│ ├── names.xlsx # 学生数据Excel文件
|
||||
│ └── images/ # 学生图片资源文件夹
|
||||
│ ├── signature.png # 签名图片
|
||||
├── fonts/ # 字体文件目录
|
||||
├── templates/ # PPT模板文件
|
||||
├── output/ # 生成的报告输出目录
|
||||
└── old/ # 旧版本文件备份
|
||||
└── public/ # 公共资源文件
|
||||
```
|
||||
|
||||
## 🤖 AI评语生成
|
||||
@@ -191,11 +199,13 @@ growth_report/
|
||||
- 表现特征
|
||||
|
||||
评语风格为"治愈系",采用三段式结构:
|
||||
|
||||
1. **开头**: 亲切问候 + 总体印象
|
||||
2. **正文**: 具体描述孩子的进步和优点
|
||||
3. **结尾**: 委婉期望 + 新学期祝福
|
||||
|
||||
支持分龄侧重评价:
|
||||
|
||||
- **小班 (3-4岁)**: 适应集体生活、情绪稳定性、基本生活自理能力
|
||||
- **中班 (4-5岁)**: 社交互动、分享与合作、动手能力、好奇心
|
||||
- **大班 (5-6岁)**: 学习习惯、逻辑思维、领导力、幼小衔接准备
|
||||
@@ -204,39 +214,55 @@ growth_report/
|
||||
|
||||
### AI配置
|
||||
|
||||
在 `config.env.toml` 中配置AI模型:
|
||||
在 `config.toml` 中配置AI模型:
|
||||
|
||||
```toml
|
||||
[ai]
|
||||
api_key = "your-api-key"
|
||||
api_url = "https://api.openai.com/v1"
|
||||
model = "gpt-3.5-turbo"
|
||||
api_url = "https://apis.iflow.cn/v1/chat/completions"
|
||||
model = "deepseek-v3.2"
|
||||
prompt = """
|
||||
你的评语风格是"治愈系"的,能让家长读完后感到欣慰...
|
||||
# Role
|
||||
你是一位拥有20年经验的资深幼儿园主班老师。你的文笔温暖、细腻、充满爱意,擅长发现每个孩子身上独特的闪光点。你的评语风格是“治愈系”的,能让家长读完后感到欣慰并对未来充满希望。
|
||||
|
||||
# Goal
|
||||
请根据用户提供的【幼儿姓名】、【年龄段/班级】以及【日常表现关键词/评分数据】,撰写一份高质量的学期末成长评语。
|
||||
"""
|
||||
```
|
||||
|
||||
### 自定义PPT模板
|
||||
### 签名配置
|
||||
|
||||
1. 在 `templates/` 目录放置PPT模板
|
||||
2. 使用占位符格式:`{{变量名}}`
|
||||
3. 支持的占位符:
|
||||
- `{{name}}`: 学生姓名
|
||||
- `{{class_name}}`: 班级名称
|
||||
- `{{comments}}`: 评语内容
|
||||
- 等等...
|
||||
在 `config.toml` 中配置签名位置:
|
||||
|
||||
```toml
|
||||
[paths]
|
||||
signature_image = "data/signature.png"
|
||||
|
||||
# 签名位置配置
|
||||
signature_left = 2987040 # 左位置
|
||||
signature_top = 8273415 # 上位置
|
||||
signature_width = 1800000 # 宽度
|
||||
signature_height = 720000 # 高度
|
||||
```
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q: PDF转换失败怎么办?
|
||||
|
||||
A: 请确保已安装Microsoft PowerPoint,并且没有其他程序占用PPT文件。
|
||||
|
||||
### Q: AI评语生成失败?
|
||||
|
||||
A: 检查API密钥配置是否正确,网络连接是否正常。
|
||||
|
||||
### Q: 字体显示异常?
|
||||
|
||||
A: 系统会自动安装所需字体,如仍有问题请手动安装 `fonts/` 目录下的字体文件。
|
||||
|
||||
### Q: 签名生成失败?
|
||||
|
||||
A: 检查签名图片路径是否正确,以及签名位置配置是否合适。
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
|
||||
19
config.toml
19
config.toml
@@ -1,25 +1,26 @@
|
||||
[paths]
|
||||
source_file = "(横板)中班 幼儿学期发展报告.pptx"
|
||||
source_file = "大四班 幼儿学期发展报告.pptx"
|
||||
output_folder = "output"
|
||||
excel_file = "names.xlsx"
|
||||
image_folder = "images"
|
||||
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_name = "K3A"
|
||||
class_name = "K4D"
|
||||
teachers = [
|
||||
"丁文敏",
|
||||
"麦芷晴",
|
||||
"徐焕奎",
|
||||
"康璐璐",
|
||||
"冯宇阳",
|
||||
"孙继艳",
|
||||
]
|
||||
class_type = 2
|
||||
|
||||
[defaults]
|
||||
default_comment = ""
|
||||
age_group = "中班上学期"
|
||||
age_group = "大班上学期"
|
||||
|
||||
[ai]
|
||||
api_key = "sk-8b0c9522df8843b4d0e7e91ecb628957"
|
||||
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\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"
|
||||
|
||||
@@ -18,24 +18,27 @@ except ImportError:
|
||||
# 如果没安装,提供一个 fallback 提示
|
||||
toml_write = None
|
||||
|
||||
|
||||
def get_base_dir():
|
||||
if getattr(sys, 'frozen', False):
|
||||
if getattr(sys, "frozen", False):
|
||||
return os.path.dirname(sys.executable)
|
||||
else:
|
||||
# 假设当前文件在项目根目录或根目录下的某个文件夹中
|
||||
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def get_resource_path(relative_path):
|
||||
base_path = get_base_dir()
|
||||
external_path = os.path.join(base_path, relative_path)
|
||||
if os.path.exists(external_path):
|
||||
return external_path
|
||||
if getattr(sys, 'frozen', False):
|
||||
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)
|
||||
# ==========================================
|
||||
@@ -44,7 +47,7 @@ def load_config(config_filename="config.toml"):
|
||||
|
||||
if not os.path.exists(config_path):
|
||||
# 如果彻底找不到,返回一个最小化的默认值,防止程序奔溃
|
||||
return { "source_file": "", "ai": {"api_key": ""}, "teachers": [] }
|
||||
return {"source_file": "", "ai": {"api_key": ""}, "teachers": []}
|
||||
|
||||
try:
|
||||
with open(config_path, "rb") as f:
|
||||
@@ -59,19 +62,32 @@ def load_config(config_filename="config.toml"):
|
||||
|
||||
config = {
|
||||
"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", ""))),
|
||||
"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", ""))),
|
||||
"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", ""))),
|
||||
|
||||
"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": ""}),
|
||||
"ai": data.get(
|
||||
"ai", {"api_key": "", "api_url": "", "model": "", "prompt": ""}
|
||||
),
|
||||
}
|
||||
return config
|
||||
|
||||
@@ -79,6 +95,7 @@ def load_config(config_filename="config.toml"):
|
||||
print(f"解析配置文件失败: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 2. 配置保存 (Config Saver)
|
||||
# ==========================================
|
||||
@@ -94,21 +111,28 @@ def save_config(config_data, config_filename="config.toml"):
|
||||
new_data = {
|
||||
"paths": {
|
||||
"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", "")),
|
||||
"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_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", {})
|
||||
"ai": config_data.get("ai", {}),
|
||||
}
|
||||
|
||||
# 写入文件
|
||||
|
||||
BIN
data/names.xlsx
BIN
data/names.xlsx
Binary file not shown.
@@ -1,32 +1,36 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from nicegui import ui, app, run, native
|
||||
from loguru import logger
|
||||
|
||||
from screeninfo import get_monitors
|
||||
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 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.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')
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
# 1. 初始化配置
|
||||
config = load_config("config.toml")
|
||||
|
||||
setup_logger()
|
||||
|
||||
|
||||
# === 关键修改:定义一个获取路径的通用函数 ===
|
||||
def get_path(relative_path):
|
||||
"""
|
||||
获取资源的绝对路径。
|
||||
兼容:开发环境(直接运行) 和 生产环境(打包成exe后解压的临时目录)
|
||||
"""
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
# 开发环境当前目录
|
||||
@@ -63,26 +67,40 @@ def calculate_window_size():
|
||||
logger.info(f"屏幕分辨率: {screen_width}x{screen_height}")
|
||||
logger.info(f"设置窗口大小为: {target_width}x{target_height}")
|
||||
|
||||
return (target_width, target_height)
|
||||
return target_width, target_height
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"无法获取屏幕分辨率 ({e}),使用默认大小 (900, 900)")
|
||||
return (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)
|
||||
app.add_static_files("/assets", static_dir)
|
||||
|
||||
|
||||
# 3. 页面路由
|
||||
@ui.page('/')
|
||||
@ui.page("/")
|
||||
def index_page():
|
||||
create_page()
|
||||
create_home_page()
|
||||
|
||||
@ui.page('/config')
|
||||
|
||||
@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:
|
||||
@@ -104,5 +122,5 @@ if __name__ in {"__main__", "__mp_main__"}:
|
||||
native=True,
|
||||
window_size=calculated_size,
|
||||
port=native.find_open_port(), # 自动寻找端口
|
||||
reload=False
|
||||
reload=True,
|
||||
)
|
||||
81
main.pyw
81
main.pyw
@@ -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()
|
||||
@@ -37,10 +37,10 @@ echo [INFO] 正在拉起主程序...
|
||||
echo ---------------------------------------------------
|
||||
|
||||
:: =======================================================
|
||||
:: 【关键修改】路径改为根目录的 main.pyw
|
||||
:: 【关键修改】路径改为根目录的 main.py
|
||||
:: 使用 start 命令启动,这样黑色的 CMD 窗口可以随后立即关闭
|
||||
:: =======================================================
|
||||
start "" uv run main.pyw
|
||||
start "" uv run main.py
|
||||
|
||||
:: 等待 1 秒确保启动
|
||||
timeout /t 1 >nul
|
||||
|
||||
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.
236
ui/app_window.py
236
ui/app_window.py
@@ -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, "任务进度: 就绪")
|
||||
@@ -2,58 +2,63 @@
|
||||
|
||||
/* 全局字体 */
|
||||
body {
|
||||
font-family: "微软雅黑", "Microsoft YaHei", sans-serif;
|
||||
background-color: #f0f4f8;
|
||||
font-family: '微软雅黑', 'Microsoft YaHei', sans-serif;
|
||||
background-color: #f0f4f8;
|
||||
}
|
||||
|
||||
/* 标题栏 */
|
||||
.app-header {
|
||||
background-color: #2E8B57; /* SeaGreen */
|
||||
color: white;
|
||||
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;
|
||||
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 */
|
||||
border-top: 4px solid #16a34a; /* green-600 */
|
||||
}
|
||||
|
||||
/* 数据管理区顶部边框 */
|
||||
.card-data {
|
||||
border-top: 4px solid #3b82f6; /* blue-500 */
|
||||
border-top: 4px solid #3b82f6; /* blue-500 */
|
||||
}
|
||||
|
||||
/* 系统操作区顶部边框 */
|
||||
.card-system {
|
||||
border-top: 4px solid #ef4444; /* red-500 */
|
||||
border-top: 4px solid #ef4444; /* red-500 */
|
||||
}
|
||||
|
||||
.card-logging {
|
||||
border-top: 4px solid #9c1be0;
|
||||
border-top: 4px solid #9c1be0;
|
||||
}
|
||||
|
||||
/* 标题文字 */
|
||||
.section-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* 绿色标题 */
|
||||
.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 */
|
||||
|
||||
@@ -68,24 +73,24 @@ body {
|
||||
* 适用于 NiceGUI 默认的 Chromium 浏览器
|
||||
*/
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
/* 完全隐藏滚动条 */
|
||||
width: 0px;
|
||||
background: transparent; /* 使滚动条轨道透明 */
|
||||
/* 完全隐藏滚动条 */
|
||||
width: 0px;
|
||||
background: transparent; /* 使滚动条轨道透明 */
|
||||
}
|
||||
|
||||
/* 2. 隐藏 Firefox 浏览器的滚动条 */
|
||||
.hide-scrollbar {
|
||||
/* 设置滚动条宽度为 thin (细),比 auto (默认) 要窄 */
|
||||
scrollbar-width: none; /* 'none' 是最新且更彻底的隐藏方式 */
|
||||
/* 设置滚动条宽度为 thin (细),比 auto (默认) 要窄 */
|
||||
scrollbar-width: none; /* 'none' 是最新且更彻底的隐藏方式 */
|
||||
|
||||
/* 确保容器内容溢出时可以滚动 */
|
||||
overflow: auto;
|
||||
/* 确保容器内容溢出时可以滚动 */
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 示例:如果你只想隐藏日志区的滚动条 */
|
||||
.card-logging .q-expansion-item__content .nicegui-log .q-scrollarea__content {
|
||||
/* 如果 nicegui-log 内部使用了 q-scrollarea,可能需要针对其内容应用样式 */
|
||||
scrollbar-width: none;
|
||||
/* 如果 nicegui-log 内部使用了 q-scrollarea,可能需要针对其内容应用样式 */
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* ---------------------------------- */
|
||||
@@ -93,38 +98,85 @@ body {
|
||||
/* 如果完全隐藏不好,可以试试这个更温和的方案 */
|
||||
/* ---------------------------------- */
|
||||
.thin-scrollbar::-webkit-scrollbar {
|
||||
width: 6px; /* 调整宽度 */
|
||||
height: 6px; /* 调整高度 */
|
||||
width: 6px; /* 调整宽度 */
|
||||
height: 6px; /* 调整高度 */
|
||||
}
|
||||
|
||||
.thin-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #a0a0a0; /* 拇指颜色 */
|
||||
border-radius: 3px;
|
||||
border: 1px solid #f0f4f8; /* 边框颜色 */
|
||||
background-color: #a0a0a0; /* 拇指颜色 */
|
||||
border-radius: 3px;
|
||||
border: 1px solid #f0f4f8; /* 边框颜色 */
|
||||
}
|
||||
|
||||
.thin-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.thin-scrollbar {
|
||||
scrollbar-width: thin; /* Firefox 细滚动条 */
|
||||
scrollbar-color: #a0a0a0 transparent; /* Firefox 颜色设置 */
|
||||
scrollbar-width: thin; /* Firefox 细滚动条 */
|
||||
scrollbar-color: #a0a0a0 transparent; /* Firefox 颜色设置 */
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
/* 完全隐藏滚动条 */
|
||||
width: 0px !important;
|
||||
height: 0px !important;
|
||||
background: transparent !important;
|
||||
/* 完全隐藏滚动条 */
|
||||
width: 0px !important;
|
||||
height: 0px !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
#nicegui-content, #q-app {
|
||||
/* 确保容器内容可以滚动,但滚动条被隐藏 */
|
||||
overflow: auto;
|
||||
#nicegui-content,
|
||||
#q-app {
|
||||
/* 确保容器内容可以滚动,但滚动条被隐藏 */
|
||||
overflow: auto;
|
||||
|
||||
/* Firefox 隐藏滚动条 */
|
||||
scrollbar-width: none;
|
||||
/* Firefox 隐藏滚动条 */
|
||||
scrollbar-width: none;
|
||||
|
||||
/* IE/Edge 隐藏滚动条 */
|
||||
-ms-overflow-style: 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;
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
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', ''))
|
||||
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)
|
||||
@@ -16,59 +18,148 @@ def create_config_page():
|
||||
ui.add_head_html('<link href="/assets/style.css" rel="stylesheet" />')
|
||||
|
||||
# 样式修正:添加全屏且不滚动条的 CSS
|
||||
ui.add_head_html('''
|
||||
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.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-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.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.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_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_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')
|
||||
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.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'):
|
||||
with ui.row().classes("w-full p-4"):
|
||||
|
||||
async def handle_save():
|
||||
new_data = {
|
||||
"source_file": source_file.value,
|
||||
@@ -77,16 +168,22 @@ def create_config_page():
|
||||
"output_folder": output_folder.value,
|
||||
"class_name": class_name.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": {
|
||||
"api_key": ai_key.value,
|
||||
"api_url": ai_url.value,
|
||||
"model": ai_model.value,
|
||||
"prompt": ai_prompt.value
|
||||
}
|
||||
"prompt": ai_prompt.value,
|
||||
},
|
||||
}
|
||||
# 修改点 4:直接调用导入的 save_config 函数名
|
||||
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
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")
|
||||
@@ -5,89 +5,116 @@ from ui.core.task_runner import run_task, select_folder
|
||||
|
||||
# 导入业务函数
|
||||
from utils.generate_utils import (
|
||||
generate_template, generate_comment_all,
|
||||
batch_convert_folder, generate_report, generate_zodiac, generate_signature
|
||||
generate_template,
|
||||
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")
|
||||
|
||||
|
||||
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'):
|
||||
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-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')
|
||||
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('系统配置')
|
||||
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
|
||||
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.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')
|
||||
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 颜色变量
|
||||
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.card().classes("func-card card-core"):
|
||||
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):
|
||||
ui.button(text, on_click=lambda: run_task(func)).props(f'outline').classes('w-full')
|
||||
def func_btn(text, func):
|
||||
ui.button(text, on_click=lambda: run_task(func)).props(
|
||||
f"outline"
|
||||
).classes("w-full")
|
||||
|
||||
func_btn('📁 生成图片路径', 'image', generate_template)
|
||||
func_btn('🤖 生成评语 (AI)', 'smart_toy', generate_comment_all)
|
||||
func_btn('📊 生成报告 (PPT)', 'analytics', generate_report)
|
||||
# 特殊处理带参数的
|
||||
async def run_convert():
|
||||
await run_task(batch_convert_folder, config.get("output_folder"))
|
||||
ui.button('📑 格式转换 (PDF)', on_click=run_convert).props('outline')
|
||||
func_btn('🐂 生肖转化 (生日)', 'pets', generate_zodiac)
|
||||
func_btn('💴 园长一键签名', 'refresh', generate_signature)
|
||||
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.grid(columns=2).classes('w-full gap-4'):
|
||||
# 数据管理
|
||||
with ui.card().classes('func-card card-data'):
|
||||
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)
|
||||
# 数据管理
|
||||
with ui.card().classes("func-card card-data"):
|
||||
ui.label("⚙️ 系统操作").classes("section-title text-blue")
|
||||
|
||||
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():
|
||||
if app_state.is_running:
|
||||
app_state.stop_event.set()
|
||||
ui.notify("发送停止信号...", type="warning")
|
||||
def stop_now():
|
||||
if app_state.is_running:
|
||||
app_state.stop_event.set()
|
||||
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.expansion('📝 系统实时日志',value=True).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')
|
||||
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_openai import ChatOpenAI
|
||||
from loguru import logger
|
||||
|
||||
import traceback
|
||||
from config.config import load_config
|
||||
|
||||
config = load_config("config.toml")
|
||||
|
||||
class_type_config =[
|
||||
"本期开展了小袋鼠整合主题课程:(语言、社会、科学、健康、艺术)、生活数学;特色课程(英语、体能、美工、篮球)。",
|
||||
"本学期开展了柏克莱主题课程(语言、社会、科学、艺术、健康);英语及特色课程(体能、舞蹈、美工、魔力猴、足球、国学)。",
|
||||
"本学期开展了双木桥主题课程(图说汉字、妙趣汉音、情智阅读、麦斯思维、专注力训练);英语及特色课程(体能、舞蹈、美工、魔力猴、足球、国学)。"
|
||||
]
|
||||
|
||||
def generate_comment(name, age_group, traits,sex):
|
||||
"""
|
||||
@@ -19,7 +22,14 @@ def generate_comment(name, age_group, traits,sex):
|
||||
:param sex: 性别
|
||||
: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"]
|
||||
llm = ChatOpenAI(
|
||||
base_url=ai_config["api_url"],
|
||||
@@ -42,7 +52,8 @@ def generate_comment(name, age_group, traits,sex):
|
||||
"name": name,
|
||||
"age_group": age_group,
|
||||
"traits": traits,
|
||||
"sex": sex
|
||||
"sex": sex,
|
||||
"class_type": class_type_config[(config.get("class_type", 0))],
|
||||
})
|
||||
cleaned_text = re.sub(r'\s+', '', comment)
|
||||
logger.success(f"学生:{name} =>生成评语成功: {cleaned_text}")
|
||||
|
||||
@@ -5,6 +5,7 @@ from loguru import logger
|
||||
import zipfile
|
||||
import traceback
|
||||
|
||||
|
||||
def export_templates_folder(output_folder, stop_event, progress_callback=None):
|
||||
"""
|
||||
将指定文件夹压缩为 zip 包
|
||||
@@ -121,7 +122,9 @@ def export_data(save_dir, root_dir=".", progress_callback=None):
|
||||
has_files = True
|
||||
# 更新进度条
|
||||
if progress_callback:
|
||||
progress_callback(processed_count + 1, total_files, "导出数据中...")
|
||||
progress_callback(
|
||||
processed_count + 1, total_files, "导出数据中..."
|
||||
)
|
||||
|
||||
if has_files:
|
||||
# 确保进度条最后能走到 100%
|
||||
@@ -139,6 +142,7 @@ def export_data(save_dir, root_dir=".", progress_callback=None):
|
||||
except Exception as e:
|
||||
logger.error(f"导出过程出错: {str(e)}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
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)
|
||||
|
||||
|
||||
def get_output_pptx_files(output_dir="output"):
|
||||
"""
|
||||
获取 output 文件夹下所有的 pptx 文件
|
||||
@@ -241,3 +246,17 @@ def get_output_pptx_files(output_dir="output"):
|
||||
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}")
|
||||
|
||||
@@ -22,26 +22,24 @@ from utils.growt_utils import (
|
||||
replace_four_page,
|
||||
replace_five_page,
|
||||
)
|
||||
from utils.pptx_utils import replace_picture
|
||||
|
||||
# 如果你之前没有全局定义 console,这里定义一个
|
||||
console = Console()
|
||||
|
||||
# ==========================================
|
||||
# 1. 配置区域 (Configuration)
|
||||
# ==========================================
|
||||
config = load_config("config.toml")
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 1. 生成模板(根据names.xlsx文件生成名字图片文件夹)
|
||||
# ==========================================
|
||||
def generate_template(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:
|
||||
# 1. 读取数据
|
||||
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 progress_callback 进度回调函数
|
||||
"""
|
||||
# 1. 加载配置文件
|
||||
try:
|
||||
config = load_config("config.toml")
|
||||
except Exception as e:
|
||||
logger.error(f"配置文件获取失败: {str(e)}")
|
||||
# 打印详细报错位置,方便调试
|
||||
logger.error(traceback.format_exc())
|
||||
try:
|
||||
# 1. 读取数据
|
||||
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():
|
||||
logger.warning("任务正在停止中,正在中断中.....")
|
||||
return # 停止任务
|
||||
# 添加进度条
|
||||
if progress_callback:
|
||||
progress_callback(i + 1, total_count, "生成学生评语")
|
||||
|
||||
# 获取学生姓名
|
||||
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() != "":
|
||||
logger.info(f"[{i + 1}/{total_count}] {name} 已有评语,跳过。")
|
||||
continue
|
||||
|
||||
# 添加进度条
|
||||
if progress_callback:
|
||||
progress_callback(
|
||||
i + 1, total_count, f"[{i + 1}/{total_count}] 正在生成评价: {name}"
|
||||
)
|
||||
logger.info(f"[{i + 1}/{total_count}] 正在生成评价: {name}")
|
||||
|
||||
try:
|
||||
@@ -176,7 +182,13 @@ def generate_report(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())
|
||||
# 1. 检查模版文件是否存在
|
||||
if not os.path.exists(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():
|
||||
logger.warning("任务正在停止中,正在中断中.....")
|
||||
return
|
||||
# 更新进度条
|
||||
if progress_callback:
|
||||
progress_callback(i + 1, total_count, "生成报告")
|
||||
# 解包数据
|
||||
(
|
||||
name,
|
||||
@@ -228,7 +237,13 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
|
||||
food,
|
||||
comments,
|
||||
) = 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}")
|
||||
|
||||
# 每次循环重新加载模版
|
||||
@@ -262,11 +277,31 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
|
||||
"birthday": (
|
||||
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 " ",
|
||||
"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 " ",
|
||||
"zodiac": (
|
||||
str(zodiac).strip()
|
||||
if str(zodiac).strip() or not str(zodiac).strip().lower()
|
||||
else " "
|
||||
),
|
||||
"friend": (
|
||||
str(friend).strip()
|
||||
if str(friend).strip() or not str(friend).strip().lower()
|
||||
else " "
|
||||
),
|
||||
"hobby": (
|
||||
str(hobby).strip()
|
||||
if str(hobby).strip() or not str(hobby).strip().lower()
|
||||
else " "
|
||||
),
|
||||
"game": (
|
||||
str(game).strip()
|
||||
if str(game).strip() or not str(game).strip().lower()
|
||||
else " "
|
||||
),
|
||||
"food": (
|
||||
str(food).strip()
|
||||
if str(food).strip() or not str(food).strip().lower()
|
||||
else " "
|
||||
),
|
||||
}
|
||||
# 获取学生个人照片路径
|
||||
me_image_path = find_image_path(student_image_folder, "me")
|
||||
@@ -334,17 +369,24 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
|
||||
# ==========================================
|
||||
# 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
|
||||
:params folder_path 需要转换的PPT文件夹
|
||||
: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())
|
||||
# 子线程初始化 COM 组件
|
||||
pythoncom.CoInitialize()
|
||||
try:
|
||||
folder_path = os.path.abspath(folder_path)
|
||||
folder_path = os.path.abspath(config.get("output_folder"))
|
||||
if not os.path.exists(folder_path):
|
||||
logger.error(f"文件夹不存在: {folder_path}")
|
||||
return
|
||||
@@ -384,7 +426,9 @@ def batch_convert_folder(folder_path, stop_event: threading.Event = None, progre
|
||||
logger.info(f"[跳过] 已存在: {filename}")
|
||||
continue
|
||||
|
||||
logger.info(f"[{files.index(filename)}/{total_count}]正在转换: {filename} ...")
|
||||
logger.info(
|
||||
f"[{files.index(filename)}/{total_count}]正在转换: {filename} ..."
|
||||
)
|
||||
|
||||
try:
|
||||
# 打开 -> 另存为 -> 关闭
|
||||
@@ -424,6 +468,13 @@ 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:
|
||||
# 1. 读取数据
|
||||
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} 位学生...")
|
||||
|
||||
# 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 并计算/更新数据
|
||||
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, "生成属相")
|
||||
|
||||
name = row.get("姓名", f"学生_{i + 1}")
|
||||
date = row['temp_date']
|
||||
date = row["temp_date"]
|
||||
|
||||
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}")
|
||||
|
||||
# 6. 清理和保存结果
|
||||
df = df.drop(columns=['temp_date'])
|
||||
df = df.drop(columns=["temp_date"])
|
||||
|
||||
save_path = excel_path
|
||||
try:
|
||||
@@ -498,13 +549,21 @@ def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
|
||||
logger.error(f"程序运行出错: {str(e)}")
|
||||
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"])
|
||||
@@ -515,31 +574,59 @@ def generate_signature(progress_callback=None) -> str:
|
||||
|
||||
logger.info(f"开始生成签名,共 {len(pptx_files)} 个 PPT 文件...")
|
||||
|
||||
img_path = config.get("signature_image") # 签名图片路径
|
||||
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)
|
||||
|
||||
# --- 关键修改点 1: 打开 PPT 对象 ---
|
||||
# 打开 PPT 对象
|
||||
prs = Presentation(pptx_path)
|
||||
|
||||
# --- 关键修改点 2: 传递 prs 对象而不是路径字符串 ---
|
||||
replace_picture(prs, 1, "signature", img_path)
|
||||
# 获取第二张幻灯片 (索引为1)
|
||||
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)
|
||||
|
||||
# 更新进度条 (如果有 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}")
|
||||
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)
|
||||
@@ -21,28 +21,28 @@ def replace_text_in_slide(prs, slide_index, placeholder, text):
|
||||
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),
|
||||
'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': []
|
||||
"alignment": paragraph.alignment,
|
||||
"space_before": getattr(paragraph, "space_before", None),
|
||||
"space_after": getattr(paragraph, "space_after", None),
|
||||
"line_spacing": getattr(paragraph, "line_spacing", None),
|
||||
"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)
|
||||
"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)
|
||||
paragraph_format["font_info"].append(run_format)
|
||||
original_paragraph_formats.append(paragraph_format)
|
||||
|
||||
# 2. 设置新文本
|
||||
@@ -51,49 +51,67 @@ def replace_text_in_slide(prs, slide_index, placeholder, 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
|
||||
if not original_paragraph_formats:
|
||||
break
|
||||
|
||||
original_para = original_paragraph_formats[orig_idx]
|
||||
|
||||
# 恢复段落属性
|
||||
for attr in ['alignment', 'space_before', 'space_after', 'line_spacing',
|
||||
'left_indent', 'right_indent', 'first_line_indent']:
|
||||
for attr in [
|
||||
"alignment",
|
||||
"space_before",
|
||||
"space_after",
|
||||
"line_spacing",
|
||||
"left_indent",
|
||||
"right_indent",
|
||||
"first_line_indent",
|
||||
]:
|
||||
if original_para[attr] is not None:
|
||||
setattr(paragraph, attr, original_para[attr])
|
||||
|
||||
# 恢复字体属性
|
||||
for j, run in enumerate(paragraph.runs):
|
||||
font_idx = j if j < len(original_para['font_info']) else 0
|
||||
if not original_para['font_info']: break
|
||||
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]
|
||||
original_font = original_para["font_info"][font_idx]
|
||||
|
||||
# 字体名称检查与回退
|
||||
if original_font['font_name']:
|
||||
if is_font_available(original_font['font_name']):
|
||||
run.font.name = original_font['font_name']
|
||||
if original_font["font_name"]:
|
||||
if is_font_available(original_font["font_name"]):
|
||||
run.font.name = original_font["font_name"]
|
||||
else:
|
||||
run.font.name = "微软雅黑"
|
||||
|
||||
# 恢复其他字体属性
|
||||
if original_font['font_size']: run.font.size = original_font['font_size']
|
||||
if original_font['bold']: run.font.bold = original_font['bold']
|
||||
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["font_size"]:
|
||||
run.font.size = original_font["font_size"]
|
||||
if original_font["bold"]:
|
||||
run.font.bold = original_font["bold"]
|
||||
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:
|
||||
run.font.space = original_font['character_space']
|
||||
run.font.space = original_font["character_space"]
|
||||
except:
|
||||
pass
|
||||
logger.error(
|
||||
f"错误: 无法设置字体间距 {original_font['character_space']}"
|
||||
)
|
||||
|
||||
if original_font['color']:
|
||||
if original_font["color"]:
|
||||
try:
|
||||
if hasattr(original_font['color'], 'rgb'):
|
||||
run.font.color.rgb = original_font['color'].rgb
|
||||
if hasattr(original_font["color"], "rgb"):
|
||||
run.font.color.rgb = original_font["color"].rgb
|
||||
except:
|
||||
pass
|
||||
logger.error(
|
||||
f"错误: 无法设置字体颜色 {original_font['color']}"
|
||||
)
|
||||
|
||||
|
||||
def replace_picture(prs, slide_index, placeholder, img_path):
|
||||
@@ -124,8 +142,17 @@ def replace_picture(prs, slide_index, placeholder, img_path):
|
||||
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
|
||||
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)
|
||||
@@ -138,3 +165,5 @@ def replace_picture(prs, slide_index, placeholder, img_path):
|
||||
|
||||
# 5. 恢复层级位置 (z-order)
|
||||
sp_tree.insert(target_index, new_shape._element)
|
||||
else:
|
||||
logger.warning(f"警告: 幻灯片 {slide_index} 中未找到占位符 {placeholder}")
|
||||
|
||||
Reference in New Issue
Block a user