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']
添加configuration參數,如下,指定wkhtmltopdf安裝路徑
config = pdfkit.configuration(wkhtmltopdf='/opt/bin/wkhtmltopdf')
pdfkit.from_string(html_string, output_file, configuration=config)
更多詳情參考官方文檔
實作步驟
1.安裝wkhtmltox
安裝完成後,找到安裝目錄下wkhtmltopdf.exe所在路徑(例中為D:\Program Files\wkhtmltopdf\bin\wkhtmlpdf.exe),添加到系統環境變量path中(實踐時發現,即便是配置了環境變量,運作時也會報錯:提示:No wkhtmltopdf executable found: "b''"
解決方案:
如下,生成pdf前指定wkhtmltopdf.exe路徑
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 {
res.msg || res.message + ":" + responseData.detail
};
reader.readAsText(res.response.data);
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>
<span style="font-weight: bold;">測試範圍:</span>
<div>
<span>${test_scope}</span>
<span style="font-weight: bold;">測試統計</span>
<img src="${defect_status_pie}" />
</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 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>
<div>{name}</div>
<div>{begin_time}</div>
<div>{start_time}</div>
<div>{end_time}</div>
<div>{finish_time}</div>
<div>{groups}</div>
<div>{environment}</div>
</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文檔
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)
# 删除生成的圖檔檔案
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)
導出效果(部分截圖)
作者:授客
QQ:1033553122
全國軟體測試QQ交流群:7156436
Git位址:https://gitee.com/ishouke
友情提示:限于時間倉促,文中可能存在錯誤,歡迎指正、評論!
作者五行缺錢,如果覺得文章對您有幫助,請掃描下邊的二維碼打賞作者,金額随意,您的支援将是我繼續創作的源動力,打賞後如有任何疑問,請聯系我!!!
微信打賞
支付寶打賞 全國軟體測試交流QQ群
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIw1mYuEDOlATQlcTRlUkQlMUQlYTRlYjQlQTOlYTRlETQlYkQlQTRlUUQlUkQlUTRl8Fdvw1M4MDO2MTMvwVZrV3boN3Lc12bj91cn9Gbi52YvwVbvNmLzd2bsJmbj5ycldWYtl2Lc9CX6MHc0RHaiojIsJye.bmp)