Django结合Vue实现前端页面导出为PDF
测试环境
Win 10
Python 3.5.4
Django-2.0.13.tar.gz
官方下载地址:
https://www.djangoproject.com/download/2.0.13/tarball/
pdfkit-0.6.1.tar.gz
下载地址:
https://pypi.org/project/pdfkit/
django REST framework-3.9.4
下载地址:
https://github.com/encode/django-rest-framework
wkhtmltox_v0.12.5.zip
下载地址:
https://wkhtmltopdf.org/downloads.html
https://downloads.wkhtmltopdf.org/0.12/0.12.5/wkhtmltox-0.12.5-1.msvc2015-win64.exe
axios 0.18.0
echarts 4.2.1
element-ui: 2.8.2
Vue 3.1.0
需求描述
如下,要将一个包含echarts图表,elementUI table的测试报告页面导出为PDF文档,页面包含以下类型的元素
解决方案
最开始采用“html2canvas和jsPDF”直接前端导出,发现存在问题,只能导出可视区内容,并且是类似截图一样的效果,无法获取翻页数据,然后考虑后台导出,前端通过js获取报告容器元素innerHtml,传递给后台,后台根据这个html元素导出为pdf,发现还是存在问题,echarts图片无法导出,另外,翻页组件等也会被导出,还有就是表格翻页数据无法获取,页面样式缺失等。
最终解决方案:
后台编写好html模板(包含用到的样式、样式链接等),收到请求时读取该模板文件为html文本。从数据库读取前端用到的表格数据,然后替换至模板中对应位置的模板变量;通过echars api先由 js把echarts图表转为base64编码数据,然后随其它导出文件必要参数信息发送到后台,后台接收后转base64编码为图片,然后替换模板中对应的模板变量,这样以后,通过pdfkit类库把模板html文本导出为pdf。最后,删除生成的图片,并且把pdf以blob数据类型返回给前端,供前端下载。
pdfkit api使用简介
基础用法
import pdfkit
pdfkit.from_url('https://www.w3school.com.cn, 'out.pdf')
pdfkit.from_file('test.html', 'out.pdf')
pdfkit.from_string('Hello!', 'out.pdf')
可以通过传递多个url、文件来生成pdf文件:
pdfkit.from_url(['https://www.w3school.com.cn', 'www.cnblogs.com'], 'out.pdf')
如上,将会把访问两个网站后打开的内容按网站在list中的顺序,写入out.pdf,也可以不带https://、http://,如下
pdfkit.from_url(['www.w3school.com.cn', 'www.cnblogs.com'], 'out.pdf')
pdfkit.from_file(['file1.html', 'file2.html'], 'out.pdf')
可以通过打开的文件来生成PDF
with open('file.html') as f:
pdfkit.from_file(f, 'out.pdf')
也可以不输出到文件,直接保存到内存中,以便后续处理
pdf = pdfkit.from_url('www.w3school.com.cn ', False)
默认的,pdfkit会显示所有wkhtmltopdf的输出,可以通过添加options参数,并设置quiet的值(quiet除外,还有很多其他选项可设置,具体参考官方文档),如下::
options = {
'quiet': ''
}
pdfkit.from_url('https://www.w3school.com.cn, 'out.pdf', options=options)
此外还可以为要生成的pdf添加css样式,特别适合css样式采用“外联样式”的目标对象。
#单个CSS样式文件
css = 'example.css'
pdfkit.from_file('file.html', options=options, css=css)
# 多个css样式
css = ['example.css', 'example2.css']
pdfkit.from_file('file.html', options=options, css=css)
添加configuration参数,如下,指定wkhtmltopdf安装路径
config = pdfkit.configuration(wkhtmltopdf='/opt/bin/wkhtmltopdf')
pdfkit.from_string(html_string, output_file, configuration=config)
更多详情参考官方文档
https://pypi.org/project/pdfkit/
实现步骤
1.安装wkhtmltox
安装完成后,找到安装目录下wkhtmltopdf.exe所在路径(例中为D:\Program Files\wkhtmltopdf\bin\wkhtmlpdf.exe),添加到系统环境变量path中(实践时发现,即便是配置了环境变量,运行时也会报错:提示:No wkhtmltopdf executable found: "b''"
解决方案:
如下,生成pdf前指定wkhtmltopdf.exe路径
config = pdfkit.configuration(wkhtmltopdf='/opt/bin/wkhtmltopdf')
pdfkit.from_string(html_string, output_file, configuration=config)
2.安装pdfkit
3.前端请求下载报告
仅保留关键代码
<script>
export default {
return {
echartPicIdDict: {}, // 存放echart图表ID 数据格式为: {" echartPicUniqueName":"echartPicUUID" },比如 {"doughnut-pie-chart":"xdfasfafafadfafafafafdasf" } // 创建Echarts图表时需要指定一个id,例中创建每个echart图表时,都会生成一个UUID作为该echart图表的id,并且会把该UUID保存到this.echartPicIdDict。
reportId: "", // 存放用户所选择的测试报告ID
...略
}
},
methods: {
...略
// 下载报告
downloadSprintTestReport() {
try {
...略
let echartBase64Info = {}; // 存放通过getDataURL获取的echarts图表base64编码信息
// 获取echart图表base64编码后的数据信息
for (let key in this.echartPicIdDict) {
// let echartObj = this.$echarts.getInstanceById(this.echartPicIdDict[key]); // 结果 echartObj=undefined
let echartDomObj = document.getElementById(this.echartPicIdDict[key]);
if (echartDomObj) {
const picBase64Data = echartDomObj.getDataURL(); //返回数据格式:data:image/png;base64,base64编码数据
echartBase64Info[key] = picBase64Data;
}
}
}
// 发送下载报告请求
downloadSprintTestReportRequest({
reportId: this.reportInfo.id,
sprintId: this.reportInfo.sprintId,
...略
echartBase64Info: echartBase64Info
})
.then(res => {
let link = document.createElement("a");
let blob = new Blob([res.data], {
type: res.headers["content-type"]
});
link.style.display = "none";
link.href = window.URL.createObjectURL(blob);
// 下载文件名无法通过后台响应获取,因为获取不到Content-Disposition响应头
link.setAttribute("download", this.reportInfo.title + ".pdf");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch(res => {
if (
Object.prototype.toString.call(res.response.data) ==
"[object Blob]"
) {
let reader = new FileReader();
reader.onload = e => {
let responseData = JSON.parse(e.target.result);
if (responseData.msg) {
this.$message.error(
res.msg || res.message + ":" + responseData.msg
);
} else {
this.$message.error(
res.msg || res.message + ":" + responseData.detail
);
}
};
reader.readAsText(res.response.data);
} else {
this.$message.error(res.msg || res.message);
}
});
} catch (err) {
this.$message.error(res.message);
}
},
}
</script>
4、 后端编写模板
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8" />
<!-- elementUI -->
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css" />
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<style>
...略
.plan-info {
border-width: 1px;
border-style: solid;
background: rgba(241, 239, 239, 0.438);
border-color: rgb(204, 206, 206);
}
.plan-info .plan-info-table-td {
text-align: center;
padding-top: 3px;
padding-bottom: 3px;
font-size: 14px;
}
.plan-info .plan-info-table-td-div {
display: inline;
}
...略
</style>
</head>
<body>
...略
<div class="sprint-test-report-detail">
<span style="font-weight: bold;">测试计划:</span>
<div class="plan-info">
<table>
<thead>
<tr>
<th style="border: none; width: 6%; height: 0px;">ID</th>
<th style="border: none; width: 20%; height: 0px;">计划名称</th>
<th style="border: none; width: 10%; height: 0px;">预估开始日期</th>
<th style="border: none; width: 10%; height: 0px;">实际开始时间</th>
<th style="border: none; width: 10%; height: 0px;">预估完成日期</th>
<th style="border: none; width: 10%; height: 0px;">实际完成时间</th>
<th style="border: none; width: 25%; height: 0px;">关联组别</th>
<th style="border: none; width: 9%; height: 0px;">测试环境</th>
</tr>
</thead>
<tbody>
${relatedPlans}
</tbody>
</table>
</div>
</div>
<div class="sprint-test-report-detail">
<span style="font-weight: bold;">测试范围:</span>
<div>
<span>${test_scope}</span>
</div>
</div>
<div class="sprint-test-report-detail">
<span style="font-weight: bold;">测试统计</span>
<div>
<div>
<img src="${defect_status_pie}" />
</div>
...略
</div>
...略
</div>
</body>
</html>