Unaipp 使用 wot UI 实现一个带数字键盘的密码输入框弹窗
最近项目里有个支付输入密码的需求,所以在这之前都是使用一个简单的输入框实现的,但是这样体验不太好。所以,这次就改成了弹窗,尝试达到类似支付宝的弹窗输入密码的形式。
前言
在 Wot UI 中是有密码输入框(wd-password-input)和数字键盘(wd-number-keyboard)两个组件的,但是在文档示例中你会发现,数字键盘是以弹窗的形式覆盖在界面顶层的。如果我们直接使用这个组件,就会出现弹窗盖在弹窗上的奇怪问题。
所以最好的方式,是改写数字键盘组件的全局样式,再将其和密码输入框组合起来,放到新的弹窗中。
防止数字键盘下沉
打开控制台管擦,我们会发现数字键盘实际上也是一个弹窗,而内部会通关组件参数 v-model:visible 进行更新。
因此,首先我们要设置 :hide-on-click-outside="false",防止数字键盘因为点击蒙版意外关闭。
<wd-keyboard
class="pass-keyboard"
:hide-on-click-outside="false"
v-model:visible="showKeyboard"
mode="custom"
:close-text="confirmText"
@input="onPassInput"
@close="handlePassClose"
@delete="onPassDelete"
></wd-keyboard>
然后我们会发现一旦点击左下角的键盘按钮,数字键盘就会被收起来,只有点击密码输入框才能弹出。显然这不是我们想要的效果,最终效果应该是数字输入框和密码输入框固定的一直显示。通过观察,弹窗的显示是通过 display 和过渡动画实现的,那么最有效的方式就是样式覆盖了
.pass-keyboard {
:deep(.wd-popup) {
position: relative;
transition: none;
display: block !important;
}
}
我们还需要禁止初始化时,弹窗淡入淡出的动画,防止数字键盘出现延迟显示,闪烁的问题
.pass-keyboard {
:deep(.wd-slide-up-enter),
:deep(.wd-slide-up-leave-to) {
transform: none;
}
}
到这里,我们就能够让数字键盘固定到界面中,作为一个普通的组件使用了。
在悬浮面板中组合 密码输入框 和 数字键盘
现在,我们把 密码输入框 和 数字键盘同时放进 Wot IU 的底部弹窗组件(wd-popup)中,会发现两个组件没有联动起来,所以还需要配合密码输入框的焦点事件, 让数字键盘一直显示。
...
<wd-password-input
v-model="payPassword"
:length="maxLength"
:gutter="10"
:mask="true"
:focused="showKeyboard"
@focus="handlePasswordFocus"
/>
<wd-keyboard
class="pass-keyboard"
:hide-on-click-outside="false"
v-model:visible="showKeyboard"
></wd-keyboard>
...
// 处理密码框聚焦
function handlePasswordFocus() {
// 强制显示键盘
showKeyboard.value = true;
}
这样我们就基本完成在不弹出系统输入法的情况下,使用数字虚拟键盘输入框密码的操作了。但是到这里你会发现支付宝的密码弹窗都是自动完成后关闭的,现在我们实现的功能,不能做到自动未完成和关闭弹窗。
不过,我们可以通过自定义数字键盘,增加提交按钮,并监听点击事件实现这个操作。在 @close 我们将关闭动作传递到父组件,让父组件直接关闭最外层的弹窗就可以了。
<wd-keyboard
class="pass-keyboard"
:hide-on-click-outside="false"
v-model:visible="showKeyboard"
mode="custom"
:close-text="confirmText"
@input="onPassInput"
@close="handlePassClose"
@delete="onPassDelete"
></wd-keyboard>
// 处理关闭 - 点击确定按钮后直接关闭弹窗
function handlePassClose() {
if (payPassword.value.length < 6) return;
// 触发输入完成事件
emit("input-complete", payPassword.value);
}
如果需要自动完成,那么就直接监听密码输入框的输入位数,手动调用上面的关闭事件就可以了
// 监听密码变化
watch(payPassword, (newVal) => {
// 密码输入完成后的处理
if (newVal.length === props.maxLength) {
// 如果启用自动关闭
if (props.autoConfirm) {
// 延迟关闭,让用户能看到输入完成的效果
setTimeout(() => {
handlePassClose();
}, 300);
}
}
});
完整实例
最后,我把这个功能封装成了一个组件,只需要在项目中引用这个组件,并且根据输入完成事件做进一步处理就行了。唯一不足的是,当密码输入错误时,不能像支付宝一样停留在弹窗输入层,只能退其次统一关闭后处理接口请求传参。
<template>
<view>
<wd-popup v-model="showPasswordPopup" position="bottom" round :close-on-click-overlay="true">
<view class="pay-pass-popup">
<div class="pass-top">
<view class="popup-title"> {{ title }} </view>
<!-- 密码长度提示 -->
<view v-if="showLengthHint" class="password-length-hint">
{{ payPassword.length }}/{{ maxLength }}
</view>
<!-- 密码输入框 -->
<wd-password-input
v-model="payPassword"
:length="maxLength"
:gutter="10"
:mask="mask"
:focused="showKeyboard"
@focus="handlePasswordFocus"
/>
</div>
<wd-keyboard
class="pass-keyboard"
:hide-on-click-outside="false"
v-model:visible="showKeyboard"
mode="custom"
:close-text="confirmText"
@input="onPassInput"
@close="handlePassClose"
@delete="onPassDelete"
></wd-keyboard>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
// 定义Props
interface Props {
// 弹窗标题
title?: string;
// 确认按钮文本
confirmText?: string;
// 是否显示弹窗
visible?: boolean;
// 密码最大长度
maxLength?: number;
// 是否显示密码长度提示
showLengthHint?: boolean;
// 是否隐藏密码(显示为圆点)
mask?: boolean;
// 是否自动关闭(输入完成后)
autoConfirm?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
title: "请输入支付密码",
confirmText: "确定",
visible: true,
maxLength: 6,
showLengthHint: false,
mask: true,
autoConfirm: false,
});
// 定义Emits
const emit = defineEmits<{
"input-complete": [value: string];
}>();
const payPassword = ref<string>("");
const showPasswordPopup = defineModel("visible", { default: false });
// 显示键盘
const showKeyboard = ref<boolean>(true);
// 监听密码变化
watch(payPassword, (newVal) => {
// console.log("当前密码:", newVal);
// 密码输入完成后的处理
if (newVal.length === props.maxLength) {
// 如果启用自动关闭
if (props.autoConfirm) {
// 延迟关闭,让用户能看到输入完成的效果
setTimeout(() => {
handlePassClose();
}, 300);
}
}
});
// 键盘输入处理 - 只接受数字
function onPassInput(val: string) {
// 只接受数字输入
if (!/^\d$/.test(val)) {
return;
}
// 如果已经输入到最大长度,不再接受输入
if (payPassword.value.length >= props.maxLength) {
return;
}
// 添加数字到密码
payPassword.value += val;
}
// 删除处理
function onPassDelete() {
if (payPassword.value.length > 0) {
// 删除最后一位
payPassword.value = payPassword.value.slice(0, -1);
}
}
// 处理密码框聚焦
function handlePasswordFocus() {
// 强制显示键盘
showKeyboard.value = true;
}
// 处理关闭 - 点击确定按钮后直接关闭弹窗
function handlePassClose() {
if (payPassword.value.length < 6) return;
// 触发输入完成事件
emit("input-complete", payPassword.value);
// 关闭密码输入弹窗
// close();
}
// 清空密码
function clearPassword() {
payPassword.value = "";
}
// 打开弹窗
function open() {
clearPassword();
showPasswordPopup.value = true;
}
// 关闭弹窗
function close() {
showPasswordPopup.value = false;
clearPassword();
}
// 获取当前密码
function getPassword(): string {
return payPassword.value;
}
// 暴露方法给父组件
defineExpose({
open,
close,
clearPassword,
getPassword,
});
</script>
<style lang="scss" scoped>
.pay-pass-popup {
justify-content: center;
}
.pass-top {
background-color: #ffffff;
padding: 40rpx;
display: flex;
flex-direction: column;
gap: 20px;
}
.popup-title {
font-size: 32rpx;
font-weight: bold;
text-align: center;
color: #333;
}
.password-length-hint {
font-size: 24rpx;
text-align: center;
color: #999;
margin-top: -10rpx;
}
.pass-keyboard {
padding: 40rpx 0;
background-color: #f5f5f5;
:deep(.wd-popup) {
position: relative;
transition: none;
display: block !important;
}
:deep(.wd-key.wd-key--close) {
background: linear-gradient(37deg, #ff3945 5%, #ff9c4a 80%);
color: white;
font-weight: bold;
}
:deep(.wd-key) {
font-size: 32rpx;
font-weight: 500;
}
:deep(.wd-key:active) {
background-color: #e0e0e0;
}
:deep(.wd-key--close:active) {
background: linear-gradient(37deg, #e6323d 5%, #e68c45 80%);
}
:deep(.wd-keyboard__keys) {
padding: 0 8rpx;
}
:deep(.wd-slide-up-enter),
:deep(.wd-slide-up-leave-to) {
transform: none;
}
}
:deep(.wd-password-input__item) {
width: 45px;
height: 40px;
padding: 0;
background: #f2f2f2;
border-radius: 10px;
}
</style>
使用示例
<template>
<ac-pass-popup
ref="passPopupRef"
v-model:visible="showPassPopup"
:title="t('withdrawPage.请输入支付密码')"
:confirmText="t('withdrawPage.提现')"
@input-complete="onInputComplete"
/>
</template>
<script setup>
const passPopupRef = ref();
function onRequest(){
// 接口处理
...
passPopupRef.value.close();
}
</script>
结语
组件库虽然方便了大部分的开发场景,但是在某些情况下,仍然需要自行做类似的功能实现处理。
另外,该组件已经归档到项目 uniapp-vitesse-wot-one