<!-- @format --> <template> <div id="slideVerify" class="slide-verify" :style="{ width: w + 'px' }" onselectstart="return false;"> <!-- 图片加载遮蔽罩 --> <div :class="{ 'slider-verify-loading': loadBlock }"></div> <canvas ref="canvas" :width="w" :height="h"></canvas> <div v-if="show" class="slide-verify-refresh-icon" @click="refresh" :style="{ pointerEvents: disabled ? 'none' : '' }"> <ns-icon name="refreash" size="20" /> </div> <canvas ref="block" :width="w" :height="h" class="slide-verify-block"></canvas> <div class="slide-verify-slider" :class="{ 'container-active': containerCls.containerActive, 'container-success': containerCls.containerSuccess, 'container-fail': containerCls.containerFail, }"> <div class="slide-verify-slider-mask" :style="{ width: sliderBox.width }"> <!-- slider --> <div class="slide-verify-slider-mask-item" :style="{ left: sliderBox.left }" @mousedown="sliderDown" @touchstart="touchStartEvent" @touchmove="touchMoveEvent" @touchend="touchEndEvent"> <i :class="[ 'slide-verify-slider-mask-item-icon', 'iconfont', `icon-${sliderBox.iconCls}`, ]"></i> </div> </div> <span class="slide-verify-slider-text">{{ sliderText }}</span> </div> </div> </template> <script lang="ts"> interface frontPictureClass { width: Number; height: Number; src: 'string'; } import { defineComponent, reactive, ref, onMounted, PropType, onBeforeUnmount, watch } from 'vue'; import { useSlideAction } from './hooks'; import { createImg, draw, getRandomImg, getRandomNumberByRange, throttle } from './util'; export default defineComponent({ name: 'SlideVerify', props: { // block length l: { type: Number, default: 42, }, // block radius r: { type: Number, default: 10, }, // canvas width w: { type: Number, default: 291, }, // canvas height h: { type: Number, default: 360, }, sliderText: { type: String, default: 'Slide filled right', }, accuracy: { type: Number, default: 5, // 若为 -1 则不进行机器判断 }, show: { type: Boolean, default: true, }, //是否是后端异步验证,而不是纯前端的机器人验证 asyncVerify: { type: Boolean, default: true, }, imgs: { type: Array as PropType<any[]>, default: () => [], }, frontPicture: { type: Object as PropType<frontPictureClass>, default: () => { return { src: '', width: 0, height: 0, }; }, }, interval: { // 节流时长间隔 type: Number, default: 50, }, }, emits: ['success', 'again', 'fail', 'refresh', 'verify'], setup(props, { emit }) { let { imgs, l, r, accuracy, interval, asyncVerify, frontPicture } = props; // 图片加载完关闭遮蔽罩 const loadBlock = ref(true); const blockX = ref(0); const blockY = ref(0); let w = ref(props.w); let h = ref(props.h); let disabled = ref(false); // class const containerCls = reactive({ containerActive: false, // container active class containerSuccess: false, // container success class containerFail: false, // container fail class }); // sliderMaskWidth sliderLeft const sliderBox = reactive({ iconCls: 'arrow-right', width: '0', left: '0', }); const block = ref<HTMLCanvasElement>(); const blockCtx = ref<CanvasRenderingContext2D | null>(); const canvas = ref<HTMLCanvasElement>(); const canvasCtx = ref<CanvasRenderingContext2D | null>(); let img: HTMLImageElement; const { success, start, move, end, verify } = useSlideAction(); const dealCTx = (canvas, context) => { // context = canvas.getContext('2d'); // 2、获取像素比,放大比例为:devicePixelRatio / webkitBackingStorePixelRatio , 以下是兼容的写法 let backingStore = context.backingStorePixelRatio || context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || context.backingStorePixelRatio || 1; let ratio = (window.devicePixelRatio || 1) / backingStore; // 3、将 Canvas 宽高进行放大,要设置canvas的画布大小,使用的是 canvas.width 和 canvas.height; let oldWidth = canvas.width; let oldHeight = canvas.height; canvas.width = oldWidth * ratio; canvas.height = oldHeight * ratio; // 4、要设置画布的实际渲染大小,使用的 style 属性或CSS设置的 width 和 height,只是简单的对画布进行缩放。 canvas.style.width = oldWidth + 'px'; canvas.style.height = oldHeight + 'px'; // 5、放大倍数:由于 Canvas 放大后,相应的绘制图片时也要放大,可以直接使用 scale 方法 context.scale(ratio, ratio); }; // event const reset = () => { success.value = false; containerCls.containerActive = false; containerCls.containerSuccess = false; containerCls.containerFail = false; sliderBox.iconCls = 'arrow-right'; sliderBox.left = '0'; sliderBox.width = '0'; block.value!.style.left = '0'; canvasCtx.value?.clearRect(0, 0, w.value, h.value); blockCtx.value?.clearRect(0, 0, w.value, h.value); if (!asyncVerify) { block.value!.width = w.value; } // generate img img.src = getRandomImg(imgs); }; const refresh = () => { reset(); emit('refresh'); }; const successFun = (name: string, timestamp?: number) => { if (name === 'success') { containerCls.containerSuccess = true; sliderBox.iconCls = 'success'; success.value = true; emit('success', timestamp); } if (name === 'again') { containerCls.containerFail = true; sliderBox.iconCls = 'fail'; emit('again'); } if (name === 'fail') { containerCls.containerFail = true; sliderBox.iconCls = 'fail'; emit('fail'); setTimeout(() => { reset(); }, 1000); } }; function moveCb(moveX: number) { sliderBox.left = moveX - 5 + 'px'; let blockLeft = ((w.value - 40 - 20) / (w.value - 40)) * moveX; block.value!.style.left = blockLeft + 'px'; containerCls.containerActive = true; sliderBox.width = moveX + 'px'; } function endCb(timestamp: number) { const { spliced, TuringTest } = verify(block.value!.style.left, blockX.value, accuracy); //不做自动校验,改为手动校验规则 if (asyncVerify) { emit('verify', block.value!.style.left, timestamp); return; } if (spliced) { if (accuracy === -1) { successFun('success', timestamp); return; } if (TuringTest) { // success successFun('success', timestamp); } else { successFun('again', timestamp); } } else { successFun('fail', timestamp); } } const touchMoveEvent = throttle((e: TouchEvent | MouseEvent) => { move(w.value, e, moveCb); }, interval); const touchEndEvent = (e: TouchEvent | MouseEvent) => { end(e, endCb); }; const setwatchFUn = (time?: number) => { !time ? (time = 100) : ''; return new Promise((resolve: Function) => { setTimeout(() => { resolve(); }, time); }); }; const resetImg = async () => { const _canvasCtx = canvas.value?.getContext('2d'); const _blockCtx = block.value?.getContext('2d'); canvasCtx.value = _canvasCtx; blockCtx.value = _blockCtx; await setwatchFUn(300); if (!imgs.length) { resetImg(); return; } if (asyncVerify) { img = createImg(imgs, () => { if (_canvasCtx) { _canvasCtx.drawImage(img, 0, 0, props.w, props.h); } }); setTimeout(() => { let frontImg = createImg([frontPicture.src], () => { if (_blockCtx) { _blockCtx.drawImage(frontImg, 0, 0, frontPicture.width, frontPicture.height); } disabled.value = false; }); }, 500); } else { img = createImg(imgs, () => { loadBlock.value = false; const L = l + r * 2 + 3; // draw block blockX.value = getRandomNumberByRange(L + 10, w.value - (L + 10)); blockY.value = getRandomNumberByRange(10 + r * 2, h.value - (L + 10)); if (_canvasCtx && _blockCtx) { draw(_canvasCtx, blockX.value, blockY.value, l, r, 'fill'); draw(_blockCtx, blockX.value, blockY.value, l, r, 'clip'); // draw image _canvasCtx.drawImage(img, 0, 0, w.value, h.value); _blockCtx.drawImage(img, 0, 0, w.value, h.value); // getImage const _y = blockY.value - r * 2 - 1; const imgData = _blockCtx.getImageData(blockX.value, _y, L, L); block.value!.width = L; _blockCtx.putImageData(imgData, 0, _y); } disabled.value = false; }); } // bindEvent document.addEventListener('mousemove', touchMoveEvent); document.addEventListener('mouseup', touchEndEvent); }; onMounted(async () => { resetImg(); }); // 移除全局事件 onBeforeUnmount(() => { document.removeEventListener('mousemove', touchMoveEvent); document.removeEventListener('mouseup', touchEndEvent); }); watch( () => props.imgs, (val: any[]) => { imgs = val; }, ); watch( () => props.frontPicture, (val: any) => { frontPicture = val; }, ); watch( () => props.w, (val: any) => { w.value = val; }, ); watch( () => props.h, (val: any) => { h.value = val; }, ); return { block, canvas, loadBlock, containerCls, sliderBox, refresh, sliderDown: start, touchStartEvent: start, touchMoveEvent, touchEndEvent, successFun, resetImg, disabled, }; }, }); </script> <style scoped lang="less"> @import url('../assets/iconfont.css'); .slide-verify-refresh-icon { z-index: 10; display: flex; align-items: center; justify-content: space-around; width: 32px; height: 32px; background-color: rgba(0, 0, 0, 0.2); } .slide-verify-slider-mask-item-icon { color: rgba(0, 0, 0, 0.4) !important; } // .slide-verify-slider-mask-item:hover { // color: #fff !important; // } .container-success .slide-verify-slider-mask .iconfont { color: #fff !important; } .container-fail .slide-verify-slider-mask .iconfont { color: #fff !important; } // .container-active .slide-verify-slider-mask { // background-color: #1991fa; // } // .container-active .slide-verify-slider-mask .iconfont { // color: #fff !important; // } .slide-verify-slider-mask-item { width: 38px !important; height: 38px !important; border-radius: 2px; } .slide-verify-slider { border-radius: 2px; } .slide-verify-refresh-icon .iconfont { font-size: 25px !important; transform: rotate(-30deg); } .position() { position: absolute; left: 0; top: 0; } .slide-verify { position: relative; &-loading { .position(); right: 0; bottom: 0; background: rgba(255, 255, 255, 0.9); z-index: 999; animation: loading 1.5s infinite; } &-block { .position(); } &-refresh-icon { position: absolute; right: 0; top: 0; width: 32px; height: 32px; cursor: pointer; .iconfont { font-size: 34px; color: #fff; } } &-slider { position: relative; text-align: center; width: 100%; height: 40px; line-height: 40px; margin-top: 15px; background: #f7f9fa; color: #45494c; border: 1px solid #e4e7eb; &-mask { .position(); height: 40px; border: 0 solid #1991fa; background: #d1e9fe; &-item { .position(); width: 40px; height: 40px; background: #fff; box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); cursor: pointer; transition: background 0.2s linear; display: flex; align-items: center; justify-content: center; &-icon { line-height: 1; font-size: 30px; color: #303030; } } } } } .container-active .slide-verify-slider-mask { height: 38px; border-width: 1px; &-item { height: 38px; top: -1px; border: 1px solid #1991fa; } } .container-success .slide-verify-slider-mask { height: 38px; border: 1px solid #52ccba; background-color: #d2f4ef; &-item { height: 38px; top: -1px; border: 1px solid #52ccba; background-color: #52ccba !important; } .iconfont { color: #fff; } } .container-fail .slide-verify-slider-mask { height: 38px; border: 1px solid #f57a7a; background-color: #fce1e1; &-item { height: 38px; top: -1px; border: 1px solid #f57a7a; background-color: #f57a7a !important; } .iconfont { color: #fff; } } .container-active .slide-verify-slider-text, .container-success .slide-verify-slider-text, .container-fail .slide-verify-slider-text { display: none; } @keyframes loading { 0% { opacity: 0.7; } 100% { opacity: 9; } } </style>