当前位置: 首页 > news >正文

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>

相关文章:

vue2 之 实现pdf电子签章

一、前情提要 1. 需求 仿照e签宝&#xff0c;实现pdf电子签章 > 拿到pdf链接&#xff0c;移动章的位置&#xff0c;获取章的坐标 技术 : 使用fabric pdfjs-dist vuedraggable 2. 借鉴 一位大佬的代码仓亏 : 地址 一位大佬写的文章 &#xff1a;地址 3. 优化 在大佬的代码…...

什么是MVC?MVC框架的优势和特点

目录 一、什么是MVC 二、MVC模式的组成部分和工作原理 1、模型&#xff08;Model&#xff09; 2、视图&#xff08;View&#xff09; 3、控制器&#xff08;Controller&#xff09; 三、MVC模式的工作过程如下&#xff1a; 用户发送请求&#xff0c;请求由控制器处理。 …...

主从复制mysql-replication | Replication故障排除

主从复制mysql-replication 准备环境 #防火墙 selinux systemctl stop firewalld --now &&setenforce 0 #修改主机名&#xff1a;hostnamectl set-hostname 名字 tip&#xff1a;vim /etc/sysconfig/network-scripts/ifcfg-ens33 BOOTPRTOTstatic IPADDR192.168.100.…...

基于Java SSM框架实现教学质量评价评教系统项目【项目源码+论文说明】计算机毕业设计

基于java的SSM框架实现教学质量评价评教系统演示 摘要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;教学质量评价系统当然也不能排除在外。教学质量评价系统是以实际运用为…...

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

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

springcloud-gateway-2-鉴权

目录 一、跨域安全设置 二、GlobalFilter实现全局的过滤与拦截。 三、GatewayFilter单个服务过滤器 1、原理-官方内置过滤器 2、自定义过滤器-TokenAuthGatewayFilterFactory 3、完善TokenAuthGatewayFilterFactory的功能 4、每一个服务编写一个或多个过滤器&#xff0c…...

实现一个最简单的内核

更好的阅读体验&#xff0c;请点击 YinKai s Blog | 实现一个最简单的内核。 ​ 这篇文章带大家实现一个最简单的操作系统内核—— Hello OS。 PC 机的引导流程 ​ 我们这里将借助 Ubuntu Linux 操纵系统上的 GRUB 引导程序来引导我们的 Hello OS。 ​ 首先我们得了解一下&a…...

2024华为OD机试真题指南宝典—持续更新(JAVAPythonC++JS)【彻底搞懂算法和数据结构—算法之翼】

PC端可直接搜索关键词 快捷键&#xff1a;CtrlF 年份关键字、题目关键字等等 注意看本文目录-快速了解本专栏 文章目录 &#x1f431;2024年华为OD机试真题&#xff08;马上更新&#xff09;&#x1f439;2023年华为OD机试真题&#xff08;更新中&#xff09;&#x1f436;新…...

【12.23】转行小白历险记-算法02

不会算法的小白不是好小白&#xff0c;可恶还有什么可以难倒我这个美女的&#xff0c;不做花瓶第二天&#xff01; 一、螺旋矩阵 59. 螺旋矩阵 II - 力扣&#xff08;LeetCode&#xff09; 1.核心思路&#xff1a;确定循环的路线&#xff0c;左闭右开循环&#xff0c;思路简…...

k8s部署nginx-ingress服务

k8s部署nginx-ingress服务 经过大佬的拷打&#xff0c;终于把这块的内容配置完成了。 首先去 nginx-ingress官网查看相关内容。 核心就是这个&#xff1a; kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/prov…...

SpringBoot Elasticsearch全文搜索

文章目录 概念全文搜索相关技术Elasticsearch概念近实时索引类型文档分片(Shard)和副本(Replica) 下载启用SpringBoot整合引入依赖创建文档类创建资源库测试文件初始化数据创建控制器 问题参考 概念 全文搜索&#xff08;检索&#xff09;&#xff0c;工作原理&#xff1a;计算…...

Python 常用模块re

Python 常用模块re 【一】正则表达式 【1】说明 正则表达式是一种强大的文本匹配和处理工具&#xff0c;主要用于字符串的模式匹配、搜索和替换。正则表达式测试网址&#xff1a;正则表达式在线测试 正则表达式手册&#xff1a;正则表达式手册 【2】字符组 字符转使用[]表…...

【华为OD题库-106】全排列-java

题目 给定一个只包含大写英文字母的字符串S&#xff0c;要求你给出对S重新排列的所有不相同的排列数。如:S为ABA&#xff0c;则不同的排列有ABA、AAB、BAA三种。 解答要求 时间限制:5000ms,内存限制:100MB 输入描述 输入一个长度不超过10的字符串S&#xff0c;确保都是大写的。…...

Three.js 详细解析(持续更新)

1、简介&#xff1b; Three.js依赖一些要素&#xff0c;第一是scene&#xff0c;第二是render&#xff0c;第三是carmea npm install --save three import * as THREE from "three"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js&quo…...

Unity中Shader平移矩阵

文章目录 前言方式一&#xff1a;对顶点本地空间下的坐标进行相加平移1、在属性面板定义一个四维变量记录在 xyz 上平移多少。2、在常量缓冲区进行申明3、在顶点着色器中&#xff0c;在进行其他坐标转化之前&#xff0c;对模型顶点本地空间下的坐标进行转化4、我们来看看效果 方…...

python dash 的学习笔记1

dash 用python开发web界面 https://dash.plotly.com/ 官方上支持jula F# python一类。当然我只会python只学习python中使用dash. 要做一个APP&#xff0c;用php,java以及.net都可以写&#xff0c;只所有选择python是因为最近在用这一个。同时也发现python除了慢全是优点。 资料…...

SQLITE如何同时查询出第一条和最后一条两条记录

一个时间记录表&#xff0c;需要同时得到整个表或一段时间内第一条和最后一条两条记录&#xff0c;按如下方法会提示错误&#xff1a;ORDER BY clause should come after UNION not before select * from sdayXX order by op_date asc limit 1 union select * from sday…...

四、ensp配置ftp服务器实验

文章目录 实验内容实验拓扑操作步骤配置路由器为ftp server 实验内容 本实验模拟企业网络。PC-1为FTP 用户端设备&#xff0c;需要访问FTP Server&#xff0c;从服务器上下载或上传文件。出于安全角度考虑&#xff0c;为防止服务器被病毒文件感染&#xff0c;不允许用户端直接…...

VS2020使用MFC开发一个贪吃蛇游戏

背景&#xff1a; 贪吃蛇游戏 按照如下步骤实现:。初始化地图 。通过键盘控制蛇运动方向&#xff0c;注意重新设置运动方向操作。 。制造食物。 。让蛇移动&#xff0c;如果吃掉食物就重新生成一个食物&#xff0c;如果会死亡就break。用蛇的坐标将地图中的空格替换为 #和”将…...

【经典LeetCode算法题目专栏分类】【第9期】深度优先搜索DFS与并查集:括号生成、岛屿问题、扫雷游戏

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能AI、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推荐--…...

linux之kylin系统nginx的安装

一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源&#xff08;HTML/CSS/图片等&#xff09;&#xff0c;响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址&#xff0c;提高安全性 3.负载均衡服务器 支持多种策略分发流量…...

关于nvm与node.js

1 安装nvm 安装过程中手动修改 nvm的安装路径&#xff0c; 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解&#xff0c;但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后&#xff0c;通常在该文件中会出现以下配置&…...

Python实现prophet 理论及参数优化

文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候&#xff0c;写过一篇简单实现&#xff0c;后期随着对该模型的深入研究&#xff0c;本次记录涉及到prophet 的公式以及参数调优&#xff0c;从公式可以更直观…...

Psychopy音频的使用

Psychopy音频的使用 本文主要解决以下问题&#xff1a; 指定音频引擎与设备&#xff1b;播放音频文件 本文所使用的环境&#xff1a; Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...

汇编常见指令

汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX&#xff08;不访问内存&#xff09;XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

C++:多态机制详解

目录 一. 多态的概念 1.静态多态&#xff08;编译时多态&#xff09; 二.动态多态的定义及实现 1.多态的构成条件 2.虚函数 3.虚函数的重写/覆盖 4.虚函数重写的一些其他问题 1&#xff09;.协变 2&#xff09;.析构函数的重写 5.override 和 final关键字 1&#…...

嵌入式常见 CPU 架构

架构类型架构厂商芯片厂商典型芯片特点与应用场景PICRISC (8/16 位)MicrochipMicrochipPIC16F877A、PIC18F4550简化指令集&#xff0c;单周期执行&#xff1b;低功耗、CIP 独立外设&#xff1b;用于家电、小电机控制、安防面板等嵌入式场景8051CISC (8 位)Intel&#xff08;原始…...

Unity中的transform.up

2025年6月8日&#xff0c;周日下午 在Unity中&#xff0c;transform.up是Transform组件的一个属性&#xff0c;表示游戏对象在世界空间中的“上”方向&#xff08;Y轴正方向&#xff09;&#xff0c;且会随对象旋转动态变化。以下是关键点解析&#xff1a; 基本定义 transfor…...

macOS 终端智能代理检测

&#x1f9e0; 终端智能代理检测&#xff1a;自动判断是否需要设置代理访问 GitHub 在开发中&#xff0c;使用 GitHub 是非常常见的需求。但有时候我们会发现某些命令失败、插件无法更新&#xff0c;例如&#xff1a; fatal: unable to access https://github.com/ohmyzsh/oh…...