DragVerify.vue 10.4 KB
<script lang="tsx">
  import { defineComponent, ref, computed, unref, reactive, watch, watchEffect } from 'vue';
  import { useTimeoutFn } from '/@/hooks/core/useTimeout';
  import { useEventListener } from '/@/hooks/event/useEventListener';
  import { basicProps } from './props';
  import { getSlot } from '/@/utils/helper/tsxHelper';
  import { CheckOutlined, DoubleRightOutlined } from '@ant-design/icons-vue';

  export default defineComponent({
    name: 'BaseDargVerify',
    props: basicProps,
    emits: ['success', 'update:value', 'change', 'start', 'move', 'end'],
    setup(props, { emit, slots, expose }) {
      const state = reactive({
        isMoving: false,
        isPassing: false,
        moveDistance: 0,
        toLeft: false,
        startTime: 0,
        endTime: 0,
      });

      const wrapElRef = ref<HTMLDivElement | null>(null);
      const barElRef = ref<HTMLDivElement | null>(null);
      const contentElRef = ref<HTMLDivElement | null>(null);
      const actionElRef = ref<HTMLDivElement | null>(null);

      useEventListener({
        el: document,
        name: 'mouseup',
        listener: () => {
          if (state.isMoving) {
            resume();
          }
        },
      });

      const getActionStyleRef = computed(() => {
        const { height, actionStyle } = props;
        const h = `${parseInt(height as string)}px`;
        return {
          left: 0,
          width: h,
          height: h,
          ...actionStyle,
        };
      });

      const getWrapStyleRef = computed(() => {
        const { height, width, circle, wrapStyle } = props;
        const h = parseInt(height as string);
        const w = `${parseInt(width as string)}px`;
        return {
          width: w,
          height: `${h}px`,
          lineHeight: `${h}px`,
          borderRadius: circle ? h / 2 + 'px' : 0,
          ...wrapStyle,
        };
      });

      const getBarStyleRef = computed(() => {
        const { height, circle, barStyle } = props;
        const h = parseInt(height as string);
        return {
          height: `${h}px`,
          borderRadius: circle ? h / 2 + 'px 0 0 ' + h / 2 + 'px' : 0,
          ...barStyle,
        };
      });

      const getContentStyleRef = computed(() => {
        const { height, width, contentStyle } = props;
        const h = `${parseInt(height as string)}px`;
        const w = `${parseInt(width as string)}px`;

        return {
          height: h,
          width: w,
          ...contentStyle,
        };
      });

      watch(
        () => state.isPassing,
        (isPassing) => {
          if (isPassing) {
            const { startTime, endTime } = state;
            const time = (endTime - startTime) / 1000;
            emit('success', { isPassing, time: time.toFixed(1) });
            emit('update:value', isPassing);
            emit('change', isPassing);
          }
        }
      );

      watchEffect(() => {
        state.isPassing = !!props.value;
      });

      function getEventPageX(e: MouseEvent | TouchEvent) {
        return (e as MouseEvent).pageX || (e as TouchEvent).touches[0].pageX;
      }

      function handleDragStart(e: MouseEvent | TouchEvent) {
        if (state.isPassing) {
          return;
        }
        const actionEl = unref(actionElRef);
        if (!actionEl) return;
        emit('start', e);
        state.moveDistance = getEventPageX(e) - parseInt(actionEl.style.left.replace('px', ''), 10);
        state.startTime = new Date().getTime();
        state.isMoving = true;
      }

      function getOffset(el: HTMLDivElement) {
        const actionWidth = parseInt(el.style.width);
        const { width } = props;
        const widthNum = parseInt(width as string);
        const offset = widthNum - actionWidth - 6;
        return { offset, widthNum, actionWidth };
      }

      function handleDragMoving(e: MouseEvent | TouchEvent) {
        const { isMoving, moveDistance } = state;
        if (isMoving) {
          const actionEl = unref(actionElRef);
          const barEl = unref(barElRef);
          if (!actionEl || !barEl) return;
          const { offset, widthNum, actionWidth } = getOffset(actionEl);
          const moveX = getEventPageX(e) - moveDistance;

          emit('move', {
            event: e,
            moveDistance,
            moveX,
          });
          if (moveX > 0 && moveX <= offset) {
            actionEl.style.left = `${moveX}px`;
            barEl.style.width = `${moveX + actionWidth / 2}px`;
          } else if (moveX > offset) {
            actionEl.style.left = `${widthNum - actionWidth}px`;
            barEl.style.width = `${widthNum - actionWidth / 2}px`;
            if (!props.isSlot) {
              checkPass();
            }
          }
        }
      }

      function handleDragOver(e: MouseEvent | TouchEvent) {
        const { isMoving, isPassing, moveDistance } = state;
        if (isMoving && !isPassing) {
          emit('end', e);
          const actionEl = unref(actionElRef);
          const barEl = unref(barElRef);
          if (!actionEl || !barEl) return;
          const moveX = getEventPageX(e) - moveDistance;
          const { offset, widthNum, actionWidth } = getOffset(actionEl);
          if (moveX < offset) {
            if (!props.isSlot) {
              resume();
            } else {
              setTimeout(() => {
                if (!props.value) {
                  resume();
                } else {
                  const contentEl = unref(contentElRef);
                  if (contentEl) {
                    contentEl.style.width = `${parseInt(barEl.style.width)}px`;
                  }
                }
              }, 0);
            }
          } else {
            actionEl.style.left = `${widthNum - actionWidth}px`;
            barEl.style.width = `${widthNum - actionWidth / 2}px`;
            checkPass();
          }
          state.isMoving = false;
        }
      }

      function checkPass() {
        if (props.isSlot) {
          resume();
          return;
        }
        state.endTime = new Date().getTime();
        state.isPassing = true;
        state.isMoving = false;
      }

      function resume() {
        state.isMoving = false;
        state.isPassing = false;
        state.moveDistance = 0;
        state.toLeft = false;
        state.startTime = 0;
        state.endTime = 0;
        const actionEl = unref(actionElRef);
        const barEl = unref(barElRef);
        const contentEl = unref(contentElRef);
        if (!actionEl || !barEl || !contentEl) return;
        state.toLeft = true;
        useTimeoutFn(() => {
          state.toLeft = false;
          actionEl.style.left = '0';
          barEl.style.width = '0';
          //  The time is consistent with the animation time
        }, 300);
        contentEl.style.width = unref(getContentStyleRef).width;
      }

      expose({
        resume,
      });

      return () => {
        const renderBar = () => {
          const cls = [`darg-verify-bar`];
          if (state.toLeft) {
            cls.push('to-left');
          }
          return <div class={cls} ref={barElRef} style={unref(getBarStyleRef)} />;
        };

        const renderContent = () => {
          const cls = [`darg-verify-content`];
          const { isPassing } = state;
          const { text, successText } = props;

          isPassing && cls.push('success');

          return (
            <div class={cls} ref={contentElRef} style={unref(getContentStyleRef)}>
              {getSlot(slots, 'text', isPassing) || (isPassing ? successText : text)}
            </div>
          );
        };

        const renderAction = () => {
          const cls = [`darg-verify-action`];
          const { toLeft, isPassing } = state;
          if (toLeft) {
            cls.push('to-left');
          }
          return (
            <div
              class={cls}
              onMousedown={handleDragStart}
              onTouchstart={handleDragStart}
              style={unref(getActionStyleRef)}
              ref={actionElRef}
            >
              {getSlot(slots, 'actionIcon', isPassing) ||
                (isPassing ? (
                  <CheckOutlined class={`darg-verify-action__icon`} />
                ) : (
                  <DoubleRightOutlined class={`darg-verify-action__icon`} />
                ))}
            </div>
          );
        };

        return (
          <div
            class="darg-verify"
            ref={wrapElRef}
            style={unref(getWrapStyleRef)}
            onMousemove={handleDragMoving}
            onTouchmove={handleDragMoving}
            onMouseleave={handleDragOver}
            onMouseup={handleDragOver}
            onTouchend={handleDragOver}
          >
            {renderBar()}
            {renderContent()}
            {renderAction()}
          </div>
        );
      };
    },
  });
</script>
<style lang="less">
  @radius: 4px;

  .darg-verify {
    position: relative;
    overflow: hidden;
    text-align: center;
    background-color: rgb(238, 238, 238);
    border: 1px solid #ddd;
    border-radius: @radius;

    &-bar {
      position: absolute;
      width: 0;
      height: 36px;
      background-color: @success-color;
      border-radius: @radius;

      &.to-left {
        width: 0 !important;
        transition: width 0.3s;
      }
    }

    &-content {
      position: absolute;
      top: 0;
      font-size: 12px;
      -webkit-text-size-adjust: none;
      background-color: -webkit-gradient(
        linear,
        left top,
        right top,
        color-stop(0, #333),
        color-stop(0.4, #333),
        color-stop(0.5, #fff),
        color-stop(0.6, #333),
        color-stop(1, #333)
      );
      animation: slidetounlock 3s infinite;
      -webkit-background-clip: text;
      -moz-user-select: none;
      -webkit-user-select: none;
      -o-user-select: none;
      -ms-user-select: none;
      user-select: none;
      -webkit-text-fill-color: transparent;

      &.success {
        -webkit-text-fill-color: @white;
      }

      & > * {
        -webkit-text-fill-color: #333;
      }
    }

    &-action {
      position: absolute;
      top: 0;
      left: 0;
      display: flex;
      cursor: move;
      background-color: @white;
      border-radius: @radius;
      justify-content: center;
      align-items: center;

      &__icon {
        cursor: inherit;
      }

      &.to-left {
        left: 0 !important;
        transition: left 0.3s;
      }
    }
  }

  @-webkit-keyframes slidetounlock {
    0% {
      background-position: -120px 0;
    }

    100% {
      background-position: 120px 0;
    }
  }
</style>