fix:实现配置功能,实现园长一键签名功能
This commit is contained in:
155
config/config.py
155
config/config.py
@@ -1,130 +1,121 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# 尝试导入 toml 解析库
|
# 1. 处理读取库
|
||||||
try:
|
try:
|
||||||
import tomllib as toml # Python 3.11+
|
import tomllib as toml_read # Python 3.11+
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
import tomli as toml # pip install tomli
|
import tomli as toml_read
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("错误: 缺少 TOML 解析库。请运行: pip install tomli")
|
print("错误: 缺少 TOML 读取库。请运行: pip install tomli")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 2. 处理写入库 (必须安装 pip install tomli-w)
|
||||||
|
try:
|
||||||
|
import tomli_w as toml_write
|
||||||
|
except ImportError:
|
||||||
|
# 如果没安装,提供一个 fallback 提示
|
||||||
|
toml_write = None
|
||||||
|
|
||||||
def get_base_dir():
|
def get_base_dir():
|
||||||
"""
|
|
||||||
获取程序运行的基准目录 (即 EXE 所在的目录 或 开发环境的项目根目录)
|
|
||||||
用于确定 output_folder 等需要写入的路径。
|
|
||||||
"""
|
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
# 打包环境: EXE 所在目录
|
|
||||||
return os.path.dirname(sys.executable)
|
return os.path.dirname(sys.executable)
|
||||||
else:
|
else:
|
||||||
# 开发环境: 项目根目录 (假设此脚本在 utils/ 文件夹中,需要向上两级)
|
# 假设当前文件在项目根目录或根目录下的某个文件夹中
|
||||||
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
|
||||||
def get_resource_path(relative_path):
|
def get_resource_path(relative_path):
|
||||||
"""
|
|
||||||
智能路径获取:
|
|
||||||
1. 优先检查 EXE 旁边是否有该文件 (外部资源)。
|
|
||||||
2. 如果没有,则使用 EXE 内部打包的资源 (内部资源)。
|
|
||||||
"""
|
|
||||||
# 1. 获取外部基准路径
|
|
||||||
base_path = get_base_dir()
|
base_path = get_base_dir()
|
||||||
|
|
||||||
# 拼接外部路径
|
|
||||||
external_path = os.path.join(base_path, relative_path)
|
external_path = os.path.join(base_path, relative_path)
|
||||||
|
|
||||||
# 如果外部文件存在,直接返回 (优先使用用户修改过的文件)
|
|
||||||
if os.path.exists(external_path):
|
if os.path.exists(external_path):
|
||||||
return external_path
|
return external_path
|
||||||
|
|
||||||
# 2. 如果外部不存在,且处于打包环境,则回退到内部临时目录 (sys._MEIPASS)
|
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
# sys._MEIPASS 是 PyInstaller 解压临时文件的目录
|
|
||||||
internal_path = os.path.join(sys._MEIPASS, relative_path)
|
internal_path = os.path.join(sys._MEIPASS, relative_path)
|
||||||
|
|
||||||
if os.path.exists(internal_path):
|
if os.path.exists(internal_path):
|
||||||
return internal_path
|
return internal_path
|
||||||
|
|
||||||
# 3. 默认返回外部路径
|
|
||||||
# (如果都没找到,让报错信息指向外部路径,提示用户文件缺失)
|
|
||||||
return external_path
|
return external_path
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 1. 配置加载 (Config Loader)
|
# 1. 配置加载 (Config Loader)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
def load_config(config_filename="config.toml"):
|
def load_config(config_filename="config.toml"):
|
||||||
"""读取 TOML 配置文件"""
|
|
||||||
|
|
||||||
# 1. 智能获取配置文件路径
|
|
||||||
# (优先找 EXE 旁边的 config.toml,找不到则用打包在里面的)
|
|
||||||
config_path = get_resource_path(config_filename)
|
config_path = get_resource_path(config_filename)
|
||||||
|
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
print(f"错误: 找不到配置文件 {config_filename}")
|
# 如果彻底找不到,返回一个最小化的默认值,防止程序奔溃
|
||||||
print(f"尝试寻找的路径是: {config_path}")
|
return { "source_file": "", "ai": {"api_key": ""}, "teachers": [] }
|
||||||
# 如果是打包环境,提示用户可能需要把 config.toml 复制出来
|
|
||||||
if getattr(sys, 'frozen', False):
|
|
||||||
print("提示: 请确保 config.toml 位于程序同级目录下,或已正确打包。")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(config_path, "rb") as f:
|
with open(config_path, "rb") as f:
|
||||||
data = toml.load(f)
|
data = toml_read.load(f)
|
||||||
|
|
||||||
# 获取基准目录(用于 output_folder)
|
|
||||||
base_dir = get_base_dir()
|
base_dir = get_base_dir()
|
||||||
|
|
||||||
# 将 TOML 的层级结构映射回扁平结构
|
# 使用 .get() 安全获取,防止 KeyError: 'paths'
|
||||||
# ⚠️ 注意:
|
paths = data.get("paths", {})
|
||||||
# - 读取类文件 (模板, Excel, 图片, 字体) 使用 get_resource_path (支持内外回退)
|
class_info = data.get("class_info", {})
|
||||||
# - 写入类文件夹 (output_folder) 使用 os.path.join(base_dir, ...) (必须在外部)
|
defaults = data.get("defaults", {})
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"root_path": base_dir,
|
"root_path": base_dir,
|
||||||
|
# 扁平化映射
|
||||||
|
"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", []),
|
||||||
# 假设 config.toml 里写的是 "report_template.pptx",文件在 templates 文件夹下
|
"default_comment": defaults.get("default_comment", "暂无评语"),
|
||||||
"source_file": get_resource_path(
|
"age_group": defaults.get("age_group", "大班上学期"),
|
||||||
os.path.join("templates", data["paths"]["source_file"])
|
"ai": data.get("ai", {"api_key": "", "api_url": "", "model": ""}),
|
||||||
),
|
|
||||||
|
|
||||||
# 假设 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"],
|
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
|
|
||||||
except KeyError as e:
|
|
||||||
print(f"配置文件格式错误,缺少键值: {e}")
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"读取配置文件出错: {e}")
|
print(f"解析配置文件失败: {e}")
|
||||||
import traceback
|
return {}
|
||||||
traceback.print_exc()
|
|
||||||
sys.exit(1)
|
# ==========================================
|
||||||
|
# 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 loguru import logger
|
||||||
|
|
||||||
from screeninfo import get_monitors
|
from screeninfo import get_monitors
|
||||||
|
import traceback
|
||||||
# 导入我们的模块
|
|
||||||
from config.config import load_config
|
from config.config import load_config
|
||||||
|
# 导入我们的模块
|
||||||
from ui.core.logger import setup_logger
|
from ui.core.logger import setup_logger
|
||||||
from utils.font_utils import install_fonts_from_directory
|
from utils.font_utils import install_fonts_from_directory
|
||||||
from ui.views.home_page import create_page
|
from ui.views.home_page import create_page
|
||||||
|
from ui.views.config_page import create_config_page
|
||||||
|
|
||||||
sys.stdout.reconfigure(encoding='utf-8')
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
sys.stderr.reconfigure(encoding='utf-8')
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
# 1. 初始化配置
|
# 1. 初始化配置
|
||||||
config = load_config("config.toml")
|
config = load_config("config.toml")
|
||||||
|
|
||||||
setup_logger()
|
setup_logger()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# === 关键修改:定义一个获取路径的通用函数 ===
|
# === 关键修改:定义一个获取路径的通用函数 ===
|
||||||
def get_path(relative_path):
|
def get_path(relative_path):
|
||||||
"""
|
"""
|
||||||
@@ -74,12 +74,14 @@ def calculate_window_size():
|
|||||||
static_dir = get_path(os.path.join("ui", "assets"))
|
static_dir = get_path(os.path.join("ui", "assets"))
|
||||||
app.add_static_files('/assets', static_dir)
|
app.add_static_files('/assets', static_dir)
|
||||||
|
|
||||||
|
|
||||||
# 3. 页面路由
|
# 3. 页面路由
|
||||||
@ui.page('/')
|
@ui.page('/')
|
||||||
def index():
|
def index_page():
|
||||||
create_page()
|
create_page()
|
||||||
|
|
||||||
|
@ui.page('/config')
|
||||||
|
def config_page():
|
||||||
|
create_config_page()
|
||||||
|
|
||||||
# 4. 启动时钩子
|
# 4. 启动时钩子
|
||||||
async def startup_check():
|
async def startup_check():
|
||||||
@@ -90,6 +92,7 @@ async def startup_check():
|
|||||||
logger.success("资源初始化完成")
|
logger.success("资源初始化完成")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"初始化失败: {e}")
|
logger.error(f"初始化失败: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
app.on_startup(startup_check)
|
app.on_startup(startup_check)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ dependencies = [
|
|||||||
"rich>=14.2.0",
|
"rich>=14.2.0",
|
||||||
"screeninfo>=0.8.1",
|
"screeninfo>=0.8.1",
|
||||||
"tomli>=2.3.0",
|
"tomli>=2.3.0",
|
||||||
|
"tomli-w>=1.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[tool.uv.index]]
|
[[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 config.config import load_config
|
||||||
from ui.core.state import app_state
|
from ui.core.state import app_state
|
||||||
from ui.core.task_runner import run_task, select_folder
|
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 (
|
from utils.generate_utils import (
|
||||||
generate_template, generate_comment_all,
|
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
|
from utils.file_utils import export_templates_folder, initialize_project, export_data
|
||||||
|
|
||||||
config = load_config("config.toml")
|
config = load_config("config.toml")
|
||||||
|
|
||||||
|
|
||||||
def create_header():
|
def create_header():
|
||||||
with ui.header().classes('app-header items-center justify-between shadow-md'):
|
with ui.header().classes('app-header items-center justify-between shadow-md'):
|
||||||
|
# 左侧:图标和标题
|
||||||
with ui.row().classes('items-center gap-2'):
|
with ui.row().classes('items-center gap-2'):
|
||||||
ui.image('/assets/icon.ico').classes('w-8 h-8').props('fit=contain')
|
ui.image('/assets/icon.ico').classes('w-8 h-8').props('fit=contain')
|
||||||
ui.label('尚城幼儿园成长报告助手').classes('text-xl font-bold')
|
ui.label('尚城幼儿园成长报告助手').classes('text-xl font-bold')
|
||||||
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():
|
def create_page():
|
||||||
# 1. 引入外部 CSS
|
# 1. 引入外部 CSS
|
||||||
@@ -49,18 +53,15 @@ def create_page():
|
|||||||
func_btn('📁 生成图片路径', 'image', generate_template)
|
func_btn('📁 生成图片路径', 'image', generate_template)
|
||||||
func_btn('🤖 生成评语 (AI)', 'smart_toy', generate_comment_all)
|
func_btn('🤖 生成评语 (AI)', 'smart_toy', generate_comment_all)
|
||||||
func_btn('📊 生成报告 (PPT)', 'analytics', generate_report)
|
func_btn('📊 生成报告 (PPT)', 'analytics', generate_report)
|
||||||
|
|
||||||
# 特殊处理带参数的
|
# 特殊处理带参数的
|
||||||
async def run_convert():
|
async def run_convert():
|
||||||
await run_task(batch_convert_folder, config.get("output_folder"))
|
await run_task(batch_convert_folder, config.get("output_folder"))
|
||||||
|
|
||||||
ui.button('📑 格式转换 (PDF)', on_click=run_convert).props('outline')
|
ui.button('📑 格式转换 (PDF)', on_click=run_convert).props('outline')
|
||||||
|
|
||||||
func_btn('🐂 生肖转化 (生日)', 'pets', generate_zodiac)
|
func_btn('🐂 生肖转化 (生日)', 'pets', generate_zodiac)
|
||||||
|
func_btn('💴 园长一键签名', 'refresh', generate_signature)
|
||||||
|
|
||||||
# === 下方双栏布局 ===
|
# === 下方双栏布局 ===
|
||||||
with ui.grid(columns=2).classes('w-full gap-4'):
|
with ui.grid(columns=2).classes('w-full gap-4'):
|
||||||
|
|
||||||
# 数据管理
|
# 数据管理
|
||||||
with ui.card().classes('func-card card-data'):
|
with ui.card().classes('func-card card-data'):
|
||||||
ui.label('📦 数据管理').classes('section-title text-blue')
|
ui.label('📦 数据管理').classes('section-title text-blue')
|
||||||
@@ -71,7 +72,6 @@ def create_page():
|
|||||||
|
|
||||||
ui.button('📦 导出模板', on_click=lambda: do_export(export_templates_folder)).props(f'outline')
|
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'):
|
with ui.card().classes('func-card card-system'):
|
||||||
ui.label('⚙️ 系统操作').classes('section-title text-red')
|
ui.label('⚙️ 系统操作').classes('section-title text-red')
|
||||||
@@ -87,7 +87,6 @@ def create_page():
|
|||||||
await run_task(initialize_project)
|
await run_task(initialize_project)
|
||||||
|
|
||||||
ui.button('⚠️ 初始化', on_click=reset_sys).props('outline color=warning').classes('flex-1')
|
ui.button('⚠️ 初始化', on_click=reset_sys).props('outline color=warning').classes('flex-1')
|
||||||
|
|
||||||
# === 日志区 ===
|
# === 日志区 ===
|
||||||
with ui.card().classes('func-card card-logging'):
|
with ui.card().classes('func-card card-logging'):
|
||||||
with ui.expansion('📝 系统实时日志',value=True).classes('w-full bg-white shadow-sm rounded'):
|
with ui.expansion('📝 系统实时日志',value=True).classes('w-full bg-white shadow-sm rounded'):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import traceback
|
||||||
|
|
||||||
def export_templates_folder(output_folder, stop_event, progress_callback=None):
|
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)
|
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
|
import comtypes.client
|
||||||
from config.config import load_config
|
from config.config import load_config
|
||||||
from utils.agent_utils import generate_comment
|
from utils.agent_utils import generate_comment
|
||||||
from utils.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.image_utils import find_image_path
|
||||||
from utils.zodiac_utils import calculate_zodiac
|
from utils.zodiac_utils import calculate_zodiac
|
||||||
from utils.growt_utils import (
|
from utils.growt_utils import (
|
||||||
@@ -22,6 +22,7 @@ from utils.growt_utils import (
|
|||||||
replace_four_page,
|
replace_four_page,
|
||||||
replace_five_page,
|
replace_five_page,
|
||||||
)
|
)
|
||||||
|
from utils.pptx_utils import replace_picture
|
||||||
|
|
||||||
# 如果你之前没有全局定义 console,这里定义一个
|
# 如果你之前没有全局定义 console,这里定义一个
|
||||||
console = 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):
|
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:
|
except Exception as e:
|
||||||
logger.error(f"程序运行出错: {str(e)}")
|
logger.error(f"程序运行出错: {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
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):
|
def replace_picture(prs, slide_index, placeholder, img_path):
|
||||||
"""在指定幻灯片中替换指定占位符的图片(包含自动旋转修复)"""
|
"""
|
||||||
|
在指定幻灯片中替换指定占位符的图片(包含自动旋转修复)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
prs: Presentation 对象
|
||||||
|
slide_index: 幻灯片索引 (从0开始)
|
||||||
|
placeholder: 占位符名称 (例如 "signature")
|
||||||
|
img_path: 图片路径
|
||||||
|
"""
|
||||||
if not os.path.exists(img_path):
|
if not os.path.exists(img_path):
|
||||||
logger.warning(f"警告: 图片路径不存在 {img_path}")
|
logger.warning(f"警告: 图片路径不存在 {img_path}")
|
||||||
return
|
return
|
||||||
|
|||||||
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 = "rich" },
|
||||||
{ name = "screeninfo" },
|
{ name = "screeninfo" },
|
||||||
{ name = "tomli" },
|
{ name = "tomli" },
|
||||||
|
{ name = "tomli-w" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -446,6 +447,7 @@ requires-dist = [
|
|||||||
{ name = "rich", specifier = ">=14.2.0" },
|
{ name = "rich", specifier = ">=14.2.0" },
|
||||||
{ name = "screeninfo", specifier = ">=0.8.1" },
|
{ name = "screeninfo", specifier = ">=0.8.1" },
|
||||||
{ name = "tomli", specifier = ">=2.3.0" },
|
{ name = "tomli", specifier = ">=2.3.0" },
|
||||||
|
{ name = "tomli-w", specifier = ">=1.2.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "tqdm"
|
name = "tqdm"
|
||||||
version = "4.67.1"
|
version = "4.67.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user