微信小程序签字功能

117 阅读4分钟

动画.gif

signature/signature.wxml

<!-- index.wxml -->
<view>
  <view class="content" style="padding-left: {{deviceInFo.safeArea.left || 10}}px">
    <view class="canvas_box">
      <!-- 定位到canvas画布的下方作为背景 -->
      <view class="canvas_tips">
        签 名 区
      </view>
      <!-- canvas画布 -->
      <canvas class="canvas_content" type="2d" style='width:{{canvasWidth}}px; height:{{canvasHeight}}px' id="myCanvas" bindtouchstart="bindtouchstart" bindtouchmove="bindtouchmove" bindtouchend="bindtouchend" bindtouchcancel="bindtouchcancel"></canvas>
    </view>
  </view>
  <!-- footer -->
  <view class="footer">
    <van-button plain class="item" block bind:click="overwrite">清空</van-button>
    <van-button plain class="item" block bind:click="prev">撤销</van-button>
    <van-button class="item" block bind:click="confirm" type="info">提交</van-button>
  </view>
</view>

signature/signature.wxss

点击展开/折叠
.content {
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  padding: 10px;
}

.canvas_box {
  width: 100%;
  height: 100%;
  background-color: #E8E9EC;
  position: relative;
}

/* 定位到canvas画布的下方作为背景 */
.canvas_tips {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  font-size: 80px;
  color: #E2E2E2;
  font-weight: bold;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* 底部按钮 */
.footer {
  box-sizing: border-box;
  z-index: 2;
  padding: 15px 0;
  background-color: #ffffff;
  position: fixed;
  width: 100%;
  box-shadow: 0 0 15rpx rgba(0, 0, 0, 0.1);
  left: 0;
  bottom: 0;
  display: flex;
  justify-content: center;
}

.item {
  width: 100px;
  margin: 0 20px;
}

.van-button {
  height: 40px !important;
  border-radius: 20px !important;
  font-size: 16px !important;
}

.weapp-button-index--van-button--plain {
  color: #333 !important;
  border-color: #ccc !important;
}

signature/signature.js

const {
  uploadFile
} = require("../../utils/file.js");

Page({
  data: {
    deviceInFo: {}, // 设备信息
    canvasWidth: '', // 画布宽
    canvasHeight: '', // 画布高
    canvas: null, // Canvas 对象实例
    ctx: null, // Canvas 对象上下文实例
    historyImage: [], // 历史记录,每一笔动作完成后的图片数据,用于每一次回退上一步是当作图片绘制到画布上
    initialCanvasImg: '', // 初始画布图,解决非ios设备重设置宽高不能清空画布的问题
    inspectionOrderId: ''
  },
  onReady() {
    // 获取可使用窗口的宽高,赋值给Canvas(宽高要减去上下左右padding的20,以及高度要减去footer区域)
    wx.createSelectorQuery()
      .select('.footer') // canvas获取节点
      .fields({
        node: true,
        size: true
      }) // 获取节点的相关信息,node:是否返回节点对应的 Node 实例,size:是否返回节点尺寸
      .exec((res) => {
        // 获取手机左侧安全区域(刘海)
        const deviceInFo = wx.getSystemInfoSync()
        const canvasWidth = deviceInFo.windowWidth - 20 - deviceInFo?.safeArea?.left || 0
        const canvasHeight = deviceInFo.windowHeight - res[0].height - 20
        this.setData({
          deviceInFo,
          canvasWidth,
          canvasHeight
        })
        this.initCanvas('init')
      })
  },
  onLoad(option) {
    this.data.inspectionOrderId = option.inspectionOrderId
  },
  // 初始化Canvas画布
  initCanvas(type) {
    let ctx = null
    let canvas = null
    let {
      historyImage,
      canvasWidth,
      canvasHeight,
      deviceInFo,
      initialCanvasImg
    } = this.data
    // 获取Canvas画布以及渲染上下文
    wx.createSelectorQuery()
      .select('#myCanvas') // canvas获取节点
      .fields({
        node: true,
        size: true
      }) // 获取节点的相关信息,node:是否返回节点对应的 Node 实例,size:是否返回节点尺寸
      .exec((res) => { // 执行所有的请求。请求结果按请求次序构成数组
        // Canvas 对象实例
        canvas = res[0].node
        // Canvas 对象上下文实例(动画动作绘图等都是在他的身上完成)
        ctx = canvas.getContext('2d')
        // Canvas 画布的实际绘制宽高
        const width = res[0].width
        const height = res[0].height
        // 获取设备像素比
        const dpr = wx.getWindowInfo().pixelRatio
        // 初始化画布大小
        canvas.width = width * dpr
        canvas.height = height * dpr
        // 画笔的颜色
        ctx.fillStyle = 'rgb(200, 0, 0)';
        // 指定了画笔(绘制线条)操作的线条宽度
        ctx.lineWidth = 5
        // 如果存在历史记录,则将历史记录最新的一张图片拿出来进行绘制。非ios时直接加载一张初始的空白图片
        if (historyImage.length !== 0 || (deviceInFo.platform !== 'ios' && type !== 'init')) {
          // 图片对象
          const image = canvas.createImage()
          // 图片加载完成回调
          image.onload = () => {
            // 将图片绘制到 canvas 上
            ctx.drawImage(image, 0, 0, canvasWidth, canvasHeight)
          }
          // 设置图片src
          image.src = historyImage[historyImage.length - 1] || initialCanvasImg;
        }
        // 缩小/放大图像
        ctx.scale(dpr, dpr)
        this.setData({
          canvas,
          ctx
        })
        // 保存一张初始空白图片
        if (type === 'init') {
          wx.canvasToTempFilePath({
            canvas,
            png: 'png',
            success: res => {
              // 生成的图片临时文件路径
              const tempFilePath = res.tempFilePath
              this.setData({
                initialCanvasImg: tempFilePath
              })
            },
          })
        }
      })
  },
  // 手指触摸动作开始
  bindtouchstart(event) {
    this.addPathDrop(event)
  },
  // 手指触摸后移动	
  bindtouchmove(event) {
    this.addPathDrop(event)
  },
  // 手指触摸动作结束
  bindtouchend(event) {
    this.addPathDrop(event)
  },
  // 手指触摸动作被打断,如来电提醒,弹窗
  bindtouchcancel(event) {
    this.addPathDrop(event)
  },
  // 添加路径点
  addPathDrop(event) {
    let {
      ctx,
      historyImage,
      canvas
    } = this.data
    let {
      type,
      changedTouches
    } = event
    let {
      x,
      y
    } = changedTouches[0]
    if (type === 'touchstart') { // 每次开始都是一次新动作
      // 最开始点
      ctx.moveTo(x, y) // 设置绘图起始坐标。
    } else {
      // 上一段终点
      ctx.lineTo(x, y) // 从最后一点到点(x,y)绘制一条直线。
      // 绘制
      ctx.stroke();
      // 下一段起点
      ctx.moveTo(x, y) // 设置绘图起始坐标。
    }
    // 每一次结束或者意外中断,保存一份图片到历史记录中
    if (type === 'touchend' || type === 'touchcancel') {
      wx.canvasToTempFilePath({
        canvas,
        png: 'png',
        success: res => {
          // 生成的图片临时文件路径
          const tempFilePath = res.tempFilePath
          historyImage.push(tempFilePath)
          this.setData(historyImage)
        },
      })
    }
  },
  // 上一步
  prev() {
    this.setData({
      historyImage: this.data.historyImage.slice(0, this.data.historyImage.length - 1)
    })
    this.initCanvas()
  },
  // 重写
  overwrite() {
    this.setData({
      historyImage: []
    })
    this.initCanvas()
  },
  // 提交
  confirm() {
    const {
      canvas,
      historyImage
    } = this.data
    if (historyImage.length === 0) {
      wx.showToast({
        title: "请先签名再提交",
        icon: 'none',
      });
      return
    }
    // 生成图片
    wx.canvasToTempFilePath({
      canvas,
      png: 'png',
      success: (res) => {
        this.handleUpload(res.tempFilePath)
      },
    })
  },
  // 上传附件并提交
  handleUpload(fileUrl) {
    uploadFile({
      fileUrl,
      requestUrl: 'uploadFile'
    }).then(res => {
      const {
        fileId,
        url,
      } = res
      wx.showToast({
        title: "上传服务器获取到url",
        icon: 'none',
      })
    })
  },
})

signature/signature.json

{
  "navigationBarTitleText": "供应商签名确认",
  "backgroundTextStyle": "dark",
  "pageOrientation": "landscape",
  "disableScroll": true,
  "usingComponents": {
    "van-button": "@vant/weapp/button/index"
  }
}

如果你觉得这篇文章对你有用,可以看看作者封装的库xtt-utils,里面封装了非常实用的js方法。如果你也是vue开发者,那更好了,除了常用的api,还有大量的基于element-ui组件库二次封装的使用方法和自定义指令等,帮你提升开发效率。不定期更新,欢迎交流~