在移动端中,经常会有下图中的交互方式,供用户输入短信验证码。
目前比较主流的两种实现方案:
- 每个单独的验证码,占据一个输入框;
- 灰色框框仅仅是展示,外层再盖上透明的输入框;
我们分别来说他们的优缺点和实现方式。
1. 每个数字单独占用一个输入框 #
按照真实个数的输入框来实现,输入框的最大长度设置为 1,然后监听所有的输入框,当输入框中有值时,若当前输入框是最后一个,则触发验证码的校验;若不是最后一个,则把光标移动到下一个输入框。
但这里存在着一个严重的问题:有的操作系统(如 iOS 等),可以帮助自动填充验证码,即便我们限制了输入框的长度,也会把所有验证码都填充到第一个输入框中。
我代码的实现:
const App = () => {
const list = new Array(6).fill(0); // 初始数组,用来循环生成输入框
const inputRef = useRef<InputRef[]>([]); // 获取所有的输入框的dom实例
/**
* 每个输入框中的数据变化时
* @param {string} value 当前输入框中的数据
* @param {number} curIndex 当前是第几个输入框
*/
const handleChange = (value: string, curIndex: number) => {
if (value) {
if (curIndex < list.length - 1) {
// 若当前输入框不是最后一个,则让下一个输入框获取到焦点
inputRef.current[curIndex + 1].focus();
} else {
// 若当前输入框是最后一个,则拼接输入框中所有的验证码,然后进行验证
let captcha = "";
inputRef.current.forEach((item) => {
captcha += item.nativeElement?.value || "";
});
onSuccess({ ...data, captcha });
}
}
};
/**
* 监听键盘按下的事件,若是删除键,则删除验证码,同时光标回退
*/
const handleKeyDown = (event: any, curIndex: number) => {
const BACK_SPACE_CODE = 8; // 回退删除键
if (event.keyCode !== BACK_SPACE_CODE) {
// 这里只处理删除操作,若按键不是删除键,则不处理,直接返回
return;
}
const hasValue = inputRef.current[curIndex].nativeElement?.value;
if (!hasValue && curIndex) {
// 若有数据,则删除数据,若没有数据,则回退光标
inputRef.current[curIndex - 1].focus();
}
};
return (
<div className="input-list">
{list.map((item) => (
<Input
key={item}
type="tel"
ref={(input) => {
if (input && inputRef.current.length < list.length) {
inputRef.current.push(input);
}
}}
disabled={loading}
maxLength={1}
onChange={(value) => handleChange(value, item)}
onKeyDown={(event) => handleKeyDown(event, item)}
/>
))}
</div>
);
};
在实际使用中,体验极度不友好,因此就放弃了这种方式。
2. 透明输入框 #
6 个灰色的框框仅用来展示使用,然后在这上面放一个透明的输入框用来进行输入。
但这里依然有一个体验上的问题,就是不能随意地切换光标。比如我中间有个验证码输错了,想只修改该数字,目前是不可以的,只能先删除后面的,然后再重新输入。
相比第一个方案的实现,目前光标无法随意切换的体验,相对来说还能接受这种方案。
具体光标在哪个灰框框中闪烁,则依据已输入验证码的长度。
const App = () => {
const CAPTCHA_DEFAULT_LENGTH = 6; // 验证码的个数
const list = new Array(CAPTCHA_DEFAULT_LENGTH).fill(1);
const inputRef = (useRef < InputRef) | (null > null);
const [inputActive, setInputActive] = useState(false); // 透明的验证码输入框是否获得焦点
// 每个输入框中的数据变化时
const handleChange = (value: string) => {
setCaptcha(value);
if (value.length >= CAPTCHA_DEFAULT_LENGTH) {
// 验证码达到约定长度时,校验验证码
onSuccess({ ...data, captcha: value });
}
};
return (
<div className="input-list">
<div className="input-content">
{list.map((_, index) => (
<p
key={index}
className={classNames("input", {
// 当光标在输入框中,让当前正在输入的输入框的光标闪烁
active: index === Math.min(captcha.length, CAPTCHA_DEFAULT_LENGTH - 1) && inputActive,
})}
>
{captcha[index] || ""}
</p>
))}
</div>
<Input
type="tel"
ref={inputRef}
maxLength={CAPTCHA_DEFAULT_LENGTH}
onChange={handleChange}
onFocus={() => setInputActive(true)}
onBlur={() => setInputActive(false)}
/>
</div>
);
};
3. 总结 #
没有完美的方案,只能在某些方面做一些取舍。