index.vue 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. <script setup lang="ts">
  2. import type { CaptchaPoint, PointSelectionCaptchaProps } from '../types';
  3. import { RotateCw } from '@vben/icons';
  4. import { $t } from '@vben/locales';
  5. import { VbenButton, VbenIconButton } from '@vben-core/shadcn-ui';
  6. import { useCaptchaPoints } from '../hooks/useCaptchaPoints';
  7. import CaptchaCard from './point-selection-captcha-card.vue';
  8. const props = withDefaults(defineProps<PointSelectionCaptchaProps>(), {
  9. height: '220px',
  10. hintImage: '',
  11. hintText: '',
  12. paddingX: '12px',
  13. paddingY: '16px',
  14. showConfirm: false,
  15. title: '',
  16. width: '300px',
  17. });
  18. const emit = defineEmits<{
  19. click: [CaptchaPoint];
  20. confirm: [Array<CaptchaPoint>, clear: () => void];
  21. refresh: [];
  22. }>();
  23. const { addPoint, clearPoints, points } = useCaptchaPoints();
  24. if (!props.hintImage && !props.hintText) {
  25. console.warn('At least one of hint image or hint text must be provided');
  26. }
  27. const POINT_OFFSET = 11;
  28. function getElementPosition(element: HTMLElement) {
  29. const rect = element.getBoundingClientRect();
  30. return {
  31. x: rect.left + window.scrollX,
  32. y: rect.top + window.scrollY,
  33. };
  34. }
  35. function handleClick(e: MouseEvent) {
  36. try {
  37. const dom = e.currentTarget as HTMLElement;
  38. if (!dom) throw new Error('Element not found');
  39. const { x: domX, y: domY } = getElementPosition(dom);
  40. const mouseX = e.clientX + window.scrollX;
  41. const mouseY = e.clientY + window.scrollY;
  42. if (typeof mouseX !== 'number' || typeof mouseY !== 'number') {
  43. throw new TypeError('Mouse coordinates not found');
  44. }
  45. const xPos = mouseX - domX;
  46. const yPos = mouseY - domY;
  47. const rect = dom.getBoundingClientRect();
  48. // 点击位置边界校验
  49. if (xPos < 0 || yPos < 0 || xPos > rect.width || yPos > rect.height) {
  50. console.warn('Click position is out of the valid range');
  51. return;
  52. }
  53. const x = Math.ceil(xPos);
  54. const y = Math.ceil(yPos);
  55. const point = {
  56. i: points.length,
  57. t: Date.now(),
  58. x,
  59. y,
  60. };
  61. addPoint(point);
  62. emit('click', point);
  63. e.stopPropagation();
  64. e.preventDefault();
  65. } catch (error) {
  66. console.error('Error in handleClick:', error);
  67. }
  68. }
  69. function clear() {
  70. try {
  71. clearPoints();
  72. } catch (error) {
  73. console.error('Error in clear:', error);
  74. }
  75. }
  76. function handleRefresh() {
  77. try {
  78. clear();
  79. emit('refresh');
  80. } catch (error) {
  81. console.error('Error in handleRefresh:', error);
  82. }
  83. }
  84. function handleConfirm() {
  85. if (!props.showConfirm) return;
  86. try {
  87. emit('confirm', points, clear);
  88. } catch (error) {
  89. console.error('Error in handleConfirm:', error);
  90. }
  91. }
  92. </script>
  93. <template>
  94. <CaptchaCard
  95. :captcha-image="captchaImage"
  96. :height="height"
  97. :padding-x="paddingX"
  98. :padding-y="paddingY"
  99. :title="title"
  100. :width="width"
  101. @click="handleClick"
  102. >
  103. <template #title>
  104. <slot name="title">{{ $t('ui.captcha.title') }}</slot>
  105. </template>
  106. <template #extra>
  107. <VbenIconButton
  108. :aria-label="$t('ui.captcha.refreshAriaLabel')"
  109. class="ml-1"
  110. @click="handleRefresh"
  111. >
  112. <RotateCw class="size-5" />
  113. </VbenIconButton>
  114. <VbenButton
  115. v-if="showConfirm"
  116. :aria-label="$t('ui.captcha.confirmAriaLabel')"
  117. class="ml-2"
  118. size="sm"
  119. @click="handleConfirm"
  120. >
  121. {{ $t('ui.captcha.confirm') }}
  122. </VbenButton>
  123. </template>
  124. <div
  125. v-for="(point, index) in points"
  126. :key="index"
  127. :aria-label="$t('ui.captcha.pointAriaLabel') + (index + 1)"
  128. :style="{
  129. top: `${point.y - POINT_OFFSET}px`,
  130. left: `${point.x - POINT_OFFSET}px`,
  131. }"
  132. 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"
  133. role="button"
  134. tabindex="0"
  135. >
  136. {{ index + 1 }}
  137. </div>
  138. <template #footer>
  139. <img
  140. v-if="hintImage"
  141. :alt="$t('ui.captcha.alt')"
  142. :src="hintImage"
  143. class="border-border h-10 w-full rounded border"
  144. />
  145. <div
  146. v-else-if="hintText"
  147. class="border-border flex-center h-10 w-full rounded border"
  148. >
  149. {{ `${$t('ui.captcha.clickInOrder')}` + `【${hintText}】` }}
  150. </div>
  151. </template>
  152. </CaptchaCard>
  153. </template>