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

前言

最近在做手机验证码登录的需求,然后遇到一个问题,就是国内 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}
  />
);
相关推荐
AbrahamCS14 分钟前
从角色协同到可运行图:Graph Engineer 与多智能体编排
前端·人工智能·智能体
BigTopOne25 分钟前
VS Code IntelliSense 配置(解决 #include 找不到的报红)
前端
愚公搬代码27 分钟前
【愚公系列】《Web应用安全》005-Visual Studio Code编辑器的使用
前端·安全·编辑器
用户9210802628630 分钟前
2. 以 Bubble 为例,看 Ant Design X Vue 组件是怎么设计的
前端
Liora_Yvonne33 分钟前
Monorepo 里的 shared、services 和 ui 到底怎么划分?
前端
PedroQue9935 分钟前
uni-router v2.1.0 升级:导航守卫全面支持返回值模式
前端·uni-app
猫七先生39 分钟前
从零到一:个人博客自动化部署踩坑全记录
前端
淼澄研学41 分钟前
Python调用通义千问API实现长尾搜题内容自动化生成实战
前端·react.js·架构
绿岛之北43 分钟前
Electron 安全入门:为什么一个 XSS 可能变成 RCE?
前端·electron
舒灿1 小时前
DeepSeek Harness——Agent自我进化的实现途径?
前端·ai编程·deepseek