Browse Source
* 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>pull/4461/head
committed by
GitHub
39 changed files with 1017 additions and 110 deletions
@ -1,3 +1,6 @@ |
|||||
export { default as CaptchaCard } from './captcha-card.vue'; |
export { default as PointSelectionCaptcha } from './point-selection-captcha/index.vue'; |
||||
export { default as PointSelectionCaptcha } from './point-selection-captcha.vue'; |
export { default as PointSelectionCaptchaCard } from './point-selection-captcha/index.vue'; |
||||
|
|
||||
|
export { default as SliderCaptcha } from './slider-captcha/index.vue'; |
||||
|
export { default as SliderRotateCaptcha } from './slider-rotate-captcha/index.vue'; |
||||
export type * from './types'; |
export type * from './types'; |
||||
|
|||||
@ -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> |
||||
@ -0,0 +1,62 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { CSSProperties } from 'vue'; |
||||
|
import { computed, ref, useTemplateRef } from 'vue'; |
||||
|
|
||||
|
import { Check, ChevronsRight } from '@vben/icons'; |
||||
|
import { Slot } from '@vben-core/shadcn-ui'; |
||||
|
|
||||
|
const props = defineProps<{ |
||||
|
actionStyle: CSSProperties; |
||||
|
isPassing: boolean; |
||||
|
toLeft: boolean; |
||||
|
}>(); |
||||
|
|
||||
|
const actionRef = useTemplateRef<HTMLDivElement>('actionRef'); |
||||
|
|
||||
|
const left = ref('0'); |
||||
|
|
||||
|
const style = computed(() => { |
||||
|
const { actionStyle } = props; |
||||
|
return { |
||||
|
...actionStyle, |
||||
|
left: left.value, |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
const isDragging = computed(() => { |
||||
|
const currentLeft = Number.parseInt(left.value as string); |
||||
|
|
||||
|
return currentLeft > 10 && !props.isPassing; |
||||
|
}); |
||||
|
|
||||
|
defineExpose({ |
||||
|
getEl: () => { |
||||
|
return actionRef.value; |
||||
|
}, |
||||
|
getStyle: () => { |
||||
|
return actionRef?.value?.style; |
||||
|
}, |
||||
|
setLeft: (val: string) => { |
||||
|
left.value = val; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div |
||||
|
ref="actionRef" |
||||
|
:class="{ |
||||
|
'transition-width !left-0 duration-300': toLeft, |
||||
|
'rounded-md': isDragging, |
||||
|
}" |
||||
|
:style="style" |
||||
|
class="bg-background dark:bg-accent absolute left-0 top-0 flex h-full cursor-move items-center justify-center px-3.5 shadow-md" |
||||
|
> |
||||
|
<Slot :is-passing="isPassing" class="text-foreground/60 size-4"> |
||||
|
<slot name="icon"> |
||||
|
<ChevronsRight v-if="!isPassing" /> |
||||
|
<Check v-else /> |
||||
|
</slot> |
||||
|
</Slot> |
||||
|
</div> |
||||
|
</template> |
||||
@ -0,0 +1,38 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { computed, type CSSProperties, ref, useTemplateRef } from 'vue'; |
||||
|
|
||||
|
const props = defineProps<{ |
||||
|
barStyle: CSSProperties; |
||||
|
toLeft: boolean; |
||||
|
}>(); |
||||
|
|
||||
|
const barRef = useTemplateRef<HTMLDivElement>('barRef'); |
||||
|
|
||||
|
const width = ref('0'); |
||||
|
|
||||
|
const style = computed(() => { |
||||
|
const { barStyle } = props; |
||||
|
return { |
||||
|
...barStyle, |
||||
|
width: width.value, |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
defineExpose({ |
||||
|
getEl: () => { |
||||
|
return barRef.value; |
||||
|
}, |
||||
|
setWidth: (val: string) => { |
||||
|
width.value = val; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div |
||||
|
ref="barRef" |
||||
|
:class="toLeft && 'transition-width !w-0 duration-300'" |
||||
|
:style="style" |
||||
|
class="bg-success absolute h-full" |
||||
|
></div> |
||||
|
</template> |
||||
@ -0,0 +1,52 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import type { CSSProperties } from 'vue'; |
||||
|
import { computed, useTemplateRef } from 'vue'; |
||||
|
|
||||
|
import { VbenSpineText } from '@vben-core/shadcn-ui'; |
||||
|
|
||||
|
const props = defineProps<{ |
||||
|
contentStyle: CSSProperties; |
||||
|
isPassing: boolean; |
||||
|
successText: string; |
||||
|
text: string; |
||||
|
}>(); |
||||
|
|
||||
|
const contentRef = useTemplateRef<HTMLDivElement>('contentRef'); |
||||
|
|
||||
|
const style = computed(() => { |
||||
|
const { contentStyle } = props; |
||||
|
|
||||
|
return { |
||||
|
...contentStyle, |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
defineExpose({ |
||||
|
getEl: () => { |
||||
|
return contentRef.value; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div |
||||
|
ref="contentRef" |
||||
|
:class="{ |
||||
|
[$style.success]: isPassing, |
||||
|
}" |
||||
|
:style="style" |
||||
|
class="absolute top-0 flex size-full select-none items-center justify-center text-xs" |
||||
|
> |
||||
|
<slot name="text"> |
||||
|
<VbenSpineText class="flex h-full items-center"> |
||||
|
{{ isPassing ? successText : text }} |
||||
|
</VbenSpineText> |
||||
|
</slot> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<style module> |
||||
|
.success { |
||||
|
-webkit-text-fill-color: hsl(0deg 0% 98%); |
||||
|
} |
||||
|
</style> |
||||
@ -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> |
||||
@ -1,7 +0,0 @@ |
|||||
export const parseValue = (value: number | string) => { |
|
||||
if (typeof value === 'number') { |
|
||||
return value; |
|
||||
} |
|
||||
const parsed = Number.parseFloat(value); |
|
||||
return Number.isNaN(parsed) ? 0 : parsed; |
|
||||
}; |
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,116 @@ |
|||||
|
<script lang="ts" setup> |
||||
|
import type { |
||||
|
CaptchaVerifyPassingData, |
||||
|
SliderCaptchaActionType, |
||||
|
} from '@vben/common-ui'; |
||||
|
|
||||
|
import { ref } from 'vue'; |
||||
|
|
||||
|
import { Page, SliderCaptcha } from '@vben/common-ui'; |
||||
|
import { Bell, Sun } from '@vben/icons'; |
||||
|
|
||||
|
import { Button, Card, message } from 'ant-design-vue'; |
||||
|
|
||||
|
function handleSuccess(data: CaptchaVerifyPassingData) { |
||||
|
const { time } = data; |
||||
|
message.success(`校验成功,耗时${time}秒`); |
||||
|
} |
||||
|
function handleBtnClick(elRef?: SliderCaptchaActionType) { |
||||
|
if (!elRef) { |
||||
|
return; |
||||
|
} |
||||
|
elRef.resume(); |
||||
|
} |
||||
|
|
||||
|
const el1 = ref<SliderCaptchaActionType>(); |
||||
|
const el2 = ref<SliderCaptchaActionType>(); |
||||
|
const el3 = ref<SliderCaptchaActionType>(); |
||||
|
const el4 = ref<SliderCaptchaActionType>(); |
||||
|
const el5 = ref<SliderCaptchaActionType>(); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Page description="用于前端简单的拖动校验场景" title="滑块校验"> |
||||
|
<Card class="mb-5" title="基础示例"> |
||||
|
<div class="flex items-center justify-center p-4 px-[30%]"> |
||||
|
<SliderCaptcha ref="el1" @success="handleSuccess" /> |
||||
|
<Button class="ml-2" type="primary" @click="handleBtnClick(el1)"> |
||||
|
还原 |
||||
|
</Button> |
||||
|
</div> |
||||
|
</Card> |
||||
|
<Card class="mb-5" title="自定义圆角"> |
||||
|
<div class="flex items-center justify-center p-4 px-[30%]"> |
||||
|
<SliderCaptcha |
||||
|
ref="el2" |
||||
|
class="rounded-full" |
||||
|
@success="handleSuccess" |
||||
|
/> |
||||
|
<Button class="ml-2" type="primary" @click="handleBtnClick(el2)"> |
||||
|
还原 |
||||
|
</Button> |
||||
|
</div> |
||||
|
</Card> |
||||
|
<Card class="mb-5" title="自定义背景色"> |
||||
|
<div class="flex items-center justify-center p-4 px-[30%]"> |
||||
|
<SliderCaptcha |
||||
|
ref="el3" |
||||
|
:bar-style="{ |
||||
|
backgroundColor: '#018ffb', |
||||
|
}" |
||||
|
success-text="校验成功" |
||||
|
text="拖动以进行校验" |
||||
|
@success="handleSuccess" |
||||
|
/> |
||||
|
<Button class="ml-2" type="primary" @click="handleBtnClick(el3)"> |
||||
|
还原 |
||||
|
</Button> |
||||
|
</div> |
||||
|
</Card> |
||||
|
<Card class="mb-5" title="自定义拖拽图标"> |
||||
|
<div class="flex items-center justify-center p-4 px-[30%]"> |
||||
|
<SliderCaptcha ref="el4" @success="handleSuccess"> |
||||
|
<template #actionIcon="{ isPassing }"> |
||||
|
<Bell v-if="isPassing" /> |
||||
|
<Sun v-else /> |
||||
|
</template> |
||||
|
</SliderCaptcha> |
||||
|
<Button class="ml-2" type="primary" @click="handleBtnClick(el4)"> |
||||
|
还原 |
||||
|
</Button> |
||||
|
</div> |
||||
|
</Card> |
||||
|
<Card class="mb-5" title="自定义文本"> |
||||
|
<div class="flex items-center justify-center p-4 px-[30%]"> |
||||
|
<SliderCaptcha |
||||
|
ref="el5" |
||||
|
success-text="成功" |
||||
|
text="拖动" |
||||
|
@success="handleSuccess" |
||||
|
/> |
||||
|
<Button class="ml-2" type="primary" @click="handleBtnClick(el5)"> |
||||
|
还原 |
||||
|
</Button> |
||||
|
</div> |
||||
|
</Card> |
||||
|
<Card class="mb-5" title="自定义内容(slot)"> |
||||
|
<div class="flex items-center justify-center p-4 px-[30%]"> |
||||
|
<SliderCaptcha ref="el5" @success="handleSuccess"> |
||||
|
<template #text="{ isPassing }"> |
||||
|
<template v-if="isPassing"> |
||||
|
<Bell class="mr-2 size-4" /> |
||||
|
成功 |
||||
|
</template> |
||||
|
<template v-else> |
||||
|
拖动 |
||||
|
<Sun class="ml-2 size-4" /> |
||||
|
</template> |
||||
|
</template> |
||||
|
</SliderCaptcha> |
||||
|
<Button class="ml-2" type="primary" @click="handleBtnClick(el5)"> |
||||
|
还原 |
||||
|
</Button> |
||||
|
</div> |
||||
|
</Card> |
||||
|
</Page> |
||||
|
</template> |
||||
@ -0,0 +1,28 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import { computed } from 'vue'; |
||||
|
|
||||
|
import { Page, SliderRotateCaptcha } from '@vben/common-ui'; |
||||
|
import { preferences } from '@vben/preferences'; |
||||
|
import { useUserStore } from '@vben/stores'; |
||||
|
|
||||
|
import { Card, message } from 'ant-design-vue'; |
||||
|
|
||||
|
const userStore = useUserStore(); |
||||
|
function handleSuccess() { |
||||
|
message.success('success!'); |
||||
|
} |
||||
|
|
||||
|
const avatar = computed(() => { |
||||
|
return userStore.userInfo?.avatar || preferences.app.defaultAvatar; |
||||
|
}); |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<Page description="用于前端简单的拖动校验场景" title="滑块旋转校验"> |
||||
|
<Card class="mb-5" title="基本示例"> |
||||
|
<div class="flex items-center justify-center p-4"> |
||||
|
<SliderRotateCaptcha :src="avatar" @success="handleSuccess" /> |
||||
|
</div> |
||||
|
</Card> |
||||
|
</Page> |
||||
|
</template> |
||||
Loading…
Reference in new issue