移动端签名组件封装 借用插件 vue-esign

需求

移动端需要实现手机横屏手写签名并上传签名图片功能。

实现讲解

vue-esign 插件文档地址 https://www.npmjs.com/package/vue-esign
SignCanvas 组件封装原理:

  1. 页面分为左右两部分:左-按钮区域,右-签名区域
  2. 按钮区域:将按钮进行旋转,视觉上制造手机横屏的效果
  3. 签名区域:由于是横屏签名,所以在签名结束提交签名时需要将签名图片进行逆时针90°旋转

工具 - 图片旋转、base64 转换为 file 对象

@/utils/index

/**
 * 图片旋转
 */
export function rotateBase64Img(src, edg, fileName, fileType, callback) {
    
    
  var canvas = document.createElement('canvas')
  var ctx = canvas.getContext('2d')

  var imgW // 图片宽度
  var imgH // 图片高度
  var size // canvas初始大小

  if (edg % 90 !== 0) {
    
    
    console.error('旋转角度必须是90的倍数!')
    return '旋转角度必须是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 = () => {
    
    
    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)
    ctx.drawImage(image, 0, 0)

    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
    }

    ctx.putImageData(imgData, 0, 0)
    callback(dataURLtoFile(canvas.toDataURL(), fileName, fileType))
    // callback(canvas.toDataURL())
  }
}
/**
 * 将 base64 转换为 file 对象
 *    dataURL:base64 格式
 *    fileName:文件名
 *    fileType:文件格式
 */
export function dataURLtoFile(dataURL, fileName, fileType) {
    
    
  const arr = dataURL.split(',')
  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: fileType || 'image/jpg' })
}

组件封装

@/components/SignCanvas.vue

<!-- 签名组件 -->
<template>
  <div class="signContainer">
    <div class="btns">
      <van-button type="default" round @click="resetHandler" class="reset">重签</van-button>
      <van-button type="info" round @click="sureHandler">确认</van-button>
    </div>
    <vue-esign
      ref="VueEsignRef"
      class="vue-esign"
      :width="width"
      :height="height"
      :lineWidth="lineWidth"
      :lineColor="lineColor"
      :bgColor="bgColor"
      :isCrop="isCrop"
      :isClearBgColor="isClearBgColor"
      :format="format"
      :quality="quality"
    />
    <div :style="{ '--width': height + 'px' }" class="tipText"><span v-if="signName">{
    
    {
    
     ` ${
      
      signName} ` }}</span
      >在此区域内签名
    </div>
  </div>
</template>

<script>
import {
    
     rotateBase64Img } from '@/utils/index'

export default {
    
    
  name: 'SignCanvas',
  components: {
    
    },

  props: {
    
    
    // 画布宽度,即导出图片的宽度
    width: {
    
    
      type: Number,
      default: () => {
    
    
        const dom = document.querySelector('#app')
        const width = dom && dom.offsetWidth
        return width ? width - 60 : 300 // 减去按钮区域的宽度
      }
    },
    // 画布高度,即导出图片的高度
    height: {
    
    
      type: Number,
      default: () => {
    
    
        const dom = document.querySelector('#app')
        return (dom && dom.offsetHeight) || 800
      }
    },
    // 画笔粗细
    lineWidth: {
    
    
      type: Number,
      default: 6
    },
    // 画笔颜色
    lineColor: {
    
    
      type: String,
      default: '#000'
    },
    // 画布背景色,为空时画布背景透明,支持多种格式 '#ccc','#E5A1A1','rgb(229, 161, 161)','rgba(0,0,0,.6)','red'
    bgColor: {
    
    
      type: String,
      default: ''
    },
    // 是否裁剪,在画布设定尺寸基础上裁掉四周空白部分
    isCrop: {
    
    
      type: Boolean,
      default: false
    },
    // 清空画布时(reset)是否同时清空设置的背景色(bgColor)
    isClearBgColor: {
    
    
      type: Boolean,
      default: true
    },
    // 生成图片格式 image/jpeg(jpg格式下生成的图片透明背景会变黑色请慎用或指定背景色)、 image/webp
    format: {
    
    
      type: String,
      default: 'image/png'
    },
    // 生成图片质量;在指定图片格式为 image/jpeg 或 image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。
    quality: {
    
    
      type: Number,
      default: 1
    },
    // 未签名时提示信息
    noSignTipText: {
    
    
      type: String,
      default: '请确保已签名!'
    },
    // 需要签名的姓名
    signName: {
    
    
      type: String,
      default: ''
    }
  },

  methods: {
    
    
    resetHandler() {
    
    
      this.$refs.VueEsignRef.reset() // 清空画布
    },
    sureHandler() {
    
    
      // 可选配置参数 ,在未设置format或quality属性时可在生成图片时配置 例如: {format:'image/jpeg', quality: 0.5}
      // this.$refs.esign.generate({format:'image/jpeg', quality: 0.5})
      this.$refs.VueEsignRef.generate()
        .then(res => {
    
    
          /**
           * res:base64图片
           */
          rotateBase64Img(res, 270, `${
      
      this.signName ? this.signName + '-签名.jpg' : 'sign.jpg'}`, '', data => {
    
    
            this.$emit('sureHandler', data)
          })
        })
        .catch(err => {
    
    
          console.log('err----', err)
          this.$dialog.alert({
    
    
            message: this.noSignTipText
          })
        })
    }
  }
}
</script>

<style lang='scss' scoped>
.signContainer {
    
    
  width: 100%;
  height: 100vh;
  display: flex;
  background-color: #fff;

  .btns {
    
    
    width: 55px;
    background-color: #f8f8f8;
    display: flex;
    flex-direction: column;
    justify-content: center;
    .reset {
    
    
      margin-bottom: 70px;
    }
  }
  .vue-esign {
    
    
    z-index: 2;
  }
  .tipText {
    
    
    position: absolute;
    top: 50%;
    width: var(--width);
    left: calc(50% + 55px);
    transform: translateX(-50%) translateY(-50%) rotateZ(90deg);
    text-align: center;
    color: #ddd;
    letter-spacing: 2px;
  }
}
::v-deep .van-button {
    
    
  width: 85px !important;
  height: 35px;
  transform: rotate(90deg) translateY(15px);
  text-align: center;
  .van-button__text {
    
    
    letter-spacing: 5px;
  }
}
</style>

组件优化 - 重签和清空画布分离

<!-- 签名组件 -->
<template>
  <div class="signContainer">
    <div class="btns">
      <van-button type="default" round @click="clearHandler" class="reset">重签</van-button>
      <van-button type="info" round @click="sureHandler">确认</van-button>
    </div>
    <vue-esign
      ref="VueEsignRef"
      class="vue-esign"
      :width="width"
      :height="height"
      :lineWidth="lineWidth"
      :lineColor="lineColor"
      :bgColor="bgColor"
      :isCrop="isCrop"
      :isClearBgColor="isClearBgColor"
      :format="format"
      :quality="quality"
    />
    <div :style="{ '--width': height + 'px' }" class="tipText"><span v-if="signName">{
   
   { ` ${signName} ` }}</span
      >在此区域内签名
    </div>
  </div>
</template>

<script>
import {
      
       rotateBase64Img } from '@/utils/index'

export default {
      
      
  name: 'SignCanvas',
  components: {
      
      },

  props: {
      
      
    // 画布宽度,即导出图片的宽度
    width: {
      
      
      type: Number,
      default: () => {
      
      
        const dom = document.querySelector('#app')
        const width = dom && dom.offsetWidth
        return width ? width - 60 : 300 // 减去按钮区域的宽度
      }
    },
    // 画布高度,即导出图片的高度
    height: {
      
      
      type: Number,
      default: () => {
      
      
        const dom = document.querySelector('#app')
        return (dom && dom.offsetHeight) || 800
      }
    },
    // 画笔粗细
    lineWidth: {
      
      
      type: Number,
      default: 6
    },
    // 画笔颜色
    lineColor: {
      
      
      type: String,
      default: '#000'
    },
    // 画布背景色,为空时画布背景透明,支持多种格式 '#ccc','#E5A1A1','rgb(229, 161, 161)','rgba(0,0,0,.6)','red'
    bgColor: {
      
      
      type: String,
      default: ''
    },
    // 是否裁剪,在画布设定尺寸基础上裁掉四周空白部分
    isCrop: {
      
      
      type: Boolean,
      default: false
    },
    // 清空画布时(reset)是否同时清空设置的背景色(bgColor)
    isClearBgColor: {
      
      
      type: Boolean,
      default: true
    },
    // 生成图片格式 image/jpeg(jpg格式下生成的图片透明背景会变黑色请慎用或指定背景色)、 image/webp
    format: {
      
      
      type: String,
      default: 'image/png'
    },
    // 生成图片质量;在指定图片格式为 image/jpeg 或 image/webp的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。
    quality: {
      
      
      type: Number,
      default: 1
    },
    // 未签名时提示信息
    noSignTipText: {
      
      
      type: String,
      default: '请确保已签名!'
    },
    // 需要签名的姓名
    signName: {
      
      
      type: String,
      default: ''
    }
  },

  methods: {
      
      
    resetHandler() {
      
      
      this.$refs.VueEsignRef.reset() // 清空画布
    },
    clearHandler() {
      
      
      this.$emit('clearHandler')
      this.resetHandler()
    },
    sureHandler() {
      
      
      // 可选配置参数 ,在未设置format或quality属性时可在生成图片时配置 例如: {format:'image/jpeg', quality: 0.5}
      // this.$refs.esign.generate({format:'image/jpeg', quality: 0.5})
      this.$refs.VueEsignRef.generate()
        .then(res => {
      
      
          /**
           * res:base64图片
           */
          rotateBase64Img(res, 270, `${ 
        this.signName ? this.signName + '-签名.jpg' : 'sign.jpg'}`, '', data => {
      
      
            this.$emit('sureHandler', data)
          })
        })
        .catch(err => {
      
      
          console.log('err----', err)
          this.$dialog.alert({
      
      
            message: this.noSignTipText
          })
        })
    }
  }
}
</script>

<style lang='scss' scoped>
.signContainer {
      
      
  width: 100%;
  height: 100vh;
  display: flex;
  background-color: #fff;

  .btns {
      
      
    width: 55px;
    background-color: #f8f8f8;
    display: flex;
    flex-direction: column;
    justify-content: center;
    .reset {
      
      
      margin-bottom: 70px;
    }
  }
  .vue-esign {
      
      
    z-index: 2;
  }
  .tipText {
      
      
    position: absolute;
    top: 50%;
    width: var(--width);
    left: calc(50% + 55px);
    transform: translateX(-50%) translateY(-50%) rotateZ(90deg);
    text-align: center;
    color: #ddd;
    letter-spacing: 2px;
  }
}
::v-deep .van-button {
      
      
  width: 85px !important;
  height: 35px;
  transform: rotate(90deg) translateY(15px);
  text-align: center;
  .van-button__text {
      
      
    letter-spacing: 5px;
  }
}
</style>

组件全局注册

main.js

import vueEsign from 'vue-esign' // 需要 npm 包下载 npm install vue-esign
Vue.use(vueEsign)

import SignCanvas from '@/components/SignCanvas'
Vue.component('SignCanvas', SignCanvas)
// ...

组件使用

<!-- XXXX签名 -->
<template>
  <SignCanvas ref="SignCanvasRef" :signName="nameList[nameIndex]" @sureHandler="sureSignHandler" />
</template>

<script>
export default {
    
    
  name: 'BloodRegisterSign',
  components: {
    
    },

  data() {
    
    
    return {
    
    
      // ...
      inputData: {
    
    }, // 该数据中 cxmjView 为需要签名的人员姓名
      nameIndex: 0, // 当前签名为第几个人签名
      signFileList: [] // 签名图片列表
    }
  },

  computed: {
    
    
    nameList() {
    
    
      return this.inputData.cxmjView ? this.inputData.cxmjView.split(',') : [] // 需要有多个签名
    }
  },

  watch: {
    
    },

  created() {
    
    
    console.log('this.$route----', this.$route)
    this.inputData = JSON.parse(this.$route.query.inputData || '{}')
	// ...
  },

  methods: {
    
    
    sureSignHandler(data) {
    
    
      this.signFileList.push(data)
      if (this.nameIndex < this.nameList.length - 1) {
    
    
        this.nameIndex++
        this.$refs.SignCanvasRef.resetHandler()
      } else {
    
    
        this.submitHandler()
      }
    },
    submitHandler() {
    
    
      // TODO:调用接口,提交签名图片等数据
    }
  }
}
</script>

<style lang='scss' scoped>
</style>

效果展示

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_53562074/article/details/133887559
今日推荐