• 移动端签名组件封装 借用插件 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' })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93

    组件封装

    @/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>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168

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

    
    <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>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172

    组件全局注册

    main.js

    import vueEsign from 'vue-esign' // 需要 npm 包下载 npm install vue-esign
    Vue.use(vueEsign)
    
    import SignCanvas from '@/components/SignCanvas'
    Vue.component('SignCanvas', SignCanvas)
    // ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    组件使用

    <!-- 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>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    效果展示

    在这里插入图片描述

  • 相关阅读:
    mysql主从复制
    导航【mysql高级】【java提高】
    《嵌入式 – GD32开发实战指南》第13章 DAC
    【AI应用】Jetson Xavier NX的详情参数
    vue - 解决vue : 无法加载文件 C:\Users\hp\AppData\Roaming\npm\vue.ps1,因为在此系统上禁
    Keepalived+Nginx+Tomcat配置高可用负载均衡系统示例
    【无标题】
    MySQL允许root远程登录
    海量数据处理
    华为云云耀云服务器L实例评测| ultralytics最先进模型YOLOv8深度学习AI训练
  • 原文地址:https://blog.csdn.net/m0_53562074/article/details/133887559