- 为 img 标签添加 object-fit: contain 样式,确保图片按比例显示 - 添加图片预加载功能,提高组件加载性能 - 增加图片加载失败的错误处理机制 - 优化滑块样式,设置背景图片不重复并居中显示 - 调整滑块位置,解决显示不完整的问题
356 lines
7.8 KiB
Vue
356 lines
7.8 KiB
Vue
<template>
|
|
<div class="puzzle-container">
|
|
<div class="puzzle-box" :style="{ height: boxHeight + 'px' }">
|
|
<!-- 背景图 -->
|
|
<img :src="bgImageUrl" class="bg-image" ref="bgImage" @load="onImageLoad" @error="handleImageError">
|
|
<!-- 滑块 -->
|
|
<div
|
|
class="slider-block"
|
|
:style="{
|
|
backgroundImage: `url(${sliderImageUrl})`,
|
|
top: `${blockY-2}px`,
|
|
left: `${moveX}px`,
|
|
visibility: loaded ? 'visible' : 'hidden'
|
|
}"
|
|
></div>
|
|
</div>
|
|
|
|
<!-- 滑动条 -->
|
|
<div class="slider-container">
|
|
<div class="slider-track">
|
|
<div
|
|
class="slider-bar"
|
|
:style="{ width: `${moveX}px` }"
|
|
></div>
|
|
<div
|
|
class="slider-button"
|
|
:style="{ left: `${moveX}px` }"
|
|
@mousedown="handleMouseDown"
|
|
@touchstart="handleTouchStart"
|
|
>
|
|
<div class="slider-icon"></div>
|
|
</div>
|
|
</div>
|
|
<!-- 验证结果提示 -->
|
|
<div v-if="verifySuccess || verifyError" class="verify-result-bar" :class="{ 'success': verifySuccess, 'error': verifyError }">
|
|
{{ verifyTip }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|
|
|
const props = defineProps({
|
|
show: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
blockY: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
bgImageUrl: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
sliderImageUrl: {
|
|
type: String,
|
|
required: true
|
|
}
|
|
})
|
|
|
|
const emit = defineEmits(['leave'])
|
|
|
|
// 响应式状态
|
|
const moveX = ref(0)
|
|
const startX = ref(0)
|
|
const oldMoveX = ref(0)
|
|
const maxMoveX = ref(0)
|
|
const boxHeight = ref(0)
|
|
const loaded = ref(false)
|
|
const isDragging = ref(false)
|
|
const verifySuccess = ref(false)
|
|
const verifyError = ref(false)
|
|
const verifyTip = ref('')
|
|
|
|
// DOM引用
|
|
const bgImage = ref(null)
|
|
|
|
// 方法
|
|
const onImageLoad = () => {
|
|
if (bgImage.value) {
|
|
try {
|
|
const img = bgImage.value
|
|
// 确保图片已经完全加载
|
|
if (!img.complete) {
|
|
return
|
|
}
|
|
|
|
const scale = img.width / img.naturalWidth // 计算图片缩放比例
|
|
boxHeight.value = img.height
|
|
|
|
// 根据图片实际显示大小调整滑块大小
|
|
const blockSize = Math.round(50 * scale)
|
|
document.documentElement.style.setProperty('--block-size', blockSize + 'px')
|
|
|
|
maxMoveX.value = img.width - blockSize // 使用实际显示的滑块大小计算最大移动距离
|
|
loaded.value = true
|
|
} catch (error) {
|
|
console.error('Image load error:', error)
|
|
// 设置默认值以确保组件仍然可用
|
|
const defaultBlockSize = 50
|
|
document.documentElement.style.setProperty('--block-size', defaultBlockSize + 'px')
|
|
maxMoveX.value = 320 - defaultBlockSize // 使用默认容器宽度
|
|
loaded.value = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// 添加图片错误处理
|
|
const handleImageError = () => {
|
|
console.error('Image failed to load')
|
|
// 设置默认值以确保组件仍然可用
|
|
const defaultBlockSize = 50
|
|
document.documentElement.style.setProperty('--block-size', defaultBlockSize + 'px')
|
|
maxMoveX.value = 320 - defaultBlockSize
|
|
loaded.value = true
|
|
}
|
|
|
|
const handleMouseDown = (event) => {
|
|
event.preventDefault()
|
|
startX.value = event.clientX
|
|
oldMoveX.value = moveX.value
|
|
isDragging.value = true
|
|
}
|
|
|
|
const handleTouchStart = (event) => {
|
|
event.preventDefault()
|
|
const touch = event.touches[0]
|
|
startX.value = touch.clientX
|
|
oldMoveX.value = moveX.value
|
|
isDragging.value = true
|
|
}
|
|
|
|
const move = (clientX) => {
|
|
let diff = clientX - startX.value
|
|
let newMoveX = oldMoveX.value + diff
|
|
|
|
// 限制移动范围
|
|
if (newMoveX < 0) newMoveX = 0
|
|
if (newMoveX > maxMoveX.value) newMoveX = maxMoveX.value
|
|
|
|
moveX.value = Math.round(newMoveX) // 取整数避免小数点导致的模糊
|
|
}
|
|
|
|
const handleMouseMove = (event) => {
|
|
if (!isDragging.value) return
|
|
move(event.clientX)
|
|
}
|
|
|
|
const handleTouchMove = (event) => {
|
|
if (!isDragging.value) return
|
|
event.preventDefault() // 防止页面滚动
|
|
move(event.touches[0].clientX)
|
|
}
|
|
|
|
const handleMouseUp = async () => {
|
|
if (!isDragging.value) return
|
|
isDragging.value = false
|
|
|
|
try {
|
|
emit('leave', moveX.value, (success) => {
|
|
if (success) {
|
|
verifySuccess.value = true
|
|
verifyError.value = false
|
|
verifyTip.value = '验证成功'
|
|
} else {
|
|
verifySuccess.value = false
|
|
verifyError.value = true
|
|
verifyTip.value = '验证失败'
|
|
moveX.value = 0 // 验证失败,滑块返回原位
|
|
}
|
|
})
|
|
} catch (error) {
|
|
verifySuccess.value = false
|
|
verifyError.value = true
|
|
verifyTip.value = '验证失败'
|
|
moveX.value = 0
|
|
}
|
|
}
|
|
|
|
const handleTouchEnd = () => {
|
|
handleMouseUp()
|
|
}
|
|
|
|
// 预加载图片
|
|
const preloadImages = () => {
|
|
const bgImg = new Image()
|
|
const sliderImg = new Image()
|
|
|
|
bgImg.onload = () => {
|
|
if (bgImage.value) {
|
|
onImageLoad()
|
|
}
|
|
}
|
|
|
|
bgImg.src = props.bgImageUrl
|
|
sliderImg.src = props.sliderImageUrl
|
|
}
|
|
|
|
// 生命周期钩子
|
|
onMounted(() => {
|
|
preloadImages()
|
|
window.addEventListener('mousemove', handleMouseMove)
|
|
window.addEventListener('mouseup', handleMouseUp)
|
|
window.addEventListener('touchmove', handleTouchMove)
|
|
window.addEventListener('touchend', handleTouchEnd)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
window.removeEventListener('mousemove', handleMouseMove)
|
|
window.removeEventListener('mouseup', handleMouseUp)
|
|
window.removeEventListener('touchmove', handleTouchMove)
|
|
window.removeEventListener('touchend', handleTouchEnd)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
:root {
|
|
--block-size: 50px;
|
|
}
|
|
|
|
.puzzle-container {
|
|
width: 100%;
|
|
max-width: 320px;
|
|
margin: 0 auto;
|
|
background: #fff;
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
touch-action: none;
|
|
-webkit-touch-callout: none;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
|
|
.puzzle-box {
|
|
position: relative;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
background: #f8f8f8;
|
|
border-radius: 8px;
|
|
touch-action: none;
|
|
}
|
|
|
|
.bg-image {
|
|
display: block;
|
|
width: 100%;
|
|
height: auto;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
pointer-events: none;
|
|
object-fit: contain;
|
|
max-width: 100%;
|
|
-webkit-touch-callout: none;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
.slider-block {
|
|
position: absolute;
|
|
width: var(--block-size);
|
|
height: var(--block-size);
|
|
background-size: 100% 100%;
|
|
background-repeat: no-repeat;
|
|
background-position: center;
|
|
cursor: pointer;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
touch-action: none;
|
|
will-change: transform;
|
|
transform: translateZ(0);
|
|
backface-visibility: hidden;
|
|
-webkit-touch-callout: none;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
.slider-container {
|
|
position: relative;
|
|
margin-top: 15px;
|
|
height: 40px;
|
|
touch-action: none;
|
|
}
|
|
|
|
.slider-track {
|
|
position: relative;
|
|
height: 40px;
|
|
background: #f5f5f5;
|
|
border-radius: 20px;
|
|
touch-action: none;
|
|
}
|
|
|
|
.slider-bar {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: #91d5ff;
|
|
border-radius: 20px;
|
|
will-change: transform;
|
|
transform-origin: left;
|
|
}
|
|
|
|
.slider-button {
|
|
position: absolute;
|
|
top: 0;
|
|
width: 40px;
|
|
height: 40px;
|
|
background: #fff;
|
|
border-radius: 50%;
|
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
cursor: pointer;
|
|
will-change: transform;
|
|
transform: translateZ(0);
|
|
backface-visibility: hidden;
|
|
}
|
|
|
|
.slider-icon {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 20px;
|
|
height: 20px;
|
|
background: #1890ff;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.verify-result-bar {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 40px;
|
|
line-height: 40px;
|
|
text-align: center;
|
|
font-size: 14px;
|
|
border-radius: 20px;
|
|
transition: all 0.3s;
|
|
z-index: 1;
|
|
}
|
|
|
|
.verify-result-bar.success {
|
|
background: #52c41a;
|
|
color: #fff;
|
|
}
|
|
|
|
.verify-result-bar.error {
|
|
background: #ff4d4f;
|
|
color: #fff;
|
|
}
|
|
|
|
/* 移除旧的提示样式 */
|
|
.verify-tip {
|
|
display: none;
|
|
}
|
|
</style>
|