前端滑块旋转验证登录

效果图如下
实现: 封装VerifyImg组件
xml 复制代码
<template>
  <el-dialog v-model="dialogShow" width="380px" top="24vh" class="verifyDialog">
    <div class="verify-v">
      <div
        class="check"
        @mousedown="onMouseDown"
        @mouseup="onMouseUp"
        @mousemove="onMouseMove"
        @touchstart="onTouchStart"
        @touchmove="onTouchMove"
        @touchend="onTouchEnd"
      >
        <p>拖动滑块使图片角度为正</p>
        <div class="img-con">
          <img :src="imgUrl" :style="{ transform: imgAngle }" />
          <div v-if="showError" class="check-state">验证失败</div>
          <div v-else-if="showSuccess" class="check-state">验证成功</div>
          <div v-else-if="checking" class="check-state">验证中</div>
        </div>
        <div
          ref="sliderCon"
          class="slider-con"
          :class="{ 'err-anim': showError }"
          :style="{ '--percent': percent, '--bgColor': showError ? bgError : bgColor }"
        >
          <div ref="slider" class="slider" id="slider" :class="{ sliding }" :style="{ '--move': `${slidMove}px` }">
            <el-icon size="22"><Right id="slider" /></el-icon>
          </div>
        </div>
      </div>
    </div>
  </el-dialog>
</template>
<script>
export default {
  data() {
    return {
      imgUrl: '',
      dialogShow: false,
      showError: false,
      showSuccess: false,
      checking: false,
      sliding: false,
      slidMove: 0,
      percent: 0,
      sliderConWidth: 0,
      bgColor: 'rgba(25, 145, 250, 0.2)',
      bgError: 'rgba(255,78,78,0.2)',
      imgList: [
        new URL(`../../assets/images/verify/fn1.png`, import.meta.url).href,
        new URL(`../../assets/images/verify/fn2.png`, import.meta.url).href,
        new URL(`../../assets/images/verify/fn3.png`, import.meta.url).href
      ]
    }
  },

  computed: {
    angle() {
      let sliderConWidth = this.sliderConWidth ?? 0
      let sliderWidth = this.sliderWidth ?? 0
      let ratio = this.slidMove / (sliderConWidth - sliderWidth)
      return 360 * ratio
    },
    imgAngle() {
      return `rotate(${this.angle}deg)`
    }
  },
  mounted() {
    this.imgUrl = this.imgList[this.rand(0, 2)]
  },

  methods: {
    onTouchMove(event) {
      console.log('onTouchMove')
      if (this.sliding && this.checking === false) {
        // 滑块向右的平移距离等于鼠标移动事件的X坐标减去鼠标按下时的初始坐标。
        let m = event.touches[0].clientX - this.sliderLeft
        if (m < 0) {
          // 如果m小于0表示用户鼠标向左移动超出了初始位置,也就是0
          // 所以直接等于 0,以防止越界
          m = 0
        } else if (m > this.sliderConWidth - this.sliderWidth) {
          // 滑块向右移动的最大距离是滑槽的宽度减去滑块的宽度。
          // 因为css的 translateX 函数是以元素的左上角坐标计算的
          // 所以要减去滑块的宽度,这样滑块在最右边时,才不会左上角和滑槽右上角重合。
          m = this.sliderConWidth - this.sliderWidth
        }
        this.slidMove = m
        console.log(this.slidMove)

        this.percent = ((this.slidMove / document.querySelector('.slider-con').offsetWidth) * 100).toFixed(2) + '%'
      }
    },
    onTouchEnd() {
      console.log('onTouchEnd')
      if (this.sliding && this.checking === false) {
        this.checking = true
        this.validApi(this.angle)
          .then(isok => {
            if (isok) {
              this.showSuccess = true
            } else {
              this.showError = true
            }
            return new Promise((resolve, reject) => {
              // setTimeout(() => {
              if (isok) {
                resolve(Math.round(this.angle))
              } else {
                reject()
              }
              setTimeout(() => {
                this.resetSlider()
              }, 1000)
              // }, 1500)
            })
          })
          .then(angle => {
            // 处理业务,或者通知调用者验证成功
            console.log(angle)
            this.$emit('toLogin')
          })
          .catch(e => {
            console.log(e)

            // alert('旋转错误')
          })
      }
    },
    onTouchStart(event) {
      console.log('onTouchStart', event)
      // 设置状态为滑动中
      this.sliding = true
      // 下面三个变量不需要监听变化,因此不放到 data 中
      this.sliderLeft = event.touches[0].clientX // 记录鼠标按下时的x位置
      this.sliderConWidth = this.$refs.sliderCon.clientWidth // 记录滑槽的宽度
      this.sliderWidth = this.$refs.slider.clientWidth // 记录滑块的宽度
      console.log(this.sliderLeft, this.sliderConWidth, this.sliderWidth)
    },
    rand(m, n) {
      return Math.ceil(Math.random() * (n - m + 1) + m - 1)
    },

    showVerify() {
      this.imgUrl = this.imgList[this.rand(0, 2)]
      this.dialogShow = true
    },
    closeVerify() {
      //1.5s后关闭弹框
      setTimeout(() => {
        this.dialogShow = false
      }, 1500)
    },
    // 重置滑块
    resetSlider() {
      this.sliding = false
      this.slidMove = 0
      this.checking = false
      this.showSuccess = false
      this.showError = false
      this.percent = 0
    },
    //拖拽开始
    onMouseDown(event) {
      console.log(event.target.id, this.checking)
      if (event.target.id !== 'slider') {
        return
      }

      if (this.checking) return
      // 设置状态为滑动中
      this.sliding = true
      // 下面三个变量不需要监听变化,因此不放到 data 中
      this.sliderLeft = event.clientX // 记录鼠标按下时的x位置
      this.sliderConWidth = this.$refs.sliderCon.clientWidth // 记录滑槽的宽度
      this.sliderWidth = this.$refs.slider.clientWidth // 记录滑块的宽度
    },
    //拖拽停止
    onMouseUp(event) {
      if (this.sliding && this.checking === false) {
        this.checking = true
        this.validApi(this.angle)
          .then(isok => {
            if (isok) {
              this.showSuccess = true
            } else {
              this.showError = true
            }
            return new Promise((resolve, reject) => {
              // setTimeout(() => {
              if (isok) {
                resolve(Math.round(this.angle))
              } else {
                reject()
              }
              setTimeout(() => {
                this.resetSlider()
              }, 1000)
              // }, 1500)
            })
          })
          .then(angle => {
            // 处理业务,或者通知调用者验证成功
            console.log(angle)
            this.$emit('toLogin')
          })
          .catch(e => {
            console.log(e)

            // alert('旋转错误')
          })
      }
    },
    //拖拽进行中
    onMouseMove(event) {
      if (this.sliding && this.checking === false) {
        // 滑块向右的平移距离等于鼠标移动事件的X坐标减去鼠标按下时的初始坐标。
        let m = event.clientX - this.sliderLeft
        if (m < 0) {
          // 如果m小于0表示用户鼠标向左移动超出了初始位置,也就是0
          // 所以直接等于 0,以防止越界
          m = 0
        } else if (m > this.sliderConWidth - this.sliderWidth) {
          // 滑块向右移动的最大距离是滑槽的宽度减去滑块的宽度。
          // 因为css的 translateX 函数是以元素的左上角坐标计算的
          // 所以要减去滑块的宽度,这样滑块在最右边时,才不会左上角和滑槽右上角重合。
          m = this.sliderConWidth - this.sliderWidth
        }
        this.slidMove = m
        this.percent = ((this.slidMove / document.querySelector('.slider-con').offsetWidth) * 100).toFixed(2) + '%'
      }
    },
    // 验证角度是否正确
    validApi(angle) {
      return new Promise((resolve, reject) => {
        // 模拟网络请求
        setTimeout(() => {
          // 图片已旋转的角度
          const imgAngle = 90
          // 图片已旋转角度和用户旋转角度之和
          let sum = imgAngle + angle
          // 误差范围
          const errorRang = 20
          // 当用户旋转角度和已旋转角度之和为360度时,表示旋转了一整圈,也就是转正了
          // 但是不能指望用户刚好转到那么多,所以需要留有一定的误差
          let isOk = Math.abs(360 - sum) <= errorRang

          resolve(isOk)
        }, 1000)
      })
    }
  }
}
</script>

<style lang="scss">
.verifyDialog {
  .el-dialog__body {
    padding: 15px !important;
  }
}
</style>
<style lang="scss" scoped>
.verify-v {
  display: flex;
  justify-content: center;
  align-items: center;
}
.check {
  --slider-size: 40px;
  width: 300px;
  background: white;
  box-shadow: 0px 0px 12px rgb(0 0 0 / 8%);
  border-radius: 5px;
  padding: 10px 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  .img-con {
    position: relative;
    overflow: hidden;
    width: 120px;
    height: 120px;
    border-radius: 50%;
    margin-top: 20px;
    img {
      width: 100%;
      height: 100%;
      user-select: none;
    }
    .check-state {
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.5);
      color: white;
      position: absolute;
      top: 0;
      left: 0;
      display: flex;
      justify-content: center;
      align-items: center;
    }
  }
}
.check .slider-con {
  width: 80%;
  height: var(--slider-size);
  border-radius: 3px;
  margin-top: 1rem;
  position: relative;
  background: #f5f5f5;
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.1) inset;
  background: linear-gradient(to right, var(--bgColor) 0%, var(--bgColor) var(--percent), #fff var(--percent), #fff 100%);
  .slider {
    &:hover {
      background: #1991fa;
      color: #fff;
    }
    background: #fff;
    width: var(--slider-size);
    height: var(--slider-size);
    border-radius: 3px;
    box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
    cursor: move;
    display: flex;
    justify-content: center;
    align-items: center;
    --move: 0px;
    transform: translateX(var(--move));
    .sliding {
      background: #4ed3ff;
    }
  }
}
.slider-con.err-anim {
  animation: jitter 0.5s;
  .slider {
    background: #ff4e4e;
  }
}
body {
  padding: 0;
  margin: 0;
  background: #fef5e0;
}

@keyframes jitter {
  20% {
    transform: translateX(-5px);
  }
  40% {
    transform: translateX(10px);
  }
  60% {
    transform: translateX(-5px);
  }
  80% {
    transform: translateX(10px);
  }
  100% {
    transform: translateX(0);
  }
}
</style>
使用
typescript 复制代码
<VerifyImg ref="verifyRef" @to-login="handleLogin"></VerifyImg>


handleLogin(){
...
}
相关推荐
John.liu_Test28 分钟前
js下载excel示例demo
前端·javascript·excel
Yaml441 分钟前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事43 分钟前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶43 分钟前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo44 分钟前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v1 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫1 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.1 小时前
Chrome调试工具(查看CSS属性)
前端·chrome
栈老师不回家2 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙2 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js