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/

https://files.pythonhosted.org/packages/a1/98/6988328f72fe3be4cbfcb6cbfc3066a00bf111ca7821a83dd0ce56e2cf57/pdfkit-0.6.1.tar.gz

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的更多相关文章

  1. 页面导出生成pdf,使用wkhtmltopdf第三方工具

    把页面导出生成pdf,这里用到第三方的工具,使用方法中文文档没有找到,网上也没找到网友详细的神作.没有深入研究,所以也不赘述了,当然最基本的使用大多数也够用了,详细参数的官网也没介绍,大家使用的时候, ...

  2. rails应用页面导出为pdf文档

    1.下载安装wkhtmltox https://wkhtmltopdf.org/downloads.html   2.gemfile添加 gem 'pdfkit' #页面导出pdf gem 'wkht ...

  3. Vue 页面导出成PDF文件

    注意事项 如果导出的页面中设计到图片或者其他文件跨域文件,需要后端服务配合 安装依赖 npm install html2Canvas --save npm install jspdf--save 封装 ...

  4. vue将页面导出成pdf

    npm i jspdf-html2canvas prinOut(){ // 导出pdf let page = document.querySelector('.app-main'); // page ...

  5. 页面导出为PDF

    一.使用环境 Vue3.Quasar.Electron 二.安装 jspdf-html2canvas npm install jspdf-html2canvas --save 安装失败可以选择cnpm ...

  6. 使用pdf.js实现前端页面预览pdf文档,解决了跨域请求

    pdf.js主要包含两个库文件,一个pdf.js和一个pdf.worker.js,,一个负责API解析,一个负责核心解析 官网地址:http://mozilla.github.io/pdf.js/ 下 ...

  7. Django分析之导出为PDF文件

    最近在公司一直忙着做exe安装包,以及为程序添加新功能,好久没有继续来写关于Django的东西了….难得这个周末清闲,来了解了解Django的一些小功能也是极好的了~ 那今天就来看看在Django的视 ...

  8. 【jsPDF】jsPDF插件实现将html页面转换成PDF,并下载,支持分页

    1.目的:在前段是 jQuery库 或者 VUE库 或者两者混合库,将html 页面和数据 转换成PDF格式并下载,支持分页 1.项目背景: 对客户报修记录进行分类统计,并生成各种饼图.柱状图.线性图 ...

  9. 前端实现html转pdf方法总结

    最近要搞前端html转pdf的功能.折腾了两天,略有所收,踩了一些坑,所以做些记录,为后来的兄弟做些提示,也算是回馈社区.经过一番调(sou)研(suo)发现html导出pdf一般有这几种方式,各有各 ...

  10. 使用JavaScript将当前页面保存成PDF,支持图片和文字的保存

    前端开发的朋友们可能会遇到这个需求:将您负责开发的网页的全部内容,包括文字和图片,一起保存成一个PDF文件.如果采用屏幕截图的话,默认Windows操作系统的截图按钮无法完整截取超过一屏幕的屏幕内容. ...

随机推荐

  1. Android 13 - Media框架(8)- MediaExtractor

    关注公众号免费阅读全文,进入音视频开发技术分享群! 上一篇我们了解了 GenericSource 需要依赖 IMediaExtractor 完成 demux 工作,这一篇我们就来学习 android ...

  2. WPF显示网络图片的几种方法

    1.利用数据流 1 Image img; 2 byte[] btyarray = GetImageFromResponse(imageUrl); 3 4 //字节数据转流 5 MemoryStream ...

  3. 面试题一《swift和oc的区别》

    一.来源 这道题来自网上一篇文章<100家公司iOS面试题管理>,这份题目虽然题目质量不高,但是覆盖面比较全,有学习的价值 二.解析 1.swift 比 OC更年轻,这意味着 swift ...

  4. jq 下载带上token设置form表单并且兼容ie8

    先在publice公共资源里面或者自己随便写一个函数自己记得调用就行 先加载form表单组件 /** * 添加form下载组件 * @param {*} wrp */ appendFormDownlo ...

  5. zkq 数学听课笔记

    线性代数 域 \(F\),OI 中常用的域是 \(\Z_{p^c}\). \(n\) 维向量 \(\vec x \in F^n\),其中 \(x_i \in F\),注意向量是列向量. \(F^n\) ...

  6. C#.NET 操作FTP

    工具类: using System; using System.Collections.Generic; using System.IO; using System.Net; namespace Co ...

  7. uniapp 开发微信小程序 使用微信小程序一键登录

    研究了一天的uniapp开发微信小程序的第一步,登录! 刚开始使用uni.getUserInfo函数No!不行,无法运行,研究文档发现是这个函数被微信小程序团队给禁用了,OK换! 后来换成了uni.g ...

  8. FEL - Fast Expression Language

    开源好用的表达式计算语言FEL,可惜了官网文档不在国内,我来copy个过来. Fel是轻量级的高效的表达式计算引擎 Fel在源自于企业项目,设计目标是为了满足不断变化的功能需求和性能需求. Fel是开 ...

  9. 洛谷 P4913 二叉树深度

    题目链接:二叉树深度 思路 存储二叉树的各个节点并递归搜索二叉树深度. 题解 #include <bits/stdc++.h> using namespace std; #define l ...

  10. Nginx SSL证书更新及密码套件更新

    一.域名更换证书 ssl证书一般包括证书文件crt.cer.pem.pfx和私钥文件key. CER.CRT.PEM 和 PFX 是不同的证书文件格式,它们之间存在一些区别: CER (DER 编码) ...