feat: award页面
This commit is contained in:
@@ -1,151 +1,131 @@
|
||||
<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 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, nextTick } from 'vue'
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
|
||||
interface Props {
|
||||
loading?: boolean
|
||||
ct: string[]
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'complete', code: string): void
|
||||
(e: 'change', code: string): void
|
||||
(e: 'sendCaptcha', password: string): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false
|
||||
})
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// 验证码输入相关
|
||||
const verificationCode = ref(['', '', '', '', '', ''])
|
||||
const loading = ref(false)
|
||||
const timeout = ref<NodeJS.Timeout | null>(null)
|
||||
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位输入,允许
|
||||
// 如果当前输入框有值,说明是替换,不允许
|
||||
// 这里我们允许输入,但会在设置后检查
|
||||
const getCtData = computed({
|
||||
get: () => props.ct,
|
||||
set: (value: string[]) => {
|
||||
// 这里需要特殊处理,因为computed通常是只读的
|
||||
// 但原代码中直接修改了getCtData,所以这里需要emit一个事件或者使用其他方式
|
||||
// 由于这是父组件传来的props,我们需要通过emit通知父组件更新
|
||||
props.ct.splice(0, props.ct.length, ...value)
|
||||
}
|
||||
})
|
||||
|
||||
// 只处理单个字符输入,粘贴由 paste 事件处理
|
||||
if (cleanVal.length === 1) {
|
||||
verificationCode.value[index] = cleanVal
|
||||
const ctSize = computed(() => getCtData.value.length)
|
||||
|
||||
// 自动跳转到下一个输入框(只有在还没到第6个时)
|
||||
if (index < 5) {
|
||||
nextTick(() => {
|
||||
inputRefs.value[index + 1]?.focus()
|
||||
})
|
||||
}
|
||||
const cIndex = computed(() => {
|
||||
let i = getCtData.value.findIndex(item => item === '')
|
||||
i = (i + ctSize.value) % ctSize.value
|
||||
return i
|
||||
})
|
||||
|
||||
// 发出变化事件
|
||||
const finalCode = verificationCode.value.join('')
|
||||
emit('change', finalCode)
|
||||
const lastCode = computed(() => getCtData.value[ctSize.value - 1])
|
||||
|
||||
// 如果完成了6位,发出完成事件
|
||||
if (finalCode.length === 6) {
|
||||
emit('complete', finalCode)
|
||||
}
|
||||
} else if (cleanVal.length === 0) {
|
||||
// 处理删除
|
||||
verificationCode.value[index] = ''
|
||||
emit('change', verificationCode.value.join(''))
|
||||
watch(cIndex, () => {
|
||||
resetCaret()
|
||||
})
|
||||
|
||||
watch(lastCode, (newVal, oldVal) => {
|
||||
if (newVal && newVal !== oldVal) {
|
||||
inputRefs.value[ctSize.value - 1]?.blur()
|
||||
sendCaptcha()
|
||||
}
|
||||
// 如果是多个字符,可能是粘贴,由 paste 事件处理,这里不做处理
|
||||
}
|
||||
})
|
||||
|
||||
const onCodePaste = (e: ClipboardEvent, index: number) => {
|
||||
e.preventDefault()
|
||||
onMounted(() => {
|
||||
resetCaret()
|
||||
})
|
||||
|
||||
// 检查是否已经输入了6位数字
|
||||
const currentCode = verificationCode.value.join('')
|
||||
if (currentCode.length === 6) {
|
||||
return // 如果已经完成,不允许粘贴
|
||||
const onInput = (val: string, index: number) => {
|
||||
if (timeout.value) {
|
||||
clearTimeout(timeout.value)
|
||||
}
|
||||
|
||||
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(''))
|
||||
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 {
|
||||
// 当前输入框有值,清空当前输入框
|
||||
verificationCode.value[index] = ''
|
||||
emit('change', verificationCode.value.join(''))
|
||||
}
|
||||
}
|
||||
|
||||
// 阻止其他非数字字符(除了允许的控制键)
|
||||
// 阻止其他非数字字符
|
||||
else if (
|
||||
e.key &&
|
||||
!/[0-9]/.test(e.key) &&
|
||||
@@ -164,73 +144,48 @@ const onCodeKeydown = (e: KeyboardEvent, index: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
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 sendCaptcha = () => {
|
||||
const password = getCtData.value.map(item => item).join('')
|
||||
emit('sendCaptcha', password)
|
||||
}
|
||||
|
||||
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('')
|
||||
// 重置。一般是验证码错误时触发。
|
||||
getCtData.value = getCtData.value.map(() => '')
|
||||
resetCaret()
|
||||
}
|
||||
|
||||
// 暴露reset方法给父组件使用
|
||||
defineExpose({
|
||||
reset,
|
||||
getCode,
|
||||
verificationCode
|
||||
reset
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.verification-code-input {
|
||||
.captcha {
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
}
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user