1. 为什么需要在前端实现手写签名?
在现代Web应用中,手写签名功能的需求场景远比我们想象的广泛。从电子合同签署、审批流程确认到医疗处方电子化,甚至是教育领域的作业批改,都需要用户能够直接在页面上留下自己的笔迹。传统的解决方案往往要求用户打印文件、手写签名后再扫描上传,这种流程不仅繁琐,还严重影响了工作效率。
Vue作为当前最流行的前端框架之一,其响应式特性和丰富的生态为我们实现手写签名提供了绝佳的基础。通过Canvas API与Vue的结合,我们可以创建出高性能、可定制化的签名组件,完美解决上述痛点。
实际开发中发现,签名功能最关键的三个指标是:笔迹流畅度、数据压缩率和跨设备兼容性。这三个指标直接决定了用户体验的好坏。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础实现:Canvas与Vue的完美结合
2.1 搭建基础签名画板
首先创建一个基础的Vue组件SignaturePad.vue:
html复制<template>
<div class="signature-container">
<canvas
ref="canvas"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="stopDrawing"
@mouseleave="stopDrawing"
@touchstart="startDrawing"
@touchmove="draw"
@touchend="stopDrawing"
></canvas>
<div class="controls">
<button @click="clearCanvas">清除</button>
<button @click="saveSignature">保存</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isDrawing: false,
lastX: 0,
lastY: 0,
ctx: null
}
},
mounted() {
this.initCanvas()
},
methods: {
initCanvas() {
const canvas = this.$refs.canvas
// 适配高清屏幕
const ratio = window.devicePixelRatio || 1
canvas.width = canvas.offsetWidth * ratio
canvas.height = canvas.offsetHeight * ratio
canvas.style.width = `${canvas.offsetWidth}px`
canvas.style.height = `${canvas.offsetHeight}px`
this.ctx = canvas.getContext('2d')
this.ctx.scale(ratio, ratio)
this.ctx.lineJoin = 'round'
this.ctx.lineCap = 'round'
this.ctx.lineWidth = 2
this.ctx.strokeStyle = '#000'
},
startDrawing(e) {
this.isDrawing = true
const pos = this.getPosition(e)
this.lastX = pos.x
this.lastY = pos.y
},
draw(e) {
if (!this.isDrawing) return
const pos = this.getPosition(e)
this.ctx.beginPath()
this.ctx.moveTo(this.lastX, this.lastY)
this.ctx.lineTo(pos.x, pos.y)
this.ctx.stroke()
this.lastX = pos.x
this.lastY = pos.y
},
stopDrawing() {
this.isDrawing = false
},
clearCanvas() {
this.ctx.clearRect(
0,
0,
this.$refs.canvas.width,
this.$refs.canvas.height
)
},
saveSignature() {
const dataUrl = this.$refs.canvas.toDataURL('image/png')
this.$emit('save', dataUrl)
},
getPosition(e) {
const canvas = this.$refs.canvas
const rect = canvas.getBoundingClientRect()
return {
x: (e.clientX || e.touches[0].clientX) - rect.left,
y: (e.clientY || e.touches[0].clientY) - rect.top
}
}
}
}
</script>
<style scoped>
.signature-container {
border: 1px solid #ddd;
position: relative;
}
canvas {
background: #fff;
width: 100%;
height: 300px;
display: block;
}
.controls {
margin-top: 10px;
text-align: right;
}
button {
margin-left: 8px;
padding: 6px 12px;
}
</style>
这段代码实现了签名功能的核心逻辑:
- 通过Canvas API进行绘图操作
- 同时支持鼠标和触摸事件
- 处理了高清屏幕的适配问题
- 提供了清除和保存功能
2.2 在父组件中使用签名组件
html复制<template>
<div>
<h1>请签署您的名字</h1>
<SignaturePad @save="handleSave" />
<div v-if="signatureData">
<h3>签名预览:</h3>
<img :src="signatureData" alt="签名" />
</div>
</div>
</template>
<script>
import SignaturePad from './SignaturePad.vue'
export default {
components: {
SignaturePad
},
data() {
return {
signatureData: null
}
},
methods: {
handleSave(dataUrl) {
this.signatureData = dataUrl
// 这里可以添加将签名数据发送到后端的逻辑
}
}
}
</script>
3. 性能优化与高级功能实现
3.1 解决移动端延迟问题
移动设备上的触摸事件通常会有300ms的延迟,这会导致签名出现断点。我们可以通过以下方式优化:
javascript复制// 在SignaturePad.vue的mounted中添加
document.addEventListener('touchmove', (e) => {
if (this.isDrawing) {
e.preventDefault()
}
}, { passive: false })
同时,我们可以实现"预测绘制"来消除视觉上的延迟:
javascript复制draw(e) {
if (!this.isDrawing) return
const pos = this.getPosition(e)
this.ctx.beginPath()
this.ctx.moveTo(this.lastX, this.lastY)
// 预测下一个点的位置
const nextX = pos.x + (pos.x - this.lastX) * 0.3
const nextY = pos.y + (pos.y - this.lastY) * 0.3
this.ctx.lineTo(nextX, nextY)
this.ctx.stroke()
this.lastX = pos.x
this.lastY = pos.y
}
3.2 笔迹美化算法
原始签名数据往往比较粗糙,我们可以通过算法进行平滑处理:
javascript复制// 在data中添加
data() {
return {
points: [],
// ...其他数据
}
}
// 修改draw方法
draw(e) {
if (!this.isDrawing) return
const pos = this.getPosition(e)
this.points.push({ x: pos.x, y: pos.y })
// 每收集5个点进行一次平滑绘制
if (this.points.length >= 5) {
this.drawSmoothLine()
this.points = this.points.slice(-2) // 保留最后两个点用于连接
}
}
// 添加平滑绘制方法
drawSmoothLine() {
if (this.points.length < 3) return
const ctx = this.ctx
const midPoints = []
// 计算中点
for (let i = 1; i < this.points.length; i++) {
midPoints.push({
x: (this.points[i-1].x + this.points[i].x) / 2,
y: (this.points[i-1].y + this.points[i].y) / 2
})
}
ctx.beginPath()
ctx.moveTo(this.points[0].x, this.points[0].y)
// 使用二次贝塞尔曲线平滑连接
for (let i = 1; i < midPoints.length; i++) {
const prevMid = midPoints[i-1]
const curr = this.points[i]
ctx.quadraticCurveTo(
prevMid.x, prevMid.y,
(prevMid.x + curr.x) / 2, (prevMid.y + curr.y) / 2
)
}
ctx.stroke()
}
3.3 压力感应支持
对于支持压感笔的设备,我们可以通过PointerEvent API获取压力值:
javascript复制initCanvas() {
// ...原有代码
canvas.addEventListener('pointerdown', this.startDrawing)
canvas.addEventListener('pointermove', this.draw)
canvas.addEventListener('pointerup', this.stopDrawing)
canvas.addEventListener('pointerleave', this.stopDrawing)
},
getPosition(e) {
const canvas = this.$refs.canvas
const rect = canvas.getBoundingClientRect()
// 设置线宽基于压力值
if (e.pressure) {
this.ctx.lineWidth = 2 + e.pressure * 4
}
return {
x: (e.clientX || e.touches[0].clientX) - rect.left,
y: (e.clientY || e.touches[0].clientY) - rect.top
}
}
4. 企业级解决方案与安全考虑
4.1 签名数据的安全处理
直接使用Canvas的toDataURL方法生成的Base64字符串体积较大,我们可以进行优化:
javascript复制saveSignature() {
const canvas = this.$refs.canvas
// 转换为JPEG格式并降低质量
const dataUrl = canvas.toDataURL('image/jpeg', 0.5)
// 进一步压缩
this.compressImage(dataUrl).then(compressed => {
this.$emit('save', compressed)
})
},
compressImage(dataUrl) {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// 按比例缩小
const maxWidth = 800
const ratio = maxWidth / img.width
canvas.width = maxWidth
canvas.height = img.height * ratio
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
resolve(canvas.toDataURL('image/jpeg', 0.7))
}
img.src = dataUrl
})
}
4.2 签名验证与防篡改
为了保证签名的法律效力,我们需要实现防篡改机制:
- 在保存签名时记录时间戳和用户信息
- 使用加密哈希算法生成签名指纹
- 将元数据与签名图像一起存储
javascript复制async saveSignature() {
const canvas = this.$refs.canvas
const dataUrl = canvas.toDataURL('image/png')
// 生成签名指纹
const signatureData = {
image: await this.compressImage(dataUrl),
timestamp: new Date().toISOString(),
userId: this.$store.state.user.id,
hash: await this.generateHash(dataUrl)
}
this.$emit('save', signatureData)
},
async generateHash(dataUrl) {
// 移除Base64前缀
const base64Data = dataUrl.replace(/^data:image\/\w+;base64,/, '')
const binaryStr = atob(base64Data)
const bytes = new Uint8Array(binaryStr.length)
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i)
}
// 使用SHA-256生成哈希
const hashBuffer = await crypto.subtle.digest('SHA-256', bytes)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
4.3 使用第三方库的考量
虽然我们可以自己实现签名功能,但在生产环境中,使用成熟的第三方库可能更可靠。以下是几个流行选择:
- signature_pad:轻量级、支持压感
- vue-signature:专为Vue封装的签名组件
- signature-canvas:功能丰富,支持多种输出格式
以signature_pad为例的集成方式:
bash复制npm install signature_pad @types/signature_pad
html复制<template>
<div>
<canvas ref="canvas"></canvas>
<button @click="save">保存</button>
<button @click="clear">清除</button>
</div>
</template>
<script>
import SignaturePad from 'signature_pad'
export default {
data() {
return {
signaturePad: null
}
},
mounted() {
const canvas = this.$refs.canvas
this.signaturePad = new SignaturePad(canvas, {
minWidth: 1,
maxWidth: 3,
penColor: '#000',
backgroundColor: '#fff'
})
this.handleResize()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
},
methods: {
handleResize() {
const canvas = this.$refs.canvas
const ratio = Math.max(window.devicePixelRatio || 1, 1)
canvas.width = canvas.offsetWidth * ratio
canvas.height = canvas.offsetHeight * ratio
canvas.getContext('2d').scale(ratio, ratio)
this.signaturePad.clear() // 防止缩放后签名变形
},
save() {
if (this.signaturePad.isEmpty()) {
alert('请先签名')
return
}
const data = this.signaturePad.toDataURL()
this.$emit('save', data)
},
clear() {
this.signaturePad.clear()
}
}
}
</script>
5. 实际应用中的经验与坑点
5.1 跨浏览器兼容性问题
不同浏览器对Canvas API的实现有细微差异,特别是在以下方面:
- 触摸事件的处理
- 图像导出格式支持
- 高清屏幕适配
解决方案:
- 使用特性检测而非浏览器嗅探
- 为不支持某些API的浏览器提供polyfill
- 在保存前检查图像是否为空
javascript复制// 检查签名是否为空
isSignatureEmpty() {
const canvas = this.$refs.canvas
const ctx = canvas.getContext('2d')
const pixelBuffer = new Uint32Array(
ctx.getImageData(0, 0, canvas.width, canvas.height).data.buffer
)
return !pixelBuffer.some(color => color !== 0)
}
5.2 移动端特殊处理
移动设备上的签名体验需要特别注意:
- 防止页面滚动与签名冲突
- 处理触摸事件的cancel情况
- 适配不同屏幕尺寸
javascript复制// 在移动设备上阻止页面滚动
startDrawing(e) {
if (e.type === 'touchstart') {
e.preventDefault()
document.body.style.overflow = 'hidden'
}
// ...原有逻辑
},
stopDrawing(e) {
if (e.type === 'touchend') {
document.body.style.overflow = ''
}
// ...原有逻辑
}
5.3 性能优化技巧
当签名区域较大或签名时间较长时,可能会遇到性能问题:
- 使用离屏Canvas:将绘制操作先在离屏Canvas上执行,再复制到主Canvas
- 分段绘制:将长线条分成小段绘制,避免单次绘制操作过大
- 使用requestAnimationFrame:对绘制操作进行节流
javascript复制// 使用离屏Canvas优化
initCanvas() {
const canvas = this.$refs.canvas
// ...原有初始化代码
// 创建离屏Canvas
this.offscreenCanvas = document.createElement('canvas')
this.offscreenCanvas.width = canvas.width
this.offscreenCanvas.height = canvas.height
this.offscreenCtx = this.offscreenCanvas.getContext('2d')
// 应用相同的样式设置
},
draw(e) {
if (!this.isDrawing) return
const pos = this.getPosition(e)
// 在离屏Canvas上绘制
this.offscreenCtx.beginPath()
this.offscreenCtx.moveTo(this.lastX, this.lastY)
this.offscreenCtx.lineTo(pos.x, pos.y)
this.offscreenCtx.stroke()
// 复制到主Canvas
this.ctx.drawImage(this.offscreenCanvas, 0, 0)
this.lastX = pos.x
this.lastY = pos.y
}
5.4 签名笔迹的真实性保障
在法律场景下,需要确保签名是实时绘制而非上传的图片:
- 记录绘制过程中的时间戳和速度变化
- 分析笔迹特征(起笔、收笔、压力变化)
- 使用AI辅助验证笔迹真实性
javascript复制// 记录绘制数据
data() {
return {
// ...其他数据
drawingData: {
timestamps: [],
points: [],
speeds: []
}
}
},
startDrawing(e) {
// ...原有逻辑
this.drawingData = {
timestamps: [Date.now()],
points: [{x: this.lastX, y: this.lastY}],
speeds: [0]
}
},
draw(e) {
if (!this.isDrawing) return
const now = Date.now()
const pos = this.getPosition(e)
// 计算速度
const lastPoint = this.drawingData.points[this.drawingData.points.length - 1]
const distance = Math.sqrt(
Math.pow(pos.x - lastPoint.x, 2) +
Math.pow(pos.y - lastPoint.y, 2)
)
const timeDiff = now - this.drawingData.timestamps[this.drawingData.timestamps.length - 1]
const speed = timeDiff > 0 ? distance / timeDiff : 0
// 保存数据
this.drawingData.timestamps.push(now)
this.drawingData.points.push({x: pos.x, y: pos.y})
this.drawingData.speeds.push(speed)
// ...原有绘制逻辑
}
6. 扩展功能与业务集成
6.1 签名位置固定与模板背景
在实际业务中,经常需要在特定位置签名:
html复制<template>
<div class="document-container">
<div class="document-content">
<!-- 合同内容 -->
<div class="signature-placeholder">
<canvas ref="canvas"></canvas>
</div>
</div>
</div>
</template>
<script>
export default {
methods: {
initCanvas() {
const canvas = this.$refs.canvas
// 设置固定尺寸
canvas.width = 300
canvas.height = 100
// 添加引导文字
this.ctx.font = '16px Arial'
this.ctx.fillStyle = '#999'
this.ctx.fillText('请在区域内签名', 10, 30)
this.ctx.strokeStyle = '#ccc'
this.ctx.strokeRect(0, 0, canvas.width, canvas.height)
}
}
}
</script>
<style>
.document-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: white;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.document-content {
line-height: 1.6;
}
.signature-placeholder {
margin-top: 50px;
border-top: 1px dashed #ccc;
padding-top: 20px;
}
</style>
6.2 多页签名与批量处理
对于需要多页签名的场景:
html复制<template>
<div>
<div v-for="(page, index) in pages" :key="index">
<h3>第 {{ index + 1 }} 页</h3>
<SignaturePad
:ref="`signaturePad${index}`"
@save="savePageSignature(index, $event)"
/>
</div>
<button @click="submitAll">提交所有签名</button>
</div>
</template>
<script>
export default {
data() {
return {
pages: ['page1', 'page2', 'page3'], // 实际业务中可能是文档页面
signatures: []
}
},
methods: {
savePageSignature(index, data) {
this.$set(this.signatures, index, data)
},
async submitAll() {
if (this.signatures.length !== this.pages.length) {
alert('请完成所有页面签名')
return
}
// 验证所有签名是否为空
const isEmpty = await Promise.all(
this.pages.map((_, i) =>
this.$refs[`signaturePad${i}`][0].isSignatureEmpty()
)
)
if (isEmpty.some(empty => empty)) {
alert('有页面签名为空')
return
}
// 提交到后端
const result = await this.$api.submitSignatures({
documentId: this.documentId,
signatures: this.signatures
})
if (result.success) {
this.$router.push('/success')
}
}
}
}
</script>
6.3 与PDF文档集成
将签名嵌入PDF文档的完整流程:
- 前端收集签名图像
- 发送到后端服务
- 后端使用PDF库(如pdf-lib)将签名嵌入指定位置
- 返回最终PDF文档
前端代码示例:
javascript复制async submitSignature() {
const signatureData = this.signaturePad.toDataURL()
try {
const { pdfUrl } = await this.$api.embedSignature({
documentId: this.documentId,
signature: signatureData,
position: {
page: 1,
x: 100,
y: 200,
width: 200,
height: 80
}
})
// 下载或预览PDF
window.open(pdfUrl, '_blank')
} catch (error) {
console.error('签名嵌入失败:', error)
alert('签名处理失败,请重试')
}
}
6.4 签名回显与验证
在需要重新显示已签名文档的场景:
html复制<template>
<div>
<h2>已签署文档</h2>
<div class="document-viewer">
<!-- 文档内容 -->
<div
v-for="signature in document.signatures"
:key="signature.id"
:style="{
position: 'absolute',
left: `${signature.position.x}px`,
top: `${signature.position.y}px`,
width: `${signature.position.width}px`,
height: `${signature.position.height}px`
}"
>
<img :src="signature.data" alt="签名" />
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
document: {
// 从后端获取的文档数据
signatures: [
{
id: 1,
data: 'data:image/png;base64,...',
position: { x: 100, y: 200, width: 200, height: 80 },
signedAt: '2023-07-20T10:30:00Z',
signer: '张三'
}
]
}
}
}
}
</script>
<style>
.document-viewer {
position: relative;
background: white;
padding: 20px;
min-height: 800px;
border: 1px solid #ddd;
}
</style>
7. 测试与质量保障
7.1 自动化测试策略
对于签名组件,我们需要特别关注以下测试点:
-
功能测试:
- 能否正常开始、持续和结束绘制
- 清除功能是否正常工作
- 保存功能是否返回正确的数据格式
-
性能测试:
- 长时间绘制是否会导致卡顿
- 大尺寸画布下的内存使用情况
-
跨平台测试:
- 不同浏览器下的表现一致性
- 移动设备上的触摸体验
使用Jest和Testing Library的测试示例:
javascript复制import { render, fireEvent } from '@testing-library/vue'
import SignaturePad from '@/components/SignaturePad.vue'
describe('SignaturePad', () => {
it('should start drawing on mousedown', async () => {
const { container, emitted } = render(SignaturePad)
const canvas = container.querySelector('canvas')
await fireEvent.mouseDown(canvas, { clientX: 10, clientY: 10 })
await fireEvent.mouseMove(canvas, { clientX: 20, clientY: 20 })
await fireEvent.mouseUp(canvas)
expect(emitted().save).toBeTruthy()
})
it('should clear canvas when clear button clicked', async () => {
const { container, getByText } = render(SignaturePad)
const canvas = container.querySelector('canvas')
const ctx = canvas.getContext('2d')
jest.spyOn(ctx, 'clearRect')
await fireEvent.click(getByText('清除'))
expect(ctx.clearRect).toHaveBeenCalled()
})
})
7.2 可视化回归测试
对于Canvas绘制结果,可以使用jest-image-snapshot进行可视化比对:
javascript复制import { toMatchImageSnapshot } from 'jest-image-snapshot'
expect.extend({ toMatchImageSnapshot })
it('should draw signature correctly', async () => {
const { container } = render(SignaturePad)
const canvas = container.querySelector('canvas')
// 模拟绘制操作
await fireEvent.mouseDown(canvas, { clientX: 10, clientY: 10 })
await fireEvent.mouseMove(canvas, { clientX: 20, clientY: 20 })
await fireEvent.mouseUp(canvas)
// 获取Canvas图像数据
const imageData = canvas.toDataURL()
expect(imageData).toMatchImageSnapshot()
})
7.3 移动端真机测试要点
在实际移动设备上需要特别验证:
- 触摸绘制是否流畅
- 多指触摸是否会导致意外行为
- 不同屏幕密度下的显示效果
- 内存占用情况(长时间使用是否会导致崩溃)
可以使用BrowserStack或Sauce Labs等云测试平台进行多设备测试。
8. 部署与性能监控
8.1 生产环境优化
- 代码分割:将签名组件单独打包
- 懒加载:在需要时才加载签名功能
- CDN加速:对签名图像资源使用CDN
javascript复制// 懒加载签名组件
const SignaturePad = () => import('@/components/SignaturePad.vue')
export default {
components: {
SignaturePad
}
}
8.2 性能监控
在组件中添加性能埋点:
javascript复制mounted() {
this.loadTime = performance.now()
// 监听绘制性能
this.performanceEntries = []
const observer = new PerformanceObserver((list) => {
this.performanceEntries.push(...list.getEntries())
})
observer.observe({ entryTypes: ['measure'] })
},
methods: {
startDrawing() {
performance.mark('drawStart')
// ...原有逻辑
},
stopDrawing() {
performance.mark('drawEnd')
performance.measure('drawing', 'drawStart', 'drawEnd')
// ...原有逻辑
// 上报性能数据
if (this.performanceEntries.length > 0) {
this.$track('signaturePerformance', {
loadTime: performance.now() - this.loadTime,
drawDuration: this.performanceEntries
.filter(e => e.name === 'drawing')
.map(e => e.duration),
device: navigator.userAgent
})
}
}
}
8.3 错误监控
捕获Canvas相关错误并上报:
javascript复制initCanvas() {
try {
const canvas = this.$refs.canvas
if (!canvas || !canvas.getContext) {
throw new Error('Canvas not supported')
}
const ctx = canvas.getContext('2d')
if (!ctx) {
throw new Error('Could not get 2D context')
}
// ...原有初始化代码
} catch (error) {
console.error('Canvas初始化失败:', error)
this.$trackError('canvasInitFailed', {
error: error.message,
userAgent: navigator.userAgent
})
this.$emit('error', error)
}
}
9. 可访问性改进
9.1 ARIA属性添加
为辅助技术提供必要信息:
html复制<canvas
ref="canvas"
aria-label="签名区域"
aria-describedby="signatureInstructions"
role="img"
></canvas>
<p id="signatureInstructions">
请在此区域使用鼠标或手指绘制您的签名
</p>
9.2 键盘操作支持
为无法使用鼠标的用户添加键盘支持:
javascript复制methods: {
handleKeyDown(e) {
if (!this.isDrawing) return
const step = 5
let newX = this.lastX
let newY = this.lastY
switch (e.key) {
case 'ArrowUp':
newY -= step
break
case 'ArrowDown':
newY += step
break
case 'ArrowLeft':
newX -= step
break
case 'ArrowRight':
newX += step
break
default:
return
}
this.ctx.beginPath()
this.ctx.moveTo(this.lastX, this.lastY)
this.ctx.lineTo(newX, newY)
this.ctx.stroke()
this.lastX = newX
this.lastY = newY
}
},
mounted() {
window.addEventListener('keydown', this.handleKeyDown)
},
beforeDestroy() {
window.removeEventListener('keydown', this.handleKeyDown)
}
9.3 高对比度模式支持
css复制@media (forced-colors: active) {
canvas {
forced-color-adjust: none;
background-color: canvas;
border: 1px solid canvasText;
}
button {
background-color: buttonFace;
color: buttonText;
border: 1px solid buttonBorder;
}
}
10. 未来可能的扩展方向
10.1 基于AI的签名验证
利用机器学习模型分析:
- 笔迹特征(压力变化、速度变化)
- 签名形状的独特性
- 与预留签名的相似度比对
javascript复制async verifySignature(signatureData) {
try {
const response = await fetch('https://api.signature-verification.com/v1/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
image: signatureData.image,
drawingData: signatureData.drawingData,
referenceSignature: this.user.referenceSignature
})
})
const result = await response.json()
return result.score > 0.8 // 相似度阈值
} catch (error) {
console.error('验证失败:', error)
return false
}
}
10.2 区块链存证
将签名哈希和元数据上链,确保不可篡改:
javascript复制async storeOnBlockchain(signatureData) {
const { hash, timestamp } = signatureData
const transaction = {
from: this.blockchainAccount,
to: '0xSignatureContractAddress',
data: web3.eth.abi.encodeFunctionCall({
name: 'storeSignature',
type: 'function',
inputs: [{
type: 'string',
name: 'hash'
}, {
type: 'uint256',
name: 'timestamp'
}]
}, [hash, Math.floor(new Date(timestamp).getTime() / 1000)])
}
try {
const receipt = await web3.eth.sendTransaction(transaction)
return receipt.transactionHash
} catch (error) {
console.error('区块链存证失败:', error)
throw error
}
}
10.3 3D签名效果
通过记录压感和倾斜度数据,实现更真实的3D签名效果:
javascript复制// 使用PointerEvent的扩展属性
startDrawing(e) {
if (e.pointerType === 'pen') {
this.penData = {
tiltX: e.tiltX,
tiltY: e.tiltY,
pressure: e.pressure
}
}
// ...原有逻辑
},
draw(e) {
if (e.pointerType === 'pen') {
// 根据倾斜度和压力计算线宽和透明度
const tiltEffect = Math.max(
0.5,
1 - Math.sqrt(e.tiltX ** 2 + e.tiltY ** 2) / 90
)
this.ctx.lineWidth = 2 + e.pressure * 6 * tiltEffect
this.ctx.strokeStyle = `rgba(0, 0, 0, ${0.5 + e.pressure * 0.5})`
}
// ...原有绘制逻辑
}
10.4 协同签名
实现多人同时在同一文档上签名的功能:
javascript复制// 使用WebSocket实时同步签名数据
initCollaboration() {
this.socket = new WebSocket('wss://api.example.com/signatures')
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === 'drawing') {
this.drawRemoteSignature(data)
}
}
},
drawRemoteSignature(data) {
// 使用不同颜色显示其他用户的签名
this.ctx.strokeStyle = data.userColor
this.ctx.beginPath()
this.ctx.moveTo(data.lastX, data.lastY)
this.ctx.lineTo(data.x, data.y)
this.ctx.stroke()
},
// 发送本地绘制数据
sendDrawingData(pos) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({
type: 'drawing',
documentId: this.documentId,
lastX: this.lastX,
lastY: this.lastY,
x: pos.x,
y: pos.y,
userColor: this.userColor
}))
}
}
