wking-jie 2 months ago
commit 7753e820be

@ -1,20 +1,23 @@
<template>
<div style="position: relative"
>
<div style="position: relative">
<!-- 验证码图片容器 -->
<div class="verify-img-out">
<div class="verify-img-panel" :style="{'width': setSize.imgWidth,
<div class="verify-img-panel" :style="{
'width': setSize.imgWidth,
'height': setSize.imgHeight,
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
'margin-bottom': vSpace + 'px'}"
>
'margin-bottom': vSpace + 'px'
}">
<!-- 刷新按钮 -->
<div class="verify-refresh" style="z-index:3" @click="refresh" v-show="showRefresh">
<i class="iconfont icon-refresh"></i>
</div>
<!-- 背景图片 -->
<img :src="'data:image/png;base64,'+pointBackImgBase"
ref="canvas"
alt="" style="width:100%; height:100%; display:block"
@click="bindingClick ? canvasClick($event) : undefined">
<!-- 点击位置标记 -->
<div v-for="(tempPoint, index) in tempPoints" :key="index" class="point-area"
:style="{
'background-color': '#1abd6c',
@ -33,29 +36,28 @@
</div>
</div>
</div>
<!-- 'height': this.barSize.height, -->
<!-- 验证条区域 -->
<div class="verify-bar-area"
:style="{'width': setSize.imgWidth,
'color': this.barAreaColor,
'border-color': this.barAreaBorderColor,
'line-height':this.barSize.height}">
:style="{'width': setSize.imgWidth, 'color': barAreaColor, 'border-color': barAreaBorderColor, 'line-height': barSize.height}">
<span class="verify-msg">{{ text }}</span>
</div>
</div>
</template>
<script type="text/babel">
/**
* VerifyPoints
* @description 点选
* */
import { resetSize, _code_chars, _code_color1, _code_color2 } from './../utils/util'
import { aesEncrypt } from './../utils/ase'
import { reqGet, reqCheck } from './../api/index'
import { computed, onMounted, reactive, ref, watch, nextTick, toRefs, watchEffect, getCurrentInstance } from 'vue'
* VerifyPoints 组件
* @description 实现点选验证码功能
*/
import {resetSize, _code_chars, _code_color1, _code_color2} from './../utils/util';
import {aesEncrypt} from './../utils/ase';
import {reqGet, reqCheck} from './../api/index';
import {computed, onMounted, reactive, ref, watch, nextTick, toRefs, watchEffect, getCurrentInstance} from 'vue';
export default {
name: 'VerifyPoints',
props: {
// popfixed
// popfixed
mode: {
type: String,
default: 'fixed'
@ -63,174 +65,224 @@ export default {
captchaType: {
type: String
},
//
//
vSpace: {
type: Number,
default: 5
},
//
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px'
}
};
}
},
//
barSize: {
type: Object,
default() {
return {
width: '310px',
height: '40px'
}
};
}
}
},
setup(props, context) {
const { mode, captchaType, vSpace, imgSize, barSize } = 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 {mode, captchaType, vSpace, imgSize, barSize} = toRefs(props);
const {proxy} = getCurrentInstance();
// AES
const secretKey = ref('');
//
const checkNum = ref(3);
//
const fontPos = reactive([]);
//
const checkPosArr = reactive([]);
//
const num = ref(1);
// base64
const pointBackImgBase = ref('');
//
const poinTextList = reactive([]);
// token
const backToken = ref('');
//
const setSize = reactive({
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0
})
const tempPoints = reactive([])
const text = ref('')
const barAreaColor = ref(undefined)
const barAreaBorderColor = ref(undefined)
const showRefresh = ref(true)
const bindingClick = ref(true)
});
//
const tempPoints = reactive([]);
//
const text = ref('');
//
const barAreaColor = ref(undefined);
//
const barAreaBorderColor = ref(undefined);
//
const showRefresh = ref(true);
//
const bindingClick = ref(true);
/**
* 初始化组件
*/
const init = () => {
//
fontPos.splice(0, fontPos.length)
checkPosArr.splice(0, checkPosArr.length)
num.value = 1
getPictrue()
//
fontPos.splice(0, fontPos.length);
checkPosArr.splice(0, checkPosArr.length);
num.value = 1;
getPictrue();
nextTick(() => {
const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
setSize.imgHeight = imgHeight
setSize.imgWidth = imgWidth
setSize.barHeight = barHeight
setSize.barWidth = barWidth
proxy.$parent.$emit('ready', proxy)
})
}
const size = resetSize(proxy);
setSize.imgHeight = size.imgHeight;
setSize.imgWidth = size.imgWidth;
setSize.barHeight = size.barHeight;
setSize.barWidth = size.barWidth;
proxy.$parent.$emit('ready', proxy);
});
};
onMounted(() => {
//
init()
//
init();
proxy.$el.onselectstart = function () {
return false
}
})
const canvas = ref(null)
return false;
};
});
const canvas = ref(null);
/**
* 处理画布点击事件
* @param {Event} e - 点击事件对象
*/
const canvasClick = (e) => {
checkPosArr.push(getMousePos(canvas, e))
if (num.value == checkNum.value) {
num.value = createPoint(getMousePos(canvas, 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)
//
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 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),
pointJson: secretKey.value
? aesEncrypt(JSON.stringify(checkPosArr), secretKey.value)
: JSON.stringify(checkPosArr),
token: backToken.value
}
};
reqCheck(data).then(res => {
if (res.repCode == '0000') {
barAreaColor.value = '#4cae4c'
barAreaBorderColor.value = '#5cb85c'
text.value = '验证成功'
bindingClick.value = false
if (mode.value == 'pop') {
if (res.repCode === '0000') {
barAreaColor.value = '#4cae4c';
barAreaBorderColor.value = '#5cb85c';
text.value = '验证成功';
bindingClick.value = false;
if (mode.value === 'pop') {
setTimeout(() => {
proxy.$parent.clickShow = false
refresh()
}, 1500)
proxy.$parent.clickShow = false;
refresh();
}, 1500);
}
proxy.$parent.$emit('success', { captchaVerification })
proxy.$parent.$emit('success', {captchaVerification});
} else {
proxy.$parent.$emit('error', proxy)
barAreaColor.value = '#d9534f'
barAreaBorderColor.value = '#d9534f'
text.value = '验证失败'
setTimeout(() => {
refresh()
}, 700)
proxy.$parent.$emit('error', proxy);
barAreaColor.value = '#d9534f';
barAreaBorderColor.value = '#d9534f';
text.value = '验证失败';
setTimeout(refresh, 700);
}
})
}, 400)
}
if (num.value < checkNum.value) {
num.value = createPoint(getMousePos(canvas, e))
}
}
//
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 refresh = 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
getPictrue()
text.value = '验证失败'
showRefresh.value = true
});
}, 400);
} else {
num.value = createPoint(getMousePos(canvas, e));
}
};
//
function getPictrue () {
const data = {
captchaType: captchaType.value
}
/**
* 获取鼠标点击坐标
* @param {HTMLElement} obj - DOM元素
* @param {Event} e - 事件对象
* @returns {Object} 坐标对象
*/
const getMousePos = (obj, e) => ({
x: e.offsetX,
y: e.offsetY
});
/**
* 创建坐标点并添加到临时点列表中
* @param {Object} pos - 坐标对象
* @returns {Number} 更新后的点击计数值
*/
const createPoint = (pos) => {
tempPoints.push(Object.assign({}, pos));
return num.value + 1;
};
/**
* 刷新验证码
*/
const refresh = () => {
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;
getPictrue();
text.value = '验证失败';
showRefresh.value = true;
};
/**
* 请求背景图片和验证图片
*/
const getPictrue = () => {
const data = {captchaType: captchaType.value};
reqGet(data).then(res => {
if (res.repCode == '0000') {
pointBackImgBase.value = res.repData.originalImageBase64
backToken.value = res.repData.token
secretKey.value = res.repData.secretKey
poinTextList.value = res.repData.wordList
text.value = '请依次点击【' + poinTextList.value.join(',') + '】'
if (res.repCode === '0000') {
pointBackImgBase.value = res.repData.originalImageBase64;
backToken.value = res.repData.token;
secretKey.value = res.repData.secretKey;
poinTextList.value = res.repData.wordList;
text.value = `请依次点击【${poinTextList.value.join(',')}`;
} else {
text.value = res.repMsg
}
})
}
//
const pointTransfrom = function (pointArr, imgSize) {
const newPointArr = pointArr.map(p => {
const x = Math.round(310 * p.x / parseInt(imgSize.imgWidth))
const y = Math.round(155 * p.y / parseInt(imgSize.imgHeight))
return { x, y }
})
return newPointArr
text.value = res.repMsg;
}
});
};
/**
* 将用户点击的坐标转换为原始图片的比例坐标
* @param {Array} pointArr - 用户点击的坐标数组
* @param {Object} imgSize - 图片尺寸信息
* @returns {Array} 转换后的坐标数组
*/
const pointTransfrom = (pointArr, imgSize) => pointArr.map(p => ({
x: Math.round(310 * p.x / parseInt(imgSize.imgWidth)),
y: Math.round(155 * p.y / parseInt(imgSize.imgHeight))
}));
return {
secretKey,
checkNum,
@ -255,7 +307,7 @@ export default {
refresh,
getPictrue,
pointTransfrom
};
}
}
}
};
</script>

@ -1,24 +1,38 @@
/**
* 此处可直接引用自己项目封装好的 axios 配合后端联调
* 此处可直接引用自己项目封装好的 axios 配合后端联调
* 使用项目内部封装的 axios 实例可以确保所有请求都遵循统一的配置
* 例如默认的基础URL请求拦截器响应拦截器等从而简化API调用
*/
// 引入组件内部封装的axios实例
import request from './../utils/axios' // 组件内部封装的axios
// 或者引入项目全局封装的axios实例根据实际路径调整
// import request from "@/api/axios.js" // 调用项目封装的axios
// 获取验证图片 以及token
/**
* 获取验证图片及token
*
* @param {Object} data - 请求体数据通常包含生成验证码所需的参数
* @returns {Promise} - 返回一个 Promise 对象解析为服务器响应的数据
*/
export function reqGet(data) {
return request({
url: '/captcha/get',
method: 'post',
data
})
url: '/captcha/get', // API endpoint for getting captcha image and token
method: 'post', // 使用POST方法发送请求
data // 将传入的数据作为请求体发送
});
}
// 滑动或者点选验证
/**
* 滑动或点选验证
*
* @param {Object} data - 请求体数据通常包含验证操作的结果和token
* @returns {Promise} - 返回一个 Promise 对象解析为服务器响应的数据
*/
export function reqCheck(data) {
return request({
url: '/captcha/check',
method: 'post',
data
})
url: '/captcha/check', // API endpoint for checking captcha validation
method: 'post', // 使用POST方法发送请求
data // 将传入的数据作为请求体发送
});
}

@ -1,11 +1,26 @@
import CryptoJS from 'crypto-js'
// 引入 CryptoJS 库,用于提供加密功能
import CryptoJS from 'crypto-js';
/**
* @word 要加密的内容
* @keyWord String 服务器随机返回的关键字
* */
* 使用 AES 算法对给定内容进行加密
*
* @param {string} word - 需要加密的内容
* @param {string} [keyWord='XwKsGlMcdPMEhR1B'] - 用于加密的密钥默认为 'XwKsGlMcdPMEhR1B'
* @returns {string} - 返回加密后的字符串使用 Base64 编码
*/
export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') {
const key = CryptoJS.enc.Utf8.parse(keyWord)
const srcs = CryptoJS.enc.Utf8.parse(word)
const encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 })
return encrypted.toString()
// 将密钥转换为 UTF-8 编码的字节数组
const key = CryptoJS.enc.Utf8.parse(keyWord);
// 将要加密的内容转换为 UTF-8 编码的字节数组
const srcs = CryptoJS.enc.Utf8.parse(word);
// 使用 AES 加密算法,采用 ECB 模式和 PKCS7 填充方式对数据进行加密
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB, // 设置加密模式为 ECB (Electronic Codebook)
padding: CryptoJS.pad.Pkcs7 // 设置填充方式为 PKCS7
});
// 返回加密后的内容,以 Base64 编码的字符串形式
return encrypted.toString();
}

@ -1,27 +1,65 @@
import axios from 'axios'
// 引入 axios 库,用于发起 HTTP 请求
import axios from 'axios';
axios.defaults.baseURL = import.meta.env.VITE_APP_BASE_API
// 设置默认的基础 URL从环境变量中读取。
// VITE_APP_BASE_API 是在构建时通过 Vite 配置提供的环境变量。
axios.defaults.baseURL = import.meta.env.VITE_APP_BASE_API;
/**
* 创建一个自定义的 axios 实例允许我们设置全局配置
*/
const service = axios.create({
// 设置请求超时时间为 40 秒40000 毫秒)
timeout: 40000,
// 设置默认的请求头信息
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json; charset=UTF-8'
'X-Requested-With': 'XMLHttpRequest', // 标识这是一个 AJAX 请求
'Content-Type': 'application/json; charset=UTF-8' // 指定发送的数据类型为 JSON
}
})
});
/**
* 请求拦截器在请求发送之前进行处理
* 可以用来添加认证令牌修改请求配置等
*/
service.interceptors.request.use(
config => {
return config
// 在这里可以对 config 进行修改,例如添加 token 或其他头部信息
// 示例config.headers.Authorization = `Bearer ${token}`;
// 返回配置对象或 Promise.resolve(config)
return config;
},
error => {
Promise.reject(error)
// 请求错误处理
// 如果请求出错,在这里可以做些事情,比如日志记录
// 返回错误或 Promise.reject(error)
return Promise.reject(error);
}
)
);
// response interceptor
/**
* 响应拦截器在接收到响应数据之后进行处理
* 可以用来统一处理响应错误解析响应数据等
*/
service.interceptors.response.use(
response => {
return response.data
// 默认情况下只返回响应体中的 data 字段
// 如果需要更多响应信息,可以直接返回整个 response 对象
return response.data;
},
error => {
// 响应错误处理
// 如果服务器返回错误状态码(如 4xx 或 5xx在这里可以进行全局错误处理
// 例如显示错误消息给用户或者跳转到错误页面
// 返回错误或 Promise.reject(error)
return Promise.reject(error);
}
)
export default service
);
// 导出 service 实例,以便其他模块可以引用它来发起 HTTP 请求
export default service;

@ -1,35 +1,68 @@
/**
* 重置图片和移动条的尺寸
*
* @param {Object} vm - Vue 组件实例包含 imgSize barSize 属性
* imgSize 包含图片的宽度和高度信息
* barSize 包含移动条的宽度和高度信息
* @returns {Object} 返回一个对象包含重置后的图片和移动条的尺寸信息
*/
export function resetSize(vm) {
let img_width, img_height, bar_width, bar_height // 图片的宽度、高度,移动条的宽度、高度
// 定义变量来存储图片和移动条的宽度、高度
let img_width, img_height, bar_width, bar_height;
const parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth
const parentHeight = vm.$el.parentNode.offsetHeight || window.offsetHeight
if (vm.imgSize.width.indexOf('%') != -1) {
img_width = parseInt(vm.imgSize.width) / 100 * parentWidth + 'px'
// 获取父容器的宽度和高度,默认为窗口的宽度和高度
const parentWidth = vm.$el.parentNode.offsetWidth || window.innerWidth;
const parentHeight = vm.$el.parentNode.offsetHeight || window.innerHeight;
// 计算图片的宽度
if (vm.imgSize.width.indexOf('%') !== -1) {
// 如果宽度是百分比,则根据父容器宽度计算实际像素值
img_width = (parseInt(vm.imgSize.width) / 100 * parentWidth) + 'px';
} else {
img_width = vm.imgSize.width
// 否则直接使用提供的宽度值
img_width = vm.imgSize.width;
}
if (vm.imgSize.height.indexOf('%') != -1) {
img_height = parseInt(vm.imgSize.height) / 100 * parentHeight + 'px'
// 计算图片的高度
if (vm.imgSize.height.indexOf('%') !== -1) {
// 如果高度是百分比,则根据父容器高度计算实际像素值
img_height = (parseInt(vm.imgSize.height) / 100 * parentHeight) + 'px';
} else {
img_height = vm.imgSize.height
// 否则直接使用提供的高度值
img_height = vm.imgSize.height;
}
if (vm.barSize.width.indexOf('%') != -1) {
bar_width = parseInt(vm.barSize.width) / 100 * parentWidth + 'px'
// 计算移动条的宽度
if (vm.barSize.width.indexOf('%') !== -1) {
// 如果宽度是百分比,则根据父容器宽度计算实际像素值
bar_width = (parseInt(vm.barSize.width) / 100 * parentWidth) + 'px';
} else {
bar_width = vm.barSize.width
// 否则直接使用提供的宽度值
bar_width = vm.barSize.width;
}
if (vm.barSize.height.indexOf('%') != -1) {
bar_height = parseInt(vm.barSize.height) / 100 * parentHeight + 'px'
// 计算移动条的高度
if (vm.barSize.height.indexOf('%') !== -1) {
// 如果高度是百分比,则根据父容器高度计算实际像素值
bar_height = (parseInt(vm.barSize.height) / 100 * parentHeight) + 'px';
} else {
bar_height = vm.barSize.height
// 否则直接使用提供的高度值
bar_height = vm.barSize.height;
}
return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height }
// 返回包含重置后尺寸的对象
return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height };
}
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和O避免混淆
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'];

Loading…
Cancel
Save