279 lines
5.8 KiB
Vue
279 lines
5.8 KiB
Vue
<template>
|
|
<view
|
|
class="verify-code-input"
|
|
@tap="focusInput"
|
|
:style="{
|
|
'--vci-active-color': activeColor,
|
|
'--vci-filled-color': filledColor,
|
|
'--vci-border-color': borderColor
|
|
}"
|
|
>
|
|
<view class="vci-title" v-if="title">
|
|
<text class="vci-title__text">{{ title }}</text>
|
|
</view>
|
|
<view class="vci-desc" v-if="desc">
|
|
<text class="vci-desc__text">{{ desc }}</text>
|
|
</view>
|
|
|
|
<view class="vci-code-box">
|
|
<view
|
|
v-for="(item, index) in codeLength"
|
|
:key="index"
|
|
class="vci-code-item"
|
|
:class="{
|
|
'vci-code-item--active': isFocused && currentIndex === index,
|
|
'vci-code-item--filled': code.length > index,
|
|
'vci-code-item--underline': type === 'underline',
|
|
'vci-code-item--box': type === 'box'
|
|
}"
|
|
>
|
|
<text v-if="code[index] && !mask" class="vci-code-text">{{ code[index] }}</text>
|
|
<view v-else-if="code[index] && mask" class="vci-code-dot"></view>
|
|
<view v-if="isFocused && currentIndex === index && code.length === index" class="vci-code-cursor"></view>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="vci-tips" v-if="errorMsg">
|
|
<text class="vci-tips__text">{{ errorMsg }}</text>
|
|
</view>
|
|
|
|
<input
|
|
ref="inputRef"
|
|
class="vci-hidden-input"
|
|
type="number"
|
|
:maxlength="codeLength"
|
|
:value="code"
|
|
:focus="isFocused"
|
|
:adjust-position="false"
|
|
@input="onInput"
|
|
@focus="onFocus"
|
|
@blur="onBlur"
|
|
/>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch } from 'vue'
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
title: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
desc: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
length: {
|
|
type: Number,
|
|
default: 6
|
|
},
|
|
mask: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
type: {
|
|
type: String,
|
|
default: 'box'
|
|
},
|
|
errorMsg: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
autoFocus: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
activeColor: {
|
|
type: String,
|
|
default: '#1677FF'
|
|
},
|
|
filledColor: {
|
|
type: String,
|
|
default: '#1A202C'
|
|
},
|
|
borderColor: {
|
|
type: String,
|
|
default: '#E2E8F0'
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue', 'complete', 'focus', 'blur'])
|
|
|
|
const code = ref(props.modelValue)
|
|
const isFocused = ref(false)
|
|
const currentIndex = ref(0)
|
|
const inputRef = ref(null)
|
|
|
|
const codeLength = computed(() => props.length)
|
|
|
|
watch(() => props.modelValue, (val) => {
|
|
code.value = val
|
|
currentIndex.value = Math.min(val.length, codeLength.value - 1)
|
|
})
|
|
|
|
function focusInput() {
|
|
isFocused.value = true
|
|
}
|
|
|
|
function onInput(e) {
|
|
let val = e.detail.value.replace(/\D/g, '').slice(0, codeLength.value)
|
|
code.value = val
|
|
currentIndex.value = Math.min(val.length, codeLength.value - 1)
|
|
emit('update:modelValue', val)
|
|
if (val.length === codeLength.value) {
|
|
emit('complete', val)
|
|
}
|
|
}
|
|
|
|
function onFocus() {
|
|
isFocused.value = true
|
|
currentIndex.value = code.value.length
|
|
emit('focus')
|
|
}
|
|
|
|
function onBlur() {
|
|
isFocused.value = false
|
|
emit('blur')
|
|
}
|
|
|
|
function clear() {
|
|
code.value = ''
|
|
currentIndex.value = 0
|
|
emit('update:modelValue', '')
|
|
}
|
|
|
|
function setFocus(val) {
|
|
isFocused.value = val
|
|
}
|
|
|
|
defineExpose({
|
|
clear,
|
|
focusInput,
|
|
setFocus
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.verify-code-input {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.vci-title {
|
|
margin-bottom: 8px;
|
|
|
|
&__text {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #1A202C;
|
|
}
|
|
}
|
|
|
|
.vci-desc {
|
|
margin-bottom: 32px;
|
|
|
|
&__text {
|
|
font-size: 13px;
|
|
color: #718096;
|
|
}
|
|
}
|
|
|
|
.vci-code-box {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.vci-code-item {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.2s ease;
|
|
|
|
&--box {
|
|
width: 44px;
|
|
height: 44px;
|
|
background: #FFFFFF;
|
|
border: 1px solid var(--vci-border-color, #E2E8F0);
|
|
border-radius: 8px;
|
|
|
|
&.vci-code-item--active {
|
|
border-color: var(--vci-active-color, #1677FF);
|
|
}
|
|
|
|
&.vci-code-item--filled {
|
|
border-color: var(--vci-filled-color, #1A202C);
|
|
}
|
|
}
|
|
|
|
&--underline {
|
|
width: 40px;
|
|
height: 50px;
|
|
border-bottom: 2px solid var(--vci-border-color, #E2E8F0);
|
|
|
|
&.vci-code-item--active {
|
|
border-bottom-color: var(--vci-active-color, #FF8C42);
|
|
}
|
|
|
|
&.vci-code-item--filled {
|
|
border-bottom-color: var(--vci-filled-color, #1A202C);
|
|
}
|
|
}
|
|
}
|
|
|
|
.vci-code-text {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: var(--vci-filled-color, #1A202C);
|
|
line-height: 1;
|
|
}
|
|
|
|
.vci-code-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
background: var(--vci-filled-color, #1A202C);
|
|
}
|
|
|
|
.vci-code-cursor {
|
|
position: absolute;
|
|
width: 2px;
|
|
height: 24px;
|
|
background: var(--vci-active-color, #1677FF);
|
|
animation: vci-blink 1s infinite;
|
|
}
|
|
|
|
@keyframes vci-blink {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0; }
|
|
}
|
|
|
|
.vci-tips {
|
|
text-align: center;
|
|
margin-bottom: 16px;
|
|
|
|
&__text {
|
|
font-size: 13px;
|
|
color: #E53E3E;
|
|
}
|
|
}
|
|
|
|
.vci-hidden-input {
|
|
position: absolute;
|
|
left: -9999px;
|
|
top: -9999px;
|
|
width: 1px;
|
|
height: 1px;
|
|
opacity: 0;
|
|
}
|
|
</style>
|