最近项目中需要做一个电子签名的控件,踩坑不少。现记录一下。
一、控件效果
这是一个之前的项目中的手写签名的一个控件,现在要改成vue版本的。
大概功能就是点击页面中的标题文字“手写签批”,有一个弹框,里面可以手写签名,底部是功能操作,包括撤销、清屏、橡皮擦功能、调节笔刷的粗细、保存等。
二、插件选择
之前因为没有接触过手写签名的功能,所以就去上 github 上面搜相关的代码,然后把代码下载下来,一个个的安装 node_modules 文件夹,查看代码效果。但是插件要不就是缺少撤销功能,要不就是没有调节画笔的粗细功能。最后筛选出来的一些主要的代码网址如下:
1、https://github.com/neighborhood999/vue-signature-pad // 带撤销效果,保存功能为在控制台打印出图片信息,可调节画笔粗细 2、https://github.com/razztyfication/vue-drawing-canvas // 锁定,撤销,清除,线条颜色、粗细,背景色
最后经过筛选,确定用 vue-signature-pad 插件进行开发。网址2用的是 vue-drawing-canvas 插件,放出来这个网址主要是参考了里面的手写签名的回显功能。
github 上对于插件 vue-signature-pad 的介绍和 npm 官网上(https://www.npmjs.com/package/vue-signature-pad)的一模一样,参考 github 和 npm 官网都可以。
三、开发过程记录
1、安装 vue-signature-pad
如果项目中用的是 vue2 的版本,那么安装 2.0.5版本:
npm install --save vue-signature-pad@2.0.5
如果项目中用的是 vue3 版本,那么安装最新的版本即可:
npm install --save vue-signature-pad
2、 main.js 中引用 vue-signature-pad
import Vue from 'vue'import VueSignaturePad from "vue-signature-pad"; Vue.use(VueSignaturePad);
3、完整的 vue 代码如下
项目中用的是 elementui 和 vxe-table 组件,这里不多赘述,和插件 vue-signature-pad 没关系。
<template> <div class="handwritten-name-wrap"> <el-button plain @click="handleClick"> 手写签名 </el-button> <div class="img-wrap"> <img :src="imgSrc" alt="" v-if="imgSrc"> </div> <vxe-modal v-model="panelVisible" title="手写签名" width="600" height="400" size="large" :destroy-on-close="true" class="signNameModel" > <template v-slot> <div class="signWrap"> <VueSignaturePad width="100%" height="100%" ref="signaturePad" :options="options" /> <footer> <div class="gtnGroup"> <el-button type="primary" size="mini" @click="undo">撤销</el-button> <el-button type="primary" size="mini" style="margin-left:20px" @click="clear">清屏</el-button> <el-button type="primary" size="mini" style="margin-left:20px" @click="save">保存</el-button> </div> <div class="otherSet"> <div class="penTxt">笔刷大小:</div> <div class="circleWrap" :class="{ active: isActive1 }" @click="selSize(1)"><b class="b1"></b></div> <div class="circleWrap" :class="{ active: isActive2 }" @click="selSize(2)"><b class="b2"></b></div> <div class="circleWrap" :class="{ active: isActive3 }" @click="selSize(3)"><b class="b3"></b></div> </div> </footer> </div> </template> </vxe-modal> </div></template><script> export default { data(){ return { panelVisible:false, panelTitle:"", options: { penColor: "#000", minWidth: 1, //控制画笔最小宽度 maxWidth: 1, //控制画笔最大宽度 }, isActive1:true, isActive2:false, isActive3:false, imgSrc:"", } }, methods: { //手写签名按钮的点击 handleClick(){ this.panelVisible=true; this.isActive1=true; this.isActive2=false; this.isActive3=false; this.options = { penColor: "#000", minWidth: 1, maxWidth: 1, } }, //撤销 undo(){ this.$refs.signaturePad.undoSignature(); }, //清除 clear(){ this.$refs.signaturePad.clearSignature(); }, //保存 save(){ console.log( this.$refs.signaturePad.saveSignature() ); const { isEmpty, data } = this.$refs.signaturePad.saveSignature(); this.imgSrc = data; this.panelVisible = false; }, //调节画笔粗细大小 selSize(val){ this.options = { penColor: "#000", minWidth: val, maxWidth: val, }; if(val==1){ this.isActive1=true; this.isActive2=false; this.isActive3=false; }else if(val==2){ this.isActive1=false; this.isActive2=true; this.isActive3=false; }else if(val==3){ this.isActive1=false; this.isActive2=false; this.isActive3=true; } } }, }</script><style lang="scss">.handwritten-name-wrap{ .img-wrap{ width:100%; height:164px; margin-top:2px; border:1px solid #ccc; img{ width:70%; height:100%; } } .signWrap{ height:100%; display:flex; flex-direction:column; justify-content:center; .signName{ flex:1; border-top:1px solid #ccc; } footer{ height:40px; border-top:1px solid #ccc; display:flex; justify-content: space-between; align-items: center; .gtnGroup{ width:50%; margin-left: 20px; } .otherSet{ width:50%; display:flex; align-items: center; .penTxt{ width:70px; } .selSize{ width:70px; } .el-select__caret{ position: absolute; right: -3px; } .b1,.b2,.b3{ display: inline-block; background: #000; border-radius: 50%; } .circleWrap{ display: flex; justify-content: center; align-items: center; width:18px; height:18px; cursor:pointer; margin-right:20px; } .active{ border:1px dashed #0074d9; } .b1{width:4px;height:4px} .b2{width:6px;height:6px} .b3{width:8px;height:8px} } } } } .signNameModel{ .vxe-modal--content{ padding:0 !important; }}</style>
最后的效果:
代码注解:
① 签名之后,点击保存,关闭弹框,然后回显签名的图片。注意回显的时候用的是 img 标签,img 标签的 src 属性值为 保存方法里面的 data 数据,data 是64位的图片信息。之前不知道 img 标签的 src 属性还可以这样设置。
img 标签加一个 v-if 的判断条件,不然没有图片的时候,会显示一个加载失败的图片,如下图:
② 调节画笔的粗细是根据 options 属性里面的 minWidth 和 maxWidth 来设置的。因为 vue-signature-pad 插件会根据用户签名时候操作鼠标的速度不同,画笔的线条粗线不一样,所以我设置的时候,把 minWidth 和 maxWidth 都设置为同样的值,来保证画笔的粗细一致。
这里注意调节画笔粗细的时候,在方法 selSize 中不能直接设置 this.options.minWidth 的值,要对整体的 this.options 设置才能起效果(此处参考了文章: vue-signature-pad在vue中实现电子签名效果 )。
后语:
项目到这里基本就结束了,目前还没有橡皮擦的功能,暂时没有添加,个人感觉橡皮擦功能用处不大。后续如果添加了,会续更文章(可参考 vue手写签名组件_Vue签名板组件)。
vue-signature-pad 插件中的 options 属性在 github 和 npm 官网中都没有详细的解释,关于 options 中的详细属性可参考 https://vuejsexamples.com/vue-signature-pad-component/ 。
此插件还有两个内置的方法比较有用,就是 “锁定目标签名板” 和“打开目标签名板”,其实就是禁用签名和启用签名。项目中有些审批是需要走流程的,到某个节点的时候,某个人可能没有没有权限签名,只能查看签名,这时候这两个方法就排上用场了。以此篇博文为例,
具体用应用方法如下:
this.$refs.signaturePad.lockSignaturePad(); //锁定目标签名板this.$refs.signaturePad.openSignaturePad(); //打开目标签名板
回来更新橡皮擦功能:
橡皮擦功能其实很简单,只需要把 options 选项里面的 penColor 值改为 #fff 即可,上面的记录的代码也懒得改了。放上截图一张:
只是添加了两个按钮:
<el-button type="primary" size="mini" style="margin-left:15px" @click="pencil">笔刷</el-button><el-button type="primary" size="mini" style="margin-left:15px" @click="eraser">橡皮擦</el-button>
pencil(){ this.options = { penColor: "#000", minWidth: this.pencilSize, maxWidth: this.pencilSize, }; }, eraser(){ this.options = { penColor: "#fff", minWidth: 5, //这里可以赋值 this.pencilSize maxWidth: 5, }; }, selSize(val){ this.pencilSize = val; }
点击橡皮擦按钮的时候,画笔的粗细我就直接写死了。既然是擦除的功能,画笔粗细稍微粗一点好,这个看个人项目需求吧。如果赋值 this.pencilSize 的话,就是和画笔粗细大小一样了。
大概功能就是这样,至于具体的样式,有需要的可以根据自己的项目需求更改。
—————————- 分割线(记录另一个踩坑记录) ————————–
这里记录一下关于签名回显的一个踩坑记录。
最终是参考了 https://github.com/razztyfication/vue-drawing-canvas 这里面的代码,才知道是通过 img 标签回显的。
一开始随意上网搜的时候看到 https://segmentfault.com/q/1010000014974220 ,这里面有介绍回显的方法,是通过插件 vue-signature-pad 的 fromData 方法回显的。
但是这样就造就了一个问题:
我们在弹框里面手写签名的地方一般都比较大,回显的区域比较小,所以就造成了回显的时候,签名有往下偏离,造成显示不全的情况。如下图:
这种方法的实现过程的代码如下:
1、定义回显的标签:
<VueSignaturePad width="100%" height="100%" ref="signaturePadShow"/>
2、data 中定义 signData ,用来存储插件内置方法 toData() 返回的数据。
data(){ return { signData:null } },
3、js 有关代码处理
save(){ const { isEmpty, data } = this.$refs.signaturePad.saveSignature(); console.log(isEmpty); console.log(data); this.panelVisible = false; this.signData=this.$refs.signaturePad.toData(); console.log(this.signData) this.$refs.signaturePadShow.fromData(this.signData); },
通过在控制台打印 this.signData 可以看出来端倪:
通过控制台的数据,可以看出来最主要的是 x、y轴的坐标值。因为插件底层是通过 canvas 绘制的,我们签名的时候,canvas 画布比较大,回显的时候画布比较小,而我们的 this.signData 数据是签名的时候的数据,所以就造成了回显的时候图片往下偏离的情况。
解决办法就是等比例的缩小 this.signData 数据中的 x 和 y 的值。
例如我们签名的时候,签名区域的 canvas 元素的高度为 300px,回显区域的 canvas 元素的高度为 150px,那么我们就应该把 this.signData 数据中的 x、y值等比例的缩小,缩小的比例为 (300-150)/300 = 50% ,即缩减比例为 0.5,所以代码更改为:
save(){ const { isEmpty, data } = this.$refs.signaturePad.saveSignature(); console.log(isEmpty); console.log(data); this.panelVisible = false; this.signData=this.$refs.signaturePad.toData(); console.log(this.signData) if(this.signData && this.signData.length>0){ this.signData.map(val=>{ val.points.map(list=>{ list.x = list.x*0.5 list.y = list.y*0.5 }) }) } this.$refs.signaturePadShow.fromData(this.signData); },
至此,签名的回显就没有问题了。
但是不建议用插件 vue-signature-pad 中的内置方法 fromData() 回显我们的签名,因为我们终究要把签名的图片数据存储到后端,回显的时候通过接口获取签名的图片相关数据进行回显。所以我们最后保存的时候给后端传递的数据为 saveSignature() 方法中返回的 data 属性值,
data为通过 base64 编码的图片信息,获取的时候也是获取相关的图片信息,然后通过设置 img 标签的 src 属性值进行回显。