前期规划
准备
nginx代理
我使用的ngin做了本地代理,以避免跨域CORS这个问题。(请参考nginx.conf配置文件)-
目录结构
运行
-
开启mock数据
python3 mock.py
-
开启nginx代理服务
brew services restart nginx
-
访问html页面
http://10.8.47.12/html/
我的本地IP是:10.8.47.12
选择全选
点击屏幕右上角的定制化导出
按钮,执行导出pptx.
代码
nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root html;
autoindex on; # autoindex 指令用于在用户访问目录时显示目录列表。如果你希望在某个 location 块中启用目录列表功能,可以使用 autoindex on; 指令。
index index.html index.htm;
}
location /html/ {
alias /opt/homebrew/var/www/html/;
index index.html;
}
# http://10.8.47.12:5000/api/pptx
# http://10.8.47.12/api/pptx
# http://10.8.47.12:5000/MOCK/api/output
# 反向代理,处理用户端发送的请求
location /MOCK_API/ {
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_pass http://10.8.47.12:5000/;
}
}
include servers/*;
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pptx in HTML</title>
<link rel="shortcut icon" href="data:image/x-icon;,">
<style type="text/css">
/* 默认样式 */
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
overflow: auto;
}
b {
color: red;
}
/* 用于全局样式 */
/*
用于移动设备的样式
在小于或等于 768px 的屏幕上生效
*/
@media (max-width: 768px) {
/* 添加适合移动设备的样式 */
body {
font-size: 14px;
/* 修改字体大小 */
}
/* 隐藏侧边栏 */
.sidebar {
display: none;
}
}
.container {
display: grid;
grid-template-rows: auto 1fr;
/* 定义两行布局,header 高度自适应,main 占据剩余空间 */
height: 100vh;
/* 使容器铺满整个视口高度 */
margin-top: 0px;
/* 顶部留出 35px 空间 */
}
header {
background-color: #D5E3F4;
padding: 0px;
}
main {
display: flex;
flex-direction: row;
flex-wrap: wrap;
/* 子元素自动换行 */
overflow-y: auto;
/* 如果内容超出主要内容区域,启用垂直滚动 */
padding: 20px;
padding-bottom: 10vh;
background-image: url(./dist/pictures/back.png);
background-repeat: no-repeat;
background-position: right;
}
main .slide {
box-shadow: 2px 2px 10px #e7e5e5;
width: 20vw;
min-width: 12vw;
height: 20vh;
padding: 10px;
}
.slide {
flex: 1;
position: relative;
border: 0px dotted #dddddd;
padding: 5px;
margin: 5px;
}
.slide1 {
display: flex;
padding: 0 !important;
margin: 0 !important;
text-indent: 1em;
}
.slide2 {
ul {
list-style: none;
/* 清除默认的数字样式 */
padding: 0;
/* 清除内边距 */
counter-reset: item 0;
/* 重置计数器的起始值为0 */
}
ul li {
position: relative;
/* 使伪元素相对定位 */
padding-left: 22px;
/* 增加左边距给序号预留空间 */
margin-bottom: 5px;
counter-increment: item;
/* 每个列表项增加计数器的值 */
height: 25px;
line-height: 30px;
}
ul li::before {
content: counter(item);
/* 使用计数器的值作为内容 */
position: absolute;
/* 绝对定位 */
left: -8px;
/* 伪元素左边距相对于父元素 */
width: 25px;
/* 设置伪元素宽度 */
height: 25px;
/* 设置伪元素高度 */
line-height: 25px;
/* 垂直居中 */
background-color: #284466;
/* 背景颜色 */
border: 1px solid #DBE4F4;
/* 边框样式 */
border-radius: 5px;
/* 圆角边框 */
text-align: center;
/* 文本居中 */
color: white;
/* 字体颜色 */
}
}
.slide3 {
min-width: 50vw !important;
height: 71vh !important;
}
.slide5 {
min-width: 50vw !important;
height: 50vh !important;
}
.slide7 {
min-width: 54vw !important;
height: 60vh !important;
}
.slide9 {
min-width: 90vw !important;
height: 25vh !important;
}
.slide11 {
min-width: 30vw !important;
height: 28vh !important;
}
.slide12 {
min-width: fit-content !important;
height: fit-content !important;
}
table {
border: 1px solid #000;
border-collapse: collapse;
width: 100%;
}
th,
td {
border: 1px solid #000;
padding: 8px;
text-align: left;
}
th {
background-color: #304567;
color: #fff;
}
img {
max-width: 100%;
height: auto;
}
.title1 {
font-size: 24px;
font-weight: bold;
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
color: #2A466A;
}
.title2 {
font-size: 18px;
font-weight: bold;
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
padding-top: 5px;
color: #44546A;
}
.date1 {
font-size: 14px;
text-align: center;
padding-top: 18px;
margin-bottom: 0;
margin-right: 5px;
}
/* 复选框样式 */
.slide-checkbox {
position: absolute;
/* 复选框使用固定定位 */
top: 10px;
/* 距离顶部 10px */
right: 10px;
/* 距离右边 10px */
}
/* 导出按钮样式 */
#output {
/* display: none; */
position: relative;
}
#exportBtn {
position: fixed;
top: 0px;
right: 10px;
padding: 5px 20px;
background-color: #4CAF50;
/* 设置按钮的背景色为绿色 */
color: white;
/* 设置按钮文本颜色为白色 */
padding: 10px 20px;
/* 设置按钮内边距 */
border: none;
/* 去掉按钮边框 */
text-align: center;
/* 文本居中 */
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 10px;
/* 设置按钮圆角边框 */
}
#selectAll {
display: block;
padding: 0 0 0 10px;
}
*.searchResult {
font-weight: bold;
background-color: yellow;
}
*.redItalic {
font-style: italic;
color: red;
}
*.demo {
border: solid darkblue 1px;
background-color: #f5f5f5;
padding: 10px;
}
#feedback { font-size: 1.4em; }
* .ui-selecting { background: #5c5b59; opacity: 0.1; }
* .ui-selected { background: #F39814; color: rgb(235, 25, 25); }
#selectable { list-style-type: none; margin: 0; padding: 0; width: 60%; }
#selectable li { margin: 3px; padding: 0.4em; font-size: 1.4em; height: 18px; }
</style>
</head>
<body>
<!-- --------------------------------------------- -->
<div id="selectAll">
<br />
全选: <input type="checkbox" id="selectAllBtn" />
</div>
<div id="output">
<button id="exportBtn">定制化导出</button>
</div>
<div class="container">
<header></header>
<main></main>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.2.2/dist/echarts.min.js"></script>
<script>
const _title = (div, item) => {
if (item.name === 'text1' || item.name === 'text2') {
let h1 = document.createElement('h1');
h1.className = 'title1';
h1.textContent = item.value;
div.appendChild(h1);
}
else if (item.name === 'text3') {
let h2 = document.createElement('h2');
h2.className = 'title2';
h2.textContent = item.value;
div.appendChild(h2);
}
else if (item.name === 'text4' || item.name === 'text5' || item.name === 'text6') {
let p = document.createElement('p');
p.className = 'date1';
p.textContent = item.value;
div.appendChild(p);
}
}
const _text = (div, value) => {
let textP = document.createElement('p');
textP.textContent = value;
div.appendChild(textP);
}
const _picture = (div, value) => {
let img = document.createElement('img');
// 在 _picture 函数中,拼接图片路径时使用了 ../dist/pictures/,这可能导致图片路径不正确,根据文件的实际目录结构,应该确认图片的相对路径是否正确
if (value.startsWith('http')) {
img.src = value;
}
else {
img.src = './dist/pictures/' + value;
}
div.appendChild(img);
}
const _table = (div, value) => {
let table = document.createElement('table');
let thead = document.createElement('thead');
let tbody = document.createElement('tbody');
let headerRow = thead.insertRow();
value.headers.forEach(headerText => {
let th = document.createElement('th');
th.textContent = headerText;
headerRow.appendChild(th);
});
value.rows.forEach(rowData => {
let row = tbody.insertRow();
rowData.forEach(cellData => {
let cell = row.insertCell();
cell.textContent = cellData;
});
});
table.appendChild(thead);
table.appendChild(tbody);
div.appendChild(table);
}
const _chart = (div, value) => {
// div 是一个空的 div 元素,需要根据其父元素的宽高来设置图表的宽高
div.style.width = '600px';
div.style.height = '400px';
div.style.userSelect = 'all';
const { series, categories, title } = value;
// 根据 series, categories, title生成图表配置项
let option = {
title: {
text: title,
left: 'left'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: categories
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: categories
},
yAxis: {
type: 'value'
},
series: series
};
// 使用刚生成的配置项和数据项生成图表
let myChart = echarts.init(div);
myChart.setOption(option);
// div外面括上一个iframe
let iframe = null;
return {iframe, myChart};
}
let ul = null; // for slide2
let mockData = null; // 从服务器获取的mock数据
// 从http://127.0.0.1:5000/api/pptx获取mockData数据
// 加载数据(pptx数据)
const loadData = (req_name) => {
const { ip, port } = getIpAndPort();
// url = ip + ':5000/MOCK/api/' + req_name;
url = '/MOCK_API/api/' + req_name;
return $.ajax({
type: 'GET',
url: url,
dataType: 'json',
contentType: 'application/json',
dataType: 'json',
success: function (response) {
return response.data;
},
error: function (xhr, status, error) {
console.error(xhr.responseText, '[line: 378]');
return null;
}
})
}
// 获取当前页面的IP和端口
const getIpAndPort = () => {
let ip = window.location.hostname
if (ip === 'localhost') {
ip = '10.8.47.12'
}
let port = window.location.port
return {
ip: 'http://' + ip,
port
}
}
// start init
async function init(req_name) {
let mockData = await loadData(req_name)
// 根据JSON数据创建功能区
mockData && mockData.data.forEach((slide, i) => {
{
let _iframe = null;
let _myChart = null;
let div = document.createElement('div');
div.classList.add('slide');
div.setAttribute('index', i);
if (i === 0) {
div.classList.add('slide1');
}
else if (i === 1) {
div.classList.add('slide2');
// ul - li 构建内容
ul = document.createElement('ul');
div.appendChild(ul);
}
else {
div.classList.add('slide' + i);
}
const itms = (slide.content || [slide])
itms.forEach((item, j) => {
if (i === 0) {
// 创建页面标题
if (item.type === 'text') {
_title(div, item);
}
}
else if (i === 1) {
let li = document.createElement('li');
li.textContent = item.value;
ul.appendChild(li);
}
else {
if (item.type === 'text') {
_text(div, item.value);
}
else if (item.type === 'picture') {
_picture(div, item.value);
}
else if (item.type === 'table') {
_table(div, item.value);
}
else if (item.type === 'chart') {
const { iframe, myChart } = _chart(div, item.value);
div.style.userSelect = 'all';
}
}
});
document.body.appendChild(div);
// -------------------------------------
// 把图表添加iframe中
if (_iframe && _myChart) {
_iframe.contentWindow.location.href = _myChart.getCu
}
if (_iframe && _myChart) {
// 把图表放到iframe的sandbox中,使其可以执行js代码
// sandbox属性的值为:allow-pointer-lock allow-scripts allow-downloads allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-modals
_iframe.sandbox = "allow-pointer-lock allow-scripts allow-downloads allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-modals";
_iframe.srcdoc = _myChart.getDom().outerHTML;
_iframe.contentWindow.innerHTML = _iframe.contentWindow.innerHTML;
_iframe.contentWindow.onload = function () {
// 图表加载完成后,将图表实例对象存储在iframe的contentWindow中
_iframe.contentWindow.myChart = _myChart;
_iframe.contentWindow.onload = null;
}
}
// -------------------------------------
if (div) {
// slide1 添加到 header 标签内
// slide 2-11 添加到 main 标签内
if (i === 0) {
// 把slide1的内容移动到<header>标签内
// 确认元素是否成功选择
let header = document.querySelector('header');
header.innerHTML = '';
header.appendChild(div);
}
else {
// 把slide的内容这移到<main>标签内
let main = document.querySelector('main');
main.appendChild(div);
}
// 移除slide的div元素
div = null;
}
}
});
// 为所有包含"slide"类名的div元素追加复选框
var slideDivs = document.querySelectorAll('.slide');
slideDivs.forEach(function (div) {
var checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.classList.add('slide-checkbox');
div.appendChild(checkbox);
});
// 监听复选框状态变化
$('.slide-checkbox').on('change', function () {
const $output = $('#output');
if ($('.slide-checkbox:checked').length > 0) {
$output.show();
} else {
$output.hide();
}
});
// 给 selectAllBtn 添加选中事件
$('#selectAllBtn').on('change', function () {
var checked = $(this).prop('checked');
// #selectAllBtn 选中时,选中所有slide的复选框都被选中,反之则取消选中
$('.slide-checkbox').prop('checked', checked);
if (checked) {
$('#output').show();
}
});
// 定制化导出
$('#exportBtn').on('click', function () {
var selectedSlideDivs = $('.slide-checkbox:checked').parents('.slide');
var jsonData = [];
selectedSlideDivs.each(function (index, div) {
var slideIndex = parseInt($(div).attr('index'));
var slideData = mockData.data[slideIndex];
jsonData.push(slideData);
});
if(jsonData.length === 0) {
alert('导出失败!');
}
else {
exportJsonData(jsonData);
}
});
// 根据node_name返回对应的类型名称
function _getTypeName(node_name) {
if(!node_name) { return null; }
const mapName = new Map();
mapName.set('img', 'picture');
mapName.set('iframe', 'chart');
mapName.set('table', 'table');
mapName.set('p', 'text');
mapName.set('h1', 'text');
mapName.set('h2', 'text');
mapName.set('h3', 'text');
mapName.set('h4', 'text');
mapName.set('h5', 'text');
mapName.set('h6', 'text');
mapName.set('span', 'text');
mapName.set('div', 'text');
mapName.set('li', 'text');
mapName.set('ul', 'text');
mapName.set('ol', 'text');
mapName.set('hr', 'text');
mapName.set('br', 'text');
mapName.set('strong', 'text');
mapName.set('em', 'text');
mapName.set('b', 'text');
mapName.set('i', 'text');
mapName.set('u', 'text');
mapName.set('s', 'text');
mapName.set('strike', 'text');
mapName.set('del', 'text');
mapName.set('a', 'text');
node_name = node_name.toLowerCase();
return mapName.get(node_name) || 'text';
}
function downloadFile(fileUrl) {
// 创建一个隐藏的链接
var hiddenLink = document.createElement('a');
hiddenLink.href = fileUrl;
hiddenLink.download = 'downloadedFile'; // 可以指定要下载的文件的名称
hiddenLink.style.display = 'none';
document.body.appendChild(hiddenLink);
// 模拟点击链接以触发下载
hiddenLink.click();
// 清理
document.body.removeChild(hiddenLink);
}
// 导出JSON数据的函数不变
function exportJsonData(data) {
// 根据data中的name属性作为排序依据,升序排序data
data.sort(function (a, b) {
return a.name.localeCompare(b.name);
});
// 在这里执行导出JSON数据的逻辑
const { ip, port } = getIpAndPort();
// url = ip + ':5000/api/output';
url = '/MOCK_API/api/output'
$.ajax({
type: 'POST',
url: url,
data: JSON.stringify(data),
contentType: 'application/json',
success: function (response) {
download_href = response.download_href;
if (download_href) {
downloadFile(window.location.origin + '/MOCK_API/' + download_href);
}
alert('导出成功!');
},
error: function (xhr, status, error) {
console.log(xhr.responseText);
alert('导出失败!');
}
});
}
}
// -------------------------------------------------------------
$(document).ready(function () {
const req_name = 'pptx'; // 'get_pptx_index2';
init(req_name);
});
</script>
</body>
</html>
requirements.txt
annotated-types==0.7.0
anyio==4.4.0
astroid==3.2.2
blinker==1.8.2
certifi==2024.2.2
charset-normalizer==3.3.2
click==8.1.7
contourpy==1.2.1
cycler==0.12.1
dill==0.3.8
dnspython==2.6.1
email_validator==2.2.0
exceptiongroup==1.2.1
fastapi==0.111.0
fastapi-cli==0.0.4
Flask==3.0.3
Flask-Cors==4.0.1
fonttools==4.52.4
gunicorn==22.0.0
h11==0.14.0
httpcore==1.0.5
httptools==0.6.1
httpx==0.27.0
idna==3.7
importlib_metadata==7.1.0
importlib_resources==6.4.0
isort==5.13.2
itsdangerous==2.2.0
Jinja2==3.1.4
kiwisolver==1.4.5
lxml==5.2.2
markdown-it-py==3.0.0
MarkupSafe==2.1.5
matplotlib==3.9.0
mccabe==0.7.0
mdurl==0.1.2
netifaces==0.11.0
networkx==3.2.1
numpy==1.26.4
orjson==3.10.5
packaging==24.0
pillow==10.3.0
platformdirs==4.2.2
pydantic==2.7.4
pydantic_core==2.18.4
Pygments==2.18.0
pylint==3.2.2
pyparsing==3.1.2
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
python-multipart==0.0.9
python-pptx==0.6.23
python-pptx-valutico==0.6.24.3
PyYAML==6.0.1
requests==2.32.2
requests-mock==1.12.1
rich==13.7.1
shellingham==1.5.4
six==1.16.0
sniffio==1.3.1
starlette==0.37.2
tomli==2.0.1
tomlkit==0.12.5
typer==0.12.3
typing_extensions==4.12.0
ujson==5.10.0
urllib3==2.2.1
uvicorn==0.30.1
uvloop==0.19.0
watchfiles==0.22.0
websockets==12.0
Werkzeug==3.0.3
XlsxWriter==3.2.0
zipp==3.18.2
index.py
# 导入随机数模块
import copy
import json
import os
import random
import shutil
import string
from copy import deepcopy
import requests
from PIL import Image
from pptx import Presentation
from pptx.chart.data import CategoryChartData, ChartData
from pptx.dml.color import RGBColor
from pptx.enum.chart import XL_CHART_TYPE
from pptx.enum.dml import MSO_THEME_COLOR
from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE
from pptx.enum.text import MSO_ANCHOR, MSO_AUTO_SIZE, PP_ALIGN
# 导入oxml模块
from pptx.oxml import parse_xml
from pptx.table import Table, _Cell, _Column, _Row
# 导入Inches
from pptx.util import Inches
# 导入time
import time
# 导入re, 用于匹配验证
import re
import socket # 用于获取当前系统ip
# 导入logging, 用于异常日志记录
import logging
# 读取配置文件
import configparser
# 用于获取真实的IP地址
import netifaces as ni
from utils import *
# mock配置项的值
MOCK_CONFIG = None
# 运行的html配置项的值
HTML_CONFIG = None
# 导入logging, 用于异常日志记录
# 删除指定索引的幻灯片
def delete_slide(presentation, arrange_indexes):
# 删除指定索引的幻灯片
xml_slides = presentation.slides._sldIdLst # pylint: disable=W0212
slides = list(xml_slides)
for index in arrange_indexes:
xml_slides.remove(slides[index])
# 添加文本,详情见:https://python-pptx.readthedocs.io/en/latest/user/text.html
# 给指定slide添加内容,当前支持的内容类型有:文本、图片、表格、图表
def run(slide, content):
# 根据内容填充幻灯片中的形状
for shape in slide.shapes:
if shape.has_table:
# 在content中找到type="table"并且name从table开头的项, 然后填充表格数据
for item in content:
if item['type'] == 'table' and item['name'].startswith('table'):
set_table(shape, item['value'], slide)
# 找到文本框
elif shape.has_text_frame:
set_text(slide, shape, content)
elif shape.has_chart:
set_chart(shape, content)
# shape.chart.element.getparent().remove(shape.chart.element)
# set_chart(shape, content)
# 注意:图片处理放到了set_text函数中, 为了模板pptx文件好处理
# elif shape.has_chart:
# set_chart(shape, content)
# 注意:图片处理放到了set_text函数中, 为了模板pptx文件好处理
# 判断当前shape是否是图片
# elif shape.shape_type == 13:
# elif shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
# picture_url = 'static/pictures/slide_img.png'
# set_image(shape, image_url, slide)
# 保存文件
def save_file(prs, output_file):
# 保存填充数据后的PowerPoint文件
save_folder = 'static/pptx'; # 保存文件的路径 'download'
save_path = os.path.join(save_folder, os.path.basename(output_file))
# 创建output文件夹(如果不存在)
if not os.path.exists(save_folder):
os.makedirs(save_folder)
# 保存填充数据后的PowerPoint
prs.save(output_file)
# 如果output文件夹下已经存在同名文件,先移除
if os.path.exists(save_path):
os.remove(save_path)
shutil.move(output_file, save_path)
# 获取output_path的本地地址
return {
save_folder: save_path,
output_file: output_file
} # + output_file
# 从接口读取数据, 对网络请求的异常进行处理,以避免网络问题导致的程序崩溃
def getData(url):
try:
# 从 URL 地址获取数据
response = requests.get(url)
response.raise_for_status() # 如果请求不成功会抛出异常
json_data = response.json()
return json_data
except requests.exceptions.RequestException as e:
logging.error('Error in getData, 网络请求发生错误: %s %s', e, url)
return None
# 读取母板文件和填充数据
# is_mock: True-从接口读取数据, False-从json_data参数读取数据
def init_data(is_mock=True, json_data=None):
download_href = None
template_file = 'master/example.pptx'
# 时间戳
timetramp = str(int(round(time.time() * 1000)))
output_file = 'output1' + timetramp + '.pptx'
if is_mock:
# get current ip address
host = MOCK_CONFIG['host']
port = MOCK_CONFIG['port']
url = 'http://'+str(host) + ':' + str(port) + '/api/pptx'
json_data = getData(url)
if json_data is not None and "code" in json_data and "data" in json_data:
if json_data["code"]==1:
is_save = True
# 读取 PowerPoint 模板
prs = Presentation(template_file)
slides = prs.slides
# 遍历数据
data_len = len(json_data['data'])
# 幻灯片数量的校验, 没数据的slide删除
slide_index_arr = []
slide_run_arr = []
# 根据json_data["data"]加载slide
for slide_index, current_slide_data in enumerate(json_data["data"], start=1):
# 获取当前slide对象
slide = None
if current_slide_data is not None:
if 'index' in current_slide_data:
current_slide_index = current_slide_data['index']-1
slide = slides[current_slide_index]
if slide is not None:
# 调用run函, 填充PowerPoint模板
run(slide, current_slide_data['content'])
slide_run_arr.append(current_slide_index) # 缓存加载过的slide
else:
continue
# 判断是否是最后一张slide, 最后一张slide保存文件
is_save = slide_index==data_len # 是否保存文件
# 根据slide_index_arr和slide_run_arr, 找到需要删除的slide的index
slide_index_arr = range(len(prs.slides))
delete_index_arr = list(set(slide_index_arr) - set(slide_run_arr))
# 根据delete_index_arr删除slide
if delete_index_arr is not None:
delete_slide(prs, delete_index_arr)
# 保存文件
if is_save:
save_file(prs, output_file)
# download_href = save_file(prs, output_file)
download_href = 'download/' + output_file
else:
print("返回的 JSON 数据中 code 不等于 1, 处理请求异常")
else:
print("获取到的 JSON 数据不完整或格式不正确, 无法继续处理")
return download_href
# 初始化配置值
def init_config():
config = configparser.ConfigParser()
config.read('config.ini')
theme_file = config.get('presentation', 'theme_file')
global MOCK_CONFIG
global HTML_CONFIG
# 读取mock配置项的值
MOCK_CONFIG = {
'host': config.get('mock','host'),
'port': config.getint('mock','port')
}
# 读取运行的html配置项的值
HTML_CONFIG = {
'host': config.get('html','host'),
'port': config.getint('html','port')
}
return config, theme_file
# 单文件测试用: 主程序入口
if __name__ == "__main__":
config, theme_file = init_config()
# init_data() # 如果想直接运行此文件时,请把这行注释去掉
# 接口: 外部文件调用
def output_pptx(json_data):
# 读取母板文件和填充数据
return init_data(False, json_data)
utils.py
# 导入requests
import requests
import socket
import http.server
import re
import os
from PIL import Image
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE, MSO_SHAPE_TYPE
from pptx.enum.text import MSO_ANCHOR, MSO_AUTO_SIZE, PP_ALIGN
from pptx.dml.color import RGBColor
from pptx.chart.data import CategoryChartData
# 导入Inches
from pptx.util import Inches, Pt, Cm
# 以下为工具类函数, 用于处理PPTX文件, 如获取图片尺寸, 下载图片等, 用于给其他页面提供服务
def get_slide_size(slide):
"""
获取PPTX文件中某个slide的大小, 返回一个元组(width, height)
"""
width = slide.shapes.placeholders[0].width
height = slide.shapes.placeholders[0].height
return (width, height)
# 点数转英寸函数
def points_to_inches(points, dpi=96):
# dpi = 96 / 72 # 96 PPI, 72 points per inch
inches = points / dpi
return inches
def pixels_to_inches(pixels, dpi=96):
inches = pixels / dpi
return inches
# 获取当前打开浏览器的ip地址和端口号
def get_http_port():
# 获取http端口号
http_port = http.server.HTTPServer.server_address[1]
return http_port
def get_https_port():
# 获取https端口号
https_port = http.server.HTTPServer.server_address[1]
return https_port
def get_http_and_ip_address():
# 获取当前打开浏览器的ip地址和端口号
ip = socket.gethostbyname(socket.gethostname())
http_port = 5000
if ip == "127.0.0.1" or ip == "localhost":
ip = "http://localhost"
# http_port = get_http_port()
print("当前浏览器的IP地址为:", ip, " HTTP端口号为:", http_port)
return ip, http_port
def inbountTagname(tagname):
'''
入选节点:
HTML 中包含各种类型的标签或元素节点, 其中一些常见的包括:
<p>: 段落元素
<a>: 锚元素
<img>: 图像元素
<ul>: 无序列表元素
<ol>: 有序列表元素
<li>: 列表项元素
<h1> - <h6>: 标题元素
<div>: 分隔元素
<span>: 行内容器元素
<input>: 输入元素
<textarea>: 文本域元素
<button>: 按钮元素
<label>: 标签元素
<form>: 表单元素
<table>: 表格元素
<tr>: 表格行元素
<td>: 表格数据元素
<th>: 表头元素
<strong>: 强调元素
<em>: 强调元素
<blockquote>: 块引用元素
<iframe>: 内联框架元素
<article>: 文章元素
<section>: 区段元素
<nav>: 导航元素
<footer>: 页脚元素
<header>: 页眉元素
这些只是一些常见的示例, HTML 提供了丰富的标签和元素, 以便开发人员可以创建丰富多样的网页内容。
入选节点:
<p>: 段落元素
<a>: 锚元素
<img>: 图像元素
<ul>: 无序列表元素
<ol>: 有序列表元素
<li>: 列表项元素
<h1> - <h6>: 标题元素
<div>: 分隔元素
<span>: 行内容器元素
<textarea>: 文本域元素
<label>: 标签元素
<form>: 表单元素
<table>: 表格元素
<strong>: 强调元素
<em>: 强调元素
<blockquote>: 块引用元素
<iframe>: 内联框架元素
<article>: 文章元素
<section>: 区段元素
<nav>: 导航元素
<main>: 主内容元素
<footer>: 页脚元素
<header>: 页眉元素
提取上面标签内的名称
arr = [
'p', 'a', 'img', 'ul', 'ol', 'li', 'h1', 'div', 'span', 'textarea',
'label', 'form', 'table', 'strong', 'em', 'blockquote', 'iframe',
'article', 'section', 'nav', 'main', 'footer', 'header'
]
'''
inboundTagname = [
'p',
'a',
'img',
'ul',
'ol',
'li',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'b', 'i', 'u', 's', 'strike', 'del',
'div',
'span',
'textarea',
'label',
'form',
'table',
'strong',
'em',
'blockquote',
'article',
'section',
'nav',
'main',
'footer',
'header',
'canvas',
'iframe',
'video',
'audio',
]
# 判断是否为入选节点
if tagname in inboundTagname:
return True
else:
return False
def get_img_size(imgObject):
width = imgObject.width
height = imgObject.height
return (width, height)
# 设置文字内容
def set_text(slide, shape, content):
text_frame = shape.text_frame
for paragraph in text_frame.paragraphs:
for run in paragraph.runs:
# 判断run.text是否以$开头, 如是则替换成content中的值
# if run.text.startswith('$'):
if re.match(r'\$\w+', run.text):
for item in content:
# print('[没有占位符] run.text = ', run.text, ', item[type] = ',item['type'], ', item[name] = ', item['name'], ', item[value] = ', item['value'])
if '$'+item['name']==run.text:
if item['type']=='title' or item['type']=='subtitle' or item['type']=='text':
run.text = item['value'] # +'(ok)'
if item['type']=='picture':
run.text = ''
set_image(slide, shape, item['value'])
break
# 在slide中插入图片, 尺寸为shape的大小, 图片路径为img_path
def set_image(slide, shape, picture):
# 判断当前picture是否为URL图片地址
is_url = picture.startswith('http')
img = None
# 加载本地图
img_file = 'new_img.png'
if is_url:
img_data = requests.get(picture).content
img = Image.open(BytesIO(img_data))
# # 下载URL图片到本地 'https://www.cofco.com/favicon.ico'
# img_data = requests.get(picture).content
# # 保存图片
# with open(img_file, 'wb') as handler:
# handler.write(img_data)
# # 打开图片
# img = Image.open(img_file)
else:
folder_path = 'static/pictures/'
img_path = picture
local_img_path = os.path.join(folder_path, img_path)
img = Image.open(local_img_path)
# img_path = 'static/pictures/'+picture
# # 根据当前文件路径
# folder_path = os.path.dirname(os.path.abspath(__file__))
# # 获取本地图片路径
# local_img_path = os.path.join(folder_path, img_path)
# # 加载本地图片
# img = Image.open(local_img_path)
# # 保存图片
# img.save(img_file)
# # 打开图片
# img = Image.open(img_file)
# 保存到PPT
# slide.shapes.add_picture(img_file, left=shape.left, top=shape.top, width=shape.width, height=shape.height)
with open('static/pictures/slide_img.png', 'rb') as img_file:
slide.shapes.add_picture(img_file, left=shape.left, top=shape.top, width=shape.width, height=shape.height)
# 删除临时图片
# os.remove(img_file)
shape.text = ''
def set_table(shape, content, slide):
data_headers = content['headers']
data_rows = content['rows']
_rows_len = len(data_rows)
_table_rows_len = 0
def _setValue(cell, value):
try:
text_frame = cell.text_frame
for paragraph in text_frame.paragraphs:
if paragraph.runs is not None and len(paragraph.runs) > 0:
for run in paragraph.runs:
run.text = str(value)
else:
cell.text = str(value)
except Exception as e:
logging.error('Error in set_table -> _setValue: %s', e)
try:
_table = shape.table
_table_rows_len = len(_table.rows)
# 追加_rows_len-1行(第一行为需要继承的行样式,类似占位符,但不是pptx的占位符)
for i in range(_rows_len-1):
# 使用_table.rows.add(-1)方法在最后一行插入新行。 -1表示在最后一行插入新行。
_table.rows.add(-1)
# 根据data_headers, 设置每列的 width
for i, data_header in enumerate(data_headers):
_cell = _table.cell(0, i)
if _cell is not None:
_setValue(_cell, data_header)
# 根据data_rows 设置每个单元格的文本
for i, row in enumerate(data_rows):
for j, cell in enumerate(row):
_cell = _table.cell(i+1, j)
if _cell is not None:
_setValue(_cell, cell)
except Exception as e:
logging.error('Error in set_table: %s', e)
def set_chart(shape, content):
categories = []
series = []
title = ''
for c in content:
if c['type'] == 'chart':
categories = c['value']['categories']
series = c['value']['series']
title = c['value']['title']
break
chart = shape.chart
if chart is not None:
for p in chart.plots:
if chart.has_title == True:
shape.chart.chart_title.text_frame.text = title
# https://python-pptx.readthedocs.io/en/stable/api/chart-data.html#pptx.chart.data.CategoryChartData
ccd = CategoryChartData(chart.chart_type)
for c in categories:
ccd.add_category(c)
for s in series:
ccd.add_series(s['name'], s['data'])
# https://python-pptx.readthedocs.io/en/stable/api/chart.html?highlight=replace_data#pptx.chart.chart.Chart.replace_data
chart.replace_data(ccd)
shape.text = ''
# 设置文字内容 slide, theme_shapes, data)
def add_text(prs, slide, theme_shape, data, left=Inches(1), top=Inches(1)):
d_name = data['name']
d_value = data['value']
d_type = data['type']
# width = Inches(2) # Inches(0.1 * len(d_value))
width = prs.slide_width
height = Inches(0.7)
if theme_shape.text_frame is not None:
if theme_shape.height is not None:
height = theme_shape.width # 使用主题模板文件里配置的height
# print('theme_shape.height = ', height, ', top = ', top)
if top == 0:
top = Inches(0)
else:
top = top + height
max_len = 26
if len(d_value)>max_len:
# print(len(d_value), '~~~')
text_len_per_line = max_len
text = d_value
chunk_size = max_len
# 计算需要分割的次数
num_chunks = (len(text) + chunk_size - 1) // chunk_size
# 分割字符串并添加换行符
d_value = '\n'.join(text[i*chunk_size:(i+1)*chunk_size] for i in range(num_chunks))
if num_chunks>1:
d_value = d_value.strip()
print('计算需要分割的次数 num_chunks = ', num_chunks , ', d_value = ', d_value)
height = height * num_chunks
txBox = slide.shapes.add_textbox(left, top, width, height)
tf = txBox.text_frame.paragraphs[0]
# p = tf.add_paragraph()
# tf.text = d_value
run = tf.add_run()
# p.text = d_value
if theme_shape.has_text_frame:
theme_p = theme_shape.text_frame.paragraphs[0]
if theme_p is not None:
theme_shape_paragraph_runs0 = theme_p.runs[0]
if theme_shape_paragraph_runs0 is not None:
theme_font = theme_shape_paragraph_runs0.font
if theme_font is not None:
# add runs to set fonts
# r = p.add_run()
run.font.bold = theme_font.bold
run.font.size = theme_font.size
run.font.italic = theme_font.italic
run.font.underline = theme_font.underline
run.font.color.rgb = theme_font.color.rgb
run.font.name = theme_font.name
tf.alignment = theme_p.alignment or PP_ALIGN.LEFT
tf.line_spacing = theme_p.line_spacing or 1.5
# print('add_text (top) = ', (top), ', theme_p.alignment = ',theme_p.alignment)
run.text = d_value
# print('name = ', d_name, ', type = ', d_type, ', value = ', d_value + ', left = ', left, ', top = ', top, ', width = ', width, ', height = ', height, ', len(value) = ', len(d_value))
def add_picture(prs, slide, theme_shape, data, left=0, top=0):
left = 0
top = 0
width = Inches(2.0)
height = Inches(1.0)
if theme_shape.text_frame is not None:
if theme_shape.height is not None:
width = theme_shape.width # 使用主题模板文件里配置的width
height = theme_shape.height # 使用主题模板文件里配置的height
# shape.insert_picture(bz['img'])
# MSO_SHAPE From: https://python-pptx.readthedocs.io/en/latest/api/enum/MsoAutoShapeType.html#msoautoshapetype
# new_shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height)
# add_textbox From: https://python-pptx.readthedocs.io/en/latest/api/shapes.html
image_file = data['value']
img_data = requests.get(image_file).content
img = Image.open(BytesIO(img_data))
with open('static/pictures/slide_img.png', 'rb') as img_file:
slide.shapes.add_picture(img_file, left=shape.left, top=shape.top, width=shape.width, height=shape.height)
# slide.shapes.add_picture(image_file, left, top, width=None, height=None)
def add_table(slide, theme_shapes, data, left=0, top=0):
left = 0
top = 0
width = Inches(2.0)
height = Inches(1.0)
cols = data['value']['headers']
rows = data['value']['rows']
# MSO_SHAPE From: https://python-pptx.readthedocs.io/en/latest/api/enum/MsoAutoShapeType.html#msoautoshapetype
new_shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height)
# shape.table ???
# add_textbox From: https://python-pptx.readthedocs.io/en/latest/api/shapes.html
new_shape.add_table(rows, cols, left, top, width, height)
def add_chart(slide, theme_shapes, data, left=0, top=0):
left = 0
top = 0
width = Inches(2.0)
height = Inches(1.0)
# MSO_SHAPE From: https://python-pptx.readthedocs.io/en/latest/api/enum/MsoAutoShapeType.html#msoautoshapetype
new_shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height)
# add_textbox From: https://python-pptx.readthedocs.io/en/latest/api/shapes.html
new_shape.add_chart(chart_type, left, top, width, height)
# 在slide中插入图片, 尺寸为shape的大小, 图片路径为img_path
def set_image0(slide, shape, picture):
# 判断当前picture是否为URL图片地址
is_url = picture.startswith('http')
img = None
# 加载本地图
img_file = 'new_img.png'
if is_url:
img_data = requests.get(picture).content
img = Image.open(BytesIO(img_data))
# # 下载URL图片到本地 'https://www.cofco.com/favicon.ico'
# img_data = requests.get(picture).content
# # 保存图片
# with open(img_file, 'wb') as handler:
# handler.write(img_data)
# # 打开图片
# img = Image.open(img_file)
else:
folder_path = 'static/pictures/'
img_path = picture
local_img_path = os.path.join(folder_path, img_path)
img = Image.open(local_img_path)
# img_path = 'static/pictures/'+picture
# # 根据当前文件路径
# folder_path = os.path.dirname(os.path.abspath(__file__))
# # 获取本地图片路径
# local_img_path = os.path.join(folder_path, img_path)
# # 加载本地图片
# img = Image.open(local_img_path)
# # 保存图片
# img.save(img_file)
# # 打开图片
# img = Image.open(img_file)
# 保存到PPT
# slide.shapes.add_picture(img_file, left=shape.left, top=shape.top, width=shape.width, height=shape.height)
with open('static/pictures/slide_img.png', 'rb') as img_file:
slide.shapes.add_picture(img_file, left=shape.left, top=shape.top, width=shape.width, height=shape.height)
# 删除临时图片
# os.remove(img_file)
shape.text = ''
def set_table0(shape, content, slide):
data_headers = content['headers']
data_rows = content['rows']
_rows_len = len(data_rows)
_table_rows_len = 0
def _setValue(cell, value):
try:
text_frame = cell.text_frame
for paragraph in text_frame.paragraphs:
if paragraph.runs is not None and len(paragraph.runs) > 0:
for run in paragraph.runs:
run.text = str(value)
else:
cell.text = str(value)
except Exception as e:
logging.error('Error in set_table -> _setValue: %s', e)
try:
_table = shape.table
_table_rows_len = len(_table.rows)
# 追加_rows_len-1行(第一行为需要继承的行样式,类似占位符,但不是pptx的占位符)
for i in range(_rows_len-1):
# 使用_table.rows.add(-1)方法在最后一行插入新行。 -1表示在最后一行插入新行。
_table.rows.add(-1)
# 根据data_headers, 设置每列的 width
for i, data_header in enumerate(data_headers):
_cell = _table.cell(0, i)
if _cell is not None:
_setValue(_cell, data_header)
# 根据data_rows 设置每个单元格的文本
for i, row in enumerate(data_rows):
for j, cell in enumerate(row):
_cell = _table.cell(i+1, j)
if _cell is not None:
_setValue(_cell, cell)
except Exception as e:
logging.error('Error in set_table: %s', e)
def set_chart0(shape, content):
categories = []
series = []
title = ''
for c in content:
if c['type'] == 'chart':
categories = c['value']['categories']
series = c['value']['series']
title = c['value']['title']
break
chart = shape.chart
if chart is not None:
for p in chart.plots:
if chart.has_title == True:
shape.chart.chart_title.text_frame.text = title
# https://python-pptx.readthedocs.io/en/stable/api/chart-data.html#pptx.chart.data.CategoryChartData
ccd = CategoryChartData(chart.chart_type)
for c in categories:
ccd.add_category(c)
for s in series:
ccd.add_series(s['name'], s['data'])
# https://python-pptx.readthedocs.io/en/stable/api/chart.html?highlight=replace_data#pptx.chart.chart.Chart.replace_data
chart.replace_data(ccd)
shape.text = ''
# 添加chart, 详情见:https://python-pptx.readthedocs.io/en/latest/user/charts.html
def add_chart0(slide, categories=['East', 'West', 'Midwest'], series=[{"name":"series1","data":[10, 20, 30]}]):
type = 'simple'
if type =='simple':
# define chart data ---------------------
chart_data = CategoryChartData()
chart_data.categories = categories or ['East', 'West', 'Midwest']
for series_item in series:
chart_data.add_series(series_item['name'], series_item['data'])
# chart_data.add_series('Series 2', (12.8, 17.2, 20.4))
# add chart to slide --------------------
x, y, cx, cy = Inches(2), Inches(2), Inches(6), Inches(4.5)
slide.shapes.add_chart(
XL_CHART_TYPE.COLUMN_CLUSTERED, x, y, cx, cy, chart_data
)
else:
chart_data = ChartData()
chart_data.categories = ['East', 'West', 'Midwest']
chart_data.add_series('Q1 Sales', (19.2, 21.4, 16.7))
chart_data.add_series('Q2 Sales', (22.3, 28.6, 15.2))
chart_data.add_series('Q3 Sales', (20.4, 26.3, 14.2))
graphic_frame = slide.shapes.add_chart(
XL_CHART_TYPE.COLUMN_CLUSTERED, x, y, cx, cy, chart_data
)
chart = graphic_frame.chart
mock.py
from flask import Flask, jsonify, request, send_from_directory
from flask_cors import CORS
# 导入index.py里的函数
from index import *
from lxml_ai import *
# from utils import *
# 读取配置文件
import configparser
config = configparser.ConfigParser()
config.read('config.ini')
host = config.get('mock', 'host')
IP = host
TEST_IMAGE_URL1 = "http://via.placeholder.com/50x50/"
TEST_IMAGE_URL2 = "http://via.placeholder.com/45x45/"
mock_data = {
"code": 1,
"data": [
{
"index": 1,
"name": "slide1",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "XXX团有限公司" },
{ "type": "text", "name": "text2", "value": "XXX理信息化系统建设项目" },
{ "type": "text", "name": "text3", "value": "项目周报(W2)" },
{ "type": "text", "name": "text4", "value": "05月17日-05月23日(上周五-本周四)" },
{ "type": "text", "name": "text5", "value": "更新时间:2023/5" },
{ "type": "text", "name": "text6", "value": "XXX理XXX系XXX项目组" },
],
},
{
"index": 2,
"name": "slide2",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "项目XXX展情况" },
{ "type": "text", "name": "text2", "value": "本周XXX成情况及成果" },
{ "type": "text", "name": "text3", "value": "下周XXX进计划" },
{ "type": "text", "name": "text4", "value": "XXX重点待跟进事项" },
]
},
{
"index": 3,
"name": "slide3",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "项XXX进展情况" },
{ "type": "text", "name": "text2", "value": "01" }
]
},
{
"index": 4,
"name": "slide4",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "XXX体进展情况" },
{ "type": "text", "name": "text2", "value": "具体XXX划如下:" },
{ "type": "text", "name": "text3", "value": "投资XXX要设有4个主要里程碑:XXX析、XXX计、XXX线、XXX验收" },
{ "type": "text", "name": "text4", "value": "正常" },
{ "type": "text", "name": "text5", "value": "XXX段" },
{ "type": "picture", "name": "picture1", "value": "slide_img.png" }
]
},
{
"index": 5,
"name": "slide5",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "本周XXX成情况" },
{ "type": "text", "name": "text2", "value": "5.17-5.23(XXX-本周四)" },
]
},
{
"index": 6,
"name": "slide6",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "本XXX完成情况(1/1)" },
{
"type": "table",
"name": "table1",
"value": {
"headers": ["序号", "工作内容", "业务\n责任人", "信息\n责任人", "开始时间", "结束时间", "工作状态", "备注"],
"rows": [
["1", "XXXXXX系统项目启动会", "XXX", "XXX\nXXX", "2024/5/17", "2024/5/23", "已完成", ""],
["2", "XXX务推进:XXX同和XXX术附件准备", "XXX", "XX\nXXX", "2024/5/17", "2024/5/23", "已完成", "完成定稿,已提交XX集团对接人审核"],
["3", "业务调研:\nXX集团-XXX-XXX理部\nXX集团-XX部-XX并购部\nXX集团-XX部\nXX化公司-XXX脂\nXXX谷\nXXXXX", "XXX", "XXX\nXXX", "2024/5/21", "2024/5/23", "已完成", "已全部完成XXX梳理调研纪要"],
["4", "功能XXX务分解", "-", "XXX", "2024/5/21", "2024/5/23", "进行中", ""]
]
}
},
]
},
{
"index": 7,
"name": "slide7",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "XXX作成果" },
{ "type": "text", "name": "text2", "value": "5.17-5.23(上周五-本周四)" },
]
},
{
"index": 8,
"name": "slide8",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "业务XXX汇总" },
{ "type": "text", "name": "text2", "value": "启动会圆满结束" },
{ "type": "text", "name": "text3", "value": "完成集团XXXXX、XXXXX、投XXX部、XXX管理部、XXX、XXX谷总部、XXX脂总部7个部门业XXX,XXX7份调研纪要。" },
{ "type": "text", "name": "text4", "value": "XXX段已经成功完成,按照计划和XXX续推进项目。" },
{ "type": "picture", "name": "picture1", "value": "8-1.png" },
{ "type": "picture", "name": "picture2", "value": "8-2.png" }
]
},
{
"index": 9,
"name": "slide9",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "下XXX推进计划" },
{ "type": "text", "name": "text2", "value": "5.17-5.23(本周五-下周四)" }
]
},
{
"index": 10,
"name": "slide10",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "下XXX推进计划(1/1)" },
{
"type": "table",
"name": "table1",
"value": {
"headers": ["序号", "工作内容", "XX\n责任人", "XXX科\n责任人", "开始时间", "结束时间", "工作状态", "备注"],
"rows": [
[
"1",
"XXX略部-产XXX部\n集XXX部-政策XXX\n集XXX资源部\n集XXX部\n集XXX室\n集XXX部-财XXX\n集XXX部-产XXX业务\nXXX务部-XXX务\n集XXX部-运XXX\n集XXX安全XXX\nXXX检组\nXXX外合作部\nXXX技创新部\n",
"XXX",
"XXX\nXXX",
"2024/5/24",
"2024/5/30",
"未启动",
""
],
[
"2",
"集XXX:\nXXX份认证系统\nXXX系统\nXXX部署\nXXX章\n协同XXX统\nXXX\n",
"-",
"XXX",
"2024/5/24",
"2024/5/30",
"XXX",
""
]
]
}
},
]
},
{
"index": 11,
"name": "slide11",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "XXX重点待XXX项" },
{ "type": "text", "name": "text2", "value": "5.17-5.23(本周五-下周四)" }
]
},
{
"index": 12,
"name": "slide12",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "XXX重点XXX事项" },
{
"type": "table",
"name": "table1",
"value": {
"headers": ["序号", "风险XXX跟进事项", "XXX", "开始时间", "结束时间", "工作状态"],
"rows": [
["1", "暂无", "", "", "", ""],
["2", "", "", "", "", ""],
["3", "", "", "", "", ""],
["4", "", "", "", "", ""]
]
}
},
]
},
{
"index": 13,
"name": "slide13",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "图表测试" },
{
"type": "chart",
"name": "chart1",
"value": {
"title": "XXX量1",
"categories": ["大豆", "玉米", "小麦"],
"series": [
{"type":"bar", "name":"大豆", "data":[10, 20, 30]},
{"type":"bar", "name":"玉米", "data":[15, 25, 35]},
{"type":"bar", "name":"小麦", "data":[5, 10, 15]},
]
}
}
]
},
{
"index": 14,
"name": "slide14",
"type": "slide",
"content": [
{ "type": "text", "name": "text1", "value": "感谢您的参与!" },
]
}
]
}
mock_data1 = {}
mock_data2 = {}
DATA = {
"0": mock_data,
"1": mock_data1,
"2": mock_data2
}
app = Flask(__name__)
# CORS(app) # 启用CORS扩展
# CORS(app, resources={r"/*": {
# "origins": "*",
# "methods": ["GET", "POST"],
# "supports_credentials": True,
# "access_control_allow_origin": "*",
# "allow_headers": ["Content-Type", "Authorization"],
# "expose_headers": ["Content-Type", "Authorization"],
# "max_age": 60 * 60 * 24 * 2,
# }})
# 模拟一些初始数据
data = []
# 处理GET请求
@app.route('/api/pptx', methods=['GET'])
def get_data():
return jsonify(DATA['0'])
@app.route('/api/get_pptx_index2', methods=['GET'])
def get_pptx_index2():
return jsonify(DATA['2'])
# 处理POST请求, 接收前端传来的json数据
@app.route('/api/output', methods=['POST'])
def output():
new_data = request.json
new_data = {
"code": 1,
"data": new_data,
"download_href": ""
}
# print('[mock.py - /api/output] new_data = ', new_data)
download_href = output_pptx(new_data)
# print('[mock.py - /api/output] download_href = ', download_href)
if download_href is not None:
new_data["download_href"] = download_href
data.append(new_data)
# print('new_data:', new_data)
return jsonify(new_data), 201
# http://xxx.xxx.xxx.xxx:5000/api/output_xml
# 处理POST请求, 接收前端传来的json数据
@app.route('/output_xml', methods=['POST'])
def output_xml():
new_data = request.json
new_data = {
"code": 1,
"data": new_data,
"download_href": ""
}
d = request.json['data']
download_href = output_pptx_xml(d) # output_pptx_html(new_data)
# print('download_href = ', download_href)
if download_href is not None:
new_data["download_href"] = download_href
data.append(new_data)
print('new_data:', new_data)
return jsonify(new_data), 201
@app.route('/download/<filename>')
def download_file(filename):
return send_from_directory('static/pptx', filename, as_attachment=True)
# 设置静态文件的目录
app.static_folder = 'static'
# 设置模板文件的目录
app.template_folder = 'templates'
if __name__ == '__main__':
# 指定flash启动ip和端口
app.run(host=host, port=5000, debug=True)
# app.run(port=5000, debug=True)
config.ini
[database]
host = localhost
part = 3000
username = root
password = 12345678
[user]
name = taopeng
email = 87694121@qq.com
[mock]
host1 = localhost
host = 10.8.47.12
port = 5000
[html]
host = 10.8.47.12
port = 8080
[presentation]
theme_file = master/theme1.pptx
[slide]
background = #FFFFFF
注:记得在config.ini文件中把10.8.47.12替换成你自己的IP