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>
|