二次封装一个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上面看下实例,使用起来很简单。后续有时间再继续看看有没有什么其他功能可以增加一下。

相关推荐
w***76552 小时前
vue2和vue3的区别
前端·javascript·vue.js
n 55!w !1082 小时前
静态网页作业
前端·css·css3
缘木之鱼2 小时前
CTFshow __Web应用安全与防护 第一章
前端·安全·渗透·ctf·ctfshow
我是一只小青蛙8882 小时前
Python文件组织:路径抽象到安全归档
java·服务器·前端
奔跑的web.2 小时前
TypeScript 泛型完全指南:写法、四大应用场景与高级用法
前端·javascript·vue.js·typescript
SevgiliD2 小时前
文本溢出省略并Tooltip组件在表单和表格内的使用
前端·javascript·vue.js
DEMO派2 小时前
Web 视频录制方案解析,轻松实现录屏!
前端·javascript·音视频
1024小神2 小时前
css主题theme变量切换实现原理学习记录
前端·css·学习
wuhen_n2 小时前
TypeScript工作流深度解析:从.ts到.js发生了什么?
前端·javascript·typescript