# 使用 signature_pad 实现数字签名

# 安装

npm install --save signature_pad

# 创建组件

<template>
  <van-popup v-model="show" position="right" get-container="body" lazy-render :overlay="false" :style="{ height: '100%',width:'100%' }">
    <div class="wrap" id="signature-wrap">
      <div v-safeBottom class="button-view">
        <div v-for="(item,i) in btnList" class="btn item" @click="itemClick(i)" :class="{'close-btn':i===0}" :key="i">
          <img class="icon" :src="require(`../img/${item.icon}.png`)" alt="">
          <span class="text">{{item.text}}</span>
        </div>
        <div class="item submit-btn" @click="itemClick(3)">提 交</div>
      </div>
      <div class="canvas-wrap">
        <div class="border">
          <canvas ref="signaturePadCanvas" class="canvas"></canvas>
        </div>
        <p class="tip">请在框里写上您的签名</p>
      </div>
      <sh-loading :loading="loading"></sh-loading>
    </div>
  </van-popup>
</template>

<script>
import { mapState } from 'vuex'
import SignaturePad from "signature_pad";
import { SAVE_PERSON_SIGN, UPLOAD_SINGLE, GET_PERSON_SIGN } from '@/apis/listCommunication'
import { ImagePreview, Dialog } from 'vant';
export default {
  props: {
    fileName: String, //文件名
    signatureId: String, //签名人员 id
    options: { // 配置
      type: Object,
      default: () => {
        return {
          penColor: "#000000",   //笔刷颜色
          minWidth: 0.5,       //最小宽度
          maxWidth: 6,
          backgroundColor: '#f2f2f2'
        }
      }
    }
  },
  components: {
    [ImagePreview.Component.name]: ImagePreview.Component,
  },
  computed: {
    ...mapState(['user'])
  },
  data() {
    return {
      SignaturePad: null,
      fileList: [],
      show: false,
      loading: false,
      btnList: [
        { text: '退出', icon: 'close' },
        { text: '重写', icon: 'refresh' },
        { text: '预览', icon: 'preview' },
      ]
    }
  },
  mounted() {
    // 禁止页面复制
    document.onselectstart = new Function("event.returnValue=false");
  },
  methods: {
    open() {
      this.show = true
      this.$nextTick(() => {
        this.initSign()
      })
    },
    initSign() {
      const canvas = this.$refs.signaturePadCanvas;
      this.signaturePad = new SignaturePad(canvas, this.options);
      // window.addEventListener("resize", this.resizeCanvas);
      this.resizeCanvas();
      this.getSign()
    },
    // 重置写字板
    resizeCanvas() {
      const canvas = this.$refs.signaturePadCanvas;
      const ratio = Math.max(window.devicePixelRatio || 1, 1);
      canvas.width = canvas.offsetWidth * ratio;
      canvas.height = canvas.offsetHeight * ratio;
      canvas.getContext("2d").scale(ratio, ratio);
      this.signaturePad.clear();
    },
    itemClick(i) {
      switch (i) {
        case 0:
          let message = '确认关闭?'
          let confirmButtonText='退出'
          let cancelButtonText='取消'
          if (!this.signaturePad.isEmpty()) {
            message = '当前签名未提交,是否退出?'
          }
          Dialog.confirm({
            title: '提示',
            message,
            confirmButtonText,
            cancelButtonText,
            className: 'sign_close_dialog',
            getContainer: '#signature-wrap'
          })
            .then(() => {
              this.show = false
              this.$emit("cancel");
            })
            .catch(() => {
              // on cancel
            });
          break;
        case 1:
          this.signaturePad.clear();
          break;
        case 2:
          if (this.signaturePad.isEmpty()) {
            this.$toast({
              message: '暂无签名!',
              className: 'sign_empty_toast',
              getContainer: '#signature-wrap'
            })
            return;
          }
          const url = this.signaturePad.toDataURL();
          ImagePreview({
            images: [url],
            closeable: true,
          })
          break;
        case 3:
          if (this.signaturePad.isEmpty()) {
            this.$toast({
              message: '签名为空!',
              className: 'sign_empty_toast',
              getContainer: '#signature-wrap'
            })
            return;
          }
          const data = this.signaturePad.toDataURL();
          let fd = new FormData();
          let blob = this.dataURItoBlob(data, this.fileName);
          fd.append('files' + 1, blob);
          this.uploadFile(fd);
          break;
      }
    },
    // 获取用户签名
    getSign() {
      this.loading = true
      const id = this.signatureId || this.user.id
      this.$post(GET_PERSON_SIGN, { id }, data => {
        this.loading = false
        if (data.data.length && data.data[0].url) {
          this.signaturePad.fromDataURL(data.data[0].url);
        }
      },
        error => {
          this.$toast.fail(error.msg)
          this.loading = false
        }
      )
    },

    // 上传生成的签名图片
    uploadFile(fd) {
      const loading = this.$toast.loading("生成中...");
      this.$post(UPLOAD_SINGLE, fd, data => {
        this.fileList = data.data
        this.saveSign()
        loading.clear();
      }, error => {
        this.$toast.fail(error.msg)
        loading.clear();
      })
    },
    saveSign() {
      this.$post(SAVE_PERSON_SIGN, { fileList: this.fileList }, data => {
        this.$emit("confirm", this.fileList);
        this.$toast.success(data.msg)
        this.show = false
      })
    },
    // 将base64,转换成 file
    dataURItoBlob(dataUrl, filename = 'file') {
      const arr = dataUrl.split(',')
      const mime = arr[0].match(/:(.*?);/)[1]
      const suffix = mime.split('/')[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}.${suffix}`, {
        type: mime
      })
    }
  }
}
</script>

<style lang="scss" scoped>
.wrap {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: space-between;
}
.border {
  border: 1px dashed #c0c7d4;
  box-sizing: border-box;
  height: 100%;
}
.canvas-wrap {
  height: 100%;
  width: 100%;
  width: calc(100% - 65px);
  padding: 20px 15px 20px 3px;
  position: relative;
  box-sizing: border-box;
  .tip {
    position: absolute;
    transform: rotate(90deg);
    top: 50%;
    left: -52px;
    color: #c0c7d4;
    font-size: 14px;
    z-index: 100;
  }
}
.canvas {
  height: 100%;
  width: 100%;
}
.button-view {
  height: 100%;
  width: 60px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-direction: column;
  background: #fff;
  padding-top: 35px;
  padding-bottom: 50px;
  position: relative;
  .item {
    transform: rotate(90deg);
  }
  .btn {
    display: flex;
    align-items: center;
    width: 100px;
    height: 40px;
    margin-bottom: 62px;
    .text {
      color: #515e78;
    }
    .icon {
      width: 24px;
      height: 24px;
      margin-right: 6px;
    }
  }
  .close-btn {
    position: absolute;
    top: 45px;
  }
  .submit-btn {
    width: 100px;
    height: 40px;
    border-radius: 4px;
    background-color: #3f7af3;
    color: #fff;
    text-align: center;
    line-height: 40px;
  }
}

#signature-wrap {
  ::v-deep.sign_close_dialog {
    transform: rotate(90deg);
    top: 37%;
    left: 10%;
  }
  ::v-deep.sign_empty_toast {
    transform: rotate(90deg);
    top: 48%;
    left: 37%;
  }
}
</style>

# 使用

this.$refs.sign.open()
Last Updated: 5/5/2022, 3:40:42 PM