Skip to content

Vue 实现拼图校验效果

思路

效果图

  • 拖动下面的方块使上面的方块同时拖动指导指定位置范围则表示成功
    • 拖动使用 Touch 事件监听元素节点的移动
    • 拼图右边缺口使用元素加透明度,左侧高亮部分使用元素的背景图来定位元素
    • 移动使用 CSSleft 属性
  • 由于我们在拖动的时候,1px 不差有点难以精准,所以需要一个像素范围,这里默认是 5px
    • 计算时加减 5px 即可
  • 如果拖动释放,则需要返回原位置,加个过渡动画,class 控制
    • 通过 CSSanimation 属性来实现过渡动画

实现代码

点击查看代码
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>

扩展

基于 MIT 许可发布