H5实现签字版签名功能

前言:H5时常需要给C端用户签名的功能,以下是基于Taro框架开发的H5页面实现

一、用到的技术库

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

二、组件具体实现

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

index.tsx

typescript 复制代码
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import Taro from '@tarojs/taro';
import SignatureCanvas from 'react-signature-canvas';
import { useSize } from 'ahooks';
import { View } from '@tarojs/components';
import { rotateImg } from './utils';
import './index.less';

interface IProps {
  visible: boolean;
  setVisible: (e) => void;
  signText?: string;
  onChange?: (e?) => void; // 生成的图片
  onSure: (e?) => void; // 确定的回调
}
// 签字版组件
const SignatureBoard = (props: IProps) => {
  const { visible, setVisible, signText = '请在此空白处签下您的姓名', onChange, onSure } = 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();
  }, [signText]);

  // 取消
  const cancelSign = useCallback(() => {
    clearSign();
    setVisible?.(false);
  }, [clearSign, setVisible]);

  // 确定
  const sureSign = useCallback(() => {
    const pointGroupArray = sigCanvasRef?.current?.toData();
    if (pointGroupArray.flat().length < 30) {
      Taro.showToast({ title: '请使用正楷字签名', icon: 'none' });
      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={sureSign}>
          <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

css 复制代码
@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: 100vw;
    height: 100vh;
    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: 100vw;
    height: 100vh;
    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

typescript 复制代码
// canvas绘制图片旋转270度
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());
  };
};
相关推荐
2401_8791036820 分钟前
24.11.10 css
前端·css
ComPDFKit1 小时前
使用 PDF API 合并 PDF 文件
前端·javascript·macos
yqcoder1 小时前
react 中 memo 模块作用
前端·javascript·react.js
优雅永不过时·2 小时前
Three.js 原生 实现 react-three-fiber drei 的 磨砂反射的效果
前端·javascript·react.js·webgl·threejs·three
神夜大侠5 小时前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱5 小时前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
柯南二号5 小时前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
wyy72935 小时前
v-html 富文本中图片使用element-ui image-viewer组件实现预览,并且阻止滚动条
前端·ui·html
前端郭德纲5 小时前
ES6的Iterator 和 for...of 循环
前端·ecmascript·es6
王解6 小时前
【模块化大作战】Webpack如何搞定CommonJS与ES6混战(3)
前端·webpack·es6