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/

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)

 

導出效果(部分截圖)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM