最近項目中需要做一個電子簽名的控件,踩坑不少。現記錄一下。
一、控件效果

這是一個之前的項目中的手寫簽名的一個控件,現在要改成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、如果項目中用的是 vue2 的版本,那麼安裝 2.0.5版本:
npm install --save [email protected]
如果項目中用的是 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 代碼如下:
<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 标簽加一個 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/ 。
---------------------------- 分割線 --------------------------
這裡記錄一下關于簽名回顯的一個踩坑記錄。
最終是參考了 https://github.com/razztyfication/vue-drawing-canvas 這裡面的代碼,才知道是通過 img 标簽回顯的。
一開始随意上網搜的時候看到 這裡面有介紹回顯的方法,是通過插件 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 屬性值進行回顯。