整合營(yíng)銷(xiāo)服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢(xún)熱線(xiàn):

          前端頁(yè)面如何原樣快速導(dǎo)出PDF?附示例

          何保持頁(yè)面樣式基本不變的前提下將HTML頁(yè)面導(dǎo)出為PDF,下面提供一些示例代碼,純屬個(gè)人原創(chuàng),如對(duì)你有幫助請(qǐng)記得加關(guān)注、加收藏、點(diǎn)贊、轉(zhuǎn)發(fā)、分享~謝謝~~

          • 基本思路:保持頁(yè)面樣式基本不變,使用 `html2canvas` 將頁(yè)面轉(zhuǎn)換為圖片,然后再通過(guò) `jspdf` 將圖片分頁(yè)導(dǎo)出為PDF文件(中間會(huì)遇到圖片或文字等內(nèi)容在分頁(yè)處被切割開(kāi)的問(wèn)題,如何解決了?詳見(jiàn)末尾干貨)
          • 上基礎(chǔ)代碼:下面為項(xiàng)目中實(shí)際代碼截取
          <div>
              <!-- 要打印的內(nèi)容區(qū) -->
              <div ref="contentRef">
                  <div class="print-item print-out-flow">這是脫離文檔流的內(nèi)容區(qū)域</div>
                  <div class="print-item">這是一行內(nèi)容,也是最小葉子元素內(nèi)容</div>
              </div>
              <!-- 打印內(nèi)容容器 -->
              <div ref="printContainerRef" class="print-container"></div>
          </div>
          /**
            * 1.使用一個(gè)隱藏div裝載有滾動(dòng)條的div.innerHTML
            * 2.隱藏div使用position: absolute, z-index: -999, left: -9999px, width: 900px 控制讓用戶(hù)無(wú)感知
            * 3.根據(jù)需要覆寫(xiě)隱藏div內(nèi)html樣式(例如textarea多行顯示有問(wèn)題, 可以新增一個(gè)隱藏的div
            *    包裹textarea的綁定值, 然后在打印樣式中覆寫(xiě)樣式, 隱藏textarea并顯示對(duì)應(yīng)div)
            */
          handleExport() {
             // 下面是VUE組件內(nèi)獲取DOM元素代碼,將內(nèi)容放置到打印區(qū)(定義的隱藏DIV)中
              const contentRef = this.$refs.contentRef as HTMLElement;
              const printContainerRef = this.$refs.printContainerRef as HTMLElement;
              // 打印區(qū)的需額外處理絕對(duì)定位值, 調(diào)整使得第一個(gè)元素的.top值為0, 以便于頁(yè)面計(jì)算
              printContainerRef.innerHTML = contentRef.innerHTML;	
              
              // 所有葉子div元素加上 print-item 樣式名, 脫離文檔流的額外添加 print-out-flow
              handlePrintItem(printContainerRef);  // 解決多頁(yè)內(nèi)容可能被切割問(wèn)題
              
              html2canvas(printContainerRef, {allowTaint: false, useCORS: true}).then((canvas: any) => {
                const contentHeight = canvas.height;
                const contentWidth = canvas.width;
                // pdf每頁(yè)顯示的內(nèi)容高度
                const pageHeight = contentWidth / 595.28 * 841.89;
                // 未生成pdf的頁(yè)面高度
                let offsetHeight = contentHeight;
                // 頁(yè)面偏移值
                let position = 0;
                // a4紙的尺寸[595.28, 841.89], canvas圖片按a4紙大小縮放后的寬高
                const imgWidth = 595.28;
                const imgHeight = 595.28 / contentWidth * contentHeight;
          
                const dataURL = canvas.toDataURL('image/jpeg', 1.0);
                const doc = new jsPDF('p', 'pt', 'a4');
          
                if (offsetHeight < pageHeight) {
                  doc.addImage(dataURL, 'JPEG', 0, 0, imgWidth, imgHeight);
                } else {
                  while (offsetHeight > 0) {
                    doc.addImage(dataURL, 'JPEG', 0, position, imgWidth, imgHeight);
                    offsetHeight -= pageHeight;
                    position -= 841.89;
          
                    if (offsetHeight > 0) {
                      doc.addPage();
                    }
                  }
                }
          
                doc.save(this.generateReportFileName());
                printContainerRef.innerHTML = '';
              });
          }

          上干貨代碼:上面分頁(yè)導(dǎo)出PDF可能網(wǎng)上能看到類(lèi)型代碼,但絕對(duì)找不到下面的代碼,純手搓解決分頁(yè)元素被切開(kāi)問(wèn)題(思路:獲取自身定位,如自己剛好在被分頁(yè)處,則加上一定的margin-top值將內(nèi)容向下移)

          /** 
           * 處理打印元素項(xiàng), 修復(fù)分頁(yè)后被切割的元素
           * @param printContainerRef 打印內(nèi)容div容器
           * @param itemClassName 打印最小元素標(biāo)識(shí)類(lèi)名
           * @param outFlowClassName 脫離文檔流的元素標(biāo)識(shí)類(lèi)名
           */
          export function handlePrintItem(
            printContainerRef: HTMLElement,
            itemClassName: string = 'print-item',
            outFlowClassName: string = 'print-out-flow'
          ): void {
            const rootClientRect = printContainerRef.getBoundingClientRect();
            // 初始化頁(yè)面相關(guān)數(shù)據(jù)
            const totalHeight = rootClientRect.height;  // 內(nèi)容總高度
            const a4PageHeight = (printContainerRef.clientWidth / 595.28) * 841.89; // a4紙高度
            let pageNum = Math.ceil(totalHeight / a4PageHeight);  // 總頁(yè)數(shù)
            let addPageHeight = 0;  // 修正被分割元素而增加的頁(yè)面高度總和
            let currentPage = 1;  // 當(dāng)前正在處理切割的頁(yè)面
            const splitItemObj: { [key: number]: HTMLElement[] } = {};  // 內(nèi)容中各頁(yè)被切割元素存儲(chǔ)對(duì)象
          
            const printItemNodes: NodeListOf<HTMLElement> = printContainerRef.querySelectorAll(`.${itemClassName}`);
            for (let item of printItemNodes) {
              // 如果當(dāng)前頁(yè)已經(jīng)是最后一頁(yè), 則中斷判斷
              if (currentPage >= pageNum) {
                break;
              }
          
              // 獲取元素絕對(duì)定位數(shù)據(jù)
              const clientRect = item.getBoundingClientRect();
              let top = clientRect.top;
              const selfHeight = clientRect.height;
              // 如果當(dāng)前元素距離頂部高度大于當(dāng)前頁(yè)面頁(yè)腳高度, 則開(kāi)始判斷下一頁(yè)頁(yè)腳被切割元素
              if (top > currentPage * a4PageHeight) {
                // 換頁(yè)前修正上一頁(yè)被切割元素
                addPageHeight += fixSplitItems(currentPage, a4PageHeight, splitItemObj[currentPage], outFlowClassName);
                pageNum = Math.ceil((totalHeight + addPageHeight) / a4PageHeight);
                top = item.getBoundingClientRect().top;
                currentPage++;
              }
              // 如果元素剛好處于兩頁(yè)之間, 則記錄該元素
              if (top > (currentPage - 1) * a4PageHeight && top < currentPage * a4PageHeight && top + selfHeight > currentPage * a4PageHeight) {
                if (!splitItemObj[currentPage]) {
                  splitItemObj[currentPage] = [];
                }
                splitItemObj[currentPage].unshift(item);
                // 如果當(dāng)前元素是最后一個(gè)元素, 則直接處理切割元素, 否則交由處理下一頁(yè)元素時(shí)再處理切割
                if (item === printItemNodes[printItemNodes.length - 1]) {
                  fixSplitItems(currentPage, a4PageHeight, splitItemObj[currentPage], outFlowClassName);
                }
              }
            }
          }
          
          /**
            * 修復(fù)當(dāng)前頁(yè)所有被切割元素
            * @param currentPage 當(dāng)前頁(yè)
            * @param pageHeight 每頁(yè)高度
            * @param splitElementItems 當(dāng)前被切割元素?cái)?shù)組
            * @param outFlowClassName 脫離文檔流的樣式類(lèi)名
            */
          function fixSplitItems(
            currentPage: number,
            pageHeight: number,
            splitElementItems: HTMLElement[],
            outFlowClassName: string
          ): number {
            if (!splitElementItems || !splitElementItems.length) {
              return 0;
            }
          
            const yMargin = 5;  // y方向距離頁(yè)眉的距離
            const splitItemsMinTop = getSplitItemsMinTop(splitElementItems);
            if (!splitItemsMinTop) {
              return 0;
            }
          
            let fixHeight = currentPage * pageHeight - splitItemsMinTop + yMargin;
            const outFlowElement = splitElementItems.find((item) => item.classList.contains(outFlowClassName));
            if (outFlowElement && outFlowElement.parentElement) {
              const parentPreviousElement = outFlowElement.parentElement.previousElementSibling as HTMLElement;
              fixHeight += getMarinTopNum(parentPreviousElement, outFlowElement.parentElement);
              outFlowElement.parentElement.style.marginTop = `${fixHeight}px`;
              return fixHeight;
            }
          
            splitElementItems.forEach((splitElement) => {
              splitElement.style.marginTop = `${fixHeight}px`;
            });
            return fixHeight;
          }
          
          /**
            * 獲取被切割元素?cái)?shù)組中最小高度值(如一行有多個(gè)元素被切割,則選出距離頂部最小的高度值)
            * @param splitElementItems 當(dāng)前被切割元素?cái)?shù)組
            */
          function getSplitItemsMinTop(
            splitElementItems: HTMLElement[]
          ): number | undefined {
            // 獲取元素中最小top值作為基準(zhǔn)進(jìn)行修正
            let minTop: number | undefined;
            let minElement: HTMLElement | undefined;
            splitElementItems.forEach((splitElement) => {
              let top = splitElement.getBoundingClientRect().top;
              if (minTop) {
                minTop = top < minTop ? top : minTop;
                minElement = top < minTop ? splitElement : minElement;
              } else {
                minTop = top;
                minElement = splitElement;
              }
            });
          
            // 修正當(dāng)前節(jié)點(diǎn)及其前面同層級(jí)節(jié)點(diǎn)的margin值
            if (minTop && minElement) {
              const previousElement = splitElementItems[splitElementItems.length - 1].previousElementSibling as HTMLElement;
              minTop -= getMarinTopNum(previousElement, minElement);
            }
            return minTop;
          }
          
          /**
            * 通過(guò)前一個(gè)兄弟元素和元素自身的位置確認(rèn)一個(gè)距離頂部高度修正值
            * @param previousElement 前一個(gè)兄弟元素
            * @param curElement 當(dāng)前元素
            */
          function getMarinTopNum(previousElement: HTMLElement, curElement: HTMLElement): number {
            let preMarginNum = 0;
            let curMarginNum = 0;
            if (previousElement) {
              // 獲取外聯(lián)樣式需要getComputedStyle(), 直接.style時(shí)對(duì)象的值都為空
              const previousMarginBottom = window.getComputedStyle(previousElement).marginBottom;
              preMarginNum = previousMarginBottom ? Number(previousMarginBottom.replace('px', '')) : 0;
            }
            const marginTop = window.getComputedStyle(curElement).marginTop;
            curMarginNum = marginTop ? Number(marginTop.replace('px', '')) : 0;
            return preMarginNum > curMarginNum ? preMarginNum : curMarginNum;
          }

          以上純?cè)瓌?chuàng)!歡迎加關(guān)注、加收藏、點(diǎn)贊、轉(zhuǎn)發(fā)、分享(代碼閑聊站)~

          文介紹

          我使用 Fabric.js 的版本是 4.6.0。

          這次要實(shí)現(xiàn)的效果是:在本地上傳一張圖片,然后渲染到 canvas 里(當(dāng)做背景圖)。

          我會(huì)用 原生 的方法實(shí)現(xiàn)一次,然后再在 Vue3 + Element-plus 環(huán)境下實(shí)現(xiàn)一次。

          最后聊聊我在真實(shí)項(xiàng)目中的做法。



          需求:

          1. 通過(guò)點(diǎn)擊上傳按鈕上傳圖片
          2. 拿到圖片,放到畫(huà)布上渲染

          需要注意的是,本文主要實(shí)現(xiàn) 上傳圖片并渲染到畫(huà)布 的邏輯,所以沒(méi)有做上傳文件類(lèi)型的限制,也沒(méi)做文件大小限制。如果你的業(yè)務(wù)中需要限制文件類(lèi)型,只需在本案例基礎(chǔ)上添加限制的方法就行了。


          本文所有代碼都在文末給出的倉(cāng)庫(kù)里。

          如果本文內(nèi)容對(duì)你有所幫助,也請(qǐng)你幫我點(diǎn)個(gè)贊唄~


          原生操作

          通過(guò) <input type="file" /> 獲取圖片路徑,會(huì)受到瀏覽器安全策略影響,所以需要處理一下。

          實(shí)現(xiàn)邏輯:

          • 定義好 上傳按鈕畫(huà)布(HTML部分);
          • 初始化畫(huà)布;
          • 點(diǎn)擊上傳按鈕 獲取圖片地址(這里需要處理一下安全策略的問(wèn)題);
          • 拿到圖片路徑,使用 canvas.setBackgroundImage 將圖片設(shè)置成畫(huà)布背景;
          • canvas.setBackgroundImage 的回調(diào)函數(shù)里刷新一下畫(huà)布;


          <div>
            <input type="file" name="file" id="upload" onchange="handleUpload()" />
            <button onclick="saveCanvas()">保存</button>
          </div>
          
          <canvas id="canvas" width="600" height="600" style="border: 1px solid #ccc;"></canvas>
          
          <!-- 引入fabric.js -->
          <script src="https://cdn.bootcdn.net/ajax/libs/fabric.js/460/fabric.js"></script>
          <script>
          
          // 上傳文件的DOM元素
          const uploadEl = document.getElementById("upload")
          
          // 畫(huà)布
          let canvas = null
          
          // 初始化畫(huà)布
          function initCanvas() {
            canvas = new fabric.Canvas('canvas')
          }
          
          // 上傳文件事件
          function handleUpload() {
            // 上傳文件列表的第一個(gè)文件
            const file = uploadEl.files[0]
          
            // 圖片文件的地址
            let imgPath = null
          
            // 獲取圖片文件真實(shí)路徑
            // 由于瀏覽器安全策略,現(xiàn)在需要這么做了
            // 這段代碼是網(wǎng)上復(fù)制下來(lái)的,想深入理解的可以百度搜搜 “C:\fakepath\”
            if (window.createObjcectURL != undefined) {
              imgPath = window.createOjcectURL(file); 
            } else if (window.URL != undefined) {
              imgPath = window.URL.createObjectURL(file); 
            } else if (window.webkitURL != undefined) {
              imgPath = window.webkitURL.createObjectURL(file);
            }
          
            // 設(shè)置畫(huà)布背景,并刷新畫(huà)布
            canvas.setBackgroundImage(
              imgPath,
              canvas.renderAll.bind(canvas)
            )
          }
          
          // 保存畫(huà)布
          function saveCanvas() {
            let data = canvas.toJSON()
            console.log(data)
          }
          
          window.onload = function() {
            initCanvas()
          }
          </script>

          上面的實(shí)現(xiàn)方式,如果是在純前端的環(huán)境下,保存時(shí)背景圖是地址是本地地址( "blob:http://127.0.0.1:5500/383e7860-3fa5-43b9-92d9-e7165760e60b" )。

          這樣其實(shí)不是很好,如果在別的電腦想通過(guò) 反序列化 渲染出來(lái)的時(shí)候,可能會(huì)出現(xiàn)一點(diǎn)問(wèn)題。


          如果純前端實(shí)現(xiàn)的方式,可以將圖片轉(zhuǎn)成 base64 再生成背景圖。

          fabric.Image.fromURL(
            imgPath, // 真實(shí)圖片地址
            img => {
              // 將圖片設(shè)置再畫(huà)布上,然后重新渲染畫(huà)布,圖片就出來(lái)了。
              canvas.setBackgroundImage(
                img, // 要設(shè)置的圖片
                canvas.renderAll.bind(canvas) // 重新渲染畫(huà)布
              )
            }
          )


          在 element-plus 里的操作

          我使用了 vue3 + element-plus 。



          實(shí)現(xiàn)邏輯和原生方法一樣。 唯一不同的是本例用了 el-upload 這個(gè)組件。 我將圖片文件轉(zhuǎn)成 base64 再放進(jìn)畫(huà)布。

          <template>
            <div>
              <div class="btn__x">
                <!-- 上傳組件 -->
                <el-upload
                  action="https://jsonplaceholder.typicode.com/posts/"
                  :multiple="false"
                  :show-file-list="false"
                  :limit="1"
                  accept=".jpg,.png"
                  :before-upload="onProgress"
                >
                  <el-button type="primary">上傳</el-button>
                </el-upload>
          
                <!-- 保存按鈕(序列化) -->
                <el-button @click="saveCanvas">保存:打開(kāi)控制臺(tái)查看</el-button>
              </div>
          
              <!-- 畫(huà)布 -->
              <canvas id="canvas" width="600" height="600" style="border: 1px solid #ccc;"></canvas>
            </div>
          </template>
          
          <script setup>
          import { onMounted, ref } from 'vue'
          import { useStore } from 'vuex'
          import { fabric } from 'fabric'
          
          const store = useStore()
          
          // 畫(huà)布
          let canvas = null
          
          // 上傳
          function onProgress(file) {
            // 拿圖片文件
            const reader = new FileReader()
            reader.readAsDataURL(file)
          
            // 圖片文件完全拿到后執(zhí)行
            reader.onload = () => {
              // 轉(zhuǎn)換成base64格式
              const base64Img = reader.result
          
              // 將base64圖片設(shè)置成背景
              canvas.setBackgroundImage(
                base64Img,
                canvas.renderAll.bind(canvas) // 刷新畫(huà)布
              )
            }
            return false
          }
          
          // 初始化畫(huà)布
          function init() {
            canvas = new fabric.Canvas('canvas')
          }
          
          // 保存
          function saveCanvas() {
            console.log(canvas.toJSON())
          }
          
          // 頁(yè)面加載完成后,初始化畫(huà)布
          onMounted(() => {
            init()
          })
          </script>
          
          <style lang="scss" scoped>
          .btn__x {
            display: flex;
          
            .el-button {
              margin-right: 20px;
            }
          }
          </style>


          在正式開(kāi)發(fā)中

          在正式的項(xiàng)目開(kāi)發(fā)中,上面兩種情況出現(xiàn)的概率應(yīng)該不多(除非你的后端伙伴是個(gè)懶人)

          先說(shuō)說(shuō)上面兩種情況存在的問(wèn)題:

          1. 圖片路徑是本地地址,保存到服務(wù)器是沒(méi)意義的。
          2. 轉(zhuǎn)成 base64 來(lái)保存,字段可能會(huì)很大。



          這種情況放到服務(wù)器可能沒(méi)什么用的。 127.0.0.1 是你本機(jī)的,你上傳的圖片在別人的電腦可能無(wú)法查看。



          這種情況雖然問(wèn)題不大,但 backgroundImage.src 的值有點(diǎn)大。


          我在項(xiàng)目中的做法:

          1. 前端上傳圖片給后端
          2. 后端把圖片存到服務(wù)器,然后返回一個(gè)圖片url給前端
          3. 前端拿到圖片url,再放到 fabric 里渲染出來(lái)

          這樣做的好處是 backgroundImage.src 的值變短了。


          在正式項(xiàng)目中,你可能還要考慮到背景圖的大小和畫(huà)布大小不匹配問(wèn)題。 你可以參考 《Fabric.js 從入門(mén)到膨脹》 中 “拉伸背景圖” 這小節(jié)。


          代碼倉(cāng)庫(kù)

          原生方式實(shí)現(xiàn):https://gitee.com/k21vin/fabricjs-demo/blob/master/demos/UploadImg/index.html

          在 Vue3+Element-plus 中實(shí)現(xiàn):https://gitee.com/k21vin/front-end-data-visualization/blob/master/src/views/FabricJS/Demo/pages/UploadImg/UploadImg.vue


          者:政采云前端團(tuán)隊(duì)

          轉(zhuǎn)發(fā)鏈接:https://juejin.im/post/5ea574cc518825736e57fcca


          主站蜘蛛池模板: 久久久久久人妻一区精品| 国产乱码一区二区三区爽爽爽| 亚洲国产欧美国产综合一区| 国内精品一区二区三区最新| 国产高清一区二区三区四区| 久久久综合亚洲色一区二区三区| 午夜性色一区二区三区不卡视频| 香蕉视频一区二区三区| 无码一区二区三区亚洲人妻| 成人免费区一区二区三区| 日本在线视频一区二区三区| 一区二区三区中文| 国产精品一级香蕉一区| 日本一区二区高清不卡| 精品国产一区二区三区香蕉| 国精无码欧精品亚洲一区| 国产乱码精品一区二区三 | 一区在线免费观看| 国产一区二区在线观看麻豆| 亚洲欧美一区二区三区日产 | 国产成人精品视频一区| 无码国产精品一区二区免费式影视| 天堂不卡一区二区视频在线观看| 国产精品一区二区久久乐下载 | 毛片无码一区二区三区a片视频| 91一区二区三区四区五区| 国产精品电影一区| 国产日产久久高清欧美一区| 变态拳头交视频一区二区| 久久久久人妻精品一区三寸蜜桃| 内射少妇一区27P| 99精品久久精品一区二区| 亚洲欧洲一区二区| 无码精品尤物一区二区三区| 无码aⅴ精品一区二区三区| 日本香蕉一区二区三区| 日韩精品一区二区三区中文版| 国产一区二区三区不卡在线观看| 亚洲一区无码中文字幕| 欧美亚洲精品一区二区| 精品一区二区三区在线播放|