惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hugging Face - Blog
Hugging Face - Blog
云风的 BLOG
云风的 BLOG
大猫的无限游戏
大猫的无限游戏
M
MIT News - Artificial intelligence
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recent Announcements
Recent Announcements
IT之家
IT之家
Google DeepMind News
Google DeepMind News
罗磊的独立博客
爱范儿
爱范儿
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
U
Unit 42
MongoDB | Blog
MongoDB | Blog
S
SegmentFault 最新的问题
B
Blog
博客园 - 叶小钗
月光博客
月光博客
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
C
Check Point Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

蚊子的前端博客

微说 | 有的人觉得只要不考XX,一定能考好!-蚊子的前端博客 微说 | 春天来了-蚊子的前端博客 明天和意外不知道哪个先来-蚊子的前端博客 微说 | 既然错过了路口,就及时止损,重新规划路线上路-蚊子的前端博客 微说 | 2025年的出生人口数是792万-蚊子的前端博客 微说 | 工作年终总结,让写对接的接口的数量?-蚊子的前端博客 微说 | 温家宝:没有政治体制改革的成功 经济体制改革不可能进行到底-蚊子的前端博客 微说 | 违法犯罪了该不该被禁言?-蚊子的前端博客 微说 | 不明白为什么在推荐去俄罗斯旅游-蚊子的前端博客 微说 | 易中天论骗子-蚊子的前端博客 微说 | 一场大风吹散了秋天-蚊子的前端博客 微说 | 既要、又要、还要、更要!-蚊子的前端博客 又是一年的国庆雨季-蚊子的前端博客 微说 | 预估下2025年的出生人口数据-蚊子的前端博客 微说 | 可惜了我那些小时候的书本-蚊子的前端博客 微说 | 牛马有的是,驴不够了!-蚊子的前端博客 微说 | 能否有一条非户口也能高考的路-蚊子的前端博客 微说 | 小聊中医-蚊子的前端博客 微说 | 将要制作的一款新产品-蚊子的前端博客 微说 | 做人要有信-蚊子的前端博客 微说 | 好一个正义联盟-蚊子的前端博客 微说 | 都是见过吃过的主儿-蚊子的前端博客 微说 | 追求8小时工作制有错吗?-蚊子的前端博客 微说 | 如果尖锐的批评完全消失-蚊子的前端博客 微说 | 很好!-蚊子的前端博客 微说 | 封禁用户可以,但要告知具体原因-蚊子的前端博客 微说 | 面朝大海,春暖花开-蚊子的前端博客 微说 | 程序员的悲哀是什么?-蚊子的前端博客 微说 | 2025年出生人口的预测-蚊子的前端博客 前端在 LiveKit 中如何获取所有的参与者-蚊子的前端博客
使用 React 实现 6 个输入框的短信验证码功能-蚊子的前端博客
author · 2024-05-07 · via 蚊子的前端博客

在移动端中,经常会有下图中的交互方式,供用户输入短信验证码。

短信验证码输入

目前比较主流的两种实现方案:

  1. 每个单独的验证码,占据一个输入框;
  2. 灰色框框仅仅是展示,外层再盖上透明的输入框;

我们分别来说他们的优缺点和实现方式。

1. 每个数字单独占用一个输入框 #

按照真实个数的输入框来实现,输入框的最大长度设置为 1,然后监听所有的输入框,当输入框中有值时,若当前输入框是最后一个,则触发验证码的校验;若不是最后一个,则把光标移动到下一个输入框。

但这里存在着一个严重的问题:有的操作系统(如 iOS 等),可以帮助自动填充验证码,即便我们限制了输入框的长度,也会把所有验证码都填充到第一个输入框中。

我代码的实现:

const App = () => {
  const list = new Array(6).fill(0); // 初始数组,用来循环生成输入框
  const inputRef = useRef<InputRef[]>([]); // 获取所有的输入框的dom实例

  /**
   * 每个输入框中的数据变化时
   * @param {string} value 当前输入框中的数据
   * @param {number} curIndex 当前是第几个输入框
   */
  const handleChange = (value: string, curIndex: number) => {
    if (value) {
      if (curIndex < list.length - 1) {
        // 若当前输入框不是最后一个,则让下一个输入框获取到焦点
        inputRef.current[curIndex + 1].focus();
      } else {
        // 若当前输入框是最后一个,则拼接输入框中所有的验证码,然后进行验证
        let captcha = "";
        inputRef.current.forEach((item) => {
          captcha += item.nativeElement?.value || "";
        });

        onSuccess({ ...data, captcha });
      }
    }
  };

  /**
   * 监听键盘按下的事件,若是删除键,则删除验证码,同时光标回退
   */
  const handleKeyDown = (event: any, curIndex: number) => {
    const BACK_SPACE_CODE = 8; // 回退删除键

    if (event.keyCode !== BACK_SPACE_CODE) {
      // 这里只处理删除操作,若按键不是删除键,则不处理,直接返回
      return;
    }
    const hasValue = inputRef.current[curIndex].nativeElement?.value;
    if (!hasValue && curIndex) {
      // 若有数据,则删除数据,若没有数据,则回退光标
      inputRef.current[curIndex - 1].focus();
    }
  };


  return (
    <div className="input-list">
      {list.map((item) => (
        <Input
          key={item}
          type="tel"
          ref={(input) => {
            if (input && inputRef.current.length < list.length) {
              inputRef.current.push(input);
            }
          }}
          disabled={loading}
          maxLength={1}
          onChange={(value) => handleChange(value, item)}
          onKeyDown={(event) => handleKeyDown(event, item)}
        />
      ))}
    </div>
  );
};

在实际使用中,体验极度不友好,因此就放弃了这种方式。

2. 透明输入框 #

6 个灰色的框框仅用来展示使用,然后在这上面放一个透明的输入框用来进行输入。

但这里依然有一个体验上的问题,就是不能随意地切换光标。比如我中间有个验证码输错了,想只修改该数字,目前是不可以的,只能先删除后面的,然后再重新输入。

相比第一个方案的实现,目前光标无法随意切换的体验,相对来说还能接受这种方案。

具体光标在哪个灰框框中闪烁,则依据已输入验证码的长度。

const App = () => {
  const CAPTCHA_DEFAULT_LENGTH = 6; // 验证码的个数
  const list = new Array(CAPTCHA_DEFAULT_LENGTH).fill(1);
  const inputRef = (useRef < InputRef) | (null > null);

  const [inputActive, setInputActive] = useState(false); // 透明的验证码输入框是否获得焦点

  // 每个输入框中的数据变化时
  const handleChange = (value: string) => {
    setCaptcha(value);

    if (value.length >= CAPTCHA_DEFAULT_LENGTH) {
      // 验证码达到约定长度时,校验验证码
      onSuccess({ ...data, captcha: value });
    }
  };

  return (
    <div className="input-list">
      <div className="input-content">
        {list.map((_, index) => (
          <p
            key={index}
            className={classNames("input", {
              // 当光标在输入框中让当前正在输入的输入框的光标闪烁
              active: index === Math.min(captcha.length, CAPTCHA_DEFAULT_LENGTH - 1) && inputActive,
            })}
          >
            {captcha[index] || ""}
          </p>
        ))}
      </div>
      <Input
        type="tel"
        ref={inputRef}
        maxLength={CAPTCHA_DEFAULT_LENGTH}
        onChange={handleChange}
        onFocus={() => setInputActive(true)}
        onBlur={() => setInputActive(false)}
      />
    </div>
  );
};

3. 总结 #

没有完美的方案,只能在某些方面做一些取舍。