【手机验证码】手机号格式化光标异常问题

前言

最近在做手机验证码登录的需求,然后遇到一个问题,就是国内 11 位手机号的显示需要按照 3 4 4 的格式显示,比如 138 1234 5678。实现起来倒也不麻烦,写个格式化函数就可以了。但是后面自己测试的时候,发现了一个问题,就是格式化后在删除或者新增数字的时候,光标会发生奇怪的跳到。具体情况详见下面

案例展示

代码采用 react 写的,具体如下:

js 复制代码
const [formmatPhone, setFormmatPhone] = useState('');
const getFormmatPhone = (phone) => {
  // 按 3 4 4 的格式格式化,中间添加空格
  if (phone.length <= 3) {
    return phone;
  } else if (phone.length <= 7) {
    return `${phone.slice(0, 3)} ${phone.slice(3)}`;
  } else {
    return `${phone.slice(0, 3)} ${phone.slice(3, 7)} ${phone.slice(7, 11)}`;
  }
};

const handleChange = (e) => {
  console.log(e.target.value);
  // 只需前11位数字
  const value = e.target.value.replace(/[^\d]/g, '').slice(0, 11);
  setFormmatPhone(getFormmatPhone(value));
};

return <input type="tel" value={formmatPhone} onChange={handleChange} />;

具体可以看下图,主要就是在空格处进行删除或者有空格前面添加会导致光标跳动到最后。 但是假如将代码 handleChange 时 setFormmatPhone 不再进行 getFormmatPhone,直接 setFormmatPhone(value)就不会产生该问题。

问题分析

这个问题主要是由 React 的受控组件+ 格式化输入共同作用引起的。核心原因是:每次输入变更时,setFormmatPhone(getFormmatPhone(value)) 会更新 value,但这个新值和用户当前光标位置不一致,React 会强制让 input 重新渲染,从而把光标跳到结尾。 因此,主要原因就在于 判断当前 DOM input 的值是否和即将渲染的值一样

以 "138 1234 567" 为例:

  1. 用户在 1234 前面输入一个数字变为 "138 11234 567";
  2. 然后代码调用 getFormmatPhone() 自动格式化为 "138 1123 4567";
  3. 但此时 React 不知道你当前光标在 "1123" 的哪里,于是它默认把光标放到结尾。

解决方案

询问 ai 后,给出的解决方案是手动设置光标位置,具体代码如下:

jsx 复制代码
const [formmatPhone, setFormmatPhone] = useState('');
const inputRef = useRef(null);
const getFormmatPhone = (phone) => {
  // 按 3 4 4 的格式格式化,中间添加空格
  if (phone.length <= 3) {
    return phone;
  } else if (phone.length <= 7) {
    return `${phone.slice(0, 3)} ${phone.slice(3)}`;
  } else {
    return `${phone.slice(0, 3)} ${phone.slice(3, 7)} ${phone.slice(7, 11)}`;
  }
};

const handleChange = (e) => {
  const rawValue = e.target.value;
  const selectionStart = e.target.selectionStart;

  // 提取数字
  const numeric = rawValue.replace(/\D/g, '').slice(0, 11);
  const formatted = getFormmatPhone(numeric);

  // 计算新光标位置(根据数字在旧字符串中位置推断)
  const digitsBeforeCursor = rawValue
    .slice(0, selectionStart)
    .replace(/\D/g, '').length;

  let newCursor = 0;
  let digitCount = 0;
  for (let i = 0; i < formatted.length; i++) {
    if (/\d/.test(formatted[i])) digitCount++;
    if (digitCount === digitsBeforeCursor) {
      newCursor = i + 1;
      break;
    }
  }

  if (digitsBeforeCursor === 0) newCursor = 0;
  if (digitCount < digitsBeforeCursor) newCursor = formatted.length;

  setFormmatPhone(formatted);

  // 等待 DOM 更新后设置光标
  requestAnimationFrame(() => {
    inputRef.current?.setSelectionRange(newCursor, newCursor);
  });
};

return (
  <input
    type="tel"
    ref={inputRef}
    value={formmatPhone}
    onChange={handleChange}
  />
);
相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax