vue2实现pdf电子签章问题记录
作者:玄鱼殇
仿照e签宝,实现pdf电子签章 => 拿到pdf链接,移动章的位置,获取章的坐标,怎么实现呢,下面小编给大家介绍vue2实现pdf电子签章问题记录,感兴趣的朋友一起看看吧
一、前情提要
1. 需求
仿照e签宝,实现pdf电子签章 => 拿到pdf链接,移动章的位置,获取章的坐标
技术 : 使用fabric + pdfjs-dist + vuedraggable
2. 借鉴
一位大佬的代码仓亏 : 地址
一位大佬写的文章 :地址
3. 优化
在大佬的代码基础上,进行了些许优化,变的更像e签宝
二、下载
ps : 怕版本不同,导致无法运行,请下载指定版本
1. fabric
fabric : 是一个功能强大且操作简单的 Javascript HTML5 canvas 工具库
npm install fabric@5.3.0
2. pdfjs-dist
npm install pdfjs-dist@2.5.207
问题一
注意 : 最好配置一下babel,因为打包的时候可能会报错
因为babel默认不会转化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'), // 转化pdfjs-dist,之所以分开写,是因为pdfjs-dist里面有很多es6的语法,但是我们只需要转化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. 准备pdf文件
text.pdf 可放置在 src/static 文件夹中
ps : 线上最好让后端返回pdf链接,因为存在pdf跨域问题
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">跳转</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">任务信息</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">发起方</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> </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预览 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: '测试盖章', 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预览 // zoomIn() { // console.log('缩小'); // if (this.scale <= 0.5) { // this.$message.error('已经显示最小比例'); // } 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('已经显示最大比例'); // } 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 }); // 设置视口大小 _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,到时还会盖章信息,在渲染时,同时显示出来,不应该在切换页码时才显示印章信息 showpdf(pdfUrl) { let caches = JSON.parse(localStorage.getItem('signs')); // 获取缓存字符串后转换为对象 // 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); }); }, /** * 盖章部分开始 */ // 设置绘图区域宽高 renderPdf(data) { this.whDatas = data; // document.querySelector("#elesign").style.width = data.width + "px"; }, // 生成绘图区域 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%"; }, // 相关事件操作哟 canvasEvents() { // 拖拽边界 不能将图片拖拽到绘图区域外 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')); // 获取缓存字符串后转换为对象 // 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(); // 获取当前页面内的所有签章信息 let caches = JSON.parse(localStorage.getItem('signs')); // 获取缓存字符串后转换为对象 let signDatas = {}; // 存储当前页的所有签章信息 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)); // 对象转字符串后存储到缓存 }, // 提交数据 submitSignature() { this.confirmSignature(); // let caches = localStorage.getItem('signs'); // console.log(JSON.parse(caches)); return false; }, // 清空数据 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 ); }, // 设置PDF预览区域高度 setPdfArea() { this.pdfUrl = './static/text.pdf'; // this.pdfurl = res.data.data.pdfurl; this.$nextTick(() => { this.showpdf(this.pdfUrl); // 接口返回的应该还有盖章信息,不只是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. 优化后的代码
<!-- //?模块说明 => 合同签章模块 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">将示例印章标识拖到文件相应区域即可获取签章位置</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> <!-- 主体区域 --> <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: '', // 左侧签章列表 mainImagelist: [], // 右侧坐标数据 coordinateList: [{ name: '名称', page: '所在页面', left: 'x坐标', top: 'Y坐标' }], // 总页数 numPages: 1, defaultNumPages: 1, // 当前页 pageNum: 1, // 缩放比例 scale: 1, // pdf是否显示 isFirst: true, isShowPdf: false, // pdf最外层的out-view outViewDom: null, // 各页pdf的canvas-layout canvasLayoutTopList: [], // 用来签章的canvas数组 canvasEle: [], // 绘图区域的宽高 whDatas: null, // pdf渲染的canvas数组 canvas: [], // pdf渲染的canvas的ctx数组 ctx: [], // pdf渲染的canvas的宽高 pdfDoc: null, // 隐藏的input,用来提交数据 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相关部分 */ // 设置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); // 接口返回的应该还有盖章信息,不只是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')); }); // 循环渲染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); }); }); }, // 设置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 }); // 设置视口大小 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); }); }, // 设置绘图区域宽高 renderPdf(data) { this.whDatas = data; }, // 生成绘图区域 renderFabric() { // 1. 拿到全部的canvas-layout const canvasLayoutDom = document.querySelectorAll('.canvas-layout'); // 2. 循环遍历 canvasLayoutDom.forEach((item) => { this.canvasLayoutTopList.push({ obj: item, top: item.offsetTop }); // 3. 设置宽高和居中 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. 设置盖章canvas的宽高 canvasEle.width = pCenter.clientWidth; canvasEle.height = this.whDatas.height; // 7. 创建fabric对象并存储 this.canvasEle.push(new fabric.Canvas(canvasEle)); // 8. 设置盖章canvas的样式 const container = item.querySelector('.canvas-container'); container.style.position = 'absolute'; container.style.left = '50%'; container.style.transform = 'translateX(-50%)'; container.style.top = '0px'; }); // 现形 this.isFirst = false; this.isShowPdf = true; this.outViewDom = document.querySelector('.out-view'); // 开启监听窗口滚动 this.outViewScroll(); }, // 开启监听窗口滚动 outViewScroll() { this.outViewDom.addEventListener('scroll', this.outViewRun); }, // 关闭监听窗口滚动 outViewScrollClose() { this.outViewDom.removeEventListener('scroll', this.outViewRun); }, // 窗口滚动 outViewRun() { const scrollTop = this.outViewDom.scrollTop; const topList = this.canvasLayoutTopList.map((item) => item.top); // 增加一个最大值 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滑块,重新渲染整个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坐标', top: 'Y坐标' }]; 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')); }); // 循环渲染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); }, /** * 签章相关部分 */ // 签章拖拽边界处理,不能将图片拖拽到绘图区域外 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(); }); }); }, // 拖拽结束 end(e) { // 找到当前拖拽到哪一个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, // 禁止旋转 lockRotation: true }); this.canvasEle[pageNum].add(oImg); // 保存签章信息 this.saveSignature({ pageNum, index, sealUrl }); }); // this.removeActive(); }, // 保存签章 saveSignature({ pageNum, index, sealUrl }) { // 1. 拿到当前签章的信息 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. 拼接数据 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. 拼接数据,只要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. 转成json字符串 this.shadowInputValue = JSON.stringify(arr); }, /** * 操作相关部分 */ // 上一页 prevPage() { if (this.pageNum <= 1) return; this.pageNum--; // 滚动到指定位置 this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top; }, // 下一页 nextPage() { if (this.pageNum >= this.numPages) return; this.pageNum++; // 滚动到指定位置 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; } // 滚动到指定位置 this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top; setTimeout(() => { this.outViewScroll(); }, 500); }, // 删除所有的签章选中状态 removeActive() { this.canvasEle.forEach((item) => { item.discardActiveObject().renderAll(); }); }, // 删除签章 removeSignature() { // 1. 判断是否有选中的签章 const findItem = this.canvasEle.filter((item) => item.getActiveObject()); // 2. 判断选中签章的个数 if (findItem.length == 0) return this.$message.error('请选择要删除的签章'); // 3. 判断选中签章的个数是否大于1 if (findItem.length > 1) { this.removeActive(); return this.$message.error('只能选择删除一个签章,请重新选择'); } // 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坐标', top: 'Y坐标' }]; this.getSignatureJson(); }, // 提交数据 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>
到此这篇关于vue2 之 实现pdf电子签章的文章就介绍到这了,更多相关vue2 pdf电子签章内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!