要实现的功能:
完全和单输入框一样的操作,甚至可以插入覆盖:
- 限制输入数字
- 正常输入
- backspace 删除
- paste 任意位置粘贴输入
- 光标选中一个数字,滚轮可以微调数字大小,限制 0-9
- 123|456 自动覆盖光标后输入的字符,此时光标在 3 后,继续输入 111,会得到 123111,而不用手动删除 456
- 封装成 vue 单文件组件,方便任意调用。
模板代码:
<template>
<div class="input-box">
<div class="input-content" @keydown="keydown" @keyup="keyup" @paste="paste" @mousewheel="mousewheel"
@input="inputEvent">
<input max="9" min="0" maxlength="1" data-index="0" v-model.trim.number="input[0]" type="number"
ref="firstinput"/>
<input max="9" min="0" maxlength="1" data-index="1" v-model.trim.number="input[1]" type="number"/>
<input max="9" min="0" maxlength="1" data-index="2" v-model.trim.number="input[2]" type="number"/>
<input
<input max="9" min="0" maxlength="1" data-index="4" v-model.trim.number="input[4]" type="number"/>
<input max="9" min="0" maxlength="1" data-index="5" v-model.trim.number="input[5]" type="number"/>
</div>
</div>
</template>
实现了键盘的 keydown/keyup/paste/input 和鼠标滚轮 mousewheel 事件,使用了 6 个输入框的方案来实现。
样式部分:使用了scss模式
<style scoped lang="scss">
.input-box {
.input-content {
width: 512px;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
input {
color: inherit;
font-family: inherit;
border: 0;
outline: 0;
border-bottom: 1px solid #919191;
height: 60px;
width: 60px;
font-size: 44px;
text-align: center;
}
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
}
</style>
具体实现逻辑:主要实现以上几个键盘事件操作。
<script>
export default {
data() {
return {
pasteResult: [],
};
},
props: ['code'],
computed: {
input() {
if (this.code && Array.isArray(this.code) && this.code.length === 6) {
return this.code;
} else if (/^\d{6}$/.test(this.code.toString())) {
return this.code.toString().split('');
} else if (this.pasteResult.length === 6) {
return this.pasteResult;
} else {
return new Array(6);
}
}
},
methods: {
// 解决一个输入框输入多个字符
inputEvent(e) {
var index = e.target.dataset.index * 1;
var el = e.target;
el.value = el.value.replace(/Digit|Numpad/i, '').replace(/1/g, '').slice(0, 1);
this.$set(this.input, index, el.value)
},
keydown(e) {
var index = e.target.dataset.index * 1;
var el = e.target;
if (e.key === 'Backspace') {
if (this.input[index].length > 0) {
this.$set(this.input, index, '')
} else {
if (el.previousElementSibling) {
el.previousElementSibling.focus()
this.$set(this.input, index - 1, '')
}
}
} else if (e.key === 'Delete') {
if (this.input[index].length > 0) {
this.$set(this.input, index, '')
} else {
if (el.nextElementSibling) {
this.$set(this.input, index + 1, '')
}
}
if (el.nextElementSibling) {
el.nextElementSibling.focus()
}
} else if (e.key === 'Home') {
el.parentElement.children[0] && el.parentElement.children[0].focus()
} else if (e.key === 'End') {
el.parentElement.children[this.input.length - 1] && el.parentElement.children[this.input.length - 1].focus()
} else if (e.key === 'ArrowLeft') {
if (el.previousElementSibling) {
el.previousElementSibling.focus()
}
} else if (e.key === 'ArrowRight') {
if (el.nextElementSibling) {
el.nextElementSibling.focus()
}
} else if (e.key === 'ArrowUp') {
if (this.input[index] * 1 < 9) {
this.$set(this.input, index, (this.input[index] * 1 + 1).toString());
}
} else if (e.key === 'ArrowDown') {
if (this.input[index] * 1 > 0) {
this.$set(this.input, index, (this.input[index] * 1 - 1).toString());
}
}
},
keyup(e) {
var index = e.target.dataset.index * 1;
var el = e.target;
// 解决输入e的问题
el.value = el.value.replace(/Digit|Numpad/i, '').replace(/1/g, '').slice(0, 1);
if (/Digit|Numpad/i.test(e.code)) {
// 必须在这里符直,否则输入框会是空值
this.$set(this.input, index, e.code.replace(/Digit|Numpad/i, ''));
el.nextElementSibling && el.nextElementSibling.focus();
if (index === 5) {
if (this.input.join('').length === 6) {
document.activeElement.blur();
this.$emit('complete', this.input);
}
}
} else {
if (this.input[index] === '') {
this.$set(this.input, index, '');
}
}
},
mousewheel(e) {
var index = e.target.dataset.index;
if (e.wheelDelta > 0) {
if (this.input[index] * 1 < 9) {
this.$set(this.input, index, (this.input[index] * 1 + 1).toString());
}
} else if (e.wheelDelta < 0) {
if (this.input[index] * 1 > 0) {
this.$set(this.input, index, (this.input[index] * 1 - 1).toString());
}
} else if (e.key === 'Enter') {
if (this.input.join('').length === 6) {
document.activeElement.blur();
this.$emit('complete', this.input);
}
}
},
paste(e) {
// 当进行粘贴时
e.clipboardData.items[0].getAsString(str => {
if (str.toString().length === 6) {
this.pasteResult = str.split('');
document.activeElement.blur();
this.$emit('complete', this.input);
this.pasteResult = [];
} else {
// 如果粘贴内容不合规,清除所有内容
this.input[0] = new Array(6)
}
})
}
},
mounted() {
// 等待dom渲染完成,在执行focus,否则无法获取到焦点
this.$nextTick(() => {
this.$refs.firstinput.focus()
})
},
}
</script>
如果你发现了bug,或者有优化空间,欢迎你的指正和建议。我会随时更新到原代码当中,分享给大家。
以下是 2024-02-26 日新增内容:
- 修复了上面旧版的一个 bug。
- 新增了下面的 vue3 版本,使代码更简洁明了。
// SixCharacterInputBox.vue
<script lang="ts" setup>
// vue 3.x setup 模式
import { defineEmits, defineProps, nextTick, onMounted, ref, watch } from 'vue'
interface IProps {
value: number | any;
}
const props = withDefaults(defineProps<IProps>(), {})
const emit = defineEmits(['update:value'])
const emptyArray = new Array(6).fill('')
const inputElement = ref<any[]>(emptyArray.map(ref))
const inputValue = ref<any[]>(emptyArray.map(() => ''))
onMounted(() => {
resetValue(props.value)
nextTick(() => {
inputEl('Home')?.focus()
})
})
watch(() => props.value, () => {
resetValue(props.value)
})
// 设置第N个输入框的值
function setInputValue(index: number, value: null | number) {
inputValue.value.splice(index, 1, value)
}
// 获取第N个输入框的dom元素
function inputEl(type: 'Home' | 'End' | number): HTMLInputElement {
const index = type === 'Home' ? 0 : type === 'End' ? inputElement.value.length - 1 : type
const firstInputRef = inputElement.value[index]
return firstInputRef.value[0]
}
function resetValue(str: string) {
if (str.length === emptyArray.length) {
inputValue.value = str.split('');
} else {
inputValue.value = emptyArray.map(() => '')
}
}
function onKeyup(e: any) {
const index = e.target.dataset.index * 1;
const el = e.target;
if (e.code === 'Backspace') {
setInputValue(index, null)
if (el.previousElementSibling) {
el.previousElementSibling.focus()
}
} else if (e.code === 'Delete') {
setInputValue(index, null)
if (el.nextElementSibling) {
el.nextElementSibling.focus()
}
} else if (e.code === 'Home') {
inputEl('Home').focus()
} else if (e.code === 'End') {
inputEl('End').focus()
} else if (e.code === 'ArrowLeft') {
if (el.previousElementSibling) {
el.previousElementSibling.focus()
}
} else if (e.code === 'ArrowRight') {
if (el.nextElementSibling) {
el.nextElementSibling.focus()
}
}
if (inputValue.value.join('').length === emptyArray.length) {
emit('update:value', inputValue.value.join(''))
}
}
function onPaste(e: any) {
// 当进行粘贴时
e.clipboardData.items[0].getAsString((str: string) => resetValue(str))
}
function onInput(e: any) {
const index = e.target.dataset.index * 1;
const old = inputValue.value[index];
const el = e.target;
const value = e.data;
if (/^\d$/.test(value)) {
setInputValue(index, value * 1)
if (index === emptyArray.length - 1) {
if (inputValue.value.join('').length === emptyArray.length) {
emit('update:value', inputValue.value.join(''))
}
} else if (el.nextElementSibling) {
el.nextElementSibling.focus()
}
} else {
if (value) {
el.value = old;
setInputValue(index, old)
}
}
}
</script>
<template>
<div class="six-character-input-box">
<input
:ref="inputElement[index]"
@keyup="onKeyup"
@paste="onPaste"
@input="onInput"
v-model="inputValue[index]"
v-for="(_, index) in inputValue"
:key="index"
:data-index="index"
type="number"
max="9"
min="0"
maxlength="1"
>
</div>
</template>
<style lang="less" scoped>
.six-character-input-box {
width: 360px;
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
input {
color: inherit;
font-family: inherit;
outline: 0;
border: 1px solid #ccc;
height: 50px;
width: 50px;
font-size: 30px;
text-align: center;
line-height: 1;
letter-spacing: 0;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
}
}
</style>
调用方式:
const inputValue = ref('')
<SixCharacterInputBox v-model:value="inputValue"/>
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。