React 发送短信验证码和验证码校验功能组件
该代码实现了一个带键盘跟随功能的验证码输入组件,主要特性包括: 响应式设计,通过useVisualViewport钩子检测键盘高度,自动调整输入框位置避免遮挡 验证码输入:支持6位数字输入,包含复制粘贴功能和样式化输入框显示 交互功能: 自动发送验证码 60秒重发倒计时 键盘弹出时自动聚焦输入框 UI组件:包含关闭按钮、手机号显示、验证码输入框和确认按钮 动画效果:淡入/上滑动画,输入框激活状态视觉效果 组件适用于移动端验证场景,提供良好的键盘交互体验。
效果预览:



1.获取键盘高度组件
import { useState, useEffect } from "react";export default function UseVisualViewport() {const [viewport, setViewport] = useState({width: window.innerWidth,height: window.innerHeight,visualWidth: window.visualViewport ? window.visualViewport.width : window.innerWidth,visualHeight: window.visualViewport ? window.visualViewport.height : window.innerHeight,keyboardHeight: 0,isKeyboardVisible: false});useEffect(() => {const handler = () => {if (!window.visualViewport) {setViewport(prev => ({...prev,width: window.innerWidth,height: window.innerHeight}));return;}const keyboardHeight = Math.max(0, window.innerHeight - window.visualViewport.height);const isKeyboardVisible = keyboardHeight > 50; // 键盘高度大于50px认为键盘可见setViewport({width: window.innerWidth,height: window.innerHeight,visualWidth: window.visualViewport.width,visualHeight: window.visualViewport.height,keyboardHeight,isKeyboardVisible});};// 添加事件监听if (window.visualViewport) {window.visualViewport.addEventListener("resize", handler);window.visualViewport.addEventListener("scroll", handler);} else {// 如果不支持visualViewport,使用resize事件作为fallbackwindow.addEventListener("resize", handler);}// 初始调用一次handler();return () => {if (window.visualViewport) {window.visualViewport.removeEventListener("resize", handler);window.visualViewport.removeEventListener("scroll", handler);} else {window.removeEventListener("resize", handler);}};}, []);return viewport;
}
2.代码主要内容
import React, { useState, useEffect, useRef } from "react";
import PropTypes from "prop-types";
import UseVisualViewport from "@/components/UseVisualViewport";
import {maskPhoneNumber} from "util/util";const CodeInputDrawer = (props) => {const {phoneNumber,visible = false,onClose,onSendCode,onConfirm,codeLength = 6,countdownTime = 60,} = props;const [code, setCode] = useState("");const [countdown, setCountdown] = useState(0);const [isInputFocused, setIsInputFocused] = useState(false);const inputRef = useRef(null);const containerRef = useRef(null);const viewport = UseVisualViewport();// 处理输入框聚焦const handleInputFocus = () => {if (inputRef.current) {inputRef.current.focus();setIsInputFocused(true);// 确保输入框在键盘上方可见if (viewport.isKeyboardVisible && containerRef.current) {const containerRect = containerRef.current.getBoundingClientRect();const viewportHeight = window.innerHeight;// 如果容器底部在键盘区域内,滚动到可见位置if (containerRect.bottom > viewport.visualHeight) {const scrollOffset = containerRect.bottom - viewport.visualHeight + 20;window.scrollBy({top: scrollOffset,behavior: 'smooth'});}}}};// 处理输入框失焦const handleInputBlur = () => {setIsInputFocused(false);};// 处理输入变化const handleInputChange = (e) => {const value = e.target.value.replace(/[^0-9]/g, "").slice(0, codeLength);setCode(value);if (value.length === codeLength && props.onComplete) {props.onComplete(value);}};// 发送验证码const handleSendCode = async () => {if (!phoneNumber || phoneNumber.length !== 11) {alert("请输入正确的手机号码");return;}if (countdown > 0) {return;}try {if (onSendCode) {await onSendCode(phoneNumber);}setCountdown(countdownTime);} catch (error) {console.error("发送验证码失败:", error);alert("发送验证码失败,请重试");}};// 确认输入的验证码const handleConfirm = () => {if (code.length !== codeLength) {alert("请输入完整的验证码");return;}if (onConfirm) {onConfirm(code);}};// 关闭抽屉const handleClose = () => {if (onClose) {onClose();}};// 倒计时效果useEffect(() => {if (countdown > 0) {const timer = setTimeout(() => {setCountdown(countdown - 1);}, 1000);return () => clearTimeout(timer);}}, [countdown]);// 当键盘显示/隐藏时调整容器位置useEffect(() => {if (!visible) return;// 键盘显示时确保内容可见if (viewport.isKeyboardVisible && containerRef.current) {const containerRect = containerRef.current.getBoundingClientRect();// 如果容器被键盘遮挡,滚动到可见位置if (containerRect.bottom > viewport.visualHeight) {const scrollOffset = containerRect.bottom - viewport.visualHeight + 20;window.scrollBy({top: scrollOffset,behavior: 'smooth'});}}}, [viewport.isKeyboardVisible, visible]);// 点击容器外部时失焦输入框useEffect(() => {if (!visible) return;const handleClickOutside = (e) => {if (containerRef.current && !containerRef.current.contains(e.target)) {handleInputBlur();}};document.addEventListener("mousedown", handleClickOutside);return () => {document.removeEventListener("mousedown", handleClickOutside);};}, [visible]);// 当抽屉可见时自动聚焦输入框useEffect(() => {if (visible) {setTimeout(() => {handleInputFocus();}, 300);} else {setCode("");setCountdown(0);setIsInputFocused(false);}}, [visible]);// 首次加载自动发送验证码useEffect(() => {if (visible) {handleSendCode().catch(() => {alert("发送验证码失败,请重试");});}}, [visible]);// 根据键盘高度获取容器样式const getContainerStyle = () => {if (viewport.isKeyboardVisible) {return {bottom: `${viewport.keyboardHeight + 20}px`, // 距离键盘20px边距transition: "bottom 0.3s ease",position: 'fixed',zIndex: 1000};}return {bottom: "0",transition: "bottom 0.3s ease",position: 'fixed',zIndex: 1000};};// 渲染单个验证码框const renderCodeBox = (index) => {const digit = code[index] || "";const isActive = index === code.length && isInputFocused;const isFilled = digit !== "";return (<divkey={index}className={`code-box ${isActive ? 'active' : ''} ${isFilled ? 'filled' : ''}`}>{digit}</div>);};if (!visible) {return null;}return (<div className="code-input-mask" onClick={handleClose}><divref={containerRef}className={`code-input-container ${visible ? 'visible' : ''}`}style={getContainerStyle()}onClick={(e) => e.stopPropagation()}>{/* Close button */}<button className="close-btn" onClick={handleClose}>×</button>{/* Phone number display */}<div className="phone-display"><div className='title-tip'>请输入验证码</div><div className='tip-content'>我们已给手机号码{phoneNumber &&<span className="phone-number">{maskPhoneNumber(phoneNumber)}</span>}发送了一个6位数的验证码,输入验证码完成补款!</div></div>{/* Code boxes area */}<div className="code-boxes-container" onClick={handleInputFocus}>{Array.from({ length: codeLength }).map((_, index) => renderCodeBox(index))}</div>{/* Hidden input field */}<inputref={inputRef}type="tel"value={code}onChange={handleInputChange}onFocus={() => setIsInputFocused(true)}onBlur={handleInputBlur}className="hidden-input"maxLength={codeLength}inputMode="numeric"pattern="[0-9]*"/>{/* Send code button */}<div className="send-code-btn-container"><divclassName="send-code-btn"onClick={handleSendCode}disabled={countdown > 0 || !phoneNumber}>{countdown > 0 ? `重新发送(${countdown}s)` : "重新发送验证码"}</div></div>{/* Confirm button */}<buttonclassName="confirm-btn"onClick={handleConfirm}disabled={code.length !== codeLength}>确定</button></div></div>);
};// Define prop types
CodeInputDrawer.propTypes = {phoneNumber: PropTypes.string,visible: PropTypes.bool,onClose: PropTypes.func,onSendCode: PropTypes.func,onConfirm: PropTypes.func,onComplete: PropTypes.func, // Add this if you have an onComplete callbackcodeLength: PropTypes.number,countdownTime: PropTypes.number,
};// Default props
CodeInputDrawer.defaultProps = {visible: false,codeLength: 6,countdownTime: 60,
};export default CodeInputDrawer;
3.代码样式
.code-input-mask {position: fixed;top: 0;left: 0;right: 0;bottom: 0;background-color: rgba(0, 0, 0, 0.5);display: flex;align-items: flex-end;justify-content: center;z-index: 9999;animation: maskFadeIn 0.2s ease;
}.code-input-container {width: 100%;max-width: 100%;background-color: white;border-radius: 16px 16px 0 0;padding: 24px 20px 40px;box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.15);animation: drawerSlideUp 0.2s ease;position: relative; /* 从absolute改为relative */transform: translateY(100%);transition: transform 0.3s ease;bottom: 0;/* 确保容器保持在键盘上方 */&.visible {transform: translateY(0);}/* 响应式设计 */@media (min-width: 768px) {max-width: 400px;border-radius: 16px;margin-bottom: 20px;}
}.phone-display {font-size: 14px;color: #666;margin-bottom: 16px;.title-tip{font-size: 20px;font-weight: 500;color: #131523;}.tip-content{font-size: 14px;font-weight: 400;color: rgba(19, 21, 35, 0.58);margin-top: 10px;}.phone-number {color: #3484FD;}
}.code-boxes-container {display: flex;justify-content: space-between;margin-bottom: 20px;.code-box {width: 44px;height: 52px;border: 1px solid #D9D9D9;border-radius: 8px;display: flex;align-items: center;justify-content: center;font-size: 20px;font-weight: bold;background-color: #F6F6F6; // 设置背景颜色为#F6F6F6color: #000000; // 设置字体颜色为黑色transition: all 0.2s ease;caret-color: #1890ff; // 设置光标颜色为蓝色&.active {border-color: #1890ff;background-color: #F6F6F6; // 保持背景颜色一致box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);// 添加蓝色竖线光标效果position: relative;&::after {content: '';position: absolute;top: 10px;bottom: 10px;left: 50%;width: 2px;background-color: #1890ff;animation: blink 1s infinite;}}&.filled {border-color: #1890ff;background-color: #F6F6F6; // 保持背景颜色一致color: #000000; // 设置字体颜色为黑色}}
}// 添加光标闪烁动画
@keyframes blink {0%, 100% { opacity: 1; }50% { opacity: 0; }
}.send-code-btn-container{width: 100%;display: flex;justify-content: flex-end;align-items: center;.send-code-btn {border: none;border-radius: 8px;font-size: 14px;font-weight: 500;margin-bottom: 16px;cursor: pointer;transition: all 0.2s ease;&:not(:disabled) {color: #999;}&:disabled {color: #999;cursor: not-allowed;}}
}.confirm-btn {width: 100%;height: 44px;border: none;border-radius: 8px;font-size: 16px;font-weight: 500;cursor: pointer;transition: all 0.2s ease;&:not(:disabled) {background-color: #52c41a;color: white;&:hover {background-color: #73d13d;}&:active {background-color: #389e0d;}}&:disabled {background-color: #f5f5f5;color: #999;cursor: not-allowed;}
}.close-btn {position: absolute;top: 12px;right: 12px;width: 24px;height: 24px;border: none;background: none;font-size: 18px;color: #999;cursor: pointer;&:hover {color: #666;}
}.hidden-input {position: absolute;opacity: 0;pointer-events: none;width: 1px;height: 1px;
}/* 动画效果 */
@keyframes maskFadeIn {from {opacity: 0;}to {opacity: 1;}
}@keyframes drawerSlideUp {from {transform: translateY(100%);}to {transform: translateY(0);}
}
4.使用组件
<CodeInputDrawerphoneNumber="19948765606"visible={showCodeInput}onClose={() => this.setState({showCodeInput: false})}onSendCode={(phone) => {console.log(phone)}}onConfirm={(code) => {// 验证验证码console.log("验证码:", code);}}/>
