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

前言

最近在做手机验证码登录的需求,然后遇到一个问题,就是国内 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}
  />
);
相关推荐
CappuccinoRose1 分钟前
JavaScript 学习文档(二)
前端·javascript·学习·数据类型·运算符·箭头函数·变量声明
这儿有一堆花7 分钟前
Vue 是什么:一套为「真实业务」而生的前端框架
前端·vue.js·前端框架
全栈前端老曹22 分钟前
【MongoDB】深入研究副本集与高可用性——Replica Set 架构、故障转移、读写分离
前端·javascript·数据库·mongodb·架构·nosql·副本集
NCDS程序员1 小时前
v-model: /v-model/ :(v-bind)三者核心区别
前端·javascript·vue.js
夏幻灵1 小时前
CSS三大特性:层叠、继承与优先级解析
前端·css
小杨同学呀呀呀呀1 小时前
Ant Design Vue <a-timeline>时间轴组件失效解决方案
前端·javascript·vue.js·typescript·anti-design-vue
华玥作者9 小时前
[特殊字符] VitePress 对接 Algolia AI 问答(DocSearch + AI Search)完整实战(下)
前端·人工智能·ai
Mr Xu_10 小时前
告别冗长 switch-case:Vue 项目中基于映射表的优雅路由数据匹配方案
前端·javascript·vue.js
前端摸鱼匠10 小时前
Vue 3 的toRefs保持响应性:讲解toRefs在解构响应式对象时的作用
前端·javascript·vue.js·前端框架·ecmascript
lang2015092810 小时前
JSR-340 :高性能Web开发新标准
java·前端·servlet