chore(@vben/common-ui): add verify component (#4390)
* chore(@vben/common-ui): 增加拖拽校验组件 * chore: 增加样式 * Merge branch 'main' into wangjue-verify-comp * chore: 封装action组件 * chore: 拆分完成拖拽功能 * chore: 样式调整为tailwindcss语法 * chore: 导出check图标 * chore: 拖动的图标变为@vben/icons的 * chore: 完成插槽功能迁移 * fix: ci error * chore: 适配暗黑主题 * chore: 国际化 * chore: resolve conflict * chore: 迁移v2的图片旋转校验组件 * chore: 完善选择校验demo * chore: 转换为tailwindcss * chore: 替换为系统的颜色变量 * chore: 使用interface代替组件的props声明 * chore: 调整props * chore: 优化demo背景 * chore: follow suggest * chore: rm unnecessary style tag * chore: update demo * perf: improve the experience of Captcha components --------- Co-authored-by: vince <vince292007@gmail.com> Co-authored-by: Vben <ann.vben@gmail.com>
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
CaptchaVerifyPassingData,
|
||||
SliderCaptchaActionType,
|
||||
SliderRotateCaptchaProps,
|
||||
SliderRotateVerifyPassingData,
|
||||
} from '../types';
|
||||
|
||||
import { computed, reactive, unref, useTemplateRef, watch } from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { useTimeoutFn } from '@vueuse/core';
|
||||
|
||||
import SliderCaptcha from '../slider-captcha/index.vue';
|
||||
|
||||
const props = withDefaults(defineProps<SliderRotateCaptchaProps>(), {
|
||||
defaultTip: '',
|
||||
diffDegree: 20,
|
||||
imageSize: 260,
|
||||
maxDegree: 300,
|
||||
minDegree: 120,
|
||||
src: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [CaptchaVerifyPassingData];
|
||||
}>();
|
||||
|
||||
const slideBarRef = useTemplateRef<SliderCaptchaActionType>('slideBarRef');
|
||||
|
||||
const state = reactive({
|
||||
currentRotate: 0,
|
||||
dragging: false,
|
||||
endTime: 0,
|
||||
imgStyle: {},
|
||||
isPassing: false,
|
||||
randomRotate: 0,
|
||||
showTip: false,
|
||||
startTime: 0,
|
||||
toOrigin: false,
|
||||
});
|
||||
|
||||
const modalValue = defineModel<boolean>({ default: false });
|
||||
|
||||
watch(
|
||||
() => state.isPassing,
|
||||
(isPassing) => {
|
||||
if (isPassing) {
|
||||
const { endTime, startTime } = state;
|
||||
const time = (endTime - startTime) / 1000;
|
||||
emit('success', { isPassing, time: time.toFixed(1) });
|
||||
}
|
||||
modalValue.value = isPassing;
|
||||
},
|
||||
);
|
||||
|
||||
const getImgWrapStyleRef = computed(() => {
|
||||
const { imageSize, imageWrapperStyle } = props;
|
||||
return {
|
||||
height: `${imageSize}px`,
|
||||
width: `${imageSize}px`,
|
||||
...imageWrapperStyle,
|
||||
};
|
||||
});
|
||||
|
||||
const getFactorRef = computed(() => {
|
||||
const { maxDegree, minDegree } = props;
|
||||
if (minDegree === maxDegree) {
|
||||
return Math.floor(1 + Math.random() * 1) / 10 + 1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
|
||||
function handleStart() {
|
||||
state.startTime = Date.now();
|
||||
}
|
||||
|
||||
function handleDragBarMove(data: SliderRotateVerifyPassingData) {
|
||||
state.dragging = true;
|
||||
const { imageSize, maxDegree } = props;
|
||||
const { moveX } = data;
|
||||
const denominator = imageSize!;
|
||||
if (denominator === 0) {
|
||||
return;
|
||||
}
|
||||
const currentRotate = Math.ceil(
|
||||
(moveX / denominator) * 1.5 * maxDegree! * unref(getFactorRef),
|
||||
);
|
||||
state.currentRotate = currentRotate;
|
||||
setImgRotate(state.randomRotate - currentRotate);
|
||||
}
|
||||
|
||||
function handleImgOnLoad() {
|
||||
const { maxDegree, minDegree } = props;
|
||||
const ranRotate = Math.floor(
|
||||
minDegree! + Math.random() * (maxDegree! - minDegree!),
|
||||
); // 生成随机角度
|
||||
state.randomRotate = ranRotate;
|
||||
setImgRotate(ranRotate);
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
const { currentRotate, randomRotate } = state;
|
||||
const { diffDegree } = props;
|
||||
|
||||
if (Math.abs(randomRotate - currentRotate) >= (diffDegree || 20)) {
|
||||
setImgRotate(randomRotate);
|
||||
state.toOrigin = true;
|
||||
useTimeoutFn(() => {
|
||||
state.toOrigin = false;
|
||||
state.showTip = true;
|
||||
// 时间与动画时间保持一致
|
||||
}, 300);
|
||||
} else {
|
||||
checkPass();
|
||||
}
|
||||
state.showTip = true;
|
||||
}
|
||||
|
||||
function setImgRotate(deg: number) {
|
||||
state.imgStyle = {
|
||||
transform: `rotateZ(${deg}deg)`,
|
||||
};
|
||||
}
|
||||
|
||||
function checkPass() {
|
||||
state.isPassing = true;
|
||||
state.endTime = Date.now();
|
||||
}
|
||||
|
||||
function resume() {
|
||||
state.showTip = false;
|
||||
const basicEl = unref(slideBarRef);
|
||||
if (!basicEl) {
|
||||
return;
|
||||
}
|
||||
state.isPassing = false;
|
||||
|
||||
basicEl.resume();
|
||||
handleImgOnLoad();
|
||||
}
|
||||
|
||||
const imgCls = computed(() => {
|
||||
return state.toOrigin ? ['transition-transform duration-300'] : [];
|
||||
});
|
||||
|
||||
const verifyTip = computed(() => {
|
||||
return state.isPassing
|
||||
? $t('ui.captcha.sliderRotateSuccessTip', [
|
||||
((state.endTime - state.startTime) / 1000).toFixed(1),
|
||||
])
|
||||
: $t('ui.captcha.sliderRotateFailTip');
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
resume,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex flex-col items-center">
|
||||
<div
|
||||
:style="getImgWrapStyleRef"
|
||||
class="border-border relative overflow-hidden rounded-full border shadow-md"
|
||||
>
|
||||
<img
|
||||
:class="imgCls"
|
||||
:src="src"
|
||||
:style="state.imgStyle"
|
||||
alt="verify"
|
||||
class="w-full rounded-full"
|
||||
@click="resume"
|
||||
@load="handleImgOnLoad"
|
||||
/>
|
||||
<div
|
||||
class="absolute bottom-3 left-0 z-10 block h-7 w-full text-center text-xs leading-[30px] text-white"
|
||||
>
|
||||
<div
|
||||
v-if="state.showTip"
|
||||
:class="{
|
||||
'bg-success/80': state.isPassing,
|
||||
'bg-destructive/80': !state.isPassing,
|
||||
}"
|
||||
>
|
||||
{{ verifyTip }}
|
||||
</div>
|
||||
<div v-if="!state.showTip && !state.dragging" class="bg-black/30">
|
||||
{{ defaultTip || $t('ui.captcha.sliderRotateDefaultTip') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SliderCaptcha
|
||||
ref="slideBarRef"
|
||||
v-model="modalValue"
|
||||
class="mt-5"
|
||||
is-slot
|
||||
@end="handleDragEnd"
|
||||
@move="handleDragBarMove"
|
||||
@start="handleStart"
|
||||
>
|
||||
<template v-for="(_, key) in $slots" :key="key" #[key]="slotProps">
|
||||
<slot :name="key" v-bind="slotProps"></slot>
|
||||
</template>
|
||||
</SliderCaptcha>
|
||||
</div>
|
||||
</template>
|
Reference in New Issue
Block a user