利用Canvas在Vue中封装一个电子写字板的组件,通常用于电子签名之类的需求,如下图动画效果所示:
电子写字板组件效果

一、封装ETablet电子写字板组件

新建组件文件components/ETablet.vue

<template>
  <div class="e-tablet">
    <div class="sig_canvas_container">
      <canvas id="signCanvas"></canvas>
      <span class="clear_btn" @click="handelClearEl">清除</span>
    </div>
  </div>
</template>
<script>
export default {
  name: 'ETablet',
  props: {
    height: { // 画布高度
      type: String,
      default: ''
    }
    // 注:这里不接收宽度的参数,因为组件中默认宽度100%,若想控制写字板的宽度,只需在父组件中引用此组件时,外面包一层div,通过设置div的宽度来限制写字板的宽度即可。
  },
  data() {
    return {
      hasDraw: false // 判断写字板上是否有内容
    }
  },
  methods: {
  	// 绘制画布并绑定事件
    initCanvas() {
      // 初始化绘制画布
      let rate = 2
      let oCanvas = document.getElementById('signCanvas')
      oCanvas.width = oCanvas.offsetWidth * rate
      oCanvas.height = oCanvas.offsetHeight * rate
      let cxt = oCanvas.getContext('2d')
      cxt.fillStyle = '#fff' // 背景颜色
      cxt.fillRect(0, 0, oCanvas.width, oCanvas.height)
      cxt.lineWidth = 2 * rate // 画笔线宽
      cxt.strokeStyle = '#101010' // 画笔颜色
      let posX = 0
      let posY = 0
      let parentPosintin = oCanvas.getBoundingClientRect()
	  // 监听touch事件
      oCanvas.addEventListener('touchstart', function(event) {
        posX = event.changedTouches[0].clientX
        posY = event.changedTouches[0].clientY - parentPosintin.top + 0.5
        cxt.beginPath()
        cxt.moveTo(posX * rate, posY * rate)
      })
      oCanvas.addEventListener('touchmove', function(event) {
      	this.hasDraw = true
        optimizedMove(event)
      })
      let requestAnimationFrame = window.requestAnimationFrame
      let optimizedMove = requestAnimationFrame
        ? function(e) {
            requestAnimationFrame(function() {
              move(e)
            })
          }
        : move
      function move(event) {
        posX = event.changedTouches[0].clientX + 0.5
        posY = event.changedTouches[0].clientY - parentPosintin.top + 0.5
        cxt.lineTo(posX * rate, posY * rate)
        cxt.stroke()
      }
    },
    // 清除画布
    handelClearEl() {
      let oCanvas = document.getElementById('signCanvas')
      let cxt = oCanvas.getContext('2d')
      cxt.clearRect(0, 0, oCanvas.width, oCanvas.height)
      this.hasDraw = false
    }
  },
  mounted() {
    if (this.height) {
      document.querySelector('.e-tablet .sig_canvas_container').style.height = this.height + 'px'
    }
    let vm = this
    this.$nextTick(() => {
      setTimeout(() => {
        vm.initCanvas()
      }, 100)
    })
    // 在画板上绘画时,阻止浏览器默认下拉行为
    document.querySelector('body').addEventListener(
      'touchmove',
      function(e) {
        if (e.target.id === 'signCanvas') {
          e.preventDefault()
        }
      },
      {passive: false}
    )
  }
}
</script>

<style lang="scss" scoped>
.e-tablet {
  .sig_canvas_container {
    position: relative;
    width: 100vw;
    height: 50vh;
    overflow: hidden;
    #signCanvas {
      width: 100%;
      height: 100%;
      background: #ffffff;
      border: none;
      box-sizing: border-box;
      overflow: hidden;
    }
    .clear_btn {
      position: absolute;
      right: 20px;
      bottom: 15px;
    }
  }
}
</style>

二、父组件调用

在需要使用写字板的页面中引用组件ETablet

<template>
  <section class="ETablet">
    <e-tablet refs="ETablet" height="500" />
  </section>
</template>
export default {
  components: {
    ETablet: () => import('components/ETablet')
  },
  data() {
    return {}
  }
}

这样就在你的页面中呈现出了写字板啦,可以画画,并且一键清除内容。

三、业务拓展

到第二步为止,你也只能在页面上画画而已,但从业务上来讲,我们最终要的是画完之后生成图片链接。因此,我们要在组件中封装一个生成图片并返回图片链接的方法:

// components/ETablet.vue
methods: {
	getSigImage() {
      let oCanvas = document.getElementById('signCanvas')
      let imgBase64 = oCanvas.toDataURL() // 将当前绘画结果的画布生成base64格式的图片
      return imgBase64
    }
}

在父组件中通过$refs获取子组件元素并调用此方法

// 父组件中
methods: {
	getPic() {
		cnsole.log(this.$refs.ETablet.getSigImage())
	}
}

但这里拿到的是长长的一串base64图片链接,既不好看,也不符合我们的业务需求,所以这里我们要把这张base64的图片转化成file格式并通过业务接口上传到服务器,最后拿到一个正常的图片地址,如下图链接
签名图链接
【图片链接】https://m.ipipa.cn/web/2020/10/24/22/hljo6gmhctnt6piqc1jozofc/thumb/v1603549434623.jpeg
在此之前,我们要封装一个将base64转化成文件流格式的函数dataURLtoFile:

/**
 * @base64转成文件类型
 * @param {*} dataurl  base64地址
 * @param {*} filename 转换后的文件名,默认为 'm+当前时间戳'
 */
export function dataURLtoFile(dataurl, filename = 'm' + +new Date()) {
  let arr = dataurl.split(',')
  let mime = arr[0].match(/:(.*?);/)[1]
  let bstr = atob(arr[1])
  let n = bstr.length
  let u8arr = new Uint8Array(n)
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n)
  }
  filename = `${filename}.${mime.split('/')[1]}`
  return new File([u8arr], filename, { type: mime })
}

然后,我们补充刚才组件中的getSigImage方法:

// components/ETablet.vue
import {dataURLtoFile} from '@/utils/change' // 引入转化方法
methods: {
	async getSigImage() {
      let oCanvas = document.getElementById('signCanvas')
      let imgBase64 = oCanvas.toDataURL() // 将当前绘画结果的画布生成base64格式的图片
      let file = dataURLtoFile(imgBase64)
      const fd = new FormData()
      fd.append('file', file)
      const {data} = await this.$http.post('/commons/file/upload', fd) // this.$http:是我的项目对axios请求的二次封装并全局引入了;'/commons/file/upload' 为项目中的上传文件接口,这两个需要各位根据自己的项目进行修改
      let url = null
      if (data.code === 200) url = data.file.url // 这里的参数接收也要根据自己接口的参数返回进行相应修改
      return url
    }
}

至此,在父组件中调用getSigImage后就会得到一个正常的图片地址。

最后,再提两点注意事项:
1、从业务严谨性来讲,我们在调用getSigImage获取图片地址之前,应该先判断用户有没有在写字板上绘画,如果没有任何内容,我们获取到的是一张空白的图片。组件中,我们声明了hasDraw变量,并用它来记录了当前画板的绘画状态。因此,我们在获取getSigImage之前,要先通过this.$refs.ETablet.hasDraw来判断当前写字板是否有内容。
2、需要注意的是,组件中的getSigImage()方法里调用了接口,而接口的调用是异步的,是需要时间的,如果你在父组件中试图直接调用获取图片地址,是拿到不的,如下错误用法示例

// 父组件中
methods: {
	getPic() {
		let url = this.$refs.ETablet.getSigImage()
		console.log(url) // 此时url打印出来的必定是undefined
	}
}

正确的用法应该是要异步调用,等getSigImage中的接口调用成功后,我们再执行后面的操作,最简单的异步处理方法就是利用ES7的async/await方法,正确用法如下:

// 父组件中
methods: {
	async getPic() {
		let url = await this.$refs.ETablet.getSigImage()
		console.log(url) // 此时url打印出来的就是正常的图片地址啦
	}
}
Logo

前往低代码交流专区

更多推荐