最近业务上需要用到移动端手写签名, 找了几个组件都不太如意, 遂自己封装一个组件
设计图如下:
本组件基于Signature Pad库, 在Signature Pad库的基础上完成上方设计图中的手写签名组件, 所以需要先引入Signature Pad npm包 --------- 点我跳转查看(Signature Pad) (opens new window)
npm install --save signature_pad
- 创建手写签名组件 components/autograph/index.vue
<template>
<div class="xhy-autograph-box">
<div class="autograph-box">
<div class="inside">
<van-icon class="icon-full" name="enlarge" color="#008cff" @click="fullScreenShow" />
<canvas class="xhy-canvas" />
<span @click="againSignature">重新签名</span>
</div>
</div>
<van-popup v-model="isShowFull" @close="closeFull" :close-on-click-overlay="false" close-on-popstate safe-area-inset-bottom closeable close-icon-position="bottom-right" position="left" :style="{ height: '100%', width: '100%' }">
<div class="popup-box">
<canvas class="xhy-canvasFull" />
<div class="panel-full">
<van-button class="btn-again" type="info" @click="againFull">重新签名</van-button>
<van-button class="btn-confirm" plain type="primary" @click="confirmFull">确认签名</van-button>
</div>
</div>
</van-popup>
</div>
</template>
<script>
import SignaturePad from 'signature_pad';
export default {
name: "xhy-autograph",
props: {
config: {
type: Object,
default: {
penColor: 'green', //笔刷颜色
minWidth: 1 //最小宽度
}
}
},
data() {
return {
isShowFull: false, // 是否横屏显示
signaturePad: null, // 存放竖屏SignaturePad对象
signaturePadFull: null, // 存放横屏SignaturePad对象
value: '',
fullValue: ''
}
},
mounted() {
this.init();
},
methods: {
init() {
if (!this.signaturePad) {
let canvas = document.querySelector('.xhy-canvas');
this.signaturePad = new SignaturePad(canvas, this.config);
this.signaturePad.onEnd = () => {
this.$emit('receive', this.signaturePad.toDataURL()); //通知父组件改变。
}
canvas.height = document.body.clientHeight / 3;
canvas.width = document.body.clientWidth - 30;
}
},
initFull() {
if (!this.signaturePadFull) {
let canvas = document.querySelector('.xhy-canvasFull');
this.signaturePadFull = new SignaturePad(canvas, this.config);
canvas.height = document.body.clientHeight * 0.82;
canvas.width = document.body.clientWidth;
}
},
closeFull() {
},
againFull() {
this.signaturePadFull.clear();
},
confirmFull() {
try {
let _flag = this.signaturePadFull.isEmpty();
this.signaturePad.clear();
if (!_flag) {
this.fullValue = this.signaturePadFull.toDataURL();
this.rotateBase64Img(this.fullValue, 270, res => {
let _option = {
width: document.body.clientWidth - 30,
height: document.body.clientHeight / 3
}
this.signaturePad.fromDataURL(res, _option);
this.$emit('receive', res); //通知父组件改变。
this.isShowFull = false;
})
}
} catch (error) {
console.log(error);
this.isShowFull = false;
}
},
fullScreenShow() {
this.isShowFull = true;
setTimeout(() => {
this.initFull();
let _flag = this.signaturePad.isEmpty();
this.signaturePadFull.clear();
if (!_flag) {
this.value = this.signaturePad.toDataURL();
this.rotateBase64Img(this.value, 90, res => {
let _option = {
width: document.body.clientWidth,
height: document.body.clientHeight * 0.82
}
this.signaturePadFull.fromDataURL(res, _option);
})
}
}, 100)
},
againSignature() {
this.signaturePad.clear();
this.$emit('receive', ''); //通知父组件改变。
},
// base64图片旋转方法
rotateBase64Img(src, edg, callback) {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var imgW; //图片宽度
var imgH; //图片高度
var size; //canvas初始大小
if (edg % 90 != 0) {
console.error("旋转角度必须是90的倍数!");
throw '旋转角度必须是90的倍数!';
}
(edg < 0) && (edg = (edg % 360) + 360)
const quadrant = (edg / 90) % 4; //旋转象限
const cutCoor = {
sx: 0,
sy: 0,
ex: 0,
ey: 0
}; //裁剪坐标
var image = new Image();
image.crossOrigin = "anonymous"
image.src = src;
image.onload = function () {
imgW = image.width;
imgH = image.height;
size = imgW > imgH ? imgW : imgH;
canvas.width = size * 2;
canvas.height = size * 2;
switch (quadrant) {
case 0:
cutCoor.sx = size;
cutCoor.sy = size;
cutCoor.ex = size + imgW;
cutCoor.ey = size + imgH;
break;
case 1:
cutCoor.sx = size - imgH;
cutCoor.sy = size;
cutCoor.ex = size;
cutCoor.ey = size + imgW;
break;
case 2:
cutCoor.sx = size - imgW;
cutCoor.sy = size - imgH;
cutCoor.ex = size;
cutCoor.ey = size;
break;
case 3:
cutCoor.sx = size;
cutCoor.sy = size - imgW;
cutCoor.ex = size + imgH;
cutCoor.ey = size + imgW;
break;
}
ctx.translate(size, size);
ctx.rotate(edg * Math.PI / 180);
ctx.drawImage(image, 0, 0);
var imgData = ctx.getImageData(cutCoor.sx, cutCoor.sy, cutCoor.ex, cutCoor.ey);
if (quadrant % 2 == 0) {
canvas.width = imgW;
canvas.height = imgH;
} else {
canvas.width = imgH;
canvas.height = imgW;
}
ctx.putImageData(imgData, 0, 0);
callback(canvas.toDataURL())
};
}
}
}
</script>
<style lang="scss" scoped>
.xhy-autograph-box {
@include wh(100%, auto);
.autograph-box {
padding: 15px;
width: 100%;
height: auto;
background-color: #fff;
.inside {
display: flex;
position: relative;
@include wh(100%, 100%);
background-color: rgb(242, 242, 242);
.icon-full {
position: absolute;
right: 0;
top: 0;
padding: 6px;
}
.xhy-canvas {}
span {
position: absolute;
right: 0;
bottom: 0;
@include sc(12px, #008cff); // font-size color
padding: 6px;
}
}
}
.popup-box {
@include wh(100%, 100%);
background-color: rgb(242, 242, 242);
.xhy-canvasFull {
@include wh(100%, 82%);
border-bottom: 1px solid #ccc;
}
.panel-full {
@include wh(100%, 18%);
position: relative;
/deep/ .van-button {
position: absolute;
transform: rotate(90deg);
}
.btn-again {
bottom: 40px;
left: 66px;
}
.btn-confirm {
bottom: 40px;
left: 0px;
}
}
}
}
</style>
- 引入组件使用
// template
<XhyAutograph :config="options" @receive="receiveQmValue"></XhyAutograph>
// script
import XhyAutograph from "@/components/autograph";
components: {
XhyAutograph
},
data() {
return {
options: {
penColor: 'black', //笔刷颜色
minWidth: 2 //最小宽度
},
qmValue: ''
}
},
methods: {
receiveQmValue(val) {
this.qmValue = val;
console.log(this.qmValue);
}
}
最后receiveQmValue方法返回的是图片的base64
到这就结束了, 如有不懂的问题可以咨询.