【vue】js原生实现横屏电子签名

前言:

本功能只实现了js原生实现横屏电子签名,至于其他后面有机会再发文详细讲解。


需求:

H5实现横向电子签名       附完成效果

待签字:

签字中:

未签字:


功能:

通过短信链接或者APP进入H5页面实现pdf合同的签订。        


实现原理:利用原生canvas 配合 移动端的事件, touchstart, touchend, touchmove, 进行 在canvas 画布上画线, 最后,把生成的签名, 通过 toDataURL()方法 保存 为 base64 图片。

注意:对功能、流程、脑图感兴趣可以参考学习。如单纯实现该功能,可以忽略!!!

流程图:

脑图:

上代码!!!!!!!!!!!!!!

//此内容为canvas的api

data() {
    return {
      radio: "#000",
      height: 50,
      direction: false, // true 代表横屏, false 代表'竖屏' -- 但是亲测没有效果
      el: "", // canvas dom
      ctx: "", // canvas context
      background: "white", // canvas background-color
      color: "#000", // 绘制时线条的颜色
      linewidth: 2, // 线条的宽度
      liColors: ["#000"],
      drawCheck: false, //用来判断是否签字
      clientHeight: document.documentElement.clientHeight,
      clientWidth: document.documentElement.clientWidth,
      loading: false,
      base64: "",
      prohibit: false,
    };
  },

1.在窗口大小改变时重新绘制画布。

mounted钩子函数中,设置了窗口的resize事件,当窗口大小改变时,重新计算客户端的高度和宽度,并调用draw方法进行重新绘制。

  mounted() {

    window.onresize = () => {
      this.clientHeight = document.documentElement.clientHeight;
      this.clientWidth = document.documentElement.clientWidth;
      this.draw();
      return;
    };

    this.draw();
  },

2.实现了在Vue中绘制线条的功能

        2.1、draw方法用于添加绘制线条的功能。首先通过$refs获取到具有ref属性为signHandle的元素,该元素将用作绘制线条的画布。然后调用initCanvas方法进行画布的初始化配置。


        2.2、initCanvas方法首先通过设置el的宽度和高度为客户端的宽度和高度,然后获取画布的上下文ctx。接下来调用setCanvas方法设置画布的背景色、线条颜色、线宽和线条两头的样式。最后分别调用drawStartdrawingdrawEnd方法来处理绘制的具体逻辑。


        2.3、drawStart方法添加了touchstart事件监听器,在触摸开始时,调用ctx.beginPath()开始绘制线条,并使用触摸点的坐标作为起始点。


        2.4、drawing方法添加了touchmove事件监听器,在触摸移动时,调用ctx.lineTo()绘制线条的路径,并调用ctx.stroke()绘制线条。


        2.5、drawEnd方法添加了touchend事件监听器,在触摸结束时,调用ctx.closePath()结束绘制线条。


        2.6、最后还提供了一个clearHandle方法,用于清空画布并重置绘制状态。

 // 添加绘制 line
    draw() {
      if (!this.prohibit) {
        this.$refs.signHandle.addEventListener(
          "touchstart",
          (e) => e.preventDefault(),
          {
            passive: false,
          }
        );
        this.el = this.$refs.signHandle;
        console.log(this.prohibit);

        this.initCanvas();
      }
    },
    // 初始化canvas配置
    initCanvas() {
      const { height, direction, el } = this;
      el.width = this.clientWidth;
      el.height = this.clientHeight;
      this.ctx = el.getContext("2d");
      this.setCanvas();
      this.drawStart();
      this.drawing();
      this.drawEnd();
    },
    // 配置 canvas
    setCanvas() {
      const { ctx, height, direction } = this;
      // 设置背景色
      ctx.fillStyle = this.background;
      ctx.fillRect(0, 0, this.clientWidth, this.clientHeight);
      // 设置线条颜色
      ctx.strokeStyle = this.color;
      // 设置线宽
      ctx.lineWidth = this.linewidth;
      // 设置线条两头的结束点和开始点是圆形的
      ctx.lineCap = "round";
    },
    // 开始绘制
    drawStart() {
      const { el, ctx } = this;
      el.addEventListener(
        "touchstart",
        (e) => {
          ctx.beginPath();
          ctx.moveTo(e.changedTouches[0].pageX, e.changedTouches[0].pageY);
          this.drawCheck = true; //代表签过字
        },
        false
      );
    },
    // 绘制中
    drawing() {
      const { el, ctx } = this;
      el.addEventListener(
        "touchmove",
        (e) => {
          ctx.lineTo(e.changedTouches[0].pageX, e.changedTouches[0].pageY);
          ctx.stroke();
        },
        false
      );
    },
    // 绘制结束
    drawEnd() {
      const { el, ctx } = this;
      el.addEventListener("touchend", () => ctx.closePath(), false);
    },
    // 清空
    clearHandle() {
      this.initCanvas();
      this.drawCheck = false; //代表签过字
    },

3.提交并保存图片


  onComfirm() {
      if (this.drawCheck == false) {
        this.prohibit = true;
        showToast({
          message:
            '<div style="transform: rotate(90deg);height:120px; line-height:60px;  margin-top: 10px; backgroundColor:black"><div style="width: 80px;padding: 38% 0 38% 0;display: flex;"><span>请</span><span>进</span><span>行</span><span>签</span><span>字</span></div></div>',
          // '<div>请进行签字</div>',
          type: "html",
        });
        return;
      }
      this.saveImg();
      // this.adminSave()
    },
    // 保存信息
    saveImg() {
      const img = new Image();
      this.loading = true;
      const imgBase64 = this.el.toDataURL();

      console.log('没处理前的base64',imgBase64)


      this.rotateBase64Img(imgBase64, -90);
    },
  


 //签完名的图片90°旋转处理
    rotateBase64Img(src, edg) {
      let that = this;
      var canvas = document.createElement("canvas");
      var ctx = canvas.getContext("2d");
      var imgW; //图片宽度
      var imgH; //图片高度
      var size; //canvas初始大小
      if (edg % 90 != 0) {
        console.error("旋转角度必须是90的倍数!");
        throw "旋转角度必须是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 = function () {
        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);
        //drawImage向画布上绘制图片
        ctx?.drawImage(image, 0, 0);
        //getImageData() 复制画布上指定矩形的像素数据
        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;
        }
        //putImageData() 将图像数据放回画布
        ctx?.putImageData(imgData, 0, 0);
        // callback(canvas.toDataURL("image/png"));
        this.base64 = canvas.toDataURL("image/png").split(",")[1];
        console.log('最终的base64',this.base64)
        // return ;
      };
    },

4.最后template部分

<template>
  <div class="signingbox">
    <div class="header">
      <van-nav-bar title="签名" @click-left="this.$router.go(-1)">
        <template #left>
          <el-icon size="20">
            <ArrowLeft />
          </el-icon>
        </template>
      </van-nav-bar>
    </div>
    <div class="page" ref="page">
      <div class="backgrounds">
        <div class="btn_container van-hairline--top">
          <el-button class="button reSign" size="normal" @click="clearHandle"
            >重签</el-button
          >
          <el-button
            type="primary"
            class="button button-right"
            size="normal"
            @click="onComfirm"
            >完成签名</el-button
          >
        </div>
      </div>
      <div class="canvasBox">
        <canvas ref="signHandle" class="canvas" />
      </div>
    </div>
    <div
      class="signatureBlock"
      @touchstart="drawCheck = true"
      v-if="!drawCheck"
    >
      签名区
    </div>
    <div v-if="loading" class="mask">
      <van-loading type="spinner" />
    </div>
  </div>
</template>

    style部分

<style lang="scss" scoped>
.mask {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 999;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100vh;
  background-color: rgba($color: #000000, $alpha: 0.4);
}
.header {
  position: fixed;
  top: 0;
  z-index: 500;
  width: 100%;
  // height: 46px;
}

.signingbox {
  position: relative;
  box-sizing: border-box;
  overflow: hidden;

  .reSign {
    color: #77adff;
    border: 1px solid #77adff;
  }
}

.signatureBlock {
  position: absolute;
  top: 47%;
  left: 48%;
  color: rgb(241, 241, 241);
  font-weight: 700;
  font-size: 24px;
  transform: translate(-50%);
  transform: rotate(90deg);
}

.backgrounds {
  position: fixed;
  width: 60px;
  width: 60px;
  height: 100%;
  margin-top: 45px;
  margin-top: 45px;
  background-color: rgb(244, 246, 248);
}

.page {
  width: 100%;
  // height: 100%;
  overflow: hidden;
  background-color: rgb(244, 246, 248);

  .btn_container {
    position: fixed;
    bottom: 147px;
    left: -35%;
    display: flex;
    align-items: center;
    justify-content: space-between;
    box-sizing: border-box;
    width: 80%;
    margin-left: 10px;
    padding: 0 8px;
    text-align: center;
    background-color: rgb(244, 246, 248);
    transform: rotate(90deg);
  }

  .button {
    width: 50%;
    height: 34px;
    line-height: 34px;
    border-radius: 5px;
  }

  .button-right {
    margin-left: 4px;
    color: white;
    background-color: rgb(28, 111, 233);
  }
}

.canvasBox {
  width: 100%;
  overflow: hidden;
}

.button-wrapper {
  display: flex;
  justify-content: space-between;
  min-width: 180px;
}
</style>

总结:用于自用的笔记记录,写的可能有些乱,希望能帮到你。

        市面上画布插件多个。对于简单的功能需求,可以考虑使用已有的JS插件;

        方法一      方法二       方法三

        而对于复杂的功能需求或更高的灵活性,可以选择使用原生JS进行开发

        如果大家有什么不懂或者问题可以提交在评论区。欢迎讨论!!!!

  • 34
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白日探险家

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值