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

前言

最近在做手机验证码登录的需求,然后遇到一个问题,就是国内 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}
  />
);
相关推荐
左夕19 分钟前
分不清apply,bind,call?看这篇文章就够了
前端·javascript
Zha0Zhun1 小时前
一个使用ViewBinding封装的Dialog
前端
兆子龙1 小时前
从微信小程序 data-id 到 React 列表性能优化:少用闭包,多用 data-*
前端
滕青山1 小时前
文本行过滤/筛选 在线工具核心JS实现
前端·javascript·vue.js
时光不负努力1 小时前
编程常用模式集合
前端·javascript·typescript
恋猫de小郭1 小时前
Apple 的 ANE 被挖掘,AI 硬件公开,宣传的 38 TOPS 居然是"数字游戏"?
前端·人工智能·ios
小岛前端1 小时前
Node.js 宣布重大调整,运行十年的规则要改了!
前端·node.js
OpenTiny社区1 小时前
OpenTiny NEXT-SDK 重磅发布:四步把你的前端应用变成智能应用
前端·javascript·ai编程
梦想CAD控件2 小时前
在线CAD开发包结构与功能说明
前端·javascript·vue.js
张拭心2 小时前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能