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,175 @@
|
||||
<script setup lang="ts">
|
||||
import type { CaptchaPoint, PointSelectionCaptchaProps } from '../types';
|
||||
|
||||
import { RotateCw } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { VbenButton, VbenIconButton } from '@vben-core/shadcn-ui';
|
||||
|
||||
import { useCaptchaPoints } from '../hooks/useCaptchaPoints';
|
||||
import CaptchaCard from './point-selection-captcha-card.vue';
|
||||
|
||||
const props = withDefaults(defineProps<PointSelectionCaptchaProps>(), {
|
||||
height: '220px',
|
||||
hintImage: '',
|
||||
hintText: '',
|
||||
paddingX: '12px',
|
||||
paddingY: '16px',
|
||||
showConfirm: false,
|
||||
title: '',
|
||||
width: '300px',
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
click: [CaptchaPoint];
|
||||
confirm: [Array<CaptchaPoint>, clear: () => void];
|
||||
refresh: [];
|
||||
}>();
|
||||
const { addPoint, clearPoints, points } = useCaptchaPoints();
|
||||
|
||||
if (!props.hintImage && !props.hintText) {
|
||||
console.warn('At least one of hint image or hint text must be provided');
|
||||
}
|
||||
|
||||
const POINT_OFFSET = 11;
|
||||
|
||||
function getElementPosition(element: HTMLElement) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
x: rect.left + window.scrollX,
|
||||
y: rect.top + window.scrollY,
|
||||
};
|
||||
}
|
||||
|
||||
function handleClick(e: MouseEvent) {
|
||||
try {
|
||||
const dom = e.currentTarget as HTMLElement;
|
||||
if (!dom) throw new Error('Element not found');
|
||||
|
||||
const { x: domX, y: domY } = getElementPosition(dom);
|
||||
|
||||
const mouseX = e.clientX + window.scrollX;
|
||||
const mouseY = e.clientY + window.scrollY;
|
||||
|
||||
if (typeof mouseX !== 'number' || typeof mouseY !== 'number') {
|
||||
throw new TypeError('Mouse coordinates not found');
|
||||
}
|
||||
|
||||
const xPos = mouseX - domX;
|
||||
const yPos = mouseY - domY;
|
||||
|
||||
const rect = dom.getBoundingClientRect();
|
||||
|
||||
// 点击位置边界校验
|
||||
if (xPos < 0 || yPos < 0 || xPos > rect.width || yPos > rect.height) {
|
||||
console.warn('Click position is out of the valid range');
|
||||
return;
|
||||
}
|
||||
|
||||
const x = Math.ceil(xPos);
|
||||
const y = Math.ceil(yPos);
|
||||
|
||||
const point = {
|
||||
i: points.length,
|
||||
t: Date.now(),
|
||||
x,
|
||||
y,
|
||||
};
|
||||
|
||||
addPoint(point);
|
||||
|
||||
emit('click', point);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
} catch (error) {
|
||||
console.error('Error in handleClick:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
try {
|
||||
clearPoints();
|
||||
} catch (error) {
|
||||
console.error('Error in clear:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
try {
|
||||
clear();
|
||||
emit('refresh');
|
||||
} catch (error) {
|
||||
console.error('Error in handleRefresh:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
if (!props.showConfirm) return;
|
||||
try {
|
||||
emit('confirm', points, clear);
|
||||
} catch (error) {
|
||||
console.error('Error in handleConfirm:', error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<CaptchaCard
|
||||
:captcha-image="captchaImage"
|
||||
:height="height"
|
||||
:padding-x="paddingX"
|
||||
:padding-y="paddingY"
|
||||
:title="title"
|
||||
:width="width"
|
||||
@click="handleClick"
|
||||
>
|
||||
<template #title>
|
||||
<slot name="title">{{ $t('ui.captcha.title') }}</slot>
|
||||
</template>
|
||||
|
||||
<template #extra>
|
||||
<VbenIconButton
|
||||
:aria-label="$t('ui.captcha.refreshAriaLabel')"
|
||||
class="ml-1"
|
||||
@click="handleRefresh"
|
||||
>
|
||||
<RotateCw class="size-5" />
|
||||
</VbenIconButton>
|
||||
<VbenButton
|
||||
v-if="showConfirm"
|
||||
:aria-label="$t('ui.captcha.confirmAriaLabel')"
|
||||
class="ml-2"
|
||||
size="sm"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ $t('ui.captcha.confirm') }}
|
||||
</VbenButton>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-for="(point, index) in points"
|
||||
:key="index"
|
||||
:aria-label="$t('ui.captcha.pointAriaLabel') + (index + 1)"
|
||||
:style="{
|
||||
top: `${point.y - POINT_OFFSET}px`,
|
||||
left: `${point.x - POINT_OFFSET}px`,
|
||||
}"
|
||||
class="bg-primary text-primary-50 border-primary-50 absolute z-20 flex h-5 w-5 cursor-default items-center justify-center rounded-full border-2"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<template #footer>
|
||||
<img
|
||||
v-if="hintImage"
|
||||
:alt="$t('ui.captcha.alt')"
|
||||
:src="hintImage"
|
||||
class="h-10 w-full rounded border border-solid border-slate-200"
|
||||
/>
|
||||
<div
|
||||
v-else-if="hintText"
|
||||
class="flex h-10 w-full items-center justify-center rounded border border-solid border-slate-200"
|
||||
>
|
||||
{{ `${$t('ui.captcha.clickInOrder')}` + `【${hintText}】` }}
|
||||
</div>
|
||||
</template>
|
||||
</CaptchaCard>
|
||||
</template>
|
Reference in New Issue
Block a user