前言:H5时常需要给C端用户签名的功能,以下是基于Taro框架开发的H5页面实现
一、用到的技术库
- 签字库:
react-signature-canvas
- 主流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());
};
};