最近项目中需要做一个电子签名的控件,踩坑不少。现记录一下。

一、控件效果

 这是一个之前的项目中的手写签名的一个控件,现在要改成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 属性值进行回显。