index.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. <script setup lang="ts">
  2. import type {
  3. CaptchaVerifyPassingData,
  4. SliderCaptchaProps,
  5. SliderRotateVerifyPassingData,
  6. } from '../types';
  7. import { reactive, unref, useTemplateRef, watch, watchEffect } from 'vue';
  8. import { $t } from '@vben/locales';
  9. import { cn } from '@vben-core/shared/utils';
  10. import { useTimeoutFn } from '@vueuse/core';
  11. import SliderCaptchaAction from './slider-captcha-action.vue';
  12. import SliderCaptchaBar from './slider-captcha-bar.vue';
  13. import SliderCaptchaContent from './slider-captcha-content.vue';
  14. const props = withDefaults(defineProps<SliderCaptchaProps>(), {
  15. actionStyle: () => ({}),
  16. barStyle: () => ({}),
  17. contentStyle: () => ({}),
  18. isSlot: false,
  19. successText: '',
  20. text: '',
  21. wrapperStyle: () => ({}),
  22. });
  23. const emit = defineEmits<{
  24. end: [MouseEvent | TouchEvent];
  25. move: [SliderRotateVerifyPassingData];
  26. start: [MouseEvent | TouchEvent];
  27. success: [CaptchaVerifyPassingData];
  28. }>();
  29. const modelValue = defineModel<boolean>({ default: false });
  30. const state = reactive({
  31. endTime: 0,
  32. isMoving: false,
  33. isPassing: false,
  34. moveDistance: 0,
  35. startTime: 0,
  36. toLeft: false,
  37. });
  38. defineExpose({
  39. resume,
  40. });
  41. const wrapperRef = useTemplateRef<HTMLDivElement>('wrapperRef');
  42. const barRef = useTemplateRef<typeof SliderCaptchaBar>('barRef');
  43. const contentRef = useTemplateRef<typeof SliderCaptchaContent>('contentRef');
  44. const actionRef = useTemplateRef<typeof SliderCaptchaAction>('actionRef');
  45. watch(
  46. () => state.isPassing,
  47. (isPassing) => {
  48. if (isPassing) {
  49. const { endTime, startTime } = state;
  50. const time = (endTime - startTime) / 1000;
  51. emit('success', { isPassing, time: time.toFixed(1) });
  52. modelValue.value = isPassing;
  53. }
  54. },
  55. );
  56. watchEffect(() => {
  57. state.isPassing = !!modelValue.value;
  58. });
  59. function getEventPageX(e: MouseEvent | TouchEvent): number {
  60. if ('pageX' in e) {
  61. return e.pageX;
  62. } else if ('touches' in e && e.touches[0]) {
  63. return e.touches[0].pageX;
  64. }
  65. return 0;
  66. }
  67. function handleDragStart(e: MouseEvent | TouchEvent) {
  68. if (state.isPassing) {
  69. return;
  70. }
  71. if (!actionRef.value) return;
  72. emit('start', e);
  73. state.moveDistance =
  74. getEventPageX(e) -
  75. Number.parseInt(
  76. actionRef.value.getStyle().left.replace('px', '') || '0',
  77. 10,
  78. );
  79. state.startTime = Date.now();
  80. state.isMoving = true;
  81. }
  82. function getOffset(actionEl: HTMLDivElement) {
  83. const wrapperWidth = wrapperRef.value?.offsetWidth ?? 220;
  84. const actionWidth = actionEl?.offsetWidth ?? 40;
  85. const offset = wrapperWidth - actionWidth - 6;
  86. return { actionWidth, offset, wrapperWidth };
  87. }
  88. function handleDragMoving(e: MouseEvent | TouchEvent) {
  89. const { isMoving, moveDistance } = state;
  90. if (isMoving) {
  91. const actionEl = unref(actionRef);
  92. const barEl = unref(barRef);
  93. if (!actionEl || !barEl) return;
  94. const { actionWidth, offset, wrapperWidth } = getOffset(actionEl.getEl());
  95. const moveX = getEventPageX(e) - moveDistance;
  96. emit('move', {
  97. event: e,
  98. moveDistance,
  99. moveX,
  100. });
  101. if (moveX > 0 && moveX <= offset) {
  102. actionEl.setLeft(`${moveX}px`);
  103. barEl.setWidth(`${moveX + actionWidth / 2}px`);
  104. } else if (moveX > offset) {
  105. actionEl.setLeft(`${wrapperWidth - actionWidth}px`);
  106. barEl.setWidth(`${wrapperWidth - actionWidth / 2}px`);
  107. if (!props.isSlot) {
  108. checkPass();
  109. }
  110. }
  111. }
  112. }
  113. function handleDragOver(e: MouseEvent | TouchEvent) {
  114. const { isMoving, isPassing, moveDistance } = state;
  115. if (isMoving && !isPassing) {
  116. emit('end', e);
  117. const actionEl = actionRef.value;
  118. const barEl = unref(barRef);
  119. if (!actionEl || !barEl) return;
  120. const moveX = getEventPageX(e) - moveDistance;
  121. const { actionWidth, offset, wrapperWidth } = getOffset(actionEl.getEl());
  122. if (moveX < offset) {
  123. if (props.isSlot) {
  124. setTimeout(() => {
  125. if (modelValue.value) {
  126. const contentEl = unref(contentRef);
  127. if (contentEl) {
  128. contentEl.getEl().style.width = `${Number.parseInt(barEl.getEl().style.width)}px`;
  129. }
  130. } else {
  131. resume();
  132. }
  133. }, 0);
  134. } else {
  135. resume();
  136. }
  137. } else {
  138. actionEl.setLeft(`${wrapperWidth - actionWidth + 10}px`);
  139. barEl.setWidth(`${wrapperWidth - actionWidth / 2}px`);
  140. checkPass();
  141. }
  142. state.isMoving = false;
  143. }
  144. }
  145. function checkPass() {
  146. if (props.isSlot) {
  147. resume();
  148. return;
  149. }
  150. state.endTime = Date.now();
  151. state.isPassing = true;
  152. state.isMoving = false;
  153. }
  154. function resume() {
  155. state.isMoving = false;
  156. state.isPassing = false;
  157. state.moveDistance = 0;
  158. state.toLeft = false;
  159. state.startTime = 0;
  160. state.endTime = 0;
  161. const actionEl = unref(actionRef);
  162. const barEl = unref(barRef);
  163. const contentEl = unref(contentRef);
  164. if (!actionEl || !barEl || !contentEl) return;
  165. contentEl.getEl().style.width = '100%';
  166. state.toLeft = true;
  167. useTimeoutFn(() => {
  168. state.toLeft = false;
  169. actionEl.setLeft('0');
  170. barEl.setWidth('0');
  171. }, 300);
  172. }
  173. </script>
  174. <template>
  175. <div
  176. ref="wrapperRef"
  177. :class="
  178. cn(
  179. 'border-border bg-background-deep relative flex h-10 w-full items-center overflow-hidden rounded-md border text-center',
  180. props.class,
  181. )
  182. "
  183. :style="wrapperStyle"
  184. @mouseleave="handleDragOver"
  185. @mousemove="handleDragMoving"
  186. @mouseup="handleDragOver"
  187. @touchend="handleDragOver"
  188. @touchmove="handleDragMoving"
  189. >
  190. <SliderCaptchaBar
  191. ref="barRef"
  192. :bar-style="barStyle"
  193. :to-left="state.toLeft"
  194. />
  195. <SliderCaptchaContent
  196. ref="contentRef"
  197. :content-style="contentStyle"
  198. :is-passing="state.isPassing"
  199. :success-text="successText || $t('ui.captcha.sliderSuccessText')"
  200. :text="text || $t('ui.captcha.sliderDefaultText')"
  201. >
  202. <template v-if="$slots.text" #text>
  203. <slot :is-passing="state.isPassing" name="text"></slot>
  204. </template>
  205. </SliderCaptchaContent>
  206. <SliderCaptchaAction
  207. ref="actionRef"
  208. :action-style="actionStyle"
  209. :is-passing="state.isPassing"
  210. :to-left="state.toLeft"
  211. @mousedown="handleDragStart"
  212. @touchstart="handleDragStart"
  213. >
  214. <template v-if="$slots.actionIcon" #icon>
  215. <slot :is-passing="state.isPassing" name="actionIcon"></slot>
  216. </template>
  217. </SliderCaptchaAction>
  218. </div>
  219. </template>