前言:H5时常需要实现给C端用户签名的功能,以下是基于Taro框架开发的H5页面实现,非 Taro 的 View 标签换成 div 即可。
一、用到的技术库
签字库:react-signature-canvas
主流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] ) ;
useEffect ( ( ) => {
if ( ( compSize?. width ?? 0 ) > ( compSize?. height ?? 1 ) ) {
setIsLandscape ( true ) ;
clearSign ( ) ;
} else {
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) {
.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) {
.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
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. 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 ) ;
ctx?. drawImage ( image, 0 , 0 ) ;
const imgData = ctx?. getImageData ( cutCoor. sx, cutCoor. sy, cutCoor. ex, cutCoor. ey) ;
canvas. width = imgH;
canvas. height = imgW;
ctx?. putImageData ( imgData as any , 0 , 0 ) ;
callback ( canvas. toDataURL ( ) ) ;
} ;
} ;
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) ;
} ;
export const throttle = ( fn, delay = 2000 ) => {
let timer: any = null ;
return ( ... args) => {
if ( timer) {
return ;
}
fn . call ( undefined , ... args) ;
timer = setTimeout ( ( ) => {
timer = null ;
} , delay) ;
} ;
} ;
三、实现效果