docker+django+vue实例开发之二:后端api实现(三)

3)使用Reportlab生成pdf报表
本项目的一个功能需求就是要生成展示考核成绩的报表,考虑到Reportlab库的成熟性、文档齐全性和示例代码丰富性,选择它作为后端创建pdf文档的库。如何使用Reportlab的例子在Django的官方文档里就有,下面代码即是官方文档的示例:
from io import BytesIO
from reportlab.pdfgen import canvas
from django.http import HttpResponse
def some_view(request):

构建响应对象为pdf文件流

response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="somefilename.pdf"'

实例化内存io对象为pdf内容载体

buffer = BytesIO()

创建pdf文档画布,这是类似于绘图形式的pdf文档创建方式

p = canvas.Canvas(buffer)

绘制文档内容

p.drawString(100, 100, "Hello world.")
p.showPage()
p.save()

将pdf文档转化为字节流并导入到响应对象中返回给前端

pdf = buffer.getvalue()
buffer.close()
response.write(pdf)
return response
官方文档给出的示例提供了一个返回pdf文档流的后端接口的基本逻辑,即
a)创建内存io对象为pdf内容载体;
b)创建pdf文档对象并绘制内容。示例中给出的是画布方式,还有一种就是文档方式,文档方式将pdf所有内容看成一个列表,文字段落、图片、表格等对象都可以安照既定顺序加入到列表中输出为文档内容,每一个对象都包括内容和样式两部分。还可以通过创建类似于html语句的方式构建pdf模板,但本项目没有用到那么深入的机制。
c)从内存io对象中提取字节流导入到响应对象,然后返回给前端。
按照上述逻辑,项目实现了返回成绩报表 pdf文档流的后端接口,部分代码如下:
from django.http import HttpResponse
from rest_framework.decorators import api_view
from io import BytesIO
import json, base64
@api_view(['POST'])
def planReport(request, id):
……
#### pdf全文列表
elements = []
#### 报表标题
title = '%s成绩登记汇总表' % title
styles = getSampleStyleSheet()
title_style = styles['Title']
title_style.fontName = 'heiti'
title_style.fontSize = 24
##表头内容
tb_head_1 = ['序号', '姓名', '性别', '出生日期', '年龄', ]
tb_head_2 = ['', '', '', '', '', ]
span_list = []
col_src = len(tb_head_1)
for item in items_ser.data:
tb_head_2 += ['成绩', '标准', '评定(分)']
tb_head_1 += ['%s\n(%s)' % (item['name'], item['unit']), '', '']
span_list.append(('SPAN', (col_src, 0), (col_src + 2, 0)))
span_list.append(('BACKGROUND', (col_src, 1), (col_src, -1), colors.HexColor('#eef1f6'))) ####成绩填写列设定不同背景色
col_src += 3
tb_head_1 += ['总评', '', '备注']
tb_head_2 += ['分数', '评定', '']
span_list.append(('SPAN', (col_src, 0), (col_src + 1, 0)))
col_src += 2
span_list.append(('SPAN', (col_src, 0), (col_src, 1)))
##表头样式
tb_head_style = [
####文字样式
('FONTNAME', (0, 0), (-1, 1), 'heiti'), # 字体
('FONTSIZE', (0, 0), (-1, 1), 11), # 字体大小
('ALIGN', (0, 0), (-1, 1), 'CENTER'), # 对齐
('VALIGN', (0, 0), (-1, 1), 'MIDDLE'), # 对齐
####线条
('GRID', (0, 0), (-1, -1), 1, colors.black), ####单元格线条
('BOX', (0, 0), (-1, -1), 2, colors.black), ####边框
####合并项
('SPAN', (0, 0), (0, 1)),
('SPAN', (1, 0), (1, 1)),
('SPAN', (2, 0), (2, 1)),
('SPAN', (3, 0), (3, 1)),
('SPAN', (4, 0), (4, 1)),
('SPAN', (5, 0), (5, 1)),
('SPAN', (6, 0), (6, 1)),
]
tb_head_style += span_list

####根据表头计算文档尺寸以适配不同打印纸张要求
table_width = (const_col_width + 2) * len(tb_head_1)  ####加上线条宽度
page_width, page_height, table_width_result = getPageSizeByContent(table_width, 30)
colWidth = const_col_width
if table_width > table_width_result:
    table_width = table_width_result
    colWidth = table_width_result / len(tb_head_1)
content_font_size = const_font_size

####每个部门有单独的标题和表头,填充表格内容
for department in departments:

……
####多次添加标题
elements.append(Paragraph(title, title_style))
####多次添加单位和日期,利用表格实现对齐
tb_data = [['填报单位:%s' % department, '', '', '', '填报日期:'], ]
tb_style = [
('FONTNAME', (0, 0), (-1, 0), 'heiti'), # 字体
('FONTSIZE', (0, 0), (-1, 0), 11), # 字体大小
('ALIGN', (0, 0), (-1, 0), 'CENTER'), # 对齐
('VALIGN', (0, 0), (-1, 0), 'MIDDLE'), # 对齐
]
tb = Table(tb_data, colWidths=table_width / len(tb_data[0]), style=tb_style, rowHeights=30)
elements.append(tb)
####分批添加表格内容
tb_body = []
####添加表头
tb_body.append(tb_head_1)
tb_body.append(tb_head_2)
##添加表格内容
tb_content_style = [
####文字样式
('FONTNAME', (0, 2), (-1, -1), 'kaiti'), # 字体
('FONTSIZE', (0, 2), (-1, -1), content_font_size), # 字体大小
('ALIGN', (0, 2), (-2, -1), 'CENTER'), # 除了备注都居中对齐
('ALIGN', (-1, 2), (-1, -1), 'LEFT'),
('VALIGN', (0, 2), (-1, -1), 'MIDDLE'), # 对齐
]
tb_body += person_record ####表格内容,通过数据库查询获得
tb_head_style += tb_content_style ####表格内容样式
tb = Table(tb_body, colWidths=colWidth, style=tb_head_style, rowHeights=35)
elements.append(tb)

    ####多次添加主考人
    tb_data = [['', '', '', '', '主考人:'], ]
    tb_style = [
        ('FONTNAME', (0, 0), (-1, 0), 'heiti'),  # 字体
        ('FONTSIZE', (0, 0), (-1, 0), 11),  # 字体大小
        ('ALIGN', (0, 0), (-1, 0), 'CENTER'),  # 对齐
        ('VALIGN', (0, 0), (-1, 0), 'MIDDLE'),  # 对齐
    ]
    tb = Table(tb_data, colWidths=table_width / len(tb_data[0]), style=tb_style, rowHeights=30)
    elements.append(tb)

    ####添加分页
    elements.append(PageBreak())

####构造文档对象
####将内存文件对象传入doc
buffer = BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=(page_width, page_height), topMargin=15, bottomMargin=15)
####将数据和格式写入pdf
doc.build(elements)

####生成文档
file_name = '%s.pdf' % title
response = HttpResponse(content_type='application/pdf', charset='UTF-8')
response['Content-Disposition'] = 'attachment; filename=%s' % file_name

###将内存文件对象写入response,为配合前端插件显示,转化为base64方式
pdf = base64.b64encode(buffer.getvalue())
response.write(pdf)
buffer.close()
return response

在上述代码示例中,创建了一个包含多页表格的报表pdf文档数据,转换成base64字符传递给前端,概括起来有以下4点:
a)Reportlab的文档内容都串行的包含在一个列表中,每个元素即代表一块数据,例如“标题、第一段文字、第一个表格”等等,代码编制时需要将这些内容按顺序添加到列表中,再通过doc.build构建文档;
b)每个元素都有单独构建的方法,例如Paragraph()构建段落,Table()构建表格,构造函数通常包括“内容+格式”两方面;
c)要注意文档页面尺寸的选取,为报表打印做准备,最好通过计算内容项中需要涉及页面尺寸布局的元素尺寸,来决定页面尺寸;
d)表格构造过程中,形如:'FONTNAME', (0, 0), (-1, 0), 'heiti'的代码,中间两个元组参数表示单元格位置,分别是(列起始位置,行起始位置), (列终止位置,行终止位置),索引都是从0开始算起,-1表示最后一个行或列的位置。

本项目前端显示pdf文档用的pdf.js插件,由于前端对文档浏览没有要求,简单起见采用了最简单的配置方式,即将pdf.js解压放到static文件中,然后将后端api传递过来的pdf文件流转化为临时url,然后通过"static/pdf/web/viewer.html?file=" + url和window.open函数,重新打开一个窗口,显示pdf文件内容。配合vue-admin使用时需要注意以下几个方面:
a)后端异步接口传递过来的pdf文件流需要在fetch.js文件中放开response对象的拦截权限,即
if (response.headers['content-type'] === 'application/pdf'){
return response
}
b)接口返回数据使用window.URL.createObjectURL函数构造成临时url传递给static/pdf/web/viewer.html,其中,createObjectURL仅接受file或blob对象,不接受原始数据,因而需要先转换数据类型,否则会报错,即
var binaryData = [];
binaryData.push(response.data);
let url = window.URL.createObjectURL(new Blob(binaryData, {type: "application/pdf"}))
c)上述操作对于pdf.js 2.0这种直接用view.js的方式,只能支持英文,后端传过来的数据流中若包含中文会出现乱码,需要额外操作。首先,后端不能直接传回pdf文件流,需要将其转化为base64编码后传回。其次,前端解析时需要将解析后的数据转化为unit8Array数组,这样才能显示中文,即
var binaryData = [];
var bstr = window.atob(response.data) // // base64解码
var len = bstr.length
var tmparray = new Uint8Array(new ArrayBuffer(len));
for (var i = 0; i < len; i++) {
tmparray[i] = bstr.charCodeAt(i);
}
binaryData.push(tmparray)
let url = window.URL.createObjectURL(new Blob(binaryData, {type: "application/pdf"}))
let pdfurl = "static/pdf/web/viewer.html?file=" + url
window.open(pdfurl)

你可能感兴趣的:(docker+django+vue实例开发之二:后端api实现(三))