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><draggablev-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-numberstyle="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-alertalert('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 contextlet renderContext = {canvasContext: _this.ctx,viewport: viewport};let renderTask = page.render(renderContext);// Wait for rendering to finishrenderTask.promise.then(() => {_this.pageRendering = false;if (_this.pageNumPending !== null) {// New page rendering is pendingthis.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 ignoreif (obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width) {return;}obj.setCoords();// top-left  cornerif (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 cornerif (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><!-- 拖拽 --><draggablev-model="mainImagelist":group="{ name: 'itext', pull: 'clone' }":sort="false"@end="end"><transition-group type="transition"><liv-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-sliderclass="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-inputclass="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-viewoutViewDom: null,// 各页pdf的canvas-layoutcanvasLayoutTopList: [],// 用来签章的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});},// 解析pdfshowpdf(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'));});// 循环渲染pdffor (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宽高,缩放比例,渲染pdfrenderPage(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 contextconst renderContext = {canvasContext: this.ctx[num - 1],viewport: viewport};page.render(renderContext);});},// 设置绘图区域宽高renderPdf(data) {this.whDatas = data;},// 生成绘图区域renderFabric() {// 1. 拿到全部的canvas-layoutconst 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. 拿到盖章canvasconst canvasEle = item.querySelector('.ele-canvas');// 5. 拿到pdf的canvasconst 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滑块,重新渲染整个pdfsliderChange() {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'));});// 循环渲染pdffor (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 ignoreif (obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width) {return;}obj.setCoords();// top-left  cornerif (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 cornerif (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,// 缩放比例,需要乘以scalescaleX: 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和pageconst 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. 判断选中签章的个数是否大于1if (findItem.length > 1) {this.removeActive();return this.$message.error('只能选择删除一个签章,请重新选择');}// 4. 拿到选中的签章的cacheKeyconst 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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/223690.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

QT中网络编程之发送Http协议的Get和Post请求

文章目录 HTTP协议GET请求POST请求QT中对HTTP协议的处理1.QNetworkAccessManager2.QNetworkRequest3.QNetworkReply QT实现GET请求和POST请求Get请求步骤Post请求步骤 测试结果 使用QT的开发产品最终作为一个客户端来使用&#xff0c;很大的一个功能就是要和后端服务器进行交互…

【mongoose】 Model.create() no longer accepts a callback 报错解决

在最新版的 mongoose 操作 MongoDB 数据库的时候&#xff0c;当我们插入一条数据时候&#xff0c;会报错 &#xff1a;Model.create() no longer accepts a callback&#xff0c;看了很多文章都说是&#xff0c;版本太高&#xff0c;都妥协选择了降低回旧版本&#xff0c;但我就…

从0开始学会nvm管理工具(node卸载,nvm安装以及使用)

NVM管理工具 一、nvm介绍 在工作中&#xff0c;我们可能同时在进行2个或者多个不同的项目开发&#xff0c;每个项目的需求不同&#xff0c;进而不同项目必须依赖不同版本的NodeJS运行环境&#xff0c;这种情况下&#xff0c;对于维护多个版本的node将会是一件非常麻烦的事情&…

Unity | HybridCLR 热更新(Windows端)

目录 一、准备工作 1.环境相关 2.Unity中配置 二、热更新 1.创建 HotUpdate 热更新模块 2.安装和配置HybridCLR 3.配置PlayerSettings 4.创建热更新相关脚本 5.打包dll 6.测试热更新 一、准备工作 1.环境相关 安装git环境。Win下需要安装visual studio 2019或更高版…

超维空间S2无人机使用说明书——21、VINS视觉定位仿真

引言&#xff1a;为了实现室内无人机的定位功能&#xff0c;S系列无人机配置了VINS-FUSION定位环境&#xff0c;主要包含了仿真跑数据集和实际操作部分。为了提前熟悉使用原理&#xff0c;可以先使用仿真环境跑数据集进行学习和理解 硬件&#xff1a;1080P显示器、Jetson orin…

LabVIEW与PID在温度测控系统中的应用

LabVIEW与PID在温度测控系统中的应用 本案例介绍LabVIEW在温度控制系统中的应用&#xff0c;特别是结合PID算法。项目使用abVIEW作为主要开发工具&#xff0c;配合NI PCI-7831R数据采集和控制设备&#xff0c;实现了高效的温度调节。 系统的核心在于LabVIEW的FPGA模块&#x…

【大模型实践】基于文心一言的对话模型设计

文心一言&#xff08;英文名&#xff1a;ERNIE Bot&#xff09;是百度全新一代知识增强大语言模型&#xff0c;文心大模型家族的新成员&#xff0c;能够与人对话互动、回答问题、协助创作&#xff0c;高效便捷地帮助人们获取信息、知识和灵感。文心一言从数万亿数据和数千亿知识…

探索 HTTP 请求的世界:get 和 post 的奥秘(上)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

leetcode 268. 丢失的数字(优质解法)

链接&#xff1a;268. 丢失的数字 代码: class Solution {public int missingNumber(int[] nums) {int result0;for(int i0;i<nums.length;i){result^i;}for(int i0;i<nums.length;i){result^nums[i];}return result;} } 题解&#xff1a; 本题是比较简单的题&#xff…

LuaTable转C#的列表List和字典Dictionary

LuaTable转C#的列表List和字典Dictionaty 介绍lua中创建表测试lua中list表表转成List表转成Dictionary 键值对表表转成Dictionary 多类型键值对表表转成Dictionary 总结 介绍 之前基本都是从C#中的List或者Dictionary转成luaTable&#xff0c;很少会把LuaTable转成C#的List或者…

sqlilabs第三十二关

Less-32&#xff08;GET - Bypass custom filter adding slashes to dangerous chars) 手工注入 由 宽字符注入可知payload 成功触发报错 http://192.168.21.149/Less-32/ ?id1%df 要写字符串的话直接吧字符串变成ascii码 注意16进制的表示方式 自动注入 sqlmap -u http:…

如何从 Android 手机免费恢复已删除的通话记录/历史记录?

有一个有合作意向的人给我打电话&#xff0c;但我没有接听。更糟糕的是&#xff0c;我错误地将其删除&#xff0c;认为这是一个骚扰电话。那么有没有办法从 Android 手机恢复已删除的通话记录呢&#xff1f;” 塞缪尔问道。如何在 Android 上恢复已删除的通话记录&#xff1f;如…

BP网络识别26个英文字母matlab

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;字母识别 获取完整源码源工程文件 一、 设计思想 字符识别在现代日常生活的应用越来越广泛&#xff0c;比如车辆牌照自动识别系统&#xff0c;手写识别系统&#xff0c;办公自动化等等。本文采用BP网络对26个英文字母进行…

C# 判断两个时间段是否重叠

public static bool IsOverlap(DateTime startTime1, DateTime endTime1, DateTime startTime2, DateTime endTime2){// 判断两个时间段是否有重叠return !(endTime1 < startTime2 || startTime1 > endTime2);//根据德摩根定律&#xff0c;等效为&#xff1a;endTime1 &g…

Flutter基建 - 12种隐式动画小组件全解析

本篇基于Flutter 3.16.4&#xff0c;Dart 3.2.3版本 Flutter 3.16.4 • channel stable • Framework • revision 2e9cb0aa71 (3 days ago) • 2023-12-11 14:35:13 -0700 Engine • revision 54a7145303 Tools • Dart 3.2.3 • DevTools 2.28.4 本篇为Flutter基建的第九篇文…

互联网上门洗衣洗鞋小程序优势有哪些?

互联网洗鞋店小程序相较于传统洗鞋方式&#xff0c;具有以下优势&#xff1b; 1. 便捷性&#xff1a;用户只需通过手机即可随时随地下单并查询&#xff0c;省去了许多不必要的时间和精力。学生们无需走出宿舍或校园&#xff0c;就能轻松预约洗鞋并取件。 2. 精准定位&#xff1…

TLC5615实现示波器波形显示——方波、三角波、锯齿波

代码&#xff1a; #include <reg52.h>sbit SCLK P2^0; // sbit&#xff1a;为寄存器的某位取名 sbit CS P2^1; sbit DIN P2^2;sbit key1 P1^0; sbit key2 P1^1; sbit key3 P1^2; sbit key4 P1^3;unsigned char rect; void delay(unsigned char i) {while(i--); }…

03|模型I/O:输入提示、调用模型、解析输出

03&#xff5c;模型I/O&#xff1a;输入提示、调用模型、解析输出 从这节课开始&#xff0c;我们将对 LangChain 中的六大核心组件一一进行详细的剖析。 模型&#xff0c;位于 LangChain 框架的最底层&#xff0c;它是基于语言模型构建的应用的核心元素&#xff0c;因为所谓 …

2016年第五届数学建模国际赛小美赛A题臭氧消耗预测解题全过程文档及程序

2016年第五届数学建模国际赛小美赛 A题 臭氧消耗预测 原题再现&#xff1a; 臭氧消耗包括自1970年代后期以来观察到的若干现象&#xff1a;地球平流层&#xff08;臭氧层&#xff09;臭氧总量稳步下降&#xff0c;以及地球极地附近平流层臭氧&#xff08;称为臭氧空洞&#x…

vscode中vue项目报错

当在vscode中写代码时&#xff0c;报错报错报错......... 已经头大&#xff0c;还没写就报错&#xff0c; 这是因为eslint对语法的要求太过严格导致的编译时&#xff0c;出现各种语法格式错误 我们打开vue.config.js&#xff0c;加上这句代码&#xff0c;就OK啦 lintOnSave:…