feat: award页面

This commit is contained in:
2026-01-19 10:56:39 +08:00
parent 3014414c97
commit 9aa3a2f889
3 changed files with 570 additions and 261 deletions

View File

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