vue项目登录模块滑块拼图验证功能实现(纯前端)

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

效果展示
在这里插入图片描述
功能介绍:
在vue项目中将此验证弹框封装成一个单独的组件,完整代码如下;
此功能中的图是利用canvas技术随机画10个图形拼接而成,然后就是画缺口和缺口的内阴影。
拖动滑轨调整小图移动位置,完成验证功能,验证失败会自动刷新再次验证,点击“刷新”也可以收到刷新图案,这是一个由纯前端实现的验证功能;

完整代码—组件封装

  <!-- 滑块拼图验证模块 -->
<template>
  <div>
    <!-- <div @click="changeBtn" class="btn">开始验证</div> -->
    <div></div>
    <!-- 本体部分 -->
    <div v-show="shoWData" :class="['vue-puzzle-vcode', { show_: show }]" @mousedown="onCloseMouseDown"
      @mouseup="onCloseMouseUp" @touchstart="onCloseMouseDown" @touchend="onCloseMouseUp">
      <div class="vue-auth-box_" @mousedown.stop @touchstart.stop>
        <div class="auth-body_" :style="`height: ${canvasHeight}px`">
          <!-- 主图,有缺口 -->
          <canvas style="border-radius: 10px" ref="canvas1" :width="canvasWidth" :height="canvasHeight"
            :style="`width:${canvasWidth}px;height:${canvasHeight}px`" />
          <!-- 成功后显示的完整图 -->
          <canvas ref="canvas3" :class="['auth-canvas3_', { show: isSuccess }]" :width="canvasWidth"
            :height="canvasHeight" :style="`width:${canvasWidth}px;height:${canvasHeight}px`" />
          <!-- 小图 -->
          <canvas :width="puzzleBaseSize" class="auth-canvas2_" :height="canvasHeight" ref="canvas2" :style="`width:${puzzleBaseSize}px;height:${canvasHeight}px;transform:translateX(${styleWidth -
            sliderBaseSize -
            (puzzleBaseSize - sliderBaseSize) *
            ((styleWidth - sliderBaseSize) /
              (canvasWidth - sliderBaseSize))}px)`
            " />

          <div :class="['info-box_', { show: infoBoxShow }, { fail: infoBoxFail }]">
            {{ infoText }}
          </div>
          <div :class="['flash_', { show: !isSuccess }]" :style="`transform: translateX(${isSuccess
            ? `${canvasWidth + canvasHeight * 0.578}px`
            : `-${canvasHeight * 0.578}px`
            }) skew(-30deg, 0);`
            "></div>
          <img class="reset_" @click="reset" :src="resetSvg" />
        </div>
        <div class="auth-control_">
          <div class="range-box" :style="`height:${sliderBaseSize}px`">
            <div class="range-text">{{ sliderText }}</div>
            <div class="range-slider" ref="range-slider" :style="`width:${styleWidth}px`">
              <div :class="['range-btn', { isDown: mouseDown }]" :style="`width:${sliderBaseSize}px`"
                @mousedown="onRangeMouseDown($event)" @touchstart="onRangeMouseDown($event)">
                <!-- 按钮内部样式 -->
                <div></div>
                <div></div>
                <div></div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import resetSvg from "@/assets/images/pc/login/Vector.png";
export default {
  props: {
    canvasWidth: { type: Number, default: 350 }, // 主canvas的宽
    canvasHeight: { type: Number, default: 200 }, // 主canvas的高
    // 是否出现,由父级控制
    show: { type: Boolean, default: true },
    puzzleScale: { type: Number, default: 1 }, // 拼图块的大小缩放比例
    sliderSize: { type: Number, default: 50 }, // 滑块的大小
    range: { type: Number, default: 10 }, // 允许的偏差值
    // 所有的背景图片
    imgs: {
      type: Array
    },
    successText: {
      type: String,
      default: "验证通过!"
    },
    failText: {
      type: String,
      default: "验证失败,请重试"
    },
    sliderText: {
      type: String,
      default: "拖动滑块完成拼图验证"
    },
    shoWData: {
      type: Boolean,
      default: false
    }
  },

  data() {
    return {
      verSuccess: false,
      isShow: false,
      mouseDown: false, // 鼠标是否在按钮上按下
      startWidth: 50, // 鼠标点下去时父级的width
      startX: 0, // 鼠标按下时的X
      newX: 0, // 鼠标当前的偏移X
      pinX: 0, // 拼图的起始X
      pinY: 0, // 拼图的起始Y
      loading: false, // 是否正在加在中,主要是等图片onload
      isCanSlide: false, // 是否可以拉动滑动条
      error: false, // 图片加在失败会出现这个,提示用户手动刷新
      infoBoxShow: false, // 提示信息是否出现
      infoText: "", // 提示等信息
      infoBoxFail: false, // 是否验证失败
      timer1: null, // setTimout1
      closeDown: false, // 为了解决Mac上的click BUG
      isSuccess: false, // 验证成功
      imgIndex: -1, // 用于自定义图片时不会随机到重复的图片
      isSubmting: false, // 是否正在判定,主要用于判定中不能点击重置按钮
      resetSvg,
    };
  },
  /** 生命周期 **/
  mounted() {
    // document.body.appendChild(this.$el);
    document.addEventListener("mousemove", this.onRangeMouseMove, { passive: false });
    document.addEventListener("mouseup", this.onRangeMouseUp, { passive: false });
    document.addEventListener("touchmove", this.onRangeMouseMove, { passive: false });
    document.addEventListener("touchend", this.onRangeMouseUp, { passive: false });
    if (this.show) {
      document.body.classList.add("vue-puzzle-overflow");
      this.reset();
    }
    // if (this.shoWData) {
    //   this.isShow = this.shoWData;
    //   console.log('我收到了验证!');
    // }
  },

  beforeDestroy() {
    clearTimeout(this.timer1);
    document.removeEventListener("mousemove", this.onRangeMouseMove, { passive: false });
    document.removeEventListener("mouseup", this.onRangeMouseUp, { passive: false });
    document.removeEventListener("touchmove", this.onRangeMouseMove, { passive: false });
    document.removeEventListener("touchend", this.onRangeMouseUp, { passive: false });
  },

  /** 监听 **/
  watch: {
    show(newV) {
      // 每次出现都应该重新初始化
      if (newV) {
        document.body.classList.add("vue-puzzle-overflow");
        this.reset();
      } else {
        this.isSubmting = false;
        this.isSuccess = false;
        this.infoBoxShow = false;
        document.body.classList.remove("vue-puzzle-overflow");
      }
    },
  },

  /** 计算属性 **/
  computed: {
    // styleWidth是底部用户操作的滑块的父级,就是轨道在鼠标的作用下应该具有的宽度
    styleWidth() {
      const w = this.startWidth + this.newX - this.startX;
      return w < this.sliderBaseSize
        ? this.sliderBaseSize
        : w > this.canvasWidth
          ? this.canvasWidth
          : w;
    },
    // 图中拼图块的60 * 用户设定的缩放比例计算之后的值 0.2~2
    puzzleBaseSize() {
      return Math.round(
        Math.max(Math.min(this.puzzleScale, 2), 0.2) * 52.5 + 6
      );
    },
    // 处理一下sliderSize,弄成整数,以免计算有偏差
    sliderBaseSize() {
      return Math.max(
        Math.min(
          Math.round(this.sliderSize),
          Math.round(this.canvasWidth * 0.5)
        ),
        10
      );
    }
  },

  /** 方法 **/
  methods: {
    changeBtn() {
      this.isShow = true;
    },
    // 关闭
    onClose() {
      if (!this.mouseDown && !this.isSubmting) {
        clearTimeout(this.timer1);
      }
    },
    onCloseMouseDown() {
      this.closeDown = true;
      this.isShow = false;
      this.init(true);
      //给父组件传一个状态
      this.$emit('submit', 'F')
    },
    onCloseMouseUp() {
      if (this.closeDown) {
        this.onClose();
      }
      this.closeDown = false;
    },
    // 鼠标按下准备拖动
    onRangeMouseDown(e) {
      if (this.isCanSlide) {
        this.mouseDown = true;
        this.startWidth = this.$refs["range-slider"].clientWidth;
        this.newX = e.clientX || e.changedTouches[0].clientX;
        this.startX = e.clientX || e.changedTouches[0].clientX;
      }
    },
    // 鼠标移动
    onRangeMouseMove(e) {
      if (this.mouseDown) {
        // e.preventDefault();
        this.newX = e.clientX || e.changedTouches[0].clientX;
      }
    },
    // 鼠标抬起
    onRangeMouseUp() {
      if (this.mouseDown) {
        this.mouseDown = false;
        this.submit();
      }
    },
    /**
     * 开始进行
     * @param withCanvas 是否强制使用canvas随机作图
     */
    init(withCanvas) {
      // 防止重复加载导致的渲染错误
      if (this.loading && !withCanvas) {
        return;
      }
      this.loading = true;
      this.isCanSlide = false;
      const c = this.$refs.canvas1;
      const c2 = this.$refs.canvas2;
      const c3 = this.$refs.canvas3;
      const ctx = c.getContext("2d", { willReadFrequently: true });
      const ctx2 = c2.getContext("2d", { willReadFrequently: true });
      const ctx3 = c3.getContext("2d", { willReadFrequently: true });
      const isFirefox = navigator.userAgent.indexOf("Firefox") >= 0 && navigator.userAgent.indexOf("Windows") >= 0; // 是windows版火狐
      const img = document.createElement("img");
      ctx.fillStyle = "rgba(255,255,255,1)";
      ctx3.fillStyle = "rgba(255,255,255,1)";
      ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
      ctx2.clearRect(0, 0, this.canvasWidth, this.canvasHeight);

      // 取一个随机坐标,作为拼图块的位置
      this.pinX = this.getRandom(this.puzzleBaseSize, this.canvasWidth - this.puzzleBaseSize - 20); // 留20的边距
      this.pinY = this.getRandom(20, this.canvasHeight - this.puzzleBaseSize - 20); // 主图高度 - 拼图块自身高度 - 20边距
      img.crossOrigin = "anonymous"; // 匿名,想要获取跨域的图片
      img.onload = () => {
        const [x, y, w, h] = this.makeImgSize(img);
        ctx.save();
        // 先画小图
        this.paintBrick(ctx);
        ctx.closePath();
        if (!isFirefox) {
          ctx.shadowOffsetX = 0;
          ctx.shadowOffsetY = 0;
          ctx.shadowColor = "#000";
          ctx.shadowBlur = 0;
          //ctx.globalAlpha = 0.4;
          ctx.fill();
          ctx.clip();
        } else {
          ctx.clip();
          ctx.save();
          ctx.shadowOffsetX = 0;
          ctx.shadowOffsetY = 0;
          ctx.shadowColor = "#000";
          ctx.shadowBlur = 0;
          //ctx.globalAlpha = 0.3;
          ctx.fill();
          ctx.restore();
        }

        ctx.drawImage(img, x, y, w, h);
        ctx3.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
        ctx3.drawImage(img, x, y, w, h);

        // 设置小图的内阴影
        ctx.globalCompositeOperation = "source-atop";

        this.paintBrick(ctx);

        ctx.arc(
          this.pinX + Math.ceil(this.puzzleBaseSize / 2),
          this.pinY + Math.ceil(this.puzzleBaseSize / 2),
          this.puzzleBaseSize * 1.2,
          0,
          Math.PI * 2,
          true
        );
        ctx.closePath();
        ctx.shadowColor = "rgba(255, 255, 255, .8)";
        ctx.shadowOffsetX = -1;
        ctx.shadowOffsetY = -1;
        ctx.shadowBlur = Math.min(Math.ceil(8 * this.puzzleScale), 12);
        ctx.fillStyle = "#ffffaa";
        ctx.fill();

        // 将小图赋值给ctx2
        const imgData = ctx.getImageData(
          this.pinX - 3, // 为了阴影 是从-3px开始截取,判定的时候要+3px
          this.pinY - 20,
          this.pinX + this.puzzleBaseSize + 5,
          this.pinY + this.puzzleBaseSize + 5
        );
        ctx2.putImageData(imgData, 0, this.pinY - 20);

        // ctx2.drawImage(c, this.pinX - 3,this.pinY - 20,this.pinX + this.puzzleBaseSize + 5,this.pinY + this.puzzleBaseSize + 5, 
        // 0, this.pinY - 20, this.pinX + this.puzzleBaseSize + 5, this.pinY + this.puzzleBaseSize + 5);

        // 清理
        ctx.restore();
        ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);

        // 画缺口
        ctx.save();
        this.paintBrick(ctx);
        ctx.globalAlpha = 1;
        ctx.fillStyle = "#ffffff";
        ctx.fill();
        ctx.restore();

        // 画缺口的内阴影
        ctx.save();
        ctx.globalCompositeOperation = "source-atop";
        this.paintBrick(ctx);
        ctx.arc(
          this.pinX + Math.ceil(this.puzzleBaseSize / 2),
          this.pinY + Math.ceil(this.puzzleBaseSize / 2),
          this.puzzleBaseSize * 1.2,
          0,
          Math.PI * 2,
          true
        );
        ctx.shadowColor = "#ffffff";
        ctx.shadowOffsetX = 2;
        ctx.shadowOffsetY = 2;
        ctx.shadowBlur = 16;
        ctx.fill();
        ctx.restore();

        // 画整体背景图
        ctx.save();
        ctx.globalCompositeOperation = "destination-over";
        ctx.drawImage(img, x, y, w, h);
        ctx.restore();

        this.loading = false;
        this.isCanSlide = true;
      };
      img.onerror = () => {
        this.init(true); // 如果图片加载错误就重新来,并强制用canvas随机作图
      };

      if (!withCanvas && this.imgs && this.imgs.length) {
        let randomNum = this.getRandom(0, this.imgs.length - 1);
        if (randomNum === this.imgIndex) {
          if (randomNum === this.imgs.length - 1) {
            randomNum = 0;
          } else {
            randomNum++;
          }
        }
        this.imgIndex = randomNum;
        img.src = this.imgs[randomNum];
      } else {
        img.src = this.makeImgWithCanvas();
      }
    },
    // 工具 - 范围随机数
    getRandom(min, max) {
      return Math.ceil(Math.random() * (max - min) + min);
    },
    // 工具 - 设置图片尺寸cover方式贴合canvas尺寸 w/h
    makeImgSize(img) {
      const imgScale = img.width / img.height;
      const canvasScale = this.canvasWidth / this.canvasHeight;
      let x = 0,
        y = 0,
        w = 0,
        h = 0;
      if (imgScale > canvasScale) {
        h = this.canvasHeight;
        w = imgScale * h;
        y = 0;
        x = (this.canvasWidth - w) / 2;
      } else {
        w = this.canvasWidth;
        h = w / imgScale;
        x = 0;
        y = (this.canvasHeight - h) / 2;
      }
      return [x, y, w, h];
    },
    // 绘制拼图块的路径
    paintBrick(ctx) {
      const moveL = Math.ceil(15 * this.puzzleScale); // 直线移动的基础距离
      ctx.beginPath();
      ctx.moveTo(this.pinX, this.pinY);
      ctx.lineTo(this.pinX + moveL, this.pinY);
      ctx.arcTo(
        this.pinX + moveL,
        this.pinY - moveL / 2,
        this.pinX + moveL + moveL / 2,
        this.pinY - moveL / 2,
        moveL / 2
      );
      ctx.arcTo(
        this.pinX + moveL + moveL,
        this.pinY - moveL / 2,
        this.pinX + moveL + moveL,
        this.pinY,
        moveL / 2
      );
      ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY);
      ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY + moveL);
      ctx.arcTo(
        this.pinX + moveL + moveL + moveL + moveL / 2,
        this.pinY + moveL,
        this.pinX + moveL + moveL + moveL + moveL / 2,
        this.pinY + moveL + moveL / 2,
        moveL / 2
      );
      ctx.arcTo(
        this.pinX + moveL + moveL + moveL + moveL / 2,
        this.pinY + moveL + moveL,
        this.pinX + moveL + moveL + moveL,
        this.pinY + moveL + moveL,
        moveL / 2
      );
      ctx.lineTo(
        this.pinX + moveL + moveL + moveL,
        this.pinY + moveL + moveL + moveL
      );
      ctx.lineTo(this.pinX, this.pinY + moveL + moveL + moveL);
      ctx.lineTo(this.pinX, this.pinY + moveL + moveL);

      ctx.arcTo(
        this.pinX + moveL / 2,
        this.pinY + moveL + moveL,
        this.pinX + moveL / 2,
        this.pinY + moveL + moveL / 2,
        moveL / 2
      );
      ctx.arcTo(
        this.pinX + moveL / 2,
        this.pinY + moveL,
        this.pinX,
        this.pinY + moveL,
        moveL / 2
      );
      ctx.lineTo(this.pinX, this.pinY);
    },
    // 用canvas随机生成图片
    makeImgWithCanvas() {
      const canvas = document.createElement("canvas");
      const ctx = canvas.getContext("2d", { willReadFrequently: true });
      canvas.width = this.canvasWidth;
      canvas.height = this.canvasHeight;
      ctx.fillStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
        100,
        255
      )},${this.getRandom(100, 255)})`;
      ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
      // 随机画10个图形
      for (let i = 0; i < 12; i++) {
        ctx.fillStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
          100,
          255
        )},${this.getRandom(100, 255)})`;
        ctx.strokeStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
          100,
          255
        )},${this.getRandom(100, 255)})`;

        if (this.getRandom(0, 2) > 1) {
          // 矩形
          ctx.save();
          ctx.rotate((this.getRandom(-90, 90) * Math.PI) / 180);
          ctx.fillRect(
            this.getRandom(-20, canvas.width - 20),
            this.getRandom(-20, canvas.height - 20),
            this.getRandom(10, canvas.width / 2 + 10),
            this.getRandom(10, canvas.height / 2 + 10)
          );
          ctx.restore();
        } else {
          // 圆
          ctx.beginPath();
          const ran = this.getRandom(-Math.PI, Math.PI);
          ctx.arc(
            this.getRandom(0, canvas.width),
            this.getRandom(0, canvas.height),
            this.getRandom(10, canvas.height / 2 + 10),
            ran,
            ran + Math.PI * 1.5
          );
          ctx.closePath();
          ctx.fill();
        }
      }
      return canvas.toDataURL("image/png");
    },
    // 开始判定
    submit() {
      this.isSubmting = true;
      // 偏差 x = puzzle的起始X - (用户真滑动的距离) + (puzzle的宽度 - 滑块的宽度) * (用户真滑动的距离/canvas总宽度)
      // 最后+ 的是补上slider和滑块宽度不一致造成的缝隙
      const x = Math.abs(
        this.pinX -
        (this.styleWidth - this.sliderBaseSize) +
        (this.puzzleBaseSize - this.sliderBaseSize) *
        ((this.styleWidth - this.sliderBaseSize) /
          (this.canvasWidth - this.sliderBaseSize)) -
        3
      );
      if (x < this.range) {
        // 成功
        this.infoText = this.successText;
        this.infoBoxFail = false;
        this.infoBoxShow = true;
        this.isCanSlide = false;
        this.isSuccess = false;
        // 成功后准备关闭
        clearTimeout(this.timer1);
        this.timer1 = setTimeout(() => {
          // 成功的回调
          this.isSubmting = false;
          this.isShow = false;
          this.verSuccess = true;
          this.$emit('submit', 'F', this.verSuccess);
          this.reset();
        }, 800);
      } else {
        // 失败
        this.infoText = this.failText;
        this.infoBoxFail = true;
        this.infoBoxShow = true;
        this.isCanSlide = false;
        // 失败的回调
        // this.$emit("fail", x);
        // 800ms后重置
        clearTimeout(this.timer1);
        this.timer1 = setTimeout(() => {
          this.isSubmting = false;
          this.reset();
        }, 800);
      }
    },
    // 重置 - 重新设置初始状态
    resetState() {
      this.infoBoxFail = false;
      this.infoBoxShow = false;
      this.isCanSlide = false;
      this.isSuccess = false;
      this.startWidth = this.sliderBaseSize; // 鼠标点下去时父级的width
      this.startX = 0; // 鼠标按下时的X
      this.newX = 0; // 鼠标当前的偏移X
    },

    // 重置
    reset() {
      if (this.isSubmting) {
        debugger
        return;
      }
      this.resetState();
      this.init();
    }
  }
};
</script>
<style lang="scss" scoped>
.btn {
  cursor: pointer;
  background-color: #6aa0ff;
  width: 80px;
  height: 30px;
  text-align: center;
  line-height: 30px;
  color: #fff;
}

.vue-puzzle-vcode {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background-color: rgba(0, 0, 0, 0.3);
  z-index: 999;
  opacity: 1;
  pointer-events: none;
  transition: opacity 200ms;

  &.show_ {
    opacity: 1;
    pointer-events: auto;
  }
}

.vue-auth-box_ {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 20px;
  background: #fff;
  user-select: none;
  border-radius: 20px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);


  .auth-body_ {
    position: relative;
    overflow: hidden;
    border-radius: 3px;

    .loading-box_ {
      position: absolute;
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;
      background-color: rgba(0, 0, 0, 0.8);
      z-index: 20;
      opacity: 1;
      transition: opacity 200ms;
      display: flex;
      align-items: center;
      justify-content: center;

      &.hide_ {
        opacity: 0;
        pointer-events: none;

        .loading-gif_ {
          span {
            animation-play-state: paused;
          }
        }
      }

      .loading-gif_ {
        flex: none;
        height: 5px;
        line-height: 0;

        @keyframes load {
          0% {
            opacity: 1;
            transform: scale(1.3);
          }

          100% {
            opacity: 0.2;
            transform: scale(0.3);
          }
        }

        span {
          display: inline-block;
          width: 5px;
          height: 100%;
          margin-left: 2px;
          border-radius: 50%;
          background-color: #888;
          animation: load 1.04s ease infinite;

          &:nth-child(1) {
            margin-left: 0;
          }

          &:nth-child(2) {
            animation-delay: 0.13s;
          }

          &:nth-child(3) {
            animation-delay: 0.26s;
          }

          &:nth-child(4) {
            animation-delay: 0.39s;
          }

          &:nth-child(5) {
            animation-delay: 0.52s;
          }
        }
      }
    }

    .info-box_ {
      position: absolute;
      bottom: 0;
      left: 0;
      width: 100%;
      height: 24px;
      line-height: 24px;
      text-align: center;
      overflow: hidden;
      font-size: 13px;
      background-color: #83ce3f;
      opacity: 0;
      transform: translateY(24px);
      transition: all 200ms;
      color: #fff;
      z-index: 10;

      &.show {
        opacity: 0.95;
        transform: translateY(0);
      }

      &.fail {
        background-color: #ce594b;
      }
    }

    .auth-canvas2_ {
      position: absolute;
      top: 0;
      left: 0;
      width: 60px;
      height: 100%;
      z-index: 2;
    }

    .auth-canvas3_ {
      position: absolute;
      top: 0;
      left: 0;
      opacity: 0;
      transition: opacity 600ms;
      z-index: 3;

      &.show {
        opacity: 1;
      }
    }

    .flash_ {
      position: absolute;
      top: 0;
      left: 0;
      width: 30px;
      height: 100%;
      background-color: rgba(255, 255, 255, 0.1);
      z-index: 3;

      &.show {
        transition: transform 600ms;
      }
    }

    .reset_ {
      position: absolute;
      top: 2px;
      right: 2px;
      width: 35px;
      height: auto;
      z-index: 12;
      cursor: pointer;
      transition: transform 200ms;
      transform: rotate(0deg);

      &:hover {
        transform: rotate(-90deg);
      }
    }
  }

  .auth-control_ {
    .range-box {
      position: relative;
      width: 100%;
      background-color: #eef1f8;
      margin-top: 20px;
      border-radius: 3px;
      // box-shadow: 0 0 8px rgba(240, 240, 240, 0.6) inset;
      box-shadow: inset -2px -2px 4px rgba(50, 130, 251, 0.1), inset 2px 2px 4px rgba(34, 73, 132, 0.2);
      border-radius: 43px;

      .range-text {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        font-size: 14px;
        color: #b7bcd1;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        text-align: center;
        width: 100%;
        /* 背景颜色线性渐变 */
        /* linear为线性渐变,也可以用下面的那种写法。left top,right top指的是渐变方向,左上到右上 */
        /* color-stop函数,第一个表示渐变的位置,0为起点,0.5为中点,1为结束点;第二个表示该点的颜色。所以本次渐变为两边灰色,中间渐白色 */
        background: -webkit-gradient(linear, left top, right top, color-stop(0, #4d4d4d), color-stop(.4, #4d4d4d), color-stop(.5, white), color-stop(.6, #4d4d4d), color-stop(1, #4d4d4d));

        /* 设置为text,意思是把文本内容之外的背景给裁剪掉 */
        -webkit-background-clip: text;
        /* 设置对象中的文字填充颜色 这里设置为透明 */
        -webkit-text-fill-color: transparent;
        /* 每隔2秒调用下面的CSS3动画 infinite属性为循环执行animate */
        -webkit-animation: animate 1.5s infinite;

      }

      /* 兼容写法,要放在@keyframes前面 */
      @-webkit-keyframes animate {

        /* 背景从-100px的水平位置,移动到+100px的水平位置。如果要移动Y轴的,设置第二个数值 */
        from {
          background-position: -100px;
        }

        to {
          background-position: 100px;
        }
      }

      @keyframes animate {
        from {
          background-position: -100px;
        }

        to {
          background-position: 100px;
        }
      }

      .range-slider {
        position: absolute;
        height: 100%;
        width: 50px;
        /**background-color: rgba(106, 160, 255, 0.8);*/
        border-radius: 3px;

        .range-btn {
          position: absolute;
          display: flex;
          align-items: center;
          justify-content: center;
          right: 0;
          width: 50px;
          height: 100%;
          background-color: #fff;
          border-radius: 3px;
          /** box-shadow: 0 0 4px #ccc;*/
          cursor: pointer;
          box-shadow: inset 0px -2px 4px rgba(0, 36, 90, 0.2), inset 0px 2px 4px rgba(194, 219, 255, 0.8);
          border-radius: 50%;

          &>div {
            width: 0;
            height: 40%;

            transition: all 200ms;

            &:nth-child(2) {
              margin: 0 4px;
            }

            border: solid 1px #6aa0ff;
          }

          &:hover,
          &.isDown {
            &>div:first-child {
              border: solid 4px transparent;
              height: 0;
              border-right-color: #6aa0ff;
            }

            &>div:nth-child(2) {
              border-width: 3px;
              height: 0;
              border-radius: 3px;
              margin: 0 6px;
              border-right-color: #6aa0ff;
            }

            &>div:nth-child(3) {
              border: solid 4px transparent;
              height: 0;
              border-left-color: #6aa0ff;
            }
          }
        }
      }
    }
  }
}

.vue-puzzle-overflow {
  overflow: hidden !important;
}
</style>

  • 13
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值