vue项目登录模块图片旋转验证功能实现(纯前端)

在当今互联网时代,随着技术的不断进步,传统的验证码验证方式已经无法满足对安全性和用户体验的需求。为了应对日益狡猾的机器人和恶意攻击,许多网站和应用程序开始引入图形验证码,其中一种备受欢迎的形式就是图片旋转验证功能。这项技术通过利用用户交互、视觉识别和动态效果,为用户提供了一种全新、有趣且高效的验证方式。本文将深入探讨如何实现这一引人注目的图片旋转验证功能,让您轻松保护网站安全,同时提升用户体验。

效果展示
在这里插入图片描述

功能介绍:
在vue项目中将此验证弹框封装成一个单独的组件,完整代码如下;
登录之前点击“安全验证”进入验证模块,拖动滑轨调整图片旋转位置,完成验证功能,验证失败会自动刷新再次验证,点击“刷新”也可以收到刷新图案,这是一个由纯前端实现的验证功能;

完整代码—组件封装

<template>
  <!-- 旋转验证图片 -->
  <div id="rotation" v-show="isShow">
    <div class="check" @mousedown="onMouseDown" @mouseup="onMouseUp" @mousemove="onMouseMove">
      <div class="title">请完成下方验证后继续操作</div>
      <div class="img-con">
        <!-- <img src="../assets/images/login/myn.png" :style="{ transform: imgAngle }" /> -->
        <img :src="showImg" :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 }">
        <div ref="slider" class="slider" id="slider" :class="{ sliding }" :style="{ '--move': `${slidMove}px` }">
        </div>
      </div>
      <div class="refresh">
        <t-button variant="text" theme="default" @click="refreshImg">
          <t-icon name="refresh" color="#77BB61" />刷新
        </t-button>
      </div>

    </div>
  </div>
</template>

<script>
export default {
  name: 'OfficeAutomationSystemClientRotationverificationImg',

  data() {
    return {
      isShow: true, //显示
      showError: false,
      showSuccess: false,
      checking: false,
      sliding: false,
      slidMove: 0,
      showImg: '',
      initAngle: 0,
      imgList: [
        { id: 1, url: require('@/assets/images/illustration/myn.png') },
        { id: 2, url: require('@/assets/images/illustration/fn7X4S.png') },
        { id: 3, url: require('@/assets/images/illustration/head.png') },
      ],

    };
  },
  computed: {
    angle() {
      let sliderConWidth = this.sliderConWidth ?? 0;
      let sliderWidth = this.sliderWidth ?? 0;
      let ratio = this.slidMove / (sliderConWidth - sliderWidth);
      // console.log(ratio);
      if (isNaN(ratio) || ratio == 0) {
        ratio = this.initAngle;
        return 360 * ratio;
      } else {
        return 360 * (this.initAngle + ratio);
      }


    },
    imgAngle() {
      return `rotate(${this.angle}deg)`;
    }
  },
  mounted() {
    this.refreshImg();
  },
  methods: {
    // 刷新图片
    refreshImg() {
      var randElement = this.imgList[Math.floor(Math.random() * this.imgList.length)];
      // console.log('img', randElement.url);
      this.showImg = randElement.url;
      this.resetSlider();
      var randomNumber = Math.floor(Math.random() * 381) + 30;
       console.log('数值',randomNumber);
      this.initAngle = randomNumber / 360;
    },
    // 重置滑块
    resetSlider() {
      this.sliding = false;
      this.slidMove = 0;
      this.checking = false;
      this.showSuccess = false;
      this.showError = false;
    },
    onMouseDown(event) {
      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();
                }
                this.resetSlider();
              }, 1000);
            });
          })
          .then((angle) => {
            // 处理业务,或者通知调用者验证成功
            // alert("旋转正确: " + angle + "度");
            // this.$message.success({
            //   content: '验证通过啦!',
            // });
            //给父组件传一个状态
            this.$emit('onEmit', 'T')
          })
          .catch((e) => {
            //alert("旋转错误");
            this.$message.error({
              content: '验证失败啦!',
            });
          });
      }
    },
    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;
      }
    },
    // 验证角度是否正确
    validApi(angle) {
      return new Promise((resolve, reject) => {
        // 模拟网络请求
        setTimeout(() => {
          // 图片已旋转的角度
          const imgAngle = this.initAngle * 360;
          // 图片已旋转角度和用户旋转角度之和
          let sum = angle;
           console.log('角度',imgAngle, angle);
          // 误差范围
          const errorRang = 20;
          // 当用户旋转角度和已旋转角度之和为360度时,表示旋转了一整圈,也就是转正了
          // 但是不能指望用户刚好转到那么多,所以需要留有一定的误差
          let isOk = Math.abs(360 - sum) <= errorRang;

          resolve(isOk);
        }, 500);
      });
    }
  }


};
</script>

<style lang="scss" scoped>
#rotation {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;

}

.check {
  --slider-size: 42px;
  width: 330px;
  background: white;
  // box-shadow: 0 0 5px grey;
  border-radius: 16px;
  padding: 20px 0;
  display: flex;
  flex-direction: column;
  align-items: center;


  .header {
    width: 90%;
    padding: 0 16px;
    padding-bottom: 16px;
    display: flex;
    justify-content: space-between;
    overflow: hidden;
    color: #464B52;
    font-size: 16px;
    font-weight: 400;
  }

  .title {
    color: #464B52;
    font-size: 16px;
    font-weight: 700;
    margin-bottom: 16px;
  }
}

.check .img-con {
  position: relative;
  overflow: hidden;
  width: 120px;
  height: 120px;
  border-radius: 50%;

}

.check .img-con img {
  width: 100%;
  height: 100%;
  user-select: none;

}

.check .slider-con {
  width: 80%;
  height: var(--slider-size);
  border-radius: 42px;
  margin-top: 1rem;
  position: relative;
  background: #f5f5f5;
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.1) inset;

}

.slider-con .slider {
  background: url('../assets/images/icon/arrow.svg') no-repeat;
  width: 42px;
  height: 42px;
  border-radius: 50%;
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
  cursor: move;

  --move: 0px;
  transform: translateX(var(--move));
}

.slider-con .slider.sliding {
  background: url('../assets/images/icon/arrow.svg') no-repeat;
}

.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;
}

body {
  padding: 0;
  margin: 0;
  background: #fef5e0;
}

.refresh {
  margin-top: 15px;
  height: 22px;
  vertical-align: middle;
  cursor: none;

  .t-button .t-button__text,
  .t-button .t-icon {
    margin-right: 5px;
    padding-top: 2px;
  }
}

@keyframes jitter {
  20% {
    transform: translateX(-5px);
  }

  40% {
    transform: translateX(10px);
  }

  60% {
    transform: translateX(-5px);
  }

  80% {
    transform: translateX(10px);
  }

  100% {
    transform: translateX(0);
  }
}

.slider-con.err-anim {
  animation: jitter 0.5s;
}

.slider-con.err-anim .slider {
  background: #ff4e4e;
}
</style>
  • 10
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值