业务场景
需要在原生输入框,或者自定义的样式密码框,屏蔽输入法弹出,弹出自定的数字键盘输入,并实现数据的双向绑定
实现思路
首先构思组件的要实现的功能以及问题,从小到大,再逐一实现,得出一下步骤结论
1、初始化组件,实现基础样式,从下往上弹出效果,并可以兼容移动端、pc端
2、禁止移动端输入法弹出,使用自己的键盘组件弹出
3、点击输入框弹出键盘组件,点击外部区域收起键盘组件,并且不能影响其他点击事件
4、键盘组件与输入框的数据双向绑定
5、解决当输入框位于屏幕下方时会被弹出的键盘组件遮盖的问题
按步骤解决,具体实现如下
1、初始化组件,插入body,css固定定位全局蒙版键盘,并且实现从下往上弹出过渡效果
默认居中占满全屏,限制最大宽度800px
打开键盘时禁止屏幕滚动
components/NumberKeyboard/index.vue
html
<template>
<div class="numberKeyboard_wrapper" ref="keyboard">
<transition name="keyboard">
<div v-if="show" class="numberKeyboard_body">
<div v-for="item in keyboardList" :key="item.value" class="numberKeyboard_body_item" @click="clickKeyboard(item)">
{{ item.label }}
</div>
</div>
</transition>
</div>
</template>
<script>
export default {
props: {
visible: {
type: Boolean,
required: true
},
},
data() {
return {
keyboardList: [
{ label: "1", value: "1" },
{ label: "2", value: "2" },
{ label: "3", value: "3" },
{ label: "4", value: "4" },
{ label: "5", value: "5" },
{ label: "6", value: "6" },
{ label: "7", value: "7" },
{ label: "8", value: "8" },
{ label: "9", value: "9" },
{ label: ".", value: "." },
{ label: "0", value: "0" },
{ label: "退格", value: "-1" },
]
}
},
computed: {
show: {
get() {
return this.visible
},
set(newVal) {
this.$emit('update:visible', newVal)
}
},
},
watch: {
// 禁止背景滚动
show(newVal) {
if(newVal) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
}
},
mounted() { // 插入body
document.body.appendChild(this.$el);
},
destroyed() { // 组件销毁后同步清除元素
this.$el.parentNode.removeChild(this.$el);
}
}
</script>
<style lang="less" scoped>
.numberKeyboard{
&_wrapper{
width: 100%;
position: fixed;
bottom: 0;
left: 0;
display: flex;
justify-content: center;
}
&_body{
width: 100%;
max-width: 800px;
height: 40vh;
background-color:#f2f3f5;
display: flex;
flex-wrap: wrap;
padding: 10px;
border-radius: 10px;
box-shadow: 0px -2px 10px rgba(0, 0, 0, 0.2);
&_item{
flex: 1 1 calc(33.333% - 10px);
margin-right: 10px;
background-color: #fff;
margin-bottom: 10px;
border-radius: 10px;
font-size: 22px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
cursor: pointer;
}
&_item:active{
background-color: #dcdee0;
}
&_item:nth-child(3n) {
margin-right: 0;
}
&_item:nth-child(n+10):nth-child(-n+12){
margin-bottom: 0;
}
}
}
.keyboard-enter-active,.keyboard-leave-active { // 元素进入和离开时的过渡动画定义
transition: transform 0.3s;
}
.keyboard-enter,.keyboard-leave-to { // 元素进入和离开时的动作
transform: translateY(100%);
}
</style>
2、在移动端禁用默认键盘
在 input 输入框 增加 readonly
属性,使输入框变为只读,也可以自制 div模拟输入框 用于显示
3、点击输入框打开键盘,点击外部区域收起键盘
使用键盘组件
html
<template>
<div class="home">
<input type="text" class="myInput" @click="visible = true">
<NumberKeyboard :visible.sync="visible"/>
<button @click="otherClick">其他点击事件</button>
</div>
</template>
<script>
import NumberKeyboard from '@/components/NumberKeyboard/index.vue'
data() {
return {
visible: false
};
},
methods: {
otherClick() {
console.log('触发其他点击事件')
},
}
</script>
在组件中 添加全局关闭事件
html
<script>
methods: {
// 点击键盘外部区域关闭键盘
handleOutsideClick(event) {
if (this.$refs.keyboard && !this.$refs.keyboard.contains(event.target)) {
this.show = false
}
}
},
mounted() {
// 添加全局关闭事件侦听器
document.addEventListener('click', this.handleOutsideClick, true);
},
destroyed() {
// 移除全局关闭事件侦听器
document.removeEventListener('click', this.handleOutsideClick, true);
}
</script>
使用 addEventListener 给整个文档添加点击关闭键盘事件,在添加的时候注意加上第三个参数为true
,表明只在捕获阶段才会触发,这样使用document.addEventListener
覆盖整个文档的点击关闭键盘事件就不会因为点击它下面的子元素后冒泡阶段 而触发自己的事件
导致打开事件和关闭事件冲突抽搐

4、键盘组件与输入框的数据双向绑定
通过 vue 的 v-model
指令 传入组件,在组件内就相当于 接收 value 的props值,在组件内通过 this.$emit('input', '更新的值')
更新双向绑定的数据,具体如下代码
NumberKeyboard/index.vue
html
<script>
export default {
props: {
value: { // v-model传入的值
type: String,
default: ''
}
},
computed: {
keyboardValue: { // 利用计算属性更新
get() {
return this.value
},
set(newVal) {
this.$emit('input', newVal)
}
}
},
methods: {
// 点击键盘按钮回调
// 更新计算属性值 会自动触发更新 value值
clickKeyboard({ value, label }) {
const decimalPoint = /\./g // 小数点正则
switch (value) {
case '-1':
this.keyboardValue = this.keyboardValue.slice(0, -1)
break;
case '.': // 只允许输入一个小数点
if (!decimalPoint.test(this.keyboardValue)) {
this.keyboardValue += label
}
break
default:
this.keyboardValue += label
break;
}
this.$emit('change', this.keyboardValue)
},
}
}
</script>
5、输入框位于屏幕下方时会被弹出的键盘组件遮盖问题
制作好键盘组件后,仍有一个需要优化的点,当你的输入框 在屏幕下方 或者在底部 时,此时打开键盘组件会将输入框遮住
要实现这个效果由于使用哪个input输入框 、哪一行dom、或是整页dom,不确定,需要传入dom props来确定不被键盘遮挡的dom元素
然后根据 dom属性 判断是否输入框被遮住,使用transform
改变输入框位置
在组件内使用watch监听键盘组件显示隐藏状态
html
<script>
export default {
props: {
inputElement: { // 传入整页dom节点 || 输入框dom节点(根据需求选择测试)
type: [Object, HTMLElement],
default: () => ({})
}
},
watch: {
// 禁止背景滚动 && 键盘显示时,判断距离会遮住输入框dom时,把输入框dom往上推
show(newVal) {
if(newVal) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
Object.keys(this.inputElement).length > 0 && this.keyboardHandle(newVal) // 有传入dom时执行
},
},
methods: {
// 键盘显示时,判断距离会遮住输入框dom时,把输入框dom往上推
async keyboardHandle(visible) {
await this.$nextTick()
if (visible) { // 键盘打开
const inputElement = this.inputElement // 输入框dom
const keyboardElement = this.$refs.keyboard //键盘组件dom
const windowHeight = window.innerHeight; // 游览器视口高度
const inputElementTop = inputElement.getBoundingClientRect().bottom; // 输入框dom元素底部 距离当前视口顶部距离
const keyboardTop = windowHeight - keyboardElement.offsetHeight // 可视高度 - 键盘高度 = 键盘距离可视区域顶部距离
if (inputElementTop > keyboardTop) { // 输入框被键盘遮挡情况
console.log('输入框被键盘遮挡了')
inputElement.style.transition = "transform 0.3s" // 加上过渡效果
inputElement.style.transform = `translateY(-${
inputElementTop - (windowHeight - keyboardElement.offsetHeight)
}px)`;
} else {
console.log('输入框没有被键盘遮挡情况')
}
} else { // 键盘关闭,恢复输入框的位置
this.inputElement.style.transform = "translateY(0)";
}
},
}
}
</script>
最终效果如下

完整代码
完整代码如下
组件代码components/NumberKeyboard/index.vue
html
<template>
<div class="numberKeyboard_wrapper" ref="keyboard">
<transition name="keyboard">
<div v-if="show" class="numberKeyboard_body">
<div v-for="item in keyboardList" :key="item.value" class="numberKeyboard_body_item" @click="clickKeyboard(item)">
{{ item.label }}
</div>
</div>
</transition>
</div>
</template>
<script>
export default {
props: {
visible: { // 显示控制
type: Boolean,
required: true
},
value: { // v-model传入的值
type: String,
default: ''
},
inputElement: { // 传入整页dom节点 || 输入框dom节点(根据需求选择测试)
type: [Object, HTMLElement],
default: () => ({})
}
},
data() {
return {
keyboardList: [
{ label: "1", value: "1" },
{ label: "2", value: "2" },
{ label: "3", value: "3" },
{ label: "4", value: "4" },
{ label: "5", value: "5" },
{ label: "6", value: "6" },
{ label: "7", value: "7" },
{ label: "8", value: "8" },
{ label: "9", value: "9" },
{ label: ".", value: "." },
{ label: "0", value: "0" },
{ label: "退格", value: "-1" },
]
}
},
computed: {
show: {
get() {
return this.visible
},
set(newVal) {
this.$emit('update:visible', newVal)
}
},
keyboardValue: { // 利用计算属性更新
get() {
return this.value
},
set(newVal) {
this.$emit('input', newVal)
}
}
},
watch: {
// 禁止背景滚动 && 键盘显示时,判断距离会遮住输入框dom时,把输入框dom往上推
show(newVal) {
if(newVal) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
Object.keys(this.inputElement).length > 0 && this.keyboardHandle(newVal) // 有传入dom时执行
},
},
methods: {
// 键盘显示时,判断距离会遮住输入框dom时,把输入框dom往上推
async keyboardHandle(visible) {
await this.$nextTick()
if (visible) { // 键盘打开
const inputElement = this.inputElement // 输入框dom
const keyboardElement = this.$refs.keyboard //键盘组件dom
const windowHeight = window.innerHeight; // 游览器视口高度
const inputElementTop = inputElement.getBoundingClientRect().bottom; // 输入框dom元素底部 距离当前视口顶部距离
const keyboardTop = windowHeight - keyboardElement.offsetHeight // 可视高度 - 键盘高度 = 键盘距离可视区域顶部距离
if (inputElementTop > keyboardTop) { // 输入框被键盘遮挡情况
console.log('输入框被键盘遮挡了')
inputElement.style.transition = "transform 0.3s" // 加上过渡效果
inputElement.style.transform = `translateY(-${
inputElementTop - (windowHeight - keyboardElement.offsetHeight)
}px)`;
} else {
console.log('输入框没有被键盘遮挡情况')
}
} else { // 键盘关闭,恢复输入框的位置
this.inputElement.style.transform = "translateY(0)";
}
},
// 点击键盘按钮回调
// 更新计算属性值 会自动触发更新 value值
clickKeyboard({ value, label }) {
const decimalPoint = /\./g // 小数点正则
switch (value) {
case '-1':
this.keyboardValue = this.keyboardValue.slice(0, -1)
break;
case '.': // 只允许输入一个小数点
if (!decimalPoint.test(this.keyboardValue)) {
this.keyboardValue += label
}
break
default:
this.keyboardValue += label
break;
}
this.$emit('change', this.keyboardValue)
},
// 点击键盘外部区域关闭键盘
handleOutsideClick(event) {
if (this.$refs.keyboard && !this.$refs.keyboard.contains(event.target)) {
this.show = false
}
}
},
mounted() {
// 插入body
document.body.appendChild(this.$el);
// 添加全局关闭事件侦听器
document.addEventListener('click', this.handleOutsideClick, true);
},
destroyed() {
// 组件销毁后同步清除元素
this.$el.parentNode.removeChild(this.$el);
// 移除全局关闭事件侦听器
document.removeEventListener('click', this.handleOutsideClick, true);
}
}
</script>
<style lang="less" scoped>
.numberKeyboard{
&_wrapper{
width: 100%;
position: fixed;
bottom: 0;
left: 0;
display: flex;
justify-content: center;
}
&_body{
width: 100%;
max-width: 800px;
height: 40vh;
background-color:#f2f3f5;
display: flex;
flex-wrap: wrap;
padding: 10px;
border-radius: 10px 10px 0 0;
border: 1px solid #EBEEF5;
&_item{
flex: 1 1 calc(33.333% - 10px);
margin-right: 10px;
background-color: #fff;
margin-bottom: 10px;
border-radius: 10px;
font-size: 22px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
cursor: pointer;
}
&_item:active{
background-color: #dcdee0;
}
&_item:nth-child(3n) {
margin-right: 0;
}
&_item:nth-child(n+10):nth-child(-n+12){
margin-bottom: 0;
}
}
}
.keyboard-enter-active,.keyboard-leave-active { // 元素进入和离开时的过渡动画定义
transition: transform 0.3s;
}
.keyboard-enter,.keyboard-leave-to { // 元素进入和离开时的动作
transform: translateY(100%);
}
</style>
使用组件 home.vue
html
<template>
<div class="home" ref="home">
<input v-model="valueText" ref="inputWrapper1" type="text" class="inputWrapper" placeholder="输入框1" @click="openKeyboard('inputWrapper1')">
<div style="height: 500px;background: antiquewhite;">占位高度区域</div>
<input v-model="valueText" ref="inputWrapper2" type="text" class="inputWrapper" placeholder="输入框2" @click="openKeyboard('inputWrapper2')">
<button @click="otherClick">其他点击事件</button>
<NumberKeyboard v-model="valueText" ref="keyboard" :visible.sync="visible" :inputElement="inputElement"/>
</div>
</template>
<script>
import NumberKeyboard from '@/components/NumberKeyboard/index.vue'
export default {
name: 'Home',
components: { NumberKeyboard },
data() {
return {
visible: false,
valueText: '',
inputElement: {} // 输入框dom
};
},
methods: {
otherClick() {
console.log('触发其他点击事件')
},
openKeyboard(target) {
this.visible = true
this.inputElement = this.$refs[target]
},
closeKeyboard() {
this.visible = false
}
}
}
</script>
<style lang="less">
.home{
.inputWrapper{
width: 200px;
padding: 5px 10px;
font-size: 22px;
border: 1px solid #dcdee0;
}
}
</style>