实现H5在线签名功能

1,751 阅读3分钟

需求:

最近我们项目增加了一个需求,需要实现在线电子签名功能,效果如下:

企业微信截图_16643621558565.png

功能点:

  1. 用户可以在画板进行签名
  2. 具备清空功能
  3. 提交生成base64格式图片
  4. 生成图片可自定义宽高
  5. 生成图片可自定义旋转角度
  6. “此区域内请使用正楷签名“要在签名区Canvas底部,但是又不影响绘图区

解决思路:

  • 底层是基于Canvas进行的画图,自己实现需要时间,先找现成的库进行需求开发吧。

  • 引入signature_pad,github地址,能把1,2,3功能点完成。参考:代码1

  • 自定义宽高,根据配置,传入宽高数据,重新生成base64图片,参考:代码2

  • 自定义旋转角度,根据配置,传入旋转角度(这里只能生成垂直跟水平角度,不然不好计算),重新生成base64图片,参考:代码3

  • “此区域内请使用正楷签名“用css position布局,placeholder 在Index=-1, 外层canvas设置透明,能实现效果,参考:代码4

代码1:

import { ReactNode, createElement, useRef, useEffect } from "react";
import SignaturePad from "signature_pad";

  useEffect(() => {
        const jpgBackground = props.backgroundColor || "#fff";
        signaturePad.current = new SignaturePad(canvasRef.current, {
            minWidth: 0.5,
            maxWidth: 2.5,
            penColor: penColor || "black",
            backgroundColor: imgType === "jpg" ? jpgBackground : "transparent"
        });

        // 等初始化函数后再进行canvas宽高赋值, 必须要进行这一步,不然canvas跟画布大小不一致
        setTimeout(() => {
            initCanvasSize();
        }, 5)
  }, []);
  
   const initCanvasSize = (): void => {
        const { clientWidth, clientHeight } = canvasRef.current;
        canvasRef.current.width = clientWidth;
        canvasRef.current.height = clientHeight;
        signaturePad.current.clear();
    };

    // 清除
    const handleCanvasClean = (): void => {
        signaturePad.current.clear();
    };
    
    // 提交
  const handleCanvasSubmit = async (): Promise<void> => {
    if (signaturePad.current.isEmpty()) {
        message.warning(emptySubmitTip || "请签名!");
        return;
    }

    let dataURL = signaturePad.current.toDataURL();

    if (rotate) {
        dataURL = await rotateBase64Img("png", dataURL, Number(rotate));
    }
    if (imageWidth || imageHeight) {
        dataURL = await proportionBase64Img("png", dataURL, Number(imageWidth), Number(imageHeight));
    }
    return dataURL;
};

// jsx
<div className="signature-canvas-container">
    <div className="canvas-tips">{canvasPlaceholder || ""}</div>
    <canvas className="signature-canvas" ref={canvasRef} />
</div>

代码2:

  • 根据宽高压缩图, 根据原来的图重新生成一张新的图
  • 使用API:ctx.drawImage(image, 0, 0, 原始图片宽, 原始图片高, 0, 0, 目标图片宽, 目标图片高)
  • 缺点:尚未解决的 压缩太小,canvas位图会出现失真的情况。暂时没找到办法处理
// 根据宽高压缩图片
    const proportionBase64Img = async (type: string, src: string, w?: number, h?: number): Promise<string> => {
        const canvas = document.createElement("canvas");
        const ctx: any = canvas.getContext("2d");

        const image = new Image();
        image.crossOrigin = "anonymous";
        image.src = src;
        let imgWidth;
        let imgHeight;

        return new Promise(resolve => {
            image.onload = function () {
                imgWidth = image.width;
                imgHeight = image.height;

                if (w) {
                    canvas.width = w;
                    canvas.height = (imgHeight * w) / imgWidth;
                    ctx.drawImage(image, 0, 0, imgWidth, imgHeight, 0, 0, w, (imgHeight * w) / imgWidth);
                } else if (h) {
                    canvas.height = h;
                    canvas.width = (imgWidth * h) / imgHeight;
                    ctx.drawImage(image, 0, 0, imgWidth, imgHeight, 0, 0, (imgWidth * h) / imgHeight, h);
                }
                resolve(canvas.toDataURL(type));
            };
        });
    };

代码3:

// 旋转图片
const rotateBase64Img = async (type: string, src: string, edg: number): Promise<string> => {
        const canvas = document.createElement("canvas");
        const ctx: any = canvas.getContext("2d");

        let imgW; // 图片宽度
        let imgH; // 图片高度
        let size; // canvas初始大小
        if (edg % 90 !== 0) {
            console.error("旋转角度必须是90的倍数!");
            return src;
        }
        const quadrant = (edg / 90) % 4; // 旋转象限
        const cutCoor = { sx: 0, sy: 0, ex: 0, ey: 0 }; // 裁剪坐标
        const image = new Image();
        image.crossOrigin = "anonymous";
        image.src = src;

        return new Promise(resolve => {
            image.onload = function () {
                imgW = image.width;
                imgH = image.height;
                size = imgW > imgH ? imgW : imgH;
                canvas.width = size * 2;
                canvas.height = size * 2;
                switch (quadrant) {
                    case 0:
                        cutCoor.sx = size;
                        cutCoor.sy = size;
                        cutCoor.ex = size + imgW;
                        cutCoor.ey = size + imgH;
                        break;
                    case 1:
                        cutCoor.sx = size - imgH;
                        cutCoor.sy = size;
                        cutCoor.ex = size;
                        cutCoor.ey = size + imgW;
                        break;
                    case 2:
                        cutCoor.sx = size - imgW;
                        cutCoor.sy = size - imgH;
                        cutCoor.ex = size;
                        cutCoor.ey = size;
                        break;
                    case 3:
                        cutCoor.sx = size;
                        cutCoor.sy = size - imgW;
                        cutCoor.ex = size + imgH;
                        cutCoor.ey = size + imgW;
                        break;
                }
                ctx.translate(size, size);
                ctx.rotate((edg * Math.PI) / 180);
                ctx.drawImage(image, 0, 0);
                const imgData = ctx.getImageData(cutCoor.sx, cutCoor.sy, cutCoor.ex, cutCoor.ey);
                if (quadrant % 2 === 0) {
                    canvas.width = imgW;
                    canvas.height = imgH;
                } else {
                    canvas.width = imgH;
                    canvas.height = imgW;
                }
                ctx.putImageData(imgData, 0, 0);
                resolve(canvas.toDataURL(type));
            };
        });
    };

代码4:

.canvas-tips {

            width0.24rem;

            font-size0.24rem;

            font-family: PingFang SC-Medium, PingFang SC;

            font-weight: bold;

            colorrgba(0000.12);

            -webkit-background-clip: text;

            position: absolute;

            top50%;

            left50%;

            transformtranslate(-50%, -50%);

            z-index: -1;
     }