普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月11日首页

Unaipp 使用 wot UI 实现一个带数字键盘的密码输入框弹窗

作者 isixe
2026年2月11日 11:57

最近项目里有个支付输入密码的需求,所以在这之前都是使用一个简单的输入框实现的,但是这样体验不太好。所以,这次就改成了弹窗,尝试达到类似支付宝的弹窗输入密码的形式。

前言

在 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

❌
❌