最近做后臺系統(tǒng)遇到挺多復(fù)雜的需求,比如導(dǎo)出pdf翼雀,word饱苟,excel
一般這種需求后端如果存文件,然后傳個流過來狼渊,前端就可以下載導(dǎo)出了箱熬。
但是如果后端不存文件,只返回字符串(富文本字符串),這時候咋辦城须?
= =不知道蚤认,但是我遇到了,也只能頭鐵干了酿傍。
路還是有的烙懦,講一下實(shí)現(xiàn)方式:
html2canvas+jspdf
具體需求是在彈窗內(nèi)預(yù)覽,然后點(diǎn)擊下載可以生成對應(yīng)pdf赤炒,預(yù)覽pdf如果后端沒有給文件地址氯析,只返回富文本字符串(類似"<p>123</p>"),比較難做莺褒,但是辦法還是有的,個人感覺難點(diǎn)在于html2canvas生成完整截圖的時機(jī)和pdf的分頁掩缓。
貼一下效果:
思路:由于pdf不可直接編輯,個人思路是先將html截屏轉(zhuǎn)化成圖片遵岩,再把圖片嵌入生成pdf你辣。
- html2canvas:直接npm i html2canvas -S ,用法是截圖dom然后轉(zhuǎn)化為canvas尘执,具體api可以去github上看舍哄。
- jspdf(項(xiàng)目編譯報(bào)錯所以選了個特定的版本):https://cdn.bootcss.com/jspdf/1.5.3/jspdf.debug.js
直接貼代碼(項(xiàng)目用的是elementUI,核心代碼在preview2pdf這個方法):
<template>
<div class="preview-modal">
<el-dialog class="common-dialog" :width="width" :visible.sync="visible" @opened="openModal" @closed="hiddenModal" destroy-on-close>
<!-- <template slot="title">
<div class="common-modal-title">
<span>{{title}}</span>
</div>
</template> -->
<div class="preview-content">
<div v-show="isLoading" class="loading">
<i class="el-icon-warning-outline"></i>正在生成
</div>
<div class="preview-data" style="min-height:2400px" v-if="previewDom || domData">
<div style="color: #333; position: relative;padding: 30px;">
<!-- 封面 封面頁面伸縮(預(yù)覽效果)通過調(diào)整.preview-data 的 width屬性控制-->
<div style="height: 1320px; padding-top: 100px;text-align: center;">
<h2 style="line-height: 50px;">
<br />
<span>我是豬扒封面</span>
</h2>
</div>
<!-- 具體正文 -->
<div class="edit-content" style="margin-top:20px"></div>
</div>
</div>
<div class="error-pdf" v-else><i class="el-icon-document-delete preview-icon"></i>找不到pdf文件</div>
</div>
<slot name="footer">
<template slot="footer" class="dialog-footer">
<el-button v-if="previewDom || domData" type="primary" :disabled="isLoading" @click="downloadPDF">下載</el-button>
<el-button v-else type="primary" @click="hiddenModal">確定</el-button>
</template>
</slot>
</el-dialog>
</div>
</template>
<script>
import '@/utlis/html2canvas'
import '@/utlis/jspdf.debug'
import moment from "moment";
/*
*@description: 預(yù)覽pdf彈窗
*@version V1.0
*@API:
*@ 參數(shù) 二選一誊锭, 二選一表悬, 二選一
*previewDom 頁面中可看見的預(yù)覽目標(biāo)類名 (比如富文本在頁面中顯示,其容器div類名為'.fuwenben',直接傳'.fuwenben'就可以生成預(yù)覽頁面dom的pdf了)
*domData 頁面中看不見的dom字符串(比如后臺返回富文本字符串'<div>111</div>',直接傳進(jìn)來就可以生成pdf)
*@ 事件
* 需要在父組件指定關(guān)閉事件 onModalHidden
* onModalHidden(){
this.previewDialogVisible = false
},
*/
export default {
name:'PreviewModal',
props:{
title: {
type: String,
default: ''
},
width: {
type: String,
default: ''
},
isVisible: {
type: Boolean,
default: false
},
// 預(yù)覽目標(biāo)類名
previewDom:{
type: String,
default: ''
},
pdfName:{
type: String,
default:'pdf'
},
domData:{
type:String,
default:''
},
dateTime: {
type: Array,
default: () => {
return [];
},
}
},
data(){
return{
visible: this.isVisible, // 將props 的屬性備份到data中
pdfFile:null,
isLoading:true,
}
},
methods: {
//當(dāng)前日期
getDate() {
let date = new Date();
const month =
date.getMonth() + 1 > 9
? date.getMonth() + 1
: 0 + (date.getMonth() + 1);
return date.getFullYear() + "年" + month + "月" + date.getDate() + "日";
},
getDateTime() {
return (
this.getSplit(this.dateTime[0]) + "至" + this.getSplit(this.dateTime[1])
);
},
getSplit(date) {
let arr = moment(date)
.format("YYYY-MM-DD")
.split("-");
return arr[0] + "年" + arr[1] + "月" + arr[2] + "日";
},
/**
* 顯示對話框
*/
showModal() {
// 如果是隱藏中才顯示
if (!this.visible) { this.visible = true }
},
/**
* 隱藏對話框
*/
hiddenModal() {
// 如果是顯示中才隱藏
this.visible = false
this.isLoading = true
this.$emit('onModalHidden')
// console.log(this.visible)
},
openModal(){
this.$emit('onModalOpen')
this.preview2pdf()
},
downloadPDF(){
this.pdfFile.save(this.pdfName);
this.$emit('downloadPDF');
},
// 預(yù)覽轉(zhuǎn)pdf
preview2pdf(){
// 非法dom直接返回
if (!this.previewDom && !this.domData) {
this.isLoading = false
return
}
const parentDom = document.querySelector('.preview-modal')
const contentDom = parentDom.querySelector('.preview-content')
// 進(jìn)行截圖的dom
const canvasDom = document.querySelector('.preview-content .preview-data')
// 找不到這個dom元素丧靡,返回
if(!canvasDom) {
this.isLoading = false
return
}
// 傳入富文本字符串,添加到原有的子節(jié)點(diǎn)中
const mainBody = canvasDom.querySelector('.edit-content')
if(mainBody) {
// 添加內(nèi)容
mainBody.innerHTML = `<div>${this.domData}</div>`
} else {
// 外部傳進(jìn)來的dom元素
const previewDom = document.querySelector(this.previewDom)
mainBody.append(previewDom)
}
// 新建ifame標(biāo)簽在線展示pdf
const iframe = document.createElement('iframe')
iframe.height = '99%'
iframe.width = '100%'
// 進(jìn)行dom截圖 必須讓dom更新完再調(diào)用
this.$nextTick(()=>{
html2canvas(canvasDom, {
allowTaint: true,
useCORS: true,
}).then((canvas)=>{
// 用iframe標(biāo)簽展示pdf生成預(yù)覽效果
contentDom.appendChild(iframe)
var contentWidth = canvas.width;
var contentHeight = canvas.height;
//一頁pdf顯示html頁面生成的canvas高度;
var pageHeight = contentWidth / 592.28 * 841.89;
//未生成pdf的html頁面高度
var leftHeight = contentHeight;
//頁面偏移
var position = 0;
//a4紙的尺寸[595.28,841.89]蟆沫,html頁面生成的canvas在pdf中圖片的寬高
var imgWidth = 555.28;
var imgHeight = 555.28/contentWidth * contentHeight;
var pageData = canvas.toDataURL('image/jpeg', 1.0);
// 取消生成狀態(tài)
this.isLoading = false
var pdf = new jsPDF('', 'pt', 'a4');
//有兩個高度需要區(qū)分,一個是html頁面的實(shí)際高度温治,和生成pdf的頁面高度(841.89)
//當(dāng)內(nèi)容未超過pdf一頁顯示的范圍饭庞,無需分頁
if (leftHeight < pageHeight) {
imgWidth = 555.28;
imgHeight = 555.28/contentWidth * contentHeight;
pdf.addImage(pageData, 'JPEG', 20, 20, imgWidth, imgHeight );
} else {
while(leftHeight > 0) {
leftHeight -= pageHeight;
pdf.addImage(pageData, 'JPEG', 20, position, imgWidth, imgHeight)
position -= 841.89;
//避免添加空白頁
if(leftHeight > 0) {
pdf.addPage();
}
}
}
// 保存pdf對象
this.pdfFile = pdf
// 生成外鏈讓iframe標(biāo)簽展示
iframe.src = pdf.output('datauristring')
})
})
}
},
watch:{
isVisible(){
this.visible = this.isVisible;
}
}
}
</script>
<style lang="scss">
.preview-modal{
height:100%;
.common-dialog{
// height:100%;
.el-dialog{
margin-top: 0!important;
height: 100%
}
.common-modal-title{
width: 100%;
height: 50px;
margin: 10px auto 0;
line-height: 48px;
border-bottom: 2px solid #d8d8d8;
box-sizing: border-box;
font-size: 20px;
color: #333;
span {
display: inline-block;
border-bottom: 3px solid #43baca;
}
}
.el-dialog__body{
padding: 10px 40px !important;
height: calc(100% - 75px)
}
.el-dialog__header{
// padding: 30px 40px 10px;
padding: 0;
}
// 預(yù)覽對話框
.preview-content{
overflow:hidden;
position: relative;
height:100%;
.preview-data{
// 蓋住隱藏dom height 和 width 可控制元素在pdf頁面的大小
// min-height: 2600px;
width: 48%;
// width: 50%;
font-size: 20px;
z-index: -1;
position: fixed;
margin-top: -9999px;
.edit-content{
}
}
.error-pdf{
height:100%;
width:100%;
display:flex;
justify-content:center;
align-items:center;
font-size:20px;
.preview-icon{
font-size:36px;
padding-right:15px
}
}
// background-color:#ff0
}
.loading{
display: flex;
justify-content: center;
align-items: center;
// text-align: center;
height:100%;
// 蓋在隱藏的canvasDom上面
z-index: 2;
font-size: 20px;
i{
color: #409EFF;
font-size: 30px;
padding-right: 20px;
}
}
.el-dialog__footer{
text-align: center;
padding: 0 20px;
}
.dialog-footer{
display:flex;
justify-content: space-around;
}
}
}
</style>
遇到坑,注意的點(diǎn)
控制pdf頁面大小取決于html2canvas截圖dom的樣式熬荆,例子中是 .preview-data 這個類,可以觀察其中的css樣式舟山,其中z-index為負(fù)一的原因是html2canvas截圖只能截可視dom元素,如果display:none或者是克隆出來的虛擬dom惶看,都截不了捏顺,所以只能采取讓元素看不見的方法來取巧。
由于后臺返回的是富文本字符串纬黎,所以渲染的內(nèi)容代碼用innerHTML賦值了幅骄,賦值后dom還未渲染,此時不能立即使用html2canvas截取本今,需要等dom更新完成再截取拆座,這就是調(diào)用vue.$nextTick的原因主巍。
關(guān)于pdf分頁問題:position 這個變量控制第二頁的偏移位置,即利用偏移制造假分頁挪凑,實(shí)際上pdf渲染出來的東西都在同一頁上孕索,只是按高度切割后,把剩余的內(nèi)容合理偏移躏碳,使得看來像分頁了而已搞旭。
分頁參考: https://blog.csdn.net/weixin_43720095/article/details/87358705
4.預(yù)覽和下載pdf:jspdf很強(qiáng)大,有一個output('datauristring')的方法菇绵,可以生成一個dataurl外鏈肄渗,把它帶給iframe標(biāo)簽或者embed標(biāo)簽src就可以在線預(yù)覽(后臺直接返回pdf地址也是這種方法預(yù)覽),下載則更為簡單咬最,調(diào)用save方法即可翎嫡。