二次封装一个vue3签字板signature pad

先贴一下这个组件的github:https://github.com/402931261/signature-pad-for-vue3和npm地址:https://www.npmjs.com/package/@npm_lx/signature-pad-for-vue3

在最近的业务需求中,遇到需要使用签字板功能,同事使用了@selemondev/vue3-signature-pad这个依赖,但是发现使用后会有一些小bug,比如图片保存不清晰,笔画漂移等,发现这些问题是因为canvas的宽高计算时刻不正确导致图片模糊,笔画漂移好像是因为window.devicePixelRate 这个变量地问题,具体没有实际去找根本问题。后面自己封装了一个基于signature_pad依赖的vue3的组件,解决了这些遇到的问题。

由于这个封装时间比较短,只实现了signature_pad在vue3中的基础使用,并没有添加比如水印之类的功能,但是根据signature_pad实现了动态改变笔画颜色、回显背景,清除笔画、清除笔画和背景、下载图片、导出base64等实用小功能。

最后再贴一下组件代码,实现起来其实功能都很简单,就是canvas的api比较少用,查来查去的,才把一些功能实现了。

html 复制代码
<template>
  <div class="signature-canvas-container" ref="containerRef">
    <canvas ref="canvasRef" class="signature-canvas-pad"></canvas>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import SignaturePad from 'signature_pad'

const emits = defineEmits(['beginStroke', 'endStroke'])
const props = defineProps({
  // 笔画最小宽度
  penMinWidth: {
    type: Number,
    default: 2,
  },
  // 笔画最大宽度
  penMaxWidth: {
    type: Number,
    default: 2,
  },
  // 笔画颜色
  penColor: {
    type: String,
    default: '#000000',
  },
  // 背景色
  backgroundColor: {
    type: String,
    default: '#F3F3F4',
  },
  // 是否有背景图(用于回显)
  bgImageUrl: {
    type: String,
    default: '',
  },
})

// 背景url改变监听
watch(
  () => props.bgImageUrl,
  () => {
    initBgImage()
  },
)

// 笔画颜色改变监听
watch(
  () => props.penColor,
  () => {
    if (signaturePadRef.value) {
      signaturePadRef.value.penColor = props.penColor
    }
  },
)

// 笔画最小宽度监听
watch(
  () => props.penMinWidth,
  (val) => {
    if (signaturePadRef.value) {
      signaturePadRef.value.minWidth = val
    }
  },
)

// 笔画最大宽度监听
watch(
  () => props.penMaxWidth,
  (val) => {
    if (signaturePadRef.value) {
      signaturePadRef.value.maxWidth = val
    }
  },
)

const containerRef = ref(null) // 画板容器
const canvasRef = ref(null)
const signaturePadRef = ref(null)
const bgImage = ref(null)
const isEmpty = ref(true)
const canvasData = ref(null) // 保存画板数据

let ratio = window.devicePixelRatio || 1

// 初始化背景图
const initBgImage = () => {
  if (props.bgImageUrl) {
    const img = new Image()
    img.crossOrigin = 'anonymous'
    img.onload = () => {
      bgImage.value = img
      if (signaturePadRef.value) {
        signaturePadRef.value.clear()
        if (canvasData.value) {
          signaturePadRef.value.fromData(canvasData.value)
        }
      }
    }
    img.src = props.bgImageUrl
  } else {
    bgImage.value = null
    if (signaturePadRef.value) {
      signaturePadRef.value.clear()
      if (canvasData.value) {
        signaturePadRef.value.fromData(canvasData.value)
      }
    }
  }
}

// 初始化画板
const initSignaturePad = () => {
  if (!canvasRef.value || !containerRef.value) return

  const canvas = canvasRef.value
  const container = containerRef.value

  const width = container.clientWidth
  const height = container.clientHeight

  canvas.width = width * ratio
  canvas.height = height * ratio
  canvas.style.width = `${width}px`
  canvas.style.height = `${height}px`

  const ctx = canvas.getContext('2d')
  ctx.scale(ratio, ratio)

  const canvasPad = new SignaturePad(canvas, {
    minWidth: props.penMinWidth,
    maxWidth: props.penMaxWidth,
    penColor: props.penColor,
    backgroundColor: props.backgroundColor,
  })

  removeEvents(canvasPad)
  addEvents(canvasPad)

  const originClear = canvasPad.clear
  canvasPad.clear = (isClearBg) => {
    originClear.call(canvasPad) // 调用原始方法
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    if (bgImage.value && !isClearBg) {
      ctx.drawImage(bgImage.value, 0, 0, width, height)
    } else {
      ctx.fillStyle = props.backgroundColor
      ctx.fillRect(0, 0, width, height)
    }
  }

  signaturePadRef.value = canvasPad
  canvasPad.clear()

  // 恢复之前的画板
  if (canvasData.value) {
    canvasPad.fromData(canvasData.value)
  }
}

// 解除事件绑定
const removeEvents = (canvasPad) => {
  canvasPad.removeEventListener('beginStroke')
  canvasPad.removeEventListener('endStroke')
}

// 添加事件绑定
const addEvents = (canvasPad) => {
  canvasPad.addEventListener('beginStroke', () => {
    emits('beginStroke')
  })
  canvasPad.addEventListener('endStroke', () => {
    emits('endStroke')
  })
}

// 清空画板 是否清除背景图
const clear = (isClearBg = false) => {
  signaturePadRef.value?.clear(isClearBg)
  canvasData.value = null
  isEmpty.value = true
}

// 保存画板 格式、质量、是否以文件导出
const getBase64Data = (format = 'png', quality = 0.95) => {
  if (signaturePadRef.value && !signaturePadRef.value.isEmpty()) {
    return signaturePadRef.value.toDataURL(`image/${format}`, quality)
  } else {
    return null
  }
}

// 导出画板图片
const getImageFile = (format = 'png', quality = 0.95) => {
  const link = document.createElement('a')
  link.href = signaturePadRef.value.toDataURL(`image/${format}`, quality)
  link.download = `signature.${format}`
  link.click()
}

// 设置背景图
const setBgImage = (url) => {
  const img = new Image()
  img.crossOrigin = 'anonymous'
  img.onload = () => {
    bgImage.value = img
    if (signaturePadRef.value) {
      signaturePadRef.value.clear()
      if (canvasData.value) {
        signaturePadRef.value.fromData(canvasData.value)
      }
    }
  }
  img.src = url
}

const handleResize = () => {
  ratio = window.devicePixelRatio || 1
  // 保存当前画板
  if (signaturePadRef.value) {
    canvasData.value = signaturePadRef.value.toData()
  }
  // 重新初始化画布
  initSignaturePad()
}

onMounted(() => {
  nextTick(() => {
    initSignaturePad()
    initBgImage()
    window.addEventListener('resize', handleResize)
  })
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})

// 画板是否空白
const isCanvasEmpty = () => {
  isEmpty.value = signaturePadRef.value.isEmpty()
  return isEmpty.value
}

defineExpose({
  clear,
  getBase64Data,
  getImageFile,
  setBgImage,
  isCanvasEmpty,
  signaturePadRef,
})
</script>

<style scoped>
.signature-canvas-container {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
}
</style>

如果有需要用到的可以试试我这个组件,直接npm i @npm_lx/signature-pad-for-vue3 然后 import SignaturePad from '@npm_lx/signature-pad-for-vue3' 使用即可,如果不清楚可以npm或者github上面看下实例,使用起来很简单。后续有时间再继续看看有没有什么其他功能可以增加一下。

相关推荐
weixin_382395232 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__2 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.3 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~4 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华5 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子7 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅7 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni7 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学9 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端