<script setup> import { ref, reactive, onMounted, onBeforeUnmount } from 'vue' //i18n import { useI18n } from 'vue-i18n' const {t} =useI18n() const props = defineProps({ options:Object, loading: Boolean, }) 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 ? t('components.form.verifySuccess') : t('components.form.verifyFailed') isVerifying.value = false 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> <template> <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` }"> <!-- 加载状态 --> <div v-if="loading" class="absolute inset-0 flex flex-col items-center justify-center bg-#f8f8f8"> <div class="fancy-loader"> <div class="fancy-loader-bar"></div> <div class="fancy-loader-bar"></div> <div class="fancy-loader-bar"></div> <div class="fancy-loader-bar"></div> </div> </div> <!-- 背景图 --> <img v-else :src="options?.canvasSrc" class="pointer-events-none w-full h-full" ref="bgImage" @load="onImageLoad" @error="onImageError" > <!-- 滑块 --> <img :src="options?.blockSrc" class="absolute cursor-pointer will-change-transform transform-gpu" v-if="!loading" :class="{ 'transition-all duration-300 ease-out': !isDragging }" :style="{ top: `${options?.blockY}px`, left: `${moveX}px`, visibility: loaded ? 'visible' : 'hidden', width: `${options?.blockWidth}px`, height: `${options?.blockHeight}px` }" > <transition name="fade-slide"> <div v-if="verifyStatus.show" class="absolute left-0 bottom-0 w-full h-25px leading-25px 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> <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; } /* 加载动画样式 */ .fancy-loader { display: flex; align-items: flex-end; justify-content: center; width: 60px; height: 40px; } .fancy-loader-bar { width: 6px; height: 15px; margin: 0 3px; background-color: #1890ff; border-radius: 3px; animation: fancy-loading 1s ease-in-out infinite; } .fancy-loader-bar:nth-child(1) { animation-delay: 0s; } .fancy-loader-bar:nth-child(2) { animation-delay: 0.2s; } .fancy-loader-bar:nth-child(3) { animation-delay: 0.4s; } .fancy-loader-bar:nth-child(4) { animation-delay: 0.6s; } @keyframes fancy-loading { 0% { transform: scaleY(0.5); opacity: 0.5; } 50% { transform: scaleY(1.2); opacity: 1; } 100% { transform: scaleY(0.5); opacity: 0.5; } } /* 保留原有的spin动画,用于其他地方 */ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>