Vue 自定义软键盘+输入框

4,574 阅读4分钟

截屏2021-07-16 下午3.37.36.png

使用vue做一个自定义软键盘+输入框的组件

预览地址 GitHub

效果

先来看看效果~

2021-07-16 15.49.36.gif

需求

这个组件是我在做<澳门停车场缴费>项目的时候,需要自制一个只有英文和数字的软键盘,并且拥有默认开头,无法对默认开头进行删除修改。对于国内你可能还需要做一个按键来切换汉子和英文,像粤Axxxx这样,需要让用户在软键盘上按下粤字,而我这个项目,因为不在国内,所以车牌号只有英文和数字,如MO-00-00,所以我没有做切换输入法的功能。

实现方法

接收的参数

  props: {
    // 文本数据源
    text: {
      type: String,
      default: "",
    },
    // 固定开头
    defaultVal: {
      type: String,
      default: "",
    },
    // 输入框数量
    length: {
      type: Number,
      default: 6,
    },

内置的数据

data(){
  return {
    // 键盘数据 用来渲染键盘
    keys: [
      [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
      ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
      ["A", "S", "D", "F", "G", "H", "J", "K", "L"],
      ["Z", "X", "C", "V", "B", "N", "M"],
    ],
    showKeyboard: false, // 控制键盘显隐
    isFocus: false, // 是否聚焦
  }
}

当前输入的位置需要用到computed来返回字符串的长度

computed: {
  /**
   * 获取当前输入框下标
   * @param {void}
   * @return {void}
   */
  currentInput: function() {
    // 返回当前文本的长度
    const length = this.text.length || 0;
    return length;
  },
},

输入框

首先来看看输入框,输入框位数可以通过外部传入的length控制数量,当然数量要在可控范围内,不然样式你就要自己想办法了

html

<!-- 输入框显示区 -->
<div class="input">
  <div
    v-for="(item, index) in length"
    :key="index"
    :class="['box-item', isFocus && currentInput === index ? 'active' : '', text[index] ? 'hight-light' : '']"
  >
    <span>{{ text[index] }}</span>
  </div>
</div>
  • 根据传入的length来生成对应数量的方框
  • 根据是否聚焦的标识isFocus输入的字符长度来判断激活的方框
  • 有内容的方框则一直高亮hight-light样式
  • 方框展示的内容则为输入的文本下标对应的文字

透明背景实现点击键盘外的区域收起键盘

<!-- 点击键盘以外的区域隐藏键盘 -->
<div @click.stop="hide" v-if="showKeyboard" class="bg"></div>

这个会导致点击页面的提交按钮时被背景阻挡,点一次会先收起键盘,等你键盘收起来后再点按钮才会被点击到。想要实现键盘存在也能点击页面按钮的情况,则需要在父组件的根元素做一个监听,然后移除透明背景dom,然后在父组件的根元素的点击事件上自己做逻辑处理。我懒,而且我不需要考虑这种情况,所以我没做。

键盘区

<!-- 键盘区 -->
<div ref="cusBoard" v-if="showKeyboard" class="cus-board">
  <div v-for="(line, index) in keys" :key="'line' + index" class="letter-line">
    <!-- 收起键盘 -->
    <div v-if="index === keys.length - 1" @click.stop="hide" class="action">
      <img :src="require('@/assets/keyboard.png')" />
    </div>
    <div  @touchstart="touchStart" @touchend="touchEnd" v-for="key in line" :key="key" :data-text="key" class="item">{{ key }}</div>
    <!-- 删除 -->
    <div v-if="index === keys.length - 1" @click.stop="handleDel" class="action">
      <img :src="require('@/assets/delete.png')" />
    </div>
  </div>
</div>

根据keys键盘数据来循环生成按键dom,需要注意的是最后一行需要在前后添加收起键盘按钮和删除按钮。当然你也可以不给用户这种体验,你说了算。keyboard.png和delete.png是键盘图标和删除图标,你自己找找替换掉就好了。

显示键盘方法

/**
 * 显示键盘
 * @param {void}
 * @return {void}
 */
show() {
  this.isFocus = true;
  this.showKeyboard = true;
  // 需定时器执行 否则会找不到dom
  setTimeout(() => {
    // 升起键盘
    this.$refs.cusBoard.style.transform = `translateY(0)`;
  }, 20);
},

这里需要注意,实现弹出的过程,需要通过定时器来延时执行,否则只有一闪而过的尴尬。

隐藏键盘

/**
 * 隐藏键盘
 * @param {void}
 * @return {void}
 */
hide() {
  // 失去焦点
  this.isFocus = false;
  // 降下键盘
  this.$refs.cusBoard.style.transform = `translateY(100%)`;
  // 需定时器执行 否则会没有动画过度
  setTimeout(() => {
    this.showKeyboard = false;
  }, 500);
},

这里也需要注意,实现收起的过程,需要通过定时器来延时隐藏键盘,否则动画还没结束,你就直接隐藏掉了键盘,只有一闪而过的尴尬。

按下

/**
 * 按下
 * @param {object} el 点击事件
 * @return {void}
 */
touchStart(el) {
  // 点击目标
  const { target } = el;

  let text = this.text;
  // 文本达到上限 不做处理 返回
  if (text.length >= this.length) return;
  // 拼接点击的 值
  const content = target.innerText;
  text += content;
  // 更新文本数据源
  this.$emit("update:text", text);

  // 背景色改变
  target.style.background = "rgb(228, 229, 228)";
  // 添加激活className 显示反馈
  target.classList.add("active");
},

这里按下的时候,判断文本长度,如果超出了就不做响应了。需要在按下的时候改变下背景色和添加按下反馈的动画来提升用户体验

比如我这里,通过短暂切换底色然后复原,和用微元素做一个放大缩小的动画在按键上方来告诉用户按了什么。

按下的反馈动画

.active {
  &::after {
    position: absolute;
    top: -40px;
    left: 0;
    width: 32px;
    height: 40px;
    background-color: #ffffff;
    content: attr(data-text);
    animation: itemActive 0.5s infinite;
  }
}
@keyframes itemActive {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(2);
  }
  100% {
    transform: scale(1);
  }
}

按键松手

touchEnd(el) {
  // 点击目标
  const { target } = el;
  // 通过定时器实现过渡效果
  setTimeout(() => {
    // 背景色改变
    target.style.background = "#fff";
    // 移除className
    target.classList.remove("active");
  }, 100);
},

按键抬起的时候记得清除样式

点击删除

/**
 * 点击删除键
 * @param {void}
 * @return {void}
 */
handleDel() {
  if (this.defaultVal && this.text.length === this.defaultVal.length && this.text.indexOf(this.defaultVal) === 0) {
    // 有默认开头 如果文本只有固定开头 没有任何输入 点击删除不做任何操作
    return;
  }
  // 从后面开始 删除一个文本
  let text = this.text;
  text = text.slice(0, text.length - 1);
  this.$emit("update:text", text);
},

这里需要注意,如果有固定的默认开头,并且删除到默认开头这里的时候,则不做任何反馈,防止删掉固定的默认开头

组件完整代码

<template>
  <div class="keyboard">
    <!-- 输入框显示区 -->
    <div class="input">
      <div
        @click.stop="show"
        v-for="(item, index) in length"
        :key="index"
        :class="['box-item', isFocus && currentInput === index ? 'active' : '', text[index] ? 'hight-light' : '']"
      >
        <span>{{ text[index] }}</span>
      </div>
    </div>
    <!-- 点击键盘以外的区域隐藏键盘 -->
    <div @click.stop="hide" v-if="showKeyboard" class="bg"></div>
    <!-- 键盘区 -->
    <div ref="cusBoard" v-if="showKeyboard" class="cus-board">
      <div v-for="(line, index) in keys" :key="'line' + index" class="letter-line">
        <!-- 收起键盘 -->
        <div v-if="index === keys.length - 1" @click.stop="hide" class="action">
          <img :src="require('@/assets/keyboard.png')" />
        </div>
        <div @touchstart="touchStart" @touchend="touchEnd" v-for="key in line" :key="key" :data-text="key" class="item">{{ key }}</div>
        <!-- 删除 -->
        <div v-if="index === keys.length - 1" @click.stop="handleDel" class="action">
          <img :src="require('@/assets/delete.png')" />
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: "Keyboard",
  props: {
    // 文本数据源
    text: {
      type: String,
      default: "",
    },
    // 固定开头
    defaultVal: {
      type: String,
      default: "",
    },
    // 输入框数量
    length: {
      type: Number,
      default: 6,
    },
  },
  data() {
    return {
      keys: [
        [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
        ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
        ["A", "S", "D", "F", "G", "H", "J", "K", "L"],
        ["Z", "X", "C", "V", "B", "N", "M"],
      ],
      showKeyboard: false, // 控制键盘显隐
      isFocus: false, // 是否聚焦
    };
  },
  computed: {
    /**
     * 获取当前输入框下标
     * @param {void}
     * @return {void}
     */
    currentInput: function() {
      // 返回当前文本的长度
      const length = this.text.length || 0;
      return length;
    },
  },
  mounted() {
    // 更新固定开头值到文本数据源上
    this.$emit("update:text", this.defaultVal);
  },
  methods: {
    /**
     * 显示键盘
     * @param {void}
     * @return {void}
     */
    show() {
      this.isFocus = true;
      this.showKeyboard = true;
      // 需定时器执行 否则会找不到dom
      setTimeout(() => {
        // 升起键盘
        this.$refs.cusBoard.style.transform = `translateY(0)`;
      }, 20);
    },
    /**
     * 隐藏键盘
     * @param {void}
     * @return {void}
     */
    hide() {
      // 失去焦点
      this.isFocus = false;
      // 降下键盘
      this.$refs.cusBoard.style.transform = `translateY(100%)`;
      // 需定时器执行 否则会没有动画过度
      setTimeout(() => {
        this.showKeyboard = false;
      }, 500);
    },
    /**
     * 按下
     * @param {object} el 点击事件
     * @return {void}
     */
    touchStart(el) {
      // 点击目标
      const { target } = el;

      let text = this.text;
      // 文本达到上限 不做处理 返回
      if (text.length >= this.length) return;
      // 拼接点击的 值
      const content = target.innerText;
      text += content;
      // 更新文本数据源
      this.$emit("update:text", text);

      // 背景色改变
      target.style.background = "rgb(228, 229, 228)";
      // 添加激活className 显示反馈
      target.classList.add("active");
    },
    touchEnd(el) {
      // 点击目标
      const { target } = el;
      // 通过定时器实现过渡效果
      setTimeout(() => {
        // 背景色改变
        target.style.background = "#fff";
        // 移除className
        target.classList.remove("active");
      }, 100);
    },
    /**
     * 点击删除键
     * @param {void}
     * @return {void}
     */
    handleDel() {
      if (this.defaultVal && this.text.length === this.defaultVal.length && this.text.indexOf(this.defaultVal) === 0) {
        // 有默认开头 如果文本只有固定开头 没有任何输入 点击删除不做任何操作
        return;
      }
      // 从后面开始 删除一个文本
      let text = this.text;
      text = text.slice(0, text.length - 1);
      this.$emit("update:text", text);
    },
  },
};
</script>

<style lang="scss" scoped>
.keyboard {
  user-select: none;
}
.bg {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 1;
  background: rgba(255, 255, 255, 0);
}
.input {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 0 auto;
}
.box-item {
  flex-basis: 35px;
  height: 40px;
  border: 1px solid #bfbfbf;
  border-radius: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
  &.active {
    position: relative;
    border-color: #348fec;
    &::after {
      position: absolute;
      content: "";
      width: 1px;
      height: 50%;
      background-color: #333333;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      animation: inputFocusLine 1s infinite;
    }
  }
  &.hight-light {
    border-color: #348fec;
  }
}
@keyframes inputFocusLine {
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}
.cus-board {
  font-size: 15px;
  width: 375px;
  background: rgb(246, 247, 246);
  padding: 15px 5px 30px 5px;
  position: fixed;
  z-index: 9999;
  bottom: 0;
  left: 0;
  right: 0;
  transform: translateY(100%);
  transition: all 0.5s;
  .active {
    &::after {
      position: absolute;
      top: -40px;
      left: 0;
      width: 32px;
      height: 40px;
      background-color: #ffffff;
      content: attr(data-text);
      animation: itemActive 0.5s infinite;
    }
  }
}
.item,
.action {
  width: 32px;
  height: 40px;
  border-radius: 5px;
  background-color: white;
  line-height: 40px;
  text-align: center;
  position: relative;
  img {
    display: inline-block;
    width: 16px;
    height: 16px;
  }
}
@keyframes itemActive {
  0% {
    transform: scale(1);
  }
  50% {
    transform: scale(2);
  }
  100% {
    transform: scale(1);
  }
}
.line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 15px;
}
.letter-line {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  .item,
  .action {
    margin: 3px;
  }
}
</style>

组件使用代码

<template>
  <div @dblclick="() => {return false;}" id="app">
    <div class="cus-keyboard">
      <Keyboard :length="length" :defaultVal="defaultVal" :text.sync="value"></Keyboard>
    </div>
  </div>
</template>

<script>
import Keyboard from "@/components/keyboard/index.vue";
export default {
  name: "App",
  components: {
    Keyboard,
  },
  data() {
    return {
      value: "",
      defaultVal: "MO",
      length: 6,
    };
  },
};
</script>

<style lang="scss">
body,
html {
  width: 100%;
  height: 100%;
}
#app {
  width: 100%;
  height: 100%;
  min-height: 100%;
  margin: 0 auto;
  overflow-x: hidden;
  overflow-y: scroll;
}
.cus-keyboard {
  width: 100%;
  padding: 50px 0;
  margin: 0 auto;
  width: 280px;
}
</style>

这里要注意加上@dblclick="() => {return false;}"防止双击缩放

结语

不用自制键盘的话,需要用到input,把input隐藏,点击自制验证码输入框,js聚焦input,然后通过监听input的文本来赋值到输入框上,实现原生键盘搭配自定义验证码输入框。思路就是这么个思路,可以通过这种思路做各种各样的验证码输入框和车牌键盘呀、身份证键盘呀、数字键盘呀。当然,如果你有更好的实现思路,还望不吝赐教。