Skip to content

数字签名

Vue 3 数字签名、手写签名组件。

vue3-signature

基本信息

特点

  • ✅ 支持触摸和鼠标绘制
  • ✅ 自定义画笔颜色和粗细
  • ✅ 导出为图片(PNG/JPEG)
  • ✅ 撤销和清空功能
  • ✅ 响应式画布
  • ✅ TypeScript 支持
  • ✅ 轻量级,无外部依赖

安装

bash
pnpm add vue3-signature
bash
bun add vue3-signature
bash
npm install vue3-signature --save
bash
yarn add vue3-signature

全局注册

typescript
// main.ts
import { createApp } from 'vue'
import Vue3Signature from 'vue3-signature'
import App from './App.vue'

const app = createApp(App)
app.use(Vue3Signature)
app.mount('#app')

局部注册

vue
<script setup lang="ts">
import Vue3Signature from 'vue3-signature'
</script>

基础用法

保存签名
清空
vue
<template>
  <div>
    <Vue3Signature ref="signatureRef" />
    <div class="mt-4 flex gap-3">
      <button 
        @click="handleSave" 
        class="flex items-center gap-2 px-5 py-2.5 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white font-medium rounded-lg shadow-sm hover:shadow-md transition-all duration-200"
      >
        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
        </svg>
        保存签名
      </button>
      <button 
        @click="handleClear" 
        class="flex items-center gap-2 px-5 py-2.5 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 text-gray-700 font-medium rounded-lg shadow-sm hover:shadow-md transition-all duration-200"
      >
        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
        </svg>
        清空
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useTemplateRef } from 'vue'
import Vue3Signature from 'vue3-signature'

const signatureRef = useTemplateRef('signatureRef')

const handleSave = () => {
  if (!signatureRef.value) return
  
  const data = signatureRef.value.save()
  if (data) {
    console.log('签名数据:', data)
    alert('签名已保存到控制台')
  } else {
    alert('请先签名')
  }
}

const handleClear = () => {
  if (!signatureRef.value) return
  signatureRef.value.clear()
}
</script>

自定义画笔样式

vue
<template>
  <Vue3Signature 
    ref="signatureRef"
    :sigOption="options"
  />
</template>

<script setup lang="ts">
import { useTemplateRef } from 'vue'
import Vue3Signature from 'vue3-signature'

const signatureRef = useTemplateRef('signatureRef')
const options = {
  penColor: '#0abab5',      // 画笔颜色
  minWidth: 2,              // 最小线宽
  maxWidth: 4,              // 最大线宽
  backgroundColor: '#ffffff' // 背景颜色
}
</script>

自定义画布尺寸

vue
<template>
  <Vue3Signature 
    ref="signatureRef"
    :w="'700px'"
    :h="'200px'"
  />
</template>

<script setup lang="ts">
import { useTemplateRef } from 'vue'
import Vue3Signature from 'vue3-signature'

const signatureRef = useTemplateRef('signatureRef')
</script>

导出不同格式

导出 PNG
导出 JPEG
vue
<template>
  <div>
    <Vue3Signature ref="signatureRef" />
    <div class="mt-4 flex gap-3">
      <button 
        @click="savePNG" 
        class="flex items-center gap-2 px-5 py-2.5 bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white font-medium rounded-lg shadow-sm hover:shadow-md transition-all duration-200"
      >
        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
        </svg>
        导出 PNG
      </button>
      <button 
        @click="saveJPEG" 
        class="flex items-center gap-2 px-5 py-2.5 bg-green-500 hover:bg-green-600 active:bg-green-700 text-white font-medium rounded-lg shadow-sm hover:shadow-md transition-all duration-200"
      >
        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
        </svg>
        导出 JPEG
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useTemplateRef } from 'vue'
import Vue3Signature from 'vue3-signature'

const signatureRef = useTemplateRef('signatureRef')

// 导出为 PNG(默认)
const savePNG = () => {
  if (!signatureRef.value) return
  
  const data = signatureRef.value.save()
  if (data) {
    // data 是 base64 格式的 PNG 图片
    downloadImage(data, 'signature.png')
  } else {
    alert('请先签名')
  }
}

// 导出为 JPEG
const saveJPEG = () => {
  if (!signatureRef.value) return
  
  const data = signatureRef.value.save('image/jpeg')
  if (data) {
    downloadImage(data, 'signature.jpg')
  } else {
    alert('请先签名')
  }
}

const downloadImage = (dataUrl: string, filename: string) => {
  const link = document.createElement('a')
  link.href = dataUrl
  link.download = filename
  link.click()
}
</script>

撤销功能

撤销
清空
vue
<template>
  <div>
    <Vue3Signature ref="signatureRef" />
    <div class="mt-4 flex gap-3">
      <button 
        @click="handleUndo" 
        class="flex items-center gap-2 px-5 py-2.5 bg-amber-500 hover:bg-amber-600 active:bg-amber-700 text-white font-medium rounded-lg shadow-sm hover:shadow-md transition-all duration-200"
      >
        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path>
        </svg>
        撤销
      </button>
      <button 
        @click="handleClear" 
        class="flex items-center gap-2 px-5 py-2.5 bg-gray-100 hover:bg-gray-200 active:bg-gray-300 text-gray-700 font-medium rounded-lg shadow-sm hover:shadow-md transition-all duration-200"
      >
        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
        </svg>
        清空
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useTemplateRef } from 'vue'
import Vue3Signature from 'vue3-signature'

const signatureRef = useTemplateRef('signatureRef')

const handleUndo = () => {
  if (!signatureRef.value) return
  signatureRef.value.undo()
}

const handleClear = () => {
  if (!signatureRef.value) return
  signatureRef.value.clear()
}
</script>

禁用状态

禁用签名
vue
<template>
  <div>
    <Vue3Signature 
      ref="signatureRef"
      :disabled="disabled"
    />
    
    <div class="mt-4 flex justify-center">
      <div 
        @click="disabled = !disabled" 
        class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-300 cursor-pointer select-none"
        :class="disabled ? 'bg-white dark:bg-gray-800 border border-brand text-brand dark:text-brand-light hover:bg-brand/10 dark:hover:bg-brand-light/10 hover:border-brand-hover hover:shadow active:bg-brand/20' : 'bg-white dark:bg-gray-800 border border-red-500 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-950 hover:border-red-600 hover:shadow active:bg-red-200'"
      >
        <svg v-if="disabled" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
        </svg>
        <svg v-else class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 715.636 5.636m12.728 12.728L5.636 5.636"></path>
        </svg>
        {{ disabled ? '启用签名' : '禁用签名' }}
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import Vue3Signature from 'vue3-signature'

const signatureRef = useTemplateRef('signatureRef')
const disabled = ref(false)
</script>

完整配置选项

vue
<script setup lang="ts">
import { ref } from 'vue'
import Vue3Signature from 'vue3-signature'

const signatureRef = ref()

const options = {
  penColor: '#000000',           // 画笔颜色
  minWidth: 2,                   // 最小线宽
  maxWidth: 4,                   // 最大线宽
  backgroundColor: '#ffffff',    // 背景颜色
  velocityFilterWeight: 0.7,     // 速度过滤权重
  dotSize: 1                     // 点的大小
}
</script>

<template>
  <Vue3Signature 
    ref="signatureRef"
    :w="'100%'"
    :h="'300px'"
    :sigOption="options"
    :disabled="false"
  />
</template>

签名表单示例

点击查看完整代码
vue
<script setup lang="ts">
import { ref, reactive } from 'vue'
import Vue3Signature from 'vue3-signature'

const signatureRef = ref()

const formData = reactive({
  name: '',
  email: '',
  signature: ''
})

const handleSubmit = () => {
  const { isEmpty, data } = signatureRef.value.save()
  
  if (!formData.name || !formData.email) {
    alert('请填写姓名和邮箱')
    return
  }
  
  if (isEmpty) {
    alert('请先签名')
    return
  }
  
  formData.signature = data
  console.log('提交数据:', formData)
  alert('提交成功!')
}

const handleClear = () => {
  signatureRef.value.clear()
}
</script>

<template>
  <div class="max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-lg">
    <h2 class="text-2xl font-bold mb-6">签名表单</h2>
    
    <div class="space-y-4 mb-6">
      <div>
        <label class="block text-sm font-medium mb-2">姓名</label>
        <input 
          v-model="formData.name"
          type="text" 
          class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
          placeholder="请输入姓名"
        />
      </div>
      
      <div>
        <label class="block text-sm font-medium mb-2">邮箱</label>
        <input 
          v-model="formData.email"
          type="email" 
          class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
          placeholder="请输入邮箱"
        />
      </div>
      
      <div>
        <label class="block text-sm font-medium mb-2">签名</label>
        <div class="border-2 border-dashed border-gray-300 rounded-lg p-2">
          <Vue3Signature 
            ref="signatureRef"
            :w="'100%'"
            :h="'200px'"
            :sigOption="{
              penColor: '#1e40af',
              minWidth: 2,
              maxWidth: 4,
              backgroundColor: '#f9fafb'
            }"
          />
        </div>
        <button 
          @click="handleClear"
          class="mt-2 text-sm text-gray-600 hover:text-gray-800"
        >
          清空签名
        </button>
      </div>
    </div>
    
    <button 
      @click="handleSubmit"
      class="w-full px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
    >
      提交
    </button>
  </div>
</template>

合同签署示例

点击查看完整代码
vue
<script setup lang="ts">
import { ref, reactive } from 'vue'
import Vue3Signature from 'vue3-signature'

const signatureRef = ref()
const agreed = ref(false)

const contract = {
  title: '服务协议',
  content: `
    1. 本协议是您与本公司之间关于使用本服务的法律协议。
    2. 您在使用本服务前,应当认真阅读并遵守本协议。
    3. 您点击同意或签署本协议,即视为您完全理解并同意接受本协议的全部内容。
    4. 本公司有权根据需要修改本协议条款。
  `
}

const handleSign = () => {
  if (!agreed.value) {
    alert('请先阅读并同意协议')
    return
  }
  
  const { isEmpty, data } = signatureRef.value.save()
  
  if (isEmpty) {
    alert('请先签名')
    return
  }
  
  console.log('签名数据:', data)
  alert('签署成功!')
}

const handleClear = () => {
  signatureRef.value.clear()
}
</script>

<template>
  <div class="max-w-4xl mx-auto p-6">
    <div class="bg-white rounded-lg shadow-lg overflow-hidden">
      <!-- 协议内容 -->
      <div class="p-6 border-b">
        <h1 class="text-3xl font-bold mb-4">{{ contract.title }}</h1>
        <div class="prose max-w-none">
          <pre class="whitespace-pre-wrap text-gray-700">{{ contract.content }}</pre>
        </div>
      </div>
      
      <!-- 签名区域 -->
      <div class="p-6 bg-gray-50">
        <div class="mb-4">
          <label class="flex items-center space-x-2">
            <input 
              v-model="agreed"
              type="checkbox" 
              class="w-4 h-4 text-blue-600"
            />
            <span class="text-sm text-gray-700">
              我已阅读并同意以上协议内容
            </span>
          </label>
        </div>
        
        <div class="mb-4">
          <label class="block text-sm font-medium mb-2">请在下方签名</label>
          <div class="bg-white border-2 border-gray-300 rounded-lg p-2">
            <Vue3Signature 
              ref="signatureRef"
              :w="'100%'"
              :h="'200px'"
              :disabled="!agreed"
              :sigOption="{
                penColor: '#000000',
                minWidth: 2,
                maxWidth: 3,
                backgroundColor: '#ffffff'
              }"
            />
          </div>
        </div>
        
        <div class="flex space-x-4">
          <button 
            @click="handleSign"
            :disabled="!agreed"
            class="flex-1 px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
          >
            确认签署
          </button>
          <button 
            @click="handleClear"
            class="px-6 py-3 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
          >
            清空
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

移动端适配

点击查看完整代码
vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import Vue3Signature from 'vue3-signature'

const signatureRef = ref()
const canvasWidth = ref('100%')
const canvasHeight = ref('300px')

const updateCanvasSize = () => {
  if (window.innerWidth < 768) {
    canvasHeight.value = '200px'
  } else {
    canvasHeight.value = '300px'
  }
}

onMounted(() => {
  updateCanvasSize()
  window.addEventListener('resize', updateCanvasSize)
})

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

const handleSave = () => {
  const { isEmpty, data } = signatureRef.value.save()
  if (!isEmpty) {
    // 保存签名
    console.log('签名数据:', data)
  }
}
</script>

<template>
  <div class="p-4">
    <div class="bg-white rounded-lg shadow-lg p-4">
      <h3 class="text-lg font-semibold mb-4">移动端签名</h3>
      
      <div class="border-2 border-gray-300 rounded-lg overflow-hidden">
        <Vue3Signature 
          ref="signatureRef"
          :w="canvasWidth"
          :h="canvasHeight"
          :sigOption="{
            penColor: '#1e40af',
            minWidth: 2,
            maxWidth: 4,
            backgroundColor: '#f9fafb'
          }"
        />
      </div>
      
      <div class="mt-4 flex space-x-2">
        <button 
          @click="handleSave"
          class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg active:bg-blue-700"
        >
          保存
        </button>
        <button 
          @click="signatureRef.clear()"
          class="px-4 py-2 bg-gray-500 text-white rounded-lg active:bg-gray-600"
        >
          清空
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
/* 移动端优化 */
@media (max-width: 768px) {
  .p-4 {
    padding: 1rem;
  }
}
</style>

API 参考

Props

属性类型默认值说明
wString'100%'画布宽度
hString'100%'画布高度
sigOptionObject{}签名配置选项
disabledBooleanfalse是否禁用

sigOption 配置

属性类型默认值说明
penColorString'rgb(0, 0, 0)'画笔颜色
minWidthNumber0.5最小线宽
maxWidthNumber2.5最大线宽
backgroundColorString'rgb(255, 255, 255)'背景颜色
velocityFilterWeightNumber0.7速度过滤权重
dotSizeNumber-点的大小

Methods

方法参数返回值说明
savetype?: string{ isEmpty: boolean, data: string }保存签名,返回 base64 数据
clear--清空画布
undo--撤销上一步
isEmpty-boolean判断画布是否为空
fromDataURLdataUrl: string-从 base64 数据加载签名

常见应用场景

1. 电子合同签署

vue
<template>
  <div class="contract-sign">
    <div class="contract-content">
      <!-- 合同内容 -->
    </div>
    <Vue3Signature ref="signatureRef" />
    <button @click="submitContract">确认签署</button>
  </div>
</template>

2. 快递签收

vue
<template>
  <div class="delivery-sign">
    <h3>请签收</h3>
    <Vue3Signature 
      ref="signatureRef"
      :sigOption="{ penColor: '#000' }"
    />
    <button @click="confirmDelivery">确认签收</button>
  </div>
</template>

3. 意见反馈签名

vue
<template>
  <div class="feedback-form">
    <textarea v-model="feedback" placeholder="请输入意见"></textarea>
    <Vue3Signature ref="signatureRef" />
    <button @click="submitFeedback">提交反馈</button>
  </div>
</template>

4. 考勤签到

vue
<template>
  <div class="attendance-sign">
    <div class="user-info">
      <p>姓名: {{ userName }}</p>
      <p>时间: {{ currentTime }}</p>
    </div>
    <Vue3Signature ref="signatureRef" />
    <button @click="signIn">签到</button>
  </div>
</template>

最佳实践

1. 验证签名

vue
<script setup lang="ts">
const validateSignature = () => {
  const { isEmpty, data } = signatureRef.value.save()
  
  if (isEmpty) {
    alert('请先签名')
    return false
  }
  
  // 可以添加更多验证逻辑
  // 例如:检查签名复杂度、大小等
  
  return true
}
</script>

2. 保存到服务器

vue
<script setup lang="ts">
const saveToServer = async () => {
  const { isEmpty, data } = signatureRef.value.save()
  
  if (isEmpty) return
  
  try {
    const response = await fetch('/api/signature', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ signature: data })
    })
    
    if (response.ok) {
      alert('保存成功')
    }
  } catch (error) {
    console.error('保存失败:', error)
  }
}
</script>

3. 响应式设计

vue
<script setup lang="ts">
import { ref, computed } from 'vue'

const windowWidth = ref(window.innerWidth)

const canvasSize = computed(() => {
  if (windowWidth.value < 640) {
    return { width: '100%', height: '200px' }
  } else if (windowWidth.value < 1024) {
    return { width: '100%', height: '250px' }
  } else {
    return { width: '100%', height: '300px' }
  }
})
</script>

<template>
  <Vue3Signature 
    :w="canvasSize.width"
    :h="canvasSize.height"
  />
</template>

4. 添加水印

vue
<script setup lang="ts">
const addWatermark = () => {
  const { isEmpty, data } = signatureRef.value.save()
  
  if (isEmpty) return
  
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')
  const img = new Image()
  
  img.onload = () => {
    canvas.width = img.width
    canvas.height = img.height
    
    // 绘制原签名
    ctx.drawImage(img, 0, 0)
    
    // 添加水印
    ctx.font = '12px Arial'
    ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'
    ctx.fillText('签署时间: ' + new Date().toLocaleString(), 10, 20)
    
    // 获取带水印的图片
    const watermarkedData = canvas.toDataURL()
    console.log('带水印的签名:', watermarkedData)
  }
  
  img.src = data
}
</script>

注意事项

  1. 性能优化: 对于大尺寸画布,建议限制画布大小以提升性能
  2. 移动端适配: 确保在移动设备上有良好的触摸体验
  3. 数据安全: 签名数据应通过 HTTPS 传输并妥善存储
  4. 用户体验: 提供清晰的签名指引和反馈
  5. 浏览器兼容: 测试不同浏览器的兼容性
  6. 图片质量: 根据需求选择合适的导出格式和质量