|
@@ -0,0 +1,375 @@
|
|
|
+<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>
|