前端生成pdf外盯?jspdf+html2canvas實(shí)現(xiàn)pdf預(yù)覽和導(dǎo)出

最近做后臺系統(tǒng)遇到挺多復(fù)雜的需求,比如導(dǎo)出pdf翼雀,word饱苟,excel
一般這種需求后端如果存文件,然后傳個流過來狼渊,前端就可以下載導(dǎo)出了箱熬。
但是如果后端不存文件,只返回字符串(富文本字符串),這時候咋辦城须?
= =不知道蚤认,但是我遇到了,也只能頭鐵干了酿傍。


吃貨鎮(zhèn)樓

路還是有的烙懦,講一下實(shí)現(xiàn)方式:

html2canvas+jspdf

具體需求是在彈窗內(nèi)預(yù)覽,然后點(diǎn)擊下載可以生成對應(yīng)pdf赤炒,預(yù)覽pdf如果后端沒有給文件地址氯析,只返回富文本字符串(類似"<p>123</p>"),比較難做莺褒,但是辦法還是有的,個人感覺難點(diǎn)在于html2canvas生成完整截圖的時機(jī)和pdf的分頁掩缓。
貼一下效果:


預(yù)覽彈窗
生成的pdf文件

思路:由于pdf不可直接編輯,個人思路是先將html截屏轉(zhuǎn)化成圖片遵岩,再把圖片嵌入生成pdf你辣。

  1. html2canvas:直接npm i html2canvas -S ,用法是截圖dom然后轉(zhuǎn)化為canvas尘执,具體api可以去github上看舍哄。
  2. 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)

  1. 控制pdf頁面大小取決于html2canvas截圖dom的樣式熬荆,例子中是 .preview-data 這個類,可以觀察其中的css樣式舟山,其中z-index為負(fù)一的原因是html2canvas截圖只能截可視dom元素,如果display:none或者是克隆出來的虛擬dom惶看,都截不了捏顺,所以只能采取讓元素看不見的方法來取巧。

  2. 由于后臺返回的是富文本字符串纬黎,所以渲染的內(nèi)容代碼用innerHTML賦值了幅骄,賦值后dom還未渲染,此時不能立即使用html2canvas截取本今,需要等dom更新完成再截取拆座,這就是調(diào)用vue.$nextTick的原因主巍。

  3. 關(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方法即可翎嫡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市永乌,隨后出現(xiàn)的幾起案子惑申,更是在濱河造成了極大的恐慌,老刑警劉巖翅雏,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件圈驼,死亡現(xiàn)場離奇詭異,居然都是意外死亡望几,警方通過查閱死者的電腦和手機(jī)碗脊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來橄妆,“玉大人,你說我怎么就攤上這事祈坠『δ耄” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵赦拘,是天一觀的道長慌随。 經(jīng)常有香客問我,道長躺同,這世上最難降的妖魔是什么阁猜? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮蹋艺,結(jié)果婚禮上剃袍,老公的妹妹穿的比我還像新娘。我一直安慰自己捎谨,他們只是感情好民效,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布憔维。 她就那樣靜靜地躺著,像睡著了一般畏邢。 火紅的嫁衣襯著肌膚如雪业扒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天舒萎,我揣著相機(jī)與錄音程储,去河邊找鬼。 笑死臂寝,一個胖子當(dāng)著我的面吹牛章鲤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播交煞,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼咏窿,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了素征?” 一聲冷哼從身側(cè)響起集嵌,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎御毅,沒想到半個月后根欧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡端蛆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年凤粗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片今豆。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡嫌拣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出呆躲,到底是詐尸還是另有隱情异逐,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布插掂,位于F島的核電站灰瞻,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏辅甥。R本人自食惡果不足惜酝润,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望璃弄。 院中可真熱鬧要销,春花似錦、人聲如沸夏块。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至凳鬓,卻和暖如春茁肠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背缩举。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工垦梆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人仅孩。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓托猩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親辽慕。 傳聞我的和親對象是個殘疾皇子京腥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344