58

image.png

要实现的功能:

完全和单输入框一样的操作,甚至可以插入覆盖:

  1. 限制输入数字
  2. 正常输入
  3. backspace 删除
  4. paste 任意位置粘贴输入
  5. 光标选中一个数字,滚轮可以微调数字大小,限制 0-9
  6. 123|456 自动覆盖光标后输入的字符,此时光标在 3 后,继续输入 111,会得到 123111,而不用手动删除 456
  7. 封装成 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 日新增内容

  1. 修复了上面旧版的一个 bug。
  2. 新增了下面的 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"/>

jsoncode
4k 声望786 粉丝

I'm jsoncode