fix:实现配置功能,实现园长一键签名功能
This commit is contained in:
157
config/config.py
157
config/config.py
@@ -1,130 +1,121 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 尝试导入 toml 解析库
|
||||
# 1. 处理读取库
|
||||
try:
|
||||
import tomllib as toml # Python 3.11+
|
||||
import tomllib as toml_read # Python 3.11+
|
||||
except ImportError:
|
||||
try:
|
||||
import tomli as toml # pip install tomli
|
||||
import tomli as toml_read
|
||||
except ImportError:
|
||||
print("错误: 缺少 TOML 解析库。请运行: pip install tomli")
|
||||
print("错误: 缺少 TOML 读取库。请运行: pip install tomli")
|
||||
sys.exit(1)
|
||||
|
||||
# 2. 处理写入库 (必须安装 pip install tomli-w)
|
||||
try:
|
||||
import tomli_w as toml_write
|
||||
except ImportError:
|
||||
# 如果没安装,提供一个 fallback 提示
|
||||
toml_write = None
|
||||
|
||||
def get_base_dir():
|
||||
"""
|
||||
获取程序运行的基准目录 (即 EXE 所在的目录 或 开发环境的项目根目录)
|
||||
用于确定 output_folder 等需要写入的路径。
|
||||
"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包环境: EXE 所在目录
|
||||
return os.path.dirname(sys.executable)
|
||||
else:
|
||||
# 开发环境: 项目根目录 (假设此脚本在 utils/ 文件夹中,需要向上两级)
|
||||
# 假设当前文件在项目根目录或根目录下的某个文件夹中
|
||||
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def get_resource_path(relative_path):
|
||||
"""
|
||||
智能路径获取:
|
||||
1. 优先检查 EXE 旁边是否有该文件 (外部资源)。
|
||||
2. 如果没有,则使用 EXE 内部打包的资源 (内部资源)。
|
||||
"""
|
||||
# 1. 获取外部基准路径
|
||||
base_path = get_base_dir()
|
||||
|
||||
# 拼接外部路径
|
||||
external_path = os.path.join(base_path, relative_path)
|
||||
|
||||
# 如果外部文件存在,直接返回 (优先使用用户修改过的文件)
|
||||
if os.path.exists(external_path):
|
||||
return external_path
|
||||
|
||||
# 2. 如果外部不存在,且处于打包环境,则回退到内部临时目录 (sys._MEIPASS)
|
||||
if getattr(sys, 'frozen', False):
|
||||
# sys._MEIPASS 是 PyInstaller 解压临时文件的目录
|
||||
internal_path = os.path.join(sys._MEIPASS, relative_path)
|
||||
|
||||
if os.path.exists(internal_path):
|
||||
return internal_path
|
||||
|
||||
# 3. 默认返回外部路径
|
||||
# (如果都没找到,让报错信息指向外部路径,提示用户文件缺失)
|
||||
return external_path
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 1. 配置加载 (Config Loader)
|
||||
# ==========================================
|
||||
def load_config(config_filename="config.toml"):
|
||||
"""读取 TOML 配置文件"""
|
||||
|
||||
# 1. 智能获取配置文件路径
|
||||
# (优先找 EXE 旁边的 config.toml,找不到则用打包在里面的)
|
||||
config_path = get_resource_path(config_filename)
|
||||
|
||||
if not os.path.exists(config_path):
|
||||
print(f"错误: 找不到配置文件 {config_filename}")
|
||||
print(f"尝试寻找的路径是: {config_path}")
|
||||
# 如果是打包环境,提示用户可能需要把 config.toml 复制出来
|
||||
if getattr(sys, 'frozen', False):
|
||||
print("提示: 请确保 config.toml 位于程序同级目录下,或已正确打包。")
|
||||
sys.exit(1)
|
||||
# 如果彻底找不到,返回一个最小化的默认值,防止程序奔溃
|
||||
return { "source_file": "", "ai": {"api_key": ""}, "teachers": [] }
|
||||
|
||||
try:
|
||||
with open(config_path, "rb") as f:
|
||||
data = toml.load(f)
|
||||
data = toml_read.load(f)
|
||||
|
||||
# 获取基准目录(用于 output_folder)
|
||||
base_dir = get_base_dir()
|
||||
|
||||
# 将 TOML 的层级结构映射回扁平结构
|
||||
# ⚠️ 注意:
|
||||
# - 读取类文件 (模板, Excel, 图片, 字体) 使用 get_resource_path (支持内外回退)
|
||||
# - 写入类文件夹 (output_folder) 使用 os.path.join(base_dir, ...) (必须在外部)
|
||||
# 使用 .get() 安全获取,防止 KeyError: 'paths'
|
||||
paths = data.get("paths", {})
|
||||
class_info = data.get("class_info", {})
|
||||
defaults = data.get("defaults", {})
|
||||
|
||||
config = {
|
||||
"root_path": base_dir,
|
||||
|
||||
# --- 资源文件 (使用智能路径) ---
|
||||
|
||||
# 假设 config.toml 里写的是 "report_template.pptx",文件在 templates 文件夹下
|
||||
"source_file": get_resource_path(
|
||||
os.path.join("templates", data["paths"]["source_file"])
|
||||
),
|
||||
|
||||
# 假设 config.toml 里写的是 "names.xlsx",文件在 data 文件夹下
|
||||
# 如果 config.toml 里写的是 "data/names.xlsx",则不需要 os.path.join("data", ...)
|
||||
"excel_file": get_resource_path(
|
||||
os.path.join("data", data["paths"]["excel_file"])
|
||||
),
|
||||
|
||||
"image_folder": get_resource_path(
|
||||
os.path.join("data", data["paths"]["image_folder"])
|
||||
),
|
||||
|
||||
"fonts_dir": get_resource_path(
|
||||
os.path.join(data["paths"]["fonts_dir"])
|
||||
),
|
||||
|
||||
# --- 输出文件夹 (必须强制在外部,不能指向临时目录) ---
|
||||
"output_folder": os.path.join(base_dir, data["paths"]["output_folder"]),
|
||||
|
||||
# --- 其他配置 ---
|
||||
"class_name": data["class_info"]["class_name"],
|
||||
"teachers": data["class_info"]["teachers"],
|
||||
"default_comment": data["defaults"].get("default_comment", "暂无评语"),
|
||||
"age_group": data["defaults"].get("age_group", "大班上学期"),
|
||||
"ai": data["ai"],
|
||||
# 扁平化映射
|
||||
"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", ""))),
|
||||
|
||||
"class_name": class_info.get("class_name", "未命名班级"),
|
||||
"teachers": class_info.get("teachers", []),
|
||||
"default_comment": defaults.get("default_comment", "暂无评语"),
|
||||
"age_group": defaults.get("age_group", "大班上学期"),
|
||||
"ai": data.get("ai", {"api_key": "", "api_url": "", "model": ""}),
|
||||
}
|
||||
return config
|
||||
|
||||
except KeyError as e:
|
||||
print(f"配置文件格式错误,缺少键值: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"读取配置文件出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
print(f"解析配置文件失败: {e}")
|
||||
return {}
|
||||
|
||||
# ==========================================
|
||||
# 2. 配置保存 (Config Saver)
|
||||
# ==========================================
|
||||
def save_config(config_data, config_filename="config.toml"):
|
||||
if not toml_write:
|
||||
return False, "未安装 tomli-w 库,无法保存。请运行 pip install tomli-w"
|
||||
|
||||
base_path = get_base_dir()
|
||||
save_path = os.path.join(base_path, config_filename)
|
||||
|
||||
try:
|
||||
# 将扁平化的数据重新打包成嵌套结构,以适配 load_config 的读取逻辑
|
||||
new_data = {
|
||||
"paths": {
|
||||
"source_file": os.path.basename(config_data.get("source_file", "")),
|
||||
"output_folder": os.path.basename(config_data.get("output_folder", "output")),
|
||||
"excel_file": os.path.basename(config_data.get("excel_file", "")),
|
||||
"image_folder": os.path.basename(config_data.get("image_folder", "")),
|
||||
"fonts_dir": os.path.basename(config_data.get("fonts_dir", "fonts")),
|
||||
"signature_image": get_resource_path(os.path.join("data", paths.get("signature_image", ""))),
|
||||
},
|
||||
"class_info": {
|
||||
"class_name": config_data.get("class_name", ""),
|
||||
"teachers": config_data.get("teachers", []),
|
||||
},
|
||||
"defaults": {
|
||||
"default_comment": config_data.get("default_comment", ""),
|
||||
"age_group": config_data.get("age_group", ""),
|
||||
},
|
||||
"ai": config_data.get("ai", {})
|
||||
}
|
||||
|
||||
# 写入文件
|
||||
with open(save_path, "wb") as f:
|
||||
f.write(toml_write.dumps(new_data).encode("utf-8"))
|
||||
|
||||
return True, f"成功保存到: {save_path}"
|
||||
|
||||
except Exception as e:
|
||||
return False, f"写入失败: {str(e)}"
|
||||
BIN
data/names.xlsx
BIN
data/names.xlsx
Binary file not shown.
BIN
data/signature.png
Normal file
BIN
data/signature.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
@@ -5,21 +5,21 @@ from nicegui import ui, app, run, native
|
||||
from loguru import logger
|
||||
|
||||
from screeninfo import get_monitors
|
||||
|
||||
# 导入我们的模块
|
||||
import traceback
|
||||
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
|
||||
|
||||
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):
|
||||
"""
|
||||
@@ -74,12 +74,14 @@ def calculate_window_size():
|
||||
static_dir = get_path(os.path.join("ui", "assets"))
|
||||
app.add_static_files('/assets', static_dir)
|
||||
|
||||
|
||||
# 3. 页面路由
|
||||
@ui.page('/')
|
||||
def index():
|
||||
def index_page():
|
||||
create_page()
|
||||
|
||||
@ui.page('/config')
|
||||
def config_page():
|
||||
create_config_page()
|
||||
|
||||
# 4. 启动时钩子
|
||||
async def startup_check():
|
||||
@@ -90,6 +92,7 @@ async def startup_check():
|
||||
logger.success("资源初始化完成")
|
||||
except Exception as e:
|
||||
logger.error(f"初始化失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
|
||||
app.on_startup(startup_check)
|
||||
|
||||
@@ -21,6 +21,7 @@ dependencies = [
|
||||
"rich>=14.2.0",
|
||||
"screeninfo>=0.8.1",
|
||||
"tomli>=2.3.0",
|
||||
"tomli-w>=1.2.0",
|
||||
]
|
||||
|
||||
[[tool.uv.index]]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
92
ui/views/config_page.py
Normal file
92
ui/views/config_page.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from nicegui import ui
|
||||
import os
|
||||
from utils.template_utils import get_template_files
|
||||
# 修改点 1:统一导入,避免与变量名 config 冲突
|
||||
from config.config import load_config, save_config
|
||||
|
||||
def create_config_page():
|
||||
# 修改点 2:将加载逻辑放入页面生成函数内,确保每次刷新页面获取最新值
|
||||
conf_data = load_config("config.toml")
|
||||
template_options = get_template_files()
|
||||
current_filename = os.path.basename(conf_data.get('source_file', ''))
|
||||
|
||||
if current_filename and current_filename not in template_options:
|
||||
template_options.append(current_filename)
|
||||
|
||||
ui.add_head_html('<link href="/assets/style.css" rel="stylesheet" />')
|
||||
|
||||
# 样式修正:添加全屏且不滚动条的 CSS
|
||||
ui.add_head_html('''
|
||||
<style>
|
||||
body { overflow: hidden; }
|
||||
.main-card { height: calc(100vh - 100px); display: flex; flex-direction: column; }
|
||||
.q-tab-panels { flex-grow: 1; overflow-y: auto !important; }
|
||||
</style>
|
||||
''')
|
||||
|
||||
with ui.header().classes('app-header items-center justify-between shadow-md'):
|
||||
# 左侧:图标和标题
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.button(icon='arrow_back', on_click=lambda: ui.navigate.to('/')).props('flat round color=white')
|
||||
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')
|
||||
|
||||
# 修改点 3:使用 flex 布局撑满
|
||||
with ui.card().classes('w-full max-w-5xl mx-auto shadow-lg main-card p-0'):
|
||||
with ui.tabs().classes('w-full') as tabs:
|
||||
tab_path = ui.tab('路径设置', icon='folder')
|
||||
tab_class = ui.tab('班级与教师', icon='school')
|
||||
tab_ai = ui.tab('AI 接口配置', icon='psychology')
|
||||
|
||||
with ui.tab_panels(tabs, value=tab_path).classes('w-full flex-grow bg-transparent'):
|
||||
# --- 路径设置 ---
|
||||
with ui.tab_panel(tab_path).classes('w-full p-0'):
|
||||
with ui.column().classes('w-full p-4 gap-4'):
|
||||
source_file = ui.select(options=template_options, label='PPT 模板', value=current_filename).props('outlined fill-input').classes('w-full')
|
||||
excel_file = ui.input('Excel 文件', value=os.path.basename(conf_data.get('excel_file', ''))).props('outlined').classes('w-full')
|
||||
image_folder = ui.input('图片目录', value=os.path.basename(conf_data.get('image_folder', ''))).props('outlined').classes('w-full')
|
||||
output_folder = ui.input('输出目录', value=os.path.basename(conf_data.get('output_folder', 'output'))).props('outlined').classes('w-full')
|
||||
|
||||
# --- 班级信息 ---
|
||||
with ui.tab_panel(tab_class).classes('w-full p-0'):
|
||||
with ui.column().classes('w-full p-4 gap-4'):
|
||||
class_name = ui.input('班级名称', value=conf_data.get('class_name', '')).props('outlined').classes('w-full')
|
||||
age_group = ui.select(
|
||||
options=['小班上学期', '小班下学期', '中班上学期', '中班下学期', '大班上学期', '大班下学期'],
|
||||
label='年龄段', value=conf_data.get('age_group', '中班上学期')
|
||||
).props('outlined').classes('w-full')
|
||||
teachers_text = ui.textarea('教师名单', value='\n'.join(conf_data.get('teachers', []))).props('outlined').classes('w-full h-40')
|
||||
|
||||
# --- AI 配置 ---
|
||||
with ui.tab_panel(tab_ai).classes('w-full p-0'):
|
||||
with ui.column().classes('w-full p-4 gap-4'):
|
||||
ai_key = ui.input('API Key', value=conf_data['ai'].get('api_key', '')).props('outlined password').classes('w-full')
|
||||
ai_url = ui.input('API URL', value=conf_data['ai'].get('api_url', '')).props('outlined').classes('w-full')
|
||||
ai_model = ui.input('Model Name', value=conf_data['ai'].get('model', '')).props('outlined').classes('w-full')
|
||||
ai_prompt = ui.textarea('System Prompt', value=conf_data['ai'].get('prompt', '')).props('outlined').classes('w-full h-full')
|
||||
# 底部固定按钮
|
||||
with ui.row().classes('w-full p-4'):
|
||||
async def handle_save():
|
||||
new_data = {
|
||||
"source_file": source_file.value,
|
||||
"excel_file": excel_file.value,
|
||||
"image_folder": image_folder.value,
|
||||
"output_folder": output_folder.value,
|
||||
"class_name": class_name.value,
|
||||
"age_group": age_group.value,
|
||||
"teachers": [t.strip() for t in teachers_text.value.split('\n') if t.strip()],
|
||||
"ai": {
|
||||
"api_key": ai_key.value,
|
||||
"api_url": ai_url.value,
|
||||
"model": ai_model.value,
|
||||
"prompt": ai_prompt.value
|
||||
}
|
||||
}
|
||||
# 修改点 4:直接调用导入的 save_config 函数名
|
||||
success, message = save_config(new_data)
|
||||
ui.notify(message, type='positive' if success else 'negative')
|
||||
|
||||
ui.button('保存配置', on_click=handle_save).classes('w-full py-4').props('outline color=primary')
|
||||
@@ -1,4 +1,4 @@
|
||||
from nicegui import ui, app
|
||||
from nicegui import ui
|
||||
from config.config import load_config
|
||||
from ui.core.state import app_state
|
||||
from ui.core.task_runner import run_task, select_folder
|
||||
@@ -6,20 +6,24 @@ 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
|
||||
batch_convert_folder, generate_report, generate_zodiac, generate_signature
|
||||
)
|
||||
from utils.file_utils import export_templates_folder, initialize_project, export_data
|
||||
|
||||
config = load_config("config.toml")
|
||||
|
||||
|
||||
def create_header():
|
||||
with ui.header().classes('app-header items-center justify-between shadow-md'):
|
||||
# 左侧:图标和标题
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.image('/assets/icon.ico').classes('w-8 h-8').props('fit=contain')
|
||||
ui.label('尚城幼儿园成长报告助手').classes('text-xl font-bold')
|
||||
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('系统配置')
|
||||
|
||||
def create_page():
|
||||
# 1. 引入外部 CSS
|
||||
@@ -49,18 +53,15 @@ def create_page():
|
||||
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)
|
||||
|
||||
# === 下方双栏布局 ===
|
||||
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')
|
||||
@@ -70,8 +71,7 @@ def create_page():
|
||||
if path: await run_task(func, path)
|
||||
|
||||
ui.button('📦 导出模板', on_click=lambda: do_export(export_templates_folder)).props(f'outline')
|
||||
ui.button('📤 导出备份', on_click=lambda: do_export(export_data)).props(f'outline')
|
||||
|
||||
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')
|
||||
@@ -87,7 +87,6 @@ def create_page():
|
||||
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'):
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import time
|
||||
from loguru import logger
|
||||
import zipfile
|
||||
|
||||
import traceback
|
||||
|
||||
def export_templates_folder(output_folder, stop_event, progress_callback=None):
|
||||
"""
|
||||
@@ -217,3 +217,27 @@ def check_file_exists(file_path):
|
||||
判断文件是否存在
|
||||
"""
|
||||
return file_path and isinstance(file_path, str) and os.path.exists(file_path)
|
||||
|
||||
def get_output_pptx_files(output_dir="output"):
|
||||
"""
|
||||
获取 output 文件夹下所有的 pptx 文件
|
||||
:param output_dir: output 文件夹路径
|
||||
"""
|
||||
try:
|
||||
folder_path = os.path.abspath(output_dir)
|
||||
if not os.path.exists(folder_path):
|
||||
logger.error(f"文件夹不存在: {folder_path}")
|
||||
return
|
||||
# 获取所有 ppt/pptx 文件
|
||||
files = [
|
||||
f for f in os.listdir(folder_path) if f.lower().endswith((".ppt", ".pptx"))
|
||||
]
|
||||
if not files:
|
||||
logger.warning("没有找到 PPT 文件")
|
||||
return
|
||||
total_count = len(files)
|
||||
logger.info(f"发现 {total_count} 个文件,准备开始转换...")
|
||||
return files
|
||||
except Exception as e:
|
||||
logger.error(f"发生未知错误: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
@@ -12,7 +12,7 @@ import traceback
|
||||
import comtypes.client
|
||||
from config.config import load_config
|
||||
from utils.agent_utils import generate_comment
|
||||
from utils.file_utils import check_file_exists
|
||||
from utils.file_utils import check_file_exists, get_output_pptx_files
|
||||
from utils.image_utils import find_image_path
|
||||
from utils.zodiac_utils import calculate_zodiac
|
||||
from utils.growt_utils import (
|
||||
@@ -22,6 +22,7 @@ from utils.growt_utils import (
|
||||
replace_four_page,
|
||||
replace_five_page,
|
||||
)
|
||||
from utils.pptx_utils import replace_picture
|
||||
|
||||
# 如果你之前没有全局定义 console,这里定义一个
|
||||
console = Console()
|
||||
@@ -331,7 +332,7 @@ def generate_report(stop_event: threading.Event = None, progress_callback=None):
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 5. 转换格式(根据names.xlsx文件生成PPT转PDF)
|
||||
# 4. 转换格式(根据names.xlsx文件生成PPT转PDF)
|
||||
# ==========================================
|
||||
def batch_convert_folder(folder_path, stop_event: threading.Event = None, progress_callback=None):
|
||||
"""
|
||||
@@ -496,3 +497,49 @@ def generate_zodiac(stop_event: threading.Event = None, progress_callback=None):
|
||||
except Exception as e:
|
||||
logger.error(f"程序运行出错: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# ==========================================
|
||||
# 6. 一键生成园长签名(根据输出文件夹生成签名)
|
||||
# ==========================================
|
||||
def generate_signature(progress_callback=None) -> str:
|
||||
"""
|
||||
生成园长签名
|
||||
"""
|
||||
try:
|
||||
# 获取所有的PPT (此时返回的是文件名或路径的列表)
|
||||
pptx_files = get_output_pptx_files(config["output_folder"])
|
||||
|
||||
if not pptx_files:
|
||||
logger.warning("没有找到 PPT 文件")
|
||||
return "未找到文件"
|
||||
|
||||
logger.info(f"开始生成签名,共 {len(pptx_files)} 个 PPT 文件...")
|
||||
|
||||
img_path = config.get("signature_image") # 签名图片路径
|
||||
if not img_path or not os.path.exists(img_path):
|
||||
logger.error(f"签名图片不存在: {img_path}")
|
||||
logger.warning(f"⚠️ 警告: 缺少签名照片('signature')")
|
||||
return
|
||||
logger.info(f"签名图片存在: {img_path}")
|
||||
for i, filename in enumerate(pptx_files):
|
||||
# 获取完整绝对路径
|
||||
pptx_path = os.path.join(config["output_folder"], filename)
|
||||
|
||||
# --- 关键修改点 1: 打开 PPT 对象 ---
|
||||
prs = Presentation(pptx_path)
|
||||
|
||||
# --- 关键修改点 2: 传递 prs 对象而不是路径字符串 ---
|
||||
replace_picture(prs, 1, "signature", img_path)
|
||||
|
||||
# --- 关键修改点 3: 保存修改后的 PPT ---
|
||||
prs.save(pptx_path)
|
||||
|
||||
# 更新进度条 (如果有 callback)
|
||||
if progress_callback:
|
||||
progress_callback(i + 1, len(pptx_files),f"[{i + 1}/{len(pptx_files)}] 生成签名完成: {filename}")
|
||||
logger.success(f"[{i + 1}/{len(pptx_files)}] 生成签名完成: {filename}")
|
||||
if progress_callback:
|
||||
progress_callback(len(pptx_files), len(pptx_files), "签名生成完成")
|
||||
except Exception as e:
|
||||
logger.error(f"generate_signature 发生未知错误: {e}")
|
||||
return str(e)
|
||||
@@ -97,7 +97,15 @@ def replace_text_in_slide(prs, slide_index, placeholder, text):
|
||||
|
||||
|
||||
def replace_picture(prs, slide_index, placeholder, img_path):
|
||||
"""在指定幻灯片中替换指定占位符的图片(包含自动旋转修复)"""
|
||||
"""
|
||||
在指定幻灯片中替换指定占位符的图片(包含自动旋转修复)
|
||||
|
||||
参数:
|
||||
prs: Presentation 对象
|
||||
slide_index: 幻灯片索引 (从0开始)
|
||||
placeholder: 占位符名称 (例如 "signature")
|
||||
img_path: 图片路径
|
||||
"""
|
||||
if not os.path.exists(img_path):
|
||||
logger.warning(f"警告: 图片路径不存在 {img_path}")
|
||||
return
|
||||
@@ -129,4 +137,4 @@ def replace_picture(prs, slide_index, placeholder, img_path):
|
||||
new_shape = slide.shapes.add_picture(img_stream, left, top, width, height)
|
||||
|
||||
# 5. 恢复层级位置 (z-order)
|
||||
sp_tree.insert(target_index, new_shape._element)
|
||||
sp_tree.insert(target_index, new_shape._element)
|
||||
24
utils/template_utils.py
Normal file
24
utils/template_utils.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import os
|
||||
from config.config import get_base_dir
|
||||
|
||||
def get_template_files():
|
||||
"""
|
||||
遍历 templates 目录,返回所有 PPTX 文件的文件名列表
|
||||
"""
|
||||
# 获取 templates 文件夹的绝对路径
|
||||
# 这里的 get_base_dir() 是你之前定义的函数
|
||||
base_dir = get_base_dir()
|
||||
templates_dir = os.path.join(base_dir, 'templates')
|
||||
|
||||
# 检查目录是否存在,不存在则返回空列表
|
||||
if not os.path.exists(templates_dir):
|
||||
return []
|
||||
|
||||
# 遍历目录
|
||||
files = []
|
||||
for filename in os.listdir(templates_dir):
|
||||
# 过滤掉隐藏文件,并只保留 .pptx 结尾的文件
|
||||
if not filename.startswith('.') and filename.endswith('.pptx'):
|
||||
files.append(filename)
|
||||
|
||||
return sorted(files)
|
||||
11
uv.lock
generated
11
uv.lock
generated
@@ -426,6 +426,7 @@ dependencies = [
|
||||
{ name = "rich" },
|
||||
{ name = "screeninfo" },
|
||||
{ name = "tomli" },
|
||||
{ name = "tomli-w" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -446,6 +447,7 @@ requires-dist = [
|
||||
{ name = "rich", specifier = ">=14.2.0" },
|
||||
{ name = "screeninfo", specifier = ">=0.8.1" },
|
||||
{ name = "tomli", specifier = ">=2.3.0" },
|
||||
{ name = "tomli-w", specifier = ">=1.2.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2029,6 +2031,15 @@ wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli-w"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
|
||||
Reference in New Issue
Block a user