前端手写签名板终极实现指南:从零到专业级解决方案
2025-02-20 本文已影响0人
前端御书房
一、基础实现:原生Canvas签名板
1.1 核心三件套搭建
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>电子签名板</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<canvas id="signCanvas"></canvas>
<div class="toolbar">
<button id="clearBtn">清除画布</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
/* style.css */
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #f0f3f5;
}
.container {
width: 800px;
background: white;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
canvas {
border: 2px dashed #ccc;
touch-action: none; /* 禁用浏览器默认触摸行为 */
}
.toolbar {
margin-top: 15px;
display: flex;
gap: 10px;
}
button {
padding: 8px 20px;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: opacity 0.3s;
}
button:hover {
opacity: 0.8;
}
// script.js基础版
class SignaturePad {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.isDrawing = false;
this.lastX = 0;
this.lastY = 0;
this.initCanvas();
this.bindEvents();
}
initCanvas() {
const ratio = Math.max(window.devicePixelRatio || 1, 1);
this.canvas.width = this.canvas.offsetWidth * ratio;
this.canvas.height = this.canvas.offsetHeight * ratio;
this.ctx.scale(ratio, ratio);
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
}
bindEvents() {
const events = {
mouse: ['mousedown', 'mousemove', 'mouseup', 'mouseout'],
touch: ['touchstart', 'touchmove', 'touchend']
};
events.mouse.forEach(event => {
this.canvas.addEventListener(event, this.handleMouse.bind(this));
});
events.touch.forEach(event => {
this.canvas.addEventListener(event, this.handleTouch.bind(this));
});
}
handleMouse(e) {
const { offsetX: x, offsetY: y } = e;
this.processEvent(e.type, x, y);
}
handleTouch(e) {
e.preventDefault();
const touch = e.touches[0];
const rect = this.canvas.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
this.processEvent(e.type, x, y);
}
processEvent(type, x, y) {
switch(type) {
case 'mousedown':
case 'touchstart':
this.startDrawing(x, y);
break;
case 'mousemove':
case 'touchmove':
this.draw(x, y);
break;
case 'mouseup':
case 'mouseout':
case 'touchend':
this.stopDrawing();
break;
}
}
startDrawing(x, y) {
this.isDrawing = true;
[this.lastthis.lastX, this.lastY] = [x, y];
}
draw(x, y) {
if (!this.isDrawing) return;
this.ctx.beginPath();
this.ctx.moveTo(this.lastX, this.lastY);
this.ctx.lineTo(x, y);
this.ctx.stroke();
[this.lastX, this.lastY] = [x, y];
}
stopDrawing() {
this.isDrawing = false;
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
}
// 初始化
const canvas = document.getElementById('signCanvas');
const signaturePad = new SignaturePad(canvas);
document.getElementById('clearBtn').addEventListener('click', () => signaturePad.clear());
二、专业级功能实现
2.1 多设备兼容方案
// 在构造函数中添加
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = '#000';
// 新增配置方法
setConfig(config) {
Object.keys(config).forEach(key => {
if (key in this.ctx) {
this.ctx[key] = config[key];
}
});
}
// 示例:设置笔刷宽度和颜色
signaturePad.setConfig({
lineWidth: 3,
strokeStyle: '#2196F3'
});
2.2 撤销/重做功能实现
class HistoryManager {
constructor() {
this.undoStack = [];
this.redoStack = [];
}
saveState(canvas) {
this.undoStack.push(canvas.toDataURL());
this.redoStack = [];
}
undo(canvas) {
if (this.undoStack.length < 2) return;
this.redoStack.push(this.undoStack.pop());
this.restoreState(canvas);
}
redo(canvas) {
if (!this.redoStack.length) return;
this.undoStack.push(this.redoStack.pop());
this.restoreState(canvas);
}
restoreState(canvas) {
const img = new Image();
img.src = this.undoStack[this.undoStack.length - 1];
img.onload = () => {
canvas.getContext('2d').drawImage(img, 0, 0);
};
}
}
// 使用示例
const history = new HistoryManager();
canvas.addEventListener('mouseup', () => history.saveState(canvas));
document.getElementById('undoBtn').addEventListener('click', () => history.undo(canvas));
document.getElementById('redoBtn').addEventListener('click', () => history.redo(canvas));
2.3 高性能导出方案
function exportSignature(format = 'png', quality = 0.92) {
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
// 创建高精度画布
const ratio = window.devicePixelRatio;
tempCanvas.width = canvas.width * ratio;
tempCanvas.height = canvas.height * ratio;
// 填充白色背景
tempCtx.fillStyle = 'white';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// 绘制原始内容
tempCtx.drawImage(canvas, 0, 0);
// 生成下载链接
const link = document.createElement('a');
link.download = `signature.${format}`;
link.href = tempCanvas.toDataURL(`image/${format}`, quality);
link.click();
}
// 使用示例
document.getElementById('exportPng').addEventListener('click', () => exportSignature('png'));
document.getElementById('exportJpeg').addEventListener('click', () => exportSignature('jpeg'));
三、企业级解决方案
3.1 使用signature_pad增强功能
<!-- 引入官方库 -->
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"></script>
<!-- 增强功能界面 -->
<div class="advanced-tools">
<input type="color" id="colorPicker" value="#000000">
<input type="range" id="widthRange" min="1" max="20" value="2">
<button id="undoBtn">撤销</button>
<button id="redoBtn">重做</button>
</div>
class ProfessionalSignaturePad {
constructor(canvas) {
this.signaturePad = new SignaturePad(canvas, {
minWidth: 1,
maxWidth: 20,
penColor: "#000",
backgroundColor: "rgba(0,0,0,0)"
});
this.initTools();
this.handleResize();
}
initTools() {
// 颜色选择
document.getElementById('colorPicker').addEventListener('input', (e) => {
this.signaturePad.penColor = e.target.value;
});
// 笔刷粗细
document.getElementById('widthRange').addEventListener('input', (e) => {
this.signaturePad.minWidth = parseInt(e.target.value);
this.signaturePad.maxWidth = parseInt(e.target.value) + 2;
});
// 窗口大小变化监听
window.addEventListener('resize', this.handleResize.bind(this));
}
handleResize() {
const ratio = Math.max(window.devicePixelRatio || 1, 1);
const canvas = this.signaturePad.canvas;
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext('2d').scale(ratio, ratio);
this.signaturePad.clear(); // 防止缩放变形
}
}
四、生产环境最佳实践
4.1 性能优化方案
// 节流处理
function throttle(fn, delay) {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) return;
lastCall = now;
return fn(...args);
}
}
// 使用防抖保存状态
const saveStateDebounced = debounce(() => {
localStorage.setItem('signature', canvas.toDataURL());
}, 1000);
canvas.addEventListener('mouseup', saveStateDebounced);
4.2 数据安全处理
// 生成数字指纹
function generateFingerprint() {
const hash = crypto.createHash('sha256');
const data = canvas.toDataURL();
return hash.update(data).digest('hex');
}
// 数据加密存储
function saveEncrypted(data) {
const key = CryptoJS.enc.Utf8.parse('16bytesSecretKey');
const encrypted = CryptoJS.AES.encrypt(data, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}
五、应用场景与扩展
5.1 典型应用场景
graph TD
A[电子签约] --> B(在线合同)
A --> C(医疗同意书)
A --> D(物流签收)
E[数字绘图] --> F(设计手稿)
E --> G(教学白板)
E --> H(会议纪要)
I[数据采集] --> J(调查问卷)
I --> K(政府审批)
I --> L(银行开户)
5.2 扩展功能建议
- 笔迹识别:集成TensorFlow.js实现笔迹分析
- 时间戳服务:对接区块链存证平台
- 多页签名:支持PDF多页连续签名
- 生物认证:集成指纹/人脸识别验证