H5 电子签名组件

今天学习一下如何手打一个电子签名组件。

效果图

思路

  • 使用 canvas 创建一个画布,绘制路径
  • 然后把 canvas 输出为一张 base64图片

实现

1. 创建一个 canvas

SignCanvas.vue

html 复制代码
<script setup>
const size = reactive({
  width: 0,
  height: 0
})
const canvasBox = useTemplateRef('canvasBox')
const canvasEl = useTemplateRef('canvasEl')
onMounted(() => {
  size.width = canvasBox.value.clientWidth
  size.height = canvasBox.value.clientHeight
  ...
})
</script>
<template>
    <div class="sign-canvas" ref="canvasBox">
        <canvas ref="canvasEl" :width="size.width" :height="size.height"></canvas>
    </div>
</template>

2. 定义一个画笔方法。

js 复制代码
function brush(beginX, beginY, stopX, stopY) {
  context.beginPath() // 开启一条新路径
  context.lineWidth = 3 // 设置线宽
  // context.globalAlpha = 1 // 设置图片的透明度
  // context.strokeStyle = 'black' // 设置路径颜色
  // context.linePressure = 1.2 // 设置笔触压力
  // context.smoothness = 30 // 设置笔触大小变化的平滑度
  context.moveTo(beginX, beginY) // 从(beginX, beginY)这个坐标点开始画图
  context.lineTo(stopX, stopY) // 定义从(beginX, beginY)到(stopX, stopY)的线条(该方法不会创建线条)
  context.closePath() // 结束该条路径 必须!!
  context.stroke() // 实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径。默认颜色是黑色。
}

3. 监听画板画笔事件

js 复制代码
function init() {
  let beginX, beginY
  // context = canvasEl.value.getContext('2d')
  canvasEl.value.addEventListener('touchstart', function (event) {
    event.preventDefault() // 阻止在canvas画布上签名的时候页面跟着滚动
    beginX = event.touches[0].clientX - this.offsetLeft
    beginY = event.touches[0].pageY - this.offsetTop
  })
  canvasEl.value.addEventListener('touchmove', (event) => {
    event.preventDefault() // 阻止在canvas画布上签名的时候页面跟着滚动
    event = event.touches[0]
    let stopX = event.clientX - canvasEl.value.offsetLeft
    let stopY = event.pageY - canvasEl.value.offsetTop
    brush(beginX, beginY, stopX, stopY)
    beginX = stopX // 这一步很关键,需要不断更新起点,否则画出来的是射线簇
    beginY = stopY
  })
}

4. 重置方法

js 复制代码
function resetHandle() {
  context.clearRect(0, 0, size.width, size.height) // 清除区域内所有痕迹
}

5. 非空校验

js 复制代码
function isEmpty() {
  const blank = document.createElement('canvas') // 获取一个空 canvas 对象
  blank.width = size.width
  blank.height = size.height
  return canvasEl.value.toDataURL() == blank.toDataURL() // 比较值相等则为空
}
  1. 文字旋转(记录)
js 复制代码
function rotateText() {
  context.save() // 关键!!!  保持原始状态
  context.translate(size.width / 2, size.height / 2) // 设置旋转中心点为正中心
  context.rotate(Math.PI / 2) // 逆时针旋转 90°
  context.font = 'bold 48px MiSans'
  context.textAlign = 'center'
  context.textBaseline = 'middle'
  context.fillStyle = 'rgba(0, 0, 0, 0.1)'
  context.fillText('请在此区域签字', 0, 0)
  context.restore() // 恢复到原始状态
}
  1. base64 转 file对象(记录)
js 复制代码
function base64ToFile(base64String, fileName) {
  const arr = base64String.split(',') // 解析data URL
  const mime = arr[0].match(/:(.*?);/)[1]
  const bstr = atob(arr[1])
  let n = bstr.length
  const u8arr = new Uint8Array(n)
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n)
  }
  return new File([u8arr], fileName, { type: mime })
}

源码

html 复制代码
<script setup>
import { base64ToFile } from '@/utils/jsUtils'
import { service } from '@/request/request'
const router = useRouter()
const size = reactive({
  width: 0,
  height: 0
})
const canvasBox = useTemplateRef('canvasBox')
const canvasEl = useTemplateRef('canvasEl')
onMounted(() => {
  size.width = canvasBox.value.clientWidth
  size.height = canvasBox.value.clientHeight
  init()
})
onBeforeUnmount(() => {
  canvasEl.value.removeEventListener('touchstart', touchstart)
  canvasEl.value.removeEventListener('touchmove', touchmove)
})

let context = null
let brushAttr = { beginX: 0, beginY: 0 }
// 监听事件
function init() {
  context = canvasEl.value.getContext('2d')
  canvasEl.value.addEventListener('touchstart', touchstart)
  canvasEl.value.addEventListener('touchmove', touchmove)
}
function touchstart(event) {
  event.preventDefault() // 阻止在canvas画布上签名的时候页面跟着滚动
  brushAttr.beginX = event.touches[0].clientX - this.offsetLeft
  brushAttr.beginY = event.touches[0].pageY - this.offsetTop
}
function touchmove(event) {
  event.preventDefault() // 阻止在canvas画布上签名的时候页面跟着滚动
  event = event.touches[0]
  const stopX = event.clientX - canvasEl.value.offsetLeft
  const stopY = event.pageY - canvasEl.value.offsetTop
  brush(brushAttr.beginX, brushAttr.beginY, stopX, stopY)
  brushAttr.beginX = stopX // 这一步很关键,需要不断更新起点,否则画出来的是射线簇
  brushAttr.beginY = stopY
}

// 笔刷
function brush(beginX, beginY, stopX, stopY) {
  context.beginPath() // 开启一条新路径
  context.lineWidth = 3 // 设置线宽
  // context.globalAlpha = 1 // 设置图片的透明度
  // context.strokeStyle = 'black' // 设置路径颜色
  // context.linePressure = 1.2 // 设置笔触压力
  // context.smoothness = 30 // 设置笔触大小变化的平滑度
  context.moveTo(beginX, beginY) // 从(beginX, beginY)这个坐标点开始画图
  context.lineTo(stopX, stopY) // 定义从(beginX, beginY)到(stopX, stopY)的线条(该方法不会创建线条)
  context.closePath() // 创建该条路径
  context.stroke() // 实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径。默认颜色是黑色。
}

// 旋转90°后导出图片
function printImage() {
  if (isEmpty()) return showToast('请先完成签字确认')
  const rotatedCanvas = document.createElement('canvas')
  // 设置旋转后的画布尺寸(宽高互换)
  rotatedCanvas.width = size.height
  rotatedCanvas.height = size.width
  const ctx = rotatedCanvas.getContext('2d')
  ctx.fillStyle = '#FFF' // 设置画布的填充颜色
  ctx.fillRect(0, 0, canvasEl.value.height, canvasEl.value.width) // 在画布上绘制一个填充矩形
  // 旋转并绘制内容
  ctx.translate(rotatedCanvas.width / 2, rotatedCanvas.height / 2) // 设置旋转中心点为正中心
  ctx.rotate(-Math.PI / 2) // 逆时针旋转 90°
  ctx.drawImage(canvasEl.value, -size.width / 2, -size.height / 2, size.width, size.height)
  const base64 = rotatedCanvas.toDataURL('image/png')
  console.log('base64', base64)
  return base64
}

function isEmpty() {
  const blank = document.createElement('canvas') // 系统获取一个空canvas对象
  blank.width = size.width
  blank.height = size.height
  return canvasEl.value.toDataURL() == blank.toDataURL() // 比较值相等则为空
}
// 重置
function resetHandle() {
  context.clearRect(0, 0, size.width, size.height)
}
</script>

<template>
  <div class="container">
    <van-nav-bar
      safe-area-inset-top
      fixed
      placeholder
      title="签字确认"
      left-arrow
      @click-left="router.back"
    />
    <div class="content">
      <div class="sign-canvas tip" ref="canvasBox">
        <!-- <canvas ref="canvasEl"></canvas> -->
        <canvas ref="canvasEl" :width="size.width" :height="size.height"></canvas>
      </div>
      <div class="btns">
        <div class="btn plain_blue" @click="resetHandle">重签</div>
        <div class="btn primary_blue" @click="printImage">确认</div>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.container {
  height: calc(100vh - 48px);
  background-color: #f9f9f9;
  .content {
    display: flex;
    flex-direction: column;
    height: 100%;
    padding: 16px;
    .sign-canvas {
      flex: 1;
      background-color: #fff;
      &.tip::after {
        position: absolute;
        top: 46%;
        content: '请在此区域签字';
        color: rgba($color: #000000, $alpha: 0.1);
        font-size: 48px;
        font-weight: 600;
        transform: rotate(90deg);
        pointer-events: none;
      }
    }
    .btns {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      gap: 20px;
      margin-top: 16px;
      .btn {
        display: flex;
        align-items: center;
        justify-content: center;
        height: 48px;
        border-radius: 6px;
      }
      .primary_blue {
        color: #fff;
        background-color: #165dff;
      }
      .plain_blue {
        color: #165dff;
        background-color: #e3eafa;
      }
    }
  }
}
</style>

参考

H5基于canvas实现电子签名并生成PDF文档 - 掘金

小程序和H5手写签名 - 掘金

相关推荐
就是帅我不改4 小时前
敏感词过滤黑科技!SpringBoot+Vue3+TS强强联手,打造无懈可击的内容安全防线
前端·vue.js·后端
香香甜甜的辣椒炒肉5 小时前
vue(7)-单页应用程序&路由
前端·javascript·vue.js
dreams_dream6 小时前
vue中axios与fetch比较
前端·javascript·vue.js
梦鱼6 小时前
Vue 项目图标一把梭:Iconify 自用小记(含 TS/JS 双版本组件)
前端·javascript·vue.js
给月亮点灯|6 小时前
Vue基础知识-脚手架开发-初始化目录解析
前端·javascript·vue.js
哆啦A梦15887 小时前
Element-Plus
前端·vue.js·ts
毕设源码-郭学长8 小时前
【开题答辩全过程】以 基于vue在线考试系统的设计与实现为例,包含答辩的问题和答案
前端·javascript·vue.js
詩句☾⋆᭄南笙8 小时前
初识Vue
前端·javascript·vue.js
qb9 小时前
vue3.5.18-编译-生成ast树
前端·vue.js·架构