微信、支付宝领取明码时的明码输入框大家都很相熟,它由6个小格子组成,输出一个明码后会主动跳转到另一个格子,明码输出实现后就能够提交数据,在web并没有原生的这种输入框,要想应用这种输入框须要本人去实现。

最近的一个我的项目中须要应用这种输入框,因为我的项目比拟小,因而就没有用其余的UI库,于是就本人造了一个明码输入框的轮子,成果如下图:

安卓设施:

iOS设施:

在ios上有一个小小的问题:输入框取得焦点后无奈主动弹窗软键盘,有大神晓得解决办法的话请评论区留言告知!!

1、DOM构造

dom构造这块比较简单,次要由:

  1. 暗藏的input
    次要作用是:调起零碎软键盘以及输出内容
  2. 6个小格子
  3. 虚构光标

3局部组成。

<template>  <div class="bs-password-input" ref="passwordInputRef">    <input      ref="realInput"      type="number"      inputmode="numeric"      class="hidden-input"      @input="onInput"      @blur="blur">    <ul      class="bs-password-input-security"      :class="{        'has-gap': hasGap      }"      @click="focus">      <li        class="bs-password-input-item"        :class="{          'is-focus': focusInputIndex === index        }"        v-for="(pwd, index) in passwords"        :key="index">        <i v-if="mask && (pwd !== ' ')" class="password-input-dot"></i>        <template v-if="!mask">{{ pwd }}</template>        <div          v-if="showInputCursor"          class="bs-password-input-cursor"></div>      </li>    </ul>    <div class="bs-password-input-info" v-if="info">{{ info }}</div>  </div></template>

2、显示明码格子

界面上的明码格子数量次要通过父组件传递的明码及明码长度来计算

let passwords = computed(function () {    let value = props.modelValue;    if (typeof value !== 'string' && typeof value !== 'number') {      value = '';    } else {      value = value + '';    }    // console.log('value', value);    let resultArr = value.split('');    let len = props.length; // 明码长度限度,默认为6位    let diff = value.length - props.length;    if (diff > 0) {      resultArr = value.substr(0, len).split('');    } else if (diff < 0) {      diff = Math.abs(diff);      // 如果传递的modelValue长度小于明码长度,则填补补空格,以达到界面上明码格子数量等于明码长度      while (diff > 0) {        resultArr.push(' ');        diff--;      }    }    return resultArr;  });

3、计算取得焦点的格子的索引

当在输出时、删除时、用户点击格子时都须要晓得以后哪个格子应该取得焦点,格子取得焦点后虚构光标才会显示进去。如果calcFocusInputIndex()执行后的值为-1则阐明明码输出实现,此时没人任何格子须要取得焦点

let calcFocusInputIndex = function () {  let pwdVal = passwords.value;  let index = -1;  let realPwdVal = trim(pwdVal.join(''));  console.log('realPwdVal', realPwdVal, realPwdVal.length, pwdVal);  for (let i = 0, len = pwdVal.length; i < len; i++) {    // pwdVal[i]为空格示意该格子并没有真正的值    if (pwdVal[i] === ' ' && realPwdVal.length !== props.length) {      index = i;      break;    }  }  console.log('index', index);  return index;};

4、监听输出

在用户输出明码时如果输出的不是数字,则需清空input里的内容
当用户输出的明码长度等于props.length时立刻实现输出

let onInput = function (evt) {  let inputValue = evt.target.value;  if (inputValue && !numberReg.test(inputValue)) { // 如果输出的不是数字则清空输入框    evt.target.value = '';    return;  }  console.log('输出的字符为:', inputValue);  let password = passwords.value.join('');  password = trim(password);  password += inputValue;  evt.target.value = '';  ctx.emit('update:modelValue', password);  if (password.length == props.length) {    ctx.emit('complete', password);  }  // 暗藏输入框焦点  nextTick(function () {    let inputIndex = calcFocusInputIndex();    if (inputIndex == -1) {      blur();    } else {      focusInputIndex.value = inputIndex;    }  });  console.log('更新modelValue', password);};

5、暗藏掉input的光标

因为格子自身就有虚构光标,那么此时input的光标就不须要再显示进去了。另外,就算没有虚构光标也很难将input的光标精确的定位到每一个格子两头。

暗藏掉input光标次要靠3个技巧:

  1. 用相对定位来暗藏input,而不是通过display: none;暗藏
  2. 字体色彩设为通明,字体暗影设为0
  3. text-indent 设置一个略微大点的负值(次要解决ios设施中光标暗藏不了问题)
.hidden-input{  position: absolute;  top: 5px;  z-index: 1;  /* 暗藏光标 start */  color: transparent;  text-shadow: 0 0 0 #000;  /* 暗藏光标 end */  /* 暗藏ios设施光标 start */  text-indent: -999em;  margin-left: -40%;  /* 暗藏ios设施光标 end */}

6、残缺代码

PasswordInput.vue

<template>  <div class="bs-password-input" ref="passwordInputRef">    <input      ref="realInput"      type="number"      inputmode="numeric"      class="hidden-input"      @input="onInput"      @blur="blur">    <ul      class="bs-password-input-security"      :class="{        'has-gap': hasGap      }"      @click="focus">      <li        class="bs-password-input-item"        :class="{          'is-focus': focusInputIndex === index        }"        v-for="(pwd, index) in passwords"        :key="index">        <i v-if="mask && (pwd !== ' ')" class="password-input-dot"></i>        <template v-if="!mask">{{ pwd }}</template>        <div          v-if="showInputCursor"          class="bs-password-input-cursor"></div>      </li>    </ul>    <div class="bs-password-input-info" v-if="info">{{ info }}</div>  </div></template><script>import {  ref,  computed,  onMounted,  onUnmounted,  nextTick} from 'vue';const trim = function (str) {  if (typeof str !== 'string' || str.length === 0) {    return str;  }  str += '';  // 革除字符串两端空格,蕴含换行符、制表符  return str.replace(/(^[\s\n\t]+|[\s\n\t]+$)/g, '');}export default {  name: "PasswordInput",  props: {    modelValue: { // 明码值      type: [String, Number],      default: ''    },    hasGap: { // 是否有间隙      type: Boolean,      default: false    },    mask: { // 是否暗藏明码内容      type: Boolean,      default: true    },    length: { // 明码最大长度      type: Number,      default: 6    },    info: { // 输入框下方文字提醒      type: String,      default: ''    }  },  setup (props, ctx) {    let passwordInputRef = ref(null);    let realInput = ref(null);    let passwords = computed(function () {      let value = props.modelValue;      if (typeof value !== 'string' && typeof value !== 'number') {        value = '';      } else {        value = value + '';      }      // console.log('value', value);      let resultArr = value.split('');      let len = props.length;      let diff = value.length - props.length;      if (diff > 0) {        resultArr = value.substr(0, len).split('');      } else if (diff < 0) {        diff = Math.abs(diff);        while (diff > 0) {          resultArr.push(' ');          diff--;        }      }      return resultArr;    });    // 计算取得焦点的虚构输入框的索引    let calcFocusInputIndex = function () {      let pwdVal = passwords.value;      let index = -1;      let realPwdVal = trim(pwdVal.join(''));      console.log('realPwdVal', realPwdVal, realPwdVal.length, pwdVal);      for (let i = 0, len = pwdVal.length; i < len; i++) {        if (pwdVal[i] === ' ' && realPwdVal.length !== props.length) {          index = i;          break;        }      }      console.log('index', index);      return index;    };    let nativeInputFocus = ref(false);    let showInputCursor = ref(false);    let focusInputIndex = ref(null);    let focus = function () {      let index = calcFocusInputIndex();      if (index > -1) {        realInput.value.focus();        nativeInputFocus.value = true;        showInputCursor.value = true;        focusInputIndex.value = index;      } else {        realInput.value.focus();        nativeInputFocus.value = true;      }    };    let blur = function () {      showInputCursor.value = false;      focusInputIndex.value = null;      realInput.value.blur();      realInput.value.value = '';      nativeInputFocus.value = false;    };    let numberReg = /^\d+$/;    let onInput = function (evt) {      let inputValue = evt.target.value;      if (inputValue && !numberReg.test(inputValue)) { // 如果输出的不是数字则清空输入框        evt.target.value = '';        return;      }      console.log('输出的字符为:', inputValue);      let password = passwords.value.join('');      password = trim(password);      password += inputValue;      evt.target.value = '';      ctx.emit('update:modelValue', password);      if (password.length == props.length) {        ctx.emit('complete', password);      }      // 暗藏输入框焦点      nextTick(function () {        let inputIndex = calcFocusInputIndex();        if (inputIndex == -1) {          blur();        } else {          focusInputIndex.value = inputIndex;        }      });      console.log('更新modelValue', password);    };    let keydownEvent = function (evt) {      let keyCode = evt.keyCode;      console.log('keyCode', keyCode);      if (!nativeInputFocus.value) {        console.log('原生输入框未取得焦点');        return;      }      if (keyCode == 8) { // 删除键        let password = passwords.value.join('');        password = trim(password);        if (password.length == 0) {          return;        }        password = password.substr(0, password.length - 1);        console.log('new password', password);        ctx.emit('update:modelValue', password);        // 暗藏输入框焦点        nextTick(function () {          let inputIndex = calcFocusInputIndex();          if (inputIndex == -1) {            blur();          } else {            focusInputIndex.value = inputIndex;            focus();          }        });      }    };    onMounted(function () {      document.addEventListener('keydown', keydownEvent, false);    });    onUnmounted(function () {      document.removeEventListener('keydown', keydownEvent, false);    });    return {      realInput,      passwordInputRef,      passwords,      showInputCursor,      focusInputIndex,      blur,      focus,      onInput    };  }};</script><style lang="less">@import "password-input";</style>

password-input.less

.bs-password-input{  position: relative;  overflow: hidden;  .hidden-input{    position: absolute;    top: 5px;    z-index: 1;    /* 暗藏光标 start */    color: transparent;    text-shadow: 0 0 0 #000;    /* 暗藏光标 end */    /* 暗藏ios设施光标 start */    text-indent: -999em;    margin-left: -40%;    /* 暗藏ios设施光标 end */  }}.bs-password-input-security{  position: relative;  z-index: 5;  display: flex;  height: 40px;  user-select: none;  background-color: #fff;}.bs-password-input-item{  position: relative;  z-index: 5;  display: flex;  flex: 1;  justify-content: center;  align-items: center;  height: 100%;  cursor: pointer;  font-size: 20px;  background-color: #F2F2F2;  &:not(:first-child)::before{    position: absolute;    top: 0;    left: 0;    bottom: 0;    content: ' ';    width: 1px;/*no*/    background-color: #ececec;  }  &.is-focus{    .password-input-dot{      visibility: hidden;    }    .bs-password-input-cursor{      display: block;    }  }}.password-input-dot{  width: 12px;  height: 12px;  border-radius: 50%;  background-color: #000;}.bs-password-input-cursor{  display: none;  position: absolute;  top: 50%;  left: 50%;  width: 1px;/*no*/  height: 40%;  transform: translate(-50%, -50%);  cursor: pointer;  background-color: rgba(32,32,32,3);  animation: 1s cursor-flicker infinite;}.bs-password-input-security{  &.has-gap{    .bs-password-input-item{      border-radius: 4px;      &::before{        display: none;      }      &:not(:first-child){        margin-left: 15px;      }    }  }}.bs-password-input-info {  margin-top: 15px;  color: #999;  text-align: center;}@keyframes cursor-flicker {  0% {    opacity: 0;  }  50% {    opacity: 1;  }  100% {    opacity: 0;  }}

应用

<template><div class="pay">  <PasswordInput    ref="passwordInputRef"    v-model="password"    has-gap    @complete="onPasswordInputComplete"></PasswordInput></div></template><script>import {  ref,  onMounted} from 'vue';import PasswordInput from './PasswordInput';export default {  name: "PayDialog",  components: {    PasswordInput  },  setup (props, ctx) {    let passwordInputRef = ref(null);    let password = ref('');    let onPasswordInputComplete = function (pwd) {      console.log('明码输出实现: ', pwd);    };    onMounted(function () {      // 让明码框取得焦点      passwordInputRef.value.focus();    });    return {      password,      passwordInputRef,      onPasswordInputComplete    };  }};</script>