Browse Source

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

---------
Squall2017 6 months ago
parent
commit
38fe6426a2

+ 18 - 0
packages/effects/common-ui/src/components/captcha/hooks/useCaptchaPoints.ts

@@ -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,
+  };
+}

+ 25 - 35
packages/effects/common-ui/src/components/captcha/point-selection-captcha.vue

@@ -1,13 +1,12 @@
 <script setup lang="ts">
 import type { CaptchaPoint, PointSelectionCaptchaProps } from './types';
 
-import { ref } from 'vue';
-
 import { RotateCw } from '@vben/icons';
 import { $t } from '@vben/locales';
 import { VbenButton, VbenIconButton } from '@vben-core/shadcn-ui';
 
 import { CaptchaCard } from '.';
+import { useCaptchaPoints } from './hooks/useCaptchaPoints';
 
 const props = withDefaults(defineProps<PointSelectionCaptchaProps>(), {
   height: '220px',
@@ -19,44 +18,24 @@ const props = withDefaults(defineProps<PointSelectionCaptchaProps>(), {
   title: '',
   width: '300px',
 });
-
 const emit = defineEmits<{
   click: [CaptchaPoint];
   confirm: [Array<CaptchaPoint>, clear: () => void];
   refresh: [];
 }>();
+const { addPoint, clearPoints, points } = useCaptchaPoints();
 
 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;
 
 function getElementPosition(element: HTMLElement) {
-  let posX = 0;
-  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;
-    }
-  }
+  const rect = element.getBoundingClientRect();
   return {
-    x: posX,
-    y: posY,
+    x: rect.left + window.scrollX,
+    y: rect.top + window.scrollY,
   };
 }
 
@@ -67,25 +46,35 @@ function handleClick(e: MouseEvent) {
 
     const { x: domX, y: domY } = getElementPosition(dom);
 
-    const mouseX = e.pageX || e.clientX;
-    const mouseY = e.pageY || e.clientY;
+    const mouseX = e.clientX + window.scrollX;
+    const mouseY = e.clientY + window.scrollY;
 
-    if (mouseX === undefined || mouseY === undefined)
-      throw new Error('Mouse coordinates not found');
+    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.value.length,
+      i: points.length,
       t: Date.now(),
       x,
       y,
     };
-    points.value.push(point);
+
+    addPoint(point);
 
     emit('click', point);
     e.stopPropagation();
@@ -97,7 +86,7 @@ function handleClick(e: MouseEvent) {
 
 function clear() {
   try {
-    points.value = [];
+    clearPoints();
   } catch (error) {
     console.error('Error in clear:', error);
   }
@@ -115,7 +104,7 @@ function handleRefresh() {
 function handleConfirm() {
   if (!props.showConfirm) return;
   try {
-    emit('confirm', points.value, clear);
+    emit('confirm', points, clear);
   } catch (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"
       role="button"
+      tabindex="0"
     >
       {{ index + 1 }}
     </div>

+ 4 - 1
playground/src/views/examples/captcha/index.vue

@@ -46,7 +46,10 @@ const handleClick = (point: CaptchaPoint) => {
     :description="$t('page.examples.captcha.pageDescription')"
     :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">
         <Input
           v-model:value="params.title"