前言:
本功能只实现了js原生实现横屏电子签名,至于其他后面有机会再发文详细讲解。
需求:
H5实现横向电子签名 附完成效果
待签字:
签字中:
未签字:
功能:
通过短信链接或者APP进入H5页面实现pdf合同的签订。
实现原理:利用原生canvas 配合 移动端的事件, touchstart, touchend, touchmove, 进行 在canvas 画布上画线, 最后,把生成的签名, 通过 toDataURL()方法 保存 为 base64 图片。
注意:对功能、流程、脑图感兴趣可以参考学习。如单纯实现该功能,可以忽略!!!
流程图:
脑图:
上代码!!!!!!!!!!!!!!
//此内容为canvas的api data() { return { radio: "#000", height: 50, direction: false, // true 代表横屏, false 代表'竖屏' -- 但是亲测没有效果 el: "", // canvas dom ctx: "", // canvas context background: "white", // canvas background-color color: "#000", // 绘制时线条的颜色 linewidth: 2, // 线条的宽度 liColors: ["#000"], drawCheck: false, //用来判断是否签字 clientHeight: document.documentElement.clientHeight, clientWidth: document.documentElement.clientWidth, loading: false, base64: "", prohibit: false, }; },
1.在窗口大小改变时重新绘制画布。
在mounted
钩子函数中,设置了窗口的resize
事件,当窗口大小改变时,重新计算客户端的高度和宽度,并调用draw
方法进行重新绘制。
mounted() {
window.onresize = () => {
this.clientHeight = document.documentElement.clientHeight;
this.clientWidth = document.documentElement.clientWidth;
this.draw();
return;
};
this.draw();
},
2.实现了在Vue中绘制线条的功能
2.1、draw
方法用于添加绘制线条的功能。首先通过$refs
获取到具有ref
属性为signHandle
的元素,该元素将用作绘制线条的画布。然后调用initCanvas
方法进行画布的初始化配置。
2.2、initCanvas
方法首先通过设置el
的宽度和高度为客户端的宽度和高度,然后获取画布的上下文ctx
。接下来调用setCanvas
方法设置画布的背景色、线条颜色、线宽和线条两头的样式。最后分别调用drawStart
、drawing
和drawEnd
方法来处理绘制的具体逻辑。
2.3、drawStart
方法添加了touchstart
事件监听器,在触摸开始时,调用ctx.beginPath()
开始绘制线条,并使用触摸点的坐标作为起始点。
2.4、drawing
方法添加了touchmove
事件监听器,在触摸移动时,调用ctx.lineTo()
绘制线条的路径,并调用ctx.stroke()
绘制线条。
2.5、drawEnd
方法添加了touchend
事件监听器,在触摸结束时,调用ctx.closePath()
结束绘制线条。
2.6、最后还提供了一个clearHandle
方法,用于清空画布并重置绘制状态。
// 添加绘制 line
draw() {
if (!this.prohibit) {
this.$refs.signHandle.addEventListener(
"touchstart",
(e) => e.preventDefault(),
{
passive: false,
}
);
this.el = this.$refs.signHandle;
console.log(this.prohibit);
this.initCanvas();
}
},
// 初始化canvas配置
initCanvas() {
const { height, direction, el } = this;
el.width = this.clientWidth;
el.height = this.clientHeight;
this.ctx = el.getContext("2d");
this.setCanvas();
this.drawStart();
this.drawing();
this.drawEnd();
},
// 配置 canvas
setCanvas() {
const { ctx, height, direction } = this;
// 设置背景色
ctx.fillStyle = this.background;
ctx.fillRect(0, 0, this.clientWidth, this.clientHeight);
// 设置线条颜色
ctx.strokeStyle = this.color;
// 设置线宽
ctx.lineWidth = this.linewidth;
// 设置线条两头的结束点和开始点是圆形的
ctx.lineCap = "round";
},
// 开始绘制
drawStart() {
const { el, ctx } = this;
el.addEventListener(
"touchstart",
(e) => {
ctx.beginPath();
ctx.moveTo(e.changedTouches[0].pageX, e.changedTouches[0].pageY);
this.drawCheck = true; //代表签过字
},
false
);
},
// 绘制中
drawing() {
const { el, ctx } = this;
el.addEventListener(
"touchmove",
(e) => {
ctx.lineTo(e.changedTouches[0].pageX, e.changedTouches[0].pageY);
ctx.stroke();
},
false
);
},
// 绘制结束
drawEnd() {
const { el, ctx } = this;
el.addEventListener("touchend", () => ctx.closePath(), false);
},
// 清空
clearHandle() {
this.initCanvas();
this.drawCheck = false; //代表签过字
},
3.提交并保存图片
onComfirm() {
if (this.drawCheck == false) {
this.prohibit = true;
showToast({
message:
'<div style="transform: rotate(90deg);height:120px; line-height:60px; margin-top: 10px; backgroundColor:black"><div style="width: 80px;padding: 38% 0 38% 0;display: flex;"><span>请</span><span>进</span><span>行</span><span>签</span><span>字</span></div></div>',
// '<div>请进行签字</div>',
type: "html",
});
return;
}
this.saveImg();
// this.adminSave()
},
// 保存信息
saveImg() {
const img = new Image();
this.loading = true;
const imgBase64 = this.el.toDataURL();
console.log('没处理前的base64',imgBase64)
this.rotateBase64Img(imgBase64, -90);
},
//签完名的图片90°旋转处理
rotateBase64Img(src, edg) {
let that = this;
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);
//drawImage向画布上绘制图片
ctx?.drawImage(image, 0, 0);
//getImageData() 复制画布上指定矩形的像素数据
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;
}
//putImageData() 将图像数据放回画布
ctx?.putImageData(imgData, 0, 0);
// callback(canvas.toDataURL("image/png"));
this.base64 = canvas.toDataURL("image/png").split(",")[1];
console.log('最终的base64',this.base64)
// return ;
};
},
4.最后template部分
<template>
<div class="signingbox">
<div class="header">
<van-nav-bar title="签名" @click-left="this.$router.go(-1)">
<template #left>
<el-icon size="20">
<ArrowLeft />
</el-icon>
</template>
</van-nav-bar>
</div>
<div class="page" ref="page">
<div class="backgrounds">
<div class="btn_container van-hairline--top">
<el-button class="button reSign" size="normal" @click="clearHandle"
>重签</el-button
>
<el-button
type="primary"
class="button button-right"
size="normal"
@click="onComfirm"
>完成签名</el-button
>
</div>
</div>
<div class="canvasBox">
<canvas ref="signHandle" class="canvas" />
</div>
</div>
<div
class="signatureBlock"
@touchstart="drawCheck = true"
v-if="!drawCheck"
>
签名区
</div>
<div v-if="loading" class="mask">
<van-loading type="spinner" />
</div>
</div>
</template>
style部分
<style lang="scss" scoped>
.mask {
position: fixed;
top: 0;
left: 0;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
background-color: rgba($color: #000000, $alpha: 0.4);
}
.header {
position: fixed;
top: 0;
z-index: 500;
width: 100%;
// height: 46px;
}
.signingbox {
position: relative;
box-sizing: border-box;
overflow: hidden;
.reSign {
color: #77adff;
border: 1px solid #77adff;
}
}
.signatureBlock {
position: absolute;
top: 47%;
left: 48%;
color: rgb(241, 241, 241);
font-weight: 700;
font-size: 24px;
transform: translate(-50%);
transform: rotate(90deg);
}
.backgrounds {
position: fixed;
width: 60px;
width: 60px;
height: 100%;
margin-top: 45px;
margin-top: 45px;
background-color: rgb(244, 246, 248);
}
.page {
width: 100%;
// height: 100%;
overflow: hidden;
background-color: rgb(244, 246, 248);
.btn_container {
position: fixed;
bottom: 147px;
left: -35%;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
width: 80%;
margin-left: 10px;
padding: 0 8px;
text-align: center;
background-color: rgb(244, 246, 248);
transform: rotate(90deg);
}
.button {
width: 50%;
height: 34px;
line-height: 34px;
border-radius: 5px;
}
.button-right {
margin-left: 4px;
color: white;
background-color: rgb(28, 111, 233);
}
}
.canvasBox {
width: 100%;
overflow: hidden;
}
.button-wrapper {
display: flex;
justify-content: space-between;
min-width: 180px;
}
</style>
总结:用于自用的笔记记录,写的可能有些乱,希望能帮到你。
市面上画布插件多个。对于简单的功能需求,可以考虑使用已有的JS插件;
而对于复杂的功能需求或更高的灵活性,可以选择使用原生JS进行开发
如果大家有什么不懂或者问题可以提交在评论区。欢迎讨论!!!!