微信小程序Canvas实现步骤式手写签字板组件

305 阅读4分钟

功能需求

  1. 法律文书签字:在远程取证产品中,要求实现用户对法律文书签写姓名、日期、备注(可多页签写),同时要求能够支持自定义签名类型,例如有些文书仅需签写姓名和日期等多种情况。 4033ce0e6d6fca03975820b8123aed6.png

  2. 证据签字:证据图片要求签写姓名和日期。

    image.png

  3. 笔录签字:笔录要求签写法律条款、姓名、日期。

    image.png

设计分析

1、签字类型

归纳签字类型分别为姓名、日期、备注、法律条款,其中姓名和日期是相对简单的,只需要单页签字,法律条款(每页签一个字)和备注都需要支持多页签字。定义签字类型如下:

const signType=[{
    name:'姓名',placeholder:'请签写姓名'
},{
    name:'日期',placeholder:'请在此签日期(今日:' +this.data.year+'年'+this.data.month+'月'+this.data.day+'日)'
},{
    name:'法律条款',clauseContent:data.clauseContent.split(""),multi:true //条款文字字符串转数组
},{
    name:'备注',placeholder:'请签写备注,写不下请点击继续签写',multi:true //multi是否多页签写
}]

2、功能按钮

清除:清除canvas绘制图形,若canvas已转为图片则清空数组图片。

上一步:上一步包含了上一个(法律条款)和上一页(多页备注)功能。

下一步:按类型划分的大步骤,最后一步不展示。

确认提交:签字完成后,提交签字信息至服务端。

继续签写:类型为备注时特有,实际功能为下一页

下一个:类型为法律条款时特有,新的一个字

3、数据定义

  data: {
    currentSignIndex:0, //当前签字步骤
    subCurrentSignIndex:0, //当前签字子步骤(备注、法律条款)
    imgUrlPath:[], //二维数组 canvas转图片展示
    imgUrlBase64:[], //二维数组 图片转base64,供后端使用
    signCxt:[], //二维数组 绘图上下文 [CanvasContext](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.html) 对象
    isShowPlaceholder:true, //未签字前的提示占位符
  },

签字功能关键代码实现

canvas页面wxml

      <canvas
        canvas-id="{{currentSignIndex+'signCanvas'+subCurrentSignIndex}}"
        id="{{currentSignIndex+'signCanvas'+subCurrentSignIndex}}"
        class="sign_area"
        disable-scroll="{{true}}"
        bindtouchstart="signTouchStart"
        bindtouchmove="signTouchMove"
        wx:if="{{lawSignType[currentSignIndex].multi&&!imgUrlPath[currentSignIndex][subCurrentSignIndex]}}"
      >
        <text wx:if="{{isShowLawPlaceholder}}">{{lawSignType[currentSignIndex].placeholder}}</text>
      </canvas>
      <canvas
        canvas-id="{{currentSignIndex+'signCanvas'}}"
        id="{{currentSignIndex+'signCanvas'}}"
        class="sign_area"
        disable-scroll="{{true}}"
        bindtouchstart="signTouchStart"
        bindtouchmove="signTouchMove"
        wx:elif="{{!imgUrlPath[currentSignIndex]}}"
      >
        <text wx:if="{{isShowLawPlaceholder}}">{{lawSignType[currentSignIndex].placeholder}}</text>
      </canvas>
      <image
        style="width:100%;height:100%;"
        mode="aspectFill"
        src="{{lawSignType[currentSignIndex].multi?imgUrlPath[currentSignIndex][subCurrentSignIndex]:imgUrlPath[currentSignIndex]}}"
        wx:else
      />

创建canvas绘图上下文

创建 canvas 的绘图上下文 CanvasContext 对象,构造二维数组。

function initCanvas(that, idName) {
  const context = wx.createCanvasContext(idName,that)
  let num,subNum
  num=Number(idName.slice(0,1)) //签字步骤
  subNum=idName.substring(11)  //签字子步骤
  const signCxt=that.data.signCxt
  const imgUrlPath=that.data.imgUrlPath
  const imgUrlBase64=that.data.imgUrlBase64
  if(subNum){
    subNum=Number(subNum)
    if(subNum===0){ 
      signCxt[num]=[context]
      imgUrlPath[num]=[null]
      imgUrlBase64[num]=[null]
    }else{
      signCxt[num].push(context)
      imgUrlPath[num].push(null)
      imgUrlBase64[num].push(null)
    }
  }else{
    signCxt[num]=context
    imgUrlPath[num]=null
    imgUrlBase64[num]=null
  }
  that.setData({signCxt})
  that.setData({imgUrlPath,imgUrlBase64})
  context.setLineCap('round') //设置签字线条端点样式
  context.setLineWidth(4) //设置签字线条宽度
}

开始绘制

.moveTo()方法把路径移动到画布中的指定点,不创建线条。用 stroke 方法来画线条

signTouchStart(e) {
  this.setData({isShowLawPlaceholder:false})
  const {lawSignType,currentSignIndex,subCurrentSignIndex}=this.data
  if(lawSignType[currentSignIndex].multi){
    this.data.signCxt[currentSignIndex][subCurrentSignIndex].moveTo(e.changedTouches[0].x, e.changedTouches[0].y)
  }else{
    this.data.signCxt[currentSignIndex].moveTo(e.changedTouches[0].x, e.changedTouches[0].y)
  } 
},

过程绘制

.lineTo()增加一个新点,然后创建一条从上次指定点到目标点的线。用 stroke 方法来画线条 .draw(boolean reserve, function callback),reserve为true时保留上一次结果,false则不保留(可用于清除功能)。

signTouchMove(e) {
  const {lawSignType,signCxt,currentSignIndex,subCurrentSignIndex}=this.data
  if(lawSignType[currentSignIndex].multi){
    signCxt[currentSignIndex][subCurrentSignIndex]=func.writeText(e, this.data.signCxt[currentSignIndex][subCurrentSignIndex])
  }else{
    signCxt[currentSignIndex]=func.writeText(e, this.data.signCxt[currentSignIndex])
  }
  this.setData({signCxt:signCxt})
},
function writeText(value, obj) {
  obj.lineTo(value.changedTouches[0].x, value.changedTouches[0].y);
  obj.stroke();
  obj.draw(true);
  obj.moveTo(value.changedTouches[0].x, value.changedTouches[0].y);
  return obj;
}

canvas转图片

canvas转本地图片,本地图片读取转base64供后续上传至服务端

function picToBaseFormat(canvasId, callback,that) {
  wx.canvasToTempFilePath({
    x: 0,
    y: 0,
    fileType: 'png',
    canvasId,
    success({ tempFilePath }) {
      wx.getFileSystemManager().readFile({
        filePath: tempFilePath,
        encoding: 'base64',
        success({ data, errMsg }) {
          if (errMsg.split(':')[1] === 'ok') {
            callback(data, tempFilePath)
          }
        },
        fail(err) {
          console.log('err', err);
        }
      })
    },
    fail(err) {
      console.log('err', err);
    }
  },that)
}

清除canvas

clearSign(){
  const {signCxt,imgUrlPath,imgUrlBase64,currentSignIndex,subCurrentSignIndex}=this.data
  if(this.data.lawSignType[currentSignIndex].multi){
    signCxt[currentSignIndex][subCurrentSignIndex].draw();
    signCxt[currentSignIndex][subCurrentSignIndex].setLineCap('round')
    signCxt[currentSignIndex][subCurrentSignIndex].setLineWidth(4);
    imgUrlPath[currentSignIndex][subCurrentSignIndex]=null
    imgUrlBase64[currentSignIndex][subCurrentSignIndex]=null
  }else{
    signCxt[currentSignIndex].draw();
    signCxt[currentSignIndex].setLineCap('round')
    signCxt[currentSignIndex].setLineWidth(4);
    imgUrlPath[currentSignIndex]=null
    imgUrlBase64[currentSignIndex]=null
  }
  this.setData({
    isShowLawPlaceholder: true,
    signCxt,
    imgUrlPath,
    imgUrlBase64
  })  
},

步骤逻辑实现

由于每个步骤都需要考虑当前页所在的类型是否支持多页签写,所以判断逻辑较多。实现上由于时间较急,优先确保了逻辑清晰,写法上后续会再优化。

下一步

NextStep(){
  const {lawSignType,currentSignIndex,subCurrentSignIndex,imgUrlPath,imgUrlBase64}=this.data
  if(lawSignType[currentSignIndex].multi){
    if(!imgUrlPath[currentSignIndex][subCurrentSignIndex]){
      func.picToBaseFormat(currentSignIndex+'signCanvas'+subCurrentSignIndex, (data, tempFilePath) => {
        imgUrlPath[currentSignIndex][subCurrentSignIndex]=tempFilePath
        imgUrlBase64[currentSignIndex][subCurrentSignIndex]=data
        this.setData({imgUrlPath,imgUrlBase64})
        this.setData({currentSignIndex:currentSignIndex+1,isShowLawPlaceholder:true})
        this.creatNextCanvas()
      },this)
    }else{
      this.setData({currentSignIndex:currentSignIndex+1,isShowLawPlaceholder:true})
      this.creatNextCanvas()
    }
  }else{
    if(!imgUrlPath[currentSignIndex]){
      func.picToBaseFormat(currentSignIndex+'signCanvas', (data, tempFilePath) => {
        const {imgUrlPath,imgUrlBase64}=this.data
        imgUrlPath[currentSignIndex]=tempFilePath
        imgUrlBase64[currentSignIndex]=data
        this.setData({imgUrlPath,imgUrlBase64})
        this.setData({currentSignIndex:currentSignIndex+1,isShowLawPlaceholder:true})
        this.creatNextCanvas()
      },this)
    }else{
      this.setData({currentSignIndex:currentSignIndex+1,isShowLawPlaceholder:true})
      this.creatNextCanvas()
    }
  }
},

确认提交

makeSureSubmit(){
      const {lawSignType,currentSignIndex,subCurrentSignIndex,imgUrlPath,imgUrlBase64,signCxt}=this.data
      if(lawSignType[currentSignIndex].multi){
        if(signCxt[currentSignIndex][subCurrentSignIndex].path.length === 0 && !imgUrlPath[currentSignIndex][subCurrentSignIndex]){
          wx.showToast({
            title: '请先签写备注信息',
            icon: 'none',
            duration: 2000
          })
          return
        }
        if(!imgUrlPath[currentSignIndex][subCurrentSignIndex]){
          func.picToBaseFormat(currentSignIndex+'signCanvas'+subCurrentSignIndex, (data, tempFilePath) => {
            imgUrlPath[currentSignIndex][subCurrentSignIndex]=tempFilePath
            imgUrlBase64[currentSignIndex][subCurrentSignIndex]=data
            this.setData({imgUrlPath,imgUrlBase64})
            this.triggerEvent('signsubmit', imgUrlBase64)
          },this)
        }else{
          this.triggerEvent('signsubmit', imgUrlBase64)
        }
      }else{
        if(!imgUrlPath[currentSignIndex]){
          func.picToBaseFormat(currentSignIndex+'signCanvas', (data, tempFilePath) => {
            const {imgUrlPath,imgUrlBase64}=this.data
            imgUrlPath[currentSignIndex]=tempFilePath
            imgUrlBase64[currentSignIndex]=data
            this.setData({imgUrlPath,imgUrlBase64})
            this.triggerEvent('signsubmit', imgUrlBase64)
          },this)
        }else{
          this.triggerEvent('signsubmit', imgUrlBase64)
        }
      }
    }
  },

上一步

lawFrontStep(){
      const {currentSignIndex,subCurrentSignIndex}=this.data
      if(this.data.subCurrentSignIndex>0){
        this.data.signCxt[currentSignIndex][subCurrentSignIndex].path=[]
        this.setData({subCurrentSignIndex:this.data.subCurrentSignIndex-1,signCxt:this.data.signCxt})
      }else{
        this.data.signCxt[currentSignIndex].path=[]
        this.setData({currentSignIndex:this.data.currentSignIndex-1,signCxt:this.data.signCxt})
      }
    },

继续签写/下一个

continueSign(){
      const {imgUrlPath,currentSignIndex,subCurrentSignIndex,imgUrlBase64}=this.data
      if(!imgUrlPath[currentSignIndex][subCurrentSignIndex]){
        func.picToBaseFormat(currentSignIndex+'signCanvas'+subCurrentSignIndex, (data, tempFilePath) => {
          imgUrlPath[currentSignIndex][subCurrentSignIndex]=tempFilePath
          imgUrlBase64[currentSignIndex][subCurrentSignIndex]=data
          this.setData({imgUrlPath,imgUrlBase64})
          this.setData({subCurrentSignIndex:subCurrentSignIndex+1,isShowLawPlaceholder:true})
          this.createNextPageCanvas()
        },this)
      }else{
        this.setData({subCurrentSignIndex:subCurrentSignIndex+1,isShowLawPlaceholder:true})
        this.createNextPageCanvas()
      }
    },

组件的使用

签字组件在使用上只需要参考设计分析中的签字类型,按要求确认应用的需求类型及结构。输出为一组签字图片,顺序同输入类型的顺序,如有类型为多页签写,则输出一组二维数组图片。