liveh5-nuxt/app/components/puzzleComponent/index.vue
xingyy 36fab5a203 feat(puzzleComponent): 添加加载状态并优化验证逻辑
- 在 puzzleComponent 中添加加载状态显示
- 优化验证逻辑,修复验证失败时滑块不重置的问题
- 在 login 页面中集成新的加载状态
- 调整验证码按钮的加载状态显示
2025-03-12 15:10:44 +08:00

259 lines
6.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>
<script setup>
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
// Props
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 ? '验证成功' : '验证失败'
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>
<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>