vue3 实现拼图滑块滑动验证
效果图:
git 有人分享了一个滑块的组件代码,可以 npm 安装,也可以直接复制组件,在项目里引用.推荐在项目里引用,方便改代码 git 地址:
这个组件是纯前端的实现,实际使用一般是前后端一起,校验放在后端
我在这个组件的基础上,自己简化了一下,弄了个前后端校验的版本 即 拼图的地址从后端获取, 移动距离调用后端接口判断是否移动成功
自己实现的vue3+滑块验证,主要是看下里面的注释,原组件备注比较少,看不明白的可以对照我的这个注释
完整代码地址:
https://github.com/shengbid/vue3-vite-demo/tree/main/src/views/userManage/login/components我这里只放后端验证的滑块组件代码,其他父组件的调用,和前端滑块组件的实现,可以直接到githup里面查看代码
前后端分离版本
<template> <div class="slider-container" :style="{ width: w + 'px' }"> <div class="slider-canvas" :style="{ width: w + 'px', height: h + 'px' }"> <!-- 大图 --> <canvas :width="w" :height="h" ref="canvas" /> <!-- 小图 --> <canvas class="slider-block" :width="slideInfo.sw" :height="h" ref="block" /> <!-- 结果遮罩层 --> <div :class="`result-mask ${resultMask.class}`" :style="{ height: `${resultMask.height}px` }" /> </div> <!-- 拖动的滑块内容 --> <div class="slider-square"> <div class="box"> <div class="slider-square-icon" @mousedown="sliderDown" :style="{ left: slideInfo.sliderLeft }" ></div> <span>{{ slideInfo.sliderText }}</span> </div> </div> </div> </template> <script> import { ref, onMounted, reactive, onDeactivated, watch } from "vue"; export default { props: { w: { // 大图的宽度 type: Number, default: 450, }, h: { // 大图的高度 type: Number, default: 300, }, l: { // 小图的宽度/高度(正方形的宽度,实际宽度需要加上圆弧的宽度) type: Number, default: 42, }, sliderText: { // 滑块文字 type: String, default: "拖动滑块向右>>", }, bigImg: { // 大图源 type: String, default: "", }, smImg: { // 小图图源 type: String, default: "", }, r: { // 小图的拼图半径 type: Number, default: 10, }, accuracy: { // 拖动误差范围 type: Number, default: 5, }, }, setup(props, { emit }) { const canvas = ref(null); // 大图ref const canvasCtx = ref(null); // 大图canvas绘制容器 const block = ref(null); // 小图ref const blockCtx = ref(null); // 小图canvas绘制容器 const { w, h, l, r, sliderText } = props; const slideInfo = reactive({ sliderLeft: 0, // 可拖动滑块的left block_x: 0, // 拼图最终要滑动的位置x block_y: 0, // 拼图位置的y sw: l + r * 2 + 3, // 拼图的实际宽度 小图宽度 + 2*半径 + 3(border) sliderText, // 滑块提示文字 bigImg: "", smImg: "", }); const origin = reactive({ // 鼠标按下初始位置 orginX: 0, originY: 0, }); const resultMask = reactive({ // 结果提示背景色 class: "", height: 0, }); // 初始化canvas dom const initDom = () => { canvasCtx.value = canvas.value.getContext("2d"); blockCtx.value = block.value.getContext("2d"); }; // 初始化图片 const initImg = () => { const bigImg = createImg(() => { canvasCtx.value.drawImage(bigImg, 0, 0, w, h); }, props.bigImg); const smImg = createImg(() => { blockCtx.value.drawImage(smImg, 0, 0, slideInfo.sw, h); }, props.smImg); slideInfo.bigImg = bigImg; slideInfo.smImg = smImg; }; const createImg = (onload, url) => { const img = document.createElement("img"); img.crossOrigin = "Anonymous"; img.onload = onload; img.onerror = () => { img.src = url; }; img.src = url; return img; }; onMounted(() => { initDom(); initImg(); bindEvents(); }); const slider = ref(false); // 鼠标按下 const sliderDown = (e) => { slider.value = true; slideInfo.sliderText = ""; origin.orginX = e.pageX; origin.originY = e.pageY; console.log(1, e); }; // 鼠标移动 const sliderMove = (e) => { if (slider.value) { const moveX = e.pageX - origin.orginX; const moveY = e.pageY - origin.originY; if (moveX < 0 || moveY + 38 > w) return false; slideInfo.sliderLeft = moveX + "px"; let blockLeft = ((w - slideInfo.sw) / (w - 40)) * moveX; block.value.style.left = blockLeft + "px"; } }; // 鼠标松开 const sliderUp = (e) => { if (!slider.value) return false; slider.value = false; console.log(3, e); resultMask.height = h / 2; const left = parseInt(block.value.style.left); emit("sliderJudge", left); }; // 将鼠标移动,鼠标松开事件绑定在dom上,如果鼠标移出滑块范围,还可以继续移动 const bindEvents = () => { document.addEventListener("mousemove", sliderMove); document.addEventListener("mouseup", sliderUp); }; // 调用接口获取图片,异步,监听图片地址变动 watch( () => props.bigImg, (newVal) => { slideInfo.bigImg.src = newVal; } ); watch( () => props.smImg, (newVal) => { slideInfo.smImg.src = newVal; } ); // 重置/刷新图片 const reset = () => { canvasCtx.value.clearRect(0, 0, w, h); blockCtx.value.clearRect(0, 0, w, h); slideInfo.sliderText = sliderText; resultMask.height = 0; resultMask.class = ""; slideInfo.sliderLeft = 0; block.value.style.left = 0; }; // 组建销毁时,移除鼠标事件 onDeactivated(() => { document.removeEventListener("mousemove", sliderMove); document.removeEventListener("mouseup", sliderUp); }); return { canvas, canvasCtx, blockCtx, block, slideInfo, sliderDown, sliderMove, sliderUp, resultMask, reset, }; }, }; </script> <style lang="less"> .slider-container { position: relative; .slider-canvas { position: relative; .slider-block { position: absolute; left: 0; top: 0; } @keyframes result-show { 0% { opacity: 0; } 100% { opacity: 0.7; } } .result-mask { position: absolute; left: 0; bottom: 0; width: 100%; opacity: 0.7; &.success { background-color: #52ccba; animation: result-show 1s; } &.fail { background-color: #f57a7a; animation: result-show 1s; } } } } .slider-square { background-color: #f7f9fa; height: 40px; text-align: center; line-height: 40px; border: 1px solid #ddd; position: relative; margin-top: 12px; .slider-square-icon { position: absolute; top: 0; left: 0; height: 39px; width: 39px; background-color: #fff; box-shadow: blanchedalmond 0px 0px 1px 2px; cursor: pointer; &:hover { background-color: rgb(249, 162, 32); } } } </style>