React之antd登录图形校验码与短信校验码

about

在前端开发中,用户登录时常见有图形校验码或手机短校验码。
此类功能可通过后台协助来完成,也可由前端react独立完成。

本例模拟真实用户登录情况来实现

  • 图形校验证即时动态生成,录入时即时校验。
  • 短信校验码即时动态生成,录入时即时校验。

效果

请添加图片描述

# tree app
app
├── node_modules
├── package.json
├── src
│   ├── index.js        //主入口文件
│   └── test
│       ├── countdown.tsx       //手机短信倒计时按扭组件
│       ├── captchainput.tsx    //图片文字验证码组件
│       └── test.tsx            //示例

├── tsconfig.json
├── typings.d.ts
└── yarn.lock

登录源文件test.tsx

import React,{useRef,useState} from 'react';
import CaptchaInput from './captchainput.tsx'   //图形验证码组件
import CountButton from './countdown.tsx'       //手机短信验证码组件
//从antd组件中选用的控件
import { 
  Form, 
  Input, 
  Space,
  Button,
  Row,
  Col,
  Tabs,
} from 'antd';

import { 
    UserOutlined,
    LockOutlined,
    MobileOutlined,
  } from '@ant-design/icons';  

const App: React.FC = () => {
  //创建 Form 实例,用于管理所有数据状态。通过 Form.useForm 对表单数据域进行交互。
  const [form] = Form.useForm();  

  const childRef = useRef()   //图形校验码ref
  const btdRef = useRef()     //手机短信校验码ref

  //登录类型切换
  type LoginType = 'phone' | 'account';
  const [loginType, setLoginType] = useState<LoginType>('account');
  const onChange = (activeKey:string) => {
    setLoginType(activeKey as LoginType)
    console.log(activeKey);
  };


  //样式
  const layout = {
    labelCol: { span: 6,offset:2},
    wrapperCol: { span:12},
  };
  //样式
  const tailLayout = {
    wrapperCol: { offset: 8, span: 16 },
  };

  //按扭"submit"的单击事件
  const onFinish = (values: any) => {
    console.log(values);
    //console.log(
    //  form.getFieldValue("username"),
    //  form.getFieldValue("password")
    //);
  };
  //按扭"reset"的单击事件
  const onReset = () => {
    form.resetFields();
  };


  //页面渲染
  return(
    <>
    <Form
      style={{ maxWidth: 600,border: "1px dotted red"}}   // form的整体样式
      {...layout}                                         // form.item的label与控件的样式
      colon={true}                                        //表示是否显示 label 后面的冒号 (只有在属性 layout 为 horizontal 时有效)
      form={form}
      onFinish={onFinish}                                 // form事件,form会把值传递给事件      
    >
      <Tabs 
            defaultActiveKey="loginType"
            activeKey={loginType}
            centered={true}         //tabs标签是否居中
            onChange={onChange}     //Tabs变化时的事件
            tabPosition="top"       //tabs标签的位置,top上面,left侧边,right,bottom
            type="card"             //页签的基本样式,可选 line(默认方式)、card editable-card 类型
          >
            <Tabs.TabPane key={'account'} tab={'账号密码登录'} />
            <Tabs.TabPane key={'phone'} tab={'手机号登录'} />
      </Tabs>
      {/*当采用用户密码方式时*/}
      {loginType === 'account' && (
        <>
            <Form.Item 
              name="username" 
              label="username"
              rules={[   //rule配置校验规则
                  { 
                    required: true,
                    message: '此项必须输入',
                  },
                  {                               
                    type: 'string',
                    message: '只能输入字串,且不能为空字串(如全部是空格)',
                  },
              ]}
            >
              <Input
                size="large" 
                placeholder="用户名称" 
                prefix={<UserOutlined />} 
              />
            </Form.Item>
            <Form.Item
              name="password"
              label="Password"
              rules={[
                  { required: true,message: '此项必须输入' },
                  { min: 5,message: '字串长度不能小于5' },
                  { max: 25,message: '字串长度不能大于5' },
              ]}
              hasFeedback={true}   //配合 validateStatus 属性使用,展示校验状态图标,建议只配合 Input 组件使用 此外,它还可以通过 Icons 属性获取反馈图标。
              validateStatus="success"
            >
               <Input.Password  placeholder="请录入密码" prefix={<LockOutlined />} /> 
            </Form.Item>
            <Form.Item label="图形校验码"  rules={[{required: true}]} extra="We must make sure that your are a human.">
              <Row gutter={8}>
                <Col span={6}>
                  <Form.Item
                    name="captcha"
                    noStyle
                    rules={[
                      { required: true, message: 'Please input the captcha you got!' },
                      {
                        validator(_, value) {
                          if (!value) {
                            return new Promise((reject) => {reject('')})
                          } else {
                            const status = childRef.current.validate(value)
                            return status
                              ? Promise.resolve()
                              : Promise.reject(new Error('验证码不正确'))
                          }
                       }
                      }
                    ]}
                  >
                   <Input />
                  </Form.Item>
                </Col>
                <Col >
                  <CaptchaInput  cRef={childRef} />
                </Col>
              </Row>
            </Form.Item>
        </>
      )}

      {/*当采用手机方式时*/}
      {loginType === 'phone' && (
        <>
            <Form.Item 
              name="mobile" 
              label="Mobile"
              rules={[   //rule配置校验规则
                  { 
                    required: true,
                    message: '此项必须输入',
                  },
                  {                               
                    pattern: /^1\d{10}$/,
                    message: '手机号格式错误!',
                  },
              ]}
            >
              <Input
                size="large" 
                placeholder="手机号" 
                prefix={<MobileOutlined />} 
              />
            </Form.Item>
            <Form.Item label="动态校验码"  rules={[{required: true}]}>
              <Row gutter={8}>
                <Col span={11}>
                  <Form.Item
                    name="mobileCode"
                    noStyle
                    rules={[
                      { required: true, message: '请输入验证码' },
                      { pattern: /^\d{6}$/, message: '手机短信验证码是由6位数字组成的数字串' },
                      {
                        validator(_, value) {           //value是当前Form.Item的值。
                            if (!value) {
                              return new Promise((reject) => {reject('')})
                            } else {
                              //拿当前Form.Item的值到CountButton组件中进行校验(btdRef.current.validate方法),
                              //若与CountButton中产生的options.code值相同,则通过校验。
                              const status = btdRef.current.validate(value)
                              return status
                                ? Promise.resolve()
                                : Promise.reject(new Error('验证码不正确'))
                            }
                         }
                        },
                    ]}
                  >
                   <Input placeholder={'请输入验证码'} prefix={<LockOutlined />}/>
                  </Form.Item>
                </Col>
                <Col >
                   <CountButton form={form} bRef={btdRef} name={"mobile"} />
                </Col>
              </Row>
            </Form.Item>
        </>
      )}

      {/*提交按扭*/}
      <Form.Item  {...tailLayout} >
        <Space align="center">
          <Button type="primary" htmlType="submit"> {/* 不指定事件时,默认采用form中定义的事件。此时,form会把值传递给事件 */}
            Submit
          </Button>
          <Button htmlType="button" onClick={onReset}>
            Reset
          </Button>
        </Space>
      </Form.Item>
    </Form>
    </>
  )
}

export default App;

图形验证码组件captchainput.tsx

import * as React from 'react'
 
const size = 4
const verifycode = {
  width: '32%',
  height: '30px',
  marginLeft: '5%',
  display: 'inline-block',
  top: '0',
  right: '0'
}

export default function CaptchaInput ({ cRef }) {
  const [options, setOptions] = React.useState({
    id: 'verifycode', // 容器Id
    canvasId: 'verifyCanvas', // canvas的ID
    width: 150, // 默认canvas宽度
    height: 36, // 默认canvas高度
    type: 'blend', // 图形验证码默认类型blend:数字字母混合类型、number:纯数字、letter:纯字母
    code: '',      // 图形验证码的值,用户录入的值必须等于此处的code
    numArr: '0,1,2,3,4,5,6,7,8,9'.split(','),
    letterArr: getAllLetter() 
  })
 
  React.useImperativeHandle(cRef, () => ({
    validate: (value: any) => {
      const vcode = value?.toLowerCase()
      const v_code = options.code?.toLowerCase()
      if (vcode === v_code) {
        return true
      } else {
        return false
      }
    }
  }))
 
  React.useEffect(() => {
    _init()
    refresh()
  })
 
  function _init() {
    const con = document.getElementById(options.id)
    const canvas: any = document.createElement('canvas')
    options.width = con.offsetWidth > 0 ? con.offsetWidth : 150
    options.height = con.offsetHeight > 0 ? con.offsetHeight : 47
    canvas.id = options.canvasId
    canvas.width = options.width
    canvas.height = options.height
    canvas.style.cursor = 'pointer'
    canvas.innerHTML = '您的浏览器版本不支持canvas'
    con.appendChild(canvas)
    canvas.onclick = function() {
      refresh()
    }
  }
 
  function refresh() {
    options.code = ''
    const canvas: any = document.getElementById(options.canvasId)
    let ctx = null
    if (canvas.getContext) {
      ctx = canvas.getContext('2d')
    } else {
      return
    }
    ctx.clearRect(0, 0, options.width, options.height)
    ctx.textBaseline = 'middle'
 
    ctx.fillStyle = randomColor(180, 240)
    ctx.fillStyle = 'rgba(206, 244, 196)'// 背景色
    ctx.fillRect(0, 0, options.width, options.height)
 
    if (options.type == 'blend') { // 判断验证码类型
      var txtArr = options.numArr.concat(options.letterArr)
    } else if (options.type == 'number') {
      var txtArr = options.numArr
    } else {
      var txtArr = options.letterArr
    }
 
    for (let i = 1; i <= size; i++) {
      const txt = txtArr[randomNum(0, txtArr.length)]
      options.code += txt
      ctx.font = randomNum(options.height / 2, options.height) + 'px SimHei' // 随机生成字体大小
      ctx.fillStyle = randomColor(50, 160) // 随机生成字体颜色
      // ctx.fillStyle = "rgb(46, 137, 255)";//固定字体颜色
      ctx.shadowOffsetX = randomNum(-3, 3)
      ctx.shadowOffsetY = randomNum(-3, 3)
      ctx.shadowBlur = randomNum(-3, 3)
      ctx.shadowColor = 'rgba(0, 0, 0, 0.3)'
      const x = options.width / (size + 1) * i
      const y = options.height / 2
      const deg = randomNum(-30, 30)
      /** 设置旋转角度和坐标原点**/
      ctx.translate(x, y)
      ctx.rotate(deg * Math.PI / 180)
      ctx.fillText(txt, 0, 0)
      /** 恢复旋转角度和坐标原点**/
      ctx.rotate(-deg * Math.PI / 180)
      ctx.translate(-x, -y)
    }
    /** 绘制干扰线**/
    for (let i = 0; i < 4; i++) {
      ctx.strokeStyle = randomColor(40, 180)
      ctx.beginPath()
      ctx.moveTo(randomNum(0, options.width), randomNum(0, options.height))
      ctx.lineTo(randomNum(0, options.width), randomNum(0, options.height))
      ctx.stroke()
    }
    // 绘制干扰点
    // for (let i = 0; i < 100; i++) {
    //   ctx.fillStyle = randomColor(0, 255)
    //   ctx.beginPath()
    //   ctx.arc(randomNum(0, options.width), randomNum(0, options.height), 1, 0, 2 * Math.PI)
    //   ctx.fill()
    // }
  }
 
  /** 生成字母数组**/
  function getAllLetter() {
    const letterStr = 'a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z'
    return letterStr.split(',')
  }
  /** 生成一个随机数**/
  function randomNum(min: number, max: number) {
    return Math.floor(Math.random() * (max - min) + min)
  }
  /** 生成一个随机色**/
  function randomColor(min: number, max: number) {
    const r = randomNum(min, max)
    const g = randomNum(min, max)
    const b = randomNum(min, max)
    return 'rgb(' + r + ',' + g + ',' + b + ')'
  }
 
  console.log("options:",options)

  return (
    <div id='verifycode' style={verifycode} />
  )
}

短信验证码组件countdown.tsx

import React, { useState, useEffect} from 'react'
import { Button,Form } from 'antd'

//随机产生n个数字成的字串(如应用于手机验证码的产生)
function getStringRand(len:any) {
  const possibleCharacters = '0123456789';
  const stringLength = parseInt(len); // 你想要生成的字符串长度
  let randomString = '';

  for (let i = 0; i < stringLength; i++) {
    const randomIndex = Math.floor(Math.random() * possibleCharacters.length);
    randomString += possibleCharacters.charAt(randomIndex);
  }
  return randomString
}

//参数
// bRef事件引用参数,供外部调用本组件定义的事件
// form表单对像,外部表单对像引用本组件中
// name外部表单某一控件的名称,通常是指手机号码录入控件
export default function CountButton({bRef,form,name},){
  //--------短信动态码状态----------------------------
  const [options, setOptions] = React.useState({
    code: '',      // 验证码的值,用户录入的值必须等于此处的code 
  })

  //--------组件定义方法(供外部调用)----------------------------
  React.useImperativeHandle(bRef, () => ({
    //数据校验
    validate: (value: any) => {
    const vcode = value?.toLowerCase()
    const v_code = options.code?.toLowerCase()
    if (vcode === v_code) {
      return true
    } else {
      return false
    }
    }
    }),
  )

  //--------按扭状态:初始状态配置----------------------------
  //按扭状态识别,若手机号不能正确录入,倒计时button不能启用
  const [submittable, setSubmittable] = React.useState<boolean>(false);
  //const values = Form.useWatch('mobile', form);   //只监控某一个Form.Item
  //const values = Form.useWatch([],form);          //Watch all values
  const values = Form.useWatch(name, form);         //只监控某一个Form.Item
  
  useEffect(() => {
    form
      //.validateFields({ validateOnly: true })   //form所有项
      .validateFields([name])                 //表单的某一项
      .then(() => setSubmittable(true))
      .catch(() => setSubmittable(false));
  }, [form, values]);


  //--------按扭状态:单击过程中的状态配置----------------------------
  //倒计时button的单击状态识别
  const [isClick, setIsClick] = useState(false);
  const [count, setCount] = useState(0);
  const [btname, setBtname] = useState('发送验证码');
  //按扭倒计时进行时
  useEffect(() => { 
    const intervalId = setInterval(() => {
      //定时任务区
      if (isClick) {
      if (count < 60) {      //当少于60时
         setCount(count + 1)
         setBtname("(" + count + "秒后)发送验证码")
         setIsClick(true)
      } else {               //当等于60时,发送按扭复位
        setCount(0)
        setBtname("发送验证码")
        setIsClick(false)
        const code = getStringRand(6)   //产生6位数字的随机码作为验证码
        setOptions({code: code})        //将之前的验证码覆盖掉。
      }
      
    }}, 1000); // 每秒更新一次
    return () => clearInterval(intervalId);
    }, 
    [btname]
  );

  //--------按扭单击事件----------------------------
  function onSendCode(){
    if (!isClick) {
        setBtname("(60 秒后)发送验证码")
        setIsClick(true)   
        const code = getStringRand(6)   //产生6位数字的随机码作为验证码
        setOptions({code: code})
        //sendCode(code,values)    //在真实的业务中,需通过第三方短信通道将此验证码发到用户手机上。
        //短信码测试
        console.log(code,values)   //code是动态码。values是父组件传递而来的手机号码
    } else {
        setBtname("发送验证码")
        setIsClick(false)
    }
  }

  //渲染倒计时按扭
  return(
    <>
      <Button 
        style={{borderRadius:"0",borderStyle:'none'}} 
        onClick={onSendCode} 
        disabled={(isClick||(!submittable))} 
      > 
        {btname}
      </Button>
    </>
  )
}

参考

本文在创作过程中参考如下文章,感谢作者分享
https://blog.csdn.net/weixin_64064932/article/details/127258353

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个简单的示例代码: 首先安装所需依赖: ```bash npm install react antd axios --save ``` 然后创建一个 `Login` 组件和一个 `Register` 组件,分别用于登录和注册的页面展示。其中,我们使用了 `Form` 和 `Input` 组件来实现表单输入,以及 `Button` 组件来实现按钮的功能。 ```jsx // Login.jsx import React, { useState } from 'react'; import { Form, Input, Button, message } from 'antd'; import axios from 'axios'; const Login = () => { const [loading, setLoading] = useState(false); const onFinish = async (values) => { setLoading(true); try { const response = await axios.post('/api/login', values); message.success(response.data.message); setLoading(false); } catch (error) { message.error(error.response.data.message); setLoading(false); } }; return ( <Form name="login" onFinish={onFinish}> <Form.Item name="email" rules={[{ required: true, message: 'Please input your email!' }]} > <Input placeholder="Email" /> </Form.Item> <Form.Item name="password" rules={[{ required: true, message: 'Please input your password!' }]} > <Input.Password placeholder="Password" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" loading={loading}> Login </Button> </Form.Item> </Form> ); }; export default Login; ``` ```jsx // Register.jsx import React, { useState } from 'react'; import { Form, Input, Button, message } from 'antd'; import axios from 'axios'; const Register = () => { const [loading, setLoading] = useState(false); const onFinish = async (values) => { setLoading(true); try { const response = await axios.post('/api/register', values); message.success(response.data.message); setLoading(false); } catch (error) { message.error(error.response.data.message); setLoading(false); } }; return ( <Form name="register" onFinish={onFinish}> <Form.Item name="name" rules={[{ required: true, message: 'Please input your name!' }]} > <Input placeholder="Name" /> </Form.Item> <Form.Item name="email" rules={[{ required: true, message: 'Please input your email!' }]} > <Input placeholder="Email" /> </Form.Item> <Form.Item name="password" rules={[{ required: true, message: 'Please input your password!' }]} > <Input.Password placeholder="Password" /> </Form.Item> <Form.Item name="confirmPassword" rules={[ { required: true, message: 'Please confirm your password!' }, ({ getFieldValue }) => ({ validator(_, value) { if (!value || getFieldValue('password') === value) { return Promise.resolve(); } return Promise.reject(new Error('The two passwords do not match!')); }, }), ]} > <Input.Password placeholder="Confirm Password" /> </Form.Item> <Form.Item> <Button type="primary" htmlType="submit" loading={loading}> Register </Button> </Form.Item> </Form> ); }; export default Register; ``` 在这里我们使用了 `axios` 来发送登录和注册的请求,需要注意的是,这里的请求地址是 `/api/login` 和 `/api/register`,而不是实际的后端接口地址,根据实际情况进行修改。 最后在需要展示登录和注册功能的页面中引入 `Login` 和 `Register` 组件即可。 ```jsx import React from 'react'; import Login from './Login'; import Register from './Register'; const LoginPage = () => { return ( <div> <h1>Login</h1> <Login /> <hr /> <h1>Register</h1> <Register /> </div> ); }; export default LoginPage; ``` 以上就是一个简单的使用 `react` 和 `antd` 实现登录和注册功能的示例代码,希望对你有帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值