Эх сурвалжийг харах

feat: 重新实现滑块验证

zhouyuhao 5 сар өмнө
parent
commit
8cb8d4b287

+ 2 - 1
src/auto-imports.d.ts

@@ -3,6 +3,7 @@
 // @ts-nocheck
 // noinspection JSUnusedGlobalSymbols
 // Generated by unplugin-auto-import
+// biome-ignore lint: disable
 export {}
 declare global {
   const $api: typeof import('@/api/index')['default']
@@ -68,6 +69,6 @@ declare global {
 // for type re-export
 declare global {
   // @ts-ignore
-  export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
+  export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
   import('vue')
 }

+ 1 - 1
src/components/VSliderVerification/src/VSliderVerification.vue

@@ -6,7 +6,7 @@ import Img03 from './image/verification-img-3.png'
 import Img04 from './image/verification-img-4.png'
 import { ref } from 'vue'
 //引入'vue3-puzzle-vcode'插件
-import Vcode from 'vue3-puzzle-vcode'
+import Vcode from './components/SliderVerification.vue'
 
 const openDialog = () => {
   isShow.value = true

BIN
src/components/VSliderVerification/src/assets/reset.png


+ 817 - 0
src/components/VSliderVerification/src/components/SliderVerification.vue

@@ -0,0 +1,817 @@
+<template>
+  <!-- 本体部分 -->
+  <div
+    :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
+          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"
+          @mousedown="onRangeMouseDown($event)"
+          @touchstart="onRangeMouseDown($event)"
+          :style="`width:${puzzleBaseSize}px;height:${canvasHeight}px;transform:translateX(${
+            styleWidth -
+            sliderBaseSize -
+            (puzzleBaseSize - sliderBaseSize) *
+              ((styleWidth - sliderBaseSize) / (canvasWidth - sliderBaseSize))
+          }px)`"
+        />
+        <div :class="['loading-box_', { hide_: !loading }]">
+          <div class="loading-gif_">
+            <span></span>
+            <span></span>
+            <span></span>
+            <span></span>
+            <span></span>
+          </div>
+        </div>
+        <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>
+</template>
+<script>
+import resetSvg from '../assets/reset.png'
+export default {
+  props: {
+    canvasWidth: { type: Number, default: 310 }, // 主canvas的宽
+    canvasHeight: { type: Number, default: 160 }, // 主canvas的高
+    // 是否出现,由父级控制
+    show: { type: Boolean, default: false },
+    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: '拖动滑块完成拼图'
+    }
+  },
+
+  data() {
+    return {
+      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, false)
+    document.addEventListener('mouseup', this.onRangeMouseUp, false)
+
+    document.addEventListener('touchmove', this.onRangeMouseMove, {
+      passive: false
+    })
+    document.addEventListener('touchend', this.onRangeMouseUp, false)
+    if (this.show) {
+      document.body.classList.add('vue-puzzle-overflow')
+      this.reset()
+    }
+  },
+  beforeUnmount() {
+    clearTimeout(this.timer1)
+    document.body.removeChild(this.$el)
+    document.removeEventListener('mousemove', this.onRangeMouseMove, false)
+    document.removeEventListener('mouseup', this.onRangeMouseUp, false)
+
+    document.removeEventListener('touchmove', this.onRangeMouseMove, {
+      passive: false
+    })
+    document.removeEventListener('touchend', this.onRangeMouseUp, 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: {
+    // 关闭
+    onClose() {
+      if (!this.mouseDown && !this.isSubmting) {
+        clearTimeout(this.timer1)
+        this.$emit('close')
+      }
+    },
+    onCloseMouseDown() {
+      this.closeDown = true
+    },
+    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')
+      const ctx2 = c2.getContext('2d')
+      const ctx3 = c3.getContext('2d')
+      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 = 3
+          ctx.fill()
+          ctx.clip()
+        } else {
+          ctx.clip()
+          ctx.save()
+          ctx.shadowOffsetX = 0
+          ctx.shadowOffsetY = 0
+          ctx.shadowColor = '#000'
+          ctx.shadowBlur = 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 = 0.8
+        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 = '#000'
+        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')
+      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 = true
+        // 成功后准备关闭
+        clearTimeout(this.timer1)
+        this.timer1 = setTimeout(() => {
+          // 成功的回调
+          this.isSubmting = false
+          this.$emit('success', x)
+        }, 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) {
+        return
+      }
+      this.resetState()
+      this.init()
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.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: 0;
+  // pointer-events: none;
+  transition: opacity 200ms;
+  // &.show_ {
+  opacity: 1;
+  pointer-events: auto;
+  // }
+}
+.vue-auth-box_ {
+  position: absolute;
+  top: 40%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  padding: 20px;
+  background: #fff;
+  user-select: none;
+  border-radius: 3px;
+  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: 4;
+    }
+    .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;
+      .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%;
+      }
+      .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;
+          & > 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>