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.mzph.cn/news/237587.shtml

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

相关文章

浅述无人机技术在地质灾害应急救援场景中的应用

12月18日23时&#xff0c;甘肃临夏州积石山县发生6.2级地震&#xff0c;震源深度10千米&#xff0c;灾区电力、通信受到影响。地震发生后&#xff0c;无人机技术也火速应用在灾区的应急抢险中。目前&#xff0c;根据受灾地区实际情况&#xff0c;翼龙-2H应急救灾型无人机已出动…

网络安全-API接口安全

本文为作者学习文章&#xff0c;按作者习惯写成&#xff0c;如有错误或需要追加内容请留言&#xff08;不喜勿喷&#xff09; 本文为追加文章&#xff0c;后期慢慢追加 API接口概念 API接口&#xff08;Application Programming Interface&#xff0c;应用程序编程接口&…

Python---socket之send和recv原理剖析

1. 认识TCP socket的发送和接收缓冲区 当创建一个TCP socket对象的时候会有一个发送缓冲区和一个接收缓冲区&#xff0c;这个发送和接收缓冲区指的就是内存中的一片空间。 2. send原理剖析 send是不是直接把数据发给服务端? 不是&#xff0c;要想发数据&#xff0c;必须得…

【SpringMVC】SpringMVC的请求与响应

文章目录 0. Tomcat环境的配置1. PostMan工具介绍创建WorkSpace建立新的请求 2. 请求映射路径案例结构与代码案例结构案例代码 案例存在问题解决方案方法方法升级版——配置请求路径前缀注解总结 3. Get请求与Post请求案例结构与案例代码案例结构案例代码 Get请求Post请求接收中…

3. BlazorSignalRApp 结合使用 ASP.NET Core SignalR 和 Blazor

参考&#xff1a;https://learn.microsoft.com/zh-cn/aspnet/core/blazor/tutorials/signalr-blazor?viewaspnetcore-8.0&tabsvisual-studio 1.创建新项目 BlazorSignalRApp 2.添加项目依赖项 依赖项&#xff1a;Microsoft.AspNetCore.SignalR.Client 方式1 管理解决方案…

SOLIDWORKS Flow Simulation升力仿真分析

仿真飞车起飞和飞机起飞的原理相同,当等质量的空气同时通过机翼上表面和下表面时,会在机翼上下方形成不同流速,空气通过机翼上表面时流速大&#xff0c;压强较小;通过下表面时流速较小,压强大。此时飞车会受一个向上的合力,即向上的升力,空气速度越快,升力越大,当升力大于飞车重…

力扣每日一题day36[112.路径总和]

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有节点值相加等于目标和 targetSum 。如果存在&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 叶子节点 是指没有子节点…

SQL基础:查询的基本使用

上一节我们讲述了记录的基本操作&#xff0c;这一节我们来单独讲一下查询。 查询基本结构 首先我们来看下查询的基本结构 SELECTcolumn1,column2,... FROMtable_name [WHEREcondition] [GROUP BYcolumn1, column2, ...] [HAVINGaggregate_function(column) condition] [ORDE…

【算法】算法题-20231222

这里写目录标题 一、1002. 查找共用字符二、1047. 删除字符串中的所有相邻重复项三、面试题 01.04. 回文排列 一、1002. 查找共用字符 给你一个字符串数组 words &#xff0c;请你找出所有在 words 的每个字符串中都出现的共用字符&#xff08; 包括重复字符&#xff09;&…

SpringIOC之MethodBasedEvaluationContext

博主介绍&#xff1a;✌全网粉丝5W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面有丰富的经验…

React和umi搭建项目的操作步骤

​​​​​​一、react脚手架新建项目 (1.1)、命令行 前提&#xff1a;react ES2015,nodejs v8 npx create-react-app myReactName //2022年v16以下版本 myReactName(自定义项目名) react中文官网&#xff0c;快速上手&#xff1a;react中文官网 react框架&#xff0c;…

基于Linphone android sdk开发Android软话机

1.Linphone简介 1.1 简介 LinPhone是一个遵循GPL协议的开源网络电话或者IP语音电话&#xff08;VOIP&#xff09;系统&#xff0c;其主要如下。使用linphone&#xff0c;开发者可以在互联网上随意的通信&#xff0c;包括语音、视频、即时文本消息。linphone使用SIP协议&#…

CGAL的3D Alpha Shapes

假设我们给定一个二维或三维的点集S&#xff0c;我们希望得到类似“这些点形成的形状”的东西。这是一个相当模糊的概念&#xff0c;可能有许多可能的解释&#xff0c;阿尔法形状就是其中之一。阿尔法形状可用于从密集的无组织数据点集进行形状重建。事实上&#xff0c;阿尔法形…

用户管理第2节课--idea 2023.2 后端--实现基本数据库操作(操作user表) -- 自动生成

一、插件 Settings... 1.1 File -- Settings 1.2 Settings -- Plugins 1.2.1 搜索框&#xff0c;也可以直接搜索 1.3 Plugins -- 【输入 & 搜索】mybatis 1.3.1 插件不同功能介绍 1.3.2 翻译如下 1.4 选中 Update&#xff0c;更新下 1.4.1 更新中 1.4.2 Restart IDE 1…

ARM GIC(一) cortex-A 处理器中断简介

对于ARM的处理器&#xff0c;中断给处理器提供了触觉&#xff0c;使处理器能够感知到外界的变化&#xff0c;从而实时的处理。本系列博文&#xff0c;是以ARM cortex-A系列处理器&#xff0c;来介绍ARM的soc中&#xff0c;中断的处理。 ARM cortex-A系列处理器&#xff0c;提供…

Python之Django项目的功能配置

1.创建Django项目 进入项目管理目录&#xff0c;比如&#xff1a;D盘 执行命令&#xff1a;diango-admin startproject demo1 创建项目 如果提示diango命令不存在&#xff0c;搜索diango-admin程序的位置&#xff0c;然后加入到环境变量path中。 进入项目&#xff0c;cd demo…

CentOS 7 Tomcat服务的安装

前提 安装ava https://blog.csdn.net/qq_36940806/article/details/134945175?spm1001.2014.3001.5501 1. 下载 wget https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-9/v9.0.84/bin/apache-tomcat-9.0.84.tar.gzps: 可选择自己需要的版本下载安装https://mirr…

【单调栈】LeetCode2334:元素值大于变化阈值的子数组

作者推荐 map|动态规划|单调栈|LeetCode975:奇偶跳 涉及知识点 单调栈 题目 给你一个整数数组 nums 和一个整数 threshold 。 找到长度为 k 的 nums 子数组&#xff0c;满足数组中 每个 元素都 大于 threshold / k 。 请你返回满足要求的 任意 子数组的 大小 。如果没有这…

STM32CubeMX驱动ST7789

环境 1、单片机:STM32F103C8T6 2、开发平台&#xff1a;STM32CUBEMXkeil mdk 3、屏幕&#xff1a;ST7789&#xff0c;分辨率240*240 STM32配置 1、使用硬件SPI1驱动屏幕。配置如下&#xff1a; 2、屏幕控制引脚配置&#xff1a; 注意&#xff1a;只配置了DC,RST,CS这3个控…

使用 Taro 开发鸿蒙原生应用 —— 探秘适配鸿蒙 ArkTS 的工作原理

背景 在上一篇文章中&#xff0c;我们已经了解到华为即将发布的鸿蒙操作系统纯血版本——鸿蒙 Next&#xff0c;以及各个互联网厂商开展鸿蒙应用开发的消息。其中&#xff0c;Taro作为一个重要的前端开发框架&#xff0c;也积极适配鸿蒙的新一代语言框架 —— ArkTS。 本文将…