官网链接 https://github.com/he4398/vue-puzzle-verification
因为官网是vue2写的,而我现在的项目是使用vue3,所有我就把官网的源码拉取下来,改成vue3的写法(没有做任何逻辑上变动)
1、进去官网,把源码拉取下来,把下面两个文件直接放在到自己项目中
我把这两个文件放在自己的项目 新建文件 verification 下
2、把源码的 puzzleVerification.vue文件改写成vue3写法(下面代码复制过去覆盖官网文件):
<template>
<div class="puzzle-container" v-show="isVerificationShow" >
<div class="puzzle-header">
<span class="puzzle-header-left">拖动下方滑块完成拼图</span>
<div>
<span class="re-btn iconfont icon-shuaxin" @click="refreshImg"></span>
<span
class="close-btn iconfont icon-guanbi"
@click="closeVerificationBox"
></span>
</div>
</div>
<div
:style="'position:relative;overflow:hidden;width:' + dataWidth + 'px;'"
>
<div
:style="
'position:relative;width:' +
dataWidth +
'px;height:' +
dataHeight +
'px;'
"
>
<img
id="scream"
ref="scream"
:src="imgRandom"
:style="'width:' + dataWidth + 'px;height:' + dataHeight + 'px;'"
/>
<canvas
id="puzzle-box"
ref="puzzleBox"
:width="dataWidth"
:height="dataHeight"
></canvas>
</div>
<div
class="puzzle-lost-box"
:style="
'left:' +
left_Num +
'px;width:' +
dataWidth +
'px;height:' +
dataHeight +
'px;'
"
>
<canvas
id="puzzle-shadow"
ref="puzzleShadow"
:width="dataWidth"
:height="dataHeight"
></canvas>
<canvas
id="puzzle-lost"
ref="puzzleLost"
:width="dataWidth"
:height="dataHeight"
></canvas>
</div>
<p
:class="'ver-tips' + (displayTips ? ' slider-tips' : '')"
ref="verTips"
>
<template v-if="verification">
<i style="background-position: -4px -1207px"></i>
<span style="color: #42ca6b">验证通过</span>
<span></span>
</template>
<template v-if="!verification">
<i style="background-position: -4px -1229px"></i>
<span style="color: red">验证失败:</span>
<span style="margin-left: 4px">拖动滑块将悬浮图像正确拼合</span>
</template>
</p>
</div>
<div class="slider-container" :style="'width:' + dataWidth + 'px;'">
<div class="slider-bar"></div>
<div
class="slider-btn"
ref="sliderBtn"
@mousedown="startMove"
@touchstart="startMove"
></div>
</div>
</div>
</template>
<script setup lang="ts">
import {
ref,
defineEmits,
defineProps,
onBeforeMount,
onMounted,
watch,
nextTick,
} from "vue";
const emits = defineEmits(["update:modelValue"]);
const props = defineProps({
// 画布图片的尺寸
width: {
type: [String, Number],
default: 260,
},
height: {
type: [String, Number],
default: 120,
},
// 图集
puzzleImgList: {
type: Array,
default: () => [
require("../assets/thumbnail-img01.jpg"),
require("../assets/thumbnail-img02.jpg"),
require("../assets/thumbnail-img03.jpg"),
],
},
// 滑块的大小
blockSize: {
type: [String, Number],
default: 40,
},
// 误差
deviation: {
type: [String, Number],
default: 4,
},
// 滑块圆角的大小(仅当其形状是square有效)
blockRadius: {
type: [String, Number],
default: 4,
},
// 滑块随机出现的范围
wraperPadding: {
type: [String, Number],
default: 20,
},
// 滑块形状 square puzzle
blockType: {
type: String,
default: "square",
},
// 成功的回调
onSuccess: {
type: Function,
default: () => {
console.log("成功");
},
},
// 失败的回调
onError: {
type: Function,
default: () => {
console.log("失败");
},
},
verificationShow: {
type: Boolean,
default: false,
},
modelValue: {
type: Boolean,
default: false
},
});
const isVerificationShow = ref(true);
const moveStart = ref<any>("");
const displayTips = ref(false);
const verification = ref(false);
const randomX = ref(0);
const randomY = ref(0);
const imgRandom = ref<any>("");
const left_Num = ref(0);
const dataWidth = ref(0);
const dataHeight = ref(0);
const puzzleSize = ref(0); // 滑块的大小
const deviationDate = ref(0);
const radiusData = ref(0);
const paddingDate = ref(0);
const puzzleBox = ref<any>(null); //元素标识
const puzzleLost = ref<any>(null); //元素标识
const puzzleShadow = ref<any>(null); //元素标识
const sliderBtn = ref<any>(null); //元素标识
/* 关闭验证 */
const closeVerificationBox = () => {
isVerificationShow.value = false;
};
/* 刷新 */
const refreshImg = () => {
let imgRandomIndex = Math.round(
Math.random() * (props.puzzleImgList.length - 1)
);
imgRandom.value = props.puzzleImgList[imgRandomIndex];
initCanvas();
};
/* 画布初始化 */
const initCanvas = () => {
clearCanvas();
let w = dataWidth.value;
let h = dataHeight.value;
let PL_Size = puzzleSize.value;
let padding = paddingDate.value;
let MinN_X = padding + PL_Size;
let MaxN_X = w - padding - PL_Size - PL_Size / 6;
let MaxN_Y = padding;
let MinN_Y = h - padding - PL_Size - PL_Size / 6;
randomX.value = Math.round(Math.random() * (MaxN_X - PL_Size) + MinN_X);
randomY.value = Math.round(Math.random() * MaxN_Y + MinN_Y);
let X = randomX.value;
let Y = randomY.value;
left_Num.value = -X + 10;
let d = PL_Size / 3;
let radius = Number(radiusData.value);
let c = puzzleBox.value;
let c_l = puzzleLost.value;
let c_s = puzzleShadow.value;
let ctx = c.getContext("2d");
let ctx_l = c_l.getContext("2d");
let ctx_s = c_s.getContext("2d");
ctx.globalCompositeOperation = "xor";
ctx.shadowBlur = 10;
ctx.shadowColor = "#fff";
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 3;
ctx.fillStyle = "rgba(0,0,0,0.7)";
ctx.beginPath();
ctx.lineWidth = "1";
ctx.strokeStyle = "rgba(0,0,0,0)";
if (props.blockType === "square") {
ctx.arc(X + radius, Y + radius, radius, Math.PI, (Math.PI * 3) / 2);
ctx.lineTo(PL_Size - radius + X, Y);
ctx.arc(
PL_Size - radius + X,
radius + Y,
radius,
(Math.PI * 3) / 2,
Math.PI * 2
);
ctx.lineTo(PL_Size + X, PL_Size + Y - radius);
ctx.arc(
PL_Size - radius + X,
PL_Size - radius + Y,
radius,
0,
(Math.PI * 1) / 2
);
ctx.lineTo(radius + X, PL_Size + Y);
ctx.arc(
radius + X,
PL_Size - radius + Y,
radius,
(Math.PI * 1) / 2,
Math.PI
);
} else {
ctx.moveTo(X, Y);
ctx.lineTo(X + d, Y);
ctx.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y);
ctx.lineTo(X + 3 * d, Y);
ctx.lineTo(X + 3 * d, Y + d);
ctx.bezierCurveTo(
X + 2 * d,
Y + d,
X + 2 * d,
Y + 2 * d,
X + 3 * d,
Y + 2 * d
);
ctx.lineTo(X + 3 * d, Y + 3 * d);
ctx.lineTo(X, Y + 3 * d);
}
ctx.closePath();
ctx.stroke();
ctx.fill();
let img = new Image();
img.src = imgRandom.value;
img.onload = function () {
ctx_l.drawImage(img, 0, 0, w, h);
};
ctx_l.beginPath();
ctx_l.strokeStyle = "rgba(0,0,0,0)";
if (props.blockType === "square") {
ctx_l.arc(X + radius, Y + radius, radius, Math.PI, (Math.PI * 3) / 2);
ctx_l.lineTo(PL_Size - radius + X, Y);
ctx_l.arc(
PL_Size - radius + X,
radius + Y,
radius,
(Math.PI * 3) / 2,
Math.PI * 2
);
ctx_l.lineTo(PL_Size + X, PL_Size + Y - radius);
ctx_l.arc(
PL_Size - radius + X,
PL_Size - radius + Y,
radius,
0,
(Math.PI * 1) / 2
);
ctx_l.lineTo(radius + X, PL_Size + Y);
ctx_l.arc(
radius + X,
PL_Size - radius + Y,
radius,
(Math.PI * 1) / 2,
Math.PI
);
} else {
ctx_l.moveTo(X, Y);
ctx_l.lineTo(X + d, Y);
ctx_l.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y);
ctx_l.lineTo(X + 3 * d, Y);
ctx_l.lineTo(X + 3 * d, Y + d);
ctx_l.bezierCurveTo(
X + 2 * d,
Y + d,
X + 2 * d,
Y + 2 * d,
X + 3 * d,
Y + 2 * d
);
ctx_l.lineTo(X + 3 * d, Y + 3 * d);
ctx_l.lineTo(X, Y + 3 * d);
}
ctx_l.closePath();
ctx_l.stroke();
ctx_l.shadowBlur = 10;
ctx_l.shadowColor = "black";
ctx_l.clip();
ctx_s.beginPath();
ctx_s.lineWidth = "1";
ctx_s.strokeStyle = "rgba(0,0,0,0)";
if (props.blockType === "square") {
ctx_s.arc(X + radius, Y + radius, radius, Math.PI, (Math.PI * 3) / 2);
ctx_s.lineTo(PL_Size - radius + X, Y);
ctx_s.arc(
PL_Size - radius + X,
radius + Y,
radius,
(Math.PI * 3) / 2,
Math.PI * 2
);
ctx_s.lineTo(PL_Size + X, PL_Size + Y - radius);
ctx_s.arc(
PL_Size - radius + X,
PL_Size - radius + Y,
radius,
0,
(Math.PI * 1) / 2
);
ctx_s.lineTo(radius + X, PL_Size + Y);
ctx_s.arc(
radius + X,
PL_Size - radius + Y,
radius,
(Math.PI * 1) / 2,
Math.PI
);
} else {
ctx_s.moveTo(X, Y);
ctx_s.lineTo(X + d, Y);
ctx_s.bezierCurveTo(X + d, Y - d, X + 2 * d, Y - d, X + 2 * d, Y);
ctx_s.lineTo(X + 3 * d, Y);
ctx_s.lineTo(X + 3 * d, Y + d);
ctx_s.bezierCurveTo(
X + 2 * d,
Y + d,
X + 2 * d,
Y + 2 * d,
X + 3 * d,
Y + 2 * d
);
ctx_s.lineTo(X + 3 * d, Y + 3 * d);
ctx_s.lineTo(X, Y + 3 * d);
}
ctx_s.closePath();
ctx_s.stroke();
ctx_s.shadowBlur = 20;
ctx_s.shadowColor = "black";
ctx_s.fill();
};
/* 通过重置画布尺寸清空画布,这种方式更彻底 */
const clearCanvas = () => {
let c = puzzleBox.value;
let c_l = puzzleLost.value;
let c_s = puzzleShadow.value;
c.setAttribute("height", c.getAttribute("height"));
c_l.setAttribute("height", c.getAttribute("height"));
c_s.setAttribute("height", c.getAttribute("height"));
};
/* 按住滑块后初始化移动监听,记录初始位置 */
const startMove = (e:any) => {
// console.log(e);
e = e || window.event;
sliderBtn.value.style.backgroundPosition = "0 -216px";
moveStart.value = e.pageX || e.targetTouches[0].pageX;
addMouseMoveListener();
};
/* 滑块移动 */
const moving = (e:any) => {
e = e || window.event;
let moveX = e.pageX || e.targetTouches[0].pageX;
let d = moveX - moveStart.value;
let w = dataWidth.value;
let PL_Size = puzzleSize.value;
let padding = paddingDate.value;
if (moveStart.value === "") {
return "";
}
if (d < 0 || d > w - padding - PL_Size) {
return "";
}
sliderBtn.value.style.left = d + "px";
sliderBtn.value.style.transition = "inherit";
puzzleLost.value.style.left = d + "px";
puzzleLost.value.style.transition = "inherit";
puzzleShadow.value.style.left = d + "px";
puzzleShadow.value.style.transition = "inherit";
};
/* 移动结束,验证并回调 */
const moveEnd = (e:any) => {
e = e || window.event;
let moveEnd_X = (e.pageX || e.changedTouches[0].pageX) - moveStart.value;
let ver_Num = randomX.value - 10;
let deviationValue = deviationDate.value;
let Min_left = ver_Num - deviationValue;
let Max_left = ver_Num + deviationValue;
if (moveStart.value !== "") {
if (Max_left > moveEnd_X && moveEnd_X > Min_left) {
displayTips.value = true;
verification.value = true;
setTimeout(function () {
displayTips.value = false;
initCanvas();
/* 成功的回调函数 */
props.onSuccess();
}, 500);
} else {
displayTips.value = true;
verification.value = false;
setTimeout(function () {
displayTips.value = false;
initCanvas();
/* 失败的回调函数 */
props.onError();
}, 800);
}
}
if (
typeof sliderBtn.value !== "undefined" &&
typeof puzzleLost.value !== "undefined" &&
typeof puzzleShadow.value !== "undefined"
) {
setTimeout(function () {
sliderBtn.value.style.left = 0;
sliderBtn.value.style.transition = "left 0.5s";
puzzleLost.value.style.left = 0;
puzzleLost.value.style.transition = "left 0.5s";
puzzleShadow.value.style.left = 0;
puzzleShadow.value.style.transition = "left 0.5s";
}, 400);
sliderBtn.value.style.backgroundPosition = "0 -84px";
}
moveStart.value = "";
};
/* 全局绑定滑块移动与滑动结束,移动过程中鼠标可在页面任何位置 */
const addMouseMoveListener = () => {
document.addEventListener("mousemove", moving);
document.addEventListener("touchmove", moving);
document.addEventListener("mouseup", moveEnd);
document.addEventListener("touchend", moveEnd);
};
watch(
//监控数据变化
() => isVerificationShow.value,
(newVal, _d) => {
emits("update:modelValue", newVal);
},
{ immediate: true }
);
watch(
() => props.modelValue,
(val) => {
isVerificationShow.value = val;
}
)
watch(
//监控数据变化
() => props.verificationShow,
(newVal, _d) => {
isVerificationShow.value = newVal;
}
);
onBeforeMount(() => {
// 随机显示一张图片
let imgRandomIndex = Math.round(
Math.random() * (props.puzzleImgList.length - 1)
);
imgRandom.value = props.puzzleImgList[imgRandomIndex];
puzzleSize.value = Number(props.blockSize);
deviationDate.value = Number(props.deviation);
radiusData.value = Number(props.blockRadius);
dataWidth.value = Number(props.width);
dataHeight.value = Number(props.height);
paddingDate.value = Number(props.wraperPadding) || 20;
});
onMounted(() => {
nextTick(() => {
initCanvas();
});
});
</script>
<style scoped>
.slider-btn {
position: absolute;
width: 44px;
height: 44px;
left: 0;
top: -7px;
z-index: 12;
cursor: pointer;
background-image: url(../assets/sprite.3.2.0.png);
background-position: 0 -84px;
transition: inherit;
}
.ver-tips {
position: absolute;
left: 0;
bottom: -22px;
background: rgba(255, 255, 255, 0.9);
height: 22px;
line-height: 22px;
font-size: 12px;
width: 100%;
margin: 0;
text-align: left;
padding: 0 8px;
transition: all 0.4s;
}
.slider-tips {
bottom: 0;
}
.ver-tips i {
display: inline-block;
width: 22px;
height: 22px;
vertical-align: top;
background-image: url(../assets/sprite.3.2.0.png);
background-position: -4px -1229px;
}
.ver-tips span {
display: inline-block;
vertical-align: top;
line-height: 22px;
color: #455;
}
.active-tips {
display: block;
}
.hidden {
display: none;
}
.puzzle-container {
position: relative;
display: inline-block;
padding: 15px 15px 28px;
border: 1px solid #ddd;
background: #ffffff;
border-radius: 16px;
}
.puzzle-header {
display: flex;
justify-content: space-between;
margin: 5px 0;
}
.puzzle-header-left {
color: #333;
}
.re-btn,
.close-btn {
font-size: 16px;
cursor: pointer;
color: #666;
}
.re-btn:hover {
color: #67c23a;
}
.close-btn:hover {
color: #f56c6c;
}
.close-btn {
margin-left: 5px;
}
.slider-container {
position: relative;
margin: 10px auto 0;
min-height: 15px;
}
.slider-bar {
height: 10px;
border: 1px solid #c3c3c3;
border-radius: 5px;
background: #e4e4e4;
box-shadow: 0 1px 1px rgba(12, 10, 10, 0.2) inset;
position: absolute;
width: 100%;
top: 7px;
}
#puzzle-box {
position: absolute;
left: 0;
top: 0;
z-index: 22;
}
#puzzle-shadow {
position: absolute;
left: 0;
top: 0;
z-index: 22;
}
#puzzle-lost {
position: absolute;
left: 0;
top: 0;
z-index: 33;
}
.puzzle-lost-box {
position: absolute;
width: 260px;
height: 116px;
left: 0;
top: 0;
z-index: 111;
}
@font-face {
font-family: "iconfont";
src: url("../assets/icon-font/iconfont.eot?t=1565160368550"); /* IE9 */
src: url("../assets/icon-font/iconfont.eot?t=1565160368550#iefix")
format("embedded-opentype"),
/* IE6-IE8 */
url("data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAANUAAsAAAAAByQAAAMIAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCCfAqCFIIkATYCJAMMCwgABCAFhG0HOhtkBsi+QDw2JxWVhBKVFukepjkT9iz2vr6/fAq8RxAPz4+9O/e9O9WkGj15Fk3LQ/NCJ41QaVASVSzTSJ7E2p//a/4CvASvALwTjbaVddq9o71hlfDegDawAW//1zQFGqAwEaokh6yK8jD/9/c4lxuw3PPkdI3ZegseIJGz/1WKn5AESUiaSApHHzJ2/2uCAT/S0Iz7q0i8eCKrBLgEQJauMPX/uZzeKPH5KSuXuSYd9aI4DiigvbFNViAJeovsrkAs8jyBdmME8ODibgRNyZwWiIdGO9DM+KXkU61Cc8XaFI9p0loe6Q3gNvh+fKdFk6KpMuceX5/7cPTJ/Tfy4spDEIxnBb+JihVAEueV9kOVKL4Ca1dZq6YAKXljibz/PCFRiGZmcSdYIInCJw6Toid+iM6+oYJmd5A8BW4pefDQFKWzc2SkXpr+P1LN9+L/JeF4UsZPUGWa/nUPMypPfme7Do2j785+zvL4Z37s1l/wcAEwhNPJqPMcHgerCdZ/wbrLlYRrg2tSMZ45fpHJLQ7humLaL9WW9r64yBa+7D3UcBOs/3jDHKz7+MTzgPkExVfzZnHa3qveHHJTfVjsFjh5X99QxlP8cdhYHOyY+Fmy4BUXoLG2ngiSbe2xv/AbW/8ptNb3/tiQ4MN+w2saBeoEoFUBLHjnAmBTaiLTUFPhO3zDUa2nvAqYK7g0MN39vv3VQzeXDK2GEihajELVagZN8go06bAKzVptQ7tlzuYOAyoqItuwZIgg9LpC0e05VL1eaJI/0GTYD5r1RgXaXcb2nh3mwiydMjlCPrpPaLwkVrZJlsLSAekidDgtCigT4tyEsCcp+dQlxcRjLMjvdV9EoeIkwgt0GYVhgiknc/KkPRNJdzpyXPWmtpdEsGQfIw5BfMj3BGU8iZjyOoulwucHiFYIObiBUGWfICxnekcqiQJAL+UxiHAt11Td0/pqhIJiLBFBNrKS0IonUKl61BzxiLa0RzS1QyatYqi8Pb8yer5d0M7co0aJGqn5TuHErmnksyD2aGIA")
format("woff2"),
url("../assets/icon-font/iconfont.woff?t=1565160368550") format("woff"),
url("../assets/icon-font/iconfont.ttf?t=1565160368550") format("truetype"),
/* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url("../assets/icon-font/iconfont.svg?t=1565160368550#iconfont")
format("svg"); /* iOS 4.1- */
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-guanbi:before {
content: "\f01f1";
}
.icon-shuaxin:before {
content: "\e609";
}
</style>
3、在需要使用的地方直接使用(具体参数参考官网):
<template>
<PuzzleVerification
v-model="isVerificationShow1"
:onSuccess="handleSuccess"
:onError="handleError"
blockType="puzzle"
/>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
import PuzzleVerification from "@/verification/components/puzzleVerification.vue";
const isVerificationShow1 = ref(true);
const handleSuccess = () => {
console.log("成功");
isVerificationShow1.value = true;
};
const handleError = () => {
console.log("handleError");
isVerificationShow1.value = true;
};
</script>
<style scoped></style>