跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaScript大前端

Vue 滑块验证组件实战:支持自定义图片与拖拽交互

Vue 滑块验证组件实战方案。通过子组件封装拖拽交互逻辑,支持自定义背景图和滑块图片。利用 mousedown、mousemove、mouseup 事件处理鼠标和触摸操作,兼容移动端。父组件接收回调距离数据完成验证。代码包含样式 SCSS 及核心 JS 逻辑,解决滑动穿透问题并限制边界范围。适合需要增强安全性的表单场景。

怪力乱神发布于 2025/2/5更新于 2026/10/777 浏览
Vue 滑块验证组件实战:支持自定义图片与拖拽交互

Vue 滑块验证组件实战:支持自定义图片与拖拽交互

在实际项目中,我们经常遇到需要防刷或人机验证的场景。原生实现滑块验证码不仅繁琐,还要处理移动端兼容和边界限制。这里分享一个基于 Vue 的滑块验证组件方案,支持自定义背景图和滑块样式,同时兼顾了鼠标和触摸事件。

核心逻辑拆解

这个组件的核心在于子组件对拖拽行为的控制。我们需要监听 mousedown、mousemove 和 mouseup(以及对应的 touch 事件),计算偏移量并限制在容器范围内。此外,为了防止页面滚动穿透,还需要动态锁定 body 的滚动位置。

子组件实现

子组件负责渲染界面和处理交互。通过 data 属性接收配置,如宽高、图片路径等,并通过 $emit 向父组件反馈拖动状态。

<template>
  <div style="padding: 6px 7px">
    <!-- 背景图区域 -->
    <div class="box" :style="getStyle(1)">
      <img :style="getStyle(1)" :src="\`data:image/jpeg;base64,${data.image}\`" alt="">
      <!-- 缺口位置 -->
      <div class="slids" :style="{'left': elePos.x + 'px', 'top': elePos.y + 'px', ...getStyle(3) }">
        <img style="width: 100%; height: 100%" :src="\`data:image/jpeg;base64,${data.slide}\`" alt="">
      </div>
    </div>
    
    <!-- 滑块轨道 -->
    <div class="slidBox" :style="getStyle(2)">
      <div class="slidBoxItem"></div>
      <img class="slid" 
        :style="{width: '52px', height: '52px', margin: '1px 0 0 0','left': elePos.x + 'px', 'top': elePos.y + 'px' }" 
        src="@/assets/images/saleAndPurchase/handler_bg.png" alt=""
        @mousedown.self="dragStartHandler"
        @touchstart.stop="dragStartHandler"
        @mousemove="draggingHandler"
        @touchmove.stop="draggingHandler"
        @mouseup="dragEndHandler"
        @touchend.stop="dragEndHandler"
      >
    </div>
    
    <!-- 操作按钮 -->
    <div class="repossessColse">
      <img @click.stop="close" style="width: 24px; height: 24px; margin: 0 16px" src="@/assets/images/saleAndPurchase/closes.png" alt="">
      <img @click.stop="repossess" style="width: 24px; height: 24px" src="@/assets/images/saleAndPurchase/repossess.png" alt="">
    </div>
  </div>
</template>

<script>
/**
 * 必传参数 data
 * data={ width:number,height:number,randomStr:string,pos:{height:number},slideWidth:number,slideHeight:number,image:string}
 * ******************************
 * 回调方法 callBack(type,distance)
 * type: 按下滑块|拖动滑块|松开滑块
 * distance: 滑块已滑动的距离
 */
export default {
  props:['data'],
  components: {},
  data() {
    return {
      fatherbox: null,
      innerbox: null,
      slidsbox: null,
      // 元素位置,用于样式绑定,动态更新位置
      elePos: {
        x: null,
        y: null
      },
      // 手指(鼠标)触摸点距离拖拽元素左上角的距离
      diffOfTarget: {
        x: 0,
        y: 0
      }
    };
  },
  mounted() {
    this.$nextTick(() => {
      // 注意:实际项目中需确保这些选择器在当前 DOM 中可用
      this.fatherbox = document.querySelector('.van-popup');
      this.innerbox = document.querySelector('.slid');
      this.slidsbox = document.querySelector('.slids');
    });
  },
  methods: {
    getStyle(e){
      if(e == 1){
        return {
          width: this.data.width+'px',
          height: this.data.height+'px',
        }
      }else if(e == 2){
        return {
          width: this.data.width+'px',
        }
      }else if(e == 3){
        return {
          width: this.data.slideWidth+'px',
          height: this.data.slideHeight+'px',
          top: this.data.pos.height + 6 + 'px',
        }
      }
    },
    dragStartHandler (e) {
      let touch = e
      if (e.touches) {
        touch = e.touches[0]
      }
      // 记录初始偏移量
      this.diffOfTarget.x = touch.clientX - e.target.offsetLeft
      this.diffOfTarget.y = touch.clientY - e.target.offsetTop
      
      // 解决滑动穿透问题,固定 body 滚动位置
      let scrollTop = document.scrollingElement.scrollTop
      document.body.classList.add('modal-open')
      document.body.style.top = -scrollTop + 'px'
      
      let dragEndHandlerLeft = this.innerbox.offsetLeft-7
      this.$emit('callBack','dragStartHandler',dragEndHandlerLeft)
    },
    draggingHandler (e) {
      let touch = e
      if (e.touches) {
        touch = e.touches[0]
      }
      
      // 设置拖拽元素的位置
      this.elePos.x = touch.clientX - this.diffOfTarget.x

      // 限制元素不能超过滑道范围
      let limitWidth = this.fatherbox.offsetWidth - this.slidsbox.offsetWidth  - 7
      let limitHeight = window.screen.height
      
      if (this.elePos.x < 7) {
        this.elePos.x = 7
      } else if (this.elePos.x > limitWidth) {
        this.elePos.x = limitWidth
      }
      if (this.elePos.y < 0) {
        this.elePos.y = 0
      } else if (this.elePos.y > limitHeight) {
        this.elePos.y = limitHeight - e.target.clientHeight
      }

      let dragEndHandlerLeft = this.innerbox.offsetLeft-7
      this.$emit('callBack','draggingHandler',dragEndHandlerLeft)
    },
    dragEndHandler (e) {
      let dragEndHandlerLeft = this.innerbox.offsetLeft-7
      // 恢复 body 滚动
      document.body.classList.remove('modal-open')
      this.$emit('callBack','dragEndHandler',dragEndHandlerLeft)
    },
    repossess() { // 刷新重置
      this.elePos.x = null
      this.diffOfTarget.x = 0
      
      let scrollTop = document.scrollingElement.scrollTop
      document.body.classList.add('modal-open')
      document.body.style.top = -scrollTop + 'px'
      document.body.classList.remove('modal-open')
      this.$emit('repossess')
    },
    close() {
      this.$emit('close')
    }
  },
};
</script>

<style lang="scss" scoped>
.box {
  width: 350px;
  height: 100px;
  overflow: hidden;
}
.repossessColse {
  height: 40px;
  display: flex;
  justify-content: flex-end;
  flex-direction: row;
  align-items:flex-end;
}
.slidBox {
  width: 100%;
  height: 52px;
  overflow: none;
  margin: auto;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  .slidBoxItem {
    width: 98%;
    height: 37px;
    background-color: #E6E6E6;
    border-radius: 50px;
  }
  &::-webkit-scrollbar {
    width: 0px;
    height: 0px;
  }
  .slid {
    width: 52px;
    height: 52px;
  }
}
.slid {
  position: fixed;
  left: 7px;
  bottom: 46px;
  cursor: pointer;
}
.slids {
  position: fixed;
  left: 7px;
  top: 0px;
  width: 50px;
  height: 50px;
  cursor: pointer;
}
</style>

注意:代码中使用了 .van-popup 作为父级容器选择器,如果你的项目使用的是其他弹窗组件(如 Element UI 或 Ant Design),请相应调整 mounted 中的 querySelector 逻辑。

父组件调用

在父组件中引入该组件,并绑定相关事件即可。通过 v-if 控制显示隐藏,利用 ref 获取实例以便调用刷新方法。

<slide-verification-puzzle
 v-if="show"
 @callBack="callBack"  
 @close="show = false"
 @repossess="repossess()"
 ref="slider"
 :data="encodedImage"
></slide-verification-puzzle>

这样一套下来,既保证了交互的流畅性,也满足了安全验证的需求。实际使用时,记得根据业务需求调整图片资源和尺寸参数。

目录

  1. Vue 滑块验证组件实战:支持自定义图片与拖拽交互
  2. 核心逻辑拆解
  3. 子组件实现
  4. 父组件调用

更多推荐文章

查看全部
  • Java 异常处理:从原理到实战最佳实践
  • ROS 2 海龟仿真器运行与 ros2 run 命令详解
  • RAG 框架全览:15 种关键技术综述
  • Mac Mini M4 本地 AI 模型实战:从 Ollama 到 Stable Diffusion 配置指南
  • Web 可访问性最佳实践:构建人人可用的前端界面
  • 算法模拟法解题实战
  • 低空经济新实践:无人机如何革新光伏电站巡检
  • 深入理解 Web Worker:Web 前端多线程实战
  • C++内存模型与原子操作
  • LeetCode 原地复写零:双指针与逆向填充的 O(n) 解法
  • Python 实现 PAT 乙级 1021 个位数统计
  • AR 眼镜移动端应用软件概述与技术展望
  • Qwen2.5 PyTorch 模型微调入门实战
  • Clawdbot 在 Ubuntu 服务器上的通用部署与配置指南
  • C++ RESTful API 开发指南:oatpp 框架快速上手与实战
  • Java 网络编程:UDP 数据报套接字编程
  • 大模型基于 llama.cpp 量化与 GGUF 格式详解
  • FPGA 中 XDMA 多通道传输架构解析
  • 接口鉴权与传输安全:签名、AK/SK、JWT 及加密方案
  • VS Code 搭配 GitHub Copilot 实战指南:配置、避坑与高效工作流

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online