H5实现签字板签名功能

7 篇文章 1 订阅

前言:H5时常需要实现给C端用户签名的功能,包括但不限于实现签名、重签、手机自动旋转样式保持等功能,以下是基于Taro框架开发的H5页面实现,非 Taro 框架的普通 H5 将 View 标签换成 div 即可

一、用到的技术库

  1. 签字库:react-signature-canvas
  2. 主流React Hooks 库:ahooks

二、组件具体实现

解决H5横竖屏样式问题,主要还是通过CSS媒体查询实现两套样式处理横屏和竖屏的样式

index.tsx

import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import SignatureCanvas from 'react-signature-canvas';
import { useSize } from 'ahooks';
import { View } from '@tarojs/components';
import { rotateImg, Toast, throttle } from './utils';
import './index.less';

interface IProps {
  visible: boolean;
  setVisible?: (e: boolean) => void;
  signText?: string;
  onChange?: (e?) => void; // 画布change回调
  onSure: (e?) => void; // 按钮 - 确定的回调
  onCancel?: (e?) => void; // 按钮 - 取消的回调
  onResign?: (e?) => void; // 按钮 - 重签的回调
}

/**
 * 签字版组件
 * 其他H5要用,View标签改为div即可
 * @param props
 * @returns
 */
const SignatureBoard = (props: IProps) => {
  const {
    visible,
    setVisible,
    signText = '请在此空白处签下您的姓名',
    onChange,
    onSure,
    onCancel,
    onResign
  } = props;
  const [signTip, setSignTip] = useState(signText);
  const sigCanvasRef = useRef<SignatureCanvas | null>(null);
  const canvasContainer = useRef<HTMLElement>(null);
  const compContainer = useRef<HTMLElement>(null);
  const compSize = useSize(compContainer);
  const canvasSize = useSize(canvasContainer);
  const [isLandscape, setIsLandscape] = useState<boolean>(false); // 是否横屏

  // 提示的文字数组,为了在竖屏的情况下,每个字样式旋转
  const tipText = useMemo(() => {
    return signTip?.split('') || [];
  }, [signTip]);

  // 重签
  const clearSign = useCallback(() => {
    setSignTip(signText);
    sigCanvasRef?.current?.clear();
    onResign?.();
  }, [onResign, signText]);

  // 取消
  const cancelSign = useCallback(() => {
    setSignTip(signText);
    sigCanvasRef?.current?.clear();
    onCancel?.();
    setVisible?.(false);
  }, [onCancel, setVisible, signText]);

  // 确定
  const sureSign = useCallback(() => {
    const pointGroupArray = sigCanvasRef?.current?.toData();
    if (pointGroupArray.flat().length < 30) {
      Toast({ title: '请使用正楷字签名', rotate: isLandscape ? 0 : 90 });
      return;
    }
    if (isLandscape) {
      // 横屏不旋转图片
      onSure?.(sigCanvasRef.current.toDataURL());
    } else {
      rotateImg(sigCanvasRef?.current?.toDataURL(), result => onSure?.(result), 270);
    }
    setVisible?.(false);
  }, [isLandscape, onSure, setVisible]);

  // 由于 onorientationchange 只能判断自动旋转,无法判断手动旋转,因此不选择监听 orientationchange;
  // 监听 resize 可以实现,比较宽高即可判断是否横屏,即宽大于高就是横屏状态,与下面为了方便使用 ahooks 的 useSize 思想一致
  useEffect(() => {
    // 如果宽度大于高度,就表示是在横屏状态
    if ((compSize?.width ?? 0) > (compSize?.height ?? 1)) {
      // console.log('横屏状态');
      setIsLandscape(true);
      clearSign();
    } else {
      // console.log('竖屏状态');
      setIsLandscape(false);
      clearSign();
    }
  }, [clearSign, compSize?.height, compSize?.width]);

  if (!visible) return null;

  return (
    <View ref={compContainer} className='signature-board-comp' onClick={e => e.stopPropagation()}>
      <View className='sign-board-btns'>
        <View className='board-btn' onClick={cancelSign}>
          <View className='board-btn-text'>取消</View>
        </View>
        <View className='board-btn' onClick={clearSign}>
          <View className='board-btn-text'>重签</View>
        </View>
        <View className='board-btn confirm-btn' onClick={throttle(sureSign, 2000)}>
          <View className='board-btn-text'>确定</View>
        </View>
      </View>
      <View className='sign-board' ref={canvasContainer}>
        <SignatureCanvas
          penColor='#000' // 笔刷颜色
          minWidth={1} // 笔刷粗细
          maxWidth={1}
          canvasProps={{
            id: 'sigCanvas',
            width: canvasSize?.width,
            height: canvasSize?.height, // 画布尺寸
            className: 'sigCanvas'
          }}
          ref={sigCanvasRef}
          onBegin={() => setSignTip('')}
          onEnd={() => {
            onChange?.(sigCanvasRef?.current?.toDataURL());
          }}
        />
        {signTip && (
          <div className='SignatureTips'>
            {tipText &&
              tipText?.map((item, index) => (
                <View key={`${index.toString()}`} className='tip-text'>
                  {item}
                </View>
              ))}
          </div>
        )}
      </View>
    </View>
  );
};

export default SignatureBoard;

inde.less

  • 注意:由于浏览器顶部地址栏和底部工具栏的原因,fixed固定定位之后的宽高要使用100%,而不是 100vh 或 100vw
@media screen and (orientation: portrait) {
  /*竖屏 css*/
  .signature-board-comp {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 9;
    display: flex;
    flex-wrap: nowrap;
    align-items: stretch;
    box-sizing: border-box;
    width: 100%;
    height: 100%;
    padding: 48px 52px 48px 0px;
    background-color: #ffffff;

    .sign-board-btns {
      display: flex;
      flex-direction: column;
      flex-wrap: nowrap;
      align-items: center;
      justify-content: flex-end;
      box-sizing: border-box;
      width: 142px;
      padding: 0px 24px;

      .board-btn {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 96px;
        height: 312px;
        margin-top: 32px;
        border: 1px solid #181916;
        border-radius: 8px;
        opacity: 1;

        &:active {
          opacity: 0.9;
        }

        .board-btn-text {
          color: #181916;
          font-size: 30px;
          transform: rotate(90deg);
        }
      }

      .confirm-btn {
        color: #ffffff;
        background: #181916;

        .board-btn-text {
          color: #ffffff;
        }
      }
    }

    .sign-board {
      position: relative;
      flex: 1;

      .sigCanvas {
        width: 100%;
        height: 100%;
        background: #f7f7f7;
        border-radius: 10px;
      }
      .SignatureTips {
        position: absolute;
        top: 0;
        left: 50%;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        width: 50px;
        height: 100%;
        color: #a2a0a8;
        font-size: 46px;
        transform: translateX(-50%);
        pointer-events: none;

        .tip-text {
          line-height: 50px;
          transform: rotate(90deg);
        }
      }
    }
  }
}

@media screen and (orientation: landscape) {
  /*横屏 css*/
  .signature-board-comp {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 9;
    display: flex;
    flex-direction: column-reverse;
    flex-wrap: nowrap;
    box-sizing: border-box;
    width: 100%;
    height: 100%;
    padding: 0px 48px 0px 48px;
    background-color: #ffffff;

    .sign-board-btns {
      display: flex;
      flex-wrap: nowrap;
      flex-wrap: nowrap;
      align-items: center;
      justify-content: flex-end;
      box-sizing: border-box;
      width: 100%;
      height: 20vh;
      padding: 12px 0px;

      .board-btn {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 156px;
        height: 100%;
        max-height: 48px;
        margin-left: 16px;
        border: 1px solid #181916;
        border-radius: 4px;
        opacity: 1;

        &:active {
          opacity: 0.9;
        }

        .board-btn-text {
          color: #181916;
          font-size: 15px;
        }
      }

      .confirm-btn {
        color: #ffffff;
        background: #181916;

        .board-btn-text {
          color: #ffffff;
        }
      }
    }

    .sign-board {
      position: relative;
      flex: 1;
      box-sizing: border-box;
      height: 80vh;

      .sigCanvas {
        box-sizing: border-box;
        width: 100%;
        height: 80vh;
        background: #f7f7f7;
        border-radius: 5px;
      }
      .SignatureTips {
        position: absolute;
        top: 0;
        left: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        box-sizing: border-box;
        width: 100%;
        height: 100%;
        color: #a2a0a8;
        font-size: 23px;
        pointer-events: none;
      }
    }
  }
}

utils.ts

/**
 * canvas绘制图片旋转,默认旋转270度
 * @param src
 * @param callback
 * @param deg
 */
export const rotateImg = (src, callback, deg = 270) => {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  const image = new Image();
  image.crossOrigin = 'anonymous';
  image.src = src;
  image.onload = function () {
    const imgW = image.width; // 图片宽度
    const imgH = image.height; // 图片高度
    const size = imgW > imgH ? imgW : imgH; // canvas初始大小
    canvas.width = size * 2;
    canvas.height = size * 2;
    // 裁剪坐标
    const cutCoor = {
      sx: size,
      sy: size - imgW,
      ex: size + imgH,
      ey: size + imgW
    };
    ctx?.translate(size, size);
    ctx?.rotate((deg * Math.PI) / 180);
    // drawImage向画布上绘制图片
    ctx?.drawImage(image, 0, 0);
    // getImageData() 复制画布上指定矩形的像素数据
    const imgData = ctx?.getImageData(cutCoor.sx, cutCoor.sy, cutCoor.ex, cutCoor.ey);
    canvas.width = imgH;
    canvas.height = imgW;
    // putImageData() 将图像数据放回画布
    ctx?.putImageData(imgData as any, 0, 0);
    callback(canvas.toDataURL());
  };
};

/**
 * 自定义 Toast
 * @param param
 * title: 内容
 * duration:toast持续时间,单位 毫秒
 * rotate:toast可旋转的角度度数,单位 deg
 */
export const Toast = ({ title, duration = 2000, rotate = 0 }) => {
  const _duration = isNaN(duration) ? 2000 : duration;
  const divElement = document.createElement('div');
  divElement.innerHTML = title;
  divElement.style.cssText = `position: fixed;top: 50%;left: 50%;z-index: 99;display: flex;
  align-items: center;justify-content: center;padding: 12px 16px;color: #ffffff;font-size: 15px;
  background: rgba(0, 0, 0, 0.8);border-radius: 4px;transform: translate(-50%, -50%) rotate(${rotate}deg);`;
  document.body.appendChild(divElement);
  setTimeout(() => {
    const d = 0.1;
    divElement.style.transition = `opacity ${d}s linear 0s`;
    divElement.style.opacity = '0';
    setTimeout(function () {
      document.body.removeChild(divElement);
    }, d * 1000);
  }, _duration);
};

/**
 * 节流
 * @param fn
 * @param delay
 * @returns
 */
export const throttle = (fn, delay = 2000) => {
  let timer: any = null;
  return (...args) => {
    if (timer) {
      return;
    } // 如果上一次没结束,直接return
    fn.call(undefined, ...args);
    timer = setTimeout(() => {
      timer = null; // 标记结束
    }, delay);
  };
};

三、实现效果

在这里插入图片描述

实现手写签名保存图片,可以使用HTML5的Canvas元素和JavaScript编写代码。 以下是一个简单的实现步骤: 1. 在HTML文件中创建一个Canvas元素,并设置它的宽度和高度。 ```html <canvas id="signature" width="400" height="200"></canvas> ``` 2. 在JavaScript文件中获取Canvas元素和它的上下文。 ```javascript var canvas = document.getElementById("signature"); var ctx = canvas.getContext("2d"); ``` 3. 添加事件监听器,以便在用户在Canvas上绘制时捕获鼠标或触摸事件。 ```javascript canvas.addEventListener("mousedown", startDrawing); canvas.addEventListener("mousemove", draw); canvas.addEventListener("mouseup", stopDrawing); canvas.addEventListener("touchstart", startDrawing); canvas.addEventListener("touchmove", draw); canvas.addEventListener("touchend", stopDrawing); ``` 4. 实现绘制函数。在mousedown或touchstart事件发生时,将Canvas上下文的起始点设置为鼠标或触摸位置。在mousemove或touchmove事件发生时,使用Canvas上下文的lineTo方法绘制直线。最后,在mouseup或touchend事件发生时,停止绘制。 ```javascript var isDrawing = false; function startDrawing(e) { isDrawing = true; ctx.beginPath(); var x = e.pageX - canvas.offsetLeft; var y = e.pageY - canvas.offsetTop; ctx.moveTo(x, y); } function draw(e) { if (!isDrawing) return; var x = e.pageX - canvas.offsetLeft; var y = e.pageY - canvas.offsetTop; ctx.lineTo(x, y); ctx.stroke(); } function stopDrawing() { isDrawing = false; } ``` 5. 添加保存功能。将Canvas元素转换为图片并将其保存到本地。 ```javascript function saveImage() { var image = canvas.toDataURL("image/png").replace("image/png", "image/octet-stream"); var link = document.createElement("a"); link.download = "signature.png"; link.href = image; link.click(); } ``` 最后,在HTML文件中添加一个保存按钮,并在JavaScript文件中添加事件监听器。 ```html <button onclick="saveImage()">保存签名</button> ``` 这样就可以实现手写签名保存图片的功能了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值