type="tel"是唯一兼顾数字键盘与空格输入的类型;需用@input拦截并手动格式化,避免v-model直绑导致光标错位,校验用/^1[3-9]\d{9}$/,格式化用/(\d{3})(\d{4})(\d{4})/,跨端取值兼容e.detail.value和e.target.value。

uni-app 中 type="tel" 是唯一能兼顾数字键盘与空格显示的类型
用 type="number" 会导致输入值被强制转为数字、无法插入空格,且在部分 Android 机型上会自动过滤非数字字符(包括空格和括号),直接破坏格式化逻辑。而 type="tel" 在所有平台都能唤起数字键盘,又允许你自由控制字符串内容——这是前提。
关键点在于:不要依赖 v-model 直接绑定格式化后的值,否则光标会跳到末尾;必须手动控制输入逻辑,用 @input 拦截原始输入,再计算出带空格的显示值。
- 始终用
@input而非@change,后者只在失焦时触发,无法实时响应 - 避免在事件中直接修改
v-model绑定的变量,否则 iOS 微信小程序会出现光标错位 - 推荐使用一个原始变量(如
rawPhone)存无格式数字,再用计算属性或方法生成带空格的显示值
正确提取并格式化手机号的正则写法:/^1[3-9]\d{9}$/ 和 /(\d{3})(\d{4})(\d{4})/
校验和格式化是两件事,不能混用同一个正则。校验用严格匹配 /^1[3-9]\d{9}$/,确保是合法 11 位大陆手机号;格式化则用分组捕获 /(\d{3})(\d{4})(\d{4})/,配合 replace 插入空格。
注意:不能用 /(\d{3})(\d{0,4})(\d{0,4})/ 这类宽松写法——它会在输入第 4 位时就提前插入空格,导致用户输入中断、光标乱跳。
- 输入过程中允许临时超长(比如输入到第 12 位),但最终提交前必须校验是否符合
/^1[3-9]\d{9}$/ - 格式化只对前 11 位生效,多余字符(如误按的字母)应被过滤,而不是参与分组
- 建议在
@input回调里先.replace(/\D/g, '')清除所有非数字字符,再截取前 11 位,最后分组格式化
跨端取值必须兼容 e.detail.value 和 e.target.value
微信小程序、支付宝小程序走的是 e.detail.value,H5 和 App 端走的是 e.target.value。如果只写一种,iOS 或 H5 就会拿不到值。
最简写法是:const value = e.detail?.value ?? e.target?.value ?? ''。别用 if (e.detail) 判断,某些低版本 H5 可能有 e.detail 但为空对象。
- 不要在
@input里直接操作 DOM(如document.getElementById),uni-app 不保证 DOM 存在 - 防抖不是必须的,但若后续要触发搜索或请求,建议用
_.debounce或自己实现简单定时器,避免高频触发 - 如果用了
uni-easyinput,它的v-model已封装跨端逻辑,但仍需手动处理格式化,因为组件不内置手机号分段逻辑
光标位置错乱是最高频的坑,必须手动修复
每次设置 v-model 绑定值后,iOS 和部分安卓 WebView 会把光标重置到末尾。用户输到“138 1234”,再输“5”,结果变成“138 12345|”,而不是“138 1234 5|”。
解决方式不是禁用格式化,而是记录上一次光标位置、对比新旧字符串差异、再用 setSelectionRange 手动还原。但 uni-app 的 input 组件不暴露原生 DOM,所以更实际的做法是:只在用户完成输入(比如失焦或点击提交)时做最终格式化,输入过程中仅做轻量过滤(去除非数字),不主动插入空格。
- 若坚持实时空格,必须用
ref获取 input 实例,再通过this.$nextTick延迟调用focus()和setSelectionRange - 真机测试比模拟器更重要——很多光标问题只在特定系统版本或厂商定制系统上复现
- 考虑降级方案:H5 和 App 端做实时格式化,小程序端只做输入过滤+失焦后格式化,体验反而更稳定


















