【Vue】基于前后端TianaiCaptch验证的滑块组件

代码解释

响应式兼容设计

通过vw和vh,百分比值,动态计算字体大小等响应式动态的滑块组件

@media (max-width: 480px) and (orientation: portrait) {
  .container {
    padding: 1.2rem;
    min-height: 280px;
  }
}

@media (max-height: 600px) {
  .container {
    transform: scale(0.9);
  }
}

@media (orientation: landscape) and (max-width: 1000px) {
  .container {
    width: 70%;
    max-width: 360px;
    padding: 1.5rem;
  }
}

@media (min-width: 1440px) {
  .container {
    max-width: 450px;
    padding: 2.5rem;
  }

  .title {
    font-size: 20px;
  }
}

@media (max-width: 768px) {
  .container {
    width: 95%;
    padding: 1.5vh 3vw;
  }

  .title {
    font-size: 14px;
  }

  .button-group .img-group svg {
    width: 12vw;
  }

  .block {
    width: 12vw !important;
  }

  .slide::before {
    font-size: 3vw !important;
  }
}

@media (max-width: 480px) {
  .container {
    min-height: 300px;
    width: 95%;
    padding: 1vh;
  }

  .slide::before {
    font-size: 12px !important;
  }
}

2560x1024
1440x1024
1024x1024
320x1024

320x579

较为美观的设计

在这里插入图片描述
在这里插入图片描述

自动转换时间格式

避免tianaiCaptch时间格式问题

/*
* @param Date() Web北京标准时间
* @return YYYY-MM-DDThh:mm:ss.sssZ
*/
getCurrentFormatDate(date) {
	if (!date) return;
	const pad = n => n.toString().padStart(2, '0');
	return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T` + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.` + `${date.getMilliseconds().toString().padStart(3, '0')}Z`;
},

批量记录轨迹坐标

轨迹列表

缓存,优化

一些CSS简化和空间内存管理优化等,优化计算
动画帧管理

 if (this.animationFrameId) {
   cancelAnimationFrame(this.animationFrameId)
 }
 this.animationFrameId = requestAnimationFrame(() => {
   if (e.cancelable && !e.defaultPrevented) e.preventDefault();
   this.handleMove(this.getmoveX(e));
 });

DOM缓存

//通过缓存并且通过watch,只有在使用才一次性获取,每次只有刷新才获取
watch: {
  backgroupImg() {
    this.$nextTick(() => {
      const bgEl = this.$el.querySelector('.inner-bg-img')
      const mvEl = this.$el.querySelector('.inner-mv-img')
      this.dimensions = {
        bgWidth: bgEl.offsetWidth,
        bgHeight: bgEl.offsetHeight,
        mvWidth: mvEl.offsetWidth,
        mvHeight: mvEl.offsetHeight
      }
    });
  }
},

加速度缓存计算

通过轨迹列表记录最后3点坐标进行简单的加速度计算,进行人机的简单判断,避免异常加速度

/*
* @param trackList[n...n-2] 取轨迹列表最后* 三段计算末速度
* 不过其实可以考虑加入异常方向判断,不过懒的加,你们可以看着加,方法就是根据xy轴进行判断
* @return a 加速度
*/
getAcceleration() {
	 if (this.trackList.length < 3) return 0;
	 const [p1, p2, p3] = this.trackList.slice(-3);
	 const dv1 = (p2.x - p1.x) / ((p2.t - p1.t) || 1) * 0.001;
	 const dv2 = (p3.x - p2.x) / ((p3.t - p2.t) || 1) * 0.001;
	 const dt = (p3.t - p1.t) / 2000;
	 const acceleration = dt !== 0 ? (dv2 - dv1) / dt * 0.0002645833 : 0;
	 return parseInt(acceleration);
 },

logo图片转base64——Bug修复

通过硬编码转换,避免同源策略导致无法显示图片,对于相对路径,需要配合构建工具

/**
* @param URL || FileObject || path(需要配合构建工具)
* @return Base64
*/
isValidUrl(url) {
  try {
    new URL(url);
    return true;
  } catch {
    return false;
  }
},

async handleFileInput() {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      this.logoImag = e.target.result;
      resolve();
    };
    reader.onerror = reject;
    reader.readAsDataURL(this.logoImag);
  });
},

async handleRemoteUrl() {
  const response = await fetch(this.logoImag, {
    mode: 'cors',
    credentials: 'same-origin'
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const blob = await response.blob();
  return this.handleFileBlob(blob);
},

async handleFileBlob(blob) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      this.logoImag = e.target.result;
      resolve();
    };
    reader.readAsDataURL(blob);
  });
},

// 处理相对路径(需要配合构建工具)
async handleLocalPath() {
  const module = await import(this.logoImag);
  this.logoImag = module.default;
},

async fileToBase64() {
  if (!this.logoImag || this.logoImag === logoImg || this.logoImag.startsWith('data:image')) return;
  try {
    if (this.logoImag instanceof File) {
      await this.handleFileInput();
    } else if (typeof this.logoImag === 'string') {
      if (this.isValidUrl(this.logoImag)) {
        await this.handleRemoteUrl();
      } else {
        await this.handleLocalPath();
      }
    }
  } catch (e) {
    this.printLog('fileToBase64', 'debug', `转换失败:${e}`);
  }
},

统一的事件处理

移动端触摸,PC端的鼠标都兼容,设备信息获取的兼容

/**
 * 统一滑块处理时事件
 */
handleMove(moveX) {
   try {
     this.maxMove = this.dimensions.bgWidth - this.dimensions.mvWidth + 5;
     this.moveX = Math.max(0, Math.min(moveX, this.maxMove))
     this.percentage = parseFloat(this.percentager);
     const tempTrack = {
       x: parseFloat((this.moveX + Math.random() * 2).toFixed(2)),
       y: 0.00,
       t: parseFloat((Date.now() - this.startTime.getTime()).toFixed(2))
     };
     if (this.startTime) {
     //节流优化,用普通数组替代vue渲染
       this._tempTrackList.push(tempTrack);
       if (this._tempTrackList.length % 60 === 0) {
         this.trackList = [...this._tempTrackList]
       }
     }
   } catch (e) { this.printLog('handleMove', 'debug', e); }
 }

使用教程README.md

请根据自行根据图片路径修改 Slider.vue 组件的图片路径

请尽量不要修改其它图片,因为响应式计算是基于图片大小的,修改图片大小会导致响应式计算失效。

  • 本组件可传递logo文件对象或者URL,若是必须要相对路径请考虑配合构建工具————加入属性 :logoImag="File || URL" 即可
  • 本组件可传递是否输出日志到控制台————加入属性 :log="true" 即可,默认值为 true

vue 组件使用教程

vue3选项式API组件使用教程

<script>
import Slider from './Slider.vue'
export default {
  components: {
    Slider;
  },
  data() {
    return {
      show: false;
    }
  },
  methods: {
    /**
     * @callback 回调函数,调用Slider组件的获取图片方法,请传递请求接口后返回的response.data参数
    */
    获取后端图片方法名(callback) {
      // 请在此处调用后端接口获取图片数据,并且callback回调函数传递response.data参数
    },
    /**
     * @param id 验证码的唯一标识,Slider组件已提供,调用后台接口带上参数即可
     * @param percentage 图片的百分比,Slider组件已提供,调用后台接口带上参数即可
     * @param Data tianai captcha的各种数据,Slider组件已提供,调用后台接口带上参数即可
     * @callback 回调函数,调用Slider组件的校验图片方法,请传递response.success||response.code参数,返回校验结果
     */
    校验图片方法(id,percentage,Data,callback) {
      // 请在此处调用校验图片请求接口方法
    },
    关闭方法() {
      // 请在此处调用关闭组件的方法,如清空图片列表等
      //类似于代码即可
        this.show = false;
    }
  }
}
</script>
<template>
<!---请用 v-if 控制组件的显示-->
  <div v-if="show">
    <Slider @getImg="获取后端图片方法名" @validImg="校验图片方法" @close="关闭方法" :logoImag="不支持相对路径,需要的请配合构建工具" :log="true"/>
  </div>
</template>

vue3API组合式使用教程

<script>
import Slider from './Slider.vue';
import {ref} from 'vue';

const show=ref(false);

/**
 * @callback 回调函数,调用Slider组件的获取图片方法,请传递请求接口后返回的response.data参数
*/
const  获取后端图片方法名=(callback)=> {
// 请在此处调用后端接口获取图片数据,并且callback回调函数传递response.data参数
 }

 /**
  * @param id 验证码的唯一标识,Slider组件已提供,调用后台接口带上参数即可
  * @param percentage 图片的百分比,Slider组件已提供,调用后台接口带上参数即可
  * @param Data tianai captcha的各种数据,Slider组件已提供,调用后台接口带上参数即可
  * @callback 回调函数,调用Slider组件的校验图片方法,请传递response.success||response.code参数,返回校验结果
  */
const  校验图片方法=(id,percentage,Data,callback)=> {
    // 请在此处调用校验图片请求接口方法
}

 //类似于代码即可
const  关闭方法()=> {
 // 请在此处调用关闭组件的方法,如清空图片列表等
this.show = false;
}
</script>
<template>
<!---请用 v-if 控制组件的显示-->
  <div v-if="show">
    <Slider @getImg="获取后端图片方法名" @validImg="校验图片方法" @close="关闭方法" :logoImag="不支持相对路径,需要的请配合构建工具" :log="true"/>
  </div>
</template>

数据传递结构

数据结构

总代码

<script>
//真要改logo,推荐直接修改这里的字符串
const logoImg = "data:image/png;base64,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";
//获取滑块图片方法
const GET_IMG_FUN = "getImg";
//校验滑块图片方法
const VALID_IMG_FUN = "validImg";
//滑块窗口关闭事件监听
const CLOST_EVENT_FUN = "close";

export default {
  data() {
    return {
      // 临时数据
      dimensions: {
        bgWidth: 0,
        bgHeight: 0,
        mvWidth: 0,
        mvHeight: 0
      },
      _tempTrackList: [],
      /**相关参数 */
      /**划过的百分比 */
      percentage: 0,
      trackList: [],
      startTime: null,
      stopTime: null,
      /**是否显示提示信息 */
      tips: false,
      /**滑块背景图片 */
      backgroupImg: "",
      /**滑块图片 */
      moveImg: "",
      /**是否已经移动滑块 */
      startMove: false,
      /**开始滑动的x轴 */
      startX: 0,
      /**验证码唯一ID */
      uuid: "",
      /**滑块移动的x轴 */
      moveX: 0,
      /** 加载遮罩标识 */
      loading: true,
      /** 请求flag */
      result: false,
      /**滑动耗时 */
      verifyTime: '0',
      /** 加速度 */
      a: 0,
      maxMove: 0,
      animationFrameId: null
    };
  },
  props: {
    logoImag: {
      type: String,
      default: logoImg
    },
    // 是否开启日志, 默认true
    log: {
      type: Boolean,
      default: true
    }
  },
  mounted() {
    this.fileToBase64();
    this.getImg();
  },
  computed: {
    getAcceleration() {
      if (this.trackList.length < 3) return 0;
      const [p1, p2, p3] = this.trackList.slice(-3);
      const dv1 = (p2.x - p1.x) / ((p2.t - p1.t) || 1) * 0.001;
      const dv2 = (p3.x - p2.x) / ((p3.t - p2.t) || 1) * 0.001;
      const dt = (p3.t - p1.t) / 2000;
      const acceleration = dt !== 0 ? (dv2 - dv1) / dt * 0.0002645833 : 0;
      return parseInt(acceleration);
    },
    percentager() {
      return (this.moveX / this.dimensions.bgWidth);
    },
  },
  methods: {
    isValidUrl(url) {
      try {
        new URL(url);
        return true;
      } catch {
        return false;
      }
    },

    async handleFileInput() {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = (e) => {
          this.logoImag = e.target.result;
          resolve();
        };
        reader.onerror = reject;
        reader.readAsDataURL(this.logoImag);
      });
    },

    async handleRemoteUrl() {
      const response = await fetch(this.logoImag, {
        mode: 'cors',
        credentials: 'same-origin'
      });
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      const blob = await response.blob();
      return this.handleFileBlob(blob);
    },

    async handleFileBlob(blob) {
      return new Promise((resolve) => {
        const reader = new FileReader();
        reader.onload = (e) => {
          this.logoImag = e.target.result;
          resolve();
        };
        reader.readAsDataURL(blob);
      });
    },

    // 处理相对路径(需要配合构建工具)
    async handleLocalPath() {
      const module = await import(this.logoImag);
      this.logoImag = module.default;
    },

    async fileToBase64() {
      if (!this.logoImag || this.logoImag === logoImg || this.logoImag.startsWith('data:image')) return;
      try {
        if (this.logoImag instanceof File) {
          await this.handleFileInput();
        } else if (typeof this.logoImag === 'string') {
          if (this.isValidUrl(this.logoImag)) {
            await this.handleRemoteUrl();
          } else {
            await this.handleLocalPath();
          }
        }
      } catch (e) {
        this.printLog('fileToBase64', 'debug', `转换失败:${e}`);
      }
    },
    /**
     * 生产环境禁用日志
     * 打印日志
     */
    printLog(msg, level, ...optionalParams) {
      if (!this.log) return;
      if (process.env.NODE_ENV === 'production' && level === 'debug') return
      if (optionalParams && optionalParams.length > 0) {
        optionalParams = (optionalParams.length === 1 ? optionalParams[0] : optionalParams);
        if (level === 'error') {
          console.error(`滑块验证码[${msg}]`, optionalParams);
        } else if (level === 'warn') {
          console.warn(`滑块验证码[${msg}]`, optionalParams);
        } else if (level === 'debug') {
          console.debug(`滑块验证码[${msg}]`, optionalParams);
        } else if (level === 'info') {
          console.info(`滑块验证码[${msg}]`, optionalParams);
        }
      }
    },
    addEventListener() {
      window.addEventListener("mousemove", this.move);
      window.addEventListener("mouseup", this.up);
      window.addEventListener("touchmove", this.move, { passive: true, capture: true });
      window.addEventListener("touchend", this.up, { passive: false });
    },
    removeEventListener() {
      window.removeEventListener("mousemove", this.move,);
      window.removeEventListener("mouseup", this.up);
      window.removeEventListener("touchmove", this.move);
      window.removeEventListener("touchend", this.up);
      window.removeEventListener("mousedown", this.start);
      window.removeEventListener("touchstart", this.start);
    },
    /**
     * 父组件调用请用callback回调函数传入(data响应数据--请看具体传回数据格式)
     * 获取滑块图片
     */
    getImg() {
      try {
        this.$emit(GET_IMG_FUN, data => {
          this.printLog(GET_IMG_FUN, data);
          if (!data) return;
          this.backgroupImg = data.captcha.backgroundImage;
          if (data.sliderImage === undefined) {
            this.moveImg = data.captcha.templateImage;
          } else {
            this.moveImg = data.captcha.sliderImage;
          }
          this.uuid = data.id;
          this.loading = false;
        });
      } catch (e) { this.printLog('getImg', 'debug', e); }
    },
    /**校验图片
     * @param id 验证码唯一ID
     * @param data 校验数据
     * @callback callback 回调函数
     * ======旧版本校验=====
     * * @param percentage 滑块划过百分比 可以使用但不推荐
     * ======新版本校验=====
     * ========Track Data========
     * * @param bgImageWidth 背景图片宽
     * * @param bgImageHeight 背景图片高
     * * @param templateImageWidth 模板图片宽
     * * @param templateImageHeight 模板图片高
     * * @param startTime 滑块开始滑动时间
     * * @param stopTime 滑块滑动结束时间
     * * @param trackList 滑动轨迹列表
     * * @param data 业务数据自定义扩展数据,用户加密等数据,请在组件外调用callback回调函数传入
     * ======== Drives Data=====
     * * @param userAgent 验证客户端信息
     * * @param windowHeight 窗口高度
     * * @param windowWidth 窗口宽度
     * * @param language 语言
     * * @param hasXhr 是否支持xhr请求
     * * @param platform 平台信息
     * * @param hardwareConcurrency 硬件并发数
     * * @param href 页面链接
     */
    validImg() {
      const isInvaild = Math.abs(this.a) > 10;
      if (isInvaild) {
        this.printLog('validImg', 'warn', `加速度异常检测:${this.a}`);
        this.reset();
        return;
      }
      if (0 === this.percentage || this.percentage >= 1) return;
      try {
        this.trackList = [...this._tempTrackList];
        const Data = {
          track: {
            bgImageWidth: this.dimensions.bgWidth,
            bgImageHeight: this.dimensions.bgHeight,
            templateImageWidth: this.dimensions.mvWidth,
            templateImageHeight: this.dimensions.mvHeight,
            startTime: this.getCurrentFormatDate(this.startTime),
            stopTime: this.getCurrentFormatDate(this.stopTime),
            trackList: this.trackList
          },
          drives: {
            userAgent: (navigator.userAgent || 'unknown'),
            windowHeight: (window.innerHeight || document.documentElement.clientHeight),
            windowWidth: (window.innerWidth || document.documentElement.clientWidth),
            language: (
              navigator.language ||
              navigator.userLanguage ||
              navigator.browserLanguage ||
              navigator.systemLanguage ||
              'unknown'
            ),
            hasXhr: (
              typeof XMLHttpRequest !== 'undefined' ||
              typeof ActiveXObject !== 'undefined'
            ),
            platform: (navigator.platform || 'unknown'),
            hardwareConcurrency: (navigator.hardwareConcurrency || 1),
            href: window.location.href
          }
        }
        this.printLog(`validImg`, 'debug', { id: this.uuid, percentage: this.percentage, Data });
        this.$emit(
          VALID_IMG_FUN,
          this.uuid,
          parseFloat(this.percentage),
          Data,
          data => {
            this.printLog(VALID_IMG_FUN, data);
            const flag = (data === false) || (parseInt(data) != 200);
            this.$el.querySelector('.tips').style.setProperty('color', flag ? '#ff4d4f' : '#52c41a');
            this.tips = true;
            if (flag) {
              setTimeout(() => this.reset(), 1500);
            } else {
              this.result = true;
              setTimeout(() => this.close(), 1500);
            }
          });
      } catch (e) {
        this.printLog('validImg', 'debug', e);
        this.reset();
      }
    },
    getCurrentFormatDate(date) {
      if (!date) return;
      const pad = n => n.toString().padStart(2, '0');
      return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T` + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.` + `${date.getMilliseconds().toString().padStart(3, '0')}Z`;
    },
    //获取耗时时间
    getTimeDifference(diffeTime) {
      if (!diffeTime) return;
      if (diffeTime < 1000) {
        return `${Math.round(diffeTime, 2)}`;
      }


      return `${diffeTime / 1000}.${(diffeTime % 1000).toFixed(2)}`;
    },
    /**
     * 重新生成图片
     */
    reset() {
      Object.assign(this.$data, {
        startTime: null,
        stopTime: null,
        trackList: [],
        verifyTime: 0,
        tips: false,
        moveX: 0,
        percentage: 0,
        startX: 0,
        blcokLeft: 0,
        result: false,
        loading: true,
        a: 0,
      });
      this.getImg();
    },
    /**
     * 按钮关闭事件
     */
    close() {
      this.printLog('close', 'debug', "关闭按钮触发");
      this.$emit(CLOST_EVENT_FUN);
    },
    /**
     * 统一滑块处理时事件
     */
    handleMove(moveX) {
      try {
        this.maxMove = this.dimensions.bgWidth - this.dimensions.mvWidth + 5;
        this.moveX = Math.max(0, Math.min(moveX, this.maxMove))
        this.percentage = parseFloat(this.percentager);
        const tempTrack = {
          x: parseFloat((this.moveX + Math.random() * 2).toFixed(2)),
          y: 0.00,
          t: parseFloat((Date.now() - this.startTime.getTime()).toFixed(2))
        };
        if (this.startTime) {
          this._tempTrackList.push(tempTrack);
          if (this._tempTrackList.length % 60 === 0) {
            this.trackList = [...this._tempTrackList]
          }
        }
      } catch (e) { this.printLog('handleMove', 'debug', e); }
    },

    getmoveX(e) {
      return (e.clientX || (e.touches && e.touches[0].clientX) || e.changedTouches[0].clientX) - this.startX;
    },

    getStartX(e) {
      return e.clientX || (e.touches && e.touches[0].clientX) || e.changedTouches[0].clientX;
    },
    /**
     * 开始滑动
     */
    start(e) {
      try {
        this.startTime = new Date();
        this.addEventListener();
        if (e.type === 'touchstart') {
          e.preventDefault();
          e.stopPropagation();
        }
        this.startMove = true;
        document.body.style.overflow = 'hidden';
        this.startX = this.getStartX(e);
        this.trackList.push({
          x: parseFloat(this.startX),
          y: 0.00,
          t: parseFloat(this.startTime.getTime()),
          type: 'DOWN'
        });
      } catch (e) { this.printLog('start', 'debug'.e) }
    },
    /**
     * 滑块滑动事件
     */
    move(e) {
      if (!this.startMove) return;
      this.startTime = new Date();
      if (this.animationFrameId) {
        cancelAnimationFrame(this.animationFrameId)
      }
      this.animationFrameId = requestAnimationFrame(() => {
        if (e.cancelable && !e.defaultPrevented) e.preventDefault();
        this.handleMove(this.getmoveX(e));
      });
    },
    /**
     * 滑块抬起事件
     */
    up(e) {
      try {
        this.stopTime = new Date();
        this.removeEventListener();
        document.body.style.overflow = '';
        if (!this.startMove || !this.startTime || !(this.startTime instanceof Date)) {
          this.startMove = false;
          return;
        }
        if (this.startTime && this.stopTime) {
          this.trackList.push({
            x: parseFloat(this.moveX),
            y: 0.00,
            t: parseFloat(this.stopTime.getTime()),
            type: 'UP'
          });
        }
        this.a = this.getAcceleration;
        this.startMove = false;
        this.verifyTime = this.getTimeDifference(this.stopTime.getTime() - this.startTime.getTime());
        this.printLog('up', 'debug', `滑动百分比:${this.percentage}`, `耗时:${this.verifyTime}ms`);
        this.validImg();
      } catch (e) { this.printLog('up', 'debug', e) }
    }
  },
  watch: {
    backgroupImg() {
      this.$nextTick(() => {
        const bgEl = this.$el.querySelector('.inner-bg-img')
        const mvEl = this.$el.querySelector('.inner-mv-img')
        this.dimensions = {
          bgWidth: bgEl.offsetWidth,
          bgHeight: bgEl.offsetHeight,
          mvWidth: mvEl.offsetWidth,
          mvHeight: mvEl.offsetHeight
        }
        console.debug('mounted', 'debug', this.dimensions);
      });
    }
  },
  /**
   * 销毁事件
   */
  beforeDestroy() {
    this.dimensions = null;
    this.trackList = null;
    this._tempTrackList = null;
    this.$el.parentNode.removeChild(this.$el);
    if (this.animationFrameId) {
      cancelAnimationFrame(this.animationFrameId);
    }
    document.body.style.overflow = '';
    document.body.style.touchAction = '';
    document.body.style.overscrollBehavior = '';
    this.removeEventListener();
    this.startTime = null;
    this.stopTime = null;
    this.printLog('beforeDestroy', 'debug', "销毁组件");
  }
}
</script>

<template>
  <div class="slider">
    <div class="container">
      <div class="title">
        <b>请完成下列验证后继续</b>
      </div>
      <div class="mask">
        <div class="loading" v-if="loading">
          <img src="../assets/loading.gif" />
        </div>
        <div class="img">
          <div class="backgroup-img">
            <img class="inner-bg-img" :src="backgroupImg" />
            <div class="tips" v-show="tips">
              <span v-if="result">
                ✅ 验证成功,耗时{{ verifyTime }}ms
              </span>
              <span v-else>
                ❌ 验证失败,耗时{{ verifyTime }}ms
              </span>
            </div>
            <div class="move-img" :style="{ transform: `translateX(${moveX}px)` }">
              <img class="inner-mv-img" :src="moveImg" />
            </div>
          </div>
        </div>
        <div class="slide">
          <div class="slider-mask" :style="{ width: `${moveX}px` }">
            <div class="block" ref="block" @mousedown="start" @touchstart="start"
              :style="{ transform: `translateX(${moveX}px)` }">
              <img class="slider-Icon">
            </div>
          </div>
        </div>
      </div>
      <div class="button-group">
        <img :src="logoImag" class="logo" />
        <div class="img-group">
          <svg @click="reset" viewBox="0 0 1024 1024">
            <path
              d="M943.8 484.1c-17.5-13.7-42.8-10.7-56.6 6.8-5.7 7.3-8.5 15.8-8.6 24.4h-0.4c-0.6 78.3-26.1 157-78 223.3-124.9 159.2-356 187.1-515.2 62.3-31.7-24.9-58.2-54-79.3-85.9h77.1c22.4 0 40.7-18.3 40.7-40.7v-3c0-22.4-18.3-40.7-40.7-40.7H105.5c-22.4 0-40.7 18.3-40.7 40.7v177.3c0 22.4 18.3 40.7 40.7 40.7h3c22.4 0 40.7-18.3 40.7-40.7v-73.1c24.2 33.3 53 63.1 86 89 47.6 37.3 101 64.2 158.9 79.9 55.9 15.2 113.5 19.3 171.2 12.3 57.7-7 112.7-24.7 163.3-52.8 52.5-29 98-67.9 135.3-115.4 37.3-47.6 64.2-101 79.9-158.9 10.2-37.6 15.4-76 15.6-114.6h-0.1c-0.3-11.6-5.5-23.1-15.5-30.9zM918.7 135.2h-3c-22.4 0-40.7 18.3-40.7 40.7V249c-24.2-33.3-53-63.1-86-89-47.6-37.3-101-64.2-158.9-79.9-55.9-15.2-113.5-19.3-171.2-12.3-57.7 7-112.7 24.7-163.3 52.8-52.5 29-98 67.9-135.3 115.4-37.3 47.5-64.2 101-79.9 158.8-10.2 37.6-15.4 76-15.6 114.6h0.1c0.2 11.7 5.5 23.2 15.4 30.9 17.5 13.7 42.8 10.7 56.6-6.8 5.7-7.3 8.5-15.8 8.6-24.4h0.4c0.6-78.3 26.1-157 78-223.3 124.9-159.2 356-187.1 515.2-62.3 31.7 24.9 58.2 54 79.3 85.9h-77.1c-22.4 0-40.7 18.3-40.7 40.7v3c0 22.4 18.3 40.7 40.7 40.7h177.3c22.4 0 40.7-18.3 40.7-40.7V175.8c0.1-22.3-18.2-40.6-40.6-40.6z"
              fill="#5e5c5c"></path>
          </svg>
          <svg @click="close" viewBox="0 0 1024 1024">
            <path
              d="M512 42.666667a469.333333 469.333333 0 1 0 469.333333 469.333333A469.333333 469.333333 0 0 0 512 42.666667z m0 864a394.666667 394.666667 0 1 1 394.666667-394.666667 395.146667 395.146667 0 0 1-394.666667 394.666667z"
              fill="#5e5c5c"></path>
            <path
              d="M670.4 300.8l-154.666667 154.666667a5.333333 5.333333 0 0 1-7.573333 0l-154.666667-154.666667a5.333333 5.333333 0 0 0-7.52 0l-45.173333 45.28a5.333333 5.333333 0 0 0 0 7.52l154.666667 154.666667a5.333333 5.333333 0 0 1 0 7.573333l-154.666667 154.666667a5.333333 5.333333 0 0 0 0 7.52l45.28 45.28a5.333333 5.333333 0 0 0 7.52 0l154.666667-154.666667a5.333333 5.333333 0 0 1 7.573333 0l154.666667 154.666667a5.333333 5.333333 0 0 0 7.52 0l45.28-45.28a5.333333 5.333333 0 0 0 0-7.52l-154.666667-154.666667a5.333333 5.333333 0 0 1 0-7.573333l154.666667-154.666667a5.333333 5.333333 0 0 0 0-7.52l-45.28-45.28a5.333333 5.333333 0 0 0-7.626667 0z"
              fill="#5e5c5c"></path>
          </svg>
        </div>
      </div>
    </div>
  </div>
</template>


<style scoped>
.silder-mask {
  position: absolute;
  border: 1px solid #1991fa;
  background: #d1e9fe;
  border-radius: 10px;
}

.slider {
  inset: 0;
  margin: 0 auto;
  top: 25%;
  width: min(90%, 400px);
  position: absolute;
  z-index: 999;

  .container {
    z-index: 999;
    display: grid;
    grid-template-rows: 18px 1fr 60px;
    gap: 0.5rem;
    width: min(90%, 400px);
    position: absolute;
    place-self: center;
    min-height: 320px;
    height: auto;
    padding: 2rem;
    background: url(../assets/images/slider-bg.png) center/cover;
    border-radius: 10px;
    box-shadow: #1991fa 0px 0px 20px 0px;
    /* box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); */

    .title {
      font-family: Arial, Helvetica, sans-serif;
      font-size: clamp(16px, 2.5vw, 18px);
      color: rgba(155, 4, 255, 1.2);
    }

    .button-group {
      display: flex;
      flex-direction: row;
      flex-wrap: nowrap;
      justify-content: space-between;

      img,
      svg {
        aspect-ratio: 1/1;
      }

      img {
        width: clamp(40px, 10vw, 60px);
      }

      svg {
        align-self: center;

        &:nth-of-type(1) {
          margin-right: 0.2rem;
        }

        width: clamp(40px, 10vw, 40px);
        cursor: pointer;
      }
    }

    .mask {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 0.5rem;
      position: relative;

      .loading {
        display: flex;
        align-items: center;
        position: absolute;
        width: 100%;
        height: 100%;
        z-index: 1003;

        img {
          margin: 0 auto;
          aspect-ratio: 1;
        }
      }

      .move-img {
        transition: transform 0.1s ease;
        will-change: transform;
        z-index: 1000;
        position: absolute;
        width: calc(110 / 600 * 100%);
        height: auto;
        backface-visibility: hidden;
      }

      .img {
        box-shadow: #e7c6e4 0px 0px 20px 0px;
        aspect-ratio: 16/9;
        position: relative;
      }

      .backgroup-img {
        aspect-ratio: 5/3;
        width: 100%;
        height: 100%;
        position: relative;
        box-shadow: 0 4px 12px rgba(24, 78, 213, 0.3);

        .inner-bg-img {
          width: 100%;
          height: 100%;
        }

        .tips {
          max-width: 600px;
          width: 100%;
          background: linear-gradient(145deg, rgba(218, 57, 254, 0.5), rgba(24, 144, 255, 0.5));
          font-size: clamp(12px, 2vw, 16px);
          bottom: 0;
          position: absolute;
          text-align: center;
          padding: 0.5rem 1rem;
          border-radius: 4px;
          color: rgb(255, 0, 0);
          transition: all 0.3s ease;
          z-index: 1002;
        }
      }

      .move-img {
        top: 0;
        left: 0;
        transition: transform 0.1s ease;
        will-change: transform;
        position: absolute;
        height: 100%;
        z-index: 1001;
        backface-visibility: hidden;

        img {
          position: absolute;
          left: 0;
          width: 100%;
          aspect-ratio: 11/36;
        }
      }

      .slide {
        touch-action: none;
        overscroll-behavior: contain;
        aspect-ratio: 1/1;
        width: 100%;
        border-radius: 10px;
        background-color: rgba(125, 171, 237, 0.3);
        position: relative;
        height: clamp(40px, 6vh, 50px);
        margin-top: 1rem;

        &::before {
          inset: 0;
          text-align: center;
          align-self: center;
          position: absolute;
          content: "按住左边按钮移动完成上方拼图";
          font-size: clamp(10px, 1.5vw, 14px);
          color: #999;
        }

        .slider-mask {
          position: absolute;
          height: 100%;
          border-radius: 10px;
          background: linear-gradient(145deg, rgba(218, 57, 254, 0.5), rgba(69, 150, 185, 0.33));
        }

        .block {
          border-radius: 10px;
          aspect-ratio: 1/1;
          width: clamp(40px, 10vw, 50px) !important;
          height: 100%;
          transition: transform 0.1s ease;
          position: absolute;
          cursor: pointer;
          will-change: transform;
          transform: translateZ(0);
          backface-visibility: hidden;
          touch-action: manipulation;
          -webkit-tap-highlight-color: transparent;

          &:active {
            filter: brightness(0.9);
            transition: filter 0.1s;
          }

          .slider-Icon {
            border-radius: 10px;
            aspect-ratio: 1/1;
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background: url("../assets/images/btn3.png") center/cover;
          }
        }
      }
    }
  }
}

@media (max-width: 480px) and (orientation: portrait) {
  .container {
    padding: 1.2rem;
    min-height: 280px;
  }
}

@media (max-height: 600px) {
  .container {
    transform: scale(0.9);
  }
}

@media (orientation: landscape) and (max-width: 1000px) {
  .container {
    width: 70%;
    max-width: 360px;
    padding: 1.5rem;
  }
}

@media (min-width: 1440px) {
  .container {
    max-width: 450px;
    padding: 2.5rem;
  }

  .title {
    font-size: 20px;
  }
}

@media (max-width: 768px) {
  .container {
    width: 95%;
    padding: 1.5vh 3vw;
  }

  .title {
    font-size: 14px;
  }

  .button-group .img-group svg {
    width: 12vw;
  }

  .block {
    width: 12vw !important;
  }

  .slide::before {
    font-size: 3vw !important;
  }
}

@media (max-width: 480px) {
  .container {
    min-height: 300px;
    width: 95%;
    padding: 1vh;
  }

  .slide::before {
    font-size: 12px !important;
  }
}

@keyframes shake {
  0% { transform: translate(-50%, 0) }
  25% { transform: translate(-55%, 0) }
  50% { transform: translate(-45%, 0) }
  75% { transform: translate(-50%, 0) }
  100% { transform: translate(-50%, 0) }
}
</style>
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值