diff --git a/config/config.py b/config/config.py index b0d177b..d097449 100644 --- a/config/config.py +++ b/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)}" \ No newline at end of file diff --git a/data/names.xlsx b/data/names.xlsx index 955601f..7a1c2d3 100644 Binary files a/data/names.xlsx and b/data/names.xlsx differ diff --git a/data/signature.png b/data/signature.png new file mode 100644 index 0000000..85a56f5 Binary files /dev/null and b/data/signature.png differ diff --git a/main_nicegui.py b/main_nicegui.py index 3f45879..d5aa2e3 100644 --- a/main_nicegui.py +++ b/main_nicegui.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml index f992a5d..97f77e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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]] diff --git a/templates/K4A 大班幼儿学期发展报告.pptx b/templates/(定制款)大班幼儿学期发展报告.pptx similarity index 99% rename from templates/K4A 大班幼儿学期发展报告.pptx rename to templates/(定制款)大班幼儿学期发展报告.pptx index faa72e0..4bfb4e9 100644 Binary files a/templates/K4A 大班幼儿学期发展报告.pptx and b/templates/(定制款)大班幼儿学期发展报告.pptx differ diff --git a/templates/横板/中班 幼儿学期发展报告.pptx b/templates/(横板)中班 幼儿学期发展报告.pptx similarity index 97% rename from templates/横板/中班 幼儿学期发展报告.pptx rename to templates/(横板)中班 幼儿学期发展报告.pptx index 29de2bd..7b0ca52 100644 Binary files a/templates/横板/中班 幼儿学期发展报告.pptx and b/templates/(横板)中班 幼儿学期发展报告.pptx differ diff --git a/templates/横板/大班 幼儿学期发展报告.pptx b/templates/(横板)大班 幼儿学期发展报告.pptx similarity index 97% rename from templates/横板/大班 幼儿学期发展报告.pptx rename to templates/(横板)大班 幼儿学期发展报告.pptx index 55da711..afcbfe3 100644 Binary files a/templates/横板/大班 幼儿学期发展报告.pptx and b/templates/(横板)大班 幼儿学期发展报告.pptx differ diff --git a/templates/横板/小班 幼儿学期发展报告.pptx b/templates/(横板)小班 幼儿学期发展报告.pptx similarity index 97% rename from templates/横板/小班 幼儿学期发展报告.pptx rename to templates/(横板)小班 幼儿学期发展报告.pptx index f3ce794..92cc379 100644 Binary files a/templates/横板/小班 幼儿学期发展报告.pptx and b/templates/(横板)小班 幼儿学期发展报告.pptx differ diff --git a/templates/竖版/中班 幼儿学期发展报告.pptx b/templates/(竖版)中班 幼儿学期发展报告.pptx similarity index 97% rename from templates/竖版/中班 幼儿学期发展报告.pptx rename to templates/(竖版)中班 幼儿学期发展报告.pptx index 1002d9c..78b773d 100644 Binary files a/templates/竖版/中班 幼儿学期发展报告.pptx and b/templates/(竖版)中班 幼儿学期发展报告.pptx differ diff --git a/templates/竖版/大班 幼儿学期发展报告.pptx b/templates/(竖版)大班 幼儿学期发展报告.pptx similarity index 99% rename from templates/竖版/大班 幼儿学期发展报告.pptx rename to templates/(竖版)大班 幼儿学期发展报告.pptx index e7d9ad2..444264f 100644 Binary files a/templates/竖版/大班 幼儿学期发展报告.pptx and b/templates/(竖版)大班 幼儿学期发展报告.pptx differ diff --git a/templates/竖版/小班 幼儿学期发展报告.pptx b/templates/(竖版)小班 幼儿学期发展报告.pptx similarity index 97% rename from templates/竖版/小班 幼儿学期发展报告.pptx rename to templates/(竖版)小班 幼儿学期发展报告.pptx index 9b5853d..2ef6f09 100644 Binary files a/templates/竖版/小班 幼儿学期发展报告.pptx and b/templates/(竖版)小班 幼儿学期发展报告.pptx differ diff --git a/ui/views/config_page.py b/ui/views/config_page.py new file mode 100644 index 0000000..fc8ead8 --- /dev/null +++ b/ui/views/config_page.py @@ -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('') + + # 样式修正:添加全屏且不滚动条的 CSS + ui.add_head_html(''' + + ''') + + 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') diff --git a/ui/views/home_page.py b/ui/views/home_page.py index e33abee..1e070b6 100644 --- a/ui/views/home_page.py +++ b/ui/views/home_page.py @@ -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'): diff --git a/utils/file_utils.py b/utils/file_utils.py index 01cd8ab..50a308a 100644 --- a/utils/file_utils.py +++ b/utils/file_utils.py @@ -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()) diff --git a/utils/generate_utils.py b/utils/generate_utils.py index 715f8b8..a0be9c1 100644 --- a/utils/generate_utils.py +++ b/utils/generate_utils.py @@ -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) \ No newline at end of file diff --git a/utils/pptx_utils.py b/utils/pptx_utils.py index 9d1b3cf..82449af 100644 --- a/utils/pptx_utils.py +++ b/utils/pptx_utils.py @@ -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) \ No newline at end of file diff --git a/utils/template_utils.py b/utils/template_utils.py new file mode 100644 index 0000000..1bc71c6 --- /dev/null +++ b/utils/template_utils.py @@ -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) \ No newline at end of file diff --git a/uv.lock b/uv.lock index edc4ee3..11b41c5 100644 --- a/uv.lock +++ b/uv.lock @@ -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"