Vue 实现拼图校验效果
思路
- 拖动下面的方块使上面的方块同时拖动指导指定位置范围则表示成功
- 拖动使用
Touch
事件监听元素节点的移动 - 拼图右边缺口使用元素加透明度,左侧高亮部分使用元素的背景图来定位元素
- 移动使用
CSS
的left
属性
- 拖动使用
- 由于我们在拖动的时候,
1px
不差有点难以精准,所以需要一个像素范围,这里默认是5px
- 计算时加减
5px
即可
- 计算时加减
- 如果拖动释放,则需要返回原位置,加个过渡动画,
class
控制- 通过
CSS
的animation
属性来实现过渡动画
- 通过
实现代码
点击查看代码
vue
<template>
<!-- 拼图校验 -->
<div class="me-jigsaw-validate" :style="`width:${width};`">
<div
class="jigsaw-img"
:style="`height:${height};--x:${missingPoint.x}px;--y:${crossPoint.y}px;--point-width:${crossPoint.width}px;--url:url(${url});`"
ref="jigsawImgRef"
>
<div
class="jigsaw-img-point"
:class="{ animation: openAnimation }"
:style="`left:${dragPoint.x + moveX}px;background-position:-${missingPoint.x}px -${crossPoint.y}px;background-size:${imgRect.width}px ${imgRect.height}px;`"
></div>
</div>
<div class="jigsaw-slide" :style="`height:${slideStyle.height};background:${slideStyle.background};`">
<div
class="slide-dot"
:class="{ animation: openAnimation }"
:style="`left:${moveX}px;background:${slideStyle.dotBackground};`"
@transitionend="onAnimationend"
@touchstart="onTouchstart"
@touchmove="onTouchmove"
@touchend="onTouchend"
></div>
<div class="slide-tips" :style="`color:${slideStyle.tips};`">{{ tips }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'MeJigsawValidate',
props: {
// 图片地址
url: {
type: String,
required: true
},
// 图片宽度
width: {
type: String,
default: '100%'
},
// 图片高度
height: {
type: String,
default: '300px'
},
// 随机位置
random: {
type: Boolean,
default: true
},
// 滑块样式
slideStyle: {
type: Object,
default: () => ({
height: '40px', // 滑块高度
background: '#f6f6f6', // 滑块背景色
dotBackground: '#409eff', // 拖拽点背景色
tips: '#494949' // 提示文本颜色
})
},
// 提示语
tips: {
type: String,
default: '按住左边按钮向右拖动完成上方图像验证'
},
// 容错值
range: {
type: Number,
default: 5
}
},
data() {
return {
dragPoint: { x: 10 }, // 拖拽点
missingPoint: { x: 200 }, // 缺失点
crossPoint: { y: 20, width: 50, height: 50 }, // 拖拽点和缺失点相同信息
startX: 0, // 横向开始点
openAnimation: false, // 动画开关
moveX: 0, // 移动距离
imgRect: { width: 0, height: 0 } // 图片范围
}
},
methods: {
// 操作开始
handleStart({ clientX }) {
this.openAnimation = false
this.startX = clientX
},
// 操作移动
handleMove({ clientX }) {
this.moveX = clientX - this.startX
},
// 操作结束
handleEnd({ clientX }) {
const dx = this.dragPoint.x // 拖拽点 x
const mx = this.missingPoint.x // 缺失点 x
const curX = clientX - this.startX // 当前位移
const resultBool = Math.abs(dx + curX - mx) < this.range // 验证结果
if (resultBool) {
this.moveX = mx - dx
} else {
this.moveX = 0
this.openAnimation = true
}
this.$emit('change', resultBool)
},
// 手指触摸开始
onTouchstart(e) {
this.handleStart(e.changedTouches[0])
},
// 手指触摸移动
onTouchmove(e) {
this.handleMove(e.changedTouches[0])
},
// 手指触摸离开
onTouchend(e) {
this.handleEnd(e.changedTouches[0])
},
// 动画结束
onAnimationend() {
this.openAnimation = false
}
},
mounted() {
const { width, height } = this.$refs.jigsawImgRef?.getBoundingClientRect()
this.imgRect = { width, height }
const split = width / 2 // 分割值
const cw = this.crossPoint.width
const ch = this.crossPoint.height
if (this.random) {
this.crossPoint.y = ~~(Math.random() * (height - ch))
this.dragPoint.x = ~~(Math.random() * (split - cw))
this.missingPoint.x = ~~(Math.random() * (width - cw - split)) + split
} else {
this.crossPoint.y = (height - ch) / 2
this.missingPoint.x = width - cw - 10
}
}
}
</script>
<style lang="less" scoped>
.me-jigsaw-validate {
.animation {
transition: left 0.4s;
}
.jigsaw {
&-img {
position: relative;
width: 100%;
background: var(--url) no-repeat center;
background-size: 100% 100%;
.point(@left) {
position: absolute;
top: var(--y);
left: @left;
width: var(--point-width);
height: var(--point-width);
border: 1px solid @border-color-default;
border-radius: @border-radius-default;
}
&::before {
.point(var(--x));
content: '';
box-sizing: border-box;
background: rgba(#000, 50%);
}
&-point {
.point(10px);
background-repeat: inherit;
background-image: inherit;
}
}
&-slide {
position: relative;
width: 100%;
height: 40px;
background: @bg-color;
overflow: hidden;
.slide {
&-dot {
position: absolute;
top: 0;
left: 0;
width: 40px;
height: 100%;
background: @color-primary;
}
&-tips {
display: flex;
justify-content: flex-end;
align-items: center;
width: 100%;
height: 100%;
padding: @padding-default;
color: @font-color;
font-size: @font-size-min;
user-select: none;
}
}
}
}
}
</style>