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:
invalid w
2024-09-21 20:52:36 +08:00
committed by GitHub
parent dbe5b33db6
commit 000172e482
39 changed files with 1017 additions and 110 deletions

View File

@@ -0,0 +1,241 @@
<script setup lang="ts">
import type {
CaptchaVerifyPassingData,
SliderCaptchaProps,
SliderRotateVerifyPassingData,
} from '../types';
import { reactive, unref, useTemplateRef, watch, watchEffect } from 'vue';
import { $t } from '@vben/locales';
import { cn } from '@vben-core/shared/utils';
import { useTimeoutFn } from '@vueuse/core';
import SliderCaptchaAction from './slider-captcha-action.vue';
import SliderCaptchaBar from './slider-captcha-bar.vue';
import SliderCaptchaContent from './slider-captcha-content.vue';
const props = withDefaults(defineProps<SliderCaptchaProps>(), {
actionStyle: () => ({}),
barStyle: () => ({}),
contentStyle: () => ({}),
isSlot: false,
successText: '',
text: '',
wrapperStyle: () => ({}),
});
const emit = defineEmits<{
end: [MouseEvent | TouchEvent];
move: [SliderRotateVerifyPassingData];
start: [MouseEvent | TouchEvent];
success: [CaptchaVerifyPassingData];
}>();
const modelValue = defineModel<boolean>({ default: false });
const state = reactive({
endTime: 0,
isMoving: false,
isPassing: false,
moveDistance: 0,
startTime: 0,
toLeft: false,
});
defineExpose({
resume,
});
const wrapperRef = useTemplateRef<HTMLDivElement>('wrapperRef');
const barRef = useTemplateRef<typeof SliderCaptchaBar>('barRef');
const contentRef = useTemplateRef<typeof SliderCaptchaContent>('contentRef');
const actionRef = useTemplateRef<typeof SliderCaptchaAction>('actionRef');
watch(
() => state.isPassing,
(isPassing) => {
if (isPassing) {
const { endTime, startTime } = state;
const time = (endTime - startTime) / 1000;
emit('success', { isPassing, time: time.toFixed(1) });
modelValue.value = isPassing;
}
},
);
watchEffect(() => {
state.isPassing = !!modelValue.value;
});
function getEventPageX(e: MouseEvent | TouchEvent): number {
if (e instanceof MouseEvent) {
return e.pageX;
} else if (e instanceof TouchEvent && e.touches[0]) {
return e.touches[0].pageX;
}
return 0;
}
function handleDragStart(e: MouseEvent | TouchEvent) {
if (state.isPassing) {
return;
}
if (!actionRef.value) return;
emit('start', e);
state.moveDistance =
getEventPageX(e) -
Number.parseInt(
actionRef.value.getStyle().left.replace('px', '') || '0',
10,
);
state.startTime = Date.now();
state.isMoving = true;
}
function getOffset(actionEl: HTMLDivElement) {
const wrapperWidth = wrapperRef.value?.offsetWidth ?? 220;
const actionWidth = actionEl?.offsetWidth ?? 40;
const offset = wrapperWidth - actionWidth - 6;
return { actionWidth, offset, wrapperWidth };
}
function handleDragMoving(e: MouseEvent | TouchEvent) {
const { isMoving, moveDistance } = state;
if (isMoving) {
const actionEl = unref(actionRef);
const barEl = unref(barRef);
if (!actionEl || !barEl) return;
const { actionWidth, offset, wrapperWidth } = getOffset(actionEl.getEl());
const moveX = getEventPageX(e) - moveDistance;
emit('move', {
event: e,
moveDistance,
moveX,
});
if (moveX > 0 && moveX <= offset) {
actionEl.setLeft(`${moveX}px`);
barEl.setWidth(`${moveX + actionWidth / 2}px`);
} else if (moveX > offset) {
actionEl.setLeft(`${wrapperWidth - actionWidth}px`);
barEl.setWidth(`${wrapperWidth - actionWidth / 2}px`);
if (!props.isSlot) {
checkPass();
}
}
}
}
function handleDragOver(e: MouseEvent | TouchEvent) {
const { isMoving, isPassing, moveDistance } = state;
if (isMoving && !isPassing) {
emit('end', e);
const actionEl = actionRef.value;
const barEl = unref(barRef);
if (!actionEl || !barEl) return;
const moveX = getEventPageX(e) - moveDistance;
const { actionWidth, offset, wrapperWidth } = getOffset(actionEl.getEl());
if (moveX < offset) {
if (props.isSlot) {
setTimeout(() => {
if (modelValue.value) {
const contentEl = unref(contentRef);
if (contentEl) {
contentEl.getEl().style.width = `${Number.parseInt(barEl.getEl().style.width)}px`;
}
} else {
resume();
}
}, 0);
} else {
resume();
}
} else {
actionEl.setLeft(`${wrapperWidth - actionWidth + 10}px`);
barEl.setWidth(`${wrapperWidth - actionWidth / 2}px`);
checkPass();
}
state.isMoving = false;
}
}
function checkPass() {
if (props.isSlot) {
resume();
return;
}
state.endTime = Date.now();
state.isPassing = true;
state.isMoving = false;
}
function resume() {
state.isMoving = false;
state.isPassing = false;
state.moveDistance = 0;
state.toLeft = false;
state.startTime = 0;
state.endTime = 0;
const actionEl = unref(actionRef);
const barEl = unref(barRef);
const contentEl = unref(contentRef);
if (!actionEl || !barEl || !contentEl) return;
state.toLeft = true;
useTimeoutFn(() => {
state.toLeft = false;
actionEl.setLeft('0');
barEl.setWidth('0');
}, 300);
}
</script>
<template>
<div
ref="wrapperRef"
:class="
cn(
'border-border bg-background-deep relative flex h-10 w-full items-center overflow-hidden rounded-md border text-center',
props.class,
)
"
:style="wrapperStyle"
@mouseleave="handleDragOver"
@mousemove="handleDragMoving"
@mouseup="handleDragOver"
@touchend="handleDragOver"
@touchmove="handleDragMoving"
>
<SliderCaptchaBar
ref="barRef"
:bar-style="barStyle"
:to-left="state.toLeft"
/>
<SliderCaptchaContent
ref="contentRef"
:content-style="contentStyle"
:is-passing="state.isPassing"
:success-text="successText || $t('ui.captcha.sliderSuccessText')"
:text="text || $t('ui.captcha.sliderDefaultText')"
>
<template v-if="$slots.text" #text>
<slot :is-passing="state.isPassing" name="text"></slot>
</template>
</SliderCaptchaContent>
<SliderCaptchaAction
ref="actionRef"
:action-style="actionStyle"
:is-passing="state.isPassing"
:to-left="state.toLeft"
@mousedown="handleDragStart"
@touchstart="handleDragStart"
>
<template v-if="$slots.actionIcon" #icon>
<slot :is-passing="state.isPassing" name="actionIcon"></slot>
</template>
</SliderCaptchaAction>
</div>
</template>