Mobile signature component encapsulation borrowing plug-in vue-esign

Directory

  • need
  • Implementation explanation
  • Tools – Image rotation, base64 conversion to file object
  • Component encapsulation
  • Component global registration
  • Component usage
    • Show results

Requirements

The mobile terminal needs to realize the function of handwriting signature and uploading signature image in horizontal screen of the mobile phone.

Implementation explanation

vue-esign Plug-in document address https://www.npmjs.com/package/vue-esign
SignCanvas component encapsulation principle:

  1. The page is divided into two parts: left – button area, right – signature area
  2. Button area: Rotate the button to visually create the effect of a mobile phone with a horizontal screen
  3. Signature area: Since it is a horizontal signature, the signature image needs to be rotated 90° counterclockwise when submitting the signature.

Tools – Image rotation, base64 conversion to file object

@/utils/index

/**
 * Image rotation
 */
export function rotateBase64Img(src, edg, fileName, fileType, callback) {<!-- -->
  var canvas = document.createElement('canvas')
  var ctx = canvas.getContext('2d')

  var imgW // image width
  var imgH // Image height
  var size //canvas initial size

  if (edg % 90 !== 0) {<!-- -->
    console.error('The rotation angle must be a multiple of 90!')
    return 'The rotation angle must be a multiple of 90!'
  }
  edg < 0 & amp; & amp; (edg = (edg % 360) + 360)
  const quadrant = (edg / 90) % 4 // rotate quadrant
  const cutCoor = {<!-- --> sx: 0, sy: 0, ex: 0, ey: 0 } // cutting coordinates

  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())
  }
}
/**
 * Convert base64 to file object
 * dataURL: base64 format
 * fileName: file name
 * fileType: file format
 */
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' })
}

Component packaging

@/components/SignCanvas.vue

<!-- Signature component -->
<template>
  <div class="signContainer">
    <div class="btns">
      <van-button type="default" round @click="resetHandler" class="reset">Resign</van-button>
      <van-button type="info" round @click="sureHandler">Confirm</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">
      Please <span v-if="signName">{<!-- -->{<!-- --> ` ${<!-- -->signName} ` }}</span
      >Sign within this area
    </div>
  </div>
</template>

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

export default {<!-- -->
  name: 'SignCanvas',
  components: {<!-- -->},

  props: {<!-- -->
    //Canvas width, that is, the width of the exported image
    width: {<!-- -->
      type: Number,
      default: () => {<!-- -->
        const dom = document.querySelector('#app')
        const width = dom & amp; & amp; dom.offsetWidth
        return width ? width - 60 : 300 // Subtract the width of the button area
      }
    },
    //Canvas height, that is, the height of the exported image
    height: {<!-- -->
      type: Number,
      default: () => {<!-- -->
        const dom = document.querySelector('#app')
        return (dom & amp; & dom.offsetHeight) || 800
      }
    },
    //Brush thickness
    lineWidth: {<!-- -->
      type: Number,
      default: 6
    },
    //Brush color
    lineColor: {<!-- -->
      type: String,
      default: '#000'
    },
    // Canvas background color. When empty, the canvas background is transparent. It supports multiple formats '#ccc', '#E5A1A1', 'rgb(229, 161, 161)', 'rgba(0, 0,0,.6)','red'
    bgColor: {<!-- -->
      type: String,
      default: ''
    },
    // Whether to crop, based on the set size of the canvas, crop the surrounding blank area
    isCrop: {<!-- -->
      type: Boolean,
      default: false
    },
    // When clearing the canvas (reset), whether to clear the set background color (bgColor) at the same time?
    isClearBgColor: {<!-- -->
      type: Boolean,
      default: true
    },
    // Generate image format image/jpeg (the transparent background of the image generated in jpg format will turn black, please use it with caution or specify the background color), image/webp
    format: {<!-- -->
      type: String,
      default: 'image/png'
    },
    // Generate image quality; when the specified image format is image/jpeg or image/webp, you can select the image quality from 0 to 1. If the value is outside the range, the default value of 0.92 will be used. Other parameters are ignored.
    quality: {<!-- -->
      type: Number,
      default: 1
    },
    // Prompt message when not signed
    noSignTipText: {<!-- -->
      type: String,
      default: 'Please make sure it is signed! '
    },
    // Name that needs to be signed
    signName: {<!-- -->
      type: String,
      default: ''
    }
  },

  methods: {<!-- -->
    resetHandler() {<!-- -->
      this.$refs.VueEsignRef.reset() // Clear the canvas
    },
    sureHandler() {<!-- -->
      // Optional configuration parameters, which can be configured when generating images when the format or quality attributes are not set. For example: {format:'image/jpeg', quality: 0.5}
      // this.$refs.esign.generate({format:'image/jpeg', quality: 0.5})
      this.$refs.VueEsignRef.generate()
        .then(res => {<!-- -->
          /**
           * res: base64 picture
           */
          rotateBase64Img(res, 270, `${<!-- -->this.signName ? this.signName + '-signature.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>

Component optimization - re-sign and clear canvas separation

<!-- Signature component -->
<template>
  <div class="signContainer">
    <div class="btns">
      <van-button type="default" round @click="clearHandler" class="reset">Resign</van-button>
      <van-button type="info" round @click="sureHandler">Confirm</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">
      Please <span v-if="signName">{<!-- -->{ ` ${signName} ` }}</span
      >Sign within this area
    </div>
  </div>
</template>

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

export default {<!-- -->
  name: 'SignCanvas',
  components: {<!-- -->},

  props: {<!-- -->
    //Canvas width, that is, the width of the exported image
    width: {<!-- -->
      type: Number,
      default: () => {<!-- -->
        const dom = document.querySelector('#app')
        const width = dom & amp; & amp; dom.offsetWidth
        return width ? width - 60 : 300 // Subtract the width of the button area
      }
    },
    //Canvas height, that is, the height of the exported image
    height: {<!-- -->
      type: Number,
      default: () => {<!-- -->
        const dom = document.querySelector('#app')
        return (dom & amp; & dom.offsetHeight) || 800
      }
    },
    //Brush thickness
    lineWidth: {<!-- -->
      type: Number,
      default: 6
    },
    //Brush color
    lineColor: {<!-- -->
      type: String,
      default: '#000'
    },
    // Canvas background color. When empty, the canvas background is transparent. It supports multiple formats '#ccc', '#E5A1A1', 'rgb(229, 161, 161)', 'rgba(0, 0,0,.6)','red'
    bgColor: {<!-- -->
      type: String,
      default: ''
    },
    // Whether to crop, based on the set size of the canvas, crop the surrounding blank area
    isCrop: {<!-- -->
      type: Boolean,
      default: false
    },
    // When clearing the canvas (reset), whether to clear the set background color (bgColor) at the same time?
    isClearBgColor: {<!-- -->
      type: Boolean,
      default: true
    },
    // Generate image format image/jpeg (the transparent background of the image generated in jpg format will turn black, please use it with caution or specify the background color), image/webp
    format: {<!-- -->
      type: String,
      default: 'image/png'
    },
    // Generate image quality; when the specified image format is image/jpeg or image/webp, you can select the image quality from 0 to 1. If the value is outside the range, the default value of 0.92 will be used. Other parameters are ignored.
    quality: {<!-- -->
      type: Number,
      default: 1
    },
    // Prompt message when not signed
    noSignTipText: {<!-- -->
      type: String,
      default: 'Please make sure it is signed! '
    },
    // Name that needs to be signed
    signName: {<!-- -->
      type: String,
      default: ''
    }
  },

  methods: {<!-- -->
    resetHandler() {<!-- -->
      this.$refs.VueEsignRef.reset() // Clear the canvas
    },
    clearHandler() {<!-- -->
      this.$emit('clearHandler')
      this.resetHandler()
    },
    sureHandler() {<!-- -->
      // Optional configuration parameters, which can be configured when generating images when the format or quality attributes are not set. For example: {format:'image/jpeg', quality: 0.5}
      // this.$refs.esign.generate({format:'image/jpeg', quality: 0.5})
      this.$refs.VueEsignRef.generate()
        .then(res => {<!-- -->
          /**
           * res: base64 picture
           */
          rotateBase64Img(res, 270, `${<!-- -->this.signName ? this.signName + '-signature.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>

Global registration of components

main.js

import vueEsign from 'vue-esign' // Requires npm package download npm install vue-esign
Vue.use(vueEsign)

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

Component usage

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

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

  data() {<!-- -->
    return {<!-- -->
      // ...
      inputData: {<!-- -->}, // cxmjView in this data is the name of the person who needs to sign
      nameIndex: 0, // Which person is the current signature?
      signFileList: [] // Signature image list
    }
  },

  computed: {<!-- -->
    nameList() {<!-- -->
      return this.inputData.cxmjView ? this.inputData.cxmjView.split(',') : [] // Multiple signatures are required
    }
  },

  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: Call the interface and submit signature pictures and other data
    }
  }
}
</script>

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

Effect display