Java实现滑块拼图验证码校验

1.后端-获取滑块实现思路
1.在若干原图中随机一张原图,生成滑块拼图验证码信息。
2.随机一个token,把(X,Y)坐标值保存到 Redis中,方便做效验
3.返回前端,两个Base64字符串图片信息,Y坐标值和 token。

2、后端-校验滑块拼图验证码接口

获取前端传入的 moveX坐标值和 token。
通过token,获取 Redis中的 (X,Y)坐标值,注意有效期。
校验 moveX与X的差的绝对值是否在阈值误差范围内(比如阈值误差为5)。
在阈值误差范围内,返回前端验证通过,否则返回前端验证不通过。

效果如图

在这里插入图片描述

package com.icourt.lawtrust.utils;

import com.icourt.lawtrust.module.login.dto.CaptchaDto;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.math.RandomUtils;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.StopWatch;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.List;
import java.util.*;

/**
 * @author syl
 * @title PuzzleCaptchaUtil
 * @description 拼图验证码工具类
 * @date 2020-11-12 15:51
 */
@Slf4j
public class PuzzleCaptchaUtil {

    /*网络图片地址*/
    private final static String IMG_URL = "";

    /*本地图片地址*/
    private static final String IMG_PATH = "image/picture/captcha/%s.png";


    /**
     * @param captchaDto
     * @author syl
     * @description 获取验证码拼图(生成的抠图和带抠图阴影的大图及抠图坐标)
     * @date 2022-1-7 10:32
     */
    public static CaptchaDto getCaptcha(CaptchaDto captchaDto) {
        //参数校验
        checkCaptcha(captchaDto);
        //获取画布的宽高
        int canvasWidth = captchaDto.getCanvasWidth();
        int canvasHeight = captchaDto.getCanvasHeight();
        //获取阻塞块的宽高/半径
        int blockWidth = captchaDto.getBlockWidth();
        int blockHeight = captchaDto.getBlockHeight();
        int blockRadius = captchaDto.getBlockRadius();
        //获取资源图
        BufferedImage canvasImage = getBufferedImage(captchaDto.getPlace());
        //调整原图到指定大小
        canvasImage = imageResize(canvasImage, canvasWidth, canvasHeight);
        //随机生成阻塞块坐标
        int blockX = getNonceByRange(blockWidth, canvasWidth - blockWidth - 10);
        int blockY = getNonceByRange(10, canvasHeight - blockHeight + 1);
        //阻塞块
        BufferedImage blockImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
        //新建的图像根据轮廓图颜色赋值,源图生成遮罩
        cutByTemplate(canvasImage, blockImage, blockWidth, blockHeight, blockRadius, blockX, blockY);
        //缓存移动横坐标
        String ticket = UUID.randomUUID().toString().replaceAll("-", "");
        //设置返回参数
        captchaDto.setTicket(ticket);
        captchaDto.setBlockY(blockY);
        captchaDto.setBlockX(blockX);
        captchaDto.setBlockSrc(toBase64(blockImage, "png"));
        captchaDto.setCanvasSrc(toBase64(canvasImage, "png"));
        return captchaDto;
    }

    /**
     * @param captchaDto
     * @return void
     * @author syl
     * @description 入参校验设置默认值
     * @date 2022-1-7 10:49
     */
    private static void checkCaptcha(CaptchaDto captchaDto) {
        //设置画布宽度默认值
        if (captchaDto.getCanvasWidth() == null) {
            captchaDto.setCanvasWidth(320);
        }
        //设置画布高度默认值
        if (captchaDto.getCanvasHeight() == null) {
            captchaDto.setCanvasHeight(155);
        }
        //设置阻塞块宽度默认值
        if (captchaDto.getBlockWidth() == null) {
            captchaDto.setBlockWidth(65);
        }
        //设置阻塞块高度默认值
        if (captchaDto.getBlockHeight() == null) {
            captchaDto.setBlockHeight(55);
        }
        //设置阻塞块凹凸半径默认值
        if (captchaDto.getBlockRadius() == null) {
            captchaDto.setBlockRadius(9);
        }
        //设置图片来源默认值
        if (captchaDto.getPlace() == null) {
            captchaDto.setPlace(0);
        }
    }

    /**
     * @param start
     * @param end
     * @return int
     * @author syl
     * @description 获取指定范围内的随机数
     * @date 2022-1-7 9:41
     */
    public static int getNonceByRange(int start, int end) {
        Random random = new Random();
        return random.nextInt(end - start + 1) + start;
    }

    /**
     * @param place
     * @return java.awt.image.BufferedImage
     * @author syl
     * @description 获取验证码资源图
     * @date 2022-1-7 10:48
     */
    private static BufferedImage getBufferedImage(Integer place) {
        try {
            //随机图片
            int nonce = getNonceByRange(0, 1000);
            //获取网络资源图片
            if (0 == place) {
                StopWatch stopWatch = new StopWatch();
                stopWatch.start();
                String imgUrl = String.format(IMG_URL, nonce);
                URL url = new URL(imgUrl);
                BufferedImage read = ImageIO.read(url.openStream());
                stopWatch.stop();
                log.info(stopWatch.prettyPrint());
                return read;
            }
            //获取本地图片
            else {
                int randNum = new Random().nextInt(12) + 1;
                ClassLoader loader = Thread.currentThread().getContextClassLoader();
                if (loader == null) {
                    loader = ClassLoader.getSystemClassLoader();
                }
                InputStream is = loader.getResourceAsStream(String.format(IMG_PATH, randNum));
                BufferedImage read = ImageIO.read(is);
                return read;
            }
        } catch (Exception e) {
            log.error("获取拼图资源失败", e);
        }
        return null;
    }

    /**
     * 获取图片,由于spring boot打包成jar之后,获取到获取不到resources里头的图片,对此进行处理
     *
     * @param path
     * @return
     * @author syl
     * @date 2022年1月6日
     */
    public static List<File> queryFileList(String path) {

        //获取容器资源解析器
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        List<File> filelist = new ArrayList<File>();
        // 获取远程服务器IP和端口
        try {
            //获取所有匹配的文件
            Resource[] resources = resolver.getResources(path);

            for (Resource resource : resources) {
                //获得文件流,因为在jar文件中,不能直接通过文件资源路径拿到文件,但是可以在jar包中拿到文件流
                InputStream stream = resource.getInputStream();
                String targetFilePath = resource.getFilename();
                File ttfFile = new File(targetFilePath);
                FileUtils.copyInputStreamToFile(stream, ttfFile);
                filelist.add(ttfFile);
            }
        } catch (Exception e) {
            log.error("解析失败", e);
//            e.printStackTrace();
        }
        return filelist;
    }

    /**
     * @param bufferedImage
     * @param width
     * @param height
     * @return java.awt.image.BufferedImage
     * @author syl
     * @description 调整图片大小
     * @date 2022-1-7 9:41
     */
    public static BufferedImage imageResize(BufferedImage bufferedImage, int width, int height) {
        Image image = bufferedImage.getScaledInstance(width, height, Image.SCALE_SMOOTH);
        BufferedImage resultImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D graphics2D = resultImage.createGraphics();
        graphics2D.drawImage(image, 0, 0, null);
        graphics2D.dispose();
        return resultImage;
    }

    /**
     * @param canvasImage
     * @param blockImage
     * @param blockWidth
     * @param blockHeight
     * @param blockRadius
     * @param blockX
     * @param blockY
     * @return void
     * @author syl
     * @description 抠图,并生成阻塞块
     * @date 2022-1-7 9:54
     */
    private static void cutByTemplate(BufferedImage canvasImage, BufferedImage blockImage, int blockWidth, int blockHeight, int blockRadius, int blockX, int blockY) {
        BufferedImage waterImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
        //阻塞块的轮廓图
        int[][] blockData = getBlockData(blockWidth, blockHeight, blockRadius);
        //创建阻塞块具体形状
        for (int i = 0; i < blockWidth; i++) {
            for (int j = 0; j < blockHeight; j++) {
                try {
                    //原图中对应位置变色处理
                    if (blockData[i][j] == 1) {
                        //背景设置为黑色
                        waterImage.setRGB(i, j, Color.BLACK.getRGB());
                        blockImage.setRGB(i, j, canvasImage.getRGB(blockX + i, blockY + j));
                        //轮廓设置为白色,取带像素和无像素的界点,判断该点是不是临界轮廓点
                        if (blockData[i + 1][j] == 0 || blockData[i][j + 1] == 0 || blockData[i - 1][j] == 0 || blockData[i][j - 1] == 0) {
                            blockImage.setRGB(i, j, Color.WHITE.getRGB());
                            waterImage.setRGB(i, j, Color.WHITE.getRGB());
                        }
                    }
                    //这里把背景设为透明
                    else {
                        blockImage.setRGB(i, j, Color.TRANSLUCENT);
                        waterImage.setRGB(i, j, Color.TRANSLUCENT);
                    }
                } catch (ArrayIndexOutOfBoundsException e) {
                    //防止数组下标越界异常
                }
            }
        }
        //在画布上添加阻塞块水印
        addBlockWatermark(canvasImage, waterImage, blockX, blockY);
    }

    /**
     * @param blockWidth
     * @param blockHeight
     * @param blockRadius
     * @return int[][]
     * @author syl
     * @description 构建拼图轮廓轨迹
     * @date 2022-1-7 9:53
     */
    private static int[][] getBlockData(int blockWidth, int blockHeight, int blockRadius) {
        int[][] data = new int[blockWidth][blockHeight];
        double po = Math.pow(blockRadius, 2);
        //随机生成两个圆的坐标,在4个方向上 随机找到2个方向添加凸/凹
        //凸/凹1
        int face1 = RandomUtils.nextInt(4);
        //凸/凹2
        int face2;
        //保证两个凸/凹不在同一位置
        do {
            face2 = RandomUtils.nextInt(4);
        } while (face1 == face2);
        //获取凸/凹起位置坐标
        int[] circle1 = getCircleCoords(face1, blockWidth, blockHeight, blockRadius);
        int[] circle2 = getCircleCoords(face2, blockWidth, blockHeight, blockRadius);
        //随机凸/凹类型
        int shape = getNonceByRange(0, 1);
        //圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心(a,b),半径为r的圆
        //计算需要的小图轮廓,用二维数组来表示,二维数组有两张值,0和1,其中0表示没有颜色,1有颜色
        for (int i = 0; i < blockWidth; i++) {
            for (int j = 0; j < blockHeight; j++) {
                data[i][j] = 0;
                //创建中间的方形区域
                if ((i >= blockRadius && i <= blockWidth - blockRadius && j >= blockRadius && j <= blockHeight - blockRadius)) {
                    data[i][j] = 1;
                }
                double d1 = Math.pow(i - Objects.requireNonNull(circle1)[0], 2) + Math.pow(j - circle1[1], 2);
                double d2 = Math.pow(i - Objects.requireNonNull(circle2)[0], 2) + Math.pow(j - circle2[1], 2);
                //创建两个凸/凹
                if (d1 <= po || d2 <= po) {
                    data[i][j] = shape;
                }
            }
        }
        return data;
    }

    /**
     * @param face
     * @param blockWidth
     * @param blockHeight
     * @param blockRadius
     * @return int[]
     * @author syl
     * @description 根据朝向获取圆心坐标
     * @date 2022-1-7 9:50
     */
    private static int[] getCircleCoords(int face, int blockWidth, int blockHeight, int blockRadius) {
        //上
        if (0 == face) {
            return new int[]{blockWidth / 2 - 1, blockRadius};
        }
        //左
        else if (1 == face) {
            return new int[]{blockRadius, blockHeight / 2 - 1};
        }
        //下
        else if (2 == face) {
            return new int[]{blockWidth / 2 - 1, blockHeight - blockRadius - 1};
        }
        //右
        else if (3 == face) {
            return new int[]{blockWidth - blockRadius - 1, blockHeight / 2 - 1};
        }
        return null;
    }

    /**
     * @param canvasImage
     * @param blockImage
     * @param x
     * @param y
     * @return void
     * @author syl
     * @description 在画布上添加阻塞块水印
     * @date 2022-1-7 11:18
     */
    private static void addBlockWatermark(BufferedImage canvasImage, BufferedImage blockImage, int x, int y) {
        Graphics2D graphics2D = canvasImage.createGraphics();
        graphics2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.8f));
        graphics2D.drawImage(blockImage, x, y, null);
        graphics2D.dispose();
    }

    /**
     * @param bufferedImage
     * @param type
     * @return java.lang.String
     * @author syl
     * @description BufferedImage转BASE64
     * @date 2022-1-7 9:41
     */
    public static String toBase64(BufferedImage bufferedImage, String type) {
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            ImageIO.write(bufferedImage, type, byteArrayOutputStream);
            String base64 = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
            return String.format("data:image/%s;base64,%s", type, base64);
        } catch (IOException e) {
            log.error("图片资源转换BASE64失败", e);

        }
        return null;
    }
}
  1. 返回前端滑块的信息,带一个ticket,redis缓存BlockX
CaptchaDto captcha = PuzzleCaptchaUtil.getCaptcha(captchaDto);
        /*
         **缓存验证码
         */
        redisTemplate.opsForValue().set(CAPTCHA_KEY_PREFIX + captcha.getTicket(), String.valueOf(captcha.getBlockX()), captchaExpire, TimeUnit.MINUTES);
       
  1. 效验滑块核心功能
 /**
     * @return com.icourt.lawtrust.module.login.vo.CaptchaVo
     * @author: shaoyingle
     * @desc:检查滑块是否正确
     * @date 2022/1/10 9:35 上午
     */
    @Override
    public void checkCaptcha(String ticket, String moveLength) {
        if (StringUtils.isBlank(ticket)) {
            throw new BusinessException("ticket为空",true);
        }
        if (StringUtils.isBlank(moveLength)) {
            throw new BusinessException("moveLength为空");
        }
        // 校验验证码是否存在
        if (!redisTemplate.hasKey(CAPTCHA_KEY_PREFIX + ticket)) {
            throw new BusinessException("验证码已失效");
        }
        // 获取缓存验证码
        int redisValue = Integer.parseInt(redisTemplate.opsForValue().get(CAPTCHA_KEY_PREFIX + ticket).toString());
        // 一次性的用完就删
        redisTemplate.delete(CAPTCHA_KEY_PREFIX + ticket);
        // 根据移动距离判断验证是否成功
        if (Math.abs(redisValue - Integer.parseInt(moveLength)) > SysLabelConstant.ALLOW_DEVIATION) {
            throw new BusinessException("验证未通过,拖动滑块将悬浮图像正确合并");
        }
    }
package com.icourt.lawtrust.module.login.dto;

import io.swagger.annotations.ApiModel;
import lombok.Data;

/**
 * @author syl
 * @title Captcha
 * @description 验证码拼图
 * @date 2022-1-7
 */
@ApiModel("验证码拼图")
@Data
public class CaptchaDto {

    //随机字符串
    private String ticket;
    //验证值
    private String value;
    //生成的画布的base64
    private String canvasSrc;
    //画布宽度
    private Integer canvasWidth;
    //画布高度
    private Integer canvasHeight;
    //生成的阻塞块的base64
    private String blockSrc;
    //阻塞块宽度
    private Integer blockWidth;
    //阻塞块高度
    private Integer blockHeight;
    //阻塞块凸凹半径
    private Integer blockRadius;
    // 阻塞块的横轴坐标
    private Integer blockX;
    // 阻塞块的纵轴坐标
    private Integer blockY;
    //图片获取位置
    private Integer place;

}

前端实现代码

<template>
  <div
    class="slide-verify"
    :style="{ width: canvasWidth + 'px' }"
    onselectstart="return false;"
  >
    <!-- 图片加载遮蔽罩 -->
    <div
      :class="{ 'img-loading': isLoading }"
      :style="{ height: canvasHeight + 'px' }"
      v-if="isLoading"
    />
    <!-- 认证成功后的文字提示 -->
    <div
      class="success-hint"
      :style="{ height: canvasHeight + 'px' }"
      v-if="verifySuccess"
    >
      {{ successHint }}
    </div>
    <!--刷新按钮-->
    <!-- <div class="refresh-icon" @click="refresh"/> -->
    <!--前端生成-->
    <template v-if="isFrontCheck">
      <!--验证图片-->
      <canvas
        ref="canvas"
        class="slide-canvas"
        :width="canvasWidth"
        :height="canvasHeight"
      />
      <!--阻塞块-->
      <canvas
        ref="block"
        class="slide-block"
        :width="canvasWidth"
        :height="canvasHeight"
      />
    </template>
    <!--后端生成-->
    <template v-else>
      <!--验证图片-->
      <img
        ref="canvas"
        class="slide-canvas"
        :width="canvasWidth"
        :height="canvasHeight"
      />
      <!--阻塞块-->
      <img
        ref="block"
        :class="['slide-block', { 'verify-fail': verifyFail }]"
      />
    </template>
    <!-- <div class="tips success" v-if="isPassing">{{successTip}}</div> -->
    <div class="tips danger" v-if="isPassing">{{ failTip }}</div>
    <!-- 滑动条 -->
    <div
      class="slider"
      :class="{
        'verify-active': verifyActive,
        'verify-success': verifySuccess,
        'verify-fail': verifyFail,
      }"
    >
      <!--滑动条提示文字-->
      <div
        class="slider-hint"
        style="width: 335px; height: 40px; font-size: 14px"
      >
        {{ sliderHint }}
      </div>
      <!--滑块-->
      <div class="slider-box" :style="{ width: sliderBoxWidth }">
        <!-- 按钮 -->
        <div
          class="slider-button"
          id="slider-button"
          :style="{ left: sliderButtonLeft }"
        >
          <!-- 按钮图标 -->
          <div class="el-icon-d-arrow-right" />
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, getCurrentInstance, ref, Ref, defineEmits } from "vue";
import { getCaptcha } from "@/api";
const { proxy }: any = getCurrentInstance();
const emit = defineEmits(["success", "fail"]);
function sum(x, y) {
  return x + y;
}

function square(x) {
  return x * x;
}
const props = defineProps({
  // 阻塞块长度
  blockLength: {
    type: Number,
    default: () => 42,
  },
  // 阻塞块弧度
  blockRadius: {
    type: Number,
    default: () => 10,
  },
  // 画布宽度
  canvasWidth: {
    type: Number,
    default: () => 355,
  },
  // 画布高度
  canvasHeight: {
    type: Number,
    default: () => 188,
  },
  // 滑块操作提示
  sliderHint: {
    type: String,
    default: () => "请按住滑块拖动",
  },
  // 可允许的误差范围小;为1时,则表示滑块要与凹槽完全重叠,才能验证成功。默认值为5,若为 -1 则不进行机器判断
  accuracy: {
    type: Number,
    default: () => 3,
  },
  // 图片资源数组
  imageList: {
    type: Array,
    default: () => [],
  },
  failTip: {
    type: String,
    default: () => "验证未通过,拖动滑块将悬浮图像正确合并",
  },
});

// 前端校验
const isFrontCheck = ref(false);
// 校验进行状态
const verifyActive = ref(false);
// 校验成功状态
const verifySuccess = ref(false);
// 校验失败状态
const verifyFail = ref(false);
// 阻塞块对象
let blockObj: any = null;
// 图片画布对象
let canvasCtx: any = null;
// 阻塞块画布对象
let blockCtx: any = null;
// 阻塞块宽度
const blockWidth = props.blockLength * 2;
// 阻塞块的横轴坐标
let blockX: any = undefined;
// 阻塞块的纵轴坐标
let blockY: any = undefined;
// 图片对象
let image: any = undefined;
// 移动的X轴坐标
let originX: any = undefined;
// 移动的Y轴做坐标
let originY: any = undefined;
// 拖动距离数组
const dragDistanceList: any[] = [];
// 滑块箱拖动宽度
const sliderBoxWidth: any = ref(0);
// 滑块按钮距离左侧起点位置
const sliderButtonLeft: any = ref(0);
// 鼠标按下状态
let isMouseDown: boolean = false;
// 图片加载提示,防止图片没加载完就开始验证
const isLoading = ref(true);
// 时间戳,计算滑动时长
let timestamp: any = null;
// 成功提示
const successHint = ref("");
// 随机字符串
let ticket: any = undefined;
// 是否显示状态文字
let isPassing = ref(false);
const block: any = ref();
const canvas: any = ref();
onMounted(() => {
  init();
});
/* 初始化 */
const init = () => {
  initDom();
  bindEvents();
};
/* 初始化DOM对象 */
const initDom = () => {
  blockObj = block.value;
  if (isFrontCheck.value) {
    canvasCtx = canvas.getContext("2d");
    blockCtx = blockObj.getContext("2d");
    initImage();
  } else {
    getCaptchaApi();
  }
};
/* 后台获取验证码 */
const getCaptchaApi = async () => {
  // 取后端默认值
  const data = {
    canvasWidth: props.canvasWidth,
    canvasHeight: props.canvasHeight,
    blockWidth: blockWidth / 1.5,
    blockHeight: blockWidth / 1.5,
    blockRadius: props.blockRadius,
    place: 1,
  };
  // 获取后台滑块验证码
  await getCaptcha(data)
    .then((response: any) => {
      const data = response.data;
      ticket = data.ticket;
      block.value.src = data.blockSrc;
      block.value.style.top = data.blockY + "px";
      canvas.value.src = data.canvasSrc;
    })
    .finally(() => {
      isLoading.value = false;
    });
};
/* 前端获取验证码 */
const initImage = () => {
  const newImage = createImage(() => {
    drawBlock();
    canvasCtx.drawImage(image, 0, 0, props.canvasWidth, props.canvasHeight);
    blockCtx.drawImage(image, 0, 0, props.canvasWidth, props.canvasHeight);
    // 将抠图防止最左边位置
    let yAxle = blockY - props.blockRadius * 2;
    let ImageData = blockCtx.getImageData(
      blockX,
      yAxle,
      blockWidth,
      blockWidth
    );
    blockObj.width = blockWidth;
    blockCtx.putImageData(ImageData, 0, yAxle);
    // 图片加载完关闭遮蔽罩
    isLoading.value = false;
    // 前端校验设置特殊值
    ticket = "loyer";
  });
  image = newImage;
};
/* 创建image对象 */
const createImage = (onload) => {
  const image: any = document.createElement("img");
  image.crossOrigin = "Anonymous";
  image.onload = onload;
  image.onerror = () => {
    image.src = new URL(
      "../../../assets/images/bgImg.jpg",
      import.meta.url
    ).href;
  };
  image.src = getImageSrc();
  return image;
};
/* 获取imgSrc */
const getImageSrc = () => {
  const len = props.imageList.length;
  return props.imageList[getNonceByRange(0, len)];
};
/* 根据指定范围获取随机数 */
const getNonceByRange = (start, end) => {
  return Math.round(Math.random() * (end - start) + start);
};
/* 绘制阻塞块 */
const drawBlock = () => {
  blockX = getNonceByRange(
    blockWidth + 10,
    props.canvasWidth - (blockWidth + 10)
  );
  blockY = getNonceByRange(
    10 + props.blockRadius * 2,
    props.canvasHeight - (blockWidth + 10)
  );
  draw(canvasCtx, "fill");
  draw(blockCtx, "clip");
};
/* 绘制事件 */
const draw = (ctx, operation) => {
  const PI = Math.PI;
  let x = blockX;
  let y = blockY;
  let l = props.blockLength;
  let r = props.blockRadius;
  // 绘制
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI);
  ctx.lineTo(x + l, y);
  ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI);
  ctx.lineTo(x + l, y + l);
  ctx.lineTo(x, y + l);
  ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true);
  ctx.lineTo(x, y);
  // 修饰
  ctx.lineWidth = 2;
  ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
  ctx.strokeStyle = "rgba(255, 255, 255, 0.9)";
  ctx.stroke();
  ctx[operation]();
  ctx.globalCompositeOperation = "destination-over";
};
/* 事件绑定 */
const bindEvents = () => {
  // 监听鼠标按下事件
  document
    .getElementById("slider-button")
    ?.addEventListener("mousedown", (event) => {
      startEvent(event.clientX, event.clientY);
    });
  // 监听鼠标移动事件
  document.addEventListener("mousemove", (event) => {
    moveEvent(event.clientX, event.clientY);
  });
  // 监听鼠标离开事件
  document.addEventListener("mouseup", (event) => {
    endEvent(event.clientX);
  });
  // 监听触摸开始事件
  document
    .getElementById("slider-button")
    ?.addEventListener("touchstart", (event) => {
      startEvent(event.changedTouches[0].pageX, event.changedTouches[0].pageY);
    });
  // 监听触摸滑动事件
  document.addEventListener("touchmove", (event) => {
    moveEvent(event.changedTouches[0].pageX, event.changedTouches[0].pageY);
  });
  // 监听触摸离开事件
  document.addEventListener("touchend", (event) => {
    endEvent(event.changedTouches[0].pageX);
  });
};
/* 校验图片是否存在 */
const checkImgSrc = () => {
  if (isFrontCheck.value) {
    return true;
  }
  return !!canvas.value.src;
};
/* 滑动开始事件 */
const startEvent = (originXx, originYy) => {
  isPassing.value = false;
  if (!checkImgSrc() || isLoading.value || verifySuccess.value) {
    return;
  }
  originX = originXx;
  originY = originYy;
  isMouseDown = true;
  timestamp = +new Date();
};
/* 滑动事件 */
const moveEvent = (originXx, originYy) => {
  if (!isMouseDown) {
    return false;
  }
  const moveX = originXx - originX;
  const moveY = originYy - originY;
  if (moveX < 0 || moveX + 40 >= props.canvasWidth) {
    return false;
  }
  sliderButtonLeft.value = moveX + "px";
  let blockLeft =
    ((props.canvasWidth - 40 - 20) / (props.canvasWidth - 40)) * moveX;
  blockObj.style.left = blockLeft + "px";
  verifyActive.value = true;
  sliderBoxWidth.value = moveX + "px";
  dragDistanceList.push(moveY);
};
/* 滑动结束事件 */
const endEvent = (originXx) => {
  if (!isMouseDown) {
    return false;
  }
  isMouseDown = false;
  if (originXx === originX) {
    return false;
  }

  // 开始校验
  isLoading.value = true;
  // 校验结束
  verifyActive.value = false;
  // 滑动时长
  timestamp = +new Date() - timestamp;
  // 移动距离
  const moveLength = parseInt(blockObj.style.left);
  // 限制操作时长10S,超出判断失败
  if (timestamp > 10000) {
    verifyFailEvent();
  }
  // 是否前端校验
  else if (isFrontCheck.value) {
    const accuracyData =
      props.accuracy <= 1 ? 1 : props.accuracy > 10 ? 10 : props.accuracy; // 容错精度值
    const spliced = Math.abs(moveLength - blockX) <= accuracyData; // 判断是否重合
    console.log(spliced, "shifou chonghe");
    if (!spliced) {
      verifyFailEvent();
    } else {
      // 设置特殊值,后台特殊处理,直接验证通过
      emit("success", {
        ticket: ticket,
        moveLength: moveLength,
      });
    }
  } else {
    emit("success", { ticket: ticket, moveLength: moveLength });
  }
};
/* 图灵测试 */
const turingTest = () => {
  const arr = dragDistanceList; // 拖动距离数组
  const average = arr.reduce(sum) / arr.length; // 平均值
  const deviations = arr.map((x) => x - average); // 偏离值
  const stdDev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length); // 标准偏差
  return average !== stdDev; // 判断是否人为操作
};
/* 校验成功 */
const verifySuccessEvent = () => {
  isLoading.value = false;
  verifySuccess.value = true;
  const elapsedTime = Number((timestamp / 1000).toFixed(1));
  if (elapsedTime < 1) {
    successHint.value = `仅仅${elapsedTime}S,你的速度快如闪电`;
  } else if (elapsedTime < 2) {
    successHint.value = `只用了${elapsedTime}S,这速度简直完美`;
  } else {
    successHint.value = `耗时${elapsedTime}S,争取下次再快一点`;
  }
};
/* 校验失败 */
const verifyFailEvent = (msg?: string) => {
  verifyFail.value = true;
  emit("fail", msg);
  refresh();
};
/* 刷新图片验证码 */
const refresh = () => {
  // 延迟class的删除,等待动画结束
  setTimeout(() => {
    verifyFail.value = false;
  }, 500);
  isLoading.value = true;
  verifyActive.value = false;
  verifySuccess.value = false;
  blockObj.style.left = 0;
  sliderBoxWidth.value = 0;
  sliderButtonLeft.value = 0;
  if (isFrontCheck.value) {
    // 刷新画布
    canvasCtx.clearRect(0, 0, props.canvasWidth, props.canvasHeight);
    blockCtx.clearRect(0, 0, props.canvasWidth, props.canvasHeight);
    blockObj.width = props.canvasWidth;
    // 刷新图片
    image.src = getImageSrc();
  } else {
    getCaptchaApi();
  }
};
defineExpose({
  verifyFailEvent,
  verifySuccessEvent,
  isPassing,
  refresh,
});
</script>

<style scoped lang="scss">
::v-deep .slider-box #slider-button .el-icon-d-arrow-right {
  color: #e8e8e8;
}
</style>
<style scoped lang="scss">
.slide-verify {
  position: relative;
}

/* 图片加载样式 */
.img-loading {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 999;
  border-radius: 5px;
  background-position: center center;
  background-repeat: no-repeat;
  background-size: 100px;
  background-color: #737c8e;
  animation: loading 1.5s infinite;
  background-image: url(../../../assets/svg/loading.svg);
}

@keyframes loading {
  0% {
    opacity: 0.7;
  }

  100% {
    opacity: 9;
  }
}

/* 认证成功后的文字提示 */
.success-hint {
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  z-index: 999;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: large;
  color: #2cd000;
  background: rgb(255 255 255 / 80%);
}

/* 刷新按钮 */
.refresh-icon {
  position: absolute;
  top: 0;
  right: 0;
  width: 35px;
  height: 35px;
  background: url("../../../assets/images/light.png") 0 -432px;
  background-size: 35px 470px;
  cursor: pointer;
}

/* 验证图片 */

/* .slide-canvas {
        border-radius: px;
    } */

/* 阻塞块 */
.slide-block {
  position: absolute;
  top: 0;
  left: 0;
}

/* 校验失败时的阻塞块样式 */
.slide-block.verify-fail {
  transition: left 0.5s linear;
}

/* 滑动条 */
.slider {
  position: relative;
  margin-top: 10px;
  border: 1px solid #e4e7eb;
  border-radius: 5px;
  width: 100%;
  height: 40px;
  text-align: center;
  color: #45494c;
  background-color: #e8e8e8;
  line-height: 40px;
}

/* 滑动盒子 */
.slider-box {
  position: absolute;
  top: 0;
  left: 0;
  border: 0 solid #1991fa;
  border-radius: 5px;
  height: 40px;
  background: #d1e9fe;
}

/* 滑动按钮 */
.slider-button {
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 5px;
  width: 40px;
  height: 40px;
  background: #fff;
  box-shadow: 0 0 3px rgb(0 0 0 / 30%);
  transition: background 0.2s linear;
  cursor: move;
}

/* 鼠标悬浮时的按钮样式 */
.slider-button:hover {
  background: #3065ea;
}

.slider-button {
  background: #3065ea;
}

/* 鼠标悬浮时的按钮图标样式 */
.slider-button:hover .slider-button-icon {
  background-position: 0 -13px;
}

/* 滑动按钮图标 */
.slider-button-icon {
  position: absolute;
  top: 15px;
  left: 13px;
  width: 15px;
  height: 13px;
  background: url("../../../assets/images/light.png") 0 -26px;
  background-size: 35px 470px;
}

/* 校验时的按钮样式 */
.verify-active .slider-button {
  top: -1px;
  border: 1px solid #1991fa;
  height: 38px;
}

/* 校验时的滑动箱样式 */
.verify-active .slider-box {
  border-width: 1px;
  height: 38px;
}

/* 校验成功时的滑动箱样式 */
.verify-success .slider-box {
  border: 1px solid #52ccba;
  height: 38px;
  background-color: #d2f4ef;
}

/* 校验成功时的按钮样式 */
.verify-success .slider-button {
  top: -1px;
  border: 1px solid #52ccba;
  height: 38px;
  background-color: #52ccba !important;
}

/* 校验成功时的按钮图标样式 */
.verify-success .slider-button-icon {
  background-position: 0 0 !important;
}

/* 校验失败时的滑动箱样式 */
.verify-fail .slider-box {
  border: 1px solid #f57a7a;
  height: 38px;
  background-color: #fce1e1;
  transition: width 0.5s linear;
}

/* 校验失败时的按钮样式 */
.verify-fail .slider-button {
  top: -1px;
  border: 1px solid #f57a7a;
  height: 38px;
  background-color: #f57a7a !important;
  transition: left 0.5s linear;
}

/* 校验失败时的按钮图标样式 */
.verify-fail .slider-button-icon {
  top: 14px;
  background-position: 0 -82px !important;
}

@keyframes slidetounlock {
  0% {
    background-position: -175px 0;
  }

  100% {
    background-position: 175px 0;
  }
}

.slider .slider-hint {
  position: absolute;
  top: 0;
  width: 100%;
  height: 0.61rem;
  font-size: 0.16rem;
  color: transparent;
  background: -webkit-gradient(
    linear,
    left top,
    right top,
    color-stop(0, #333),
    color-stop(0.4, #333),
    color-stop(0.5, #fff),
    color-stop(0.6, #333),
    color-stop(1, #333)
  );
  background-clip: text;
  user-select: none;
  -webkit-text-fill-color: transparent;
  text-size-adjust: none;
  animation: slidetounlock 3s infinite;
}

.tips {
  position: absolute;
  bottom: 57px;
  z-index: 200;
  width: 100%;
  height: 20px;
  font-size: 12px;
  text-align: center;
  line-height: 20px;
}

.tips.success {
  color: green;
  background: rgb(255 255 255 / 60%);
}

.tips.danger {
  color: yellow;
  background: rgb(0 0 0 / 60%);
}

/* 校验状态下的提示文字隐藏 */
.verify-active .slider-hint,
.verify-success .slider-hint,
.verify-fail .slider-hint {
  display: none;
}
</style>

<template>
  <div class="login">
    <div class="login-logo">
      <img src="@/assets/images/welcome/LawTrust.png" alt="" />
      <span>demo</span>
    </div>
    <div class="login-content">
      <div class="login-left">
        <div class="login-left-content">
          <div class="login-left-title">
            demo
          </div>
        </div>
        <img
          class="login-index"
          src="@/assets/images/common/login-index2.png"
          alt=""
        />
      </div>
      <div class="login-right">
        <div class="login-right-box">
          <div class="login-right-box-tabs" v-show="!isShowSliderVerify">
            <div class="login-right-box-tabs-item" @click="active = 2">
              <span
                :style="{
                  fontWeight: active === 2 ? 500 : 400,
                }"
                >密码登录</span
              >
              <div
                :style="{
                  backgroundColor: active === 2 ? '#2DC783' : '#fff',
                }"
              ></div>
            </div>
            <div class="login-right-box-tabs-item" @click="active = 1">
              <span :style="{ 'font-weight': active === 1 ? 500 : 400 }"
                >验证码登录</span
              >
              <div
                :style="{
                  backgroundColor: active === 1 ? '#2DC783' : '#fff',
                }"
              ></div>
            </div>
          </div>
          <div class="login-right-box-form" v-show="!isShowSliderVerify">
            <div class="login-right-box-form-item" v-show="active === 1">
              <input
                v-model="phoneNum"
                @focus="phoneNumTips = ''"
                placeholder="请输入手机号"
                class="login-right-box-form-item-input"
              />
              <div v-show="phoneNumTips">{{ phoneNumTips }}</div>
            </div>
            <div class="login-right-box-form-item" v-show="active === 1">
              <input
                v-model="verificationCode"
                @focus="verificationCodeTips = ''"
                maxlength="8"
                placeholder="请输入验证码"
                class="login-right-box-form-item-input"
              />
              <el-button
                class="login-right-box-form-code"
                :disabled="isCode"
                size="small"
                @click="getCodeButton()"
                >{{
                  countDown === 60 ? "获取验证码" : countDown + "s"
                }}</el-button
              >
              <div v-show="verificationCodeTips">
                {{ verificationCodeTips }}
              </div>
            </div>
            <div class="login-right-box-form-item" v-show="active === 2">
              <input
                v-model="userPhone"
                @focus="userPhoneTips = ''"
                placeholder="请输入手机号"
                class="login-right-box-form-item-input"
              />
              <div v-show="userPhoneTips">{{ userPhoneTips }}</div>
            </div>
            <div class="login-right-box-form-item" v-show="active === 2">
              <input
                v-model="password"
                @focus="passwordTips = ''"
                placeholder="请输入密码"
                type="password"
                class="login-right-box-form-item-input"
              />
              <div v-show="passwordTips">{{ passwordTips }}</div>
            </div>
            <!-- <div class="login-right-box-form-item" v-show="active === 2">
            <div style="display: flex;algin-item: center;padding-left: 0px;">
              <input class="login-right-box-form-item-code" v-model="verification" @focus="verificationTips = ''" placeholder="请输入图形验证码" />
              <div class="login-right-box-form-item-img">rysv</div>
            </div>
            <div v-if="verificationTips">{{verificationTips}}</div>
          </div> -->
            <!-- <el-checkbox v-model="weekend" size="large" class="login-check"
              >一周内免密登录</el-checkbox
            > -->
            <el-checkbox v-model="agreeLogin" size="large" class="login-check">
              阅读并同意<span @click.stop.prevent="to('user')"
                >《用户协议》</span
              ><span @click.stop.prevent="to('privacy')">《隐私政策》</span>
            </el-checkbox>
            <el-button
              @click="submitDebounce()"
              class="login-button"
              type="primary"
              style="width: 100%"
              >立即登录</el-button
            >
            <div class="login-apply">
              还没有Law Trust账号
              <span @click="apply">免费申请试用</span>
            </div>
          </div>
          <div
            class="login-right-box-form verifyClass"
            v-if="isShowSliderVerify"
          >
            <slider-verify
              ref="sliderVerify"
              :canvasWidth="340"
              :canvasHeight="200"
              @success="onSuccess"
              @fail="onFail"
              @again="onAgain"
              :failTip="failTip"
            >
            </slider-verify>
            <el-button
              @click="submitDebounce()"
              type="primary"
              class="login-button"
              login-button
              style="margin: 20px 0; width: 100%"
              >立即登录</el-button
            >
          </div>
        </div>
      </div>
      <!--滑块验证-->
      <!-- <el-dialog title="请拖动滑块完成拼图" width="360px" :visible.sync="isShowSliderVerify" :close-on-click-modal="true" @closed="refresh" append-to-body>
                <slider-verify ref="sliderVerify" @success="onSuccess" @fail="onFail" @again="onAgain"/>
            </el-dialog> -->
    </div>
    <div class="login-footer">copyright © iLAW All Rights Reserved</div>
  </div>
</template>

<script setup lang="ts">
import { getCurrentInstance, ref, nextTick, onMounted } from "vue";
import SliderVerify from "./component/SliderVerify.vue";
import { useRouter } from "vue-router";
import { useMainStore } from "@/store";
import {
  loginAccount,
  loginCode,
  getVerificationCode,
  loginSlider,
} from "@/api";
const router = useRouter();
const mainStore = useMainStore();
const { proxy }: any = getCurrentInstance();
const sliderVerify = ref(null) as unknown as any;
const failTip = ref(""); // 滑块的错误提示
const active = ref(2);
const phoneNum = ref("");
const verificationCode = ref("");
const userPhone = ref("");
const password = ref("");
const verification = ref("");
const phoneNumTips = ref("");
const verificationCodeTips = ref("");
const userPhoneTips = ref("");
const passwordTips = ref("");
const verificationTips = ref("");
const countDown = ref(60);
let timer: any = null;
const isCode = ref(false);
// 随机字符串
const ticket = "";
// 验证值
// 验证码图片
const captchaSrc = "";
let loginParam: {} = {};
// 登录状态
const isLoading = false;
// 是否显示滑块验证
const showSlider = ref(false);
const isShowSliderVerify = ref(false);
const weekend = ref(false);
const agreeLogin = ref(false);
/* 滑动验证成功 */
const onSuccess = (captcha: Object) => {
  if (active.value === 1) {
    getCode(captcha);
  } else {
    Object.assign(loginParam, captcha);
    login();
  }
};
const apply = () => {
  window.open(`http://lawtrust.cn/#/home`, "_blank");
};
const to = (type: string) => {
  //user privacy
  window.open(`/document?type=${type}`, "_blank");
};
const login = async () => {
  let res: any = null;
  if (active.value === 1) res = await loginCode(loginParam);
  if (active.value === 2) res = await loginAccount(loginParam);
  if (res.code === 0) {
    if (isShowSliderVerify.value) {
      sliderVerify.value.verifySuccessEvent();
      isShowSliderVerify.value = false;
    }
    localStorage.setItem("permissions", JSON.stringify(res.data.permissions));
    localStorage.setItem("USER_ID", res.data.userId);
    localStorage.setItem("USER_NAME", res.data.userName);
    localStorage.setItem("isAdmin", res.data.isAdmin);
    localStorage.setItem("TOKEN_KEY", res.data.token);
    localStorage.setItem("headImg", res.data.headImg);
    localStorage.setItem("headImgUrl", res.data.headImgUrl);
    localStorage.setItem("USER_PHONE", res.data.phone);
    mainStore.changeUserName(res.data.userName);
    mainStore.changeHeadImg(res.data.headImg);
    mainStore.changePermissions(res.data.permissions);
    router.push({ name: "WelcomePage" });
    // location.reload();
    return;
  }
  showSlider.value = res.data;
  if (isShowSliderVerify.value) {
    // 账号密码登录 存在滑块
    showSlider.value = true;
    isShowSliderVerify.value = false;
  }
  if (res.msg === "ticket为空" && res.data) {
    isShowSliderVerify.value = true;
    return;
  }
  proxy.$message.error(res.msg);
};
/* 滑动验证失败 */
const onFail = (msg) => {
  failTip.value = msg;
  sliderVerify.value.isPassing = true;
  // this.message('error', '验证失败,请控制拼图对齐缺口1111')
};
/* 滑动验证异常 */
const onAgain = () => {
  proxy.$message("error", "滑动操作异常,请重试");
};
/* 刷新验证码 */
const refresh = () => {
  sliderVerify.value.refresh();
};
/* 提示弹框 */
const message = (type, message) => {
  proxy.$message({
    showClose: true,
    type: type,
    message: message,
    duration: 1500,
  });
};
const VueDebounce = (time) => {
  let timeout: any = null;
  return function () {
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => {
      submit();
    }, time);
  };
};
const submitDebounce = VueDebounce(500);
const getCode = async (captcha) => {
  await getVerificationCode({
    phoneNum: phoneNum.value,
    type: "LOGIN_TYPE",
    ...captcha,
  }).then((res: any) => {
    isCode.value = false;
    if (res.code === 0) {
      timer = setInterval(() => {
        if (countDown.value === 1) {
          countDown.value = 60;
          isCode.value = false;
          clearInterval(timer);
        } else {
          countDown.value -= 1;
        }
      }, 1000);
      isShowSliderVerify.value = false;
    } else {
      if (res.msg === "验证未通过,拖动滑块将悬浮图像正确合并") {
        sliderVerify.value.verifyFailEvent(res.msg);
      } else {
        isShowSliderVerify.value = false;
      }
      proxy.$message.error(res.msg);
    }
  });
};
const getCodeButton = async () => {
  if (countDown.value !== 60) return false;
  if (phoneNum.value && /^1[3456789]\d{9}$/.test(phoneNum.value)) {
    isShowSliderVerify.value = true;
    isCode.value = true;
  } else {
    if (phoneNum.value && !/^1[3456789]\d{9}$/.test(phoneNum.value)) {
      phoneNumTips.value = "请输入正确格式的手机号";
    } else {
      phoneNumTips.value = "请输入手机号";
    }
  }
};

const submit = () => {
  let flag = true;
  if (active.value === 1) {
    if (!phoneNum.value) {
      flag = false;
      phoneNumTips.value = "请输入手机号";
      return;
    }
    if (phoneNum.value && !/^1[3456789]\d{9}$/.test(phoneNum.value)) {
      flag = false;
      phoneNumTips.value = "请输入正确格式的手机号";
      return;
    }
    if (!verificationCode.value) {
      flag = false;
      verificationCodeTips.value = "请输入验证码";
      return;
    }
    if (!agreeLogin.value) {
      proxy.$message.error("请勾选 阅读并同意《用户协议》和《隐私政策》");
      flag = false;
      return;
    }
    loginParam = {
      phoneNum: phoneNum.value,
      type: "LOGIN_TYPE",
      verificationCode: verificationCode.value,
      rememberMe: weekend.value,
    };
    login();
  } else if (active.value === 2) {
    if (!userPhone.value) {
      flag = false;
      userPhoneTips.value = "请输入手机号";
      return;
    }
    if (!password.value) {
      flag = false;
      passwordTips.value = "请输入密码";
      return;
    }
    if (!agreeLogin.value) {
      proxy.$message.error("请勾选 阅读并同意《用户协议》和《隐私政策》");
      flag = false;
      return;
    }
    // if (!verification.value) {
    //   flag = false
    //   verificationTips.value = '请输入图形验证码'
    // }
    loginParam = {
      userPhone: userPhone.value,
      password: password.value,
      rememberMe: weekend.value,
    };
    // 判断次数小于2 则发送请求,否则
    if (!showSlider.value) {
      login();
      return;
    }
    if (flag) {
      isShowSliderVerify.value = true;
    }
  }
};
onMounted(async () => {
  const res: any = await loginSlider();
  showSlider.value = res.data;
});
</script>

<style scoped lang="scss">
.login {
  height: 100%;
  background: linear-gradient(180deg, #f6f4f2 0%, #f6f4f2 101.66%);
  min-width: 1280px;
  position: relative;
  min-height: 660px;
  .login-logo {
    position: absolute;
    top: 5%;
    left: 6%;
    display: flex;
    flex-direction: column;
    align-items: center;

    img {
      margin: 0 18px 8px 0;
      width: 148px;
      height: 26px;
    }

    span {
      font-size: 13px;
      font-family: "MiSans";
      font-weight: 500;
      letter-spacing: 0.32em;
      color: #a4a8aa;
      font-style: normal;
      line-height: 20px;
    }
  }

  .login-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 100%;
  }

  .login-left {
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
    padding-top: 133px;
    width: 50%;
    min-width: 660px;
    height: 100%;
    flex-direction: column;

    .login-left-content {
      display: flex;
      justify-content: space-between;
      padding-left: 40px;
      width: 660px;
      flex-direction: column;

      .login-left-title {
        margin-bottom: 39px;
        font-size: 28px;
        font-family: "PingFang SC";
        font-weight: 500;
        color: #263b49;
        font-style: normal;
        line-height: 24px;
      }

      .login-left-tips {
        font-size: 24px;
        font-family: "PingFang SC";
        font-weight: 400;
        color: #a4a8ab;
        font-style: normal;
        line-height: 24px;
      }
    }

    .login-index {
      margin: 56px 0 70px;
      width: 577px;
      height: 236px;
    }
  }

  .login-right {
    position: relative;
    width: 50%;
    height: 100%;

    .login-right-box {
      position: absolute;
      top: 50%;
      left: 50%;
      border-radius: 6px;
      padding: 45px 80px 41px;
      width: 500px;
      height: 430px;
      background: #fff;
      box-shadow: 0 0 12px 9px rgb(39 111 44 / 4%);
      transform: translate(-50%, -50%);
      box-sizing: border-box;

      .login-right-box-tabs {
        display: flex;
        align-items: center;
        margin: 0 0 30px;

        .login-right-box-tabs-item {
          margin-right: 32px;
          text-align: center;
          cursor: pointer;

          span {
            font-size: 20px;
            font-family: "PingFang SC";
            font-weight: 400;
            text-align: center;
            color: #263b49;
            font-style: normal;
            line-height: 28px;
            letter-spacing: 1.53846px;
          }

          div {
            margin-top: 7px;
            border-radius: 3px;
            height: 2px;
            background: #fff;
          }
        }
      }

      .login-right-box-form {
        .login-right-box-form-item {
          position: relative;
          margin-bottom: 24px;
          cursor: pointer;

          .login-right-box-form-item-input {
            border: 1px solid #c7c9cb;
            border-radius: 4px;
            padding: 0 24px;
            width: 100%;
            height: 40px;
            background: #fff;
            outline: none;
            box-shadow: 0 2px 4px rgb(0 0 0 / 3%), 0 1px 2px rgb(0 0 0 / 3%);
            box-sizing: border-box;
            line-height: 40px;
          }

          > input:-webkit-autofill {
            box-shadow: 0 0 0 400px #fff inset;
          }

          > div {
            padding-top: 6px;
            font-size: 12px;
            color: rgb(245 108 108);
            line-height: 1;
            position: absolute;
          }

          .login-right-box-form-code {
            position: absolute;
            top: 8px;
            right: 10px;
            border: 0;
            border-left: 1px solid #d8d8d8;
            border-radius: 0 !important;
            padding-left: 20px;
            font-size: 14px;
            font-family: "PingFang SC";
            font-weight: 400;
            color: #000;
            background: #fff;
            // transform: translateY(-50%);
          }

          .login-right-box-form-item-code {
            margin-right: 10px;
            border: none;
            border-radius: 8px;
            padding: 0 24px;
            height: 0.64rem;
            background: #f7f7f7;
            outline: none;
            flex: 1;
            box-sizing: border-box;
            line-height: 0.64rem;
          }

          .login-right-box-form-item-img {
            display: inline-block;
            border-radius: 8px;
            width: 101px;
            height: 48px;
            background: #fafafa;
            flex-shrink: 0;
            box-sizing: border-box;
            line-height: 48px;
          }
        }

        .login-check {
          margin-bottom: 5px;
          height: 24px;
          font-size: 12px;
          font-family: "PingFang SC";
          font-weight: 400;
          text-align: center;
          font-style: normal;
          line-height: 17px;

          span {
            color: #2dc783;
            cursor: pointer;
          }
        }

        ::v-deep .el-checkbox__input.is-checked .el-checkbox__inner {
          border: 1px solid #263b49;
          border-radius: 3px;
          background-color: #263b49;
        }

        ::v-deep .el-checkbox.el-checkbox--large .el-checkbox__label {
          font-size: 12px;
        }

        .login-check + .login-check {
          margin: 0;
        }

        .login-button {
          margin-top: 10px;
          border-radius: 100px;
          height: 40px;
          background: #263b49;
          border: 0;

          span {
            color: #ffff;
          }
        }

        .login-apply {
          margin-top: 30px;
          font-size: 14px;
          font-family: "PingFang SC";
          font-weight: 400;
          text-align: center;
          color: #000;
          font-style: normal;
          line-height: 20px;

          span {
            color: #2dc783;
            cursor: pointer;
          }
        }
      }
    }
  }

  .login-footer {
    position: absolute;
    bottom: 3%;
    left: 50%;
    font-size: 14px;
    font-family: "PingFang SC";
    font-weight: 400;
    color: #000;
    font-style: normal;
    line-height: 22px;
    transform: translateX(-50%);
  }
}

.verifyClass {
  margin-top: 20px;
}

.verifyClas .el-button {
  margin-top: 10px !important;
}
</style>

bgImg.jpg
在这里插入图片描述
light.png

在这里插入图片描述

Logo

基于 Vue 的企业级 UI 组件库和中后台系统解决方案,为数万开发者服务。

更多推荐