123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- <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 ('pageX' in e) {
- return e.pageX;
- } else if ('touches' in e && 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;
- contentEl.getEl().style.width = '100%';
- 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>
|