<!-- //?模块说明 => 合同盖章模块 addToTab--> <template> <div class="contract-signature-view"> <!-- <div class="title-operation"> <h2 class="title">图片盖章</h2> </div> --> <div class="section-box"> <!-- 盖章图片 --> <aside class="signature-img"> <div class="info"> <h3 class="name">图片章列表</h3> <p class="text">将图片章拖到文件相应区域即可</p> </div> <!-- 拖拽 --> <draggable v-model="mainImagelist" :group="{ name: 'itext', pull: 'clone' }" :sort="false" @end="end"> <transition-group type="transition"> <li v-for="item in mainImagelist" :key="item.img" class="item" style="text-align: center"> <img :src="item.img" width="100%;" height="100%" class="img" /> </li> </transition-group> </draggable> </aside> <!-- 主体区域 --> <section class="main-layout" :class="{ 'is-first': isFirst }"> <!-- 操作 --> <div class="operate-box"> <!-- <div class="slider-box"> <el-slider class="slider" v-model="scale" :min="0.5" :max="2" :step="0.1" :show-tooltip="false" @change="sliderChange" /> <span class="scale-value">{{ (scale * 100).toFixed(0) + '%' }}</span> </div> --> <div class="page-change"> <i class="icon el-icon-arrow-left" @click="prevPage" /> <!-- :min="1" --> <el-input class="input-box" v-model.number="pageNum" :max="defaultNumPages" @change="cutover" @blur="pageNum = pageNum.replace(/[^\d]/g, '')" /> <span class="default-text">/{{ defaultNumPages }}</span> <i class="icon el-icon-arrow-right" @click="nextPage" /> </div> <div class="operation"> <el-button class="btn-sc btn-red" @click="removeSignature">删除印章</el-button> <el-button class="btn-plzf btn-red" @click="clearSignature">清空印章</el-button> <el-button class="btn-tj" @click="submitSignature">保存盖章</el-button> </div> </div> <div id='pop'></div> <!-- 画图 --> <div ref="pdfView" id="pdf-view" class="out-view" :class="{ 'is-show': isShowPdf }"> <div class="canvas-layout" v-for="item in numPages" :key="item"> <!-- pdf部分 --> <canvas class="the-canvas"></canvas> <!-- 盖章部分 --> <canvas :ref="'ele-canvas'+item" 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'; import api from "../../public/config/api/api"; // pdf插件 import { fabric } from 'fabric'; import html2canvas from "html2canvas"; import JsPDF from "jspdf"; import workerSrc from 'pdfjs-dist/legacy/build/pdf.worker.entry'; import { getBase64FromUrl, changeImageToBase64 } from "./download" import { Loading } from 'element-ui' const pdfjsLib = require('pdfjs-dist/legacy/build/pdf.js'); pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc; export default { components: { draggable }, props: { // pdfUrl pdfUrl: { type: String, default: "", } }, data () { return { // pdf地址 // 左侧盖章列表 mainImagelist: [], // 右侧坐标数据 coordinateList: [{ name: '印章名称', page: '所在页面', left: 'x坐标', top: 'Y坐标' }], // 总页数 numPages: 1, defaultNumPages: 1, // 当前页 pageNum: 1, // 缩放比例 scale: 1.5, // pdf是否显示 isFirst: true, isShowPdf: false, // pdf最外层的out-view outViewDom: null, // 各页pdf的canvas-layout canvasLayoutTopList: [], // 用来盖章的canvas数组 canvasEle: [], // 绘图区域的宽高 whDatas: null, // pdf渲染的canvas数组 canvas: [], // pdf渲染的canvas的ctx数组 ctx: [], // pdf渲染的canvas的宽高 pdfDoc: null, // 隐藏的input,用来提交数据 shadowInputValue: '', loadingInstance: '', pdfBase64: '', accessToken: '', position: 0, judge: true, //循环清空pdfDoc避免内存溢出 pdfDocNullTime: null, }; }, async created () { this.outViewScrollClose() this.accessToken = JSON.parse(localStorage.getItem('token'))?.access_token || '' this.mainImagelist = []; this.getSignChapter(); this.showpdf(this.pdfUrl) }, beforeUnload () { // 在组件即将被销毁之前执行的清理工作或操作 this.judge = false }, mounted () { }, methods: { // 提交数据 submitSignature () { this.removeActive();//清除所有的盖章选中状态 if (this.coordinateList.length > 1) { this.formatSeal() setTimeout(() => { this.saveSign() }, 100); } else { this.$message.warning("至少添加一个印章!") } }, // 保存前格式印章 formatSeal () { let arr = [] const keys = ['width', 'height', 'top', 'left', 'angle', 'scaleX', 'scaleY', 'absTop', 'widthX', 'heightY']; this.canvasEle.forEach((item) => { let object = item.getObjects(); if (object.length > 0) { object.forEach((obj) => { for (let i = 0; i < this.coordinateList.length; i++) { var coord = this.coordinateList[i]; if (coord.cacheKey == obj.cacheKey) { coord.absTop = Math.abs(obj.top - (obj.canvas.height - obj.getBoundingRect().height)) coord.widthX = obj.width * obj.scaleX coord.heightY = obj.height * obj.scaleY coord.width = obj.width coord.height = obj.height coord.top = obj.top coord.left = obj.left keys.forEach((item) => { coord[item] = Math.ceil(coord[item] / this.scale); }); break; } } }) } }); }, //保存并提交盖章数据 async saveSign () { this.loadingInstance = Loading.service({ fullscreen: true }) let sealList = [...this.coordinateList] sealList.splice(0, 1) for (let seal of sealList) { seal.top = seal.absTop seal.width = seal.widthX seal.height = seal.heightY } let data = { pdfUrl: this.pdfUrl, sealList: sealList } this.$parent.$parent.$parent.saveSignSeal(data); }, closeLoading () { this.loadingInstance.close(); }, // 获取文件详情 async getSignChapter () { var that = this; const { data: res } = await this.$http(api.getSignChapter) if (!res.success) { this.$message.warning('请添加图片章!'); return } for (const key in res.data) { var path = res.data[key].fileUrl //这里的图片URL链接跟pdf的要同域 changeImageToBase64(path).then((data) => { var name = res.data[key].name; that.mainImagelist.push({ name: name, img: data, path: path }) }).catch((err) => { // console.log('输出reject的结果:',err) //输出reject的结果:数字大于5 }) } }, // 解析pdf async showpdf (data) { //文字无法正常显示,需要添加PDFJS.cMaoUrl PDFJS.cMapPacked 这两个值 await pdfjsLib .getDocument({ url: data, rangeChunkSize: 65536 * 16, disableAutoFetch: true, disableStream: true }) .promise.then((pdfDoc_) => { this.pdfDoc = pdfDoc_; this.numPages = this.pdfDoc.numPages; this.defaultNumPages = this.pdfDoc.numPages; this.$nextTick(() => { var ratio = window.devicePixelRatio || 1; //设置设备像素比 devicePixelRatio this.canvas = document.querySelectorAll('.the-canvas'); this.canvas.forEach((item) => { var ctx = item.getContext('2d', { willReadFrequently: true }) ctx.scale(ratio, ratio); ctx.font = (48 * ratio) + 'px Microsoft YaHei'; this.ctx.push(ctx); }); // 循环渲染pdf let i = 1 this.forPdfShow(i); }); }); }, async forPdfShow (i) { if (!this.judge) { return } try { await this.renderPage(i).then(() => { this.renderPdf({ width: this.canvas[i - 1].width, height: this.canvas[i - 1].height }); i++; if (i <= this.numPages) { setTimeout(() => { this.forPdfShow(i) }, 1); } if (i - 1 == this.numPages) { setTimeout(() => { this.renderFabric(); this.canvasEvents(); }, 100); } }); } catch (error) { console.log(error); this.pdfDoc = null; this.judge = false } }, beforeDom () { this.pdfDoc = null }, // 设置pdf宽高,缩放比例,渲染pdf renderPage (num) { return this.pdfDoc.getPage(num).then((page) => { const viewport = page.getViewport({ scale: this.scale }); // 设置视口大小 this.canvas[num - 1].height = viewport.height; this.canvas[num - 1].width = viewport.width; // Render PDF page into canvas context const renderContext = { canvasContext: this.ctx[num - 1], viewport: viewport }; page.render(renderContext); }); }, // 设置绘图区域宽高 renderPdf (data) { this.whDatas = data; }, // 生成绘图区域 renderFabric () { let width = 0 let height = 0 var _that = this; // 1. 拿到全部的canvas-layout const canvasLayoutDom = document.querySelectorAll('.canvas-layout'); // 2. 循环遍历 for (let i = 0; i < canvasLayoutDom.length; i++) { const item = canvasLayoutDom[i]; this.canvasLayoutTopList.push({ obj: item, top: item.offsetTop }); // 5. 拿到pdf的canvas const pCenter = item.querySelector('.the-canvas'); width = pCenter.clientWidth height = pCenter.clientHeight // 3. 设置宽高和居中 item.style.width = width + 'px'; item.style.height = this.whDatas.height + 'px'; item.style.margin = '0 auto 18px'; // item.style.boxShadow = '4px 4px 4px #e9e9e9'; // 4. 拿到盖章canvas const canvasEle = item.querySelector('.ele-canvas'); // 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'; if (i == canvasLayoutDom.length - 1) { // 现形 setTimeout(() => { this.pdfDoc = null this.isFirst = false; this.isShowPdf = true; this.outViewDom = document.querySelector('.out-view'); // 开启监听窗口滚动 this.outViewScroll(); this.$nextTick(() => { _that.pdfDoc = null let j = 0 this.pdfDocNullTime = setInterval(() => { _that.pdfDoc = null j++; if (j > 15) { clearInterval(_that.pdfDocNullTime); } }, 1000); }) }, 100); } } const divElement = this.$refs.pdfView; divElement.style.width = width + 'px'; // 修改div的宽度 }, // 开启监听窗口滚动 outViewScroll () { this.outViewDom.addEventListener('scroll', this.outViewRun); }, // 关闭监听窗口滚动 outViewScrollClose () { try { this.outViewDom.removeEventListener('scroll', this.outViewRun); } catch (error) { // } }, // 窗口滚动 outViewRun () { const scrollTop = this.outViewDom.scrollTop; const topList = this.canvasLayoutTopList.map((item) => item.top); // 增加一个最大值 topList.push(Number.MAX_SAFE_INTEGER); for (let index = 0; index < topList.length; index++) { const element = topList[index]; if (element <= scrollTop && scrollTop < topList[index + 1]) { this.pageNum = index + 1; break; } } }, // scale滑块,重新渲染整个pdf sliderChange () { this.pageNum = 1; this.numPages = 0; this.canvasLayoutTopList = []; this.canvasEle = []; this.ctx = []; this.canvas = []; this.isShowPdf = false; // this.outViewScrollClose(); this.whDatas = null; this.coordinateList = [{ name: '名称', page: '所在页面', left: 'x坐标', top: 'Y坐标' }]; this.getSignatureJson(); setTimeout(() => { this.numPages = this.pdfDoc.numPages; this.$nextTick(() => { this.canvas = document.querySelectorAll('.the-canvas'); this.canvas.forEach((item) => { this.ctx.push(item.getContext('2d')); }); // 循环渲染pdf for (let i = 1; i <= this.numPages; i++) { this.renderPage(i).then(() => { this.renderPdf({ width: this.canvas[i - 1].width, height: this.canvas[i - 1].height }); }); } setTimeout(() => { this.renderFabric(); this.canvasEvents(); }, 1000); }); }, 1000); }, /** * 盖章相关部分 */ // 盖章拖拽边界处理,不能将图片拖拽到绘图区域外 canvasEvents () { this.canvasEle.forEach((item) => { item.on('object:moving', (e) => { const obj = e.target; // if object is too big ignore if (obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width) { return; } obj.setCoords(); // top-left corner if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) { obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top); obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left); } // bot-right corner if ( obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height || obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width ) { obj.top = Math.min( obj.top, obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top ); obj.left = Math.min( obj.left, obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left ); } obj.absTop = Math.abs(obj.top - (obj.canvas.height - obj.getBoundingRect().height)) obj.widthX = obj.width * obj.scaleX obj.heightY = obj.height * obj.scaleY const findIndex = this.coordinateList .slice(1) .findIndex((coord) => coord.cacheKey == obj.cacheKey); const keys = ['width', 'height', 'top', 'left', 'angle', 'scaleX', 'scaleY', 'absTop', 'widthX', 'heightY']; 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); let absTop = '' this.addSeal({ sealUrl: this.mainImagelist[e.newDraggableIndex].path, left, top, absTop, index: e.newDraggableIndex, pageNum: findIndex }); }, // 添加公章 addSeal ({ sealUrl, left, top, index, pageNum }) { fabric.Image.fromURL(sealUrl, (oImg) => { oImg.set({ // 距离左边的距离 left: left, // 距离顶部的距离 top: top, // 角度 // angle: 10, // 缩放比例,需要乘以scale scaleX: 0.8 * this.scale, scaleY: 0.8 * this.scale, index, // 禁止缩放 // lockScalingX: true, // lockScalingY: true, // // 禁止旋转 lockRotation: true }); this.canvasEle[pageNum].add(oImg); // 保存盖章信息 this.saveSignature({ pageNum, index, sealUrl }); }); // this.removeActive(); }, // 保存盖章 saveSignature ({ pageNum, index, sealUrl }) { // 1. 拿到当前盖章的信息 let length = 0; let pageConfig = this.coordinateList.filter((item) => item.page - 1 == pageNum); if (pageConfig) length = pageConfig.length; const currentSignInfo = this.canvasEle[pageNum].getObjects()[length]; currentSignInfo.absTop = Math.abs(currentSignInfo.top - (currentSignInfo.canvas.height - currentSignInfo.getBoundingRect().height)) currentSignInfo.widthX = currentSignInfo.width * currentSignInfo.scaleX currentSignInfo.heightY = currentSignInfo.height * currentSignInfo.scaleY // 2. 拼接数据 const keys = ['width', 'height', 'top', 'left', 'angle', 'scaleX', 'scaleY', 'absTop', 'widthX', 'heightY']; 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}`; obj.page = pageNum + 1; this.coordinateList.push(obj); this.getSignatureJson(); }, // 盖章生成json字符串 getSignatureJson () { // 1. 判断是否有盖章 if (this.coordinateList.length <= 1) return (this.shadowInputValue = ''); // 2. 拿到盖章的信息,去除第一条 const signatureList = this.coordinateList.slice(1); // 3. 拼接数据,只要left和top和page const keys = ['page', 'left', 'top']; const arr = []; signatureList.forEach((item) => { const obj = {}; keys.forEach((key) => { obj[key] = item[key]; }); arr.push(obj); }); // 4. 转成json字符串 this.shadowInputValue = JSON.stringify(arr); }, /** * 操作相关部分 */ // 上一页 prevPage () { if (this.pageNum <= 1) return; this.pageNum--; // 滚动到指定位置 this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top; }, // 下一页 nextPage () { if (this.pageNum >= this.numPages) return; this.pageNum++; // 滚动到指定位置 this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top; }, // 切换页码 cutover () { this.outViewScrollClose(); if (this.pageNum < 1) { this.pageNum = 1; } else if (this.pageNum > this.numPages) { this.pageNum = this.numPages; } // 滚动到指定位置 this.outViewDom.scrollTop = this.canvasLayoutTopList[this.pageNum - 1].top; setTimeout(() => { this.outViewScroll(); }, 500); }, // 删除所有的盖章选中状态 removeActive () { this.canvasEle.forEach((item) => { item.discardActiveObject().renderAll(); }); }, // 删除盖章 removeSignature () { // 1. 判断是否有选中的盖章 const findItem = this.canvasEle.filter((item) => item.getActiveObject()); // 2. 判断选中盖章的个数 if (findItem.length == 0) return this.$message.warning('请选择要删除的印章'); // 3. 判断选中盖章的个数是否大于1 if (findItem.length > 1) { this.removeActive(); return this.$message.warning('只能选择删除一个印章,请重新选择'); } // 4. 拿到选中的盖章的cacheKey const activeObj = findItem[0].getActiveObject(); const findIndex = this.coordinateList.findIndex( (item) => item.cacheKey == activeObj.cacheKey ); // 5. 删除选中的盖章 findItem[0].remove(activeObj); // 6. 删除选中的盖章的信息 this.coordinateList.splice(findIndex, 1); this.getSignatureJson(); }, // 清空盖章 clearSignature () { this.canvasEle.forEach((item) => { item.clear(); }); this.coordinateList = [{ name: '名称', page: '所在页面', left: 'x坐标', top: 'Y坐标' }]; this.getSignatureJson(); }, } }; </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; width: 100px; .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; margin-left: 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: unset; } } .loading { width: 20px; height: 20px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 999; .el-loading-mask { background-color: transparent; } } } .position-info { width: 420px; 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; } } } } } .eui-three-btn .el-button span::before { } } </style>
标签:盖章,canvas,vue,obj,width,top,电子,height,item From: https://www.cnblogs.com/aknife/p/17980272