项目导出pdf
- 一、前台导出pdf的两种方式
-
- 1. html2canvas.js & pdf.js
-
- 简介
- 使用
- 问题及解决方法
- 2. 浏览器自带的打印功能 & print.js
-
- 简介
- 使用
- 问题及解决方法
- 3. vue-print-nb
-
- 1. 安装vue-print-nb
- 2. 引入Vue项目
- 3. 参数说明
- 4. 应用
- 5. 注意点
- 二、后台输出pdf
-
- 1. 利用iText导出
一、前台导出pdf的两种方式
1. html2canvas.js & pdf.js
简介
我们可以直接在浏览器端使用html2canvas,对整个或局部页面进行‘截图’。但这并不是真的截图,而是通过遍历页面DOM结构,收集所有元素信息及相应样式,渲染出canvas image。
由于html2canvas只能将它能处理的生成canvas image,因此渲染出来的结果并不是100%与原来一致。但它不需要服务器参与,整个图片都由客户端浏览器生成,使用很方便。
使用
先下载html2canvas.js
和jspdf
插件:
yarn add html2canvas
npm install --save html2canvasyarn add jspdf
npm install jspdf --save
使用的API也很简洁,下面代码可以将某个元素渲染成canvas:
html2canvas(element, {
onrendered: function(canvas) {
document.body.appendChild(canvas);}
});
当然了,这是个非常简单的html转成pdf的js方法。
问题及解决方法
如果我们想要输出多页pdf,需要清晰度更高的,同时内容不被截断的改怎么办呢?
- 输出多页pdf
// // 导出页面为PDF格式
import html2canvas from 'html2canvas'
import JSPDF from 'jspdf'
export default {
install (Vue, options) {
Vue.prototype.ExportSavePdf = function (htmlTitle, currentTime) {
// 导出之前先将滚动条置顶,不然会出现数据不全的现象window.pageYOffset = 0document.documentElement.scrollTop = 0document.body.scrollTop = 0var element = document.getElementById('pdfCentent')element.style.background = '#FFFFFF'html2canvas(element, {
logging: false,dpi: window.devicePixelRatio * 4, // 将分辨率提高到特定的DPI 提高四倍scale: 4, // 按比例增加分辨率element: element,backgroundColor: '#ffffff',allowTaint: true,useCORS: true}).then(function (canvas) {
var pdf = new JSPDF('p', 'mm', 'a4') // A4纸,纵向pdf.setFontSize(30) // 字体大小// pdf.text(20, 30, pdf)var ctx = canvas.getContext('2d')var a4w = 170; var a4h = 257 // A4大小,210mm x 297mm,四边各保留20mm的边距,显示区域170x257var imgHeight = Math.floor(a4h * canvas.width / a4w) // 按A4显示比例换算一页图像的像素高度var renderedHeight = 0var options = {
pagesplit: true }while (renderedHeight < canvas.height) {
var page = document.createElement('canvas')page.width = canvas.widthpage.height = Math.min(imgHeight, canvas.height - renderedHeight)// 可能内容不足一页// 用getImageData剪裁指定区域,并画到前面创建的canvas对象中page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0)pdf.addImage(page.toDataURL('image/jpeg', 1.0), 'JPEG', 10, 10, a4w, Math.min(a4h, a4w * page.height / page.width)) // 添加图像到页面,保留10mm边距renderedHeight += imgHeightif (renderedHeight < canvas.height) {
pdf.addPage(element, options, function () {
}) }// 如果后面还有内容,添加一个空页// delete page;}pdf.save(htmlTitle + currentTime)})}}
}
2.清晰度更高:
// // 导出页面为PDF格式
import html2canvas from 'html2canvas'
import JSPDF from 'jspdf'
export default {
install (Vue, options) {
Vue.prototype.ExportSavePdf = function (htmlTitle, currentTime) {
// 清晰度更高var downPdf = document.getElementById('pdfCentent')html2canvas(downPdf, {
logging: false,// dpi: 172,dpi: window.devicePixelRatio * 4, // 将分辨率提高到特定的DPI 提高四倍scale: 4 // 按比例增加分辨率}).then(function (canvas) {
console.log('canvas: ', canvas)var contentWidth = canvas.widthvar contentHeight = canvas.height// 一页pdf显示html页面生成的canvas高度;var pageHeight = contentWidth / 592.28 * 841.89// 未生成pdf的html页面高度var leftHeight = contentHeight// pdf页面偏移var position = 0// html页面生成的canvas在pdf中图片的宽高(a4纸的尺寸[595.28,841.89])var imgWidth = 595.28var imgHeight = 592.28 / contentWidth * contentHeightvar pageData = canvas.toDataURL('image/jpeg', 1.0)var pdf = new JSPDF('', 'pt', 'a4')// 有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)// 当内容未超过pdf一页显示的范围,无需分页if (leftHeight < pageHeight) {
pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)} else {
while (leftHeight > 0) {
pdf.addImage(pageData, 'JPEG', 0, position, imgWidth, imgHeight)leftHeight -= pageHeightposition -= 841.89// 避免添加空白页if (leftHeight > 0) {
pdf.addPage()}}}pdf.save(htmlTitle + currentTime)// 背景设为白色(默认为黑色)// background: '#fff'})}}
}
3.内容不被截断,也就是把所有的内容放一页:
// // 导出页面为PDF格式
import html2canvas from 'html2canvas'
import JSPDF from 'jspdf'
export default {
install (Vue, options) {
Vue.prototype.ExportSavePdf = function (htmlTitle, currentTime) {
// -------------输出pdf为一页,解决分页内容截断问题,单在pdf打印时会出错-------------var mainRight = document.getElementById('pdfCentent')html2canvas(mainRight, {
allowTaint: true,scale: 2 // 提升画面质量,但是会增加文件大小}).then(function (canvas) {
var contentWidth = canvas.widthvar contentHeight = canvas.heightvar pageData = canvas.toDataURL('image/jpeg', 0.4)var pdfWidth = (contentWidth + 10) / 2 * 0.75var pdfHeight = (contentHeight + 200) / 2 * 0.75 // 500为底部留白var imgWidth = pdfWidthvar imgHeight = (contentHeight / 2 * 0.75) // 内容图片这里不需要留白的距离var pdf = new JSPDF('', 'pt', [pdfWidth, pdfHeight])pdf.addImage(pageData, 'jpeg', 0, 0, imgWidth, imgHeight)// pdf.save('report_pdf_' + new Date().getTime() + '.pdf')pdf.save(htmlTitle + currentTime)
})}}
}
问题:
使用这个插件固然可以将html转换成pdf输出,但是原理是将html先转换成canvas图片,然后将图片转成pdf,这样图片的清晰度不高,同时存在内容被截断的情况,虽然可以通过修改js文件来判断是否截断内容,但是设计的文本内容比较多,例如:表格、文本、图片、图表等,不好判断。
于是采用下面的方法实现前台导出pdf,利用print.js
。
2. 浏览器自带的打印功能 & print.js
简介
浏览器自带的打印功能也可以输出pdf,图证如下:
使用
两种引入方式:
1.引入安装vue-print.js
:
cnpm i vue-printjs --save-dev
2.下载print.js
包:
https://github.com/zxc19890923/print/blob/master/print.js
使用方法
1.main.js
中引入插件
import Print from './plugins/print/Print'
Vue.use(Print)
2.vue项目中使用:
<!--* @Autor: Sugar_W* @Date: 2020-11-24 19:01:10* @LastEditors: Sugar_W* @LastEditTime: 2020-11-24 19:55:32* @Description: Still waters run deep!* @FilePath: \CPSWeb\src\views\report\ReportTest.vue
-->
<template><div><template><div ref="print" class="app-container" id="print"><p>qhfqihbfhubqshufbuqhwbufbqwuvfbugsdghjcbhjasbvfgyugqwetyggdfytgqwvgtgyufvbaswdguyhfb</p><a-button type="primary" class="button" @click="printContext">查询</a-button></div></template></div>
</template><script>export default {
methods: {
// 打印printContext () {
this.$print(this.$refs.print)}// 不打印方法1. 添加no-print样式类// 不打印方法2. this.$print(this.$refs.print,{'no-print':'.do-not-print-div'})}
}
</script>
问题及解决方法
这个插件用的人比较少,所以我在项目中遇见的问题还是比较多的,下面就一一解决。程序员就是解决问题的,QAQ
。
1、如果项目需要多页pdf,那么肯定需要分页,首先确定好,每一页的大小,去适配你的界面,因为每个人的需求不一样,大小也不一样,下面放我设置的大小:
// 报告右侧块样式
.reportBlock{
border-radius: 2px;border:1px solid;border-color:#dcdddd;width:731px;height:1024px;clear: both;margin: 0 auto;position: relative;margin-top: 20px;margin-left: 0px;margin-bottom: 20px;
}
2、每页大小确定了,那么就需要实现分页了,这里用到一个css样式:
page-break-after: always; // 实现分页的重要一步
整个代码就是:
// 报告右侧块样式
.reportBlock{
border-radius: 2px;border:1px solid;border-color:#dcdddd;width:731px;height:1024px;clear: both;margin: 0 auto;position: relative;margin-top: 20px;margin-left: 0px;margin-bottom: 20px;page-break-after: always; // 实现分页的重要一步
}
3、既然有了分页,自然也就需要滚动条,总不至于使用浏览器自带的滚动条,那么影响整个系统使用。再者,如果界面内容使用滚动条,出现滚动条也是不美观的,特别对于我们前端开发人员来说,非常碍眼。我们可以利用两个div
块来实现:
.rightBlock{
overflow: hidden;width: 840px;height: 750px;}
第一个div块里是空的,设置宽度较小,目的是挡住第二个div
的滚动条;
.printBlock{
width: 860px;height: 100%;overflow-y: scroll;overflow-x: hidden;
}
第二个div块里放置我们需要展示的内容,设置可滚动,然后设置宽度大于第一div
宽度。
这样就实现既有滚动条又隐藏起来啦。
这里要注意不能使用这个css属性:
min-height:666px;max-height:666px;overflowY:scroll;
这个css样式会导致分页失败哟!
4、在弹出打印预览的时候,会发现出现页眉页脚,这就需要去除。使用如下代码:
// 去除页眉页脚
#print{
@page {
margin: 0; /* this affects the margin in the printer settings */}}
这里的print
就是对应的你想输出pdf的div
块。
5、在出现需要空白的地方,比如第一页,我们第一个想到的是不是加个矩形框,但是事实证明这是失败的。需要使用css外边框属性,类似:margin-top
。而不是另外加div框。
6、如果你的项目使用了echarts图表直接浏览器打印显示不出来,因为print.js
本身是不支持canvas
转成图片的功能的,需要将echarts的转成图片,提前把canvas图表转成图片,这里使用自己写的代码,记得这里的代码是在刚刚引入的main.js
的print.js
中:
var canvass = document.querySelectorAll('canvas');
//canvass echars图表转为图片for (var k4 = 0; k4 < canvass.length; k4++) {
var imageURL = canvass[k4].toDataURL("image/png");var img = document.createElement("img");img.src = imageURL;img.setAttribute('style', 'max-width: 100%;');img.className = 'isNeedRemove'// canvass[k4].style.display = 'none'// canvass[k4].parentNode.style.width = '100%'// canvass[k4].parentNode.style.textAlign = 'center'canvass[k4].parentNode.insertBefore(img,canvass[k4].nextElementSibling);}
在print.js
中还写了分页的函数:
//做分页//style="page-break-after: always"var pages = document.querySelectorAll('.result');for (var k5 = 0; k5 < pages.length; k5++) {
pages[k5].setAttribute('style', 'page-break-after: always');}return this.dom.outerHTML;
writeIframe: function (content) {
var w, doc, iframe = document.createElement('iframe'),f = document.body.appendChild(iframe);iframe.id = "myIframe";//iframe.style = "position:absolute;width:0;height:0;top:-10px;left:-10px;";iframe.setAttribute('style', 'position:absolute;width:' + document.querySelector('.results').clientWidth + 'px;height:0;top:-10px;left:-10px;');w = f.contentWindow || f.contentDocument;doc = f.contentDocument || f.contentWindow.document;doc.open();doc.write(content);doc.close();var removes = document.querySelectorAll('.isNeedRemove');for (var k = 0; k < removes.length; k++) {
removes[k].parentNode.removeChild(removes[k]);}var _this = thisiframe.onload = function(){
_this.toPrint(w);setTimeout(function () {
document.body.removeChild(iframe)}, 100)}},
担心你们找不到位置就截图把需要添加的地方标出来,然后代码紧接着放在地下,供你们复制喔。毕竟大家都是面向CV编程嘛,QAQ
。
但是有的小伙伴说,这一个个找的太费力了,我就想直接CV,好的满足你,下面自取喔:
// 打印类属性、方法定义
/* eslint-disable */
const Print = function (dom, options) {
if (!(this instanceof Print)) return new Print(dom, options);this.options = this.extend({
'noPrint': '.no-print'}, options);if ((typeof dom) === "string") {
this.dom = document.querySelector(dom);} else {
this.isDOM(dom)this.dom = this.isDOM(dom) ? dom : dom.$el;}this.init();
};
Print.prototype = {
init: function () {
var content = this.getStyle() + this.getHtml();this.writeIframe(content);},extend: function (obj, obj2) {
for (var k in obj2) {
obj[k] = obj2[k];}return obj;},getStyle: function () {
var str = "",styles = document.querySelectorAll('style,link');for (var i = 0; i < styles.length; i++) {
str += styles[i].outerHTML;}str += "<style>" + (this.options.notPrint ? this.options.notPrint : '.no-print') + "{display:none;}</style>";str += "<style>.results{width:100%!important;} .result .title{width:100%;}</style>";return str;},getHtml: function () {
var inputs = document.querySelectorAll('input');var textareas = document.querySelectorAll('textarea');var selects = document.querySelectorAll('select');var canvass = document.querySelectorAll('canvas');for (var k = 0; k < inputs.length; k++) {
if (inputs[k].type == "checkbox" || inputs[k].type == "radio") {
if (inputs[k].checked == true) {
inputs[k].setAttribute('checked', "checked")} else {
inputs[k].removeAttribute('checked')}} else if (inputs[k].type == "text") {
inputs[k].setAttribute('value', inputs[k].value)} else {
inputs[k].setAttribute('value', inputs[k].value)}}for (var k2 = 0; k2 < textareas.length; k2++) {
if (textareas[k2].type == 'textarea') {
textareas[k2].innerHTML = textareas[k2].value}}for (var k3 = 0; k3 < selects.length; k3++) {
if (selects[k3].type == 'select-one') {
var child = selects[k3].children;for (var i in child) {
if (child[i].tagName == 'OPTION') {
if (child[i].selected == true) {
child[i].setAttribute('selected', "selected")} else {
child[i].removeAttribute('selected')}}}}}//canvass echars图表转为图片for (var k4 = 0; k4 < canvass.length; k4++) {
var imageURL = canvass[k4].toDataURL("image/png");var img = document.createElement("img");img.src = imageURL;img.setAttribute('style', 'max-width: 100%;');img.className = 'isNeedRemove'// canvass[k4].style.display = 'none'// canvass[k4].parentNode.style.width = '100%'// canvass[k4].parentNode.style.textAlign = 'center'canvass[k4].parentNode.insertBefore(img,canvass[k4].nextElementSibling);}//做分页//style="page-break-after: always"var pages = document.querySelectorAll('.result');for (var k5 = 0; k5 < pages.length; k5++) {
pages[k5].setAttribute('style', 'page-break-after: always');}return this.dom.outerHTML;},writeIframe: function (content) {
var w, doc, iframe = document.createElement('iframe'),f = document.body.appendChild(iframe);iframe.id = "myIframe";//iframe.style = "position:absolute;width:0;height:0;top:-10px;left:-10px;";iframe.setAttribute('style', 'position:absolute;width:' + document.querySelector('.results').clientWidth + 'px;height:0;top:-10px;left:-10px;');w = f.contentWindow || f.contentDocument;doc = f.contentDocument || f.contentWindow.document;doc.open();doc.write(content);doc.close();var removes = document.querySelectorAll('.isNeedRemove');for (var k = 0; k < removes.length; k++) {
removes[k].parentNode.removeChild(removes[k]);}var _this = thisiframe.onload = function(){
_this.toPrint(w);setTimeout(function () {
document.body.removeChild(iframe)}, 100)}},toPrint: function (frameWindow) {
try {
setTimeout(function () {
frameWindow.focus();try {
if (!frameWindow.document.execCommand('print', false, null)) {
frameWindow.print();}} catch (e) {
frameWindow.print();}frameWindow.close();}, 10);} catch (err) {
console.log('err', err);}},isDOM: (typeof HTMLElement === 'object') ?function (obj) {
return obj instanceof HTMLElement;} :function (obj) {
return obj && typeof obj === 'object' && obj.nodeType === 1 && typeof obj.nodeName === 'string';}
};
const MyPlugin = {
}
MyPlugin.install = function (Vue, options) {
// 4. 添加实例方法Vue.prototype.$print = Print
}
export default MyPlugin
7、有时候预览的时候会出现中间突然多一页空白页,这可能是你用的组件出了问题,比如说我用的vue
的card
,封装好的自带内边距padding:24px
,这个会导致的。
两个解决办法:
1、如果你的card
都想改,或者你引用的某个组件,更改内联样式不影响其他的,你可以使用vue的深度内联deep
来更改:
/deep/.ant-transfer-list-header{
background: #01ada8;
}
类似这样的,具体更改card的我没用,这个号小问题对于各位看官来说问题不大,自己解决咯。
2、将card
改成div
就好啦。
总结
前台输出pdf就到这了,可能还有问题没写在里面,以后遇见还会更新。也欢迎大家有问题提问,共同进步。
3. vue-print-nb
1. 安装vue-print-nb
没有什么前提要求,直接安装即可,但因为Vue2.0和Vue3.0有着不同的用法,因此需要安装的版本也不同,各位看官自行取舍。
Vue2.0版本安装方法:
npm install vue-print-nb --save
Vue3.0版本安装方法:
npm install vue3-print-nb --save
2. 引入Vue项目
Vue2.0引入方式:
// 1. 全局挂载
import Print from 'vue-print-nb'
Vue.use(Print)// or// 2. 自定义指令
import print from 'vue-print-nb'
directives: {
print
}
Vue3.0引入方式:
// 1. 全局挂载
import {
createApp } from 'vue'
import App from './App.vue'
import print from 'vue3-print-nb'
const app = createApp(App)
app.use(print)
app.mount('#app')// or // 2. 自定义指令
import print from 'vue3-print-nb'directives: {
print
}
3. 参数说明
参数 | 作用 | 类型 | 可选项 | 默认值 |
---|---|---|---|---|
id | 局部打印有效,标识符 | String | – | ‘printId’ |
standard | 局部打印有效,打印的文本类型 | String | HTML5/loose/strict | HTML5 |
extraHead | 局部打印有效,添加在打印区域的最顶端 | String | – | – |
extraCss | 局部打印有效,为打印区域提供Stylesheet样式表 | String | – | – |
popTitle | 局部打印有效,编辑页眉的标题 | String | – | Document Title |
clickMounted | 全局有效,调用v-print绑定的按钮点击事件callback | Function | – | this.Object |
openCallback | 全局有效,调用打印时的callback | Function | – | this.Object |
closeCallback | 全局有效,调用关闭打印的callback(无法区分确认or取消) | Function | – | this.Object |
beforeOpenCallback | 全局有效,调用开始打印之前的callback | Function | – | this.Object |
preview | 全局有效,控制打印预览 | Boolean | true/false | false |
previewTitle | 编辑预览页面的预览标题 | String | – | ‘打印预览’ |
previewPrintBtnLabel | 编辑预览页面的打印按钮文本 | String | – | ‘打印’ |
previewBeforeOpenCallback | 调用打开预览页面之前的callback | Function | – | this.Object |
previewOpenCallback | 调用打开预览页面之后的callback | Function | – | this.Object |
url | 非局部打印有效,打印指定的URL,确保同源策略相同 | String | – | – |
asyncUrl | 非局部打印有效,异步加载打印指定的URL,确保同源策略相同 | Function | – | – |
zIndex | 预览有效,预览窗口的z-index,默认是20002,最好比默认值更高 | String,Number | – | 20002 |
4. 应用
template示例:
<template><div class="hello"><h1>{
{
msg }}</h1><h2>Essential Links</h2>// 局部打印文本以及按钮<div id="printArea">Print Area</div><button v-print="print">Print!</button><ul><li><ahref="https://vuejs.org"target="_blank">Core Docs</a></li><li><ahref="https://forum.vuejs.org"target="_blank">Forum</a></li><li><ahref="https://chat.vuejs.org"target="_blank">Community Chat</a></li><li><ahref="https://twitter.com/vuejs"target="_blank">Twitter</a></li><br><li><ahref="http://vuejs-templates.github.io/webpack/"target="_blank">Docs for This Template</a></li></ul><h2>Ecosystem</h2><ul><li><ahref="http://router.vuejs.org/"target="_blank">vue-router</a></li><li><ahref="http://vuex.vuejs.org/"target="_blank">vuex</a></li><li><ahref="http://vue-loader.vuejs.org/"target="_blank">vue-loader</a></li><li><ahref="https://github.com/vuejs/awesome-vue"target="_blank">awesome-vue</a></li></ul></div>
</template>
script示例:
export default {
name: 'HelloWorld',data () {
let that = thisreturn {
msg: 'Welcome to Your Vue.js App',print: {
id: 'printArea',popTitle: '配置页眉标题', // 打印配置页上方的标题extraHead: '打印', // 最上方的头部文字,附加在head标签上的额外标签,使用逗号分割preview: true, // 是否启动预览模式,默认是falsepreviewTitle: '预览的标题', // 打印预览的标题previewPrintBtnLabel: '预览结束,开始打印', // 打印预览的标题下方的按钮文本,点击可进入打印zIndex: 20002, // 预览窗口的z-index,默认是20002,最好比默认值更高previewBeforeOpenCallback () {
console.log('正在加载预览窗口!'); console.log(that.msg, this) }, // 预览窗口打开之前的callbackpreviewOpenCallback () {
console.log('已经加载完预览窗口,预览打开了!') }, // 预览窗口打开时的callbackbeforeOpenCallback () {
console.log('开始打印之前!') }, // 开始打印之前的callbackopenCallback () {
console.log('执行打印了!') }, // 调用打印时的callbackcloseCallback () {
console.log('关闭了打印工具!') }, // 关闭打印的callback(无法区分确认or取消)clickMounted () {
console.log('点击v-print绑定的按钮了!') },// url: 'http://localhost:8080/', // 打印指定的URL,确保同源策略相同// asyncUrl (reslove) {
// setTimeout(() => {
// reslove('http://localhost:8080/')// }, 2000)// },standard: '',extarCss: ''}}}
}
5. 注意点
Callback函数中this指向当前print object对象,that返回Vue对象;
不需要页眉页脚可以在打印弹窗页面的更多设置里面取消选择;
不设置popTitle参数页眉标题为undifined;
popTitle参数为空时,页眉标题默认为Document Title。
二、后台输出pdf
1. 利用iText导出
因为我使用的前台导出,当时也看了后台,因为项目中有很多复杂图表,后台实现不了,所以放弃这个途径。小伙伴在抉择的时候也要注意哦。
下面放上几个当时我看的几篇博客,希望对大家有帮助!
1、iText5实现Java生成PDF文件完整版
2、iText5官方系列教程-iText in Action(一)
3、【iText5 生成PDF】纯Java代码实现生成PDF(自定义表格、文本水印、单元格样式)
设计到前后台文件流传输的文章:
1、PDF功能实现1——Java实现动态页面在后台生成PDF文件
2、自动把动态的jsp页面(或静态html)生成PDF文档,并且上传至服务器
3、前端接受后端文件流并下载到本地的方法
4、后台返回文件流,前端实现预览pdf