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

前言

最近在做手机验证码登录的需求,然后遇到一个问题,就是国内 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}
  />
);
相关推荐
li35744 小时前
将已有 Vue 项目通过 Electron 打包为桌面客户端的完整步骤
前端·vue.js·electron
Icoolkj4 小时前
VuePress 与 VitePress 深度对比:特性、差异与选型指南
前端·javascript·vue.js
excel5 小时前
CNN 分层详解:卷积、池化到全连接的作用与原理
前端
excel5 小时前
CNN 多层设计详解:从边缘到高级特征的逐层学习
前端
西陵6 小时前
Nx带来极致的前端开发体验——任务编排
前端·javascript·架构
大前端helloworld6 小时前
从初中级如何迈入中高级-其实技术只是“入门卷”
前端·面试
东风西巷8 小时前
Balabolka:免费高效的文字转语音软件
前端·人工智能·学习·语音识别·软件需求
萌萌哒草头将军8 小时前
10个 ES2025 新特性速览!🚀🚀🚀
前端·javascript·vue.js
半夏陌离8 小时前
SQL 入门指南:排序与分页查询(ORDER BY 多字段排序、LIMIT 分页实战)
java·前端·数据库