当前位置: 首页 > news >正文

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);}}/>

http://www.dtcms.com/a/578684.html

相关文章:

  • 做视频解析网站犯法荆州网站推广怎么做
  • 中国网站优化公司关键词分析工具
  • Temp Mail4.09 | 生成匿名临时邮箱地址,避免垃圾邮件,保护隐私
  • 可以把网站服务器放在哪里wordpress代码添加
  • 使用Requests和LXML实现安居客二手房数据高效爬取
  • 办公用品十大购物网站排名辞职做网站
  • 威海外贸网站建设联系方式网站域名后缀代表什么意思
  • 从生产到质量: RFID 技术赋能汽车制造全链路管控
  • redis中的offset是什么意思
  • soho做网站谷歌推广短视频推广渠道有哪些
  • 广州微信网站建设报价表网站技术方案
  • 【论文精读】Lumiere:重塑视频生成的时空扩散模型
  • 黑龙江住房和城乡建设厅网站襄城县做网站的
  • 外贸网站vps服务器gstatic wordpress
  • 陕西省建设安全协会网站wordpress采集中文
  • 标签Labels、Scheduler:调度器、k8s污点与容忍度
  • 网站开发询价单重庆网站建设报价
  • [论文阅读] AI + 软件工程 | 3340个Python ML项目实证:PyQu工具+61种代码变更,精准提升软件质量!
  • 【数据结构+算法】进栈顺序推算、卡特兰数与逆波兰表达式
  • 网站源码 一品资源网电龙网站建设
  • 文山网站建设代理成都品牌形象设计
  • SEO超级外链工具 - SEO超级外链工具 - 网站自动外链群发与推广优化助手
  • 企业数据查询网站怎么自己做H5网站
  • 单位门户网站可以做百度百科指数型基金是什么意思
  • 详细阐述时间复杂度和空间复杂度定义、算法、和原理,,举例通过C/C++里面说明
  • 神鹰网站建设公司各行业的专业网址论坛资料
  • 慕枫网站建设做网站电话销售的话术
  • 免费网站制作多少钱西宁网站网站建设
  • 【AI应用探索】-8- OpenManus使用及源码解析
  • 网站建设与管理的条件常州建站程序