美文网首页vue
前端生成pdf?jspdf+html2canvas实现pdf预览

前端生成pdf?jspdf+html2canvas实现pdf预览

作者: 人猿Jim | 来源:发表于2020-05-20 16:47 被阅读0次

    最近做后台系统遇到挺多复杂的需求,比如导出pdf,word,excel
    一般这种需求后端如果存文件,然后传个流过来,前端就可以下载导出了。
    但是如果后端不存文件,只返回字符串(富文本字符串),这时候咋办?
    = =不知道,但是我遇到了,也只能头铁干了。


    吃货镇楼

    路还是有的,讲一下实现方式:

    html2canvas+jspdf

    具体需求是在弹窗内预览,然后点击下载可以生成对应pdf,预览pdf如果后端没有给文件地址,只返回富文本字符串(类似"<p>123</p>"),比较难做,但是办法还是有的,个人感觉难点在于html2canvas生成完整截图的时机和pdf的分页。
    贴一下效果:


    预览弹窗 生成的pdf文件

    思路:由于pdf不可直接编辑,个人思路是先将html截屏转化成图片,再把图片嵌入生成pdf。

    1. html2canvas:直接npm i html2canvas -S ,用法是截图dom然后转化为canvas,具体api可以去github上看。
    2. jspdf(项目编译报错所以选了个特定的版本):https://cdn.bootcss.com/jspdf/1.5.3/jspdf.debug.js

    直接贴代码(项目用的是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;">
                              <!-- 封面 封面页面伸缩(预览效果)通过调整.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: 预览pdf弹窗
     *@version V1.0
     *@API:
     *@ 参数 二选一, 二选一, 二选一
     *previewDom 页面中可看见的预览目标类名 (比如富文本在页面中显示,其容器div类名为'.fuwenben',直接传'.fuwenben'就可以生成预览页面dom的pdf了)
     *domData    页面中看不见的dom字符串(比如后台返回富文本字符串'<div>111</div>',直接传进来就可以生成pdf)
     *@ 事件
     * 需要在父组件指定关闭事件 onModalHidden
     * onModalHidden(){
            this.previewDialogVisible = false
        },
    */
    export default {
      name:'PreviewModal',
      props:{
        title: {
          type: String,
          default: ''
        },
        width: {
          type: String,
          default: ''
        },
        isVisible: {
          type: Boolean,
          default: false
        },
        // 预览目标类名
        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: {
        //当前日期
        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');
        },
        // 预览转pdf
        preview2pdf(){
          // 非法dom直接返回
          if (!this.previewDom && !this.domData) {
            this.isLoading = false
            return
          }
    
          const parentDom = document.querySelector('.preview-modal')
          const contentDom = parentDom.querySelector('.preview-content')
          // 进行截图的dom
          const canvasDom = document.querySelector('.preview-content .preview-data')
          // 找不到这个dom元素,返回
          if(!canvasDom) {
            this.isLoading = false
            return
          }
          // 传入富文本字符串,添加到原有的子节点中
          const mainBody = canvasDom.querySelector('.edit-content')
    
          if(mainBody) {
            // 添加内容
            mainBody.innerHTML = `<div>${this.domData}</div>`
          } else {
            // 外部传进来的dom元素
            const previewDom = document.querySelector(this.previewDom)
            mainBody.append(previewDom)
          }
    
          // 新建ifame标签在线展示pdf
          const iframe = document.createElement('iframe')
          iframe.height = '99%'
          iframe.width = '100%'
          // 进行dom截图 必须让dom更新完再调用
          this.$nextTick(()=>{
            html2canvas(canvasDom, {
              allowTaint: true,
              useCORS: true,
          }).then((canvas)=>{
                // 用iframe标签展示pdf生成预览效果
                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);
                // 取消生成状态
                this.isLoading = false
                var pdf = new jsPDF('', 'pt', 'a4');
    
                //有两个高度需要区分,一个是html页面的实际高度,和生成pdf的页面高度(841.89)
                //当内容未超过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标签展示
                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;
          }
            // 预览对话框
            .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>
    
    母猪焊接

    遇到坑,注意的点

    1. 控制pdf页面大小取决于html2canvas截图dom的样式,例子中是 .preview-data 这个类,可以观察其中的css样式,其中z-index为负一的原因是html2canvas截图只能截可视dom元素,如果display:none或者是克隆出来的虚拟dom,都截不了,所以只能采取让元素看不见的方法来取巧。

    2. 由于后台返回的是富文本字符串,所以渲染的内容代码用innerHTML赋值了,赋值后dom还未渲染,此时不能立即使用html2canvas截取,需要等dom更新完成再截取,这就是调用vue.$nextTick的原因。

    3. 关于pdf分页问题:position 这个变量控制第二页的偏移位置,即利用偏移制造假分页,实际上pdf渲染出来的东西都在同一页上,只是按高度切割后,把剩余的内容合理偏移,使得看来像分页了而已。
      分页参考: https://blog.csdn.net/weixin_43720095/article/details/87358705

    4.预览和下载pdf:jspdf很强大,有一个output('datauristring')的方法,可以生成一个dataurl外链,把它带给iframe标签或者embed标签src就可以在线预览(后台直接返回pdf地址也是这种方法预览),下载则更为简单,调用save方法即可。

    相关文章

      网友评论

        本文标题:前端生成pdf?jspdf+html2canvas实现pdf预览

        本文链接:https://www.haomeiwen.com/subject/uncsohtx.html