192 lines
4.3 KiB
Vue
192 lines
4.3 KiB
Vue
<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>
|