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:
- The page is divided into two parts: left – button area, right – signature area
- Button area: Rotate the button to visually create the effect of a mobile phone with a horizontal screen
- 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>