美文网首页JavaScript
前端手写签名板终极实现指南:从零到专业级解决方案

前端手写签名板终极实现指南:从零到专业级解决方案

作者: 前端御书房 | 来源:发表于2025-02-20 20:05 被阅读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 扩展功能建议

  1. 笔迹识别:集成TensorFlow.js实现笔迹分析
  2. 时间戳服务:对接区块链存证平台
  3. 多页签名:支持PDF多页连续签名
  4. 生物认证:集成指纹/人脸识别验证

相关文章

  • Vue Canvas 实现电子签名 手写板

    最近再做移动端电子签名,Vue+Canvas实现,移动端、PC端均可,也可以从github下载 。我在做这个功能的...

  • 实现半年目标

    实现半年目标,自己手写春联 从零基础到自己手写春联 有事者事竟成 再练一个月,到时候好好写

  • 前端性能优化:手写实现节流防抖

    前端性能优化:手写实现节流防抖 本文首发于 前端性能优化:手写实现节流防抖[https://gitee.com/r...

  • 【微信小程序canvas】实现小程序手写板用户签名(附代码)

    【微信小程序canvas】实现小程序手写板用户签名(附代码) 工作中公司业务需要的微信小程序用户签字功能 先看效果...

  • UItouch事件被tableview拦截

    最近做项目用到了手写签名的功能,手写签名的view是通过UItouch相关方法画线实现的,因为页面UI是列表形式,...

  • Android手写板签名

    演示效果: Github代码地址 实现步骤 1.监听按下滑动手势,使用path绘制路径2.加入清除画板功能3.画笔...

  • Vue使用手写签名组件Sign-Canvas

    前言 使用【sign-canvas】组件二次封装自定义手写签名组件,一个基于canvas开发封装实现的通用手写签名...

  • H5 Canvas 签名板

    签名板介绍 在最近的一个项目中,最后的一个功能是实现一个签名板供客户签名使用。这需要用到canvas来实现。 我将...

  • 前端异常监控平台搭建

    一、资料收集 前端异常监控系统的落地前端异常监控解决方案研究从初级到高级前端---异常监控系统的搭建从无到有<前端...

  • ionic 3.X实现手写板签名

    一般店子里的手持设备会有手写签名,实现如下 1.下载组件 npm install angular2-signatu...

网友评论

    本文标题:前端手写签名板终极实现指南:从零到专业级解决方案

    本文链接:https://www.haomeiwen.com/subject/invppjtx.html