Files
SaaS-lib/lib/component/form/slider/components/slide-verify.vue
xuziqiang d0155dbe3c push
2024-05-15 17:29:42 +08:00

554 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- @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>