当前位置:首页 » 《我的小黑屋》 » 正文

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

24 人参与  2024年04月14日 11:53  分类 : 《我的小黑屋》  评论

点击全文阅读


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

一、简述

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

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

二、canvas介绍

什么是canvas

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

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

如何实现canvas

创建canvas元素获取canvas元素创建context对象

Vue2(2.6.11)

<template><div class="ml_sign">        <!-- 创建canvas元素(标签) -->        <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>

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

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

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

<canvas ref="signature" id="signature"></canvas><div class="btn-wrapper">    <button>取消</button>    <button>保存</button></div>

三、签名实现

实现步骤:

配置基础内容获取canvas实例基础内容设置设备兼容 - 绑定事件开始绘制绘制结束绘制取消功能/清空画布保存功能 - 图片显示、本地下载、上传后端存储
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>
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>
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>
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>
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>
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>
7.结束绘制
closePath结束绘制;移除 鼠标移动或手势移动 监听器;
<script>export default {    ...,    methods: {        // 初始化        init () { ... },        // 开始绘制        startDraw () { ... },        // 绘制        draw () { ... },        // 结束绘制        cloaseDraw () {              // 结束绘制              this.ctx.closePath()              // 移除鼠标移动或手势移动监听器              this.canvas.removeEventListener('mousemove', this.draw)        }    }}</script>
8.取消功能/清空画布
绑定 取消功能/清空画布 事件;清空当前画布上的所有绘制内容;清空坐标;
<div class="btn-wrapper">    <!-- 添加点击事件 -->    <button @click="clear">取消</button>    <button>保存</button></div>
<script>export default {    ...,    methods: {        // 初始化        init () { ... },        // 开始绘制        startDraw () { ... },        // 绘制        draw () { ... },        // 结束绘制        cloaseDraw () { ... },        // 取消/清空画布        clear () {              // 清空当前画布上的所有绘制内容              this.ctx.clearRect(0, 0, this.config.width, this.config.height)              // 清空坐标              this.points = []        }    }}</script>
9.保存功能 - 图片显示、本地下载、上传后端存储

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

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

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

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

基础保存设置
绑定保存事件;签名判空;操作事件;
<div class="btn-wrapper">    <!-- 添加点击事件 -->    <button @click="clear">取消</button>    <button @click="save">保存</button></div>
<script>export default {    ...,    methods: {        // 初始化        init () { ... },        // 开始绘制        startDraw () { ... },        // 绘制        draw () { ... },        // 结束绘制        cloaseDraw () { ... },        // 取消/清空画布        clear () { ... },        // 保存        save () {              // 判断至少有20个坐标 才算有签名              if (this.points.length < 20) {                alert('签名不能为空!')                return              }              // 操作事件              ...        }    }}</script>
显示图片
创建img标签,并绑定路径;定义操作事件;将canvas内容转成base64,并赋值img绑定路径;
<template><div class="ml_sign">        ...        <!-- 创建img标签, 绑定路径 -->        <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>

在这里插入图片描述

本地下载
定义操作事件;将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>

在这里插入图片描述

上传后端存储
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>

四、完整代码

<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>

点击全文阅读


本文链接:http://zhangshiyu.com/post/95128.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1