VUE 实现滑块验证 ①

news2024/11/22 10:33:11

请添加图片描述

@作者 : SYFStrive

 
请添加图片描述

@博客首页 : HomePage

📜: 微信小程序

📌:个人社区(欢迎大佬们加入) 👉:社区链接🔗

📌:觉得文章不错可以点点关注 👉:专栏连接🔗

💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞

请添加图片描述
在这里插入图片描述
相关专栏

👉 VUE专栏(🔥)

目录

  • V u e j s Vuejs Vuejs
  • 滑块图示
  • 结构框架
  •   Html 结构
  •   Css 结构
  •   JS 结构
  •   完整代码
  •   实现效果
  • 总结

                    ⡖⠒⠒⠒⠤⢄⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸   ⠀⠀⠀⡼⠀⠀⠀⠀ ⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢶⣲⡴⣗⣲⡦⢤⡏⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⠋⠉⠉⠓⠛⠿⢷⣶⣦⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⠇⠀⠀⠀⠀⠀⠀⠘⡇⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡞⠀⠀⠀⠀⠀⠀⠀⢰⠇⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⡴⠊⠉⠳⡄⠀⢀⣀⣀⡀⠀⣸⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢸⠃⠀⠰⠆⣿⡞⠉⠀⠀⠉⠲⡏⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠈⢧⡀⣀⡴⠛⡇⠀⠈⠃⠀⠀⡗⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣱⠃⡴⠙⠢⠤⣀⠤⡾⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⢀⡇⣇⡼⠁⠀⠀⠀⠀⢰⠃⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣸⢠⣉⣀⡴⠙⠀⠀⠀⣼⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⡏⠀⠈⠁⠀⠀⠀⠀⢀⡇⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢸⠃⠀⠀⠀⠀⠀⠀⠀⡼⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢸⠀⠀⠀⠀⠀⠀⠀⣰⠃⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⣀⠤⠚⣶⡀⢠⠄⡰⠃⣠⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⢀⣠⠔⣋⣷⣠⡞⠀⠉⠙⠛⠋⢩⡀⠈⠳⣄⠀⠀⠀⠀⠀⠀⠀
⠀⡏⢴⠋⠁⠀⣸⠁⠀⠀⠀⠀⠀ ⠀⣹⢦⣶⡛⠳⣄⠀⠀⠀⠀⠀
⠀⠙⣌⠳⣄⠀⡇   不能   ⡏⠀⠀  ⠈⠳⡌⣦⠀⠀⠀⠀
⠀⠀⠈⢳⣈⣻⡇   白嫖 ⢰⣇⣀⡠⠴⢊⡡⠋⠀⠀⠀⠀
⠀⠀⠀⠀⠳⢿⡇⠀⠀⠀⠀⠀⠀⢸⣻⣶⡶⠊⠁⠀⠀
⠀⠀⠀⠀⠀⢠⠟⠙⠓⠒⠒⠒⠒⢾⡛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣠⠏⠀⣸⠏⠉⠉⠳⣄⠀⠙⢆⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⡰⠃⠀⡴⠃⠀⠀⠀⠀⠈⢦⡀⠈⠳⡄⠀⠀⠀⠀⠀⠀⠀
⠀⠀⣸⠳⣤⠎⠀⠀⠀⠀⠀⠀⠀⠀⠙⢄⡤⢯⡀⠀⠀⠀⠀⠀⠀
⠀⠐⡇⠸⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⡆⢳⠀⠀⠀⠀⠀⠀
⠀⠀⠹⡄⠹⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣇⠸⡆⠀⠀⠀⠀⠀
⠀⠀⠀⠹⡄⢳⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⡀⣧⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢹⡤⠳⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣷⠚⣆⠀⠀⠀⠀
⠀⠀⠀⡠⠊⠉⠉⢹⡀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡎⠉⠀⠙⢦⡀⠀
⠀⠀⠾⠤⠤⠶⠒⠊⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠙⠒⠲⠤⠽   

提示:以下是本篇文章正文内容

V u e j s Vuejs Vuejs


简介 : Vue 是一套用于构建用户界面的 渐进式 框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

Canvas参考链接 :https://blog.csdn.net/u01246837

滑块图示

在这里插入图片描述

在这里插入图片描述

结构框架

  Html 结构

 <!--滑块验证模块包裹-->
  <div class="slide-authCode-wrap" v-show="isOpen">
    <!--底下小箭头-->
    <div class="arrow"></div>
    <!--关闭按钮-->
    <div class="close" @click="Close">
      <span class="iconfont icon-chacha"></span>
    </div>

    <!--滑块主要容器-->
    <div class="validate-wrap">
      <!--header 头部部分-->
      <div class="refresh">
        <div class="refresh-text">完成拼图验证</div>
        <!--刷新数据按钮-->
        <div class="refresh-icon" @click="Refresh">
          <!--刷新按钮Icon-->
          <span class="icon iconfont icon-gengxin" ref="iconRotate"></span>
          <span>换一张</span>
        </div>
      </div>
      <!--滑块区域-->
      <div class="slider-main-container">
        <!-- 画布容器Box -->
        <div id="captcha" ref="captcha" style="position: relative">
          <!-- 画布bg -->
          <canvas ref="canvas_bg" width="364" height="142"
          >浏览器版本过低,请升级浏览器
          </canvas
          >
          <!-- 滑块box -->
          <canvas ref="blockBox" width="364" height="142" class="block"
          >浏览器版本过低,请升级浏览器
          </canvas
          >
          <!--用来加载图片标签 不显示-->
          <img ref="img" src="" style="display: none" width="0" height="0"/>
          <!-- <img
            ref="img"
            src="./images/722-300x150.jpg"
            width="0"
            height="0"
            style="display: none"
          /> -->
          <!-- 拖动容器Box -->
          <div
              class="slider-container"
              :class="slideVerifyStatus === 4 ? 'slider-container-fail' : ''"
          >
            <div class="slide-bg">
              <div class="left"></div>
              <div class="center">拖动滑块完成拼图,进行账号验证</div>
              <div class="right"></div>
            </div>
            <!-- 拖动遮罩 -->
            <div ref="slider_mask" class="slider-mask">
              <!-- 拖动块 -->
              <div
                  ref="slider"
                  class="slider"
                  @mousedown="SliderMousedownEvent"
              >
                <!-- 拖动Icon -->
                <span
                    class="slider-icon iconfont"
                    :class="[
                    slideVerifyStatus === 0 && 'icon-tubiao-xiaoshou',
                    slideVerifyStatus === 2 && 'icon-gouxuan',
                    slideVerifyStatus === 4 && 'icon-close',
                  ]"
                >
                </span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  Css 结构


<style scoped lang="less">
//滑块验证
.slide-authCode-wrap {
  position: absolute;
  left: 0;
  z-index: 110;
  bottom: 65px;
  width: 364px;
  height: 216.5px;
  padding: 12px 12px 12px 20px;
  border: 1px solid #eee;
  box-shadow: 0 0 2px 2px #eee;
  background-color: #fff;

  //关闭验证
  .close {
    cursor: pointer;
    z-index: 100;
    position: absolute;
    right: 10px;
    top: 10px;
    display: block;
    width: 20px;
    height: 20px;
    line-height: 20px;

    span {
      font-size: 20px;

      &:active {
        color: #a4a4a4;
      }
    }
  }

  //箭头
  .arrow {
    display: block;
    position: absolute;
    background-image: url("./images/tips.gif");
    background-repeat: no-repeat;
    width: 16px;
    height: 8px;
    background-position: 0 -8px;
    overflow: hidden;
    bottom: -8px;
    left: 190px;
  }

  //滑块主要容器
  .validate-wrap {
    //提醒区域
    .refresh {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding-right: 30px;
      font-family: Helvetica, Tahoma, Arial, "Microsoft YaHei", "微软雅黑",
      sans-serif;

      .refresh-text {
        font-size: 15px;
        color: #666;
      }

      .refresh-icon {
        cursor: pointer;
        color: #06c;

        .icon {
          display: inline-block;
          margin-right: 4px;
          vertical-align: revert;
          transition: 0.6s linear;
        }

        span {
          font-size: 15px;
        }
      }
    }

    //滑块组要容器
    .slider-main-container {
      margin-top: 5px;
      //画布容器Box
      #captcha {
        display: flex;
        justify-content: center;
        flex-direction: column;
        /* 小拼图 */

        .block {
          position: absolute;
          left: 0;
          top: 0;
        }

        /* 滑动条 */

        .slider-container {
          position: relative;
          margin: 10px auto 0;
          opacity: 1;
          font-size: 14px;
          visibility: visible;
          width: 364px;
          height: 40px;
          line-height: 40px;
          text-align: center;
          color: #05a4ea;

          //滑块Bg
          .slide-bg {
            .left {
              float: left;
              width: 40px;
              height: 40px;
              background: url("./images/slide-left-icon2.png") no-repeat;
            }

            .center {
              background-image: url("./images/slide-center-bg.png");
              margin-left: 40px;
              margin-right: 40px;
              overflow: hidden;
              white-space: nowrap;
              user-select: none;
              -moz-user-select: none;
              -ms-user-select: none;
              -webkit-user-select: none;
            }

            .right {
              width: 40px;
              height: 40px;
              background: url("./images/slide-right-icon2.png") no-repeat;
              position: absolute;
              right: 0;
              top: 0;
            }
          }

          /* 拖动遮罩容器 */

          .slider-mask {
            position: absolute;
            top: 0;
            left: 0;
            width: 364px;
            height: 40px;
            border-radius: 36px;
            border: 0px solid #1991fa;
            background: linear-gradient(#33b5fb, #8fdfff);
          }

          /* 拖动块 */

          .slider {
            position: absolute;
            left: -3px;
            top: -3px;
            width: 45px;
            height: 45px;
            background: #fff;
            box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
            cursor: pointer;
            transition: background 0.2s linear;
            border-radius: 50%;
          }

          .slider:hover {
            background: #aed6ff;
            color: #06c;
          }

          /* 拖动Icon */

          .slider-icon {
            font-size: 25px;
            font-weight: 700;
            vertical-align: middle;
          }
        }

        //活动状态CSS

        /* 滑动条失败态 */

        .slider-container-fail {
          .slider-mask {
            background: linear-gradient(#ff5e5e, #ffb3b3);
          }

          .slider {
            padding-top: 2px;
            box-sizing: border-box;
          }

          .slider-icon {
            color: red;
          }
        }
      }
    }
  }
}
</style>

  JS 结构

<script>
import {
  computed,
  getCurrentInstance,
  onBeforeUnmount,
  onMounted,
  reactive,
  ref,
  toRefs,
} from "vue";
//第三方模块
import throttle from "lodash/throttle"; // 引入节流会防抖插件
import { ElMessage } from "element-plus";
//自定义模块
import { mapActionsFun } from "@/hooks/VueX";
//获取两个值之间的随机数
import { GetRandomNumberByRange } from "@/utils/Random";
// //回去两个值之间的随机数
// function GetRandomNumberByRange(start, end) {
//     return Math.round(Math.random() * (end - start) + start);
// }
import GetGlobalData from "@/utils/Global/GetGlobalData";

export default {
  name: "SliderVerification",
  emits: ["successCallback", "failCallback"],
  setup(props, context) {
    const data = reactive({
      ll: 0,
      slideVerifyStatus: 0, //控制滑块的状态Icon
      isOpen: false,
    });

    const slidingData = {
      l: 35, //去掉突出的部分滑块总边长
      r: 7.5, //滑块突出的小圆圈半径
      w: 364, //canvas宽度
      h: 142, //canvas高度
      PI: Math.PI, //2PI = 360
      ll: 0, //滑块的实际边长(包括突出部分)
    };
    data.ll = computed(() => {
      return slidingData.l + slidingData.r * 2; //滑块的实际边长(包括突出部分)
    });
    const thisData = {
      x: 0,
      y: 0,
      captcha: null,
      canvas_bg: null,
      blockBox: null,
      img: null,
      slider_mask: null,
      slider: null,
      url: "",
      iconRotate: null,
      currentGlobalData: null,
    };
    const ctx = {
      canvasCtx: null,
      blockBoxCtx: null,
    };
    const methods = {
      Refresh: null,
      close: null,
      SliderMousedownEvent: null,
    };
    const eventData = {
      originX: 0,
      originY: 0,
      trail: [],
      isMouseDown: false,
    };

    //#region 生命周期
    const currentInstance = getCurrentInstance();
    data.currentGlobalData = GetGlobalData();
    onMounted(async () => {
      await GetElement();
      await EventGlobal();
    });

    onBeforeUnmount(() => {
      data.currentGlobalData.$bus.all.delete("openSlideVerify");
    });

    //#endregion

    //#region 封装方法
    const GetData = throttle(async () => {
      try {
        // Ajax请求 获取图片
        thisData.url = (
          await mapActionsFun(["getVerifySlideImg"]).getVerifySlideImg()
        ).url;
        thisData.img.src = thisData.url;
        await DrawInitialize();
        await InitImg();
      } catch (e) {
        ElMessage({
          showClose: true,
          dangerouslyUseHTMLString: true,
          type: "error",
          message: e,
          duration: 1500,
        });
      }
    }, 1500);

    // 获取需要的元素
    const GetElement = async () => {
      thisData.captcha = currentInstance.proxy.$refs.captcha;
      thisData.canvas_bg = currentInstance.proxy.$refs.canvas_bg;
      thisData.blockBox = currentInstance.proxy.$refs.blockBox;
      thisData.img = currentInstance.proxy.$refs.img;
      thisData.slider_mask = currentInstance.proxy.$refs.slider_mask;
      thisData.slider = currentInstance.proxy.$refs.slider;
      thisData.iconRotate = currentInstance.proxy.$refs.iconRotate;
      // alpha(boolean):表示canvas是否包含一个alpha通道,设为false则浏览器知道背景永远不透明,能加速对于透明场景和图像的绘制。
      // willReadFrequently(Boolean):表示是否计划有大量的回读操作,频繁调用getImageData()方法时能节省内存,仅Gecko内核浏览器支持。
      // storage(String):声明使用的storage类型,默认为”persistent”。
      ctx.canvasCtx = thisData.canvas_bg.getContext("2d", {
        willReadFrequently: true,
      });
      ctx.blockBoxCtx = thisData.blockBox.getContext("2d", {
        willReadFrequently: true,
      });
    };
    // 绘画方法
    const DrawInitialize = () => {
      thisData.x = GetRandomNumberByRange(
        data.ll + 10,
        slidingData.w - (data.ll + 10)
      );
      thisData.y = GetRandomNumberByRange(
        slidingData.r * 2 + 10,
        slidingData.h - (data.ll + 10)
      );
      // fill 通过填充路径的内容区域生成实心的图形
      // clip 把已经创建的路径转换成裁剪路径。裁剪路径的作用是遮罩。只显示裁剪路径内的区域,裁剪路径外的区域会被隐藏。
      // 注意:clip()只能遮罩在这个方法调用之后绘制的图像,如果是clip()方法调用之前绘制的图像,则无法实现遮罩。
      Draw(ctx.canvasCtx, "fill", thisData.x, thisData.y);
      Draw(ctx.blockBoxCtx, "clip", thisData.x, thisData.y);
    };

    // 绘制  2D渲染,渲染方式,坐标(X,Y)
    function Draw(ctx, operation, x, y) {
      // ★前提要创建Canvas 否者无法绘画
      // 新建一条路径,路径一旦创建成功,图形绘制命令被指向到路径上生成路径
      ctx.beginPath();
      // 把画笔移动到指定的坐标(x, y)。相当于设置路径的起始点坐标。
      ctx.moveTo(x, y);
      //(绘制一条从当前位置到指定坐标x,y)的直线. 假如 x 60~304 y 25~82
      ctx.lineTo(x + slidingData.l / 2, y);
      // 绘制圆弧 x, y, r, startAngle, endAngle, anticlockwise 六个参数
      // 以(x, y)为圆心,以r为半径,从 startAngle弧度开始到endAngle弧度结束。anticlosewise是布尔值,true表示逆时针,false表示顺时针。(默认是顺时针)
      // 注意1 这里的度数都是弧度 👉 0弧度是指的x轴正方形
      // 注意2 arc绘制的坐标是从最开始的位置计算的
      ctx.arc(
        x + slidingData.l / 2,
        y - slidingData.r,
        slidingData.r,
        0,
        2 * slidingData.PI
      );
      // 回到原来起步画圆的位置
      ctx.lineTo(x + slidingData.l / 2, y);
      // 半径 直径是指 L
      // 向右再走 直径 位置
      ctx.lineTo(x + slidingData.l, y);
      // 向右再走 半径 位置
      ctx.lineTo(x + slidingData.l, y + slidingData.l / 2);
      // arc方法 须从最开始的位置计算 走到最右边 再向右走半径位置 向下走半径位置
      ctx.arc(
        x + slidingData.l + slidingData.r,
        y + slidingData.l / 2,
        slidingData.r,
        0,
        2 * slidingData.PI
      );
      ctx.lineTo(x + slidingData.l, y + slidingData.l / 2);
      ctx.lineTo(x + slidingData.l, y + slidingData.l);
      ctx.lineTo(x, y + slidingData.l);
      ctx.lineTo(x, y);
      ctx.fillStyle = "#fff";
      ctx[operation]();
      ctx.beginPath();
      ctx.arc(
        x,
        y + slidingData.l / 2,
        slidingData.r,
        1.5 * slidingData.PI,
        0.5 * slidingData.PI
      );
      // 合成 xor属性作用:重叠部分变透明
      ctx.globalCompositeOperation = "xor";
      ctx.fill();
    }

    // 初始化图像
    const InitImg = () => {
      ctx.canvasCtx.drawImage(thisData.img, 0, 0, slidingData.w, slidingData.h);
      ctx.blockBoxCtx.drawImage(
        thisData.img,
        0,
        0,
        slidingData.w,
        slidingData.h
      );
      const y = thisData.y - slidingData.r * 2; //减去突出圆的大小
      // 参数 获取那块区域坐标x,y 宽
      const imageData = ctx.blockBoxCtx.getImageData(
        thisData.x,
        y,
        data.ll,
        data.ll
      );
      thisData.blockBox.width = data.ll;
      // 后面两个参数从哪里放上去
      ctx.blockBoxCtx.putImageData(imageData, 0, y);
    };

    // 绑定事件 刷新
    let index = ref(0);
    methods.Refresh = throttle(() => {
      index.value++;
      thisData.iconRotate.style.rotate = -360 * index.value + "deg";
      Reset();
    }, 2000);
    // 关闭滑块验证
    methods.Close = () => {
      data.isOpen = false;
    };

    // ----------------按着---------------
    methods.SliderMousedownEvent = (e) => {
      eventData.originX = e.x;
      eventData.originY = e.y;
      eventData.isMouseDown = true;
    };
    // ----------------拖动---------------
    document.addEventListener("mousemove", (e) => {
      if (!eventData.isMouseDown) return false;
      const moveY = e.y - eventData.originY;
      const moveX = e.x - eventData.originX;
      // 判断时候超出或者小于0
      if (moveX < 0 || moveX + 40 >= slidingData.w) return false;
      // 拖动按钮位置
      thisData.slider.style.left = moveX + "px";
      // 拖动填充滑块位置
      const blockLeft = moveX;
      thisData.blockBox.style.left = blockLeft + "px";
      // 遮罩宽长度
      thisData.slider_mask.style.width = moveX + 40 + "px";
      // 添加位置
      eventData.trail.push(moveY);
    });
    // ----------------抬起---------------
    document.addEventListener("mouseup", () => {
      if (!eventData.isMouseDown) return false;
      else eventData.isMouseDown = false;
      // 验证位置;
      const spliced = Verify();
      if (spliced) {
        // 添加成功
        data.slideVerifyStatus = 2;
        SuccessCallback();
        methods.Close();
      } else {
        // 添加失败样式
        data.slideVerifyStatus = 4;
        FailCallback();
        setTimeout(() => {
          Reset();
        }, 1000);
      }
    });

    // 清除
    function CleanCtx() {
      ctx.canvasCtx.clearRect(0, 0, slidingData.w, slidingData.h);
      ctx.blockBoxCtx.clearRect(0, 0, slidingData.w, slidingData.h);
      thisData.blockBox.width = slidingData.w;
    }

    // 重置
    async function Reset() {
      data.slideVerifyStatus = 0;
      thisData.slider.style.left = 0;
      thisData.blockBox.style.left = 0;
      thisData.slider_mask.style.width = 0;
      await CleanCtx();
      await GetData();
    }

    // 验证
    function Verify() {
      const left = parseInt(thisData.blockBox.style.left);
      return Math.abs(left - thisData.x) < 1; //10表示容错率,值越小,需要拼得越精确
    }

    //#endregion

    //#region 子父传递事件
    async function EventGlobal() {
      data.currentGlobalData.$bus.on("openSlideVerify", (boolValue) => {
        if (data.isOpen) return;
        data.isOpen = boolValue;
        Reset();
      });
    }

    // 成功总事件
    function SuccessCallback() {
      context.emit("successCallback");
    }
    // 失败总事件
    function FailCallback() {
      context.emit("failCallback");
    }

    //#endregion

    return {
      ...toRefs(data),
      ...methods,
    };
  },
};
</script>

  完整代码

<template>
  <!--滑块验证的包裹-->
  <div class="slide-authCode-wrap" v-show="isOpen">
    <!--箭头-->
    <div class="arrow"></div>
    <!--关闭-->
    <div class="close" @click="Close">
      <span class="iconfont icon-chacha"></span>
    </div>

    <!--滑块主要容器-->
    <div class="validate-wrap">
      <!--header 头部-->
      <div class="refresh">
        <div class="refresh-text">完成拼图验证</div>
        <!--刷新区域-->
        <div class="refresh-icon" @click="Refresh">
          <!--刷新按钮Icon-->
          <span class="icon iconfont icon-gengxin" ref="iconRotate"></span>
          <span>换一张</span>
        </div>
      </div>
      <!--滑块区域-->
      <div class="slider-main-container">
        <!-- 画布容器Box -->
        <div id="captcha" ref="captcha" style="position: relative">
          <!-- 画布bg -->
          <canvas ref="canvas_bg" width="364" height="142"
            >浏览器版本过低,请升级浏览器
          </canvas>
          <!-- 滑块box -->
          <canvas ref="blockBox" width="364" height="142" class="block"
            >浏览器版本过低,请升级浏览器
          </canvas>
          <!--显示的图片-->
          <img ref="img" src="" style="display: none" width="0" height="0" />
          <!-- <img
            ref="img"
            src="./images/722-300x150.jpg"
            width="0"
            height="0"
            style="display: none"
          /> -->
          <!-- 拖动容器Box -->
          <div
            class="slider-container"
            :class="slideVerifyStatus === 4 ? 'slider-container-fail' : ''"
          >
            <div class="slide-bg">
              <div class="left"></div>
              <div class="center">拖动滑块完成拼图,进行账号验证</div>
              <div class="right"></div>
            </div>
            <!-- 拖动遮罩 -->
            <div ref="slider_mask" class="slider-mask">
              <!-- 拖动块 -->
              <div
                ref="slider"
                class="slider"
                @mousedown="SliderMousedownEvent"
              >
                <!-- 拖动Icon -->
                <span
                  class="slider-icon iconfont"
                  :class="[
                    slideVerifyStatus === 0 && 'icon-tubiao-xiaoshou',
                    slideVerifyStatus === 2 && 'icon-gouxuan',
                    slideVerifyStatus === 4 && 'icon-close',
                  ]"
                >
                </span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import {
  computed,
  getCurrentInstance,
  onBeforeUnmount,
  onMounted,
  reactive,
  ref,
  toRefs,
} from "vue";
//第三方模块
import throttle from "lodash/throttle"; // 引入节流会防抖插件
import { ElMessage } from "element-plus";
//自定义模块
import { mapActionsFun } from "@/hooks/VueX";
//获取两个值之间的随机数
import { GetRandomNumberByRange } from "@/utils/Random";
// //回去两个值之间的随机数
// function GetRandomNumberByRange(start, end) {
//     return Math.round(Math.random() * (end - start) + start);
// }
import GetGlobalData from "@/utils/Global/GetGlobalData";

export default {
  name: "SliderVerification",
  emits: ["successCallback", "failCallback"],
  setup(props, context) {
    const data = reactive({
      ll: 0,
      slideVerifyStatus: 0, //控制滑块的状态Icon
      isOpen: false,
    });

    const slidingData = {
      l: 35, //去掉突出的部分滑块总边长
      r: 7.5, //滑块突出的小圆圈半径
      w: 364, //canvas宽度
      h: 142, //canvas高度
      PI: Math.PI, //2PI = 360
      ll: 0, //滑块的实际边长(包括突出部分)
    };
    data.ll = computed(() => {
      return slidingData.l + slidingData.r * 2; //滑块的实际边长(包括突出部分)
    });
    const thisData = {
      x: 0,
      y: 0,
      captcha: null,
      canvas_bg: null,
      blockBox: null,
      img: null,
      slider_mask: null,
      slider: null,
      url: "",
      iconRotate: null,
      currentGlobalData: null,
    };
    const ctx = {
      canvasCtx: null,
      blockBoxCtx: null,
    };
    const methods = {
      Refresh: null,
      close: null,
      SliderMousedownEvent: null,
    };
    const eventData = {
      originX: 0,
      originY: 0,
      trail: [],
      isMouseDown: false,
    };

    //#region 生命周期
    const currentInstance = getCurrentInstance();
    data.currentGlobalData = GetGlobalData();
    onMounted(async () => {
      await GetElement();
      await EventGlobal();
    });

    onBeforeUnmount(() => {
      data.currentGlobalData.$bus.all.delete("openSlideVerify");
    });

    //#endregion

    //#region 封装方法
    const GetData = throttle(async () => {
      try {
        // Ajax请求 获取图片
        thisData.url = (
          await mapActionsFun(["getVerifySlideImg"]).getVerifySlideImg()
        ).url;
        thisData.img.src = thisData.url;
        await DrawInitialize();
        await InitImg();
      } catch (e) {
        ElMessage({
          showClose: true,
          dangerouslyUseHTMLString: true,
          type: "error",
          message: e,
          duration: 1500,
        });
      }
    }, 1500);

    // 获取需要的元素
    const GetElement = async () => {
      thisData.captcha = currentInstance.proxy.$refs.captcha;
      thisData.canvas_bg = currentInstance.proxy.$refs.canvas_bg;
      thisData.blockBox = currentInstance.proxy.$refs.blockBox;
      thisData.img = currentInstance.proxy.$refs.img;
      thisData.slider_mask = currentInstance.proxy.$refs.slider_mask;
      thisData.slider = currentInstance.proxy.$refs.slider;
      thisData.iconRotate = currentInstance.proxy.$refs.iconRotate;
      // alpha(boolean):表示canvas是否包含一个alpha通道,设为false则浏览器知道背景永远不透明,能加速对于透明场景和图像的绘制。
      // willReadFrequently(Boolean):表示是否计划有大量的回读操作,频繁调用getImageData()方法时能节省内存,仅Gecko内核浏览器支持。
      // storage(String):声明使用的storage类型,默认为”persistent”。
      ctx.canvasCtx = thisData.canvas_bg.getContext("2d", {
        willReadFrequently: true,
      });
      ctx.blockBoxCtx = thisData.blockBox.getContext("2d", {
        willReadFrequently: true,
      });
    };
    // 绘画方法
    const DrawInitialize = () => {
      thisData.x = GetRandomNumberByRange(
        data.ll + 10,
        slidingData.w - (data.ll + 10)
      );
      thisData.y = GetRandomNumberByRange(
        slidingData.r * 2 + 10,
        slidingData.h - (data.ll + 10)
      );
      // fill 通过填充路径的内容区域生成实心的图形
      // clip 把已经创建的路径转换成裁剪路径。裁剪路径的作用是遮罩。只显示裁剪路径内的区域,裁剪路径外的区域会被隐藏。
      // 注意:clip()只能遮罩在这个方法调用之后绘制的图像,如果是clip()方法调用之前绘制的图像,则无法实现遮罩。
      Draw(ctx.canvasCtx, "fill", thisData.x, thisData.y);
      Draw(ctx.blockBoxCtx, "clip", thisData.x, thisData.y);
    };

    // 绘制  2D渲染,渲染方式,坐标(X,Y)
    function Draw(ctx, operation, x, y) {
      // ★前提要创建Canvas 否者无法绘画
      // 新建一条路径,路径一旦创建成功,图形绘制命令被指向到路径上生成路径
      ctx.beginPath();
      // 把画笔移动到指定的坐标(x, y)。相当于设置路径的起始点坐标。
      ctx.moveTo(x, y);
      //(绘制一条从当前位置到指定坐标x,y)的直线. 假如 x 60~304 y 25~82
      ctx.lineTo(x + slidingData.l / 2, y);
      // 绘制圆弧 x, y, r, startAngle, endAngle, anticlockwise 六个参数
      // 以(x, y)为圆心,以r为半径,从 startAngle弧度开始到endAngle弧度结束。anticlosewise是布尔值,true表示逆时针,false表示顺时针。(默认是顺时针)
      // 注意1 这里的度数都是弧度 👉 0弧度是指的x轴正方形
      // 注意2 arc绘制的坐标是从最开始的位置计算的
      ctx.arc(
        x + slidingData.l / 2,
        y - slidingData.r,
        slidingData.r,
        0,
        2 * slidingData.PI
      );
      // 回到原来起步画圆的位置
      ctx.lineTo(x + slidingData.l / 2, y);
      // 半径 直径是指 L
      // 向右再走 直径 位置
      ctx.lineTo(x + slidingData.l, y);
      // 向右再走 半径 位置
      ctx.lineTo(x + slidingData.l, y + slidingData.l / 2);
      // arc方法 须从最开始的位置计算 走到最右边 再向右走半径位置 向下走半径位置
      ctx.arc(
        x + slidingData.l + slidingData.r,
        y + slidingData.l / 2,
        slidingData.r,
        0,
        2 * slidingData.PI
      );
      ctx.lineTo(x + slidingData.l, y + slidingData.l / 2);
      ctx.lineTo(x + slidingData.l, y + slidingData.l);
      ctx.lineTo(x, y + slidingData.l);
      ctx.lineTo(x, y);
      ctx.fillStyle = "#fff";
      ctx[operation]();
      ctx.beginPath();
      ctx.arc(
        x,
        y + slidingData.l / 2,
        slidingData.r,
        1.5 * slidingData.PI,
        0.5 * slidingData.PI
      );
      // 合成 xor属性作用:重叠部分变透明
      ctx.globalCompositeOperation = "xor";
      ctx.fill();
    }

    // 初始化图像
    const InitImg = () => {
      ctx.canvasCtx.drawImage(thisData.img, 0, 0, slidingData.w, slidingData.h);
      ctx.blockBoxCtx.drawImage(
        thisData.img,
        0,
        0,
        slidingData.w,
        slidingData.h
      );
      const y = thisData.y - slidingData.r * 2; //减去突出圆的大小
      // 参数 获取那块区域坐标x,y 宽
      const imageData = ctx.blockBoxCtx.getImageData(
        thisData.x,
        y,
        data.ll,
        data.ll
      );
      thisData.blockBox.width = data.ll;
      // 后面两个参数从哪里放上去
      ctx.blockBoxCtx.putImageData(imageData, 0, y);
    };

    // 绑定事件 刷新
    let index = ref(0);
    methods.Refresh = throttle(() => {
      index.value++;
      thisData.iconRotate.style.rotate = -360 * index.value + "deg";
      Reset();
    }, 2000);
    // 关闭滑块验证
    methods.Close = () => {
      data.isOpen = false;
    };

    // ----------------按着---------------
    methods.SliderMousedownEvent = (e) => {
      eventData.originX = e.x;
      eventData.originY = e.y;
      eventData.isMouseDown = true;
    };
    // ----------------拖动---------------
    document.addEventListener("mousemove", (e) => {
      if (!eventData.isMouseDown) return false;
      const moveY = e.y - eventData.originY;
      const moveX = e.x - eventData.originX;
      // 判断时候超出或者小于0
      if (moveX < 0 || moveX + 40 >= slidingData.w) return false;
      // 拖动按钮位置
      thisData.slider.style.left = moveX + "px";
      // 拖动填充滑块位置
      const blockLeft = moveX;
      thisData.blockBox.style.left = blockLeft + "px";
      // 遮罩宽长度
      thisData.slider_mask.style.width = moveX + 40 + "px";
      // 添加位置
      eventData.trail.push(moveY);
    });
    // ----------------抬起---------------
    document.addEventListener("mouseup", () => {
      if (!eventData.isMouseDown) return false;
      else eventData.isMouseDown = false;
      // 验证位置;
      const spliced = Verify();
      if (spliced) {
        // 添加成功
        data.slideVerifyStatus = 2;
        SuccessCallback();
        methods.Close();
      } else {
        // 添加失败样式
        data.slideVerifyStatus = 4;
        FailCallback();
        setTimeout(() => {
          Reset();
        }, 1000);
      }
    });

    // 清除
    function CleanCtx() {
      ctx.canvasCtx.clearRect(0, 0, slidingData.w, slidingData.h);
      ctx.blockBoxCtx.clearRect(0, 0, slidingData.w, slidingData.h);
      thisData.blockBox.width = slidingData.w;
    }

    // 重置
    async function Reset() {
      data.slideVerifyStatus = 0;
      thisData.slider.style.left = 0;
      thisData.blockBox.style.left = 0;
      thisData.slider_mask.style.width = 0;
      await CleanCtx();
      await GetData();
    }

    // 验证
    function Verify() {
      const left = parseInt(thisData.blockBox.style.left);
      return Math.abs(left - thisData.x) < 1; //10表示容错率,值越小,需要拼得越精确
    }

    //#endregion

    //#region 子父传递事件
    async function EventGlobal() {
      data.currentGlobalData.$bus.on("openSlideVerify", (boolValue) => {
        if (data.isOpen) return;
        data.isOpen = boolValue;
        Reset();
      });
    }

    // 成功总事件
    function SuccessCallback() {
      context.emit("successCallback");
    }
    // 失败总事件
    function FailCallback() {
      context.emit("failCallback");
    }

    //#endregion

    return {
      ...toRefs(data),
      ...methods,
    };
  },
};
</script>

<style scoped lang="less">
//滑块验证
.slide-authCode-wrap {
  position: absolute;
  left: 0;
  z-index: 110;
  bottom: 65px;
  width: 364px;
  height: 216.5px;
  padding: 12px 12px 12px 20px;
  border: 1px solid #eee;
  box-shadow: 0 0 2px 2px #eee;
  background-color: #fff;

  //关闭验证
  .close {
    cursor: pointer;
    z-index: 100;
    position: absolute;
    right: 10px;
    top: 10px;
    display: block;
    width: 20px;
    height: 20px;
    line-height: 20px;

    span {
      font-size: 20px;

      &:active {
        color: #a4a4a4;
      }
    }
  }

  //箭头
  .arrow {
    display: block;
    position: absolute;
    background-image: url("./images/tips.gif");
    background-repeat: no-repeat;
    width: 16px;
    height: 8px;
    background-position: 0 -8px;
    overflow: hidden;
    bottom: -8px;
    left: 190px;
  }

  //滑块主要容器
  .validate-wrap {
    //提醒区域
    .refresh {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding-right: 30px;
      font-family: Helvetica, Tahoma, Arial, "Microsoft YaHei", "微软雅黑",
        sans-serif;

      .refresh-text {
        font-size: 15px;
        color: #666;
      }

      .refresh-icon {
        cursor: pointer;
        color: #06c;

        .icon {
          display: inline-block;
          margin-right: 4px;
          vertical-align: revert;
          transition: 0.6s linear;
        }

        span {
          font-size: 15px;
        }
      }
    }

    //滑块组要容器
    .slider-main-container {
      margin-top: 5px;
      //画布容器Box
      #captcha {
        display: flex;
        justify-content: center;
        flex-direction: column;
        /* 小拼图 */

        .block {
          position: absolute;
          left: 0;
          top: 0;
        }

        /* 滑动条 */

        .slider-container {
          position: relative;
          margin: 10px auto 0;
          opacity: 1;
          font-size: 14px;
          visibility: visible;
          width: 364px;
          height: 40px;
          line-height: 40px;
          text-align: center;
          color: #05a4ea;

          //滑块Bg
          .slide-bg {
            .left {
              float: left;
              width: 40px;
              height: 40px;
              background: url("./images/slide-left-icon2.png") no-repeat;
            }

            .center {
              background-image: url("./images/slide-center-bg.png");
              margin-left: 40px;
              margin-right: 40px;
              overflow: hidden;
              white-space: nowrap;
              user-select: none;
              -moz-user-select: none;
              -ms-user-select: none;
              -webkit-user-select: none;
            }

            .right {
              width: 40px;
              height: 40px;
              background: url("./images/slide-right-icon2.png") no-repeat;
              position: absolute;
              right: 0;
              top: 0;
            }
          }

          /* 拖动遮罩容器 */

          .slider-mask {
            position: absolute;
            top: 0;
            left: 0;
            width: 364px;
            height: 40px;
            border-radius: 36px;
            border: 0px solid #1991fa;
            background: linear-gradient(#33b5fb, #8fdfff);
          }

          /* 拖动块 */

          .slider {
            position: absolute;
            left: -3px;
            top: -3px;
            width: 45px;
            height: 45px;
            background: #fff;
            box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
            cursor: pointer;
            transition: background 0.2s linear;
            border-radius: 50%;
          }

          .slider:hover {
            background: #aed6ff;
            color: #06c;
          }

          /* 拖动Icon */

          .slider-icon {
            font-size: 25px;
            font-weight: 700;
            vertical-align: middle;
          }
        }

        //活动状态CSS

        /* 滑动条失败态 */

        .slider-container-fail {
          .slider-mask {
            background: linear-gradient(#ff5e5e, #ffb3b3);
          }

          .slider {
            padding-top: 2px;
            box-sizing: border-box;
          }

          .slider-icon {
            color: red;
          }
        }
      }
    }
  }
}
</style>

  实现效果

在这里插入图片描述

总结

以上是个人学习Vue的相关知识点,一点一滴的记录了下来,有问题请评论区指正,共同进步,这才是我写文章的原因之,如果这篇文章对您有帮助请三连支持一波

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

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

相关文章

2023年6月DAMA-CDGP数据治理专家认证,来这里

DAMA认证为数据管理专业人士提供职业目标晋升规划&#xff0c;彰显了职业发展里程碑及发展阶梯定义&#xff0c;帮助数据管理从业人士获得企业数字化转型战略下的必备职业能力&#xff0c;促进开展工作实践应用及实际问题解决&#xff0c;形成企业所需的新数字经济下的核心职业…

Windows批处理文件倒计时且循环执行文件/程序

Windows批处理文件倒计时且循环执行文件/程序&#xff1a; 最近想循环测试下语音唤醒设备&#xff0c;所以需要用bat文件在Windows上一直循环播放指定的mp3文件&#xff0c;且设置了间隔时间&#xff0c;也就是倒计时时间&#xff0c;最后网上查了一堆之后整理了一个bat&#x…

微信小程序开发中遇到的坑

目录 1、clearInterval不起作用 2、设置background: linear-gradient(180deg, #FCF8F5 0%, #FCF8F5 99.9%, transparent 100%);解决元素底部有黑线的问题。但是在ios中不起作用。 3、wx.createAnimation&#xff0c;设置的动画只能执行一次 4、swiper在苹果手机上显示不全&…

C#,码海拾贝(33)——约化“一般实矩阵”为“赫申伯格矩阵”的“初等相似变换法”之C#源代码

using System; namespace Zhou.CSharp.Algorithm { /// <summary> /// 矩阵类 /// 作者&#xff1a;周长发 /// 改进&#xff1a;深度混淆 /// https://blog.csdn.net/beijinghorn /// </summary> public partial class Matrix {…

【React】脚手架,组件化开发,类组件/函数式组件,声明周期,组件的嵌套,子父传递,插槽,Context,事件总线,setState原理

❤️ Author&#xff1a; 老九 ☕️ 个人博客&#xff1a;老九的CSDN博客 &#x1f64f; 个人名言&#xff1a;不可控之事 乐观面对 &#x1f60d; 系列专栏&#xff1a; 文章目录 脚手架目录结构组件化开发类组件函数式组件 声明周期组件的嵌套组件之间的通信插槽 Context事件…

vscode实现代码片段快捷输入设置

1.编写想要的代码片段 <template> <div>AppContent</div> </template> <script> export default { } </script> <style scoped> </style> 2.打开网站:snippet generator 如下图添加描述,快捷键和代码片段.右边会有生成内容 …

93.构建样品餐部分第一节

之前我们实现得页面是这个样子的 现在让我们来完成剩下的样品餐部分吧&#xff01; ● 大致实现的页面是这样的 ● 让我们先简单的生成这些框架 <section class"section-meals"><div class"container"><span class"subheading&qu…

SpringCloud Alibaba Seata配置到Nacos

SpringCloud Alibaba Seata 1 Seata 简介 单体应用被拆分成微服务应用&#xff0c;原来的三个模块被拆分成三个独立的应用&#xff0c;分别使用 三个独立的数据源业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保 证但是全局的数据—致性问题没法…

H5嵌入原生开发小结----兼容安卓与ios的填坑之路

一开始听说开发H5&#xff0c;以为就是做适配现代浏览器的移动网页&#xff0c;心想不用管IE了&#xff0c;欧也。到今天&#xff0c;发现当初too young too simple&#xff0c;兼容IE和兼容安卓与IOS&#xff0c;后者让你更抓狂。接下来数一下踩过的坑。主要分UI展示&#xff…

如何借助AI,产品文案语音配图片一键生成视频

const name "AI生成视频";console.log(name); 以前我们做视频都是要找素材、剪辑、配音&#xff0c;太费时间了&#x1f629;&#xff0c;现在只需要通过AI&#xff0c;输入文字&#xff0c;它就能自动帮我们生成一段有声有色的视频。 我们来看看文本生成的视频效…

案例35:基于Springboot图书商城管理系统开题报告设计

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

面向流媒体的确定时延传输:从QUIC出发,走向未来

QUIC&#xff08;Quick UDP Internet Connections&#xff09;是Google设计的一套可靠UDP传输协议&#xff0c;旨在为HTTP提供一个安全、可靠、高效和低延时的通信基础。QUIC协议已被IETF采纳为标准&#xff0c;并且HTTP/3已选择使用QUIC来代替TCP作为其传输层协议。LiveVideoS…

python怎么退出执行/退出程序语句

python怎么退出执行/退出程序语句 文章目录 python怎么退出执行/退出程序语句sys.exit()函数 raise SystemExit()异常os._exit()函数CtrlC中断程序执行具体情况具体处理参考资料 在Python中&#xff0c;退出执行是一个常见的操作。退出方法介绍&#xff1a; sys.exit()函数 sy…

最近几年,国内好多家实体企业都开始用上低代码了,它有什么好?

前言&#xff1a; 裹挟大数据、云计算、人工智能等数字技术的第四次工业革命浪潮正加速来袭&#xff0c;全球经济已行至历史的十字路口。 站上技术浪潮潮头者澎湃生长&#xff0c;错过技术浪潮者黯然败退。那么&#xff0c;对于中国的普通制造企业来说&#xff0c;如何抓住向…

【媒体广告】的现状与未来发展趋势!

媒体广告是一种重要的市场推广手段&#xff0c;随着技术的不断发展和市场环境的变化&#xff0c;媒体广告也在不断地演变和发展。本文将从以下几个方面探讨媒体广告的发展趋势。 一、数字化、数据化和智能化趋势 随着互联网和移动互联网技术的发展&#xff0c;数字化、数据化…

2.1 初探MyBatis实现简单查询

一、创建数据库与表 1、创建数据库 在Navicat里创建MySQL数据库 - testdb&#xff0c;采用utf8mb4字符集 2、创建用户表 用户表 - t_user CREATE TABLE t_user (id int(11) NOT NULL AUTO_INCREMENT,name varchar(50) DEFAULT NULL,age int(11) DEFAULT NULL,address var…

LLM-Pruner: 剪枝+少量数据+少量训练 = 高效的LLM压缩

概要 大语言模型&#xff08;LLMs, Large Language Models&#xff09;在各种任务上展现出了惊人的能力&#xff0c;这些能力很大程度上来自于模型庞大的模型规模以及海量的训练语料。为了应对这些模型部署上存在的挑战&#xff0c;许多研究者开始关注大语言模型的轻量化问题。…

华为认证 | HCIE-存储 V3.0 即将发布!

华为认证HCIE-Storage V3.0&#xff08;中文版&#xff09;预计将于2023年6月30日正式对外发布。为了帮助您做好学习、培训和考试计划&#xff0c;现进行预发布通知&#xff0c;请您关注。 01 发布概述 基于“平台生态”战略&#xff0c;围绕“云-管-端”协同的新ICT技术架构&…

5周年更新 | OpenVINO™  2023.0,让AI部署和加速更容易

时光匆匆&#xff0c;岁月荏苒&#xff0c;OpenVINO™迎来了5岁生日。5岁&#xff0c;对于OpenVINO™来说还是个很年轻的年纪&#xff0c;一如正在茁壮成长的少年&#xff0c;每天都迸发着无穷的生命力。 在这5年里&#xff0c;OpenVINO™密切关注市场需求&#xff0c;着眼未来…

JavaScript拖动元素在一个范围内移动

基于 jQuery移动范围由 div 搭建(div 模仿表格)&#xff0c;卡片的移动不允许超出该范围移动卡片会有一个淡蓝色卡片的标记出将要放置的位置有禁止放置标记的位置&#xff0c;不允许卡片放置&#xff08;会放到前一个可放置的位置&#xff09;卡片放置会覆盖单元格中的文字卡片…