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

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

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

功能介绍:
在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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1515480.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

力扣每日一题 最大二进制奇数 模拟 贪心

Problem: 2864. 最大二进制奇数 由于奇数的二进制末尾一定是 111&#xff0c;我们可以把一个 111 放在末尾&#xff0c;其余的 111 全部放在开头&#xff0c;这样构造出的奇数尽量大。 复杂度 时间复杂度: O ( n ) O(n) O(n) 空间复杂度: O ( 1 ) O(1) O(1) Code class…

行业认可 | 海云安上榜《2024年网络与信息安全行业全景图》多个领域

近日&#xff0c;深圳市网络与信息安全行业协会正式发布《2024年网络与信息安全行业全景图》。海云安凭借过硬的技术实力及成熟的网络与信息安全产品及服务获得行业认可&#xff0c;入围6大类目共计17项细分领域。包括&#xff1a; 业务安全&#xff08;软硬件开发安全、人工智…

ARMV8-aarch64的虚拟内存(mmutlbcache)介绍-概念扫盲

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《C》 《Linux》 《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 思考: 1、cache的entry里都是有什么&#xff1f; 2、TLB的entry里都是有什么? 3、MMU操作…

让若依生成的service、mapper继承mybatisPlus的基类

前言&#xff1a;若依继承mybatisPlus后&#xff0c;生成代码都要手动去service、serviceImpl、mapper文件去继承mybatisplus的基类&#xff0c;繁琐死了。这里通过修改若依生成模版从而达到生成文件后直接使用mybatisPlus的方法。 一、首先找到若依生成模版文件位置&#xff…

如何使用vue定义组件之——父组件调用子组件数据

首先&#xff0c;准备父子容器&#xff1a; <div class"container"><my-father></my-father><my-father></my-father><my-father></my-father><!-- 此处无法调用子组件&#xff0c;子组件必须依赖于父组件进行展示 --&…

爱普生晶振发布RTC模块晶振(压电侠)

爱普生晶振一直以”省&#xff0c;小&#xff0c;精”技术作为资深核心&#xff0c;并且已经建立了一个原始的垂直整合制造模型&#xff0c;可以自己创建独特的核心技术和设备&#xff0c;使用这些作为基地的规划和设计提供独特价值的产品. 世界领先的石英晶体技术精工爱普生公…

08.JavaScript中的编程思想,构造函数和原型对象

一、编程思想 学习 JavaScript 中基于原型的面向对象编程序的语法实现&#xff0c;理解面向对象编程的特征。 1.面向过程 面向过程就是分析出解决问题所需要的步骤&#xff0c;然后用函数把这些步骤一步一步实现&#xff0c;使用的时候再一个一个的依次 调用就可以了。 举个…

基于log4cpp封装日志类

一、log4cpp的使用 1. 下载log4cpp log4cpp官方下载地址 2. 安装log4cpp 第一步&#xff1a;解压 tar zxvf log4cpp-1.1.4.tar.gz 第二步&#xff1a;进入log4cpp文件夹并执行 ./configure tips&#xff1a;如果是ARM架构的CPU可能会失败&#xff0c;如下面这种情况&a…

揭秘WMM:wifi中的QOS

更多内容在 WiFi WMM&#xff08;无线多媒体&#xff09;是一种用于无线局域网&#xff08;WLAN&#xff09;的QoS&#xff08;服务质量&#xff09;标准。WMM旨在提供更好的网络性能&#xff0c;特别是在传输多媒体内容&#xff08;如音频和视频&#xff09;时。它通过对不同类…

魔域枫叶魔方

目录 魔域枫叶魔方 1&#xff0c;魔方三要素 2&#xff0c;复原方法 &#xff08;1&#xff09;复原6个面的正方形&#xff08;待续&#xff09; 魔域枫叶魔方 1&#xff0c;魔方三要素 &#xff08;1&#xff09;组成部件 6个中心块和8个角块&#xff0c;另外每个面还有…

shell控制多线程并发处理

一、前言 我们在用shell编程时&#xff0c;当用到循环语句时&#xff0c;如果循环的对象数量比较多&#xff0c;则代码一条一条处理&#xff0c;时间消耗会特别慢。如果此时机器资源充足&#xff0c;不妨学会多线程并发处理这招&#xff0c;帮助你提前打卡完成工作。 二、控制…

第二证券|炒股最好用的6个指标?

炒股存在以下好用的6个目标&#xff1a; 1、kdj目标 当k线从下方往上穿过d线时&#xff0c;构成金叉&#xff0c;是一种买入信号&#xff0c;投资者能够考虑在此刻买入一些个股&#xff0c;其间kdj金叉方位越低&#xff0c;买入信号越强&#xff1b;当k线从上往下穿过d线时&a…

Go——数组

Golang Array和以往认知的数组有很大的。 数组是同一种数据类型的固定长度的序列。数组定义&#xff1a;var a[len] int&#xff0c;比如&#xff1a;var a [5]int&#xff0c;数组长度必须是常量&#xff0c;且类型的组成部分。一旦定义&#xff0c;长度不能变。长度是数组类…

STM32串口通信—串口的接收和发送详解

目录 前言&#xff1a; STM32串口通信基础知识&#xff1a; 1&#xff0c;STM32里的串口通信 2&#xff0c;串口的发送和接收 串口发送&#xff1a; 串口接收&#xff1a; 串口在STM32中的配置&#xff1a; 1. RCC开启USART、串口TX/RX所对应的GPIO口 2. 初始化GPIO口 …

高级JAVA工程师解决生产环境JVM宕机Java进程挡掉操作系统内存异常实例讲解

高级JAVA工程师解决生产环境JVM宕机Java进程挡掉内存溢出实例讲解 一、事故描述 生产环境Java进程莫名挡掉&#xff0c;JVM宕机。监控平台报警。生产停了&#xff0c;老板急了&#xff0c;客户爆了&#xff0c;怎么迅速解决事故&#xff1f;每次出现生产事故&#xff0c;都是…

【JVM】什么是运行时数据区?

什么是运行时数据区&#xff1f; 运行时数据区指的是JVM所管理的内存区域&#xff0c;其中分成两大类&#xff1a; 线程共享 – 方法区、堆 方法区&#xff1a;存放每一个加载的类的元信息、运行时常量池、字符串常量池。 堆&#xff1a;存放创建出来的对象。 线程不共享 – …

VB+ACCESS学籍管理系统-264-(代码+说明)

转载地址: http://www.3q2008.com/soft/search.asp?keyword264 设计要求&#xff1a; 第一&#xff1a;一篇论文&#xff08;5000到10000字&#xff09;不包括图表和程序代码。A4纸20页之内。 论文结构如下&#xff1a; 设计题目&#xff1a;学籍管理系统 附&#xff1a;程…

Jeff Bezos的投资正开始见效

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

计算机组成原理练习-计算机硬件组成

冯诺依曼结构计算机 1.冯诺依曼结构计算机中数据采用二进制编码表示&#xff0c;其主要原因是&#xff08;&#xff09;。 I.二进制的运算规则简单 Ⅱ.制造两个稳态的物理器件较容易 Ⅲ.便于用逻辑门电路实现算术运算 A.仅I、Ⅱ B.仅I…

Spring具体拓展点:后置处理器

一图胜千言 mermaid示例图&#xff1a; #mermaid-svg-YEqFb5JcEk5FWkwO {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-YEqFb5JcEk5FWkwO .error-icon{fill:#552222;}#mermaid-svg-YEqFb5JcEk5FWkwO .error-text{fi…