Uniapp中实现APP手写签名功能

壬炎V8
发布于 2025-4-10 13:01
浏览
0收藏

手写签名功能在移动应用中有着广泛的应用场景,如电子合同签署、审批确认、单据验证等。本文将介绍如何在Uniapp中实现APP端的手写签名功能。

一、实现思路
在Uniapp中实现手写签名主要有两种方式:

使用Canvas绘图API实现自定义手写板

使用第三方插件或原生模块

本文将重点介绍第一种方式,即使用Canvas实现,这种方式跨平台兼容性好,且不需要依赖第三方库。

二、基础实现步骤

  1. 创建签名画布
<template>
  <view class="signature-container">
    <canvas 
      canvas-id="signatureCanvas"
      class="signature-canvas"
      @touchstart="handleTouchStart"
      @touchmove="handleTouchMove"
      @touchend="handleTouchEnd"
    ></canvas>
    <view class="button-group">
      <button @click="clearSignature">清除</button>
      <button @click="saveSignature">保存</button>
    </view>
  </view>
</template>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  1. 样式设置
<style>
.signature-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20rpx;
}

.signature-canvas {
  width: 100%;
  height: 500rpx;
  background-color: #f8f8f8;
  border: 1rpx solid #ddd;
}

.button-group {
  display: flex;
  justify-content: space-around;
  width: 100%;
  margin-top: 40rpx;
}

button {
  width: 45%;
}
</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.
  1. JavaScript逻辑实现
<script>
export default {
  data() {
    return {
      points: [], // 存储触摸点
      ctx: null, // canvas上下文
      canvasWidth: 0,
      canvasHeight: 0,
      isDrawing: false
    }
  },
  onReady() {
    this.initCanvas()
  },
  methods: {
    // 初始化画布
    initCanvas() {
      const query = uni.createSelectorQuery().in(this)
      query.select('.signature-canvas')
        .fields({ node: true, size: true })
        .exec(res => {
          const canvas = res[0].node
          this.ctx = canvas.getContext('2d')
          this.canvasWidth = res[0].width
          this.canvasHeight = res[0].height
          
          // 设置canvas实际渲染尺寸
          canvas.width = res[0].width * uni.getSystemInfoSync().pixelRatio
          canvas.height = res[0].height * uni.getSystemInfoSync().pixelRatio
          
          // 缩放画布以适配高清屏
          this.ctx.scale(
            uni.getSystemInfoSync().pixelRatio,
            uni.getSystemInfoSync().pixelRatio
          )
          
          this.ctx.lineWidth = 3
          this.ctx.lineCap = 'round'
          this.ctx.lineJoin = 'round'
          this.ctx.strokeStyle = '#000000'
        })
    },
    
    // 触摸开始
    handleTouchStart(e) {
      this.isDrawing = true
      const point = {
        x: e.touches[0].x,
        y: e.touches[0].y
      }
      this.points.push(point)
      this.ctx.beginPath()
      this.ctx.moveTo(point.x, point.y)
    },
    
    // 触摸移动
    handleTouchMove(e) {
      if (!this.isDrawing) return
      
      const point = {
        x: e.touches[0].x,
        y: e.touches[0].y
      }
      this.points.push(point)
      
      this.ctx.lineTo(point.x, point.y)
      this.ctx.stroke()
    },
    
    // 触摸结束
    handleTouchEnd() {
      this.isDrawing = false
      this.points = []
    },
    
    // 清除签名
    clearSignature() {
      this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
      this.points = []
    },
    
    // 保存签名
    saveSignature() {
      uni.canvasToTempFilePath({
        canvasId: 'signatureCanvas',
        success: res => {
          // 这里可以保存到本地或上传服务器
          uni.saveImageToPhotosAlbum({
            filePath: res.tempFilePath,
            success: () => {
              uni.showToast({
                title: '签名保存成功',
                icon: 'success'
              })
            },
            fail: () => {
              uni.showToast({
                title: '保存失败',
                icon: 'none'
              })
            }
          })
        },
        fail: err => {
          console.error(err)
          uni.showToast({
            title: '保存失败',
            icon: 'none'
          })
        }
      }, this)
    }
  }
}
</script>
  • 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.

三、功能优化

  1. 添加背景网格线
drawGrid() {
  this.ctx.strokeStyle = '#e0e0e0'
  this.ctx.lineWidth = 1
  
  // 画横线
  for (let i = 0; i < this.canvasHeight; i += 20) {
    this.ctx.beginPath()
    this.ctx.moveTo(0, i)
    this.ctx.lineTo(this.canvasWidth, i)
    this.ctx.stroke()
  }
  
  // 画竖线
  for (let i = 0; i < this.canvasWidth; i += 20) {
    this.ctx.beginPath()
    this.ctx.moveTo(i, 0)
    this.ctx.lineTo(i, this.canvasHeight)
    this.ctx.stroke()
  }
  
  this.ctx.strokeStyle = '#000000'
  this.ctx.lineWidth = 3
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  1. 添加撤销功能
// 在data中添加
history: [],

// 修改handleTouchEnd方法
handleTouchEnd() {
  if (this.points.length > 0) {
    this.history.push(this.ctx.getImageData(0, 0, this.canvasWidth, this.canvasHeight))
  }
  this.isDrawing = false
  this.points = []
},

// 添加撤销方法
undo() {
  if (this.history.length > 0) {
    this.ctx.putImageData(this.history.pop(), 0, 0)
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  1. 支持不同颜色和粗细
// 在data中添加
penOptions: {
  colors: ['#000000', '#ff0000', '#00ff00', '#0000ff'],
  sizes: [1, 3, 5, 8],
  selectedColor: '#000000',
  selectedSize: 3
},

// 修改绘图方法
handleTouchStart(e) {
  // ...
  this.ctx.lineWidth = this.penOptions.selectedSize
  this.ctx.strokeStyle = this.penOptions.selectedColor
  // ...
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

四、注意事项
Canvas尺寸问题:在移动设备上,Canvas的实际渲染尺寸和逻辑尺寸可能不同,需要根据设备像素比进行适配。

性能优化:对于复杂的签名,可以适当减少记录的点数或使用贝塞尔曲线优化。

跨平台兼容性:虽然Uniapp是跨平台的,但不同平台对Canvas的支持可能有细微差异,需要充分测试。

高清屏适配:在高DPI设备上,需要特别注意Canvas的清晰度问题。

保存格式:根据需求选择合适的图片格式(PNG/JPG),PNG支持透明背景,JPG文件更小。

五、完整组件封装
可以将签名功能封装为可复用的组件:

<!-- components/signature-pad/signature-pad.vue -->
<template>
  <view class="signature-pad">
    <canvas 
      canvas-id="signatureCanvas"
      class="signature-canvas"
      @touchstart="handleTouchStart"
      @touchmove="handleTouchMove"
      @touchend="handleTouchEnd"
    ></canvas>
    <view class="controls">
      <button @click="clear">清除</button>
      <button @click="undo">撤销</button>
      <button @click="save">保存</button>
    </view>
    <view class="pen-options" v-if="showPenOptions">
      <view class="color-options">
        <view 
          v-for="color in penOptions.colors" 
          :key="color"
          class="color-option"
          :style="{backgroundColor: color}"
          @click="selectColor(color)"
        ></view>
      </view>
      <view class="size-options">
        <view 
          v-for="size in penOptions.sizes" 
          :key="size"
          class="size-option"
          @click="selectSize(size)"
        >
          <view 
            class="size-dot"
            :style="{
              width: size + 'px',
              height: size + 'px',
              backgroundColor: penOptions.selectedColor
            }"
          ></view>
        </view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  name: 'SignaturePad',
  props: {
    showPenOptions: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      // ...之前的data内容
    }
  },
  methods: {
    // ...之前的方法
    selectColor(color) {
      this.penOptions.selectedColor = color
    },
    selectSize(size) {
      this.penOptions.selectedSize = size
    },
    getSignature() {
      return new Promise((resolve, reject) => {
        uni.canvasToTempFilePath({
          canvasId: 'signatureCanvas',
          success: res => resolve(res.tempFilePath),
          fail: err => reject(err)
        }, this)
      })
    }
  }
}
</script>
  • 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.

六、使用第三方插件
如果需要更强大的功能,可以考虑使用第三方插件:

uniapp-signature:专为Uniapp开发的签名插件

html2canvas:通过webview方式实现的截图方案

原生插件:对于性能要求高的场景,可以考虑开发原生插件

七、总结
本文介绍了在Uniapp中实现手写签名功能的完整方案,从基础的Canvas绘图到功能优化和组件封装。这种实现方式跨平台兼容性好,不需要依赖第三方库,适合大多数应用场景。对于更复杂的需求,可以考虑使用第三方插件或开发原生模块。

实际开发中,还需要根据具体业务需求进行调整,如添加时间戳、用户信息水印、多页签名等功能。

收藏
回复
举报


回复
    相关推荐