数字签名
Vue 3 数字签名、手写签名组件。
vue3-signature
基本信息
- 简介: 轻量级 Vue 3 手写签名组件
- npm: https://www.npmjs.com/package/vue3-signature
- GitHub: https://github.com/WangShayne/vue3-signature
特点
- ✅ 支持触摸和鼠标绘制
- ✅ 自定义画笔颜色和粗细
- ✅ 导出为图片(PNG/JPEG)
- ✅ 撤销和清空功能
- ✅ 响应式画布
- ✅ TypeScript 支持
- ✅ 轻量级,无外部依赖
安装
bash
pnpm add vue3-signaturebash
bun add vue3-signaturebash
npm install vue3-signature --savebash
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
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
w | String | '100%' | 画布宽度 |
h | String | '100%' | 画布高度 |
sigOption | Object | {} | 签名配置选项 |
disabled | Boolean | false | 是否禁用 |
sigOption 配置
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
penColor | String | 'rgb(0, 0, 0)' | 画笔颜色 |
minWidth | Number | 0.5 | 最小线宽 |
maxWidth | Number | 2.5 | 最大线宽 |
backgroundColor | String | 'rgb(255, 255, 255)' | 背景颜色 |
velocityFilterWeight | Number | 0.7 | 速度过滤权重 |
dotSize | Number | - | 点的大小 |
Methods
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
save | type?: string | { isEmpty: boolean, data: string } | 保存签名,返回 base64 数据 |
clear | - | - | 清空画布 |
undo | - | - | 撤销上一步 |
isEmpty | - | boolean | 判断画布是否为空 |
fromDataURL | dataUrl: 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>注意事项
- 性能优化: 对于大尺寸画布,建议限制画布大小以提升性能
- 移动端适配: 确保在移动设备上有良好的触摸体验
- 数据安全: 签名数据应通过 HTTPS 传输并妥善存储
- 用户体验: 提供清晰的签名指引和反馈
- 浏览器兼容: 测试不同浏览器的兼容性
- 图片质量: 根据需求选择合适的导出格式和质量