Files
aida_front/src/views/AwardPage/components/VerificationCodeInput.vue
2026-01-19 10:56:39 +08:00

192 lines
4.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="captcha">
<input
v-for="(c, index) in getCtData"
:key="index"
type="text"
v-model="getCtData[index]"
ref="inputRefs"
inputmode="numeric"
pattern="[0-9]*"
@input="e => onInput(e.target.value, index)"
@keydown="e => onKeydown(e, index)"
@keypress="e => onKeypress(e)"
@focus="onFocus"
@pause="onPause"
:disabled="loading"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
interface Props {
ct: string[]
}
interface Emits {
(e: 'sendCaptcha', password: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const loading = ref(false)
const timeout = ref<NodeJS.Timeout | null>(null)
const inputRefs = ref<HTMLInputElement[]>([])
const getCtData = computed({
get: () => props.ct,
set: (value: string[]) => {
// 这里需要特殊处理因为computed通常是只读的
// 但原代码中直接修改了getCtData所以这里需要emit一个事件或者使用其他方式
// 由于这是父组件传来的props我们需要通过emit通知父组件更新
props.ct.splice(0, props.ct.length, ...value)
}
})
const ctSize = computed(() => getCtData.value.length)
const cIndex = computed(() => {
let i = getCtData.value.findIndex(item => item === '')
i = (i + ctSize.value) % ctSize.value
return i
})
const lastCode = computed(() => getCtData.value[ctSize.value - 1])
watch(cIndex, () => {
resetCaret()
})
watch(lastCode, (newVal, oldVal) => {
if (newVal && newVal !== oldVal) {
inputRefs.value[ctSize.value - 1]?.blur()
sendCaptcha()
}
})
onMounted(() => {
resetCaret()
})
const onInput = (val: string, index: number) => {
if (timeout.value) {
clearTimeout(timeout.value)
}
timeout.value = setTimeout(() => {
val = String(val).replace(/\D/g, '')
getCtData.value[index] = val
if (index === ctSize.value - 1) {
getCtData.value[ctSize.value - 1] = val[0] // 最后一个码,只允许输入一个字符。
} else if (val.length > 1) {
let i = index
for (i = index; i < ctSize.value && i - index < val.length; i++) {
getCtData.value[i] = val[i - index]
}
resetCaret()
} else if (!(val + '')) {
getCtData.value[index] = ''
}
}, 10)
}
const onPause = () => {}
const resetCaret = () => {
inputRefs.value[ctSize.value - 1]?.focus()
}
const onFocus = () => {
// 监听 focus 事件,将光标重定位到"第一个空白符的位置"。
let index = getCtData.value.findIndex(item => item === '')
index = (index + ctSize.value) % ctSize.value
inputRefs.value[index]?.focus()
}
const onKeypress = (e: KeyboardEvent) => {
// 只允许输入数字0-9
const char = String.fromCharCode((e as any).which)
if (!/[0-9]/.test(char)) {
e.preventDefault()
}
}
const onKeydown = (e: KeyboardEvent, index: number) => {
// 处理删除键
if (e.key === 'Backspace' || e.key === 'Delete') {
const val = (e.target as HTMLInputElement).value
if (val === '') {
// 删除上一个input里的值并对其focus。
if (index > 0) {
getCtData.value[index - 1] = ''
inputRefs.value[index - 1]?.focus()
}
}
}
// 阻止其他非数字字符
else if (
e.key &&
!/[0-9]/.test(e.key) &&
![
'Backspace',
'Delete',
'Tab',
'Enter',
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'ArrowDown'
].includes(e.key)
) {
e.preventDefault()
}
}
const sendCaptcha = () => {
const password = getCtData.value.map(item => item).join('')
emit('sendCaptcha', password)
}
const reset = () => {
// 重置。一般是验证码错误时触发。
getCtData.value = getCtData.value.map(() => '')
resetCaret()
}
// 暴露reset方法给父组件使用
defineExpose({
reset
})
</script>
<style scoped lang="less">
.captcha {
width: 100%;
display: flex;
justify-content: space-between;
}
input {
width: 6rem;
height: 6rem;
border: 0.2rem solid #e6e6e6;
border-radius: 0.8rem;
text-align: center;
font-size: 2.4rem;
line-height: 6rem;
outline: none;
background-color: #f6f6f4;
}
input:last-of-type {
margin-right: 0;
}
input:disabled {
color: #000;
background-color: #f6f6f4;
}
.msg {
text-align: center;
}
</style>