push
This commit is contained in:
29
lib/component/form/slider/assets/iconfont.css
Normal file
29
lib/component/form/slider/assets/iconfont.css
Normal file
@@ -0,0 +1,29 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3015018 */
|
||||
src: url('iconfont.ttf?t=1639466613974') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-fail:before {
|
||||
content: "\e6d5";
|
||||
}
|
||||
|
||||
.icon-success:before {
|
||||
content: "\eb09";
|
||||
}
|
||||
|
||||
.icon-arrow-right:before {
|
||||
content: "\eb08";
|
||||
}
|
||||
|
||||
.icon-refresh:before {
|
||||
content: "\e654";
|
||||
}
|
||||
|
||||
BIN
lib/component/form/slider/assets/iconfont.ttf
Normal file
BIN
lib/component/form/slider/assets/iconfont.ttf
Normal file
Binary file not shown.
75
lib/component/form/slider/components/hooks/index.ts
Normal file
75
lib/component/form/slider/components/hooks/index.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { reactive, ref } from "vue";
|
||||
import { sum, square } from "../util";
|
||||
|
||||
export function useSlideAction() {
|
||||
const origin = reactive({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const success = ref(false);
|
||||
const isMouseDown = ref(false);
|
||||
const timestamp = ref(0);
|
||||
const trail = ref<number[]>([]);
|
||||
|
||||
const start = (e: MouseEvent | TouchEvent) => {
|
||||
if (success.value) return;
|
||||
if (e instanceof MouseEvent) {
|
||||
origin.x = e.clientX;
|
||||
origin.y = e.clientY;
|
||||
} else {
|
||||
origin.x = e.changedTouches[0].pageX;
|
||||
origin.y = e.changedTouches[0].pageY;
|
||||
}
|
||||
isMouseDown.value = true;
|
||||
timestamp.value = Date.now();
|
||||
};
|
||||
|
||||
const move = (w: number, e: MouseEvent | TouchEvent, cb: (moveX: number) => void) => {
|
||||
if (!isMouseDown.value) return false;
|
||||
let moveX = 0;
|
||||
let moveY = 0;
|
||||
if (e instanceof MouseEvent) {
|
||||
moveX = e.clientX - origin.x;
|
||||
moveY = e.clientY - origin.y;
|
||||
} else {
|
||||
moveX = e.changedTouches[0].pageX - origin.x;
|
||||
moveY = e.changedTouches[0].pageY - origin.y;
|
||||
}
|
||||
if (moveX < 0 || moveX + 38 >= w) return false;
|
||||
cb(moveX);
|
||||
trail.value.push(moveY);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param left : block.style.left;
|
||||
* @param blockX
|
||||
* @param accuracy
|
||||
* @returns
|
||||
*/
|
||||
const verify = (left: string, blockX: number, accuracy: number) => {
|
||||
const arr = trail.value; // drag y move distance
|
||||
const average = arr.reduce(sum) / arr.length; // average
|
||||
const deviations = arr.map((x) => x - average); // deviation array
|
||||
const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length); // standard deviation
|
||||
const leftNum = parseInt(left);
|
||||
accuracy = accuracy <= 1 ? 1 : accuracy > 10 ? 10 : accuracy;
|
||||
return {
|
||||
spliced: Math.abs(leftNum - blockX) <= accuracy,
|
||||
TuringTest: average !== stddev, // equal => not person operate
|
||||
};
|
||||
};
|
||||
|
||||
const end = (e: MouseEvent | TouchEvent, cb: (timestamp: number) => void) => {
|
||||
if (!isMouseDown.value) return false;
|
||||
isMouseDown.value = false;
|
||||
const moveX = e instanceof MouseEvent ? e.clientX : e.changedTouches[0].pageX;
|
||||
if (moveX === origin.x) return false;
|
||||
timestamp.value = Date.now() - timestamp.value;
|
||||
|
||||
cb(timestamp.value);
|
||||
};
|
||||
|
||||
return { origin, success, isMouseDown, timestamp, trail, start, move, end, verify };
|
||||
}
|
||||
5
lib/component/form/slider/components/index.ts
Normal file
5
lib/component/form/slider/components/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import SlideVerify from "./slide-verify.vue";
|
||||
|
||||
export type SlideVerifyInstance = InstanceType<typeof SlideVerify>;
|
||||
|
||||
export default SlideVerify;
|
||||
553
lib/component/form/slider/components/slide-verify.vue
Normal file
553
lib/component/form/slider/components/slide-verify.vue
Normal file
@@ -0,0 +1,553 @@
|
||||
<!-- @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>
|
||||
121
lib/component/form/slider/components/util.ts
Normal file
121
lib/component/form/slider/components/util.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
export const PI = Math.PI;
|
||||
|
||||
export function sum(x: number, y: number) {
|
||||
return x + y;
|
||||
}
|
||||
|
||||
export function square(x: number) {
|
||||
return x * x;
|
||||
}
|
||||
|
||||
export function draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
l: number,
|
||||
r: number,
|
||||
operation: "fill" | "clip",
|
||||
) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI);
|
||||
ctx.lineTo(x + l, y);
|
||||
ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI);
|
||||
ctx.lineTo(x + l, y + l);
|
||||
ctx.lineTo(x, y + l);
|
||||
ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true);
|
||||
ctx.lineTo(x, y);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.fillStyle = "rgba(255, 255, 255, 0.7)";
|
||||
ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";
|
||||
ctx.stroke();
|
||||
ctx[operation]();
|
||||
ctx.globalCompositeOperation = "destination-over";
|
||||
// ctx.globalCompositeOperation = "xor"; // 卡片不出来是可切换
|
||||
}
|
||||
|
||||
export function createImg(imgs: any[], onload: () => void) {
|
||||
const img = document.createElement("img");
|
||||
img.crossOrigin = "Anonymous";
|
||||
img.onload = onload;
|
||||
img.onerror = () => {
|
||||
img.src = getRandomImg(imgs);
|
||||
};
|
||||
img.src = getRandomImg(imgs);
|
||||
return img;
|
||||
}
|
||||
|
||||
export function getRandomNumberByRange(start: number, end: number) {
|
||||
return Math.round(Math.random() * (end - start) + start);
|
||||
}
|
||||
|
||||
// 随机生成img src
|
||||
export function getRandomImg(imgs: string[]) {
|
||||
const len = imgs.length;
|
||||
return len > 0
|
||||
? imgs[getRandomNumberByRange(0, len - 1)]
|
||||
: // : "https://picsum.photos/300/150/?image=" + getRandomNumberByRange(0, 1084);
|
||||
"https://source.unsplash.com/300x150/?book,library";
|
||||
}
|
||||
|
||||
type optType = {
|
||||
leading?: boolean;
|
||||
trailing?: boolean;
|
||||
resultCallback?: (res: any) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param fn 回调
|
||||
* @param interval 时间间隔
|
||||
* @param options 头节流,尾节流
|
||||
* @returns function
|
||||
*/
|
||||
export function throttle(
|
||||
fn: (args: any) => any,
|
||||
interval: number,
|
||||
options: optType = { leading: true, trailing: true },
|
||||
) {
|
||||
const { leading, trailing, resultCallback } = options;
|
||||
let lastTime = 0;
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
|
||||
const _throttle = function (this: any, ...args: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const nowTime = new Date().getTime();
|
||||
if (!lastTime && !leading) lastTime = nowTime;
|
||||
|
||||
const remainTime = interval - (nowTime - lastTime);
|
||||
if (remainTime <= 0) {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
|
||||
const result = fn.apply(this, args);
|
||||
if (resultCallback) resultCallback(result);
|
||||
resolve(result);
|
||||
lastTime = nowTime;
|
||||
return;
|
||||
}
|
||||
|
||||
if (trailing && !timer) {
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
lastTime = !leading ? 0 : new Date().getTime();
|
||||
const result = fn.apply(this, args);
|
||||
if (resultCallback) resultCallback(result);
|
||||
resolve(result);
|
||||
}, remainTime);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
_throttle.cancel = function () {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = null;
|
||||
lastTime = 0;
|
||||
};
|
||||
|
||||
return _throttle;
|
||||
}
|
||||
8
lib/component/form/slider/index.ts
Normal file
8
lib/component/form/slider/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { App } from 'vue';
|
||||
import nsSlider from './slider.vue';
|
||||
import nsSliderVerify from './slider-verify-custom.vue';
|
||||
export const NsSlider = function (app: App) {
|
||||
app.component(nsSlider.name, nsSlider);
|
||||
app.component(nsSliderVerify.name, nsSliderVerify);
|
||||
return app;
|
||||
};
|
||||
190
lib/component/form/slider/slider-verify-custom.vue
Normal file
190
lib/component/form/slider/slider-verify-custom.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<!-- @format -->
|
||||
|
||||
<template>
|
||||
<div class="silde_box">
|
||||
<slide-verify
|
||||
class="silde_box"
|
||||
ref="block"
|
||||
:w="width"
|
||||
:h="height"
|
||||
:imgs="imgs"
|
||||
:show="imgShow"
|
||||
:customVerify="customVerify"
|
||||
:frontPicture="frontPicture"
|
||||
:slider-text="text"
|
||||
:accuracy="accuracy"
|
||||
:asyncVerify="asyncVerify"
|
||||
@again="onAgain"
|
||||
@success="onSuccess"
|
||||
@fail="onFail"
|
||||
@refresh="onRefresh"
|
||||
@verify="verify" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
interface frontPictureClass {
|
||||
width: Number;
|
||||
height: Number;
|
||||
src: 'string';
|
||||
}
|
||||
import { defineComponent, ref, PropType, nextTick, reactive, toRefs } from 'vue';
|
||||
// 局部注册组件,需要单独引用组件样式
|
||||
// 只提供局部引用的方式,不再采用插件形式,方便按需加载,减少主包大小
|
||||
import SlideVerify, { SlideVerifyInstance } from './components/index';
|
||||
import { HttpRequestConfig, useApi } from '/nerv-lib/use/use-api';
|
||||
// import 'vue3-slide-verify/dist/style.css';
|
||||
import { cloneDeep, get, isArray, isEqual, isFunction, isString, isUndefined } from 'lodash-es';
|
||||
export default defineComponent({
|
||||
name: 'NsSildeVerify',
|
||||
components: { SlideVerify },
|
||||
props: {
|
||||
api: {
|
||||
type: [String, Object, Function] as PropType<string | Function | HttpRequestConfig>,
|
||||
required: true,
|
||||
},
|
||||
params: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
imgShow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
resultField: {
|
||||
type: String,
|
||||
default: 'data.data',
|
||||
},
|
||||
//数据筛选函数
|
||||
filterData: {
|
||||
type: Function,
|
||||
},
|
||||
asyncVerify: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
customVerify: {
|
||||
type: Function,
|
||||
},
|
||||
imgWidth: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
onSuccess: Function,
|
||||
},
|
||||
emits: ['change', 'validateChange'],
|
||||
setup(props, { attrs, emit }) {
|
||||
const msg = ref('');
|
||||
const block = ref<SlideVerifyInstance>();
|
||||
const imgs = ref<Array<string>>([]);
|
||||
const frontPicture = ref<frontPictureClass>();
|
||||
const state = reactive({
|
||||
width: 290,
|
||||
height: 177,
|
||||
verifyData: {},
|
||||
});
|
||||
const loadImage = (callback?) => {
|
||||
const requestConfig: HttpRequestConfig = { method: 'get' };
|
||||
const { api, params: _params, resultField, filterData } = props;
|
||||
const params: Recordable = cloneDeep(_params);
|
||||
const { httpRequest } = useApi();
|
||||
httpRequest({ api, params, requestConfig })
|
||||
.then((res: Recordable) => {
|
||||
imgs.value = [res.data.snapPicture];
|
||||
let scale: number = props.imgWidth / res.data.expendData.bgImageWidth;
|
||||
frontPicture.value = {
|
||||
src: res.data.frontPicture,
|
||||
width: res.data.expendData.sliderImageWidth * scale,
|
||||
height: res.data.expendData.sliderImageHeight * scale,
|
||||
};
|
||||
state.height = res.data.expendData.bgImageHeight * scale;
|
||||
state.width = props.imgWidth;
|
||||
state.verifyData = res.data;
|
||||
emit('validateChange', { help: undefined });
|
||||
callback();
|
||||
})
|
||||
.catch((error: any) => {
|
||||
if (error?.response?.status === 403) {
|
||||
emit('validateChange', { help: '暂无权限', validateStatus: 'error' });
|
||||
nextTick(() => {
|
||||
//清空编辑初始值
|
||||
// modelValue.value = undefined;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
loadImage();
|
||||
const onAgain = () => {
|
||||
msg.value = '检测到非人为操作的哦! try again';
|
||||
// 刷新
|
||||
block.value?.refresh();
|
||||
};
|
||||
|
||||
// const onSuccess = (times: number) => {
|
||||
// msg.value = `login success, 耗时${(times / 1000).toFixed(1)}s`;
|
||||
// };
|
||||
|
||||
const onFail = () => {
|
||||
msg.value = '验证不通过';
|
||||
loadImage(() => {
|
||||
block.value?.resetImg();
|
||||
});
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
block.value.disabled = true;
|
||||
loadImage(() => {
|
||||
block.value?.resetImg();
|
||||
});
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
// 刷新
|
||||
block.value?.refresh();
|
||||
msg.value = '';
|
||||
};
|
||||
|
||||
const verify = (x, timestamp?: number) => {
|
||||
if (props.customVerify) {
|
||||
props
|
||||
.customVerify(x, state.verifyData)
|
||||
.then((res) => {
|
||||
let checkValue = {
|
||||
verifyImgCode: res.resultCode,
|
||||
verifyImgKey: state.verifyData.k,
|
||||
verifyNeedStep: 2,
|
||||
verifyStep: 2,
|
||||
};
|
||||
emit('change', checkValue);
|
||||
block.value?.successFun('success', timestamp);
|
||||
})
|
||||
.catch((err) => {
|
||||
// loadImage();
|
||||
emit('change', null);
|
||||
block.value?.successFun('fail');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
block,
|
||||
msg,
|
||||
text: '向右拖动滑块填充拼图',
|
||||
accuracy: 1,
|
||||
imgs,
|
||||
frontPicture,
|
||||
...toRefs(state),
|
||||
onAgain,
|
||||
onFail,
|
||||
onRefresh,
|
||||
handleClick,
|
||||
verify,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.silde_box {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
94
lib/component/form/slider/slider.vue
Normal file
94
lib/component/form/slider/slider.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<a-row>
|
||||
<a-col :span="16">
|
||||
<a-slider
|
||||
v-model:value="inputValue"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:marks="marks"
|
||||
:tooltipVisible="tooltipVisible"
|
||||
@change="changeValues"
|
||||
:default-value="defaultValue"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="4" style="padding-left: 20px; padding-top: 4px">
|
||||
<a-input-number v-model:value="inputValue" :min="min" :max="max" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NsSlider',
|
||||
props: {
|
||||
// 最小值
|
||||
min: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
// 最大值
|
||||
max: {
|
||||
type: Number,
|
||||
require: true,
|
||||
},
|
||||
// 默认值
|
||||
defaultValue: {
|
||||
type: Number,
|
||||
default: () => {
|
||||
// return this.min;
|
||||
},
|
||||
},
|
||||
tooltipVisible: {
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
console.log(props.max);
|
||||
const { min, max, marks, defaultValue, tooltipVisible } = props;
|
||||
let inputValue = ref(defaultValue);
|
||||
// const sliderState = reactive({
|
||||
// value: 1,
|
||||
// defaultValue: 10,
|
||||
// });
|
||||
// const markerState = reactive({
|
||||
// marks: {
|
||||
// 1: '1',
|
||||
// 3: '3',
|
||||
// 5: '5',
|
||||
// 10: '10',
|
||||
// 20: {
|
||||
// style: {
|
||||
// color: '#f50',
|
||||
// },
|
||||
// label: '100'
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
const changeValues = function (value) {
|
||||
inputValue.value = value;
|
||||
// sliderState.value = value;
|
||||
};
|
||||
|
||||
return {
|
||||
// sliderState,
|
||||
// markerState,
|
||||
inputValue,
|
||||
tooltipVisible,
|
||||
|
||||
min,
|
||||
max,
|
||||
marks,
|
||||
defaultValue,
|
||||
tooltipVisible,
|
||||
changeValues,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
Reference in New Issue
Block a user