#uniapp 滑块验证组件
slider-verify.vue
<template>
<view class="slider-verify-container" :style="{ width: width + 'rpx', height: height + 'rpx' }" ref="containerRef">
<!-- 背景轨道 -->
<view class="slider-track" :class="{ verified: isVerified }" :style="{ height: height + 'rpx', borderRadius: borderRadius + 'rpx' }">
<!-- 验证通过文字 -->
<text v-if="isVerified" class="verified-text" :style="{ fontSize: fontSize + 'rpx' }">验证通过</text>
<!-- 默认文字 -->
<text v-else class="slider-text" :style="{ fontSize: fontSize + 'rpx' }">{{ sliderText }}</text>
<!-- 进度条 -->
<view
class="slider-progress"
:class="{ dragging: isDragging }"
:style="{
width: progressWidth + 'rpx',
height: height + 'rpx',
borderRadius: borderRadius + 'rpx',
background: !showFailAnimation ? progressColor : 'rgba(255, 107, 107, 0.5)'
}"
></view>
</view>
<!-- 滑块按钮 -->
<view
class="slider-button"
:class="{ verified: isVerified, dragging: isDragging, failed: showFailAnimation }"
:style="{
left: buttonLeft + 'rpx',
width: buttonSize + 'rpx',
height: buttonSize + 'rpx'
}"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
@touchcancel="onTouchEnd"
>
<wd-icon v-if="!isVerified && !showFailAnimation" name="arrow-right" size="20" color="#ffffff"></wd-icon>
<wd-icon v-if="isVerified" name="check" size="20" color="#ffffff"></wd-icon>
<wd-icon v-if="showFailAnimation" name="close" size="20" color="#ffffff"></wd-icon>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
// v-model 绑定验证状态:0-初始化,1-验证成功
const modelValue = defineModel({
type: Number,
default: 0
});
const props = defineProps({
// 组件宽度
width: {
type: Number,
default: 600
},
// 组件高度
height: {
type: Number,
default: 100
},
// 圆角大小
borderRadius: {
type: Number,
default: 16
},
// 按钮大小
buttonSize: {
type: Number,
default: 100
},
// 文字大小
fontSize: {
type: Number,
default: 32
},
// 滑块文字
sliderText: {
type: String,
default: '向右滑动验证'
},
// 进度条颜色
progressColor: {
type: String,
default: 'rgba(69, 225, 129, 0.5)'
},
// 最大滑动距离
maxDistance: {
type: Number,
default: 0
},
// 是否开启验证
enable: {
type: Boolean,
default: true
},
// 自定义验证函数,返回 Promise<boolean>
validator: {
type: Function,
default: null
}
});
const emit = defineEmits(['success', 'fail', 'change', 'reset', 'update:modelValue']);
// 响应式状态
const isDragging = ref(false);
const isVerified = ref(false);
const showFailAnimation = ref(false);
const startX = ref(0);
const currentX = ref(0);
const buttonLeft = ref(0);
const progressWidth = ref(0);
const containerLeft = ref(0);
const hasGotContainerLeft = ref(false);
// 获取容器左边距
const getContainerLeft = () => {
return new Promise((resolve) => {
const query = uni.createSelectorQuery();
query.select('.slider-verify-container').boundingClientRect();
query.exec((res) => {
if (res && res[0]) {
containerLeft.value = res[0].left;
hasGotContainerLeft.value = true;
resolve(res[0].left);
} else {
resolve(0);
}
});
});
};
// 计算最大滑动距离
const computedMaxDistance = computed(() => {
if (props.maxDistance > 0) {
return props.maxDistance;
}
return props.width - props.buttonSize;
});
// 触摸开始
const onTouchStart = async (e) => {
if (!props.enable || isVerified.value) return;
// 获取容器左边距
if (!hasGotContainerLeft.value) {
await getContainerLeft();
}
isDragging.value = true;
// 使用 clientX(视窗坐标)减去容器左边距,得到相对于容器的坐标(px)
const relativeClientX = e.touches[0].clientX - containerLeft.value;
// 转换为 rpx:2rpx = 2rpx
startX.value = relativeClientX * 2;
currentX.value = relativeClientX * 2;
emit('change', {
status: 'dragging',
progress: progressWidth.value / computedMaxDistance.value
});
};
// 触摸移动
const onTouchMove = (e) => {
if (!isDragging.value || !props.enable || isVerified.value) return;
// 使用 clientX(视窗坐标)减去容器左边距,得到相对于容器的坐标(px)
const relativeClientX = e.touches[0].clientX - containerLeft.value;
// 转换为 rpx:2rpx = 2rpx
currentX.value = relativeClientX * 2;
const diff = currentX.value - startX.value;
// 限制滑动范围
const distance = Math.max(0, Math.min(diff, computedMaxDistance.value));
buttonLeft.value = distance;
progressWidth.value = distance + 20;
emit('change', {
status: 'dragging',
progress: progressWidth.value / computedMaxDistance.value
});
};
// 触摸结束
const onTouchEnd = async () => {
if (!isDragging.value) return;
isDragging.value = false;
// 检查是否滑动到最大距离
const isAtMaxDistance = progressWidth.value >= computedMaxDistance.value;
if (isAtMaxDistance) {
// 执行验证
let verifyResult = true;
// 如果有自定义验证器,优先使用
if (props.validator) {
try {
verifyResult = await props.validator();
} catch (error) {
console.error('验证失败:', error);
verifyResult = false;
}
}
if (verifyResult) {
// 验证成功
isVerified.value = true;
buttonLeft.value = computedMaxDistance.value;
progressWidth.value = computedMaxDistance.value;
modelValue.value = 1; // 更新 v-model 状态为验证成功
emit('success');
emit('change', {
status: 'success',
progress: 1
});
} else {
// 验证失败,触发失败动画和重置
triggerFailAnimation();
}
} else {
// 未滑动到最大距离,重置
triggerFailAnimation();
setTimeout(() => {
reset();
}, 300);
}
};
// 触发失败动画
const triggerFailAnimation = () => {
showFailAnimation.value = true;
emit('fail');
emit('change', {
status: 'fail',
progress: progressWidth.value / computedMaxDistance.value
});
// 失败动画持续300ms后重置
setTimeout(() => {
reset();
}, 1000);
};
// 重置组件
const reset = () => {
isDragging.value = false;
isVerified.value = false;
showFailAnimation.value = false;
startX.value = 0;
currentX.value = 0;
buttonLeft.value = 0;
progressWidth.value = 0;
modelValue.value = 0; // 重置 v-model 状态为初始化
emit('reset');
emit('change', {
status: 'reset',
progress: 0
});
};
// 监听 enable 属性变化
watch(
() => props.enable,
(newVal) => {
if (!newVal) {
reset();
}
}
);
// 监听 v-model 值变化,同步组件状态
watch(
() => modelValue.value,
(newVal) => {
if (newVal === 1) {
// v-model 设置为 1,表示验证成功
isVerified.value = true;
buttonLeft.value = computedMaxDistance.value;
progressWidth.value = computedMaxDistance.value;
} else if (newVal === 0) {
// v-model 设置为 0,表示重置
isVerified.value = false;
buttonLeft.value = 0;
progressWidth.value = 0;
}
}
);
// 对外暴露的重置方法
defineExpose({
reset,
isVerified: () => isVerified.value
});
</script>
<style lang="scss" scoped>
.slider-verify-container {
position: relative;
overflow: hidden;
}
.slider-track {
position: relative;
width: 100%;
height: 100%;
background-color: #e0e0e0;
overflow: hidden;
transition: background-color 0.3s;
&.verified {
background-color: #4cd964;
}
&.failed {
background-color: #f56c6c !important;
}
}
.slider-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #999999;
font-weight: 500;
z-index: 3;
pointer-events: none;
white-space: nowrap;
}
.verified-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #ffffff;
font-weight: 500;
z-index: 3;
pointer-events: none;
white-space: nowrap;
}
.slider-progress {
position: absolute;
left: 0;
top: 0;
z-index: 2;
border-radius: unset !important;
}
.slider-button {
position: absolute;
top: 0;
left: 0;
background: linear-gradient(135deg, #42d77d 0%, #26c26a 100%);
box-shadow: 0 4rpx 15rpx rgba(66, 215, 125, 0.5), 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
touch-action: none;
z-index: 10;
border-radius: 10rpx;
&.dragging {
box-shadow: 0 6rpx 25rpx rgba(66, 215, 125, 0.6), 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
transform: scale(1.02);
}
&.verified {
background: linear-gradient(135deg, #42d77d 0%, #26c26a 100%);
box-shadow: 0 4rpx 15rpx rgba(66, 215, 125, 0.5), 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
}
&.failed {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
box-shadow: 0 4rpx 15rpx rgba(255, 107, 107, 0.5), 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
animation: shake 0.3s ease-in-out;
}
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-10rpx);
}
75% {
transform: translateX(10rpx);
}
}
</style>
#组件使用
<SliderVerify v-model="sliderStatus"></SliderVerify>
