H5实现签字板签名功能

前言:H5时常需要实现给C端用户签名的功能,以下是基于Taro框架开发的H5页面实现,非 Taro 的 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) => 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) {
      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);
  };
};

三、实现效果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/67667.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2024考研408-计算机网络 第四章-网络层学习笔记

文章目录 前言一、网络层的功能1.1、网络层功能概述&#xff08;三种功能介绍&#xff09;1.2、SDN基本概念1.2.1、理解转发与路由选择1.2.1.1、转发1.2.1.2、路由选择 1.2.2、数据平面&#xff08;转发&#xff09;1.2.3、控制平面&#xff08;路由计算与选择&#xff09;实现…

【SpringBoot框架篇】33.优雅集成i18n实现国际化信息返回

文章目录 1.简介2.MessageSource配置和工具类封装2.1.配置MessageSource相关配置2.2.配置工具类2.3.测试返回国际级文本信息 3.不优雅的web调用示例(看看就行&#xff0c;别用)4.优雅使用示例4.1.错误响应消息枚举类4.2.ThreadLocal工具类配置4.2.1.ThreadLocal工具类数据封装4…

W6100-EVB-PICO 做TCP Server进行回环测试(六)

前言 上一章我们用W6100-EVB-PICO开发板做TCP 客户端连接服务器进行数据回环测试&#xff0c;那么本章将用开发板做TCP服务器来进行数据回环测试。 TCP是什么&#xff1f;什么是TCP Server&#xff1f;能干什么&#xff1f; TCP (Transmission Control Protocol) 是一种面向连…

selenium爬虫,配置谷歌浏览器的driver

用selenium爬虫时&#xff0c;明明已经安装了selenium模块&#xff0c;程序却运行不了。在使用selenium之前必须先配置浏览器对应版本的webdriver 本文主要涉及驱动有问题driver 网上有很多手动的方法&#xff08;查看谷歌浏览的版本然后在其他博主分享的webdriver中下载与自己…

研发工程师玩转Kubernetes——通过PV的节点亲和性影响Pod部署

在《研发工程师玩转Kubernetes——PVC通过storageClassName进行延迟绑定》一文中&#xff0c;我们利用Node亲和性&#xff0c;让Pod部署在节点ubuntud上。因为Pod使用的PVC可以部署在节点ubuntuc或者ubuntud上&#xff0c;而系统为了让Pod可以部署成功&#xff0c;则让PVC与Pod…

【Windows系统】磁盘、Partition和Volume的联系与区别

1、磁盘 Disk&#xff0c;磁盘。 以下摘自微软 磁盘设备和分区 - Win32 apps | Microsoft Learn 硬盘由一组堆积的盘片组成&#xff0c;其中每个盘片的数据都以电磁方式存储在同心圆或 轨道中。 每个盘片都有两个头&#xff0c;一个在盘片的两侧&#xff0c;在磁盘旋转时读取…

idea-常用插件汇总

idea-常用插件汇总 码云插件 这个插件是码云提供的ps-码云是国内的一款类似github的代码托管工具。 Lombok Lombok是一个通用Java类库&#xff0c;能自动插入编辑器并构建工具&#xff0c;简化Java开发。通过添加注解的方式&#xff0c;不需要为类编写getter或setter等方法…

1. CUDA编程手册中文版---CUDA简介

1.CUDA简介 1.1 我们为什么要使用GPU 更多精彩内容&#xff0c;请扫描下方二维码或者访问https://developer.nvidia.com/zh-cn/developer-program 来加入NVIDIA开发者计划 GPU&#xff08;Graphics Processing Unit&#xff09;在相同的价格和功率范围内&#xff0c;比CPU提供…

【C++】多态的底层原理(虚函数表)

文章目录 前言一、虚函数表二、派生类中虚函数表1.原理2.例子&#xff1a; 三、虚函数的存放位置四 、单继承中的虚函数表五、多继承中的虚函数表六、问答题 前言 一、虚函数表 通过观察测试我们发现b对象是8bytes&#xff0c;除了_b成员&#xff0c;还多一个__vfptr放在对象的…

易服客工作室:7个优质WordPress LMS线上教育系统插件比较(优点和缺点)

您是否正在为您的 WordPress 网站寻找最好的 LMS 插件&#xff1f;在线学习管理系统 (LMS) 插件允许您使用 WordPress 创建和运行类似 Udemy 的在线课程。 一个完美的 WordPress LMS 插件包括管理在线课程内容、处理订阅、运行和评分测验、接受付款等功能。 在本文中&#xf…

lwip使用收发线程和不使用收发线程差异

使用收发线程的方式相对于不使用收发线程的方式&#xff0c;效率可能会稍低一些&#xff0c;这取决于具体的应用场景和实现方式。 lwIP&#xff08;轻量级IP协议栈&#xff09;是一个针对嵌入式系统的开源TCP/IP协议栈。它可以在单个线程中运行&#xff0c;也可以在多个线程中…

Golang bitset 基本使用

安装&#xff1a; go get github.com/bits-and-blooms/bitset下面代码把fmtx换成fmt就行 //------------基本操作------------//构建一个64bit长度的bitsetb : bitset.New(64)//放入一个数b.Set(10)fmtx.Println("add-10&#xff1a;", b.DumpAsBits()) // 0000000…

MyBatis查询数据库之三(#{}vs${},like查询,resultMap,as,多表查询)

目录 查询操作 1.单表查询 1.1 参数占位符#{}和${} 1.2 ${}的优点 1.3 sql注入问题 ​编辑 面试常问&#xff1a;${}与#{}的区别 1.4 like查询 2.多表查询 2.1 返回字典映射&#xff1a;resultMap 2.2 多表查询 &#xff08;1&#xff09;建立 Articalinfo 实体类&a…

Qt 使用QLabel的派生类实现QLabel的双击响应

1 介绍 在QLabel中没有双击等事件响应&#xff0c;需要构建其派生类&#xff0c;自定义信号(signals)、重载事件函数(event)&#xff0c;最后在Qwidget中使用connect链接即可&#xff0c;进而实现响应功能。 对于其余没有需求事件响应的QObject同样适用。 此外&#xff0c;该功…

【OpenVINOSharp】 基于C#和OpenVINO2023.0部署Yolov8全系列模型

基于C#和OpenVINO2023.0部署Yolov8全系列模型 1 项目简介1.1 OpenVINOTM 2 OpenVinoSharp2.1 OpenVINOTM 2023.0安装配置2.2 C 动态链接库2.3 C#构建Core推理类2.4 NuGet安装OpenVinoSharp 3 获取和转换Yolov8模型3.1 安装ultralytics3.2 导出yolov8模型3.3 安装OpenVINOTM Pyt…

XXL-Job 具体通过docker 配置,再通过springboot执行注册实现完整流程

【2023】XXL-Job 具体通过docker 配置安装容器&#xff0c;再通过springboot执行注册实现 一、概述二、安装1、拉取镜像2、创建数据库3、创建容器并运行3、查看容器和日志4、打开网页 127.0.0.1:9051/xxl-job-admin/ 三、实现注册测试1、创建一个SpringBoot项目、添加依赖。2、…

python 基础

1.Python 1.1 环境搭建&#xff1a;官网下载python&#xff0c;编译器&#xff1a;pycharm或jupyter 1.2 变量 &#xff08;1&#xff09;语法&#xff1a;变量名变量 &#xff08;2&#xff09;标识符命名规则&#xff1a;由数字、字母、下划线组成&#xff1b;不能以数字…

ORACLE和MYSQL区别

1&#xff0c;Oracle没有offet,limit&#xff0c;在mysql中我们用它们来控制显示的行数&#xff0c;最多的是分页了。oracle要分页的话&#xff0c;要换成rownum。 2&#xff0c;oracle建表时&#xff0c;没有auto_increment&#xff0c;所有要想让表的一个字段自增&#xff0c…

Dockerfile 简单实战

将flask项目打包成镜像 1. 准备flask文件 创建 app.py 文件&#xff0c;内容如下 from flask import Flask app Flask(__name__)app.route(/) def hello_world():return Hello Worldif __name__ __main__:app.run(host0.0.0.0, port8000, debugTrue) 并开启外网访问&#xf…

【2.1】Java微服务: Nacos的使用

目录 Nacos介绍 Nacos安装 下载和安装 修改端口 启动 服务注册与发现 导入Nacos管理依赖 导入服务依赖 配置Nacos的服务地址 启动服务&#xff0c;查看已注册的服务 服务分级存储模型 分级存储模型介绍 具体结构 配置实例集群 同集群优先的负载均衡策略 服务权重配置…