You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React输入掩码问题:用户输入时替换占位符的手机号输入框

问题

需要创建一个带有特殊输入掩码的手机号输入框,占位符格式为(___) ___-____。用户输入时,占位符文本需逐步被输入内容替换,例如:
(12_) ___-____
(123) 4__-____
(123) 456-7___

当前代码存在两处问题:无法正确处理退格键,且光标位置控制异常。格式化函数如下:

export const formatTelphoneNumber = (value: string): string => {
    const phoneNumber = getDigitsOnly(value)

    const areaCode = phoneNumber.substring(0, 3)
    const nextThree = phoneNumber.substring(3, 6)
    const lastFour = phoneNumber.substring(6, 10)

    const formattedAreaCode = `${areaCode}${"_".repeat(3 - areaCode.length)}`
    const formattedNextThree = `${nextThree}${"_".repeat(3 - nextThree.length)}`
    const formattedLastFour = `${lastFour}${"_".repeat(4 - lastFour.length)}`

    return `(${formattedAreaCode}) ${formattedNextThree}-${formattedLastFour}`
}
解决方案

1. 修复退格键逻辑

原函数仅提取输入中的纯数字并拼接掩码,完全忽略了掩码符号(括号、空格、横杠)的存在。当用户退格时,如果光标停在掩码符号位置,或者删除的是掩码符号,原逻辑无法识别这种操作,导致退格后数字内容没有正确减少,甚至出现光标混乱。

解决思路:

  • 区分用户操作是删除数字还是掩码符号:如果光标在掩码符号区域,仅调整光标位置,不修改数字内容;如果在数字区域,再删除对应数字并重新格式化。
  • 通过映射光标位置与数字索引,精准定位需要删除的数字位。

2. 精准控制光标位置

格式化后的手机号字符串长度固定为14位((xxx) xxx-xxxx),用户输入或退格时,光标容易被默认逻辑跳到末尾或错误位置。需要建立光标位置与数字索引的映射关系,确保操作后光标停在正确的数字输入位。

核心映射规则

格式化字符串的结构与位置类型:

索引: 0 1 2 3 4 5 6 7 8 9 10 11 12 13
字符: ( x x x )  x x x - x  x  x  x
类型: M D D D M  D D D M D  D  D  D

(M=掩码位,D=数字位)

  • 输入数字时:若光标在掩码位,自动跳转到下一个数字位;输入完成后,光标移动到下一个有效数字位(跳过掩码位)。
  • 退格时:若光标在掩码位,先移动到前一个数字位再执行删除;删除后,光标停留在当前数字位的前一位(若前一位是掩码位则继续向前)。

优化后的完整代码

工具函数与格式化逻辑

// 提取输入中的纯数字
const getDigitsOnly = (value: string): string => {
  return value.replace(/\D/g, '');
};

// 根据光标位置计算对应的数字索引(忽略掩码位)
const getDigitIndexFromCursor = (cursorPos: number): number => {
  const maskIndexes = [0, 4, 8]; // 掩码字符的索引:( ) -
  let digitCount = 0;
  for (let i = 0; i < cursorPos; i++) {
    if (!maskIndexes.includes(i)) digitCount++;
  }
  return digitCount;
};

// 根据数字索引计算对应的光标位置
const getCursorFromDigitIndex = (digitIndex: number): number => {
  if (digitIndex === 0) return 1;
  if (digitIndex <= 3) return digitIndex + 1;
  if (digitIndex <= 6) return digitIndex + 2;
  return digitIndex + 3;
};

// 格式化手机号并返回新的光标位置
export const formatPhoneWithCursor = (
  value: string,
  cursorPos: number,
  isBackspace: boolean
): { formatted: string; newCursorPos: number } => {
  let digits = getDigitsOnly(value);
  const maxDigits = 10;

  // 处理退格操作
  if (isBackspace) {
    const targetDigitIndex = getDigitIndexFromCursor(cursorPos);
    if (targetDigitIndex > 0) {
      digits = digits.slice(0, targetDigitIndex - 1);
    }
  }

  // 生成带占位符的格式化字符串
  const areaCode = digits.slice(0, 3).padEnd(3, '_');
  const middleSegment = digits.slice(3, 6).padEnd(3, '_');
  const lastSegment = digits.slice(6, 10).padEnd(4, '_');
  const formatted = `(${areaCode}) ${middleSegment}-${lastSegment}`;

  // 计算新的光标位置
  let newCursor = getCursorFromDigitIndex(digits.length);
  if (!isBackspace && digits.length < maxDigits) {
    newCursor = Math.min(newCursor + 1, getCursorFromDigitIndex(digits.length + 1));
  }

  return { formatted, newCursorPos: newCursor };
};

输入组件实现

import { useState, useRef } from 'react';

const PhoneNumberInput = () => {
  const [inputValue, setInputValue] = useState('(___) ___-____');
  const inputRef = useRef<HTMLInputElement>(null);

  const handleBackspace = (e: React.KeyboardEvent<HTMLInputElement>) => {
    e.preventDefault();
    const cursorPos = inputRef.current?.selectionStart || 0;
    const { formatted, newCursorPos } = formatPhoneWithCursor(inputValue, cursorPos, true);
    setInputValue(formatted);
    setTimeout(() => {
      inputRef.current?.setSelectionRange(newCursorPos, newCursorPos);
    }, 0);
  };

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const cursorPos = inputRef.current?.selectionStart || 0;
    const { formatted, newCursorPos } = formatPhoneWithCursor(e.target.value, cursorPos, false);
    setInputValue(formatted);
    setTimeout(() => {
      inputRef.current?.setSelectionRange(newCursorPos, newCursorPos);
    }, 0);
  };

  return (
    <input
      ref={inputRef}
      value={inputValue}
      onChange={handleInputChange}
      onKeyDown={(e) => e.key === 'Backspace' && handleBackspace(e)}
      maxLength={14}
      type="tel"
    />
  );
};

export default PhoneNumberInput;

内容的提问来源于stack exchange,提问作者Michael Pugh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 10:17:04