前端滑块旋转验证登录

效果图如下
实现: 封装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(){
...
}
相关推荐
多多米100541 分钟前
初学Vue(2)
前端·javascript·vue.js
柏箱1 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑1 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
hmz8561 小时前
最新网课搜题答案查询小程序源码/题库多接口微信小程序源码+自带流量主
前端·微信小程序·小程序
看到请催我学习1 小时前
内存缓存和硬盘缓存
开发语言·前端·javascript·vue.js·缓存·ecmascript
blaizeer2 小时前
深入理解 CSS 浮动(Float):详尽指南
前端·css
编程老船长2 小时前
网页设计基础 第一讲:软件分类介绍、工具选择与课程概览
前端
编程老船长2 小时前
网页设计基础 第二讲:安装与配置 VSCode 开发工具,创建第一个 HTML 页面
前端
速盾cdn2 小时前
速盾:网页游戏部署高防服务器有什么优势?
服务器·前端·web安全
小白求学12 小时前
CSS浮动
前端·css·css3