1
0
Files
PPTX/preview/server.py
lanyuanxiaoyao f1aae96a04 refactor: 重构外部模板系统,改为单文件模板库模式
主要变更:
- 将 templates_dir 参数改为 template_file,支持单个模板库 YAML 文件
- 添加模板库 YAML 验证功能
- 为模板添加 base_dir 支持,正确解析相对路径资源
- 内联模板与外部模板同名时改为警告(内联优先)
- 移除模板缓存机制,直接使用模板库字典
- 更新所有相关测试以适配新的模板加载方式

此重构简化了模板管理,使模板资源的路径解析更加清晰明确。
2026-03-05 13:27:12 +08:00

250 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
预览服务器模块
提供浏览器预览功能,支持文件监听和热重载。
"""
import sys
import queue
import webbrowser
import random
from pathlib import Path
from threading import Thread
try:
from flask import Flask, Response
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
except ImportError:
Flask = None
Observer = None
FileSystemEventHandler = None
from core.presentation import Presentation
from renderers.html_renderer import HtmlRenderer
from loaders.yaml_loader import YAMLError
from utils import log_info, log_error
# HTML 模板
HTML_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>YAML Preview</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
padding: 20px;
background: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.slide {
width: 960px;
height: 540px;
position: relative;
background: white;
margin: 20px auto;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
.slide-number {
position: absolute;
bottom: 10px;
right: 10px;
font-size: 12px;
color: #999;
z-index: 1000;
}
.element {
position: absolute;
}
table.element {
border-collapse: collapse;
}
table.element td {
padding: 8px;
border: 1px solid #ddd;
}
</style>
</head>
<body>
{{ slides_html }}
<script>
const eventSource = new EventSource('/events');
eventSource.onmessage = (e) => {
if (e.data === 'reload') {
console.log('[Preview] 重新加载...');
location.reload();
}
};
eventSource.onerror = () => {
console.error('[Preview] 连接断开');
};
</script>
</body>
</html>
"""
ERROR_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>预览错误</title>
<style>
body {
margin: 0;
padding: 40px;
background: #f5f5f5;
font-family: monospace;
}
.error {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 4px;
color: #856404;
}
h1 { margin-top: 0; }
</style>
</head>
<body>
<div class="error">
<h1>⚠️ YAML 解析错误</h1>
<pre>{{ error }}</pre>
</div>
<script>
const eventSource = new EventSource('/events');
eventSource.onmessage = () => location.reload();
</script>
</body>
</html>
"""
# 全局变量
app = None
change_queue = None
current_yaml_file = None
current_template_file = None
class YAMLChangeHandler:
"""文件变化处理器"""
def on_modified(self, event):
if event.src_path.endswith('.yaml'):
log_info(f"检测到文件变化: {event.src_path}")
if change_queue:
change_queue.put('reload')
def generate_preview_html(yaml_file, template_file):
"""生成完整的预览 HTML 页面"""
try:
pres = Presentation(yaml_file, template_file)
renderer = HtmlRenderer()
slides_html = ""
for i, slide_data in enumerate(pres.data.get('slides', [])):
rendered = pres.render_slide(slide_data)
slides_html += renderer.render_slide(rendered, i, Path(yaml_file).parent)
return HTML_TEMPLATE.replace('{{ slides_html }}', slides_html)
except YAMLError as e:
return ERROR_TEMPLATE.replace('{{ error }}', str(e))
def create_flask_app():
"""创建 Flask 应用"""
flask_app = Flask(__name__)
@flask_app.route('/')
def index():
"""主页面"""
try:
return generate_preview_html(current_yaml_file, current_template_file)
except Exception as e:
return ERROR_TEMPLATE.replace('{{ error }}', f"生成预览失败: {str(e)}")
@flask_app.route('/events')
def events():
"""SSE 事件流"""
def event_stream():
while True:
change_queue.get()
yield 'data: reload\\n\\n'
return Response(event_stream(), mimetype='text/event-stream')
return flask_app
def start_preview_server(yaml_file, template_file, port, host='127.0.0.1', open_browser=True):
"""启动预览服务器
Args:
yaml_file: YAML 文件路径
template_file: 模板库文件路径
port: 服务器端口
host: 主机地址默认127.0.0.1
open_browser: 是否自动打开浏览器默认True
"""
global app, change_queue, current_yaml_file, current_template_file
if Flask is None:
log_error("预览功能需要 flask 和 watchdog 依赖")
log_error("请确保使用 uv 运行脚本,依赖会自动安装")
sys.exit(1)
current_yaml_file = yaml_file
current_template_file = template_file
change_queue = queue.Queue()
# 创建 Flask 应用
app = create_flask_app()
# 启动文件监听
if FileSystemEventHandler:
handler = YAMLChangeHandler()
if hasattr(handler, 'on_modified'):
# 创建一个简单的事件处理器
class SimpleHandler(FileSystemEventHandler):
def on_modified(self, event):
handler.on_modified(event)
observer = Observer()
observer.schedule(SimpleHandler(), str(Path(yaml_file).parent), recursive=False)
observer.start()
# 输出日志
log_info(f"正在监听: {yaml_file}")
log_info(f"预览地址: http://{host}:{port}")
log_info("按 Ctrl+C 停止")
# 自动打开浏览器(如果启用)
if open_browser:
Thread(target=lambda: webbrowser.open(f'http://localhost:{port}')).start()
# 启动 Flask
try:
app.run(host=host, port=port, debug=False, threaded=True)
except OSError as e:
if 'Address already in use' in str(e):
log_error(f"端口 {port} 已被占用")
log_error(f"请使用 --port 参数指定其他端口,例如: --port {port + 1}")
else:
log_error(f"启动服务器失败: {str(e)}")
sys.exit(1)
except KeyboardInterrupt:
if 'observer' in locals():
observer.stop()
observer.join()
log_info("已停止")