您现在的位置是:首页 > 技术教程 正文

Vue2,实现电子签名(web、移动端)功能

admin 阅读: 2024-04-02
后台-插件-广告管理-内容页头部广告(手机)

Vue2,实现电子签名(web、移动端)功能

一、简述

现如今,电子签名与手写签名一样具有法律效应。越来越多的项目开发中会有电子签名的需求,自己最近的项目也会频繁出现该需求。一般开发时会用到现有的npm依赖包vue-signature-pad,但是自己所处的开发环境不能连接外网,所以打算自己研究和总结实现电子签名功能。

实现电子签名功能,需要用到html5中一个重要级别的辅助标签——canvas。

二、canvas介绍

什么是canvas?

HTML5的用于图形的绘制。它只是一个图形容器,不提供任何绘制对象的信息。画布的内容并不像html那样具有语义并能暴露出来。

它的图形绘制,通常是使用javascript来完成的,可以通过多种方法来绘制路径、盒、圆、字符以及添加图像等。

如何实现canvas

  1. 创建canvas元素
  2. 获取canvas元素
  3. 创建context对象

Vue2(2.6.11)

<template> <div class="ml_sign"> <canvas ref="signature" id="signature">canvas> div> template> <script> export default { name: 'Signatrue', data () { return { ctx: null } }, mounted () { // 获取canvas实例 const canvas = this.$refs.signature // 创建context对象 this.ctx = canvas.getContext('2d') } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

canvas给我们提供了很多的Api,供我们使用。

getContext('2d')对象是内建的HTML5对象,拥有多种绘制路径、矩形、圆形、字符、以及添加图像的方法。

在这里需要先添加两个按钮,分别是取消和保存,后续会用到。

<canvas ref="signature" id="signature">canvas> <div class="btn-wrapper"> <button>取消button> <button>保存button> div>
  • 1
  • 2
  • 3
  • 4
  • 5

三、签名实现

实现步骤:

  1. 配置基础内容
  2. 获取canvas实例
  3. 基础内容设置
  4. 设备兼容 - 绑定事件
  5. 开始绘制
  6. 绘制
  7. 结束绘制
  8. 取消功能/清空画布
  9. 保存功能 - 图片显示、本地下载、上传后端存储
1.配置基础内容
  • 定义宽、高、线条颜色、线条宽度等基础内容;
<script> export default { ..., data () { return { canvas: null, // 存储canvas节点 ctx: null, // 存储canvas的context上下文 config: { width: 400, // 宽度 height: 200, // 高度 strokeStyle: 'red', // 线条颜色 lineWidth: 4, // 线条宽度 lineCap: 'round', // 设置线条两端圆角 lineJoin: 'round' // 线条交汇处圆角 }, client: { offsetX: 0, // 偏移量 offsetY: 0, endX: 0, // 坐标 endY: 0 }, points: [] // 记录坐标 用来判断是否有签名的 } } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
2.获取canvas实例
<script> export default { ..., // 注意:习惯使用created生命周期的童鞋,将无法获取到canvas节点。 mounted () { // 初始化 this.init() }, methods: { // 初始化 init () { const canvas = this.$refs.signature // 存储canvas节点 this.canvas = canvas // 创建context对象 this.ctx = canvas.getContext('2d') } } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
3.基础内容设置
  • 设置canvas的宽、高等基础配置;
  • 注意:这里需要注意的是,canvas的默认宽高是 width: 300 height: 150,若是style设置width和height,可能会出现拉伸问题。所以尽量使用canvas内置属性设置width和height,不然会有bug。
<script> export default { ..., methods: { // 初始化 init () { const canvas = this.$refs.signature canvas.width = this.config.width // 设置canvas的宽 canvas.height = this.config.height // 设置canvas的高 // 设置一个边框 canvas.style.border = '1px solid #000' // 存储canvas节点 this.canvas = canvas // 创建context对象 this.ctx = canvas.getContext('2d') // 设置相应配置 this.ctx.fillStyle = 'transparent' this.ctx.lineWidth = this.config.lineWidth this.ctx.strokeStyle = this.config.strokeStyle this.ctx.lineCap = this.config.lineCap this.ctx.lineJoin = this.config.lineJoin // 绘制填充矩形 this.ctx.fillRect( 0, // x 轴起始绘制位置 0, // y 轴起始绘制位置 this.config.width, // 宽度 this.config.height // 高度 ) } } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
4.设备兼容 - 绑定事件
  • 定义计算属性,判断是否为移动端;
  • 监听canvas 鼠标/手势按下 和 鼠标/手势 弹起/离开 事件;
<script> export default { ..., computed: { // 判断是否为移动端 mobileStatus () { return (/Mobile|Android|iPhone/i.test(navigator.userAgent)) } }, methods: { // 初始化 init () { ... // 创建鼠标/手势按下监听器 canvas.addEventListener(this.mobileStatus ? 'touchstart' : 'mousedown', this.startDraw) // 创建鼠标/手势 弹起/离开 监听器 canvas.addEventListener(this.mobileStatus ? 'touchend' : 'mouseup', this.cloaseDraw) } } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
5.开始绘制
  • 鼠标/手势按下后,获取偏移量及坐标并存储;
  • 清除以上一次 beginPath 之后的所有路径,进行绘制;
  • moveTo设置画线起始点位;
  • 监听 鼠标移动或手势移动;
<script> export default { ..., methods: { // 初始化 init () { ... }, // 开始绘制 startDraw (event) { // 获取偏移量及坐标 const { offsetX, offsetY, pageX, pageY } = this.mobileStatus ? event.changedTouches[0] : event // 修改上次的偏移量及坐标 this.client.offsetX = offsetX this.client.offsetY = offsetY this.client.endX = pageX this.client.endY = pageY // 清除以上一次 beginPath 之后的所有路径,进行绘制 this.ctx.beginPath() // 设置画线起始点位 this.ctx.moveTo(this.client.endX, this.client.endY) // 监听 鼠标移动或手势移动 this.canvas.addEventListener(this.mobileStatus ? 'touchmove' : 'mousemove', this.draw) } } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
6.绘制
  • 获取当前坐标点位;
  • lineTo根据坐标点位移动添加线条;
  • stroke绘制;
  • 记录坐标;
<script> export default { ..., methods: { // 初始化 init () { ... }, // 开始绘制 startDraw () { ... }, // 绘制 draw (event) { // 获取当前坐标点位 const { pageX, pageY } = this.mobileStatus ? event.changedTouches[0] : event // 修改最后一次绘制的坐标点 this.client.endX = pageX this.client.endY = pageY const obj = { x: pageX, y: pageY } // 根据坐标点位移动添加线条 this.ctx.lineTo(pageX, pageY) // 绘制 this.ctx.stroke() // 记录坐标 this.points.push(obj) } } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
7.结束绘制
  • closePath结束绘制;
  • 移除 鼠标移动或手势移动 监听器;
<script> export default { ..., methods: { // 初始化 init () { ... }, // 开始绘制 startDraw () { ... }, // 绘制 draw () { ... }, // 结束绘制 cloaseDraw () { // 结束绘制 this.ctx.closePath() // 移除鼠标移动或手势移动监听器 this.canvas.removeEventListener('mousemove', this.draw) } } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
8.取消功能/清空画布
  • 绑定 取消功能/清空画布 事件;
  • 清空当前画布上的所有绘制内容;
  • 清空坐标;
<div class="btn-wrapper"> <button @click="clear">取消button> <button>保存button> div>
  • 1
  • 2
  • 3
  • 4
  • 5
<script> export default { ..., methods: { // 初始化 init () { ... }, // 开始绘制 startDraw () { ... }, // 绘制 draw () { ... }, // 结束绘制 cloaseDraw () { ... }, // 取消/清空画布 clear () { // 清空当前画布上的所有绘制内容 this.ctx.clearRect(0, 0, this.config.width, this.config.height) // 清空坐标 this.points = [] } } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
9.保存功能 - 图片显示、本地下载、上传后端存储

保存功能做了三个项目业务场景常用的方法,供大家参考或使用

图片显示:将签名转成base64,并放在img路径上,进行签名的图片展示;

本地下载:将签名转成blob流,并下载至本地(默认png格式图片);

上传后端存储:将签名转成base64,然后将base64转成File文件对象,再上传后端;

基础保存设置
  • 绑定保存事件;
  • 签名判空;
  • 操作事件;
<div class="btn-wrapper"> <button @click="clear">取消button> <button @click="save">保存button> div>
  • 1
  • 2
  • 3
  • 4
  • 5
<script> export default { ..., methods: { // 初始化 init () { ... }, // 开始绘制 startDraw () { ... }, // 绘制 draw () { ... }, // 结束绘制 cloaseDraw () { ... }, // 取消/清空画布 clear () { ... }, // 保存 save () { // 判断至少有20个坐标 才算有签名 if (this.points.length < 20) { alert('签名不能为空!') return } // 操作事件 ... } } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
显示图片
  • 创建img标签,并绑定路径;
  • 定义操作事件;
  • 将canvas内容转成base64,并赋值img绑定路径;
<template> <div class="ml_sign"> ... <img :src="imgurl"> div> template> <script> export default { ..., data () { ..., imgurl: '' // img图片路径 }, methods: { ..., // 保存 save () { // 判断至少有20个坐标 才算有签名 if (this.points.length < 20) { alert('签名不能为空!') return } // 操作事件 this.dataToImg() }, // img显示签名 dataToImg () { // 转成base64 const baseFile = this.canvas.toDataURL() // 默认转成png格式的图片编码 this.imgurl = baseFile } } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

在这里插入图片描述

本地下载
  • 定义操作事件;
  • 将canvas内容转成blob流;
  • 通过a标签进行下载;
<script> export default { ..., methods: { ..., // 保存 save () { // 判断至少有20个坐标 才算有签名 if (this.points.length < 20) { alert('签名不能为空!') return } // 操作事件 this.dataUrlToPng() }, // 将签名生成png图片 dataUrlToPng () { // 将canvas内容转成blob流 this.canvas.toBlob(blob => { // 获取当前时间并转成字符串,用来当做文件名 const date = Date.now().toString() // 创建一个 a 标签 const a = document.createElement('a') // 设置 a 标签的下载文件名 a.download = `${date}.png` // 设置 a 标签的跳转路径为 文件流地址 a.href = URL.createObjectURL(blob) // 手动触发 a 标签的点击事件 a.click() // 移除 a 标签 a.remove() }) } } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

在这里插入图片描述

上传后端存储
  • 将canvas内容转成base64,自定义文件名;
  • 将base64转成File文件对象;
  • 上传签名;
<script> export default { ..., methods: { ..., // 保存 save () { // 判断至少有20个坐标 才算有签名 if (this.points.length < 20) { alert('签名不能为空!') return } // 操作事件 const baseFile = this.canvas.toDataURL() // 转成base64,默认转成png格式的图片编码 const filename = `${Date.now()}.png` // 文件名字 const file = this.dataURLToFile(baseFile, filename) // 图片文件形式 传给后端存储即可 this.uploadSignatrue(file) }, // 将base64转成File文件对象 dataURLToFile (dataURL, filename) { const arr = dataURL.split(',') // 获取图片格式 const imgType = arr[0].match(/:(.*?);/)[1] // atob() 方法用于解码使用 base-64 编码的字符串 const dec = atob(arr[1]) let n = dec.length const u8arr = new Uint8Array(n) while (n--) { // 转成ASCII码 u8arr[n] = dec.charCodeAt(n) } return new File([u8arr], filename, { type: imgType }) }, // 上传签名 uploadSignatrue (file) { const formData = new FormData() formData.append('file', file) formData.append('paramsOne', paramsOne) ... // 上传接口 这里就不赘述了 uploadFile(formData, ...) }, } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

四、完整代码

<template> <div class="ml_sign"> <canvas ref="signature" id="signature">canvas> <div class="btn-wrapper"> <button @click="clear">取消button> <button @click="save">保存button> div> <img :src="imgurl"> div> template> <script> export default { name: 'Signatrue', data () { return { canvas: null, // 存储canvas节点 ctx: null, // 存储canvas的context上下文 config: { width: 400, // 宽度 height: 200, // 高度 strokeStyle: 'red', // 线条颜色 lineWidth: 4, // 线条宽度 lineCap: 'round', // 设置线条两端圆角 lineJoin: 'round' // 线条交汇处圆角 }, points: [], // 记录坐标 用来判断是否有签名的 client: { offsetX: 0, // 偏移量 offsetY: 0, endX: 0, // 坐标 endY: 0 }, imgurl: '' } }, computed: { // 判断是否为移动端 mobileStatus () { return (/Mobile|Android|iPhone/i.test(navigator.userAgent)) } }, mounted () { this.init() }, methods: { // 初始化 init () { const canvas = this.$refs.signature canvas.width = this.config.width // 设置canvas的宽 canvas.height = this.config.height // 设置canvas的高 // 设置一个边框 canvas.style.border = '1px solid #000' // 存储canvas节点 this.canvas = canvas // 创建context对象 this.ctx = canvas.getContext('2d') // 设置相应配置 this.ctx.fillStyle = 'transparent' this.ctx.lineWidth = this.config.lineWidth this.ctx.strokeStyle = this.config.strokeStyle this.ctx.lineCap = this.config.lineCap this.ctx.lineJoin = this.config.lineJoin // 绘制填充矩形 this.ctx.fillRect( 0, // x 轴起始绘制位置 0, // y 轴起始绘制位置 this.config.width, // 宽度 this.config.height // 高度 ) // 创建鼠标/手势按下监听器 canvas.addEventListener(this.mobileStatus ? 'touchstart' : 'mousedown', this.startDraw) // 创建鼠标/手势 弹起/离开 监听器 canvas.addEventListener(this.mobileStatus ? 'touchend' : 'mouseup', this.cloaseDraw) }, // 开始绘制 startDraw (event) { // 获取偏移量及坐标 const { offsetX, offsetY, pageX, pageY } = this.mobileStatus ? event.changedTouches[0] : event // 修改上次的偏移量及坐标 this.client.offsetX = offsetX this.client.offsetY = offsetY this.client.endX = pageX this.client.endY = pageY // 清除以上一次 beginPath 之后的所有路径,进行绘制 this.ctx.beginPath() // 设置画线起始点位 this.ctx.moveTo(this.client.endX, this.client.endY) // 监听 鼠标移动或手势移动 this.canvas.addEventListener(this.mobileStatus ? 'touchmove' : 'mousemove', this.draw) }, // 绘制 draw (event) { // 获取当前坐标点位 const { pageX, pageY } = this.mobileStatus ? event.changedTouches[0] : event // 修改最后一次绘制的坐标点 this.client.endX = pageX this.client.endY = pageY const obj = { x: pageX, y: pageY } // 根据坐标点位移动添加线条 this.ctx.lineTo(pageX, pageY) // 绘制 this.ctx.stroke() // 记录坐标 this.points.push(obj) }, // 结束绘制 cloaseDraw () { // 结束绘制 this.ctx.closePath() // 移除鼠标移动或手势移动监听器 this.canvas.removeEventListener('mousemove', this.draw) }, // 取消/清空画布 clear () { // 清空当前画布上的所有绘制内容 this.ctx.clearRect(0, 0, this.config.width, this.config.height) // 清空坐标 this.points = [] }, // 保存 save () { // 判断至少有20个坐标 才算有签名 if (this.points.length < 20) { alert('签名不能为空!') return } // 操作事件 const baseFile = this.canvas.toDataURL() // 转成base64,默认转成png格式的图片编码 const filename = `${Date.now()}.png` // 文件名字 const file = this.dataURLToFile(baseFile, filename) // 图片文件形式 传给后端存储即可 this.uploadSignatrue(file) // this.dataUrlToPng() // this.dataToImg() }, // img显示签名 dataToImg () { // 转成base64 const baseFile = this.canvas.toDataURL() // 默认转成png格式的图片编码 this.imgurl = baseFile }, // 将签名生成png图片 dataUrlToPng () { // 将canvas上的内容转成blob流 this.canvas.toBlob(blob => { // 获取当前时间并转成字符串,用来当做文件名 const date = Date.now().toString() // 创建一个 a 标签 const a = document.createElement('a') // 设置 a 标签的下载文件名 a.download = `${date}.png` // 设置 a 标签的跳转路径为 文件流地址 a.href = URL.createObjectURL(blob) // 手动触发 a 标签的点击事件 a.click() // 移除 a 标签 a.remove() }) }, // 将base64转成File文件对象 dataURLToFile (dataURL, filename) { const arr = dataURL.split(',') // 获取图片格式 const imgType = arr[0].match(/:(.*?);/)[1] // atob() 方法用于解码使用 base-64 编码的字符串 const dec = atob(arr[1]) let n = dec.length const u8arr = new Uint8Array(n) while (n--) { // 转成ASCII码 u8arr[n] = dec.charCodeAt(n) } return new File([u8arr], filename, { type: imgType }) }, // 上传签名 uploadSignatrue (file) { const formData = new FormData() formData.append('file', file) // formData.append('paramsOne', paramsOne) // ... console.log(formData) // 上传接口 这里就不赘述了 // uploadFile(params, ...) } } } script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
标签:
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

在线投稿:投稿 站长QQ:1888636

后台-插件-广告管理-内容页尾部广告(手机)
关注我们

扫一扫关注我们,了解最新精彩内容

搜索