vue使用pdfjs-dist+fabric實現(xiàn)pdf電子簽章的思路詳解
一、需求
最近領(lǐng)導(dǎo)提了一個新需求:仿照e簽寶,實現(xiàn)pdf電子簽章!
最終實現(xiàn)效果圖
這是做出來的效果圖,當(dāng)然還有很多待修改
二、思路
然后我就去看了下人家e簽寶的操作界面,左側(cè)是印章,右側(cè)是pdf,然后拖拽印章到pdf上面,點擊保存,下次打開時顯示印章的位置。
思路:我首先想到了拖拽、pdf預(yù)覽、坐標(biāo);分工明確,前端來實現(xiàn)拖拽,pdf預(yù)覽及把印章信息和坐標(biāo)傳給后端,后端只需要把信息和坐標(biāo)保存下來就可以了。
三、使用插件
之前實現(xiàn)pdf預(yù)覽就是通過window.open,打開一個窗口,顯示pdf,功能很多,但是和需求不符,需要做的事是把pdf顯示出來,同時可以可以拖拽印章到上面去
,也不要放大與縮小及其他的功能。百度下了,說是用pdfjs-dist
,這個pdf插件可以自定義很多的功能,但是實際用起來,發(fā)現(xiàn)好坑。。最后去百度了下,vue實現(xiàn)pdf電子簽章, 看有沒有現(xiàn)成的,然后還真找到了一個。js處理pdf展示、分頁和簽章等功能,下載到本地(只許查看index.htm
l文件即可)后發(fā)現(xiàn)大佬用的不是vue-cli
腳手架,是引用的cdn鏈接,然后就cv到項目里面了,跟著步驟,安裝了pdfjs-dist
插件(pdf插件)和fabric
插件(專門處理印章的插件)這兩個插件,但是項目本地運行后,報錯了。。
四、遇到的問題
1.TypeError: Cannot read properties of undefined( reading 'Globalworkeroptions ')
百思不得其解啊,照著步驟來的啊,為啥呢,然后又回去看了下大佬的代碼,發(fā)現(xiàn)他的pdf.js不是用的pdfjs-dist
,而是引入的pdf的cdn鏈接
然后我就在項目的public/index.html下面引入這個鏈接
pdf路徑則是使用的一個在線的pdf鏈接,https://www.gjtool.cn/pdfh5/git.pdf
,發(fā)現(xiàn)可以打開了(樣式做了些修改)
2.Dev Tools failed to load source map: Could not load content for https //mozilla github.ia/pdf js/build/pdf js map: Load canceled due to loadtimeout
開始覺得似乎已經(jīng)大功告成了,到時候和后端商量下返回數(shù)據(jù)的格式就完事了的,誰知道還是有問題的。。多次打開關(guān)閉pdf后,有時候pdf會不加載出來了。人麻了,然后看了下提示,加載超時了,取消加載。
明顯是cdn鏈接的問題,那就把pdf.js文件下載到本地唄,本地加載快,應(yīng)該不會出現(xiàn)加載超時的問題,結(jié)果還是有問題。
唉,真的是服了,使用cdn鏈接吧,會加載超時,下載到本地引用吧,又會報這么個莫名其妙的問題,然后今天瀏覽博客時,發(fā)現(xiàn)一個兄弟碰到了一樣的問題,哈哈,發(fā)現(xiàn)還是引入pdf方式的問題
/* 引用cdn鏈接,可以使用但會加載超時 */ // let pdfjsLib = window["pdfjs-dist/build/pdf"]; // pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://mozilla.github.io/pdf.js/build/pdf.worker.js'; /* 下載到本地,看著官方文檔引用,報個莫名其妙的錯 */ // import pdfjsLib from 'pdfjs-dist'; // pdfjsLib.GlobalWorkerOptions.workerSrc='pdfjs-dist/build/pdf.worker.js'; /* 下載到本地,照著大佬的方式引用,完美! */ let pdfjsLib =require("pdfjs-dist/legacy/build/pdf.js"); import workerSrc from "pdfjs-dist/legacy/build/pdf.worker.entry"; pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
這是大佬的博客鏈接pdf.js 使用攻略及錯誤集合
不過這項目的電子簽章有些與眾不同,用戶打開的pdf,是可以自定義的,即用戶打開彈窗,在tinymac編輯器里面輸入內(nèi)容,然后切換tab,會立即生成一個pdf,接下來才是用戶使用電子簽章的過程
以下是電子簽章的主要代碼,和大佬的index.html的代碼差不多,就是做了點修改(ps:目前印章的位置和坐標(biāo)保存,使用的得本地緩存,便于調(diào)試,后期會保存到接口里面?。?/p>
代碼部分
首先 引入pdfjs-dist插件和fabric插件
npm install pdfjs-dist
npm i fabric --save
2023年10月12日修改
pdfjs-dist的安裝版本盡量安裝2.0的版本,如 npm install pdfjs-dist@2.16.105 --save
,不然直接安裝最新的pdfjs-dist版本可能出現(xiàn)pdf不顯示的問題
html部分
<div id="elesign" class="elesign"> <el-row> <el-col :span="4" style="margin-top:1%;"> <div class="left-title">我的印章</div> <draggable v-model="mainImagelist" :group="{ name: 'itext', pull: 'clone' }" :sort="false" @end="end"> <transition-group type="transition"> <li v-for="item in mainImagelist" :key="item" class="item" style="text-align:center;"> <img :src="item" width="100%;" height="100%" class="imgstyle" /> </li> </transition-group> </draggable> </el-col> <el-col :span="16" style="text-align:center;" class="pCenter"> <div class="page"> <!-- <el-button class="btn-outline-dark" @click="zoomIn">-</el-button> <span style="color:red;">{{(percentage*100).toFixed(0)+'%'}}</span> <el-button class="btn-outline-dark" @click="zoomOut">+</el-button> --> <el-button class="btn-outline-dark" @click="prevPage">上一頁</el-button> <el-button class="btn-outline-dark" @click="nextPage">下一頁</el-button> <el-button class="btn-outline-dark">{{ pageNum }}/{{ numPages }}頁</el-button> <el-input-number style="margin:0 5px;border-radius:5px;" class="btn-outline-dark" v-model="pageNum" :min="1" :max="numPages" label="輸入頁碼"></el-input-number> <el-button class="btn-outline-dark" @click="cutover">跳轉(zhuǎn)</el-button> </div> <canvas id="the-canvas" /> <!-- 蓋章部分 --> <canvas id="ele-canvas"></canvas> <div class="ele-control" style="margin-bottom:2%;"> <el-button class="btn-outline-dark" @click="removeSignature"> 刪除簽章</el-button> <el-button class="btn-outline-dark" @click="clearSignature"> 清除所有簽章</el-button> <el-button class="btn-outline-dark" @click="submitSignature">提交所有簽章信息</el-button> </div> </el-col> <el-col :span="4" style="margin-top:1%;"> <div class="left-title">任務(wù)信息</div> <div style="text-align:center;"> <div> <div class="right-item"> <div class="right-item-title">文件主題</div> <div class="detail-item-desc">{{ taskInfo.title }}</div> </div> <div class="right-item"> <div class="right-item-title">發(fā)起方</div> <div class="detail-item-desc">{{ taskInfo.uname }}</div> </div> <div class="right-item"> <div class="right-item-title">截止時間</div> <div class="detail-item-desc">{{ taskInfo.endtime }}</div> </div> </div> </div> </el-col> </el-row> </div>
js部分
import {fabric} from 'fabric'; let pdfjsLib =require("pdfjs-dist/legacy/build/pdf.js"); import workerSrc from "pdfjs-dist/legacy/build/pdf.worker.entry"; pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc; import draggable from "vuedraggable"; export default { components: {draggable}, data() { return { //pdf預(yù)覽 pdfUrl: '', pdfDoc: null, numPages: 1, pageNum: 1, scale: 2.2, pageRendering: false, pageNumPending: null, sealUrl: '', signUrl: '', canvas: null, ctx: null, canvasEle: null, whDatas: null, mainImagelist: [], taskInfo: {}, } }, computed: { hasSigna() { return this.canvasEle && this.canvasEle.getObjects()[0] ? true : false; }, }, created(){ var that = this; that.mainImagelist = [require('./sign.png'),require('./seal.png')]; that.taskInfo = {'title':'測試蓋章', uname:'張三', endtime:'2021-09-01 17:59:59'}; }, methods: { //pdf預(yù)覽 // zoomIn() { // console.log("縮小"); // if(this.scale<=0.5){ // this.$message.error("已經(jīng)顯示最小比例") // }else{ // this.scale-=0.1; // this.percentage-=0.1; // this.renderPage(this.pageNum); // this.renderFabric(); // } // }, // zoomOut() { // console.log("放大") // if(this.scale>=2.2){ // this.$message.error('已經(jīng)顯示最大比例') // }else{ // this.scale+=0.1; // this.percentage+=0.1; // this.renderPage(this.pageNum); // this.renderFabric(); // } // }, renderPage(num) { let _this = this; this.pageRendering = true; return this.pdfDoc.getPage(num).then((page) => { let viewport = page.getViewport({ scale: _this.scale });//設(shè)置視口大小 _this.canvas.height = viewport.height; _this.canvas.width = viewport.width; // Render PDF page into canvas context let renderContext = { canvasContext: _this.ctx, viewport: viewport, }; let renderTask = page.render(renderContext); // Wait for rendering to finish renderTask.promise.then(() => { _this.pageRendering = false; if (_this.pageNumPending !== null) { // New page rendering is pending this.renderPage(_this.pageNumPending); _this.pageNumPending = null; } }); }); }, queueRenderPage(num) { if (this.pageRendering) { this.pageNumPending = num; } else { this.renderPage(num); } }, prevPage() { this.confirmSignature(); if (this.pageNum <= 1) { return; } this.pageNum--; }, nextPage() { this.confirmSignature(); if (this.pageNum >= this.numPages) { return; } this.pageNum++; }, cutover() { this.confirmSignature(); }, //渲染pdf,到時還會蓋章信息,在渲染時,同時顯示出來,不應(yīng)該在切換頁碼時才顯示印章信息 showpdf(pdfUrl) { let caches = JSON.parse(localStorage.getItem('signs')); //獲取緩存字符串后轉(zhuǎn)換為對象 console.log(caches); if(caches == null) return false; let datas = caches[this.pageNum]; if(datas != null && datas != undefined) { for (let index in datas) { this.addSeal(datas[index].sealUrl, datas[index].left, datas[index].top, datas[index].index); } } this.canvas = document.getElementById("the-canvas"); this.ctx = this.canvas.getContext("2d"); pdfjsLib.getDocument({url:pdfUrl, rangeChunkSize:65536, disableAutoFetch:false}).promise.then((pdfDoc_) => { this.pdfDoc = pdfDoc_; this.numPages = this.pdfDoc.numPages; this.renderPage(this.pageNum).then(() => { this.renderPdf({ width: this.canvas.width, height: this.canvas.height, }); }); this.commonSign(this.pageNum, true); }); }, /** * 蓋章部分開始 */ // 設(shè)置繪圖區(qū)域?qū)捀? renderPdf(data) { this.whDatas = data; // document.querySelector("#elesign").style.width = data.width + "px"; }, // 生成繪圖區(qū)域 renderFabric() { let canvaEle = document.querySelector("#ele-canvas"); let pCenter=document.querySelector(".pCenter"); canvaEle.width = pCenter.clientWidth; // canvaEle.height = (this.whDatas.height)*(this.scale); canvaEle.height = this.whDatas.height; this.canvasEle = new fabric.Canvas(canvaEle); let container = document.querySelector(".canvas-container"); container.style.position = "absolute"; container.style.top = "50px"; // container.style.left = "30%"; }, // 相關(guān)事件操作喲 canvasEvents() { // 拖拽邊界 不能將圖片拖拽到繪圖區(qū)域外 this.canvasEle.on("object:moving", function (e) { var obj = e.target; // if object is too big ignore if(obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width){ return; } obj.setCoords(); // top-left corner if(obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0){ obj.top = Math.max(obj.top, obj.top-obj.getBoundingRect().top); obj.left = Math.max(obj.left, obj.left-obj.getBoundingRect().left); } // bot-right corner if(obj.getBoundingRect().top+obj.getBoundingRect().height > obj.canvas.height || obj.getBoundingRect().left+obj.getBoundingRect().width > obj.canvas.width){ obj.top = Math.min(obj.top, obj.canvas.height-obj.getBoundingRect().height+obj.top-obj.getBoundingRect().top); obj.left = Math.min(obj.left, obj.canvas.width-obj.getBoundingRect().width+obj.left-obj.getBoundingRect().left); } }); }, // 添加公章 addSeal(sealUrl, left, top, index) { fabric.Image.fromURL( sealUrl, (oImg) => { oImg.set({ left: left, top: top, // angle: 10, scaleX: 0.8, scaleY: 0.8, index:index, }); // oImg.scale(0.5); //圖片縮小一 this.canvasEle.add(oImg); } ); }, // 刪除簽章 removeSignature() { this.canvasEle.remove(this.canvasEle.getActiveObject()) }, //翻頁展示蓋章信息 commonSign(pageNum, isFirst = false) { if(isFirst == false) this.canvasEle.remove(this.canvasEle.clear()); //清空頁面所有簽章 let caches = JSON.parse(localStorage.getItem('signs')); //獲取緩存字符串后轉(zhuǎn)換為對象 console.log(caches); if(caches == null) return false; let datas = caches[this.pageNum]; if(datas != null && datas != undefined) { for (let index in datas) { this.addSeal(datas[index].sealUrl, datas[index].left, datas[index].top, datas[index].index); } } }, //確認簽章位置并保存到緩存 confirmSignature() { let data = this.canvasEle.getObjects(); //獲取當(dāng)前頁面內(nèi)的所有簽章信息 let caches = JSON.parse(localStorage.getItem('signs')); //獲取緩存字符串后轉(zhuǎn)換為對象 let signDatas = {}; //存儲當(dāng)前頁的所有簽章信息 let i = 0; // let sealUrl = ''; for(var val of data) { signDatas[i] = { width: val.width, height: val.height, top: val.top, left: val.left, angle: val.angle, translateX: val.translateX, translateY: val.translateY, scaleX: val.scaleX, scaleY: val.scaleY, pageNum: this.pageNum, sealUrl: this.mainImagelist[val.index], index:val.index } i++; } if(caches == null) { caches = {}; caches[this.pageNum] = signDatas; } else { caches[this.pageNum] = signDatas; } localStorage.setItem('signs', JSON.stringify(caches)); //對象轉(zhuǎn)字符串后存儲到緩存 }, //提交數(shù)據(jù) submitSignature() { this.confirmSignature(); let caches = localStorage.getItem('signs'); console.log(JSON.parse(caches)); return false }, //清空數(shù)據(jù) clearSignature() { this.canvasEle.remove(this.canvasEle.clear()); //清空頁面所有簽章 localStorage.removeItem('signs'); //清除緩存 }, end(e){ this.addSeal(this.mainImagelist[e.newDraggableIndex], e.originalEvent.layerX, e.originalEvent.layerY, e.newDraggableIndex) }, //設(shè)置PDF預(yù)覽區(qū)域高度 setPdfArea(){ this.pdfUrl = 'https://www.gjtool.cn/pdfh5/git.pdf'; this.pdfurl=res.data.data.pdfurl; this.$nextTick(() => { this.showpdf(this.pdfUrl);//接口返回的應(yīng)該還有蓋章信息,不只是pdf }); }, }, watch: { whDatas: { handler() { const loading = this.$loading({ lock: true, text: 'Loading', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }); if (this.whDatas) { console.log(this.whDatas) loading.close(); this.renderFabric(); this.canvasEvents(); let eleCanvas=document.querySelector("#ele-canvas"); eleCanvas.style="border:1px solid #5ea6ef"; } }, }, pageNum: function() { this.commonSign(this.pageNum); this.queueRenderPage(this.pageNum); } } },
css部分
<style scoped> /*pdf部分*/ .pCenter{ overflow-x: hidden; } #the-canvas{ margin-top:10px; } html:fullscreen { background: white; } .elesign { display: flex; flex: 1; flex-direction: column; position: relative; /* padding-left: 180px; */ margin: auto; /* width:600px; */ } .page { text-align:center; margin:0 auto; margin-top: 1%; } #ele-canvas { /* border: 1px solid #5ea6ef; */ overflow: hidden; } .ele-control { text-align: center; margin-top: 3%; } #page-input { width: 7%; } @keyframes ani-demo-spin { from { transform: rotate(0deg);} 50% { transform: rotate(180deg);} to { transform: rotate(360deg);} } /* .loadingclass{ position: absolute; top:30%; left:49%; z-index: 99; } */ .left { position: absolute; top: 42px; left: -5px; padding: 5px 5px; /*border: 1px solid #eee;*/ /*border-radius: 4px;*/ } .left-title { text-align:center; padding-bottom: 10px; border-bottom: 1px solid #eee; } li { list-style-type:none; padding: 10px; } .imgstyle{ vertical-align: middle; width: 130px; border: solid 1px #e8eef2; background-image: url("tuo.png"); background-repeat:no-repeat; } .right { position: absolute; top: 7px; right: -177px; margin-top: 34px; padding-top: 10px; padding-bottom: 20px; width: 152px; /*border: 1px solid #eee;*/ /*border-radius: 4px;*/ } .right-item { margin-bottom: 15px; margin-left: 10px; } .right-item-title { color: #777; height: 20px; line-height: 20px; font-size: 12px; font-weight: 400; text-align: left !important; } .detail-item-desc { color: #333; line-height: 20px; width: 100%; font-size: 12px; display: inline-block; text-align: left; } .btn-outline-dark { color: #0f1531; background-color: transparent; background-image: none; border:1px solid #3e4b5b; } .btn-outline-dark:hover { color: #fff; background-color: #3e4b5b; border-color: #3e4b5b; }
2022.9.13修改
使用的時候,發(fā)現(xiàn)在pdf第一頁添加的印章,下次再打開時,不在顯示,本地緩存的也是顯示{},所以琢磨了下,應(yīng)該是每次打開pdf頁面重置了,代碼做了以下修改
將選中部分改為以下代碼
到此這篇關(guān)于vue里面使用pdfjs-dist+fabric實現(xiàn)pdf電子簽章的文章就介紹到這了,更多相關(guān)vue pdf電子簽章內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決betterScroll在vue中存在圖片時,出現(xiàn)拉不動的問題
今天小編就為大家分享一篇解決betterScroll在vue中存在圖片時,出現(xiàn)拉不動的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09解決vue 給window添加和移除resize事件遇到的坑
這篇文章主要介紹了解決vue 給window添加和移除resize事件遇到的坑,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07Vue 實現(xiàn)顯示/隱藏層的思路(加全局點擊事件)
這篇文章主要介紹了Vue 實現(xiàn)顯示/隱藏層的思路(加全局點擊事件),本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-12-12在Vue中使用CSS3實現(xiàn)內(nèi)容無縫滾動的示例代碼
這篇文章主要介紹了在Vue中使用CSS3實現(xiàn)內(nèi)容無縫滾動的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11Vue+Element實現(xiàn)表格編輯、刪除、以及新增行的最優(yōu)方法
這篇文章主要為大家詳細介紹了Vue+Element實現(xiàn)表格的編輯、刪除、以及新增行的最優(yōu)方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-05-05