236 lines
6.1 KiB
Vue
236 lines
6.1 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="verification-code-input">
|
|||
|
|
<input
|
|||
|
|
v-for="(code, index) in verificationCode"
|
|||
|
|
:key="index"
|
|||
|
|
type="text"
|
|||
|
|
:value="verificationCode[index]"
|
|||
|
|
ref="inputRefs"
|
|||
|
|
inputmode="numeric"
|
|||
|
|
pattern="[0-9]*"
|
|||
|
|
:disabled="loading"
|
|||
|
|
@input="(e) => onCodeInput(e, index)"
|
|||
|
|
@paste="(e) => onCodePaste(e, index)"
|
|||
|
|
@keydown="(e) => onCodeKeydown(e, index)"
|
|||
|
|
@keypress="(e) => onCodeKeypress(e)"
|
|||
|
|
@focus="onCodeFocus"
|
|||
|
|
class="code-input"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, nextTick } from 'vue'
|
|||
|
|
|
|||
|
|
interface Props {
|
|||
|
|
loading?: boolean
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface Emits {
|
|||
|
|
(e: 'complete', code: string): void
|
|||
|
|
(e: 'change', code: string): void
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const props = withDefaults(defineProps<Props>(), {
|
|||
|
|
loading: false
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const emit = defineEmits<Emits>()
|
|||
|
|
|
|||
|
|
// 验证码输入相关
|
|||
|
|
const verificationCode = ref(['', '', '', '', '', ''])
|
|||
|
|
const inputRefs = ref<HTMLInputElement[]>([])
|
|||
|
|
|
|||
|
|
// 验证码输入相关方法
|
|||
|
|
const onCodeInput = (e: Event, index: number) => {
|
|||
|
|
const input = e.target as HTMLInputElement
|
|||
|
|
const val = input.value
|
|||
|
|
const cleanVal = String(val).replace(/\D/g, '')
|
|||
|
|
|
|||
|
|
// 检查是否已经输入了5位数字(不包括当前正在输入的第6位)
|
|||
|
|
const filledCount = verificationCode.value.filter(v => v !== '').length
|
|||
|
|
const isAlmostComplete = filledCount >= 5
|
|||
|
|
|
|||
|
|
// 如果已经输入了5位数字,检查当前输入是否会导致超过6位
|
|||
|
|
if (isAlmostComplete && cleanVal.length === 1 && verificationCode.value[index] === '') {
|
|||
|
|
// 如果当前输入框是空的,说明这是第6位输入,允许
|
|||
|
|
// 如果当前输入框有值,说明是替换,不允许
|
|||
|
|
// 这里我们允许输入,但会在设置后检查
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只处理单个字符输入,粘贴由 paste 事件处理
|
|||
|
|
if (cleanVal.length === 1) {
|
|||
|
|
verificationCode.value[index] = cleanVal
|
|||
|
|
|
|||
|
|
// 自动跳转到下一个输入框(只有在还没到第6个时)
|
|||
|
|
if (index < 5) {
|
|||
|
|
nextTick(() => {
|
|||
|
|
inputRefs.value[index + 1]?.focus()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发出变化事件
|
|||
|
|
const finalCode = verificationCode.value.join('')
|
|||
|
|
emit('change', finalCode)
|
|||
|
|
|
|||
|
|
// 如果完成了6位,发出完成事件
|
|||
|
|
if (finalCode.length === 6) {
|
|||
|
|
emit('complete', finalCode)
|
|||
|
|
}
|
|||
|
|
} else if (cleanVal.length === 0) {
|
|||
|
|
// 处理删除
|
|||
|
|
verificationCode.value[index] = ''
|
|||
|
|
emit('change', verificationCode.value.join(''))
|
|||
|
|
}
|
|||
|
|
// 如果是多个字符,可能是粘贴,由 paste 事件处理,这里不做处理
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const onCodePaste = (e: ClipboardEvent, index: number) => {
|
|||
|
|
e.preventDefault()
|
|||
|
|
|
|||
|
|
// 检查是否已经输入了6位数字
|
|||
|
|
const currentCode = verificationCode.value.join('')
|
|||
|
|
if (currentCode.length === 6) {
|
|||
|
|
return // 如果已经完成,不允许粘贴
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const pasteData = (e.clipboardData || (window as any).clipboardData).getData('text')
|
|||
|
|
const cleanData = pasteData.replace(/\D/g, '') // 只保留数字
|
|||
|
|
|
|||
|
|
if (cleanData.length === 0) return
|
|||
|
|
|
|||
|
|
console.log('Paste detected:', cleanData)
|
|||
|
|
|
|||
|
|
// 从当前输入框开始填充
|
|||
|
|
for (let i = 0; i < Math.min(cleanData.length, 6 - index); i++) {
|
|||
|
|
verificationCode.value[index + i] = cleanData[i]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 移动焦点到下一个空白输入框
|
|||
|
|
const nextEmptyIndex = verificationCode.value.findIndex((val, i) => i >= index && val === '')
|
|||
|
|
if (nextEmptyIndex !== -1) {
|
|||
|
|
nextTick(() => {
|
|||
|
|
inputRefs.value[nextEmptyIndex]?.focus()
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
nextTick(() => {
|
|||
|
|
inputRefs.value[5]?.focus()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发出完成事件
|
|||
|
|
emit('complete', verificationCode.value.join(''))
|
|||
|
|
emit('change', verificationCode.value.join(''))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const onCodeKeydown = (e: KeyboardEvent, index: number) => {
|
|||
|
|
// 处理删除键
|
|||
|
|
if (e.key === 'Backspace') {
|
|||
|
|
const input = e.target as HTMLInputElement
|
|||
|
|
const val = input.value
|
|||
|
|
|
|||
|
|
if (val === '') {
|
|||
|
|
// 当前输入框为空,删除上一个输入框的值
|
|||
|
|
if (index > 0) {
|
|||
|
|
verificationCode.value[index - 1] = ''
|
|||
|
|
nextTick(() => {
|
|||
|
|
inputRefs.value[index - 1]?.focus()
|
|||
|
|
})
|
|||
|
|
emit('change', verificationCode.value.join(''))
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 当前输入框有值,清空当前输入框
|
|||
|
|
verificationCode.value[index] = ''
|
|||
|
|
emit('change', verificationCode.value.join(''))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 阻止其他非数字字符(除了允许的控制键)
|
|||
|
|
else if (
|
|||
|
|
e.key &&
|
|||
|
|
!/[0-9]/.test(e.key) &&
|
|||
|
|
![
|
|||
|
|
'Backspace',
|
|||
|
|
'Delete',
|
|||
|
|
'Tab',
|
|||
|
|
'Enter',
|
|||
|
|
'ArrowLeft',
|
|||
|
|
'ArrowRight',
|
|||
|
|
'ArrowUp',
|
|||
|
|
'ArrowDown'
|
|||
|
|
].includes(e.key)
|
|||
|
|
) {
|
|||
|
|
e.preventDefault()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const onCodeKeypress = (e: KeyboardEvent) => {
|
|||
|
|
// 检查是否已经输入了6位数字
|
|||
|
|
const currentCode = verificationCode.value.join('')
|
|||
|
|
if (currentCode.length === 6) {
|
|||
|
|
e.preventDefault() // 如果已经完成,阻止任何按键输入
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只允许输入数字0-9
|
|||
|
|
const char = String.fromCharCode(e.which || (e as any).keyCode)
|
|||
|
|
if (!/[0-9]/.test(char)) {
|
|||
|
|
e.preventDefault()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const onCodeFocus = () => {
|
|||
|
|
// 聚焦到第一个空白输入框
|
|||
|
|
const index = verificationCode.value.findIndex(item => item === '')
|
|||
|
|
const focusIndex = index === -1 ? 5 : index
|
|||
|
|
nextTick(() => {
|
|||
|
|
inputRefs.value[focusIndex]?.focus()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 暴露给父组件的方法
|
|||
|
|
const reset = () => {
|
|||
|
|
verificationCode.value = ['', '', '', '', '', '']
|
|||
|
|
nextTick(() => {
|
|||
|
|
inputRefs.value[0]?.focus()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getCode = () => {
|
|||
|
|
return verificationCode.value.join('')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
defineExpose({
|
|||
|
|
reset,
|
|||
|
|
getCode,
|
|||
|
|
verificationCode
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped lang="less">
|
|||
|
|
.verification-code-input {
|
|||
|
|
display: flex;
|
|||
|
|
column-gap: 1.2rem;
|
|||
|
|
|
|||
|
|
.code-input {
|
|||
|
|
width: 5rem;
|
|||
|
|
height: 5rem;
|
|||
|
|
border: 0.15rem solid #d5d5d5;
|
|||
|
|
border-radius: 0.8rem;
|
|||
|
|
text-align: center;
|
|||
|
|
font-size: 2rem;
|
|||
|
|
line-height: 5rem;
|
|||
|
|
outline: none;
|
|||
|
|
transition: border-color 0.2s;
|
|||
|
|
|
|||
|
|
&:focus {
|
|||
|
|
border-color: #232323;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&:disabled {
|
|||
|
|
background-color: #f5f5f5;
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|