2025-03-12 03:13:46 +00:00
|
|
|
<template>
|
2025-03-12 06:33:33 +00:00
|
|
|
<div class="m-auto bg-white p-15px rd-10px touch-none select-none">
|
|
|
|
<div class="relative w-full overflow-hidden bg-#f8f8f8 rd-10px" :style="{ width: `${options?.canvasWidth}px`, height: `${options?.canvasHeight}px` }">
|
2025-03-12 03:13:46 +00:00
|
|
|
<!-- 背景图 -->
|
|
|
|
<img
|
|
|
|
:src="options?.canvasSrc"
|
2025-03-12 06:33:33 +00:00
|
|
|
class="pointer-events-none w-full h-full"
|
|
|
|
|
2025-03-12 03:13:46 +00:00
|
|
|
ref="bgImage"
|
|
|
|
@load="onImageLoad"
|
|
|
|
@error="onImageError"
|
|
|
|
>
|
|
|
|
<!-- 滑块 -->
|
|
|
|
<img
|
|
|
|
:src="options?.blockSrc"
|
|
|
|
class="absolute cursor-pointer will-change-transform transform-gpu"
|
|
|
|
|
|
|
|
:class="{ 'transition-all duration-300 ease-out': !isDragging }"
|
|
|
|
:style="{
|
|
|
|
top: `${options?.blockY}px`,
|
|
|
|
left: `${moveX}px`,
|
|
|
|
visibility: loaded ? 'visible' : 'hidden',
|
2025-03-12 06:33:33 +00:00
|
|
|
width: `${options?.blockWidth}px`, height: `${options?.blockHeight}px`
|
2025-03-12 03:13:46 +00:00
|
|
|
}"
|
|
|
|
>
|
|
|
|
<transition name="fade-slide">
|
|
|
|
<div
|
|
|
|
v-if="verifyStatus.show"
|
|
|
|
class="absolute left-0 bottom-0 w-full h-24px leading-24px text-center text-14px text-white"
|
|
|
|
:class="verifyStatus.type === 'success' ? 'bg-#52c41a' : 'bg-#ff4d4f'"
|
|
|
|
>
|
|
|
|
{{ verifyStatus.message }}
|
|
|
|
</div>
|
|
|
|
</transition>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 滑动条 -->
|
|
|
|
<div class="relative mt-15px h-40px">
|
|
|
|
<div class="relative h-40px bg-#f5f5f5 rd-20px">
|
|
|
|
<div
|
|
|
|
class="absolute h-full bg-#91d5ff rd-20px"
|
|
|
|
:class="{ 'transition-all duration-300 ease-out': !isDragging }"
|
|
|
|
:style="{ width: `${moveX}px` }"
|
|
|
|
></div>
|
|
|
|
<div
|
|
|
|
class="absolute top-0 w-40px h-40px bg-white rd-full shadow-[0_2px_6px_rgba(0,0,0,0.15)] cursor-pointer will-change-transform"
|
|
|
|
:class="{ 'transition-all duration-300 ease-out': !isDragging }"
|
|
|
|
:style="{ left: `${moveX}px` }"
|
|
|
|
@mousedown.prevent="startDrag"
|
|
|
|
@touchstart.prevent="startDrag"
|
|
|
|
>
|
|
|
|
<div
|
|
|
|
class="absolute top-50% left-50% translate--50% w-20px h-20px bg-#1890ff rd-full"
|
|
|
|
:class="{ 'animate-loading': isVerifying }"
|
|
|
|
></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
|
|
|
|
|
|
|
// Props
|
|
|
|
const props = defineProps({
|
|
|
|
options:Object
|
|
|
|
})
|
|
|
|
|
|
|
|
const emit = defineEmits(['leave'])
|
|
|
|
|
|
|
|
// 状态管理
|
|
|
|
const moveX = ref(0)
|
|
|
|
|
|
|
|
const loaded = ref(false)
|
|
|
|
const isDragging = ref(false)
|
|
|
|
const isVerifying = ref(false)
|
|
|
|
const maxMoveX = ref(0)
|
|
|
|
const bgImage = ref(null)
|
|
|
|
|
|
|
|
const verifyStatus = reactive({
|
|
|
|
show: false,
|
|
|
|
type: '',
|
|
|
|
message: ''
|
|
|
|
})
|
|
|
|
|
|
|
|
const dragState = reactive({
|
|
|
|
startX: 0,
|
|
|
|
oldMoveX: 0
|
|
|
|
})
|
|
|
|
|
|
|
|
// 图片加载处理
|
|
|
|
const onImageLoad = () => {
|
|
|
|
if (!bgImage.value?.complete) return
|
|
|
|
|
|
|
|
const img = bgImage.value
|
|
|
|
const scale = img.width / img.naturalWidth
|
|
|
|
const blockSize = Math.round(50 * scale)
|
|
|
|
maxMoveX.value = img.width - blockSize
|
|
|
|
loaded.value = true
|
|
|
|
}
|
|
|
|
|
|
|
|
const onImageError = () => {
|
|
|
|
console.error('Image failed to load')
|
|
|
|
maxMoveX.value = 270
|
|
|
|
loaded.value = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// 拖动处理
|
|
|
|
const startDrag = (e) => {
|
|
|
|
isDragging.value = true
|
|
|
|
dragState.startX = e.touches?.[0].clientX ?? e.clientX
|
|
|
|
dragState.oldMoveX = moveX.value
|
|
|
|
}
|
|
|
|
|
|
|
|
const onDrag = (e) => {
|
|
|
|
if (!isDragging.value) return
|
|
|
|
|
|
|
|
const clientX = e.touches?.[0].clientX ?? e.clientX
|
|
|
|
let newMoveX = dragState.oldMoveX + (clientX - dragState.startX)
|
|
|
|
|
|
|
|
moveX.value = Math.max(0, Math.min(newMoveX, maxMoveX.value))
|
|
|
|
}
|
|
|
|
|
|
|
|
const endDrag = async () => {
|
|
|
|
if (!isDragging.value) return
|
|
|
|
|
|
|
|
isDragging.value = false
|
|
|
|
isVerifying.value = true
|
|
|
|
|
|
|
|
try {
|
|
|
|
emit('leave', moveX.value, (success) => {
|
|
|
|
showVerifyResult(success)
|
|
|
|
})
|
|
|
|
} catch (error) {
|
|
|
|
showVerifyResult(false)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 验证结果展示
|
|
|
|
const showVerifyResult = (success) => {
|
|
|
|
verifyStatus.show = true
|
|
|
|
verifyStatus.type = success ? 'success' : 'error'
|
|
|
|
verifyStatus.message = success ? '验证成功' : '验证失败'
|
|
|
|
isVerifying.value = false
|
|
|
|
|
|
|
|
if (!success) moveX.value = 0
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
verifyStatus.show = false
|
|
|
|
verifyStatus.message = ''
|
|
|
|
moveX.value = 0
|
|
|
|
}, 1500)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 事件监听
|
|
|
|
onMounted(() => {
|
|
|
|
window.addEventListener('mousemove', onDrag)
|
|
|
|
window.addEventListener('mouseup', endDrag)
|
|
|
|
window.addEventListener('touchmove', onDrag)
|
|
|
|
window.addEventListener('touchend', endDrag)
|
|
|
|
})
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
window.removeEventListener('mousemove', onDrag)
|
|
|
|
window.removeEventListener('mouseup', endDrag)
|
|
|
|
window.removeEventListener('touchmove', onDrag)
|
|
|
|
window.removeEventListener('touchend', endDrag)
|
|
|
|
})
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style>
|
|
|
|
@keyframes loading {
|
|
|
|
from { transform: translate(-50%, -50%) rotate(0deg); }
|
|
|
|
to { transform: translate(-50%, -50%) rotate(360deg); }
|
|
|
|
}
|
|
|
|
|
|
|
|
.animate-loading {
|
|
|
|
animation: loading 1s linear infinite;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* 添加过渡动画样式 */
|
|
|
|
.fade-slide-enter-active,
|
|
|
|
.fade-slide-leave-active {
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
}
|
|
|
|
|
|
|
|
.fade-slide-enter-from,
|
|
|
|
.fade-slide-leave-to {
|
|
|
|
transform: translateY(100%);
|
|
|
|
opacity: 0;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|