@作者 : 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的相关知识点,一点一滴的记录了下来,有问题请评论区指正,共同进步,这才是我写文章的原因之,如果这篇文章对您有帮助请三连支持一波