vue2實(shí)現(xiàn)pdf電子簽章問題記錄

一、前情提要
1. 需求
仿照e簽寶,實(shí)現(xiàn)pdf電子簽章 => 拿到pdf鏈接,移動章的位置,獲取章的坐標(biāo)
技術(shù) : 使用fabric + pdfjs-dist + vuedraggable

2. 借鑒
一位大佬的代碼倉虧 : 地址
一位大佬寫的文章 :地址

3. 優(yōu)化
在大佬的代碼基礎(chǔ)上,進(jìn)行了些許優(yōu)化,變的更像e簽寶

二、下載
ps : 怕版本不同,導(dǎo)致無法運(yùn)行,請下載指定版本
1. fabric
fabric : 是一個(gè)功能強(qiáng)大且操作簡單的 Javascript HTML5 canvas 工具庫
npm install fabric@5.3.0
2. pdfjs-dist
npm install pdfjs-dist@2.5.207
問題一
注意 : 最好配置一下babel,因?yàn)榇虬臅r(shí)候可能會報(bào)錯
因?yàn)閎abel默認(rèn)不會轉(zhuǎn)化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'),
// 轉(zhuǎn)化pdfjs-dist,之所以分開寫,是因?yàn)閜dfjs-dist里面有很多es6的語法,但是我們只需要轉(zhuǎn)化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. 準(zhǔn)備pdf文件
text.pdf 可放置在 src/static 文件夾中
ps : 線上最好讓后端返回pdf鏈接,因?yàn)榇嬖趐df跨域問題
2. 大佬的代碼

<!-- //?模塊說明 => 合同簽章模塊 -->
<template>
<div id="elesign" class="elesign">
<el-row>
<el-col :span="4" style="margin-top: 1%">
<div class="left-title">我的印章</div>
<draggable
v-model="mainImagelist"
:group="{ name: 'itext', pull: 'clone' }"
:sort="false"
@end="end"
>
<transition-group type="transition">
<li v-for="item in mainImagelist" :key="item" class="item" style="text-align: center">
<img :src="item" width="100%;" height="100%" class="imgstyle" />
</li>
</transition-group>
</draggable>
</el-col>
<el-col :span="16" style="text-align: center" class="pCenter">
<div class="page">
<!-- <el-button class="btn-outline-dark" @click="zoomIn">-</el-button>
<span style="color: red">{{ (percentage * 100).toFixed(0) + '%' }}</span>
<el-button class="btn-outline-dark" @click="zoomOut">+</el-button> -->
<el-button class="btn-outline-dark" @click="prevPage">上一頁</el-button>
<el-button class="btn-outline-dark" @click="nextPage">下一頁</el-button>
<el-button class="btn-outline-dark">{{ pageNum }}/{{ numPages }}頁</el-button>
<el-input-number
style="margin: 0 5px; border-radius: 5px"
class="btn-outline-dark"
v-model="pageNum"
:min="1"
:max="numPages"
label="輸入頁碼"
></el-input-number>
<el-button class="btn-outline-dark" @click="cutover">跳轉(zhuǎn)</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">任務(wù)信息</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">發(fā)起方</div>
<div class="detail-item-desc">{{ taskInfo.uname }}</div>
</div>
<div class="right-item">
<div class="right-item-title">截止時(shí)間</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預(yù)覽
pdfUrl: '',
pdfDoc: null,
numPages: 1,
pageNum: 1,
scale: 2.2,
pageRendering: false,
pageNumPending: null,
sealUrl: '',
signUrl: '',
canvas: null,
ctx: null,
canvasEle: null,
whDatas: null,
mainImagelist: [],
taskInfo: {}
// percentage: 1
};
},
computed: {
hasSigna() {
if (this.canvasEle && this.canvasEle.getObjects()[0]) {
return true;
} else {
return false;
}
}
},
created() {
var that = this;
that.mainImagelist = [require('@/assets/img/projectCenter/sign.png'), require('@/assets/img/projectCenter/seal.png')];
that.taskInfo = { title: '測試蓋章', uname: '張三', endtime: '2021-09-01 17:59:59' };
this.setPdfArea();
},
mounted() {
// this.showpdf(this.pdfUrl);
if (!pdfjsLib.getDocument || !pdfjsViewer.PDFViewer) {
// eslint-disable-next-line no-alert
alert('Please build the pdfjs-dist library using\n `gulp dist-install`');
}
},
methods: {
// pdf預(yù)覽
// zoomIn() {
// console.log('縮小');
// if (this.scale <= 0.5) {
// this.$message.error('已經(jīng)顯示最小比例');
// } 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('已經(jīng)顯示最大比例');
// } 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 }); // 設(shè)置視口大小
_this.canvas.height = viewport.height;
_this.canvas.width = viewport.width;
// Render PDF page into canvas context
let renderContext = {
canvasContext: _this.ctx,
viewport: viewport
};
let renderTask = page.render(renderContext);
// Wait for rendering to finish
renderTask.promise.then(() => {
_this.pageRendering = false;
if (_this.pageNumPending !== null) {
// New page rendering is pending
this.renderPage(_this.pageNumPending);
_this.pageNumPending = null;
}
});
});
},
queueRenderPage(num) {
if (this.pageRendering) {
this.pageNumPending = num;
} else {
this.renderPage(num);
}
},
prevPage() {
this.confirmSignature();
if (this.pageNum <= 1) {
return;
}
this.pageNum--;
},
nextPage() {
this.confirmSignature();
if (this.pageNum >= this.numPages) {
return;
}
this.pageNum++;
},
cutover() {
this.confirmSignature();
},
// 渲染pdf,到時(shí)還會蓋章信息,在渲染時(shí),同時(shí)顯示出來,不應(yīng)該在切換頁碼時(shí)才顯示印章信息
showpdf(pdfUrl) {
let caches = JSON.parse(localStorage.getItem('signs')); // 獲取緩存字符串后轉(zhuǎn)換為對象
// 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);
});
},
/**
* 蓋章部分開始
*/
// 設(shè)置繪圖區(qū)域?qū)捀?
renderPdf(data) {
this.whDatas = data;
// document.querySelector("#elesign").style.width = data.width + "px";
},
// 生成繪圖區(qū)域
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%";
},
// 相關(guān)事件操作喲
canvasEvents() {
// 拖拽邊界 不能將圖片拖拽到繪圖區(qū)域外
this.canvasEle.on('object:moving', function (e) {
var obj = e.target;
// if object is too big ignore
if (obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width) {
return;
}
obj.setCoords();
// top-left corner
if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) {
obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top);
obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left);
}
// bot-right corner
if (
obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height ||
obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width
) {
obj.top = Math.min(
obj.top,
obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top
);
obj.left = Math.min(
obj.left,
obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left
);
}
});
},
// 添加公章
addSeal(sealUrl, left, top, index) {
fabric.Image.fromURL(sealUrl, (oImg) => {
oImg.set({
left: left,
top: top,
// angle: 10,
scaleX: 0.8,
scaleY: 0.8,
index: index
});
// oImg.scale(0.5); //圖片縮小一
this.canvasEle.add(oImg);
});
},
// 刪除簽章
removeSignature() {
this.canvasEle.remove(this.canvasEle.getActiveObject());
},
// 翻頁展示蓋章信息
commonSign(pageNum, isFirst = false) {
if (isFirst == false) this.canvasEle.remove(this.canvasEle.clear()); // 清空頁面所有簽章
let caches = JSON.parse(localStorage.getItem('signs')); // 獲取緩存字符串后轉(zhuǎn)換為對象
// 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
);
}
}
},
// 確認(rèn)簽章位置并保存到緩存
confirmSignature() {
let data = this.canvasEle.getObjects(); // 獲取當(dāng)前頁面內(nèi)的所有簽章信息
let caches = JSON.parse(localStorage.getItem('signs')); // 獲取緩存字符串后轉(zhuǎn)換為對象
let signDatas = {}; // 存儲當(dāng)前頁的所有簽章信息
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)); // 對象轉(zhuǎn)字符串后存儲到緩存
},
// 提交數(shù)據(jù)
submitSignature() {
this.confirmSignature();
// let caches = localStorage.getItem('signs');
// console.log(JSON.parse(caches));
return false;
},
// 清空數(shù)據(jù)
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
);
},
// 設(shè)置PDF預(yù)覽區(qū)域高度
setPdfArea() {
this.pdfUrl = './static/text.pdf';
// this.pdfurl = res.data.data.pdfurl;
this.$nextTick(() => {
this.showpdf(this.pdfUrl); // 接口返回的應(yīng)該還有蓋章信息,不只是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. 優(yōu)化后的代碼

<!-- //?模塊說明 => 合同簽章模塊 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">將示例印章標(biāo)識拖到文件相應(yīng)區(qū)域即可獲取簽章位置</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>
<!-- 主體區(qū)域 -->
<section class="main-layout" :class="{ 'is-first': isFirst }">
<!-- 操作 -->
<div class="operate-box">
<div class="slider-box">
<el-slider
class="slider"
v-model="scale"
:min="0.5"
:max="2"
:step="0.1"
:show-tooltip="false"
@change="sliderChange"
/>
<span class="scale-value">{{ (scale * 100).toFixed(0) + '%' }}</span>
</div>
<div class="page-change">
<i class="icon el-icon-arrow-left" @click="prevPage" />
<!-- :min="1" -->
<el-input
class="input-box"
v-model.number="pageNum"
:max="defaultNumPages"
@change="cutover"
/>
<span class="default-text">/{{ defaultNumPages }}</span>
<i class="icon el-icon-arrow-right" @click="nextPage" />
</div>
</div>
<!-- 畫圖 -->
<div class="out-view" :class="{ 'is-show': isShowPdf }">
<div class="canvas-layout" v-for="item in numPages" :key="item">
<!-- pdf部分 -->
<canvas class="the-canvas" />
<!-- 蓋章部分 -->
<canvas class="ele-canvas"></canvas>
</div>
</div>
<i class="loading" v-loading="!isShowPdf" />
</section>
<!-- 位置信息 -->
<div class="position-info">
<h3 class="title">位置信息</h3>
<ul class="nav">
<li class="item" v-for="(item, index) in coordinateList" :key="index">
<span>{{ item.name }}</span>
<span>{{ item.page }}</span>
<span>{{ item.left }}</span>
<span>{{ item.top }}</span>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
// 拖拽插件
import draggable from 'vuedraggable';
// pdf插件
import { fabric } from 'fabric';
import workerSrc from 'pdfjs-dist/es5/build/pdf.worker.entry';
const pdfjsLib = require('pdfjs-dist/es5/build/pdf.js');
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
export default {
components: { draggable },
data() {
return {
// pdf地址
pdfUrl: '',
// 左側(cè)簽章列表
mainImagelist: [],
// 右側(cè)坐標(biāo)數(shù)據(jù)
coordinateList: [{ name: '名稱', page: '所在頁面', left: 'x坐標(biāo)', top: 'Y坐標(biāo)' }],
// 總頁數(shù)
numPages: 1,
defaultNumPages: 1,
// 當(dāng)前頁
pageNum: 1,
// 縮放比例
scale: 1,
// pdf是否顯示
isFirst: true,
isShowPdf: false,
// pdf最外層的out-view
outViewDom: null,
// 各頁pdf的canvas-layout
canvasLayoutTopList: [],
// 用來簽章的canvas數(shù)組
canvasEle: [],
// 繪圖區(qū)域的寬高
whDatas: null,
// pdf渲染的canvas數(shù)組
canvas: [],
// pdf渲染的canvas的ctx數(shù)組
ctx: [],
// pdf渲染的canvas的寬高
pdfDoc: null,
// 隱藏的input,用來提交數(shù)據(jù)
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相關(guān)部分
*/
// 設(shè)置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); // 接口返回的應(yīng)該還有蓋章信息,不只是pdf
});
},
// 解析pdf
showpdf(pdfUrl) {
pdfjsLib
.getDocument({ url: pdfUrl, rangeChunkSize: 65536, disableAutoFetch: false })
.promise.then((pdfDoc_) => {
this.pdfDoc = pdfDoc_;
this.numPages = this.pdfDoc.numPages;
this.defaultNumPages = this.pdfDoc.numPages;
this.$nextTick(() => {
this.canvas = document.querySelectorAll('.the-canvas');
this.canvas.forEach((item) => {
this.ctx.push(item.getContext('2d'));
});
// 循環(huán)渲染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);
});
});
},
// 設(shè)置pdf寬高,縮放比例,渲染pdf
renderPage(num) {
// console.log('this.canvas', this.canvas[num], num);
return this.pdfDoc.getPage(num).then((page) => {
const viewport = page.getViewport({ scale: this.scale }); // 設(shè)置視口大小
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);
});
},
// 設(shè)置繪圖區(qū)域?qū)捀?
renderPdf(data) {
this.whDatas = data;
},
// 生成繪圖區(qū)域
renderFabric() {
// 1. 拿到全部的canvas-layout
const canvasLayoutDom = document.querySelectorAll('.canvas-layout');
// 2. 循環(huán)遍歷
canvasLayoutDom.forEach((item) => {
this.canvasLayoutTopList.push({ obj: item, top: item.offsetTop });
// 3. 設(shè)置寬高和居中
item.style.width = this.whDatas.width + 'px';
item.style.height = this.whDatas.height + 'px';
item.style.margin = '0 auto 18px';
item.style.boxShadow = '4px 4px 4px #e9e9e9';
// 4. 拿到蓋章canvas
const canvasEle = item.querySelector('.ele-canvas');
// 5. 拿到pdf的canvas
const pCenter = item.querySelector('.the-canvas');
// 6. 設(shè)置蓋章canvas的寬高
canvasEle.width = pCenter.clientWidth;
canvasEle.height = this.whDatas.height;
// 7. 創(chuàng)建fabric對象并存儲
this.canvasEle.push(new fabric.Canvas(canvasEle));
// 8. 設(shè)置蓋章canvas的樣式
const container = item.querySelector('.canvas-container');
container.style.position = 'absolute';
container.style.left = '50%';
container.style.transform = 'translateX(-50%)';
container.style.top = '0px';
});
// 現(xiàn)形
this.isFirst = false;
this.isShowPdf = true;
this.outViewDom = document.querySelector('.out-view');
// 開啟監(jiān)聽窗口滾動
this.outViewScroll();
},
// 開啟監(jiān)聽窗口滾動
outViewScroll() {
this.outViewDom.addEventListener('scroll', this.outViewRun);
},
// 關(guān)閉監(jiān)聽窗口滾動
outViewScrollClose() {
this.outViewDom.removeEventListener('scroll', this.outViewRun);
},
// 窗口滾動
outViewRun() {
const scrollTop = this.outViewDom.scrollTop;
const topList = this.canvasLayoutTopList.map((item) => item.top);
// 增加一個(gè)最大值
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滑塊,重新渲染整個(gè)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坐標(biāo)', top: 'Y坐標(biāo)' }];
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'));
});
// 循環(huán)渲染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);
},
/**
* 簽章相關(guān)部分
*/
// 簽章拖拽邊界處理,不能將圖片拖拽到繪圖區(qū)域外
canvasEvents() {
this.canvasEle.forEach((item) => {
item.on('object:moving', (e) => {
const obj = e.target;
// if object is too big ignore
if (obj.currentHeight > obj.canvas.height || obj.currentWidth > obj.canvas.width) {
return;
}
obj.setCoords();
// top-left corner
if (obj.getBoundingRect().top < 0 || obj.getBoundingRect().left < 0) {
obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top);
obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left);
}
// bot-right corner
if (
obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height ||
obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width
) {
obj.top = Math.min(
obj.top,
obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top
);
obj.left = Math.min(
obj.left,
obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left
);
}
// console.log('obj.cacheKey',obj.cacheKey);
const findIndex = this.coordinateList
.slice(1)
.findIndex((coord) => coord.cacheKey == obj.cacheKey);
const keys = ['width', 'height', 'top', 'left', 'angle', 'scaleX', 'scaleY'];
keys.forEach((item) => {
this.coordinateList[findIndex + 1][item] = Math.ceil(obj[item] / this.scale);
});
this.getSignatureJson();
});
});
},
// 拖拽結(jié)束
end(e) {
// 找到當(dāng)前拖拽到哪一個(gè)canvas-layout上
const currentCanvasLayout = e.originalEvent.target.parentElement.parentElement;
const findIndex = this.canvasLayoutTopList.findIndex(
(item) => item.obj == currentCanvasLayout
);
if (findIndex == -1) return false;
// 取整
const left = e.originalEvent.layerX < 0 ? 0 : Math.ceil(e.originalEvent.layerX / this.scale);
const top = e.originalEvent.layerY < 0 ? 0 : Math.ceil(e.originalEvent.layerY / this.scale);
// console.log('e', e, findIndex);
this.addSeal({
sealUrl: this.mainImagelist[e.newDraggableIndex].img,
left,
top,
index: e.newDraggableIndex,
pageNum: findIndex
});
},
// 添加公章
addSeal({ sealUrl, left, top, index, pageNum }) {
fabric.Image.fromURL(sealUrl, (oImg) => {
oImg.set({
// 距離左邊的距離
left: left,
// 距離頂部的距離
top: top,
// 角度
// angle: 10,
// 縮放比例,需要乘以scale
scaleX: 0.8 * this.scale,
scaleY: 0.8 * this.scale,
index,
// 禁止縮放
lockScalingX: true,
lockScalingY: true,
// 禁止旋轉(zhuǎn)
lockRotation: true
});
this.canvasEle[pageNum].add(oImg);
// 保存簽章信息
this.saveSignature({ pageNum, index, sealUrl });
});
// this.removeActive();
},
// 保存簽章
saveSignature({ pageNum, index, sealUrl }) {
// 1. 拿到當(dāng)前簽章的信息
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. 拼接數(shù)據(jù)
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. 拼接數(shù)據(jù),只要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. 轉(zhuǎn)成json字符串
this.shadowInputValue = JSON.stringify(arr);
},
/**
* 操作相關(guān)部分
*/
// 上一頁
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);
},
// 刪除所有的簽章選中狀態(tài)
removeActive() {
this.canvasEle.forEach((item) => {
item.discardActiveObject().renderAll();
});
},
// 刪除簽章
removeSignature() {
// 1. 判斷是否有選中的簽章
const findItem = this.canvasEle.filter((item) => item.getActiveObject());
// 2. 判斷選中簽章的個(gè)數(shù)
if (findItem.length == 0) return this.$message.error('請選擇要刪除的簽章');
// 3. 判斷選中簽章的個(gè)數(shù)是否大于1
if (findItem.length > 1) {
this.removeActive();
return this.$message.error('只能選擇刪除一個(gè)簽章,請重新選擇');
}
// 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坐標(biāo)', top: 'Y坐標(biāo)' }];
this.getSignatureJson();
},
// 提交數(shù)據(jù)
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>到此這篇關(guān)于vue2 之 實(shí)現(xiàn)pdf電子簽章的文章就介紹到這了,更多相關(guān)vue2 pdf電子簽章內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue使用axios時(shí)關(guān)于this的指向問題詳解
最近在學(xué)習(xí)使用vue+axios,在使用中發(fā)現(xiàn)了一個(gè)問題,下面總結(jié)分享給大家,這篇文章主要給大家介紹了關(guān)于vue使用axios時(shí)this的指向問題的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下。2017-12-12
Vue-Ant Design Vue-普通及自定義校驗(yàn)實(shí)例
這篇文章主要介紹了Vue-Ant Design Vue-普通及自定義校驗(yàn)實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10
Vue3生命周期Hooks原理與調(diào)度器Scheduler關(guān)系
這篇文章主要為大家介紹了Vue3生命周期Hooks原理與調(diào)度器Scheduler關(guān)系詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
詳解VUE-地區(qū)選擇器(V-Distpicker)組件使用心得
這篇文章主要介紹了詳解VUE-地區(qū)選擇器(V-Distpicker)組件使用心得,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05

