feat: add verification comp
parent
e6939e22b1
commit
31315a7f76
|
@ -42,9 +42,13 @@
|
|||
"@vben/utils": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
"ant-design-vue": "catalog:",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "catalog:",
|
||||
"pinia": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/crypto-js": "^4.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ export function getUserInfo() {
|
|||
* 获取验证图片 以及token
|
||||
*/
|
||||
export function getCaptcha(data: any) {
|
||||
return requestClient.post('/system/captcha/get', data, {
|
||||
return baseRequestClient.post('/system/captcha/get', data, {
|
||||
// isReturnNativeResponse: true,
|
||||
});
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ export function getCaptcha(data: any) {
|
|||
* 滑动或者点选验证
|
||||
*/
|
||||
export function checkCaptcha(data: any) {
|
||||
return requestClient.post('/system/captcha/check', data, {
|
||||
return baseRequestClient.post('/system/captcha/check', data, {
|
||||
// isReturnNativeResponse: true,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { default as Verify } from './src/Verify.vue';
|
|
@ -0,0 +1,162 @@
|
|||
<script type="text/babel">
|
||||
/**
|
||||
* Verify 验证码组件
|
||||
* @description 分发验证码使用
|
||||
*/
|
||||
import { computed, ref, toRefs, watchEffect } from 'vue';
|
||||
|
||||
// import { $t } from '@vben/locales';
|
||||
|
||||
import VerifyPoints from './Verify/VerifyPoints.vue';
|
||||
import VerifySlide from './Verify/VerifySlide.vue';
|
||||
|
||||
import './style/verify.css';
|
||||
|
||||
export default {
|
||||
name: 'Vue3Verify',
|
||||
components: {
|
||||
VerifyPoints,
|
||||
VerifySlide,
|
||||
},
|
||||
props: {
|
||||
arith: {
|
||||
default: 0,
|
||||
type: Number,
|
||||
},
|
||||
barSize: {
|
||||
default: () => {
|
||||
return {
|
||||
height: '40px',
|
||||
width: '310px',
|
||||
};
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
blockSize: {
|
||||
default() {
|
||||
return {
|
||||
height: '50px',
|
||||
width: '50px',
|
||||
};
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
captchaType: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
explain: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
figure: {
|
||||
default: 0,
|
||||
type: Number,
|
||||
},
|
||||
imgSize: {
|
||||
default() {
|
||||
return {
|
||||
height: '155px',
|
||||
width: '310px',
|
||||
};
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
mode: {
|
||||
default: 'pop',
|
||||
type: String,
|
||||
},
|
||||
vSpace: {
|
||||
default: 5,
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { captchaType, mode } = toRefs(props);
|
||||
const clickShow = ref(false);
|
||||
const verifyType = ref(undefined);
|
||||
const componentType = ref(undefined);
|
||||
|
||||
const instance = ref({});
|
||||
|
||||
const showBox = computed(() => {
|
||||
return mode.value === 'pop' ? clickShow.value : true;
|
||||
});
|
||||
/**
|
||||
* refresh
|
||||
* @description 刷新
|
||||
*/
|
||||
const refresh = () => {
|
||||
if (instance.value.refresh) instance.value.refresh();
|
||||
};
|
||||
const closeBox = () => {
|
||||
clickShow.value = false;
|
||||
refresh();
|
||||
};
|
||||
const show = () => {
|
||||
if (mode.value === 'pop') clickShow.value = true;
|
||||
};
|
||||
watchEffect(() => {
|
||||
switch (captchaType.value) {
|
||||
case 'blockPuzzle': {
|
||||
verifyType.value = '2';
|
||||
componentType.value = 'VerifySlide';
|
||||
break;
|
||||
}
|
||||
case 'clickWord': {
|
||||
verifyType.value = '';
|
||||
componentType.value = 'VerifyPoints';
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
clickShow,
|
||||
closeBox,
|
||||
componentType,
|
||||
instance,
|
||||
show,
|
||||
showBox,
|
||||
verifyType,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-show="showBox" :class="mode === 'pop' ? 'mask' : ''">
|
||||
<div
|
||||
:class="mode === 'pop' ? 'verifybox' : ''"
|
||||
:style="{ 'max-width': `${parseInt(imgSize.width) + 20}px` }"
|
||||
>
|
||||
<div v-if="mode === 'pop'" class="verifybox-top">
|
||||
请完成安全验证
|
||||
<span class="verifybox-close" @click="closeBox">
|
||||
<i class="iconfont icon-close"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
:style="{ padding: mode === 'pop' ? '10px' : '0' }"
|
||||
class="verifybox-bottom"
|
||||
>
|
||||
<!-- 验证码容器 -->
|
||||
<component
|
||||
:is="componentType"
|
||||
v-if="componentType"
|
||||
ref="instance"
|
||||
:arith="arith"
|
||||
:bar-size="barSize"
|
||||
:block-size="blockSize"
|
||||
:captcha-type="captchaType"
|
||||
:explain="explain"
|
||||
:figure="figure"
|
||||
:img-size="imgSize"
|
||||
:mode="mode"
|
||||
:type="verifyType"
|
||||
:v-space="vSpace"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,276 @@
|
|||
<script type="text/babel" setup>
|
||||
/**
|
||||
* VerifyPoints
|
||||
* @description 点选
|
||||
*/
|
||||
import {
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
toRefs,
|
||||
} from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { checkCaptcha, getCaptcha } from '#/api/core/auth';
|
||||
|
||||
import { aesEncrypt } from './../utils/ase';
|
||||
import { resetSize } from './../utils/util';
|
||||
|
||||
const props = defineProps({
|
||||
barSize: {
|
||||
default() {
|
||||
return {
|
||||
height: '40px',
|
||||
width: '310px',
|
||||
};
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
captchaType: {
|
||||
default() {
|
||||
return 'VerifyPoints';
|
||||
},
|
||||
type: String,
|
||||
},
|
||||
imgSize: {
|
||||
default() {
|
||||
return {
|
||||
height: '155px',
|
||||
width: '310px',
|
||||
};
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
// 弹出式pop,固定fixed
|
||||
mode: {
|
||||
default: 'fixed',
|
||||
type: String,
|
||||
},
|
||||
// 间隔
|
||||
vSpace: {
|
||||
default: 5,
|
||||
type: Number,
|
||||
},
|
||||
});
|
||||
|
||||
const { captchaType, mode } = toRefs(props);
|
||||
const { proxy } = getCurrentInstance();
|
||||
const secretKey = ref(''); // 后端返回的ase加密秘钥
|
||||
const checkNum = ref(3); // 默认需要点击的字数
|
||||
const fontPos = reactive([]); // 选中的坐标信息
|
||||
const checkPosArr = reactive([]); // 用户点击的坐标
|
||||
const num = ref(1); // 点击的记数
|
||||
const pointBackImgBase = ref(''); // 后端获取到的背景图片
|
||||
const poinTextList = reactive([]); // 后端返回的点击字体顺序
|
||||
const backToken = ref(''); // 后端返回的token值
|
||||
const setSize = reactive({
|
||||
barHeight: 0,
|
||||
barWidth: 0,
|
||||
imgHeight: 0,
|
||||
imgWidth: 0,
|
||||
});
|
||||
const tempPoints = reactive([]);
|
||||
const text = ref('');
|
||||
const barAreaColor = ref(undefined);
|
||||
const barAreaBorderColor = ref(undefined);
|
||||
const showRefresh = ref(true);
|
||||
const bindingClick = ref(true);
|
||||
|
||||
function init() {
|
||||
// 加载页面
|
||||
fontPos.splice(0, fontPos.length);
|
||||
checkPosArr.splice(0, checkPosArr.length);
|
||||
num.value = 1;
|
||||
getPictrue();
|
||||
nextTick(() => {
|
||||
const { barHeight, barWidth, imgHeight, imgWidth } = resetSize(proxy);
|
||||
setSize.imgHeight = imgHeight;
|
||||
setSize.imgWidth = imgWidth;
|
||||
setSize.barHeight = barHeight;
|
||||
setSize.barWidth = barWidth;
|
||||
proxy.$parent.$emit('ready', proxy);
|
||||
});
|
||||
}
|
||||
onMounted(() => {
|
||||
// 禁止拖拽
|
||||
init();
|
||||
proxy.$el.addEventListener('selectstart', () => {
|
||||
return false;
|
||||
});
|
||||
});
|
||||
const canvas = ref(null);
|
||||
|
||||
// 获取坐标
|
||||
const getMousePos = function (obj, e) {
|
||||
const x = e.offsetX;
|
||||
const y = e.offsetY;
|
||||
return { x, y };
|
||||
};
|
||||
// 创建坐标点
|
||||
const createPoint = function (pos) {
|
||||
tempPoints.push(Object.assign({}, pos));
|
||||
return num.value + 1;
|
||||
};
|
||||
|
||||
// 坐标转换函数
|
||||
const pointTransfrom = function (pointArr, imgSize) {
|
||||
const newPointArr = pointArr.map((p) => {
|
||||
const x = Math.round((310 * p.x) / Number.parseInt(imgSize.imgWidth));
|
||||
const y = Math.round((155 * p.y) / Number.parseInt(imgSize.imgHeight));
|
||||
return { x, y };
|
||||
});
|
||||
return newPointArr;
|
||||
};
|
||||
|
||||
const refresh = async function () {
|
||||
tempPoints.splice(0, tempPoints.length);
|
||||
barAreaColor.value = '#000';
|
||||
barAreaBorderColor.value = '#ddd';
|
||||
bindingClick.value = true;
|
||||
fontPos.splice(0, fontPos.length);
|
||||
checkPosArr.splice(0, checkPosArr.length);
|
||||
num.value = 1;
|
||||
await getPictrue();
|
||||
showRefresh.value = true;
|
||||
};
|
||||
|
||||
function canvasClick(e) {
|
||||
checkPosArr.push(getMousePos(canvas, e));
|
||||
if (num.value === checkNum.value) {
|
||||
num.value = createPoint(getMousePos(canvas, e));
|
||||
// 按比例转换坐标值
|
||||
const arr = pointTransfrom(checkPosArr, setSize);
|
||||
checkPosArr.length = 0;
|
||||
checkPosArr.push(...arr);
|
||||
// 等创建坐标执行完
|
||||
setTimeout(() => {
|
||||
// var flag = this.comparePos(this.fontPos, this.checkPosArr);
|
||||
// 发送后端请求
|
||||
const captchaVerification = secretKey.value
|
||||
? aesEncrypt(
|
||||
`${backToken.value}---${JSON.stringify(checkPosArr)}`,
|
||||
secretKey.value,
|
||||
)
|
||||
: `${backToken.value}---${JSON.stringify(checkPosArr)}`;
|
||||
const data = {
|
||||
captchaType: captchaType.value,
|
||||
pointJson: secretKey.value
|
||||
? aesEncrypt(JSON.stringify(checkPosArr), secretKey.value)
|
||||
: JSON.stringify(checkPosArr),
|
||||
token: backToken.value,
|
||||
};
|
||||
checkCaptcha(data).then((response) => {
|
||||
const res = response.data;
|
||||
if (res.repCode === '0000') {
|
||||
barAreaColor.value = '#4cae4c';
|
||||
barAreaBorderColor.value = '#5cb85c';
|
||||
text.value = $t('components.captcha.success');
|
||||
bindingClick.value = false;
|
||||
if (mode.value === 'pop') {
|
||||
setTimeout(() => {
|
||||
proxy.$parent.clickShow = false;
|
||||
refresh();
|
||||
}, 1500);
|
||||
}
|
||||
proxy.$parent.$emit('success', { captchaVerification });
|
||||
} else {
|
||||
proxy.$parent.$emit('error', proxy);
|
||||
barAreaColor.value = '#d9534f';
|
||||
barAreaBorderColor.value = '#d9534f';
|
||||
text.value = $t('components.captcha.fail');
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
}, 700);
|
||||
}
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
if (num.value < checkNum.value)
|
||||
num.value = createPoint(getMousePos(canvas, e));
|
||||
}
|
||||
|
||||
// 请求背景图片和验证图片
|
||||
async function getPictrue() {
|
||||
const data = {
|
||||
captchaType: captchaType.value,
|
||||
};
|
||||
const res = await getCaptcha(data);
|
||||
if (res.data.repCode === '0000') {
|
||||
pointBackImgBase.value = res.data.repData.originalImageBase64;
|
||||
backToken.value = res.data.repData.token;
|
||||
secretKey.value = res.data.repData.secretKey;
|
||||
poinTextList.value = res.data.repData.wordList;
|
||||
text.value = `${$t('components.captcha.point')}【${poinTextList.value.join(',')}】`;
|
||||
} else {
|
||||
text.value = res.data.repMsg;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="position: relative">
|
||||
<div class="verify-img-out">
|
||||
<div
|
||||
:style="{
|
||||
width: setSize.imgWidth,
|
||||
height: setSize.imgHeight,
|
||||
'background-size': `${setSize.imgWidth} ${setSize.imgHeight}`,
|
||||
'margin-bottom': `${vSpace}px`,
|
||||
}"
|
||||
class="verify-img-panel"
|
||||
>
|
||||
<div
|
||||
v-show="showRefresh"
|
||||
class="verify-refresh"
|
||||
style="z-index: 3"
|
||||
@click="refresh"
|
||||
>
|
||||
<i class="iconfont icon-refresh"></i>
|
||||
</div>
|
||||
<img
|
||||
ref="canvas"
|
||||
:src="`data:image/png;base64,${pointBackImgBase}`"
|
||||
alt=""
|
||||
style="display: block; width: 100%; height: 100%"
|
||||
@click="bindingClick ? canvasClick($event) : undefined"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-for="(tempPoint, index) in tempPoints"
|
||||
:key="index"
|
||||
:style="{
|
||||
'background-color': '#1abd6c',
|
||||
color: '#fff',
|
||||
'z-index': 9999,
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
'text-align': 'center',
|
||||
'line-height': '20px',
|
||||
'border-radius': '50%',
|
||||
position: 'absolute',
|
||||
top: `${parseInt(tempPoint.y - 10)}px`,
|
||||
left: `${parseInt(tempPoint.x - 10)}px`,
|
||||
}"
|
||||
class="point-area"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 'height': this.barSize.height, -->
|
||||
<div
|
||||
:style="{
|
||||
width: setSize.imgWidth,
|
||||
color: barAreaColor,
|
||||
'border-color': barAreaBorderColor,
|
||||
'line-height': barSize.height,
|
||||
}"
|
||||
class="verify-bar-area"
|
||||
>
|
||||
<span class="verify-msg">{{ text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,396 @@
|
|||
<script type="text/babel" setup>
|
||||
/**
|
||||
* VerifySlide
|
||||
* @description 滑块
|
||||
*/
|
||||
import {
|
||||
computed,
|
||||
getCurrentInstance,
|
||||
nextTick,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
toRefs,
|
||||
watch,
|
||||
} from 'vue';
|
||||
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { checkCaptcha, getCaptcha } from '#/api/core/auth';
|
||||
|
||||
import { aesEncrypt } from './../utils/ase';
|
||||
import { resetSize } from './../utils/util';
|
||||
|
||||
const props = defineProps({
|
||||
barSize: {
|
||||
default() {
|
||||
return {
|
||||
height: '30px',
|
||||
width: '310px',
|
||||
};
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
blockSize: {
|
||||
default() {
|
||||
return {
|
||||
height: '50px',
|
||||
width: '50px',
|
||||
};
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
captchaType: {
|
||||
default() {
|
||||
return 'VerifySlide';
|
||||
},
|
||||
type: String,
|
||||
},
|
||||
explain: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
imgSize: {
|
||||
default() {
|
||||
return {
|
||||
height: '155px',
|
||||
width: '310px',
|
||||
};
|
||||
},
|
||||
type: Object,
|
||||
},
|
||||
// 弹出式pop,固定fixed
|
||||
mode: {
|
||||
default: 'fixed',
|
||||
type: String,
|
||||
},
|
||||
type: {
|
||||
default: '1',
|
||||
type: String,
|
||||
},
|
||||
vSpace: {
|
||||
default: 5,
|
||||
type: Number,
|
||||
},
|
||||
});
|
||||
|
||||
const { blockSize, captchaType, explain, mode, type } = toRefs(props);
|
||||
const { proxy } = getCurrentInstance();
|
||||
const secretKey = ref(''); // 后端返回的ase加密秘钥
|
||||
const passFlag = ref(''); // 是否通过的标识
|
||||
const backImgBase = ref(''); // 验证码背景图片
|
||||
const blockBackImgBase = ref(''); // 验证滑块的背景图片
|
||||
const backToken = ref(''); // 后端返回的唯一token值
|
||||
const startMoveTime = ref(''); // 移动开始的时间
|
||||
const endMovetime = ref(''); // 移动结束的时间
|
||||
const tipWords = ref('');
|
||||
const text = ref('');
|
||||
const finishText = ref('');
|
||||
const setSize = reactive({
|
||||
barHeight: 0,
|
||||
barWidth: 0,
|
||||
imgHeight: 0,
|
||||
imgWidth: 0,
|
||||
});
|
||||
const moveBlockLeft = ref(undefined);
|
||||
const leftBarWidth = ref(undefined);
|
||||
// 移动中样式
|
||||
const moveBlockBackgroundColor = ref(undefined);
|
||||
const leftBarBorderColor = ref('#ddd');
|
||||
const iconColor = ref(undefined);
|
||||
const iconClass = ref('icon-right');
|
||||
const status = ref(false); // 鼠标状态
|
||||
const isEnd = ref(false); // 是够验证完成
|
||||
const showRefresh = ref(true);
|
||||
const transitionLeft = ref('');
|
||||
const transitionWidth = ref('');
|
||||
const startLeft = ref(0);
|
||||
|
||||
const barArea = computed(() => {
|
||||
return proxy.$el.querySelector('.verify-bar-area');
|
||||
});
|
||||
function init() {
|
||||
text.value =
|
||||
explain.value === '' ? $t('components.captcha.slide') : explain.value;
|
||||
|
||||
getPictrue();
|
||||
nextTick(() => {
|
||||
const { barHeight, barWidth, imgHeight, imgWidth } = resetSize(proxy);
|
||||
setSize.imgHeight = imgHeight;
|
||||
setSize.imgWidth = imgWidth;
|
||||
setSize.barHeight = barHeight;
|
||||
setSize.barWidth = barWidth;
|
||||
proxy.$parent.$emit('ready', proxy);
|
||||
});
|
||||
|
||||
window.removeEventListener('touchmove', move);
|
||||
window.removeEventListener('mousemove', move);
|
||||
|
||||
// 鼠标松开
|
||||
window.removeEventListener('touchend', end);
|
||||
window.removeEventListener('mouseup', end);
|
||||
|
||||
window.addEventListener('touchmove', move);
|
||||
window.addEventListener('mousemove', move);
|
||||
|
||||
// 鼠标松开
|
||||
window.addEventListener('touchend', end);
|
||||
window.addEventListener('mouseup', end);
|
||||
}
|
||||
watch(type, () => {
|
||||
init();
|
||||
});
|
||||
onMounted(() => {
|
||||
// 禁止拖拽
|
||||
init();
|
||||
proxy.$el.addEventListener('selectstart', () => {
|
||||
return false;
|
||||
});
|
||||
});
|
||||
// 鼠标按下
|
||||
function start(e) {
|
||||
e = e || window.event;
|
||||
const x = e.touches ? e.touches[0].pageX : e.clientX;
|
||||
startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left);
|
||||
startMoveTime.value = Date.now(); // 开始滑动的时间
|
||||
if (isEnd.value === false) {
|
||||
text.value = '';
|
||||
moveBlockBackgroundColor.value = '#337ab7';
|
||||
leftBarBorderColor.value = '#337AB7';
|
||||
iconColor.value = '#fff';
|
||||
e.stopPropagation();
|
||||
status.value = true;
|
||||
}
|
||||
}
|
||||
// 鼠标移动
|
||||
function move(e) {
|
||||
e = e || window.event;
|
||||
if (status.value && isEnd.value === false) {
|
||||
const x = e.touches ? e.touches[0].pageX : e.clientX;
|
||||
const bar_area_left = barArea.value.getBoundingClientRect().left;
|
||||
let move_block_left = x - bar_area_left; // 小方块相对于父元素的left值
|
||||
if (
|
||||
move_block_left >=
|
||||
barArea.value.offsetWidth -
|
||||
Number.parseInt(Number.parseInt(blockSize.value.width) / 2) -
|
||||
2
|
||||
)
|
||||
move_block_left =
|
||||
barArea.value.offsetWidth -
|
||||
Number.parseInt(Number.parseInt(blockSize.value.width) / 2) -
|
||||
2;
|
||||
|
||||
if (move_block_left <= 0)
|
||||
move_block_left = Number.parseInt(
|
||||
Number.parseInt(blockSize.value.width) / 2,
|
||||
);
|
||||
|
||||
// 拖动后小方块的left值
|
||||
moveBlockLeft.value = `${move_block_left - startLeft.value}px`;
|
||||
leftBarWidth.value = `${move_block_left - startLeft.value}px`;
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标松开
|
||||
function end() {
|
||||
endMovetime.value = Date.now();
|
||||
// 判断是否重合
|
||||
if (status.value && isEnd.value === false) {
|
||||
let moveLeftDistance = Number.parseInt(
|
||||
(moveBlockLeft.value || '').replace('px', ''),
|
||||
);
|
||||
moveLeftDistance =
|
||||
(moveLeftDistance * 310) / Number.parseInt(setSize.imgWidth);
|
||||
const data = {
|
||||
captchaType: captchaType.value,
|
||||
pointJson: secretKey.value
|
||||
? aesEncrypt(
|
||||
JSON.stringify({ x: moveLeftDistance, y: 5 }),
|
||||
secretKey.value,
|
||||
)
|
||||
: JSON.stringify({ x: moveLeftDistance, y: 5 }),
|
||||
token: backToken.value,
|
||||
};
|
||||
checkCaptcha(data).then((response) => {
|
||||
const res = response.data;
|
||||
if (res.repCode === '0000') {
|
||||
moveBlockBackgroundColor.value = '#5cb85c';
|
||||
leftBarBorderColor.value = '#5cb85c';
|
||||
iconColor.value = '#fff';
|
||||
iconClass.value = 'icon-check';
|
||||
showRefresh.value = false;
|
||||
isEnd.value = true;
|
||||
if (mode.value === 'pop') {
|
||||
setTimeout(() => {
|
||||
proxy.$parent.clickShow = false;
|
||||
refresh();
|
||||
}, 1500);
|
||||
}
|
||||
passFlag.value = true;
|
||||
tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(2)}s
|
||||
${$t('components.captcha.success')}`;
|
||||
const captchaVerification = secretKey.value
|
||||
? aesEncrypt(
|
||||
`${backToken.value}---${JSON.stringify({ x: moveLeftDistance, y: 5 })}`,
|
||||
secretKey.value,
|
||||
)
|
||||
: `${backToken.value}---${JSON.stringify({ x: moveLeftDistance, y: 5 })}`;
|
||||
setTimeout(() => {
|
||||
tipWords.value = '';
|
||||
proxy.$parent.closeBox();
|
||||
proxy.$parent.$emit('success', { captchaVerification });
|
||||
}, 1000);
|
||||
} else {
|
||||
moveBlockBackgroundColor.value = '#d9534f';
|
||||
leftBarBorderColor.value = '#d9534f';
|
||||
iconColor.value = '#fff';
|
||||
iconClass.value = 'icon-close';
|
||||
passFlag.value = false;
|
||||
setTimeout(() => {
|
||||
refresh();
|
||||
}, 1000);
|
||||
proxy.$parent.$emit('error', proxy);
|
||||
tipWords.value = $t('components.captcha.fail');
|
||||
setTimeout(() => {
|
||||
tipWords.value = '';
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
status.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
showRefresh.value = true;
|
||||
finishText.value = '';
|
||||
|
||||
transitionLeft.value = 'left .3s';
|
||||
moveBlockLeft.value = 0;
|
||||
|
||||
leftBarWidth.value = undefined;
|
||||
transitionWidth.value = 'width .3s';
|
||||
|
||||
leftBarBorderColor.value = '#ddd';
|
||||
moveBlockBackgroundColor.value = '#fff';
|
||||
iconColor.value = '#000';
|
||||
iconClass.value = 'icon-right';
|
||||
isEnd.value = false;
|
||||
|
||||
await getPictrue();
|
||||
setTimeout(() => {
|
||||
transitionWidth.value = '';
|
||||
transitionLeft.value = '';
|
||||
text.value = explain.value;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 请求背景图片和验证图片
|
||||
async function getPictrue() {
|
||||
const data = {
|
||||
captchaType: captchaType.value,
|
||||
};
|
||||
const res = await getCaptcha(data);
|
||||
if (res.data.repCode === '0000') {
|
||||
backImgBase.value = res.data.repData.originalImageBase64;
|
||||
blockBackImgBase.value = `data:image/png;base64,${res.data.repData.jigsawImageBase64}`;
|
||||
backToken.value = res.data.repData.token;
|
||||
secretKey.value = res.data.repData.secretKey;
|
||||
} else {
|
||||
tipWords.value = res.data.repMsg;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="position: relative">
|
||||
<div
|
||||
v-if="type === '2'"
|
||||
:style="{ height: `${parseInt(setSize.imgHeight) + vSpace}px` }"
|
||||
class="verify-img-out"
|
||||
>
|
||||
<div
|
||||
:style="{ width: setSize.imgWidth, height: setSize.imgHeight }"
|
||||
class="verify-img-panel"
|
||||
>
|
||||
<img
|
||||
:src="`data:image/png;base64,${backImgBase}`"
|
||||
alt=""
|
||||
style="display: block; width: 100%; height: 100%"
|
||||
/>
|
||||
<div v-show="showRefresh" class="verify-refresh" @click="refresh">
|
||||
<i class="iconfont icon-refresh"></i>
|
||||
</div>
|
||||
<transition name="tips">
|
||||
<span
|
||||
v-if="tipWords"
|
||||
:class="passFlag ? 'suc-bg' : 'err-bg'"
|
||||
class="verify-tips"
|
||||
>
|
||||
{{ tipWords }}
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 公共部分 -->
|
||||
<div
|
||||
:style="{
|
||||
width: setSize.imgWidth,
|
||||
height: barSize.height,
|
||||
'line-height': barSize.height,
|
||||
}"
|
||||
class="verify-bar-area"
|
||||
>
|
||||
<span class="verify-msg" v-text="text"></span>
|
||||
<div
|
||||
:style="{
|
||||
width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
|
||||
height: barSize.height,
|
||||
'border-color': leftBarBorderColor,
|
||||
transaction: transitionWidth,
|
||||
}"
|
||||
class="verify-left-bar"
|
||||
>
|
||||
<span class="verify-msg" v-text="finishText"></span>
|
||||
<div
|
||||
:style="{
|
||||
width: barSize.height,
|
||||
height: barSize.height,
|
||||
'background-color': moveBlockBackgroundColor,
|
||||
left: moveBlockLeft,
|
||||
transition: transitionLeft,
|
||||
}"
|
||||
class="verify-move-block"
|
||||
@mousedown="start"
|
||||
@touchstart="start"
|
||||
>
|
||||
<i
|
||||
:class="[iconClass]"
|
||||
:style="{ color: iconColor }"
|
||||
class="iconfont verify-icon"
|
||||
></i>
|
||||
<div
|
||||
v-if="type === '2'"
|
||||
:style="{
|
||||
width: `${Math.floor((parseInt(setSize.imgWidth) * 47) / 310)}px`,
|
||||
height: setSize.imgHeight,
|
||||
top: `-${parseInt(setSize.imgHeight) + vSpace}px`,
|
||||
'background-size': `${setSize.imgWidth} ${setSize.imgHeight}`,
|
||||
}"
|
||||
class="verify-sub-block"
|
||||
>
|
||||
<img
|
||||
:src="blockBackImgBase"
|
||||
alt=""
|
||||
style="
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-user-drag: none;
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,3 @@
|
|||
export { default as VerifyPoints } from './VerifyPoints.vue';
|
||||
|
||||
export { default as VerifySlide } from './VerifySlide.vue';
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,15 @@
|
|||
import CryptoJS from 'crypto-js';
|
||||
|
||||
/**
|
||||
* @word 要加密的内容
|
||||
* @keyWord String 服务器随机返回的关键字
|
||||
*/
|
||||
export function aesEncrypt(word: string, keyWord = 'XwKsGlMcdPMEhR1B') {
|
||||
const key = CryptoJS.enc.Utf8.parse(keyWord);
|
||||
const src = CryptoJS.enc.Utf8.parse(word);
|
||||
const encrypted = CryptoJS.AES.encrypt(src, key, {
|
||||
mode: CryptoJS.mode.ECB,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
export function resetSize(vm: any) {
|
||||
const EmployeeWindow = window as any;
|
||||
const parentWidth =
|
||||
vm.$el.parentNode.offsetWidth || EmployeeWindow.offsetWidth;
|
||||
const parentHeight =
|
||||
vm.$el.parentNode.offsetHeight || EmployeeWindow.offsetHeight;
|
||||
const img_width = vm.imgSize.width.includes('%')
|
||||
? `${(Number.parseInt(vm.imgSize.width) / 100) * parentWidth}px`
|
||||
: vm.imgSize.width;
|
||||
|
||||
const img_height = vm.imgSize.height.includes('%')
|
||||
? `${(Number.parseInt(vm.imgSize.height) / 100) * parentHeight}px`
|
||||
: vm.imgSize.height;
|
||||
|
||||
const bar_width = vm.barSize.width.includes('%')
|
||||
? `${(Number.parseInt(vm.barSize.width) / 100) * parentWidth}px`
|
||||
: vm.barSize.width;
|
||||
|
||||
const bar_height = vm.barSize.height.includes('%')
|
||||
? `${(Number.parseInt(vm.barSize.height) / 100) * parentHeight}px`
|
||||
: vm.barSize.height;
|
||||
|
||||
return {
|
||||
barHeight: bar_height,
|
||||
barWidth: bar_width,
|
||||
imgHeight: img_height,
|
||||
imgWidth: img_width,
|
||||
};
|
||||
}
|
||||
|
||||
export const _code_chars = [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'g',
|
||||
'h',
|
||||
'i',
|
||||
'j',
|
||||
'k',
|
||||
'l',
|
||||
'm',
|
||||
'n',
|
||||
'o',
|
||||
'p',
|
||||
'q',
|
||||
'r',
|
||||
's',
|
||||
't',
|
||||
'u',
|
||||
'v',
|
||||
'w',
|
||||
'x',
|
||||
'y',
|
||||
'z',
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
'O',
|
||||
'P',
|
||||
'Q',
|
||||
'R',
|
||||
'S',
|
||||
'T',
|
||||
'U',
|
||||
'V',
|
||||
'W',
|
||||
'X',
|
||||
'Y',
|
||||
'Z',
|
||||
];
|
||||
export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0'];
|
||||
export const _code_color2 = [
|
||||
'#FF0033',
|
||||
'#006699',
|
||||
'#993366',
|
||||
'#FF9900',
|
||||
'#66CC66',
|
||||
'#FF33CC',
|
||||
];
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"captcha": {
|
||||
"fail": "verification failed",
|
||||
"point": "Please click",
|
||||
"slide": "Swipe right to complete verification",
|
||||
"success": "Verification succeeded",
|
||||
"verification": "Please complete security verification"
|
||||
}
|
||||
}
|
|
@ -4,7 +4,14 @@
|
|||
"register": "Register",
|
||||
"codeLogin": "Code Login",
|
||||
"qrcodeLogin": "Qr Code Login",
|
||||
"forgetPassword": "Forget Password"
|
||||
"forgetPassword": "Forget Password",
|
||||
"tenantname": "Tenant Name",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"usernameTip": "Please enter username",
|
||||
"tenantNameTip": "Please enter tenant name",
|
||||
"passwordTip": "Please enter password",
|
||||
"rememberMe": "Remember Me"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"captcha": {
|
||||
"fail": "验证失败",
|
||||
"point": "请依次点击",
|
||||
"slide": "向右滑动完成验证",
|
||||
"success": "验证成功",
|
||||
"verification": "请完成安全验证"
|
||||
}
|
||||
}
|
|
@ -4,7 +4,14 @@
|
|||
"register": "注册",
|
||||
"codeLogin": "验证码登录",
|
||||
"qrcodeLogin": "二维码登录",
|
||||
"forgetPassword": "忘记密码"
|
||||
"forgetPassword": "忘记密码",
|
||||
"tenantname": "租户",
|
||||
"username": "账号",
|
||||
"password": "密码",
|
||||
"usernameTip": "请输入用户名",
|
||||
"tenantNameTip": "请输入租户名",
|
||||
"passwordTip": "请输入密码",
|
||||
"rememberMe": "记住账号"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "概览",
|
||||
|
|
|
@ -667,6 +667,9 @@ importers:
|
|||
ant-design-vue:
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.6(vue@3.5.12(typescript@5.6.3))
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
dayjs:
|
||||
specifier: 'catalog:'
|
||||
version: 1.11.13
|
||||
|
@ -679,6 +682,10 @@ importers:
|
|||
vue-router:
|
||||
specifier: 'catalog:'
|
||||
version: 4.4.5(vue@3.5.12(typescript@5.6.3))
|
||||
devDependencies:
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
|
||||
apps/web-ele:
|
||||
dependencies:
|
||||
|
@ -3631,22 +3638,22 @@ packages:
|
|||
resolution: {integrity: sha512-GG428DkrrWCMhxRMRQZjuS7zmSUzarYcaHJqG9VB8dXAxw4iQDoKVQ7ChJRB6ZtsCsX3Jse1PEUlHrJiyQrOTg==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/message-compiler@10.0.0':
|
||||
resolution: {integrity: sha512-OcaWc63NC/9p1cMdgoNKBj4d61BH8sUW1Hfs6YijTd9656ZR4rNqXAlRnBrfS5ABq0vjQjpa8VnyvH9hK49yBw==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/message-compiler@10.0.4':
|
||||
resolution: {integrity: sha512-AFbhEo10DP095/45EauinQJ5hJ3rJUmuuqltGguvc3WsvezZN+g8qNHLGWKu60FHQVizMrQY7VJ+zVlBXlQQkQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/shared@10.0.0':
|
||||
resolution: {integrity: sha512-6ngLfI7DOTew2dcF9WMJx+NnMWghMBhIiHbGg+wRvngpzD5KZJZiJVuzMsUQE1a5YebEmtpTEfUrDp/NqVGdiw==}
|
||||
'@intlify/message-compiler@11.0.0-beta.0':
|
||||
resolution: {integrity: sha512-HWHv6jj7wJmHY5I73k80lBffDfnpqjx5vvn965YJB4lLvo0zkP3H15WGkwrLa/OR6fyYoP0DJVUKj9g2q7QJCA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/shared@10.0.4':
|
||||
resolution: {integrity: sha512-ukFn0I01HsSgr3VYhYcvkTCLS7rGa0gw4A4AMpcy/A9xx/zRJy7PS2BElMXLwUazVFMAr5zuiTk3MQeoeGXaJg==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/shared@11.0.0-beta.0':
|
||||
resolution: {integrity: sha512-v/IAS+BBeaWIKPI4CgSKsil2vJ64naIUUENha3e7jfhq1CxinXQquQYUM2GcCC86USxzTGgu67nafbaYzHS3vA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/unplugin-vue-i18n@5.3.1':
|
||||
resolution: {integrity: sha512-76huP8TpMOtBMLsYYIMLNbqMPXJ7+Q6xcjP6495h/pmbOQ7sw/DB8E0OFvDFeIZ2571a4ylzJnz+KMuYbAs1xA==}
|
||||
engines: {node: '>= 18'}
|
||||
|
@ -4473,6 +4480,9 @@ packages:
|
|||
'@types/conventional-commits-parser@5.0.0':
|
||||
resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==}
|
||||
|
||||
'@types/crypto-js@4.2.2':
|
||||
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
|
||||
|
||||
'@types/eslint@9.6.1':
|
||||
resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
|
||||
|
||||
|
@ -5626,6 +5636,9 @@ packages:
|
|||
crossws@0.3.1:
|
||||
resolution: {integrity: sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==}
|
||||
|
||||
crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
|
||||
crypto-random-string@2.0.0:
|
||||
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -12461,8 +12474,8 @@ snapshots:
|
|||
|
||||
'@intlify/bundle-utils@9.0.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))':
|
||||
dependencies:
|
||||
'@intlify/message-compiler': 10.0.0
|
||||
'@intlify/shared': 10.0.0
|
||||
'@intlify/message-compiler': 11.0.0-beta.0
|
||||
'@intlify/shared': 11.0.0-beta.0
|
||||
acorn: 8.14.0
|
||||
escodegen: 2.1.0
|
||||
estree-walker: 2.0.2
|
||||
|
@ -12478,20 +12491,20 @@ snapshots:
|
|||
'@intlify/message-compiler': 10.0.4
|
||||
'@intlify/shared': 10.0.4
|
||||
|
||||
'@intlify/message-compiler@10.0.0':
|
||||
dependencies:
|
||||
'@intlify/shared': 10.0.0
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/message-compiler@10.0.4':
|
||||
dependencies:
|
||||
'@intlify/shared': 10.0.4
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/shared@10.0.0': {}
|
||||
'@intlify/message-compiler@11.0.0-beta.0':
|
||||
dependencies:
|
||||
'@intlify/shared': 11.0.0-beta.0
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/shared@10.0.4': {}
|
||||
|
||||
'@intlify/shared@11.0.0-beta.0': {}
|
||||
|
||||
'@intlify/unplugin-vue-i18n@5.3.1(@vue/compiler-dom@3.5.12)(eslint@9.14.0(jiti@2.4.0))(rollup@4.26.0)(typescript@5.6.3)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0(jiti@2.4.0))
|
||||
|
@ -13439,6 +13452,8 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 22.9.0
|
||||
|
||||
'@types/crypto-js@4.2.2': {}
|
||||
|
||||
'@types/eslint@9.6.1':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
|
@ -14838,6 +14853,8 @@ snapshots:
|
|||
dependencies:
|
||||
uncrypto: 0.1.3
|
||||
|
||||
crypto-js@4.2.0: {}
|
||||
|
||||
crypto-random-string@2.0.0: {}
|
||||
|
||||
cspell-config-lib@8.16.0:
|
||||
|
|
Loading…
Reference in New Issue