Files
aida_front/src/views/AwardPage/components/VerificationCodeInput.vue

192 lines
4.3 KiB
Vue
Raw Normal View History

2026-01-16 14:30:48 +08:00
<template>
2026-01-19 10:56:39 +08:00
<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"
/>
2026-01-16 14:30:48 +08:00
</div>
</template>
<script setup lang="ts">
2026-01-19 10:56:39 +08:00
import { ref, computed, watch, onMounted } from 'vue'
2026-01-16 14:30:48 +08:00
interface Props {
2026-01-19 10:56:39 +08:00
ct: string[]
2026-01-16 14:30:48 +08:00
}
interface Emits {
2026-01-19 10:56:39 +08:00
(e: 'sendCaptcha', password: string): void
2026-01-16 14:30:48 +08:00
}
2026-01-19 10:56:39 +08:00
const props = defineProps<Props>()
2026-01-16 14:30:48 +08:00
const emit = defineEmits<Emits>()
2026-01-19 10:56:39 +08:00
const loading = ref(false)
const timeout = ref<NodeJS.Timeout | null>(null)
2026-01-16 14:30:48 +08:00
const inputRefs = ref<HTMLInputElement[]>([])
2026-01-19 10:56:39 +08:00
const getCtData = computed({
get: () => props.ct,
set: (value: string[]) => {
// 这里需要特殊处理因为computed通常是只读的
// 但原代码中直接修改了getCtData所以这里需要emit一个事件或者使用其他方式
// 由于这是父组件传来的props我们需要通过emit通知父组件更新
props.ct.splice(0, props.ct.length, ...value)
2026-01-16 14:30:48 +08:00
}
2026-01-19 10:56:39 +08:00
})
2026-01-16 14:30:48 +08:00
2026-01-19 10:56:39 +08:00
const ctSize = computed(() => getCtData.value.length)
2026-01-16 14:30:48 +08:00
2026-01-19 10:56:39 +08:00
const cIndex = computed(() => {
let i = getCtData.value.findIndex(item => item === '')
i = (i + ctSize.value) % ctSize.value
return i
})
2026-01-16 14:30:48 +08:00
2026-01-19 10:56:39 +08:00
const lastCode = computed(() => getCtData.value[ctSize.value - 1])
2026-01-16 14:30:48 +08:00
2026-01-19 10:56:39 +08:00
watch(cIndex, () => {
resetCaret()
})
watch(lastCode, (newVal, oldVal) => {
if (newVal && newVal !== oldVal) {
inputRefs.value[ctSize.value - 1]?.blur()
sendCaptcha()
2026-01-16 14:30:48 +08:00
}
2026-01-19 10:56:39 +08:00
})
2026-01-16 14:30:48 +08:00
2026-01-19 10:56:39 +08:00
onMounted(() => {
resetCaret()
})
2026-01-16 14:30:48 +08:00
2026-01-19 10:56:39 +08:00
const onInput = (val: string, index: number) => {
if (timeout.value) {
clearTimeout(timeout.value)
2026-01-16 14:30:48 +08:00
}
2026-01-19 10:56:39 +08:00
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)
}
2026-01-16 14:30:48 +08:00
2026-01-19 10:56:39 +08:00
const onPause = () => {}
2026-01-16 14:30:48 +08:00
2026-01-19 10:56:39 +08:00
const resetCaret = () => {
inputRefs.value[ctSize.value - 1]?.focus()
}
2026-01-16 14:30:48 +08:00
2026-01-19 10:56:39 +08:00
const onFocus = () => {
// 监听 focus 事件,将光标重定位到"第一个空白符的位置"。
let index = getCtData.value.findIndex(item => item === '')
index = (index + ctSize.value) % ctSize.value
inputRefs.value[index]?.focus()
}
2026-01-16 14:30:48 +08:00
2026-01-19 10:56:39 +08:00
const onKeypress = (e: KeyboardEvent) => {
// 只允许输入数字0-9
const char = String.fromCharCode((e as any).which)
if (!/[0-9]/.test(char)) {
e.preventDefault()
2026-01-16 14:30:48 +08:00
}
}
2026-01-19 10:56:39 +08:00
const onKeydown = (e: KeyboardEvent, index: number) => {
2026-01-16 14:30:48 +08:00
// 处理删除键
2026-01-19 10:56:39 +08:00
if (e.key === 'Backspace' || e.key === 'Delete') {
const val = (e.target as HTMLInputElement).value
2026-01-16 14:30:48 +08:00
if (val === '') {
2026-01-19 10:56:39 +08:00
// 删除上一个input里的值并对其focus。
2026-01-16 14:30:48 +08:00
if (index > 0) {
2026-01-19 10:56:39 +08:00
getCtData.value[index - 1] = ''
inputRefs.value[index - 1]?.focus()
2026-01-16 14:30:48 +08:00
}
}
}
2026-01-19 10:56:39 +08:00
// 阻止其他非数字字符
2026-01-16 14:30:48 +08:00
else if (
e.key &&
!/[0-9]/.test(e.key) &&
![
'Backspace',
'Delete',
'Tab',
'Enter',
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'ArrowDown'
].includes(e.key)
) {
e.preventDefault()
}
}
2026-01-19 10:56:39 +08:00
const sendCaptcha = () => {
const password = getCtData.value.map(item => item).join('')
emit('sendCaptcha', password)
2026-01-16 14:30:48 +08:00
}
const reset = () => {
2026-01-19 10:56:39 +08:00
// 重置。一般是验证码错误时触发。
getCtData.value = getCtData.value.map(() => '')
resetCaret()
2026-01-16 14:30:48 +08:00
}
2026-01-19 10:56:39 +08:00
// 暴露reset方法给父组件使用
2026-01-16 14:30:48 +08:00
defineExpose({
2026-01-19 10:56:39 +08:00
reset
2026-01-16 14:30:48 +08:00
})
</script>
<style scoped lang="less">
2026-01-19 10:56:39 +08:00
.captcha {
width: 100%;
2026-01-16 14:30:48 +08:00
display: flex;
2026-01-19 10:56:39 +08:00
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;
2026-01-16 14:30:48 +08:00
}
2026-01-19 10:56:39 +08:00
</style>