vue2實(shí)現(xiàn)pdf電子簽章問題記錄
一、前情提要
1. 需求
仿照e簽寶,實(shí)現(xiàn)pdf電子簽章 => 拿到pdf鏈接,移動(dòng)章的位置,獲取章的坐標(biāo)
技術(shù) : 使用fabric + pdfjs-dist + vuedraggable
2. 借鑒
一位大佬的代碼倉虧 : 地址
一位大佬寫的文章 :地址
3. 優(yōu)化
在大佬的代碼基礎(chǔ)上,進(jìn)行了些許優(yōu)化,變的更像e簽寶
二、下載
ps : 怕版本不同,導(dǎo)致無法運(yùn)行,請(qǐng)下載指定版本
1. fabric
fabric : 是一個(gè)功能強(qiáng)大且操作簡(jiǎn)單的 Javascript HTML5 canvas 工具庫
npm install fabric@5.3.0
2. pdfjs-dist
npm install pdfjs-dist@2.5.207
問題一
注意 : 最好配置一下babel,因?yàn)榇虬臅r(shí)候可能會(huì)報(bào)錯(cuò)
因?yàn)閎abel默認(rèn)不會(huì)轉(zhuǎn)化node_modules中的包,但是pdfjs-dist用了es6的東東
// 安裝包 npm install babel-loader @babel/core @babel/preset-env -D
在webpack.config.js中配置
{ test: /\.js$/, loader: 'babel-loader', include: [ resolve('src'), // 轉(zhuǎn)化pdfjs-dist,之所以分開寫,是因?yàn)閜dfjs-dist里面有很多es6的語法,但是我們只需要轉(zhuǎn)化pdfjs-dist里面的web文件夾下的js文件 resolve('node_modules/pdfjs-dist/web/pdf_viewer.js'), resolve('node_modules/pdfjs-dist/build/pdf.js'), resolve('node_modules/pdfjs-dist/build/pdf.worker.js'), resolve('node_modules/pdfjs-dist/build/pdf.worker.entry.js') ] },
問題二
pdf.js文件過大,可以給 .babelrc 加上屬性,"compact": false
3. vuedraggable
npm install vuedraggable@2.24.3
三、代碼
1. 準(zhǔn)備pdf文件
text.pdf 可放置在 src/static 文件夾中
ps : 線上最好讓后端返回pdf鏈接,因?yàn)榇嬖趐df跨域問題
2. 大佬的代碼
<!-- //?模塊說明 => 合同簽章模塊 --> <template> <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">截止時(shí)間</div> <div class="detail-item-desc">{{ taskInfo.endtime }}</div> </div> </div> </div> </el-col> </el-row> </div> </template> <script> import draggable from 'vuedraggable'; import { fabric } from 'fabric'; import workerSrc from 'pdfjs-dist/es5/build/pdf.worker.entry'; import * as pdfjsViewer from 'pdfjs-dist/web/pdf_viewer'; const pdfjsLib = require('pdfjs-dist/es5/build/pdf.js'); pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc; 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: {} // percentage: 1 }; }, computed: { hasSigna() { if (this.canvasEle && this.canvasEle.getObjects()[0]) { return true; } else { return false; } } }, created() { var that = this; that.mainImagelist = [require('@/assets/img/projectCenter/sign.png'), require('@/assets/img/projectCenter/seal.png')]; that.taskInfo = { title: '測(cè)試蓋章', uname: '張三', endtime: '2021-09-01 17:59:59' }; this.setPdfArea(); }, mounted() { // this.showpdf(this.pdfUrl); if (!pdfjsLib.getDocument || !pdfjsViewer.PDFViewer) { // eslint-disable-next-line no-alert alert('Please build the pdfjs-dist library using\n `gulp dist-install`'); } }, 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,到時(shí)還會(huì)蓋章信息,在渲染時(shí),同時(shí)顯示出來,不應(yīng)該在切換頁碼時(shí)才顯示印章信息 showpdf(pdfUrl) { let caches = JSON.parse(localStorage.getItem('signs')); // 獲取緩存字符串后轉(zhuǎn)換為對(duì)象 // console.log(caches); if (caches != null) { 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)換為對(duì)象 // 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 ); } } }, // 確認(rèn)簽章位置并保存到緩存 confirmSignature() { let data = this.canvasEle.getObjects(); // 獲取當(dāng)前頁面內(nèi)的所有簽章信息 let caches = JSON.parse(localStorage.getItem('signs')); // 獲取緩存字符串后轉(zhuǎn)換為對(duì)象 let signDatas = {}; // 存儲(chǔ)當(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)); // 對(duì)象轉(zhuǎn)字符串后存儲(chǔ)到緩存 }, // 提交數(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 = './static/text.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;margin-top: 10px;'; } } }, pageNum: function () { this.commonSign(this.pageNum); this.queueRenderPage(this.pageNum); } } }; </script> <style lang="scss" scoped> /*pdf部分*/ #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('~@/assets/img/projectCenter/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; } </style>
3. 優(yōu)化后的代碼
<!-- //?模塊說明 => 合同簽章模塊 addToTab--> <template> <div class="contract-signature-view"> <div class="title-operation"> <h2 class="title">合同簽章</h2> <div class="operation"> <el-button type="danger" @click="removeSignature">刪除簽章</el-button> <el-button type="danger" @click="clearSignature">清空簽章</el-button> <el-button type="primary" @click="submitSignature">提交簽章</el-button> </div> </div> <div class="section-box"> <!-- 簽章圖片 --> <aside class="signature-img"> <div class="info"> <h3 class="name">印章</h3> <p class="text">將示例印章標(biāo)識(shí)拖到文件相應(yīng)區(qū)域即可獲取簽章位置</p> </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.img" class="item" style="text-align: center" > <img :src="item.img" width="100%;" height="100%" class="img" /> </li> </transition-group> </draggable> </aside> <!-- 主體區(qū)域 --> <section class="main-layout" :class="{ 'is-first': isFirst }"> <!-- 操作 --> <div class="operate-box"> <div class="slider-box"> <el-slider class="slider" v-model="scale" :min="0.5" :max="2" :step="0.1" :show-tooltip="false" @change="sliderChange" /> <span class="scale-value">{{ (scale * 100).toFixed(0) + '%' }}</span> </div> <div class="page-change"> <i class="icon el-icon-arrow-left" @click="prevPage" /> <!-- :min="1" --> <el-input class="input-box" v-model.number="pageNum" :max="defaultNumPages" @change="cutover" /> <span class="default-text">/{{ defaultNumPages }}</span> <i class="icon el-icon-arrow-right" @click="nextPage" /> </div> </div> <!-- 畫圖 --> <div class="out-view" :class="{ 'is-show': isShowPdf }"> <div class="canvas-layout" v-for="item in numPages" :key="item"> <!-- pdf部分 --> <canvas class="the-canvas" /> <!-- 蓋章部分 --> <canvas class="ele-canvas"></canvas> </div> </div> <i class="loading" v-loading="!isShowPdf" /> </section> <!-- 位置信息 --> <div class="position-info"> <h3 class="title">位置信息</h3> <ul class="nav"> <li class="item" v-for="(item, index) in coordinateList" :key="index"> <span>{{ item.name }}</span> <span>{{ item.page }}</span> <span>{{ item.left }}</span> <span>{{ item.top }}</span> </li> </ul> </div> </div> </div> </template> <script> // 拖拽插件 import draggable from 'vuedraggable'; // pdf插件 import { fabric } from 'fabric'; import workerSrc from 'pdfjs-dist/es5/build/pdf.worker.entry'; const pdfjsLib = require('pdfjs-dist/es5/build/pdf.js'); pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc; export default { components: { draggable }, data() { return { // pdf地址 pdfUrl: '', // 左側(cè)簽章列表 mainImagelist: [], // 右側(cè)坐標(biāo)數(shù)據(jù) coordinateList: [{ name: '名稱', page: '所在頁面', left: 'x坐標(biāo)', top: 'Y坐標(biāo)' }], // 總頁數(shù) numPages: 1, defaultNumPages: 1, // 當(dāng)前頁 pageNum: 1, // 縮放比例 scale: 1, // pdf是否顯示 isFirst: true, isShowPdf: false, // pdf最外層的out-view outViewDom: null, // 各頁pdf的canvas-layout canvasLayoutTopList: [], // 用來簽章的canvas數(shù)組 canvasEle: [], // 繪圖區(qū)域的寬高 whDatas: null, // pdf渲染的canvas數(shù)組 canvas: [], // pdf渲染的canvas的ctx數(shù)組 ctx: [], // pdf渲染的canvas的寬高 pdfDoc: null, // 隱藏的input,用來提交數(shù)據(jù) shadowInputValue: '' }; }, created() { this.mainImagelist = [ { name: '印章', img: require('@/assets/img/projectCenter/contract-sign-img.png') } // { name: '印章', img: require('./sign.png') }, // { name: '紅章', img: require('@/assets/img/projectCenter/seal.png') } ]; this.setPdfArea(); }, mounted() {}, methods: { /** * pdf相關(guān)部分 */ // 設(shè)置PDF地址 setPdfArea() { // // 1. 獲取地址欄 // const urlString = window.location.href; // // 2. 截取地址欄 // const pdfStr = urlString.split('?')[1]; // // 3. 截取pdf地址并解碼 // this.pdfUrl = decodeURIComponent(pdfStr.split('=')[1]); this.pdfUrl = './static/text.pdf'; this.$nextTick(() => { this.showpdf(this.pdfUrl); // 接口返回的應(yīng)該還有蓋章信息,不只是pdf }); }, // 解析pdf showpdf(pdfUrl) { pdfjsLib .getDocument({ url: pdfUrl, rangeChunkSize: 65536, disableAutoFetch: false }) .promise.then((pdfDoc_) => { this.pdfDoc = pdfDoc_; this.numPages = this.pdfDoc.numPages; this.defaultNumPages = this.pdfDoc.numPages; this.$nextTick(() => { this.canvas = document.querySelectorAll('.the-canvas'); this.canvas.forEach((item) => { this.ctx.push(item.getContext('2d')); }); // 循環(huán)渲染pdf for (let i = 1; i <= this.numPages; i++) { this.renderPage(i).then(() => { this.renderPdf({ width: this.canvas[i - 1].width, height: this.canvas[i - 1].height }); }); } setTimeout(() => { this.renderFabric(); this.canvasEvents(); }, 1000); }); }); }, // 設(shè)置pdf寬高,縮放比例,渲染pdf renderPage(num) { // console.log('this.canvas', this.canvas[num], num); return this.pdfDoc.getPage(num).then((page) => { const viewport = page.getViewport({ scale: this.scale }); // 設(shè)置視口大小 this.canvas[num - 1].height = viewport.height; this.canvas[num - 1].width = viewport.width; // Render PDF page into canvas context const renderContext = { canvasContext: this.ctx[num - 1], viewport: viewport }; page.render(renderContext); }); }, // 設(shè)置繪圖區(qū)域?qū)捀? renderPdf(data) { this.whDatas = data; }, // 生成繪圖區(qū)域 renderFabric() { // 1. 拿到全部的canvas-layout const canvasLayoutDom = document.querySelectorAll('.canvas-layout'); // 2. 循環(huán)遍歷 canvasLayoutDom.forEach((item) => { this.canvasLayoutTopList.push({ obj: item, top: item.offsetTop }); // 3. 設(shè)置寬高和居中 item.style.width = this.whDatas.width + 'px'; item.style.height = this.whDatas.height + 'px'; item.style.margin = '0 auto 18px'; item.style.boxShadow = '4px 4px 4px #e9e9e9'; // 4. 拿到蓋章canvas const canvasEle = item.querySelector('.ele-canvas'); // 5. 拿到pdf的canvas const pCenter = item.querySelector('.the-canvas'); // 6. 設(shè)置蓋章canvas的寬高 canvasEle.width = pCenter.clientWidth; canvasEle.height = this.whDatas.height; // 7. 創(chuàng)建fabric對(duì)象并存儲(chǔ) this.canvasEle.push(new fabric.Canvas(canvasEle)); // 8. 設(shè)置蓋章canvas的樣式 const container = item.querySelector('.canvas-container'); container.style.position = 'absolute'; container.style.left = '50%'; container.style.transform = 'translateX(-50%)'; container.style.top = '0px'; }); // 現(xiàn)形 this.isFirst = false; this.isShowPdf = true; this.outViewDom = document.querySelector('.out-view'); // 開啟監(jiān)聽窗口滾動(dòng) this.outViewScroll(); }, // 開啟監(jiān)聽窗口滾動(dòng) outViewScroll() { this.outViewDom.addEventListener('scroll', this.outViewRun); }, // 關(guān)閉監(jiān)聽窗口滾動(dòng) outViewScrollClose() { this.outViewDom.removeEventListener('scroll', this.outViewRun); }, // 窗口滾動(dòng) outViewRun() { const scrollTop = this.outViewDom.scrollTop; const topList = this.canvasLayoutTopList.map((item) => item.top); // 增加一個(gè)最大值 topList.push(Number.MAX_SAFE_INTEGER); for (let index = 0; index < topList.length; index++) { const element = topList[index]; if (element <= scrollTop && scrollTop < topList[index + 1]) { this.pageNum = index + 1; break; } } }, // scale滑塊,重新渲染整個(gè)pdf sliderChange() { this.pageNum = 1; this.numPages = 0; this.canvasLayoutTopList = []; this.canvasEle = []; this.ctx = []; this.canvas = []; this.isShowPdf = false; // this.outViewScrollClose(); this.whDatas = null; this.coordinateList = [{ name: '名稱', page: '所在頁面', left: 'x坐標(biāo)', top: 'Y坐標(biāo)' }]; this.getSignatureJson(); setTimeout(() => { this.numPages = this.pdfDoc.numPages; this.$nextTick(() => { this.canvas = document.querySelectorAll('.the-canvas'); this.canvas.forEach((item) => { this.ctx.push(item.getContext('2d')); }); // 循環(huán)渲染pdf for (let i = 1; i <= this.numPages; i++) { this.renderPage(i).then(() => { this.renderPdf({ width: this.canvas[i - 1].width, height: this.canvas[i - 1].height }); }); } setTimeout(() => { this.renderFabric(); this.canvasEvents(); }, 1000); }); }, 1000); }, /** * 簽章相關(guān)部分 */ // 簽章拖拽邊界處理,不能將圖片拖拽到繪圖區(qū)域外 canvasEvents() { this.canvasEle.forEach((item) => { item.on('object:moving', (e) => { const 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 ); } // console.log('obj.cacheKey',obj.cacheKey); const findIndex = this.coordinateList .slice(1) .findIndex((coord) => coord.cacheKey == obj.cacheKey); const keys = ['width', 'height', 'top', 'left', 'angle', 'scaleX', 'scaleY']; keys.forEach((item) => { this.coordinateList[findIndex + 1][item] = Math.ceil(obj[item] / this.scale); }); this.getSignatureJson(); }); }); }, // 拖拽結(jié)束 end(e) { // 找到當(dāng)前拖拽到哪一個(gè)canvas-layout上 const currentCanvasLayout = e.originalEvent.target.parentElement.parentElement; const findIndex = this.canvasLayoutTopList.findIndex( (item) => item.obj == currentCanvasLayout ); if (findIndex == -1) return false; // 取整 const left = e.originalEvent.layerX < 0 ? 0 : Math.ceil(e.originalEvent.layerX / this.scale); const top = e.originalEvent.layerY < 0 ? 0 : Math.ceil(e.originalEvent.layerY / this.scale); // console.log('e', e, findIndex); this.addSeal({ sealUrl: this.mainImagelist[e.newDraggableIndex].img, left, top, index: e.newDraggableIndex, pageNum: findIndex }); }, // 添加公章 addSeal({ sealUrl, left, top, index, pageNum }) { fabric.Image.fromURL(sealUrl, (oImg) => { oImg.set({ // 距離左邊的距離 left: left, // 距離頂部的距離 top: top, // 角度 // angle: 10, // 縮放比例,需要乘以scale scaleX: 0.8 * this.scale, scaleY: 0.8 * this.scale, index, // 禁止縮放 lockScalingX: true, lockScalingY: true, // 禁止旋轉(zhuǎn) lockRotation: true }); this.canvasEle[pageNum].add(oImg); // 保存簽章信息 this.saveSignature({ pageNum, index, sealUrl }); }); // this.removeActive(); }, // 保存簽章 saveSignature({ pageNum, index, sealUrl }) { // 1. 拿到當(dāng)前簽章的信息 let length = 0; let pageConfig = this.coordinateList.filter((item) => item.page - 1 == pageNum); if (pageConfig) length = pageConfig.length; const currentSignInfo = this.canvasEle[pageNum].getObjects()[length]; // 2. 拼接數(shù)據(jù) const keys = ['width', 'height', 'top', 'left', 'angle', 'scaleX', 'scaleY']; const obj = {}; keys.forEach((item) => { obj[item] = Math.ceil(currentSignInfo[item] / this.scale); }); obj.cacheKey = currentSignInfo.cacheKey; obj.sealUrl = sealUrl; obj.index = index; obj.name = `${this.mainImagelist[index].name}${this.coordinateList.length}`; obj.page = pageNum + 1; this.coordinateList.push(obj); this.getSignatureJson(); }, // 簽章生成json字符串 getSignatureJson() { // 1. 判斷是否有簽章 if (this.coordinateList.length <= 1) return (this.shadowInputValue = ''); // 2. 拿到簽章的信息,去除第一條 const signatureList = this.coordinateList.slice(1); // 3. 拼接數(shù)據(jù),只要left和top和page const keys = ['page', 'left', 'top']; const arr = []; signatureList.forEach((item) => { const obj = {}; keys.forEach((key) => { obj[key] = item[key]; }); arr.push(obj); }); // 4. 轉(zhuǎn)成json字符串 this.shadowInputValue = JSON.stringify(arr); }, /** * 操作相關(guān)部分 */ // 上一頁 prevPage() { if (this.pageNum <= 1) return; this.pageNum--; // 滾動(dòng)到指定位置 this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top; }, // 下一頁 nextPage() { if (this.pageNum >= this.numPages) return; this.pageNum++; // 滾動(dòng)到指定位置 this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top; }, // 切換頁碼 cutover() { this.outViewScrollClose(); if (this.pageNum < 1) { this.pageNum = 1; } else if (this.pageNum > this.numPages) { this.pageNum = this.numPages; } // 滾動(dòng)到指定位置 this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top; setTimeout(() => { this.outViewScroll(); }, 500); }, // 刪除所有的簽章選中狀態(tài) removeActive() { this.canvasEle.forEach((item) => { item.discardActiveObject().renderAll(); }); }, // 刪除簽章 removeSignature() { // 1. 判斷是否有選中的簽章 const findItem = this.canvasEle.filter((item) => item.getActiveObject()); // 2. 判斷選中簽章的個(gè)數(shù) if (findItem.length == 0) return this.$message.error('請(qǐng)選擇要?jiǎng)h除的簽章'); // 3. 判斷選中簽章的個(gè)數(shù)是否大于1 if (findItem.length > 1) { this.removeActive(); return this.$message.error('只能選擇刪除一個(gè)簽章,請(qǐng)重新選擇'); } // 4. 拿到選中的簽章的cacheKey const activeObj = findItem[0].getActiveObject(); const findIndex = this.coordinateList.findIndex( (item) => item.cacheKey == activeObj.cacheKey ); // 5. 刪除選中的簽章 findItem[0].remove(activeObj); // 6. 刪除選中的簽章的信息 this.coordinateList.splice(findIndex, 1); this.getSignatureJson(); }, // 清空簽章 clearSignature() { this.canvasEle.forEach((item) => { item.clear(); }); this.coordinateList = [{ name: '名稱', page: '所在頁面', left: 'x坐標(biāo)', top: 'Y坐標(biāo)' }]; this.getSignatureJson(); }, // 提交數(shù)據(jù) submitSignature() { console.log('this.coordinateList', this.coordinateList); } } }; </script> <style lang="scss" scoped> .contract-signature-view { /*pdf部分*/ .ele-canvas { overflow: hidden; } .title-operation { height: 80px; padding: 20px 40px; display: flex; align-items: center; justify-content: space-between; .title { font-size: 20px; font-weight: 600; } border-bottom: 1px solid #e4e4e4; } .section-box { position: relative; display: flex; height: calc(100vh - 60px); .signature-img { width: 240px; min-width: 240px; background-color: #fff; padding: 40px 15px; border-right: 1px solid #e4e4e4; .info { margin-bottom: 38px; .name { font-size: 18px; font-weight: 600; color: #000000; line-height: 25px; margin-bottom: 20px; } .text { font-size: 14px; color: #000000; line-height: 20px; } } .item { padding: 10px; border: 1px dashed rgba(0, 0, 0, 0.3); &:not(:last-child) { margin-bottom: 10px; } .img { vertical-align: middle; width: 120px; background-repeat: no-repeat; } } } .main-layout { flex: 1; background-color: #f7f8fa; position: relative; &.is-first { .operate-box { opacity: 0; } } .operate-box { opacity: 1; position: absolute; top: 0; left: 0; width: 100%; height: 40px; background-color: #fff; border-bottom: 1px solid #e4e4e4; display: flex; justify-content: center; align-items: center; .slider-box { width: 230px; display: flex; justify-content: center; align-items: center; border-left: 1px solid #e4e4e4; border-right: 1px solid #e4e4e4; .slider { width: 120px; } .scale-value { margin-left: 24px; font-size: 16px; color: #000000; line-height: 22px; } } .page-change { display: flex; align-items: center; margin-left: 30px; .icon { cursor: pointer; padding: 0 5px; color: #c1c1c1; } .input-box { border: none; /deep/ .el-input__inner { width: 34px; height: 20px; border: none; padding: 0; text-align: center; border-bottom: 1px solid #e4e4e4; } } .default-text { display: flex; line-height: 22px; margin-right: 5px; } } } .out-view { height: calc(100vh - 100px); margin: 40px auto; overflow-x: auto; overflow-y: auto; padding-top: 20px; text-align: center; opacity: 0; transition: all 0.5s; &.is-show { opacity: 1; } .canvas-layout { position: relative; text-align: center; margin: 0 auto 18px; } } .loading { width: 20px; height: 20px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 999; /deep/ .el-loading-mask { background-color: transparent; } } } .position-info { width: 355px; min-width: 355px; border-left: 1px solid #e4e4e4; background-color: #fff; padding: 14px 15px; .title { font-size: 14px; font-weight: 400; color: #000000; line-height: 20px; padding-bottom: 18px; } .nav { display: flex; flex-direction: column; .item { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #eee; &:first-child { background-color: #f7f8fa; } span { flex: 1; text-align: center; font-size: 12px; color: #000000; line-height: 20px; } } } } } } </style>
到此這篇關(guān)于vue2 之 實(shí)現(xiàn)pdf電子簽章的文章就介紹到這了,更多相關(guān)vue2 pdf電子簽章內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue頁面中實(shí)現(xiàn)平滑滾動(dòng)功能
這是一個(gè)實(shí)現(xiàn)平滑滾動(dòng)的函數(shù),可以讓頁面在滾動(dòng)到指定位置時(shí)產(chǎn)生緩動(dòng)效果,本文給大家介紹了如何在在Vue頁面中實(shí)現(xiàn)平滑滾動(dòng)功能,<BR>,文中詳細(xì)的代碼講解供大家參考,具有一定的參考價(jià)值,需要的朋友可以參考下2023-12-12vue使用axios時(shí)關(guān)于this的指向問題詳解
最近在學(xué)習(xí)使用vue+axios,在使用中發(fā)現(xiàn)了一個(gè)問題,下面總結(jié)分享給大家,這篇文章主要給大家介紹了關(guān)于vue使用axios時(shí)this的指向問題的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-12-12Vue路由切換和Axios接口取消重復(fù)請(qǐng)求詳解
在web項(xiàng)目開發(fā)的過程中,經(jīng)常會(huì)遇到客服端重復(fù)發(fā)送請(qǐng)求的場(chǎng)景,下面這篇文章主要給大家介紹了關(guān)于Vue路由切換和Axios接口取消重復(fù)請(qǐng)求的相關(guān)資料,需要的朋友可以參考下2022-05-05Vue-Ant Design Vue-普通及自定義校驗(yàn)實(shí)例
這篇文章主要介紹了Vue-Ant Design Vue-普通及自定義校驗(yàn)實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-10-10Vue3生命周期Hooks原理與調(diào)度器Scheduler關(guān)系
這篇文章主要為大家介紹了Vue3生命周期Hooks原理與調(diào)度器Scheduler關(guān)系詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07詳解VUE-地區(qū)選擇器(V-Distpicker)組件使用心得
這篇文章主要介紹了詳解VUE-地區(qū)選擇器(V-Distpicker)組件使用心得,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05