liveh5-nuxt/app/components/puzzleComponent/index.vue

258 lines
6.8 KiB
Vue
Raw Normal View History

<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>