Vuetify VOtpInput 组件深度指南:用 v-otp-input 构建 MFA 一次性密码输入
【免费下载链接】vuetify🐉 Vue Component Framework项目地址: https://gitcode.com/gh_mirrors/vu/vuetify
本指南围绕 Vuetify 的VOtpInput(v-otp-input)组件展开,系统讲解其在邮箱/短信一次性密码(OTP)、多因素认证(MFA)场景中的完整用法。你将掌握组件的全部核心 props(长度、掩码、合并、字符模式限制等)、finish事件与v-model的数据流、fields插槽自定义布局、以及隐藏输入框背后的 grapheme 分段与 IME 输入处理等源码级原理,可直接用于构建生产级验证码界面。
组件定位与典型场景
OTP(One-Time Password)输入框是 MFA 认证流程的标准交互形式:服务端通过邮件或短信向用户下发一次性验证码,用户在若干字符槽中逐位输入。v-otp-input正是为这一场景设计的组件,官方文档(otp-input.md)将其定位为 “used for MFA authentication via input field”。
它具备以下开箱即用的能力:
- 渲染可配置数量的字符字段,默认 6 位;
- 自动处理键盘导航(方向键、退格键)、整段粘贴与浏览器/系统自动填充(
autocomplete="one-time-code"); - 支持密码掩码、字符模式限制(数字/字母/混合/Unicode)、加载与错误状态;
- 在填满全部字段时通过
finish事件通知外部。
基础用法
最简单的使用方式与任何表单输入组件一致,仅需一行:
<v-otp-input></v-otp-input>配合v-model双向绑定当前已输入的字符串,并监听finish事件判断用户是否输完:
<template> <v-otp-input v-model="otp" @finish="verifyOtp" ></v-otp-input> </template> <script setup> const otp = ref('') function verifyOtp (value) { console.log('OTP 已输入完毕:', value) // 调用后端接口校验验证码 } </script>官方使用示例(usage.vue)展示了通过配置面板实时调整length、placeholder、disabled、loading、pattern、focus-all、variant等参数的效果,是快速理解各 props 交互差异的起点。
组件解剖:单一隐藏输入 + 多个视觉字段
v-otp-input的 DOM 结构与传统“多个独立输入框”的实现完全不同。从源码(VOtpInput.tsx)可以看到,其渲染结构为:
| 结构层 | 说明 | | - | - | |.v-otp-input容器 | 持有全部视觉字段与隐藏输入框,居中布局 | |.v-otp-input__content| 字段与分隔符的排列区域,可容纳fields、divider、loader、default插槽 | |.v-otp-input__field| 由VField渲染的单个字符槽(对应官方文档 Anatomy 中的 “Field”) | |.v-otp-input__input| 一个透明的<input type="text">,承载真实的键盘交互、粘贴与自动填充 |
关键设计是:整个组件只有唯一一个真实输入框,它被定位到容器底部、仅有 1px 大小、文字与光标透明(样式见 VOtpInput.sass)。而在触屏设备(any-pointer: coarse)上,该隐藏输入框会扩展铺满整个区域,让移动端键盘、IME 候选框与系统自动填充栏正常工作。
每个字符槽则由VOtpField内部的VField渲染,并根据槽位数据展示三种内容之一:
- 输入中的字符(或掩码后的
•); - IME 组合中的临时字符(
.v-otp-input__composition); - 虚拟光标(
.v-otp-input__caret,闪烁动画)或占位符。
槽位数据由useOtpInput的slotscomputed 计算得出(useOtpInput.ts),每个槽包含char、compositionChar、placeholderChar、isActive、hasFakeCaret五个字段。
Props 详解
v-otp-input支持大部分v-field的 props,并遵循与其他输入组件一致的密度、尺寸与焦点设计模式。其 props 定义见 VOtpInput.tsx。
length:字符位数
length决定渲染多少个字符槽,默认值为6,支持数字或字符串形式传入:
<v-otp-input length="4"></v-otp-input>源码中通过toRef(() => Number(props.length))统一转为数字。注意length同时约束了隐藏输入框可接受的最大字符数(maxLength以槽数为准,而非代码单元数),因此输入内容永远不会超过槽位数量。
autofocus 与 focus-all:聚焦行为
官方文档将两个焦点相关 props 并列说明:
autofocus:组件挂载并进入视口后自动聚焦。实现上借助useIntersectionObserver(VOtpInput.tsx),即只有元素真正可见时才触发聚焦,避免页面加载瞬间抢焦点;focus-all:聚焦时高亮全部字段。VOtpField通过判断isFocused && focusAll && !isActive给非活动槽添加v-otp-input__field--highlighted类(VOtpField.tsx),其描边会以 50% 透明度呈现(见 VOtpInput.sass)。
error:错误状态
error将组件整体置入错误态,用于展示校验失败。它被透传给内部的VField(provideDefaults统一注入,见 VOtpInput.tsx),因此错误边框、错误色等样式与v-text-field等输入组件完全一致。示例见 prop-error.vue。
variant:外观变体
variant支持与v-field、v-text-field相同的全部变体,默认值为outlined。可用的典型值包括filled、plain、solo、solo-filled、solo-inverted、underlined、outlined等。示例见 prop-variant.vue。
masked:掩码显示
masked隐藏已输入的字符,效果类似type="password",每个字符显示为•。与密码框不同的是,它仍可配合type="number"将键盘限制为数字键盘。源码中isMasked = masked || type === 'password'(useOtpInput.ts),即type="password"也会自动触发掩码。示例见 prop-masked.vue。
loader:加载状态
loader在组件进入加载态时显示加载指示器。其实现是在内容上方叠加一个VOverlay(contained模式),默认渲染一个 24px 的VProgressCircular(indeterminate 无限旋转),也可通过loader插槽自定义。loading支持布尔值与字符串(此时字符串作为加载圈颜色)。示例见 prop-loader.vue。
merged:合并分组
merged将所有字段渲染为一个连续连接的组:共享统一的圆角与投影,字段之间无间隙,边框相接(通过margin-inline-start: -1px重叠轮廓线,聚焦或悬停时恢复边框宽度以突出当前槽,见 VOtpInput.sass)。示例见 prop-merged.vue。
pattern:字符模式限制
pattern限制可接受的字符。它支持以下预设值与自定义正则:
| 预设值 | 对应正则(useOtpInput.ts) | 说明 | | - | - | - | |numeric|/[0-9]/| 仅数字 | |alpha|/[a-zA-Z]/| 仅英文字母 | |alphanumeric|/[a-zA-Z0-9]/| 字母与数字 | |unicode-alpha|/\p{L}/u| 任意语言的字母(含 CJK、西里尔、带重音字符) | |unicode-alphanumeric|/[\p{L}\p{N}]/u| 任意语言的字母与数字 |
关键行为:
- 当
type="number"时,即使未显式设置pattern,也会自动默认为numeric; - 也可传入自定义
RegExp对象,例如仅允许大写字母::pattern="/[A-Z]/"; - 过滤基于 grapheme(字素簇)而非单个代码点,因此 emoji、ZWJ 序列、肤色修饰符都能作为一个整体被校验;
inputMode会根据生效的 pattern 自动切换:numeric模式返回numeric,否则返回text,从而在移动端自动弹出对应键盘。
示例见 prop-pattern.vue,其中分别演示了numeric、alpha与自定义大写正则/[A-Z]/三种情况。
divider:字段间分隔符
divider接受一个字符串,渲染在每个字段之间的分隔位置:
<v-otp-input divider="-"></v-otp-input>从源码(VOtpInput.tsx)可见,分隔符出现在索引非 0 的字段之前,由VOtpSeparator渲染,同时根元素会加上v-otp-input--divided类并放宽内容最大宽度($otp-input-divided-content-max-width)。示例见 prop-divider.vue。
其他透传 props
组件还透传了v-field的部分 props 与通用输入 props:
- 视觉相关:
color、bgColor、baseColor、rounded、theme、disabled、loading、class、style; - 输入相关:
label(默认$vuetify.input.otp,会作为隐藏输入框的aria-label,便于无障碍访问)、placeholder(显示于空槽位的占位字符)、type(text/password/number,默认number)、modelValue(默认空字符串); - 密度与尺寸:
density、height、width、max-width等(来自makeDensityProps与makeDimensionProps)。
事件与组件实例方法
组件仅发出三个事件(VOtpInput.tsx):
| 事件 | 载荷 | 触发时机 | | - | - | - | |update:modelValue| 字符串 | 每次输入变化 | |finish| 完整字符串 | 输入长度恰好等于length时(watch 判定val.length === length.value) | |update:focused| 布尔 | 焦点变化 |
同时,通过模板 ref 可调用组件实例方法(见 VOtpInput.tsx):
focus():聚焦隐藏输入框;blur():失焦;reset():清空全部内容、选区与组合状态。
Slots:插槽体系
divider 插槽
divider插槽用于自定义字段间的分隔内容,并接收分隔符的index作为插槽 prop(从 1 开始计数):
<v-otp-input> <template #divider="{ index }"> <v-icon>{{ index % 2 ? 'mdi-minus' : 'mdi-dot' }}</v-icon> </template> </v-otp-input>插槽实现见 slot-divider.vue。当使用插槽而非字符串时,dividerprop 可以为空,插槽内容会取代默认字符串。
fields 插槽与三个子组件
fields插槽是最强大的扩展点,它允许完全重排字段结构。结合三个子组件可以构建任意布局:
v-otp-field:渲染单个字符槽,必须传入:index(对应length中的槽位序号)。它必须位于v-otp-input内部,否则会抛出VOtpField must be used inside VOtpInput错误(VOtpField.tsx);v-otp-group:将多个字段包成一组,merged属性可独立控制该组是否合并(默认继承父级v-otp-input的merged,见 VOtpGroup.tsx)。合并时组会计算子节点数量并分配 flex 权重;v-otp-separator:显示在字段/组之间的视觉分隔元素,默认插槽可接收任意内容(文本、图标等),本质是createSimpleFunctional('v-otp-input__divider')生成的轻量组件(VOtpSeparator.tsx)。
例如经典的 “3-3” 分组验证码(示例见 misc-custom-layout.vue):
<v-otp-input v-model="otp"> <template #fields> <v-otp-group merged> <v-otp-field :index="0"></v-otp-field> <v-otp-field :index="1"></v-otp-field> <v-otp-field :index="2"></v-otp-field> </v-otp-group> <v-otp-separator>-</v-otp-separator> <v-otp-group merged> <v-otp-field :index="3"></v-otp-field> <v-otp-field :index="4"></v-otp-field> <v-otp-field :index="5"></v-otp-field> </v-otp-group> </template> </v-otp-input>同一个示例中还演示了 “2-4 混合”布局:前两个字段为独立(非合并)槽,中间用v-icon图标作分隔符,后四个字段合并为组。
loader 与 default 插槽
loader:自定义加载指示器内容,取代默认的VProgressCircular;default:渲染在内容区域末尾,可用于放置辅助提示文案。
实战场景示例
官方文档提供了三个贴近真实业务的示例:
卡片内的验证码
v-misc-card.vue 将v-otp-input放入v-card,配合标题、说明文字与提交按钮,构成完整的 MFA 弹窗卡片。适合在登录、支付确认等场景中直接复用。
移动端短信验证码
misc-mobile.vue 模拟移动端短信验证码交互,展示了在窄屏与触屏设备上(隐藏输入框铺满组件区域)的输入体验,以及发送验证码按钮与倒计时等常见配套元素。
账户验证
misc-verify.vue 演示完整的账户验证流程:接收验证码 → 输入 → 点击验证。可结合finish事件自动触发校验,配合errorprop 展示服务端校验失败信息。
源码级原理:useOtpInput 与输入管线
组件的全部输入逻辑收敛在useOtpInputcomposable(useOtpInput.ts)中,理解它可以解释许多看似“魔法”的行为:
1. Grapheme(字素簇)分段组件用Intl.Segmenter将文本按字素切分,槽位边界基于字素而非 UTF-16 代码单元。这意味着 emoji、ZWJ 序列、组合字符(如肤色修饰符)各占一个槽位,选区换算在“字素空间”与“代码单元空间”之间双向转换,保证setSelectionRange与槽位渲染始终一致。
2. 字符过滤与截断所有写入路径(setValue、insert)都经过filter(按生效 pattern 过滤非法字素)与clampGraphemes(按length截断)。onBeforeinput还会在插入前拦截非法字符(VOtpInput.tsx),实现“输不进去”而非“输进去再删”。
3. 粘贴与自动填充onPaste读取剪贴板纯文本、去首尾空白、过滤非法字符后插入当前选区(VOtpInput.tsx)。隐藏输入框声明了autocomplete="one-time-code",让 iOS/Android 与桌面浏览器的 OTP 自动填充(短信提取码)能直接写入组件。
4. IME 与组合输入组件完整处理compositionstart/update/end事件:组合中的内容先显示在compositionChar槽位(斜体下划线样式,见 VOtpInput.sass),compositionend后才提交到模型。对 CJK 输入法,组合内容通过覆盖层渲染,避免与隐藏输入框的选区互相干扰;同时通过IME_SCRIPT_RE识别汉字、假名、谚文等脚本,兼容死键与三星预测输入等非 CJK 组合场景。
5. 选区同步组件监听原生selectionchange事件将系统选区同步为“至少覆盖一个槽”的选区(syncSelection),保证任意时刻都有一个槽处于 active 状态;方向键移动按字素计算,Shift+方向键扩展选区支持多槽批量删除。
6. RTL 支持组件接入useRtl:根输入框方向切换为rtl,方向键移动方向自动取反(VOtpInput.tsx),分隔符与合并组的样式同样使用逻辑属性(margin-inline-start等)适配镜像布局。
样式定制
组件的视觉样式集中在 VOtpInput.sass,可通过 _variables.scss 中的 Sass 变量覆盖,例如:
$otp-input-content-gap:字段/分隔符间距,默认0.5rem;$otp-input-content-height:内容区高度,默认64px(随 density 变化);$otp-input-content-max-width:内容区最大宽度,默认320px(启用divider时为360px);$otp-input-field-font-size:字段字体大小,默认1.25rem;$otp-input-divider-margin:分隔符外边距,默认0 8px。
示例中加载圈的尺寸与颜色也可通过loader插槽或loading字符串值调整。
测试保障
组件行为由浏览器级测试覆盖(VOtpInput.spec.browser.tsx),涉及输入、粘贴、键盘导航、焦点与事件触发等场景。在自行扩展或修复该组件时,可运行对应测试验证行为不回归。
小结
v-otp-input是一个“内部结构复杂、对外 API 简洁”的组件:对使用者而言,只需v-model、length、pattern、finish即可完成 90% 的 MFA 验证码需求;对需要定制体验的团队,fields插槽配合v-otp-field/v-otp-group/v-otp-separator三个子组件可以构建任意分组布局,而useOtpInput中基于 grapheme 的输入管线保证了多语言、IME、粘贴与自动填充场景下的输入一致性。更多组合方式可继续查阅 VOtpInput 源码目录与 示例目录。
【免费下载链接】vuetify🐉 Vue Component Framework项目地址: https://gitcode.com/gh_mirrors/vu/vuetify
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考