257 lines
4.3 KiB
Plaintext
257 lines
4.3 KiB
Plaintext
<template>
|
|
<view
|
|
class="cl-input-otp"
|
|
:class="[
|
|
{
|
|
'cl-input-otp--disabled': disabled
|
|
},
|
|
pt.className
|
|
]"
|
|
>
|
|
<view class="cl-input-otp__inner">
|
|
<cl-input
|
|
v-model="value"
|
|
ref="inputRef"
|
|
:type="inputType"
|
|
:pt="{
|
|
className: '!h-full'
|
|
}"
|
|
:disabled="disabled"
|
|
:autofocus="autofocus"
|
|
:maxlength="length"
|
|
:hold-keyboard="false"
|
|
:clearable="false"
|
|
@change="onChange"
|
|
></cl-input>
|
|
</view>
|
|
|
|
<view class="cl-input-otp__list" :class="[pt.list?.className]">
|
|
<view
|
|
v-for="(item, index) in list"
|
|
:key="index"
|
|
class="cl-input-otp__item"
|
|
:class="[
|
|
{
|
|
'is-disabled': disabled,
|
|
'is-dark': isDark,
|
|
'is-active': value.length >= index && isFocus
|
|
},
|
|
pt.item?.className
|
|
]"
|
|
>
|
|
<cl-text
|
|
:color="value.length >= index && isFocus ? 'primary' : ''"
|
|
:pt="{
|
|
className: pt.value?.className
|
|
}"
|
|
>{{ item }}</cl-text
|
|
>
|
|
<view
|
|
class="cl-input-otp__cursor"
|
|
:class="[pt.cursor?.className]"
|
|
v-if="value.length == index && isFocus && item == ''"
|
|
></view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watch, type PropType, type Ref } from "vue";
|
|
import type { ClInputType, PassThroughProps } from "../../types";
|
|
import { isDark, parsePt } from "@/cool";
|
|
|
|
defineOptions({
|
|
name: "cl-input-otp"
|
|
});
|
|
|
|
// 定义组件属性
|
|
const props = defineProps({
|
|
// 透传样式
|
|
pt: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
// 绑定值
|
|
modelValue: {
|
|
type: String,
|
|
default: ""
|
|
},
|
|
// 是否自动聚焦
|
|
autofocus: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
// 验证码位数
|
|
length: {
|
|
type: Number,
|
|
default: 4
|
|
},
|
|
// 是否禁用
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
// 输入框类型
|
|
inputType: {
|
|
type: String as PropType<ClInputType>,
|
|
default: "number"
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 事件定义
|
|
* update:modelValue - 更新绑定值
|
|
* done - 输入完成
|
|
*/
|
|
const emit = defineEmits(["update:modelValue", "done"]);
|
|
|
|
/**
|
|
* 透传样式类型定义
|
|
*/
|
|
type PassThrough = {
|
|
className?: string;
|
|
list?: PassThroughProps;
|
|
item?: PassThroughProps;
|
|
cursor?: PassThroughProps;
|
|
value?: PassThroughProps;
|
|
};
|
|
|
|
// 解析透传样式
|
|
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
|
|
|
// 输入框引用
|
|
const inputRef = ref<ClInputComponentPublicInstance | null>(null);
|
|
|
|
// 输入值
|
|
const value = ref(props.modelValue) as Ref<string>;
|
|
|
|
/**
|
|
* 是否聚焦状态
|
|
*/
|
|
const isFocus = computed<boolean>(() => {
|
|
if (props.disabled) {
|
|
return false;
|
|
}
|
|
|
|
if (inputRef.value == null) {
|
|
return false;
|
|
}
|
|
|
|
return (inputRef.value as ClInputComponentPublicInstance).isFocus;
|
|
});
|
|
|
|
/**
|
|
* 验证码数组
|
|
* 根据长度生成空数组,每个位置填充对应的输入值
|
|
*/
|
|
const list = computed<string[]>(() => {
|
|
const arr = [] as string[];
|
|
|
|
for (let i = 0; i < props.length; i++) {
|
|
arr.push(value.value.charAt(i));
|
|
}
|
|
|
|
return arr;
|
|
});
|
|
|
|
/**
|
|
* 监听绑定值变化
|
|
* 同步更新内部值
|
|
*/
|
|
watch(
|
|
computed(() => props.modelValue),
|
|
(val: string) => {
|
|
value.value = val;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* 输入事件处理
|
|
* @param val 输入值
|
|
*/
|
|
function onChange(val: string) {
|
|
// 更新绑定值
|
|
emit("update:modelValue", val);
|
|
|
|
// 输入完成时触发done事件
|
|
if (val.length == props.length) {
|
|
uni.hideKeyboard();
|
|
emit("done", val);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.cl-input-otp {
|
|
@apply relative overflow-visible;
|
|
|
|
&__inner {
|
|
@apply absolute top-0 h-full z-10;
|
|
opacity: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
}
|
|
|
|
&__list {
|
|
@apply flex flex-row relative;
|
|
margin: 0 -10rpx;
|
|
}
|
|
|
|
&__item {
|
|
@apply flex flex-row items-center justify-center duration-100;
|
|
@apply border border-solid border-surface-200 rounded-lg bg-white;
|
|
height: 80rpx;
|
|
width: 80rpx;
|
|
margin: 0 10rpx;
|
|
|
|
&.is-disabled {
|
|
@apply bg-surface-100 opacity-70;
|
|
}
|
|
|
|
&.is-dark {
|
|
@apply bg-surface-800 border-surface-600;
|
|
|
|
&.is-disabled {
|
|
@apply bg-surface-700;
|
|
}
|
|
}
|
|
|
|
&.is-active {
|
|
@apply border-primary-500;
|
|
}
|
|
}
|
|
|
|
&__cursor {
|
|
@apply absolute duration-100;
|
|
@apply bg-primary-500;
|
|
width: 2rpx;
|
|
height: 24rpx;
|
|
}
|
|
|
|
// #ifdef H5 || MP
|
|
&__cursor {
|
|
animation: flash 1s infinite ease;
|
|
}
|
|
|
|
@keyframes flash {
|
|
0% {
|
|
opacity: 0;
|
|
}
|
|
|
|
50% {
|
|
opacity: 1;
|
|
}
|
|
|
|
100% {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
// #endif
|
|
|
|
&--disabled {
|
|
@apply opacity-50;
|
|
}
|
|
}
|
|
</style>
|