2022.9.6
一、需求
最近领导提了一个新需求:仿照e签宝,实现pdf电子签章!
最终实现效果图
这是做出来的效果图,当然还有很多待修改
二、思路
然后我就去看了下人家e签宝的操作界面,左侧是印章,右侧是pdf,然后拖拽印章到pdf上面,点击保存,下次打开时显示印章的位置。
思路:我首先想到了拖拽、pdf预览、坐标;分工明确,前端来实现拖拽,pdf预览及把印章信息和坐标传给后端,后端只需要把信息和坐标保存下来就可以了。
三、使用插件
之前实现pdf预览就是通过window.open,打开一个窗口,显示pdf,功能很多,但是和需求不符,需要做的事是把pdf显示出来,同时可以可以拖拽印章到上面去
,也不要放大与缩小及其他的功能。百度下了,说是用pdfjs-dist
,这个pdf插件可以自定义很多的功能,但是实际用起来,发现好坑。。最后去百度了下,vue实现pdf电子签章, 看有没有现成的,然后还真找到了一个。js处理pdf展示、分页和签章等功能,下载到本地(只许查看index.htm
l文件即可)后发现大佬用的不是vue-cli
脚手架,是引用的cdn链接,然后就cv到项目里面了,跟着步骤,安装了pdfjs-dist
插件(pdf插件)和fabric
插件(专门处理印章的插件)这两个插件,但是项目本地运行后,报错了。。
四、遇到的问题
1.TypeError: Cannot read properties of undefined( reading 'Globalworkeroptions ')
百思不得其解啊,照着步骤来的啊,为啥呢,然后又回去看了下大佬的代码,发现他的pdf.js不是用的pdfjs-dist
,而是引入的pdf的cdn链接
然后我就在项目的public/index.html下面引入这个链接
pdf路径则是使用的一个在线的pdf链接,https://www.gjtool.cn/pdfh5/git.pdf
,发现可以打开了(样式做了些修改)
2.Dev Tools failed to load source map: Could not load content for https //mozilla github.ia/pdf js/build/pdf js map: Load canceled due to loadtimeout
开始觉得似乎已经大功告成了,到时候和后端商量下返回数据的格式就完事了的,谁知道还是有问题的。。多次打开关闭pdf后,有时候pdf会不加载出来了。人麻了,然后看了下提示,加载超时了,取消加载。
明显是cdn链接的问题,那就把pdf.js文件下载到本地呗,本地加载快,应该不会出现加载超时的问题,结果还是有问题。
唉,真的是服了,使用cdn链接吧,会加载超时,下载到本地引用吧,又会报这么个莫名其妙的问题,然后今天浏览博客时,发现一个兄弟碰到了一样的问题,哈哈,发现还是引入pdf方式的问题
/* 引用cdn链接,可以使用但会加载超时 */
// let pdfjsLib = window["pdfjs-dist/build/pdf"];
// pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://mozilla.github.io/pdf.js/build/pdf.worker.js';
/* 下载到本地,看着官方文档引用,报个莫名其妙的错 */
// import pdfjsLib from 'pdfjs-dist';
// pdfjsLib.GlobalWorkerOptions.workerSrc='pdfjs-dist/build/pdf.worker.js';
/* 下载到本地,照着大佬的方式引用,完美! */
let pdfjsLib =require("pdfjs-dist/legacy/build/pdf.js");
import workerSrc from "pdfjs-dist/legacy/build/pdf.worker.entry";
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
这是大佬的博客链接pdf.js 使用攻略及错误集合
不过这项目的电子签章有些与众不同,用户打开的pdf,是可以自定义的,即用户打开弹窗,在tinymac编辑器里面输入内容,然后切换tab,会立即生成一个pdf,接下来才是用户使用电子签章的过程
以下是电子签章的主要代码,和大佬的index.html的代码差不多,就是做了点修改(ps:目前印章的位置和坐标保存,使用的得本地缓存,便于调试,后期会保存到接口里面!)
代码部分
首先 引入pdfjs-dist插件和fabric插件
npm install pdfjs-dist
npm i fabric --save
html部分
<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">跳转</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>
js部分
import {fabric} from 'fabric';
let pdfjsLib =require("pdfjs-dist/legacy/build/pdf.js");
import workerSrc from "pdfjs-dist/legacy/build/pdf.worker.entry";
pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
import draggable from "vuedraggable";
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: {},
}
},
computed: {
hasSigna() {
return this.canvasEle && this.canvasEle.getObjects()[0] ? true : false;
},
},
created(){
var that = this;
that.mainImagelist = [require('./sign.png'),require('./seal.png')];
that.taskInfo = {'title':'测试盖章', uname:'张三', endtime:'2021-09-01 17:59:59'};
},
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 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,到时还会盖章信息,在渲染时,同时显示出来,不应该在切换页码时才显示印章信息
showpdf(pdfUrl) {
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);
}
}
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 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')); //获取缓存字符串后转换为对象
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 = 'https://www.gjtool.cn/pdfh5/git.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";
}
},
},
pageNum: function() {
this.commonSign(this.pageNum);
this.queueRenderPage(this.pageNum);
}
}
},
css部分
<style scoped>
/*pdf部分*/
.pCenter{
overflow-x: hidden;
}
#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("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;
}
2022.9.13修改
使用的时候,发现在pdf第一页添加的印章,下次再打开时,不在显示,本地缓存的也是显示{},所以琢磨了下,应该是每次打开pdf页面重置了,代码做了以下修改
将选中部分改为以下代码