<script lang="tsx">
import type { Ref } from 'vue';
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(null) as Ref<HTMLDivElement | 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;
background-clip: text;
user-select: none;
&.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;
}
}
}
@keyframes slidetounlock {
0% {
background-position: -120px 0;
}
100% {
background-position: 120px 0;
}
}
</style>