You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
554 lines
14 KiB
554 lines
14 KiB
7 months ago
|
<!-- @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>
|