Django 结合Vue实现前端页面导出为PDF
Django结合Vue实现前端页面导出为PDF
by:授客 QQ:1033553122
测试环境
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>
注意:html中需要在head元素中添加<meta charset="UTF-8">,以防生成的pdf中文乱码,另外,确保系统中有中文字体,否则也会出现乱码,如下:
5、 后端接口
仅保留关键代码
#!/usr/bin/env python
# -*- coding:utf-8 -*-
__author__ = '授客'
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from backend.models import SprintTestReport
from django.utils import timezone
from django.http import FileResponse
from django.conf import settings
import pdfkit
import json
import base64
import uuid
import os
import logging
logger = logging.getLogger('mylogger')
class SprintTestreportPDFAPIView(APIView):
'''迭代测试报告pdf文件下载'''
@staticmethod
def convert_related_plans_to_html(self, related_plans):
'''转换报告相关联的测试计划数据格式为html格式数据,返回转换后的数据'''
result = ''
tr = '''<tr>
<td>
<div>{id}</div>
</td>
<td>
<div>{name}</div>
</td>
<td>
<div>{begin_time}</div>
</td>
<td>
<div>{start_time}</div>
</td>
<td>
<div>{end_time}</div>
</td>
<td>
<div>{finish_time}</div>
</td>
<td>
<div>{groups}</div>
</td>
<td>
<div>{environment}</div>
</td>
</tr>'''
for related_plan in related_plans:
result += tr.format(**related_plan)
return result
...略
def post(self, request, format=None):
'''下载pdf格式报告'''
result = {}
try:
data = request.data
report_id = data.get('report_id')
echart_base64_info_dict = data.get('echart_base64_info')
# 读取迭代测试报告html模板
report_html_str = '' # 存放html格式的迭代测试报告
current_dir, tail = os.path.split(os.path.abspath(__file__))
template_filepath = os.path.normpath(os.path.join(current_dir, 'sprint_test_report/sprint_test_report_template.html'))
with open(template_filepath, 'r', encoding='utf-8') as f:
for line in f:
report_html_str += line
# 读取报告数据
sprint_report = SprintTestReport.objects.filter(id=report_id)
if sprint_report.first():
try:
...略
report_data = sprint_report.values('title','introduction', 'related_plans', 'test_scope', 'individual_test_statistics', 'individual_dev_statistics', 'product_test_statistics', 'conclusion', 'suggestion', 'risk_analysis')[0]
# 替换测试计划
related_plans = json.loads(report_data['related_plans'])
related_plans = self.convert_related_plans_to_html(related_plans)
report_html_str = report_html_str.replace('${relatedPlans}', related_plans)
...略
# 生成echart图表图片
time_str = timezone.now().strftime('%Y%m%d')
uuid_time_str = str(uuid.uuid1()).replace('-', '') + time_str
file_name_dict = {}
for key, value in echart_base64_info_dict.items():
data_type, base64_data = value.split(',') # value 数据格式 data:image/png;base64,base64编码数据
file_suffix = '.' + data_type.split('/')[1].split(';')[0]
file_name = key + uuid_time_str + file_suffix
file_name_dict[key] = file_name
file_path = os.path.normpath(os.path.join(current_dir, 'sprint_test_report/%s' % file_name))
with open(file_path, 'wb') as f:
imgdata = base64.b64decode(base64_data)
f.write(imgdata)
# 替换 echart图表
for key in echart_base64_info_dict.keys():
# report_html_str = report_html_str.replace('${%s}' % key, '%s/sprint_test_report/%s' % (current_dir, file_name_dict[key])) # 注意,这里,迭代测试报告模板中的变量名称被设置为和key一样的值,所以可以这么操作
report_html_str = report_html_str.replace('${%s}' % key,os.path.normpath(os.path.join(current_dir, 'sprint_test_report/%s' % file_name_dict[key])))
# 生成pdf文档
time_str = timezone.now().strftime('%Y%m%d')
file_name = str(uuid.uuid1()).replace('-', '') + time_str + '.pdf'
config = pdfkit.configuration(wkhtmltopdf=settings.WKHTMLTOPDF)
file_dir = settings.MEDIA_ROOT + '/sprint/testreport'
options = {'dpi': 300, 'image-dpi':600, 'page-size':'A3', 'encoding':'UTF-8', 'page-width':'1903px'}
pdfkit.from_string(report_html_str, '%s/%s' % (file_dir, file_name), configuration=config, options=options)
file_absolute_path = '%s/%s' % (file_dir, file_name)
# 删除生成的图片文件
for key in echart_base64_info_dict.keys():
os.remove('%s/sprint_test_report/%s' % (current_dir, file_name_dict[key]))
# 返回数据给前端
if os.path.exists(file_absolute_path) and os.path.isfile(file_absolute_path):
file = open(file_absolute_path, 'rb')
file_response = FileResponse(file)
file_response['Content-Type']='application/octet-stream'
file_response['Content-Disposition']='attachment;filename={}.pdf'.format(report_data['title'] ) # 不知道为啥,前端获取不到请求头Content-Disposition
return file_response
else:
result['msg'] = '生成pdf报告失败'
result['success'] = False
return Response(result, status.HTTP_400_BAD_REQUEST)
except Exception as e:
result['msg'] = '%s' % e
result['success'] = False
return Response(result, status.HTTP_500_INTERNAL_SERVER_ERROR)
else:
result['msg'] = '生成迭代测试报告失败,报告不存在'
result['success'] = False
return Response(result, status.HTTP_400_BAD_REQUEST)
except Exception as e:
result['msg'] = '%s' % e
result['success'] = False
return Response(result, status.HTTP_500_INTERNAL_SERVER_ERROR)
导出效果(部分截图)
Django 结合Vue实现前端页面导出为PDF的更多相关文章
- 页面导出生成pdf,使用wkhtmltopdf第三方工具
把页面导出生成pdf,这里用到第三方的工具,使用方法中文文档没有找到,网上也没找到网友详细的神作.没有深入研究,所以也不赘述了,当然最基本的使用大多数也够用了,详细参数的官网也没介绍,大家使用的时候, ...
- rails应用页面导出为pdf文档
1.下载安装wkhtmltox https://wkhtmltopdf.org/downloads.html 2.gemfile添加 gem 'pdfkit' #页面导出pdf gem 'wkht ...
- Vue 页面导出成PDF文件
注意事项 如果导出的页面中设计到图片或者其他文件跨域文件,需要后端服务配合 安装依赖 npm install html2Canvas --save npm install jspdf--save 封装 ...
- vue将页面导出成pdf
npm i jspdf-html2canvas prinOut(){ // 导出pdf let page = document.querySelector('.app-main'); // page ...
- 页面导出为PDF
一.使用环境 Vue3.Quasar.Electron 二.安装 jspdf-html2canvas npm install jspdf-html2canvas --save 安装失败可以选择cnpm ...
- 使用pdf.js实现前端页面预览pdf文档,解决了跨域请求
pdf.js主要包含两个库文件,一个pdf.js和一个pdf.worker.js,,一个负责API解析,一个负责核心解析 官网地址:http://mozilla.github.io/pdf.js/ 下 ...
- Django分析之导出为PDF文件
最近在公司一直忙着做exe安装包,以及为程序添加新功能,好久没有继续来写关于Django的东西了….难得这个周末清闲,来了解了解Django的一些小功能也是极好的了~ 那今天就来看看在Django的视 ...
- 【jsPDF】jsPDF插件实现将html页面转换成PDF,并下载,支持分页
1.目的:在前段是 jQuery库 或者 VUE库 或者两者混合库,将html 页面和数据 转换成PDF格式并下载,支持分页 1.项目背景: 对客户报修记录进行分类统计,并生成各种饼图.柱状图.线性图 ...
- 前端实现html转pdf方法总结
最近要搞前端html转pdf的功能.折腾了两天,略有所收,踩了一些坑,所以做些记录,为后来的兄弟做些提示,也算是回馈社区.经过一番调(sou)研(suo)发现html导出pdf一般有这几种方式,各有各 ...
- 使用JavaScript将当前页面保存成PDF,支持图片和文字的保存
前端开发的朋友们可能会遇到这个需求:将您负责开发的网页的全部内容,包括文字和图片,一起保存成一个PDF文件.如果采用屏幕截图的话,默认Windows操作系统的截图按钮无法完整截取超过一屏幕的屏幕内容. ...
随机推荐
- springboot~封装依赖引用包jar还是pom,哪种更规范
将多个第三方包封装成一个项目后,如果你的目的是让其他开发人员可以直接引用这些依赖,一般来说有两种常见的方式: 打成JAR包:将封装好的项目编译打包成JAR文件,其他开发人员可以将这个JAR文件添加到他 ...
- 机器学习策略篇:详解超过人的表现(Surpassing human- level performance)
超过人的表现 讨论过机器学习进展,会在接近或者超越人类水平的时候变得越来越慢.举例谈谈为什么会这样. 假设有一个问题,一组人类专家充分讨论辩论之后,达到0.5%的错误率,单个人类专家错误率是1%,然后 ...
- Java中类的构造 与 方法的重载
类的构造方法 定义:构造方法与类名相同,且没有返回值,且不需要void修饰 Car bmcar = new Car(); 特点:类中没有定义时,会默认有一个无参的构造方法,在无参的构造方法中为成员变量 ...
- 通过Webpack搭建react
安装解析react的相关babel和插件 nmp i -D babel-loader @babel/core @babel/preset-react @babel/preset-env 进行loade ...
- 关于前三次pta的总结
前言 这三次pta难度在不断上升的同时,要求我们线上慕课+自主学习来了解更多的java中的各种方法,如:正则表达式 List Map等.与此同时要求我们展开尝试并熟练类的构造,类的引用,链表的基本运用 ...
- C# .NET 国密 SM2 签名 默认USER ID
C# .NET 国密 SM2 签名 默认USER ID: 1234567812345678 string userId = "1234567812345678"; byte[] b ...
- Vue学习:6.认识计算属性
计算属性是 Vue.js 提供的一种特殊属性,用于在模板中动态计算和返回数据.计算属性使得在模板中使用动态计算的数据变得非常简洁和方便,同时又能保持响应式更新的特性,提高了代码的可读性和可维护性. 与 ...
- 请写出常用的linux指令
a.cd /home 进入 '/ home' 目录' b.cd .. 返回上一级目录 c.cd ../.. 返回上两级目录 d.mkdir dir1 创建一个叫做 'dir1' 的目录' e.mkdi ...
- AI赋能ITSM:企业运维跃迁之路
随着企业信息化建设的深入,IT运维管理作为保证企业信息系统稳定运行的重要工作,越来越受到重视. 那么,什么是IT运维呢? 简单地说,IT运维是一系列维护.管理和优化企业IT基础设施.系统和应用程序的活 ...
- SpringBoot+Selenium模拟用户操作浏览器
Selenium Selenium是一个用于Web应用程序自动化测试的开源工具套件.它主要用于以下目的: 浏览器自动化:Selenium能够模拟真实用户在不同浏览器(如Chrome.Firefox.I ...