fix: Improve the problem of inaccurate captcha accuracy (#4401)
* feat: captcha example * fix: fix lint errors * chore: event handling and methods * chore: add accessibility features ARIA labels and roles * refactor: refactor code structure and improve captcha demo page * feat: add captcha internationalization * chore: 适配时间戳国际化展示 * fix: 1. 添加点击位置边界校验,防止点击外部导致x,y误差。2. 演示页面宽度过长添加滚动条。3. 添加hooks ---------pull/48/MERGE
parent
b8a4fba78c
commit
38fe6426a2
|
@ -0,0 +1,18 @@
|
||||||
|
import type { CaptchaPoint } from '../types';
|
||||||
|
|
||||||
|
import { reactive } from 'vue';
|
||||||
|
|
||||||
|
export function useCaptchaPoints() {
|
||||||
|
const points = reactive<CaptchaPoint[]>([]);
|
||||||
|
function addPoint(point: CaptchaPoint) {
|
||||||
|
points.push(point);
|
||||||
|
}
|
||||||
|
function clearPoints() {
|
||||||
|
points.splice(0, points.length);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
addPoint,
|
||||||
|
clearPoints,
|
||||||
|
points,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,13 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CaptchaPoint, PointSelectionCaptchaProps } from './types';
|
import type { CaptchaPoint, PointSelectionCaptchaProps } from './types';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
import { RotateCw } from '@vben/icons';
|
import { RotateCw } from '@vben/icons';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { VbenButton, VbenIconButton } from '@vben-core/shadcn-ui';
|
import { VbenButton, VbenIconButton } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import { CaptchaCard } from '.';
|
import { CaptchaCard } from '.';
|
||||||
|
import { useCaptchaPoints } from './hooks/useCaptchaPoints';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<PointSelectionCaptchaProps>(), {
|
const props = withDefaults(defineProps<PointSelectionCaptchaProps>(), {
|
||||||
height: '220px',
|
height: '220px',
|
||||||
|
@ -19,44 +18,24 @@ const props = withDefaults(defineProps<PointSelectionCaptchaProps>(), {
|
||||||
title: '',
|
title: '',
|
||||||
width: '300px',
|
width: '300px',
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
click: [CaptchaPoint];
|
click: [CaptchaPoint];
|
||||||
confirm: [Array<CaptchaPoint>, clear: () => void];
|
confirm: [Array<CaptchaPoint>, clear: () => void];
|
||||||
refresh: [];
|
refresh: [];
|
||||||
}>();
|
}>();
|
||||||
|
const { addPoint, clearPoints, points } = useCaptchaPoints();
|
||||||
|
|
||||||
if (!props.hintImage && !props.hintText) {
|
if (!props.hintImage && !props.hintText) {
|
||||||
throw new Error('At least one of hint image or hint text must be provided');
|
console.warn('At least one of hint image or hint text must be provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
const points = ref<CaptchaPoint[]>([]);
|
|
||||||
const POINT_OFFSET = 11;
|
const POINT_OFFSET = 11;
|
||||||
|
|
||||||
function getElementPosition(element: HTMLElement) {
|
function getElementPosition(element: HTMLElement) {
|
||||||
let posX = 0;
|
const rect = element.getBoundingClientRect();
|
||||||
let posY = 0;
|
|
||||||
if (element.getBoundingClientRect) {
|
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
const doc = document.documentElement;
|
|
||||||
posX =
|
|
||||||
rect.left +
|
|
||||||
Math.max(doc.scrollLeft, document.body.scrollLeft) -
|
|
||||||
doc.clientLeft;
|
|
||||||
posY =
|
|
||||||
rect.top +
|
|
||||||
Math.max(doc.scrollTop, document.body.scrollTop) -
|
|
||||||
doc.clientTop;
|
|
||||||
} else {
|
|
||||||
while (element !== document.body) {
|
|
||||||
posX += element.offsetLeft;
|
|
||||||
posY += element.offsetTop;
|
|
||||||
element = element.offsetParent as HTMLElement;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
x: posX,
|
x: rect.left + window.scrollX,
|
||||||
y: posY,
|
y: rect.top + window.scrollY,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,25 +46,35 @@ function handleClick(e: MouseEvent) {
|
||||||
|
|
||||||
const { x: domX, y: domY } = getElementPosition(dom);
|
const { x: domX, y: domY } = getElementPosition(dom);
|
||||||
|
|
||||||
const mouseX = e.pageX || e.clientX;
|
const mouseX = e.clientX + window.scrollX;
|
||||||
const mouseY = e.pageY || e.clientY;
|
const mouseY = e.clientY + window.scrollY;
|
||||||
|
|
||||||
if (mouseX === undefined || mouseY === undefined)
|
if (typeof mouseX !== 'number' || typeof mouseY !== 'number') {
|
||||||
throw new Error('Mouse coordinates not found');
|
throw new TypeError('Mouse coordinates not found');
|
||||||
|
}
|
||||||
|
|
||||||
const xPos = mouseX - domX;
|
const xPos = mouseX - domX;
|
||||||
const yPos = mouseY - domY;
|
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 x = Math.ceil(xPos);
|
||||||
const y = Math.ceil(yPos);
|
const y = Math.ceil(yPos);
|
||||||
|
|
||||||
const point = {
|
const point = {
|
||||||
i: points.value.length,
|
i: points.length,
|
||||||
t: Date.now(),
|
t: Date.now(),
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
};
|
};
|
||||||
points.value.push(point);
|
|
||||||
|
addPoint(point);
|
||||||
|
|
||||||
emit('click', point);
|
emit('click', point);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -97,7 +86,7 @@ function handleClick(e: MouseEvent) {
|
||||||
|
|
||||||
function clear() {
|
function clear() {
|
||||||
try {
|
try {
|
||||||
points.value = [];
|
clearPoints();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in clear:', error);
|
console.error('Error in clear:', error);
|
||||||
}
|
}
|
||||||
|
@ -115,7 +104,7 @@ function handleRefresh() {
|
||||||
function handleConfirm() {
|
function handleConfirm() {
|
||||||
if (!props.showConfirm) return;
|
if (!props.showConfirm) return;
|
||||||
try {
|
try {
|
||||||
emit('confirm', points.value, clear);
|
emit('confirm', points, clear);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in handleConfirm:', error);
|
console.error('Error in handleConfirm:', error);
|
||||||
}
|
}
|
||||||
|
@ -164,6 +153,7 @@ function handleConfirm() {
|
||||||
}"
|
}"
|
||||||
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"
|
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"
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -46,7 +46,10 @@ const handleClick = (point: CaptchaPoint) => {
|
||||||
:description="$t('page.examples.captcha.pageDescription')"
|
:description="$t('page.examples.captcha.pageDescription')"
|
||||||
:title="$t('page.examples.captcha.pageTitle')"
|
:title="$t('page.examples.captcha.pageTitle')"
|
||||||
>
|
>
|
||||||
<Card :title="$t('page.examples.captcha.basic')" class="mb-4">
|
<Card
|
||||||
|
:title="$t('page.examples.captcha.basic')"
|
||||||
|
class="mb-4 overflow-x-auto"
|
||||||
|
>
|
||||||
<div class="mb-3 flex items-center justify-start">
|
<div class="mb-3 flex items-center justify-start">
|
||||||
<Input
|
<Input
|
||||||
v-model:value="params.title"
|
v-model:value="params.title"
|
||||||
|
|
Loading…
Reference in New Issue