React - 验证码输入器

2022 年 6 月 25 日

更新于 2022 年 7 月 6 日

简介

每次用到验证码输入的时候都会幻想自己实现一个, 今天在高铁上百无聊赖, 于是动手做了一个.

演示

话不多说, 先上演示: demo .

代码

VerificationCode.js

import { useState } from 'react';
import './VerificationCode.scss';
import $ from 'jquery';

/**
 * Replace the nth char of a string with another specified char.
 * @param {String} str the str that needs to be modified
 * @param {int} index the index for change
 * @param {character} char the new char
 * @returns 
 */
function replaceChar(str, index, char) {
    let strArray = str.split("");
    strArray[index] = char;
    return strArray.join("");
}

export function VerificationCode() {

    // The code
    const [code, setCode] = useState("      ");

    // Current focused position
    const [focusPosition, setFocusPosition] = useState(-1);

    // Processing
    const [processing, setProcessing] = useState(false);

    /**
     * Handles body click.
     * @returns void
     */
    function handleClick() {
        if (processing) {
            return;
        }

        // focus the input
        $("#code").focus();

        // last position, do nothing
        if (focusPosition == 5) {
            return;
        }

        // initialization
        if (focusPosition == -1) {
            setFocusPosition(0);
        } else { // activate
            setFocusPosition(focusPosition);
        }
    }


    /**
     * Handles input value change.
     * @param {*} e change event
     * @returns nothing
     */
    async function handleChange(e) {
        // get input value
        let value = e.target.value;

        // get input value length
        let length = value.length;

        if (length == 6) {
            setProcessing(true);
            sleep(1000).then(() => {
                    setProcessing(false);
                    reset();
            });
            
        }

        // if length is greater than 6, revert back
        if (length > 6) {
            $("#code").val(code.substring(0, 6));
            return;
        }

        // if current focus position is less than the previous position
        // then set the last char to space.
        if (length - 1 < focusPosition) {
            let modifiedCode = replaceChar(code, focusPosition, ' ');
            setCode(modifiedCode);
            setFocusPosition(focusPosition - 1)
            return;
        }

        // set current focus position
        let index = length - 1 >= 0 ? length - 1 : 0;

        // update current focused index
        setFocusPosition(index);

        // update and set code
        setCode(replaceChar(code, index, value.charAt(value.length - 1)));
    }

    function reset() {
        setCode('      ');
        setFocusPosition(-1);
        $('#code').val('');
    }

    return (
        <div className={`playground`}>
            {/* <div className={'my-logo'}>
                <div className={'d'}>
                    d
                </div>
            </div> */}

            <div className={'label'}>
                请输入验证码:
            </div>

            <div
                className={"numbers"}
                onClick={handleClick}>
                {code.split("").map((item, key) => (
                    <div key={key} className={"number " + ((focusPosition == key && !processing) ? "focused" : "")}>
                        {item}
                    </div>
                ))}
                <input id={"code"} type={'number'} maxLength={6} onChange={handleChange}/>
            </div>

            {(!processing) && (
                <div className={'button'} onClick={reset}>重新输入</div>
            )}

            {(processing) && (
                <div className={'processing'} onClick={reset}>验证中...</div>
            )}
            

        </div>
    );
}

VerificationCode.scss

.verification-code {
  width: 100%;

  /* Chrome, Safari, Edge, Opera */
  input::-webkit-outer-spin-button,
  input::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }

  /* Firefox */
  input[type=number] {
    -moz-appearance: textfield;
  }

  .label {
    padding: 10px;
  }
  
  #code {
    opacity: 0;
    cursor: default;
    width: 0;
    height: 0;
  }
  
  .numbers {
    padding: 10px;
    display: flex;
  
    .number {
      width: 40px;
      height: 40px;
      border: 1px solid #bbb;
      border-radius: 5px;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-right: 10px;
      font-weight: bold;
      font-size: 20px;
      color: #888;
      cursor: text;
  
      &:nth-last-child(1) {
        margin-right: 0;
      }
    }
  
    .focused {
      border: 1px solid #888;
      box-shadow: 0 5px 20px rgba(0,0,0, 0.2);
    }
  }
  
  .button {
    margin: 10px;
    cursor: pointer;
    font-weight: bold;
    color: #0087fc;
    width: fit-content;
  
    &:hover {
      text-decoration: underline;
    }
  }
  
  .processing {
    margin: 10px;
    color: #888;
    font-weight: bold;
  }
}