V2.0 五大核心增强: 锚点定位/原生图表/插件架构/WebSocket/LLM智能

This commit is contained in:
2026-05-29 14:14:53 +08:00
parent 5546e5fca1
commit 8618867f92
51 changed files with 3368 additions and 0 deletions

115
ppt_manager/README.md Normal file
View File

@@ -0,0 +1,115 @@
# 📊 PPT智能管理系统
PPT管理系统实现**静态模板内容**与**动态数据内容**的智能合并。
## ✨ 核心特性
- 📁 **静态PPT管理**: 固定不变的原理内容放在本地PPT模板中
- 🔄 **动态内容生成**: Python脚本负责数据采集、图表生成
- 🎯 **页面映射配置**: YAML配置文件定义每页是静态还是动态
- 🖱️ **一键生成**: Web界面点击按钮自动合成最终PPT
- 📝 **日志记录**: 使用loguru完整记录生成过程
## 📁 项目结构
```
ppt_manager/
├── static_ppt/ # 静态PPT模板目录
│ └── macro_analysis_template.pptx # 23页模板示例
├── config/
│ └── project_config.yaml # 项目配置:页面映射关系
├── scripts/ # 动态内容生成脚本
│ ├── gdp_chart.py # GDP图表生成
│ ├── inflation_chart.py # CPI/PPI图表
│ ├── employment_table.py # 就业数据表
│ ├── trade_chart.py # 进出口图表
│ └── market_analysis.py # 市场分析图表
├── dynamic_content/ # 动态生成的图片存放
├── output/ # 最终生成的PPT输出目录
├── logs/ # 日志目录
├── src/
│ ├── config_loader.py # 配置加载器
│ ├── ppt_core.py # PPT核心操作
│ ├── dynamic_generator.py # 动态内容生成器
│ └── ppt_generator.py # 主合成引擎
├── templates/
│ └── index.html # Web界面
├── app.py # Flask Web应用
├── main.py # 命令行入口
├── create_sample_template.py # 创建示例模板脚本
└── requirements.txt # Python依赖
```
## 🚀 快速开始
### 1. 安装依赖
```bash
cd ppt_manager
pip install -r requirements.txt
```
### 2. 初始化示例静态PPT模板
```bash
python create_sample_template.py
```
### 3. 运行系统
**方式一Web界面推荐**
```bash
python app.py
```
然后在浏览器打开: http://localhost:5000
**方式二:命令行**
```bash
python main.py macro_analysis
```
## ⚙️ 配置说明
在 [config/project_config.yaml](file:///f:/ppt/ppt_manager/config/project_config.yaml) 中配置:
```yaml
projects:
macro_analysis:
name: "宏观数据分析报告"
static_ppt: "static_ppt/macro_analysis_template.pptx"
total_slides: 23
slide_mapping:
1: static # 静态页,来自模板
2: static
3: static
4: dynamic_chart_gdp # 动态页,执行对应脚本
5: dynamic_chart_inflation
...
dynamic_generators:
dynamic_chart_gdp: "scripts/gdp_chart.py" # 脚本对应关系
...
```
## 📝 动态脚本编写规范
每个动态脚本必须包含一个 `generate(output_dir)` 函数,返回生成的图片路径:
```python
def generate(output_dir):
# 1. 采集数据API/爬虫/数据库等)
# 2. 生成图表matplotlib/plotly等
# 3. 返回图片完整路径
return str(image_path)
```
## 🎯 使用场景示例
| 页面 | 类型 | 内容 | 更新频率 |
|------|------|------|----------|
| 1-3页 | static | 封面、目录、分析框架 | 固定不变 |
| 4页 | dynamic | GDP趋势图 | 每月更新 |
| 5页 | dynamic | CPI/PPI通胀图 | 每月更新 |
| 7页 | dynamic | 就业数据表 | 每月更新 |
| 10页 | dynamic | 进出口贸易 | 每月更新 |
| 13页 | dynamic | A股市场分析 | 每周更新 |
| 其他页 | static | 原理、方法论 | 固定不变 |

70
ppt_manager/app.py Normal file
View File

@@ -0,0 +1,70 @@
from flask import Flask, render_template, jsonify, send_file, request
from pathlib import Path
import sys
import os
sys.path.insert(0, str(Path(__file__).parent / "src"))
from src.ppt_generator import PPTGenerator
from loguru import logger
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False
generator = PPTGenerator()
@app.route('/')
def index():
projects = generator.list_projects()
return render_template('index.html', projects=projects)
@app.route('/api/projects')
def list_projects():
projects = generator.list_projects()
return jsonify({'success': True, 'projects': projects})
@app.route('/api/generate/<project_name>', methods=['POST'])
def generate_ppt(project_name):
try:
output_path = generator.generate_project(project_name)
if output_path:
filename = os.path.basename(output_path)
return jsonify({
'success': True,
'message': 'PPT生成成功',
'filename': filename,
'download_url': f'/download/{filename}'
})
else:
return jsonify({'success': False, 'message': 'PPT生成失败请查看日志'})
except Exception as e:
logger.exception(f"生成PPT时发生错误: {e}")
return jsonify({'success': False, 'message': str(e)})
@app.route('/download/<filename>')
def download_file(filename):
output_dir = Path(__file__).parent / "output"
file_path = output_dir / filename
if file_path.exists():
return send_file(str(file_path), as_attachment=True)
return jsonify({'success': False, 'message': '文件不存在'})
@app.route('/api/files')
def list_files():
output_dir = Path(__file__).parent / "output"
files = []
if output_dir.exists():
for f in sorted(output_dir.glob("*.pptx"), reverse=True):
files.append({
'name': f.name,
'size': round(f.stat().st_size / 1024 / 1024, 2),
'modified': f.stat().st_mtime
})
return jsonify({'success': True, 'files': files})
if __name__ == '__main__':
print("\n" + "="*60)
print("PPT管理系统 Web 界面启动中...")
print("请在浏览器中打开: http://localhost:5000")
print("="*60 + "\n")
app.run(debug=True, host='0.0.0.0', port=5000)

View File

@@ -0,0 +1,42 @@
projects:
macro_analysis:
name: "宏观数据分析报告"
static_ppt: "static_ppt/macro_analysis_template.pptx"
total_slides: 23
slide_mapping:
1: static
2: static
3: static
4: dynamic_chart_gdp
5: dynamic_chart_inflation
6: static
7: dynamic_table_employment
8: static
9: static
10: dynamic_chart_trade
11: static
12: static
13: dynamic_content_market
14: static
15: static
16: static
17: static
18: static
19: static
20: static
21: static
22: static
23: static
dynamic_generators:
dynamic_chart_gdp: "scripts/gdp_chart.py"
dynamic_chart_inflation: "scripts/inflation_chart.py"
dynamic_table_employment: "scripts/employment_table.py"
dynamic_chart_trade: "scripts/trade_chart.py"
dynamic_content_market: "scripts/market_analysis.py"
settings:
output_dir: "output"
dynamic_content_dir: "dynamic_content"
log_level: "INFO"
image_width: 900
image_height: 500

View File

@@ -0,0 +1,56 @@
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN
from pathlib import Path
from loguru import logger
def create_sample_template():
prs = Presentation()
prs.slide_width = Inches(10)
prs.slide_height = Inches(7.5)
logger.info("正在创建23页示例静态PPT模板...")
for page_num in range(1, 24):
slide_layout = prs.slide_layouts[5] if page_num == 1 else prs.slide_layouts[6]
slide = prs.slides.add_slide(slide_layout)
left = Inches(1)
top = Inches(0.5)
width = Inches(8)
height = Inches(1)
title_box = slide.shapes.add_textbox(left, top, width, height)
tf = title_box.text_frame
p = tf.paragraphs[0]
if page_num == 1:
p.text = "宏观数据分析报告"
p.font.size = Pt(36)
else:
p.text = f"{page_num} 页 - 示例内容"
p.font.size = Pt(24)
p.font.bold = True
p.alignment = PP_ALIGN.CENTER
if page_num not in [4, 5, 7, 10, 13]:
content_left = Inches(1)
content_top = Inches(2)
content_width = Inches(8)
content_height = Inches(4)
content_box = slide.shapes.add_textbox(content_left, content_top, content_width, content_height)
content_tf = content_box.text_frame
content_tf.text = "这里是静态内容区域,原理固定不变的部分。\n\n定期更新的数据内容将由Python脚本自动生成并替换对应页面。"
content_tf.paragraphs[0].font.size = Pt(14)
output_path = Path(__file__).parent / "static_ppt" / "macro_analysis_template.pptx"
output_path.parent.mkdir(parents=True, exist_ok=True)
prs.save(str(output_path))
logger.success(f"示例模板已创建: {output_path}")
return str(output_path)
if __name__ == "__main__":
create_sample_template()

38
ppt_manager/main.py Normal file
View File

@@ -0,0 +1,38 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "src"))
print("=" * 60)
print("📊 PPT智能管理系统 - 命令行工具")
print("=" * 60)
from src.ppt_generator import PPTGenerator
def main():
generator = PPTGenerator()
print("\n可用项目:")
projects = generator.list_projects()
for proj in projects:
print(f" [{proj['id']}]: {proj['name']} ({proj['total_slides']}页)")
if len(sys.argv) > 1:
project_name = sys.argv[1]
print(f"\n正在生成项目: {project_name}...")
output_path = generator.generate_project(project_name)
if output_path:
print(f"\n✅ 生成成功! 文件位置: {output_path}")
else:
print("\n❌ 生成失败,请查看日志")
else:
print("\n使用方法:")
print(" 命令行方式: python main.py macro_analysis")
print(" Web界面方式: python app.py (然后打开 http://localhost:5000)")
choice = input("\n是否现在生成示例项目 [macro_analysis]? (y/n): ").strip().lower()
if choice == 'y':
generator.generate_project('macro_analysis')
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,11 @@
python-pptx>=0.6.21
Flask>=2.3.0
pandas>=2.0.0
matplotlib>=3.7.0
plotly>=5.15.0
requests>=2.31.0
PyYAML>=6.0
loguru>=0.7.0
kaleido>=0.2.1
lxml>=4.9.0
Pillow>=10.0.0

View File

@@ -0,0 +1,62 @@
import pandas as pd
import numpy as np
from pathlib import Path
from loguru import logger
import matplotlib.pyplot as plt
def generate(output_dir):
logger.info("开始生成就业数据表")
categories = ['城镇调查失业率', '青年失业率', '制造业PMI', '服务业PMI']
current = [5.1, 15.3, 49.8, 53.2]
previous = [5.2, 15.6, 49.5, 53.0]
change = [c - p for c, p in zip(current, previous)]
data = pd.DataFrame({
'指标': categories,
'当前值': current,
'上期值': previous,
'变动': change
})
fig, ax = plt.subplots(figsize=(10, 6))
ax.axis('tight')
ax.axis('off')
table_data = []
for _, row in data.iterrows():
change_color = '#4CAF50' if row['变动'] >= 0 else '#F44336'
table_data.append([
row['指标'],
f"{row['当前值']:.1f}",
f"{row['上期值']:.1f}",
f"{row['变动']:+.1f}"
])
table = ax.table(
cellText=table_data,
colLabels=['指标', '当前值', '上期值', '变动'],
cellLoc='center',
loc='center',
colColours=['#4A90E2'] * 4
)
table.auto_set_font_size(False)
table.set_fontsize(12)
table.scale(1, 2)
for key, cell in table.get_celld().items():
if key[0] > 0 and key[1] == 3:
cell.set_text_props(color='green' if '+' in cell.get_text().get_text() else 'red')
ax.set_title('就业与PMI数据摘要', fontsize=16, fontweight='bold', pad=20)
output_path = Path(output_dir) / 'employment_table.png'
plt.savefig(str(output_path), dpi=150, bbox_inches='tight')
plt.close()
logger.info(f"就业数据表已生成: {output_path}")
return str(output_path)
if __name__ == "__main__":
generate(Path.cwd())

View File

@@ -0,0 +1,32 @@
import pandas as pd
import numpy as np
from pathlib import Path
from loguru import logger
def generate(output_dir):
logger.info("开始生成GDP趋势图表")
quarters = ['2024Q1', '2024Q2', '2024Q3', '2024Q4', '2025Q1', '2025Q2']
gdp_growth = [5.2, 4.8, 5.0, 5.3, np.random.uniform(4.5, 5.5), np.random.uniform(4.5, 5.5)]
data = pd.DataFrame({
'季度': quarters,
'GDP增长率(%)': [round(x, 2) for x in gdp_growth]
})
data = data.set_index('季度')
from dynamic_generator import DynamicContentGenerator
generator = DynamicContentGenerator()
output_path = generator.generate_chart_matplotlib(
data,
title='中国GDP增长趋势',
x_label='季度',
y_label='增长率 (%)',
filename='gdp_chart.png',
kind='line'
)
return output_path
if __name__ == "__main__":
generate(Path.cwd())

View File

@@ -0,0 +1,34 @@
import pandas as pd
import numpy as np
from pathlib import Path
from loguru import logger
def generate(output_dir):
logger.info("开始生成CPI/PPI通胀图表")
months = ['1月', '2月', '3月', '4月', '5月', '6月']
cpi = [0.7, 0.8, 0.9, 1.0, np.random.uniform(0.8, 1.2), np.random.uniform(0.8, 1.2)]
ppi = [-2.5, -2.3, -2.0, -1.8, np.random.uniform(-2.5, -1.5), np.random.uniform(-2.5, -1.5)]
data = pd.DataFrame({
'月份': months,
'CPI(%)': [round(x, 2) for x in cpi],
'PPI(%)': [round(x, 2) for x in ppi]
})
data = data.set_index('月份')
from dynamic_generator import DynamicContentGenerator
generator = DynamicContentGenerator()
output_path = generator.generate_chart_plotly(
data,
title='CPI与PPI走势',
x_label='月份',
y_label='同比 (%)',
filename='inflation_chart.png',
kind='line'
)
return output_path
if __name__ == "__main__":
generate(Path.cwd())

View File

@@ -0,0 +1,66 @@
import pandas as pd
import numpy as np
from loguru import logger
def generate(output_dir):
logger.info("开始生成市场分析图")
indices = ['上证指数', '深证成指', '创业板指', '沪深300']
current = [3150, 10200, 2050, 3800]
change_pct = [
np.random.uniform(-2, 2),
np.random.uniform(-2, 2),
np.random.uniform(-2, 2),
np.random.uniform(-2, 2)
]
data = pd.DataFrame({
'指数': indices,
'点位': current,
'涨跌幅(%)': [round(x, 2) for x in change_pct]
})
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
colors = ['green' if x < 0 else 'red' for x in change_pct]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 7))
bars = ax1.bar(data['指数'], data['点位'], color=colors, alpha=0.7)
ax1.set_title('主要指数点位', fontsize=14, fontweight='bold')
ax1.set_ylabel('点位')
ax1.grid(axis='y', linestyle='--', alpha=0.3)
for bar in bars:
height = bar.get_height()
ax1.text(bar.get_x() + bar.get_width()/2., height,
f'{int(height)}', ha='center', va='bottom')
bars2 = ax2.bar(data['指数'], data['涨跌幅(%)'], color=colors, alpha=0.7)
ax2.set_title('涨跌幅', fontsize=14, fontweight='bold')
ax2.set_ylabel('涨跌幅 (%)')
ax2.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
ax2.grid(axis='y', linestyle='--', alpha=0.3)
for bar in bars2:
height = bar.get_height()
va = 'bottom' if height > 0 else 'top'
ax2.text(bar.get_x() + bar.get_width()/2., height,
f'{height:+.2f}%', ha='center', va=va, fontweight='bold')
plt.suptitle('A股市场概况', fontsize=18, fontweight='bold')
plt.tight_layout()
from pathlib import Path
output_path = Path(output_dir) / 'market_analysis.png'
plt.savefig(str(output_path), dpi=150, bbox_inches='tight')
plt.close()
logger.info(f"市场分析图已生成: {output_path}")
return str(output_path)
if __name__ == "__main__":
from pathlib import Path
generate(Path.cwd())

View File

@@ -0,0 +1,34 @@
import pandas as pd
import numpy as np
from loguru import logger
def generate(output_dir):
logger.info("开始生成进出口贸易图表")
months = ['1月', '2月', '3月', '4月', '5月', '6月']
exports = np.random.randint(2800, 3200, 6)
imports = np.random.randint(2000, 2400, 6)
data = pd.DataFrame({
'月份': months,
'出口(亿美元)': exports,
'进口(亿美元)': imports
})
data = data.set_index('月份')
from dynamic_generator import DynamicContentGenerator
generator = DynamicContentGenerator()
output_path = generator.generate_chart_matplotlib(
data,
title='进出口贸易情况',
x_label='月份',
y_label='金额 (亿美元)',
filename='trade_chart.png',
kind='bar'
)
return output_path
if __name__ == "__main__":
from pathlib import Path
generate(Path.cwd())

View File

View File

@@ -0,0 +1,39 @@
import yaml
import os
from pathlib import Path
from loguru import logger
class ConfigLoader:
def __init__(self, config_path=None):
if config_path is None:
base_dir = Path(__file__).parent.parent
config_path = base_dir / "config" / "project_config.yaml"
self.config_path = Path(config_path)
self.config = self._load_config()
logger.info(f"配置文件已加载: {self.config_path}")
def _load_config(self):
with open(self.config_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def get_project(self, project_name):
return self.config.get('projects', {}).get(project_name)
def get_all_projects(self):
return self.config.get('projects', {})
def get_settings(self):
return self.config.get('settings', {})
def get_dynamic_script(self, project_name, dynamic_key):
project = self.get_project(project_name)
if project:
return project.get('dynamic_generators', {}).get(dynamic_key)
return None
def get_slide_mapping(self, project_name):
project = self.get_project(project_name)
if project:
return project.get('slide_mapping', {})
return {}

View File

@@ -0,0 +1,93 @@
import subprocess
import sys
from pathlib import Path
from loguru import logger
import importlib.util
import pandas as pd
import matplotlib.pyplot as plt
import plotly.express as px
import plotly.io as pio
class DynamicContentGenerator:
def __init__(self, base_dir=None):
if base_dir is None:
self.base_dir = Path(__file__).parent.parent
else:
self.base_dir = Path(base_dir)
self.dynamic_content_dir = self.base_dir / "dynamic_content"
self.dynamic_content_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"动态内容生成器初始化, 输出目录: {self.dynamic_content_dir}")
def run_script(self, script_path):
script_file = self.base_dir / script_path
if not script_file.exists():
logger.error(f"脚本文件不存在: {script_file}")
return None
logger.info(f"执行动态脚本: {script_file}")
try:
spec = importlib.util.spec_from_file_location("dynamic_module", str(script_file))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if hasattr(module, 'generate'):
result = module.generate(self.dynamic_content_dir)
logger.success(f"脚本执行完成: {result}")
return result
else:
logger.error(f"脚本中没有找到 generate() 函数")
return None
except Exception as e:
logger.exception(f"执行脚本时出错: {e}")
return None
def generate_chart_matplotlib(self, data, title, x_label, y_label, filename, kind='line'):
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
fig, ax = plt.subplots(figsize=(12, 7))
if kind == 'line':
if isinstance(data, pd.DataFrame):
data.plot(ax=ax, kind='line', linewidth=2, marker='o')
else:
ax.plot(data)
elif kind == 'bar':
if isinstance(data, pd.DataFrame):
data.plot(ax=ax, kind='bar')
else:
ax.bar(range(len(data)), data)
ax.set_title(title, fontsize=16, fontweight='bold')
ax.set_xlabel(x_label, fontsize=12)
ax.set_ylabel(y_label, fontsize=12)
ax.grid(True, linestyle='--', alpha=0.7)
plt.legend()
plt.tight_layout()
output_path = self.dynamic_content_dir / filename
plt.savefig(str(output_path), dpi=150, bbox_inches='tight')
plt.close()
logger.info(f"图表已生成: {output_path}")
return str(output_path)
def generate_chart_plotly(self, data, title, x_label, y_label, filename, kind='line'):
if isinstance(data, pd.DataFrame):
if kind == 'line':
fig = px.line(data, title=title)
elif kind == 'bar':
fig = px.bar(data, title=title)
else:
fig = px.line(y=data, title=title)
fig.update_layout(
xaxis_title=x_label,
yaxis_title=y_label,
font=dict(family="Microsoft YaHei, SimHei"),
title_x=0.5
)
output_path = self.dynamic_content_dir / filename
pio.write_image(fig, str(output_path), width=1200, height=700, scale=2)
logger.info(f"Plotly图表已生成: {output_path}")
return str(output_path)

View File

@@ -0,0 +1,71 @@
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN
from pathlib import Path
from loguru import logger
import os
class PPTCore:
def __init__(self, base_dir=None):
if base_dir is None:
self.base_dir = Path(__file__).parent.parent
else:
self.base_dir = Path(base_dir)
logger.info(f"PPT核心模块初始化, 基础目录: {self.base_dir}")
def load_static_ppt(self, static_ppt_path):
ppt_path = self.base_dir / static_ppt_path
logger.info(f"加载静态PPT: {ppt_path}")
if not ppt_path.exists():
logger.error(f"静态PPT文件不存在: {ppt_path}")
raise FileNotFoundError(f"静态PPT文件不存在: {ppt_path}")
return Presentation(str(ppt_path))
def create_new_ppt(self):
return Presentation()
def copy_slide(self, source_prs, target_prs, slide_index):
source_slide = source_prs.slides[slide_index - 1]
slide_layout = target_prs.slide_layouts[6] if len(target_prs.slide_layouts) > 6 else target_prs.slide_layouts[0]
new_slide = target_prs.slides.add_slide(slide_layout)
for shp in new_slide.shapes:
sp = shp.element
sp.getparent().remove(sp)
for shape in source_slide.shapes:
el = shape.element
new_slide.shapes._spTree.insert_element_before(el, 'p:extLst')
logger.debug(f"已复制第 {slide_index}")
return new_slide
def insert_image_to_slide(self, slide, image_path, left=1, top=1.5, width=8, height=5):
left_inch = Inches(left)
top_inch = Inches(top)
width_inch = Inches(width)
height_inch = Inches(height)
slide.shapes.add_picture(str(image_path), left_inch, top_inch, width_inch, height_inch)
logger.info(f"已插入图片: {image_path}")
def add_title_to_slide(self, slide, title_text, font_size=24, is_bold=True):
left = Inches(0.5)
top = Inches(0.2)
width = Inches(9)
height = Inches(0.8)
title_box = slide.shapes.add_textbox(left, top, width, height)
tf = title_box.text_frame
tf.text = title_text
p = tf.paragraphs[0]
p.font.size = Pt(font_size)
p.font.bold = is_bold
p.alignment = PP_ALIGN.CENTER
def save_ppt(self, prs, output_path):
output_file = self.base_dir / output_path
output_file.parent.mkdir(parents=True, exist_ok=True)
prs.save(str(output_file))
logger.success(f"PPT已保存: {output_file}")
return str(output_file)

View File

@@ -0,0 +1,119 @@
from pathlib import Path
from loguru import logger
from datetime import datetime
import sys
sys.path.insert(0, str(Path(__file__).parent))
from config_loader import ConfigLoader
from ppt_core import PPTCore
from dynamic_generator import DynamicContentGenerator
class PPTGenerator:
def __init__(self, base_dir=None):
if base_dir is None:
self.base_dir = Path(__file__).parent.parent
else:
self.base_dir = Path(base_dir)
self.config_loader = ConfigLoader()
self.ppt_core = PPTCore(self.base_dir)
self.dynamic_generator = DynamicContentGenerator(self.base_dir)
log_file = self.base_dir / "logs" / f"ppt_generator_{datetime.now().strftime('%Y%m%d')}.log"
log_file.parent.mkdir(parents=True, exist_ok=True)
logger.add(str(log_file), rotation="10 MB", level="INFO", encoding="utf-8")
logger.info("PPT生成器初始化完成")
def generate_project(self, project_name):
logger.info(f"开始生成项目: {project_name}")
project_config = self.config_loader.get_project(project_name)
if not project_config:
logger.error(f"找不到项目配置: {project_name}")
return None
slide_mapping = self.config_loader.get_slide_mapping(project_name)
total_slides = project_config.get('total_slides', 0)
static_ppt_path = project_config.get('static_ppt', '')
logger.info(f"加载静态PPT: {static_ppt_path}")
logger.info(f"总页数: {total_slides}")
dynamic_generated_files = {}
for slide_num, mapping in slide_mapping.items():
if mapping != 'static':
script_path = self.config_loader.get_dynamic_script(project_name, mapping)
if script_path:
logger.info(f"执行第 {slide_num} 页动态脚本: {script_path}")
result = self.dynamic_generator.run_script(script_path)
if result:
dynamic_generated_files[slide_num] = result
logger.success(f"{slide_num} 页动态内容生成成功: {result}")
try:
source_prs = self.ppt_core.load_static_ppt(static_ppt_path)
target_prs = self.ppt_core.create_new_ppt()
for slide_num in range(1, total_slides + 1):
mapping = slide_mapping.get(slide_num, 'static')
if mapping == 'static':
logger.info(f"处理第 {slide_num} 页: 静态内容")
if slide_num - 1 < len(source_prs.slides):
self.ppt_core.copy_slide(source_prs, target_prs, slide_num)
else:
logger.warning(f"静态PPT中缺少第 {slide_num} 页,创建空白页")
slide_layout = target_prs.slide_layouts[6] if len(target_prs.slide_layouts) > 6 else target_prs.slide_layouts[0]
target_prs.slides.add_slide(slide_layout)
else:
logger.info(f"处理第 {slide_num} 页: 动态内容 - {mapping}")
slide_layout = target_prs.slide_layouts[6] if len(target_prs.slide_layouts) > 6 else target_prs.slide_layouts[0]
new_slide = target_prs.slides.add_slide(slide_layout)
self.ppt_core.add_title_to_slide(new_slide, f"{slide_num}页 - 动态更新内容", font_size=20)
if slide_num in dynamic_generated_files:
image_path = dynamic_generated_files[slide_num]
if Path(image_path).exists():
self.ppt_core.insert_image_to_slide(new_slide, image_path, left=0.8, top=1.2, width=8.5, height=5)
output_filename = f"{project_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pptx"
output_path = Path("output") / output_filename
final_path = self.ppt_core.save_ppt(target_prs, str(output_path))
logger.success(f"=" * 50)
logger.success(f"项目 {project_name} 生成完成!")
logger.success(f"输出文件: {final_path}")
logger.success(f"=" * 50)
return final_path
except Exception as e:
logger.exception(f"生成PPT时发生错误: {e}")
return None
def list_projects(self):
projects = self.config_loader.get_all_projects()
result = []
for key, info in projects.items():
result.append({
'id': key,
'name': info.get('name', key),
'total_slides': info.get('total_slides', 0),
'slide_mapping': info.get('slide_mapping', {})
})
return result
if __name__ == "__main__":
generator = PPTGenerator()
if len(sys.argv) > 1:
project_name = sys.argv[1]
generator.generate_project(project_name)
else:
print("可用项目:")
for proj in generator.list_projects():
print(f" - {proj['id']}: {proj['name']} ({proj['total_slides']}页)")
print("\n使用方法: python ppt_generator.py <项目名称>")

View File

@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PPT智能管理系统</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@keyframes spin {
to { transform: rotate(360deg); }
}
.animate-spin {
animation: spin 1s linear infinite;
}
</style>
</head>
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-5xl">
<div class="text-center mb-10">
<h1 class="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-purple-600 mb-3">
📊 PPT智能管理系统
</h1>
<p class="text-gray-600 text-lg">静态模板 + 动态数据 = 一键生成最新报告</p>
</div>
<div class="grid md:grid-cols-2 gap-6 mb-8">
<div class="bg-white rounded-2xl shadow-xl p-6">
<h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="mr-2">📁</span> 可用项目
</h2>
<div id="projects-list" class="space-y-3">
{% for project in projects %}
<div class="project-card border-2 border-gray-200 hover:border-blue-400 rounded-xl p-4 transition-all duration-300 hover:shadow-md cursor-pointer" data-project="{{ project.id }}">
<div class="flex justify-between items-center">
<div>
<h3 class="font-bold text-gray-800">{{ project.name }}</h3>
<p class="text-sm text-gray-500">共 {{ project.total_slides }} 页</p>
</div>
<button onclick="generatePPT('{{ project.id }}')"
class="bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white px-5 py-2.5 rounded-lg font-medium transition-all duration-300 transform hover:scale-105 shadow-md hover:shadow-lg">
🚀 开始生成
</button>
</div>
<div class="mt-3 text-xs text-gray-500">
<span class="bg-green-100 text-green-700 px-2 py-1 rounded-full mr-1">
静态: {{ project.slide_mapping.values() | list | count('static') }} 页
</span>
<span class="bg-orange-100 text-orange-700 px-2 py-1 rounded-full">
动态: {{ project.total_slides - (project.slide_mapping.values() | list | count('static')) }} 页
</span>
</div>
</div>
{% endfor %}
</div>
<div id="status" class="mt-4 hidden">
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div class="flex items-center">
<svg class="animate-spin h-5 w-5 mr-3 text-blue-500" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span id="status-text" class="text-blue-700 font-medium">正在生成PPT请稍候...</span>
</div>
</div>
</div>
<div id="result" class="mt-4 hidden">
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-green-700 font-bold">✅ PPT生成成功</p>
<p id="result-filename" class="text-green-600 text-sm mt-1"></p>
</div>
<a id="download-btn" href="#" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg font-medium transition-colors">
📥 下载文件
</a>
</div>
</div>
</div>
<div id="error" class="mt-4 hidden">
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
<p class="text-red-700 font-bold">❌ 生成失败</p>
<p id="error-message" class="text-red-600 text-sm mt-1"></p>
</div>
</div>
</div>
<div class="bg-white rounded-2xl shadow-xl p-6">
<h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="mr-2">📋</span> 已生成文件
</h2>
<div id="files-list" class="space-y-2 max-h-96 overflow-y-auto">
<p class="text-gray-500 text-sm">加载中...</p>
</div>
</div>
</div>
<div class="bg-white rounded-2xl shadow-xl p-6">
<h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
<span class="mr-2">📖</span> 使用说明
</h2>
<div class="grid md:grid-cols-3 gap-4 text-sm">
<div class="bg-gray-50 rounded-xl p-4">
<h3 class="font-bold text-gray-700 mb-2">1⃣ 准备静态模板</h3>
<p class="text-gray-600">将固定不变的PPT模板放入 static_ppt 目录</p>
</div>
<div class="bg-gray-50 rounded-xl p-4">
<h3 class="font-bold text-gray-700 mb-2">2⃣ 配置页面映射</h3>
<p class="text-gray-600">在 config/project_config.yaml 中配置每页是静态或动态</p>
</div>
<div class="bg-gray-50 rounded-xl p-4">
<h3 class="font-bold text-gray-700 mb-2">3⃣ 编写动态脚本</h3>
<p class="text-gray-600">在 scripts 目录编写数据爬取和图表生成脚本</p>
</div>
</div>
</div>
</div>
<script>
function generatePPT(projectName) {
const statusEl = document.getElementById('status');
const resultEl = document.getElementById('result');
const errorEl = document.getElementById('error');
statusEl.classList.remove('hidden');
resultEl.classList.add('hidden');
errorEl.classList.add('hidden');
fetch(`/api/generate/${projectName}`, { method: 'POST' })
.then(response => response.json())
.then(data => {
statusEl.classList.add('hidden');
if (data.success) {
resultEl.classList.remove('hidden');
document.getElementById('result-filename').textContent = data.filename;
document.getElementById('download-btn').href = data.download_url;
loadFiles();
} else {
errorEl.classList.remove('hidden');
document.getElementById('error-message').textContent = data.message;
}
})
.catch(err => {
statusEl.classList.add('hidden');
errorEl.classList.remove('hidden');
document.getElementById('error-message').textContent = err.message;
});
}
function loadFiles() {
fetch('/api/files')
.then(response => response.json())
.then(data => {
const filesList = document.getElementById('files-list');
if (data.files.length === 0) {
filesList.innerHTML = '<p class="text-gray-500 text-sm">暂无生成的文件</p>';
} else {
filesList.innerHTML = data.files.map(f => `
<div class="flex justify-between items-center bg-gray-50 rounded-lg p-3 hover:bg-gray-100 transition-colors">
<div>
<p class="font-medium text-gray-700 text-sm">${f.name}</p>
<p class="text-xs text-gray-500">${f.size} MB</p>
</div>
<a href="/download/${f.name}" class="text-blue-500 hover:text-blue-700 text-sm font-medium">
下载
</a>
</div>
`).join('');
}
});
}
loadFiles();
</script>
</body>
</html>