fix 验证码添加点击验证,角色改为树形结构

This commit is contained in:
yxh 2023-12-25 12:02:32 +08:00
parent 17cb2a1d65
commit ab1d7bee42
11 changed files with 1194 additions and 56 deletions

View File

@ -29,6 +29,7 @@
"pinia": "^2.0.34",
"print-js": "^1.6.0",
"qrcodejs2-fixes": "^0.0.2",
"qs": "^6.11.1",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.0",
"spark-md5": "^3.0.2",
@ -36,13 +37,12 @@
"vue": "^3.2.47",
"vue-clipboard3": "^2.0.0",
"vue-codemirror": "^6.1.1",
"vue-demi": "^0.13.11",
"vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6",
"vue-simple-uploader": "^1.0.0-beta.5",
"vue-ueditor-wrap": "^3.0.8",
"qs": "^6.11.1",
"vue-demi": "^0.13.11"
"vue-ueditor-wrap": "^3.0.8"
},
"devDependencies": {
"@types/node": "^18.15.11",

View File

@ -22,6 +22,27 @@ export function captcha(){
})
}
/**
* V2
*/
export function captchaV2(){
return request({
url:"/api/v1/pub/captcha/v2",
method:"get"
})
}
/**
* v2验证码
*/
export function checkCaptchaV2(data: { dots: string, key: string }){
return request({
url:"/api/v1/pub/captcha/v2Check",
data:data,
method:"post"
})
}
/**
* 退
*/

View File

@ -0,0 +1,378 @@
<template>
<div class="wg-cap-wrap">
<div class="wg-cap-wrap__header">
<span>请在下图<em>依次</em>点击</span>
<img class="wg-cap-wrap__thumb" v-if="thumbBase64Code" :src="thumbBase64Code" alt=" ">
</div>
<div class="wg-cap-wrap__body" :style="style">
<img class="wg-cap-wrap__picture" v-if="imageBase64Code" :src="imageBase64Code" alt=" " @click="handleClickPos($event)">
<img class="wg-cap-wrap__loading" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBzdHlsZT0ibWFyZ2luOiBhdXRvOyBiYWNrZ3JvdW5kOiByZ2JhKDI0MSwgMjQyLCAyNDMsIDApOyBkaXNwbGF5OiBibG9jazsgc2hhcGUtcmVuZGVyaW5nOiBhdXRvOyIgd2lkdGg9IjY0cHgiIGhlaWdodD0iNjRweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIj4KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjM2LjgxMDEiIHI9IjEzIiBmaWxsPSIjM2U3Y2ZmIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN5IiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgY2FsY01vZGU9InNwbGluZSIga2V5U3BsaW5lcz0iMC40NSAwIDAuOSAwLjU1OzAgMC40NSAwLjU1IDAuOSIga2V5VGltZXM9IjA7MC41OzEiIHZhbHVlcz0iMjM7Nzc7MjMiPjwvYW5pbWF0ZT4KICA8L2NpcmNsZT4KPC9zdmc+" alt="正在加载中...">
<template v-for="(dot, key) in dots" :key="key">
<div class="wg-cap-wrap__dot" :style="`top: ${dot.y}px; left:${dot.x}px;`">
<span>{{ dot.index }}</span>
</div>
</template>
</div>
<div class="wg-cap-wrap__footer">
<div class="wg-cap-wrap__ico">
<img @click="handleCloseEvent"
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDM5NDIzIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9Ijg2NzUiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNNTEyIDIzLjI3MjcyN2MyNjkuOTE3MDkxIDAgNDg4LjcyNzI3MyAyMTguODEwMTgyIDQ4OC43MjcyNzMgNDg4LjcyNzI3M2E0ODYuNjMyNzI3IDQ4Ni42MzI3MjcgMCAwIDEtODQuOTQ1NDU1IDI3NS40MDk0NTUgNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDAgMS03Ni44NDY1NDUtNTIuNTI2NTQ2QTM5My41NDE4MTggMzkzLjU0MTgxOCAwIDAgMCA5MDcuNjM2MzY0IDUxMmMwLTIxOC41MDc2MzYtMTc3LjEyODcyNy0zOTUuNjM2MzY0LTM5NS42MzYzNjQtMzk1LjYzNjM2NFMxMTYuMzYzNjM2IDI5My40OTIzNjQgMTE2LjM2MzYzNiA1MTJzMTc3LjEyODcyNyAzOTUuNjM2MzY0IDM5NS42MzYzNjQgMzk1LjYzNjM2NGEzOTUuMTcwOTA5IDM5NS4xNzA5MDkgMCAwIDAgMTI1LjQ0LTIwLjI5MzgxOSA0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMCAxIDI5LjQ4NjU0NSA4OC4yOTY3MjhBNDg4LjI2MTgxOCA0ODguMjYxODE4IDAgMCAxIDUxMiAxMDAwLjcyNzI3M0MyNDIuMDgyOTA5IDEwMDAuNzI3MjczIDIzLjI3MjcyNyA3ODEuOTE3MDkxIDIzLjI3MjcyNyA1MTJTMjQyLjA4MjkwOSAyMy4yNzI3MjcgNTEyIDIzLjI3MjcyN3ogbS0xMTUuMiAzMDcuNzEyTDUxMiA0NDYuMTM4MTgybDExNS4yLTExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEgNjUuODE1MjczIDY1Ljg2MTgxOEw1NzcuODYxODE4IDUxMmwxMTUuMiAxMTUuMmE0Ni41NDU0NTUgNDYuNTQ1NDU1IDAgMSAxLTY1Ljg2MTgxOCA2NS44MTUyNzNMNTEyIDU3Ny44NjE4MThsLTExNS4yIDExNS4yYTQ2LjU0NTQ1NSA0Ni41NDU0NTUgMCAxIDEtNjUuODE1MjczLTY1Ljg2MTgxOEw0NDYuMTM4MTgyIDUxMmwtMTE1LjItMTE1LjJhNDYuNTQ1NDU1IDQ2LjU0NTQ1NSAwIDEgMSA2NS44NjE4MTgtNjUuODE1MjczeiIgcC1pZD0iODY3NiIgZmlsbD0iIzcwNzA3MCI+PC9wYXRoPjwvc3ZnPg=="
alt="关闭">
<img @click="handleRefreshEvent"
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI2NjE0NDk5NjM4IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEzNjAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTg3LjQ1NiA0MjUuMDI0YTMzNiAzMzYgMCAwIDAgMzY4LjM4NCA0MjAuMjI0IDQ4IDQ4IDAgMCAxIDEyLjU0NCA5NS4xNjggNDMyIDQzMiAwIDAgMS00NzMuNjY0LTU0MC4xNmwtNTcuMjgtMTUuMzZhMTIuOCAxMi44IDAgMCAxLTYuMjcyLTIwLjkyOGwxNTkuMTY4LTE3OS40NTZhMTIuOCAxMi44IDAgMCAxIDIyLjE0NCA1Ljg4OGw0OC4wNjQgMjM1LjA3MmExMi44IDEyLjggMCAwIDEtMTUuODA4IDE0LjkxMmwtNTcuMjgtMTUuMzZ6TTgzNi40OCA1OTkuMDRhMzM2IDMzNiAwIDAgMC0zNjguMzg0LTQyMC4yMjQgNDggNDggMCAxIDEtMTIuNTQ0LTk1LjE2OCA0MzIgNDMyIDAgMCAxIDQ3My42NjQgNTQwLjE2bDU3LjI4IDE1LjM2YTEyLjggMTIuOCAwIDAgMSA2LjI3MiAyMC45MjhsLTE1OS4xNjggMTc5LjQ1NmExMi44IDEyLjggMCAwIDEtMjIuMTQ0LTUuODg4bC00OC4wNjQtMjM1LjA3MmExMi44IDEyLjggMCAwIDEgMTUuODA4LTE0LjkxMmw1Ny4yOCAxNS4zNnoiIGZpbGw9IiM3MDcwNzAiIHAtaWQ9IjEzNjEiPjwvcGF0aD48L3N2Zz4="
alt="刷新">
</div>
<div class="wg-cap-wrap__btn">
<button @click="handleConfirmEvent">确认</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'GoCaptcha',
props: {
value: Boolean,
width: {
type: String,
default: '300px'
},
height: {
type: String,
default: '240px'
},
calcPosType: {
type: String,
default: 'dom',
validator: value => ['dom', 'screen'].includes(value)
},
maxDot: {
type: Number,
default: 5
// validator: value => value > 10
},
imageBase64: String,
thumbBase64: String
},
data() {
return {
dots: [],
imageBase64Code: '',
thumbBase64Code: ''
}
},
watch: {
value() {
this.dots = []
this.imageBase64Code = ''
this.thumbBase64Code = ''
},
imageBase64(val) {
this.dots = []
this.imageBase64Code = val
},
thumbBase64(val) {
this.dots = []
this.thumbBase64Code = val
}
},
computed: {
style() {
return `width:${this.width}; height:${this.height};`
}
},
methods: {
/**
* @Description: 处理关闭事件
*/
handleCloseEvent() {
this.$emit('close')
this.dots = []
this.imageBase64Code = ''
this.thumbBase64Code = ''
},
/**
* @Description: 处理刷新事件
*/
handleRefreshEvent() {
this.dots = []
this.$emit('refresh')
},
/**
* @Description: 处理确认事件
*/
handleConfirmEvent() {
this.$emit('confirm', this.dots)
},
/**
* @Description: 处理dot
* @param ev
*/
handleClickPos(ev) {
if (this.dots.length >= this.maxDot) {
return
}
const e = ev || window.event
e.preventDefault()
const dom = e.currentTarget
const {domX, domY} = this.getDomXY(dom)
// ===============================================
// @notice getDomXY 使 calcLocationLeft calcLocationTop
// const domX = this.calcLocationLeft(dom)
// const domY = this.calcLocationTop(dom)
// ===============================================
let mouseX = (navigator.appName === 'Netscape') ? e.pageX : e.x + document.body.offsetTop
let mouseY = (navigator.appName === 'Netscape') ? e.pageY : e.y + document.body.offsetTop
if (this.calcPosType === 'screen') {
mouseX = (navigator.appName === 'Netscape') ? e.clientX : e.x
mouseY = (navigator.appName === 'Netscape') ? e.clientY : e.y
}
//
const xPos = mouseX - domX
const yPos = mouseY - domY
//
const xp = parseInt(xPos.toString())
const yp = parseInt(yPos.toString())
//
this.dots.push({
x: xp - 11,
y: yp - 11,
index: this.dots.length + 1
})
return false
},
/**
* @Description: 找到元素的屏幕位置
* @param el
*/
calcLocationLeft(el) {
let tmp = el.offsetLeft
let val = el.offsetParent
while (val != null) {
tmp += val.offsetLeft
val = val.offsetParent
}
return tmp
},
/**
* @Description: 找到元素的屏幕位置
* @param el
*/
calcLocationTop(el) {
let tmp = el.offsetTop
let val = el.offsetParent
while (val != null) {
tmp += val.offsetTop
val = val.offsetParent
}
return tmp
},
/**
* @Description: 找到元素的屏幕位置
* @param dom
*/
getDomXY(dom){
let x = 0
let y = 0
if (dom.getBoundingClientRect) {
let box = dom.getBoundingClientRect();
let D = document.documentElement;
x = box.left + Math.max(D.scrollLeft, document.body.scrollLeft) - D.clientLeft;
y = box.top + Math.max(D.scrollTop, document.body.scrollTop) - D.clientTop
}
else{
while (dom !== document.body) {
x += dom.offsetLeft
y += dom.offsetTop
dom = dom.offsetParent
}
}
return {
domX: x,
domY: y
}
}
}
}
</script>
<style>
.wg-cap-wrap{
width: 100%;
height: 100%;
background: #ffffff;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.wg-cap-wrap__header{
height: 50px;
width: 100%;
font-size: 15px;
display:-webkit-box;
display:-webkit-flex;
display:-ms-flexbox;
display:flex;
-webkit-box-align:center;
-webkit-align-items:center;
-ms-flex-align:center;
align-items: center;
}
.wg-cap-wrap__header span{
padding-right: 5px;
}
.wg-cap-wrap__header span em{
padding: 0 3px;
font-weight: bold;
color: #3e7cff;
font-style: normal;
}
.wg-cap-wrap__header .wg-cap-wrap__image{
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
overflow: hidden;
text-align: center;
line-height: 1;
}
.wg-cap-wrap__header .wg-cap-wrap__thumb{
min-width: 150px;
text-align: center;
line-height: 1;
max-height: 100%;
}
.wg-cap-wrap__header .wg-cap-wrap__thumb.wg-cap-wrap__hidden{
display: none;
}
.wg-cap-wrap__body{
position: relative;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
background: #34383e;
margin: auto;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
overflow: hidden;
}
.wg-cap-wrap__body .wg-cap-wrap__picture{
position: relative;
z-index: 10;
width: 100%;
/*height: 100%;*/
/*max-width: 100%;*/
/*max-height: 100%;*/
/*object-fit: cover;*/
/*text-align: center;*/
}
.wg-cap-wrap__body .wg-cap-wrap__picture.wg-cap-wrap__hidden{
display: none;
}
.wg-cap-wrap__body .wg-cap-wrap__loading{
position: absolute;
z-index: 9;
top: 50%;
left: 50%;
width: 68px;
height: 68px;
margin-left: -34px;
margin-top: -34px;
line-height: 68px;
text-align: center;
}
.wg-cap-wrap__body .wg-cap-wrap__dot{
position: absolute;
z-index: 10;
width: 22px;
height: 22px;
color: #cedffe;
background: #3e7cff;
border: 2px solid #f7f9fb;
line-height: 20px;
text-align: center;
-webkit-border-radius: 22px;
-moz-border-radius: 22px;
border-radius: 22px;
cursor: default;
}
.wg-cap-wrap__footer {
width: 100%;
height: 40px;
color: #34383e;
display:-webkit-box;
display:-webkit-flex;
display:-ms-flexbox;
display:flex;
-webkit-box-align:center;
-webkit-align-items:center;
-ms-flex-align:center;
align-items: center;
padding-top: 15px;
}
.wg-cap-wrap__footer .wg-cap-wrap__ico{
flex: 1;
}
.wg-cap-wrap__footer .wg-cap-wrap__ico img{
width: 24px;
height: 24px;
color: #34383e;
margin: 0 5px;
cursor: pointer;
}
.wg-cap-wrap__footer .wg-cap-wrap__btn{
width: 120px;
height: 40px;
}
.wg-cap-wrap__footer .wg-cap-wrap__btn button{
width: 100%;
height: 40px;
letter-spacing: 2px;
text-align: center;
padding: 9px 15px;
font-size: 15px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
color: #fff;
background-color: #409eff;
border: 1px solid #409eff;
-webkit-appearance: none;
box-sizing: border-box;
outline: none;
margin: 0;
transition: .1s;
font-weight: 500;
-moz-user-select: none;
-webkit-user-select: none;
}
.wg-cap-wrap__footer .wg-cap-wrap__btn button:hover {
background: #66b1ff;
border-color: #66b1ff;
color: #fff;
}
</style>

View File

@ -0,0 +1,334 @@
<template>
<div class="wg-cap-btn" :style="style">
<div class="wg-cap-btn__inner" :class="activeClass">
<!-- wg-cap-active__default wg-cap-active__error wg-cap-active__over wg-cap-active__success -->
<el-popover
:visible="popoverVisible"
placement="top"
width="330px"
@hide="handleCloseEvent"
trigger="click">
<go-captcha
v-model="popoverVisible"
width="300px"
height="240px"
:max-dot="maxDot"
:image-base64="captchaData.captBase64"
:thumb-base64="captchaData.captThumbBase64"
@close="handleCloseEvent"
@refresh="handleRefreshEvent"
@confirm="handleConfirmEvent"
/>
<template v-slot:reference>
<div @click="handleBtnEvent" class="wg-cap-state__default" v-if="captStatus==='default'">
<!-- 初始状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico wg-cap-btn__verify">
<img
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSLlm77lsYJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiCgkgdmlld0JveD0iMCAwIDIwMCAyMDAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDIwMCAyMDA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojM0U3Q0ZGO30KCS5zdDF7ZmlsbDojRkZGRkZGO30KPC9zdHlsZT4KPGNpcmNsZSBjbGFzcz0ic3QwIiBjeD0iMTAwIiBjeT0iMTAwIiByPSI5Ni4zIi8+CjxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0xNDAuOCw2NC40bC0zOS42LTExLjloLTIuNEw1OS4yLDY0LjRjLTEuNiwwLjgtMi44LDIuNC0yLjgsNHYyNC4xYzAsMjUuMywxNS44LDQ1LjksNDIuMyw1NC42CgljMC40LDAsMC44LDAuNCwxLjIsMC40YzAuNCwwLDAuOCwwLDEuMi0wLjRjMjYuNS04LjcsNDIuMy0yOC45LDQyLjMtNTQuNlY2OC4zQzE0My41LDY2LjgsMTQyLjMsNjUuMiwxNDAuOCw2NC40eiIvPgo8L3N2Zz4K">
</div>
<span class="wg-cap-btn__text">点击按键进行人机验证</span>
</div>
</div>
<div @click="()=>false" class="wg-cap-state__check" v-if="captStatus==='check'">
<!-- 验证状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico">
<img
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI3MDU1NTg2NTk0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEyMTEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTIwLjI1OTQ1NiA1MTIuMDAxMDIzbS0xMTcuOTIzNzYgMGExMTUuMjM4IDExNS4yMzggMCAxIDAgMjM1Ljg0NzUxOSAwIDExNS4yMzggMTE1LjIzOCAwIDEgMC0yMzUuODQ3NTE5IDBaIiBwLWlkPSIxMjEyIiBmaWxsPSIjZmZhMDAwIj48L3BhdGg+PHBhdGggZD0iTTUxMS45OTk0ODggNTEyLjAwMTAyM20tMTE3LjkyMTcxMyAwYTExNS4yMzYgMTE1LjIzNiAwIDEgMCAyMzUuODQzNDI2IDAgMTE1LjIzNiAxMTUuMjM2IDAgMSAwLTIzNS44NDM0MjYgMFoiIHAtaWQ9IjEyMTMiIGZpbGw9IiNmZmEwMDAiPjwvcGF0aD48cGF0aCBkPSJNOTAzLjczOTUyMSA1MTIuMDAxMDIzbS0xMTcuOTIzNzYgMGExMTUuMjM4IDExNS4yMzggMCAxIDAgMjM1Ljg0NzUxOSAwIDExNS4yMzggMTE1LjIzOCAwIDEgMC0yMzUuODQ3NTE5IDBaIiBwLWlkPSIxMjE0IiBmaWxsPSIjZmZhMDAwIj48L3BhdGg+PC9zdmc+"
alt="">
</div>
<span class="wg-cap-btn__text">正在进行人机验证...</span>
</div>
</div>
<div @click="handleBtnEvent" class="wg-cap-state__error" v-if="captStatus==='error'">
<!-- 验证失败状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico">
<img
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6I0VENDYzMDt9Cjwvc3R5bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xODQsMjYuNkwxMDIuNCwyLjFoLTQuOUwxNiwyNi42Yy0zLjMsMS42LTUuNyw0LjktNS43LDguMnY0OS44YzAsNTIuMiwzMi42LDk0LjcsODcuMywxMTIuNgoJYzAuOCwwLDEuNiwwLjgsMi40LDAuOHMxLjYsMCwyLjQtMC44YzU0LjctMTgsODcuMy01OS42LDg3LjMtMTEyLjZWMzQuN0MxODkuOCwzMS41LDE4Ny4zLDI4LjIsMTg0LDI2LjZ6IE0xMzQuNSwxMjMuMQoJYzMuMSwzLjEsMy4xLDguMiwwLDExLjNjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zcy00LjEtMC44LTUuNy0yLjNMMTAwLDExMS4zbC0yMy4xLDIzLjFjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zCgljLTIsMC00LjEtMC44LTUuNy0yLjNjLTMuMS0zLjEtMy4xLTguMiwwLTExLjNMODguNywxMDBMNjUuNSw3Ni45Yy0zLjEtMy4xLTMuMS04LjIsMC0xMS4zYzMuMS0zLjEsOC4yLTMuMSwxMS4zLDBMMTAwLDg4LjcKCWwyMy4xLTIzLjFjMy4xLTMuMSw4LjItMy4xLDExLjMsMGMzLjEsMy4xLDMuMSw4LjIsMCwxMS4zTDExMS4zLDEwMEwxMzQuNSwxMjMuMXoiLz4KPC9zdmc+Cg=="
alt="失败">
</div>
<span>人机验证失败 <em>点击重试</em></span>
</div>
</div>
<div @click="handleBtnEvent" class="wg-cap-state__over" v-if="captStatus==='over'">
<!-- 验证次数过多状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico">
<img
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6I0VENDYzMDt9Cjwvc3R5bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xODQsMjYuNkwxMDIuNCwyLjFoLTQuOUwxNiwyNi42Yy0zLjMsMS42LTUuNyw0LjktNS43LDguMnY0OS44YzAsNTIuMiwzMi42LDk0LjcsODcuMywxMTIuNgoJYzAuOCwwLDEuNiwwLjgsMi40LDAuOHMxLjYsMCwyLjQtMC44YzU0LjctMTgsODcuMy01OS42LDg3LjMtMTEyLjZWMzQuN0MxODkuOCwzMS41LDE4Ny4zLDI4LjIsMTg0LDI2LjZ6IE0xMzQuNSwxMjMuMQoJYzMuMSwzLjEsMy4xLDguMiwwLDExLjNjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zcy00LjEtMC44LTUuNy0yLjNMMTAwLDExMS4zbC0yMy4xLDIzLjFjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zCgljLTIsMC00LjEtMC44LTUuNy0yLjNjLTMuMS0zLjEtMy4xLTguMiwwLTExLjNMODguNywxMDBMNjUuNSw3Ni45Yy0zLjEtMy4xLTMuMS04LjIsMC0xMS4zYzMuMS0zLjEsOC4yLTMuMSwxMS4zLDBMMTAwLDg4LjcKCWwyMy4xLTIzLjFjMy4xLTMuMSw4LjItMy4xLDExLjMsMGMzLjEsMy4xLDMuMSw4LjIsMCwxMS4zTDExMS4zLDEwMEwxMzQuNSwxMjMuMXoiLz4KPC9zdmc+Cg=="
alt="失败">
</div>
<span>点击次数过多 <em>点击重试</em></span>
</div>
</div>
<div @click="()=>false" class="wg-cap-state__success" v-if="captStatus==='success'">
<!-- 验证成功状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico">
<img
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6IzVFQUEyRjt9Cjwvc3R5bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xODMuMywyNy4yTDEwMi40LDIuOWgtNC45TDE2LjcsMjcuMkMxMy40LDI4LjgsMTEsMzIsMTEsMzUuM3Y0OS40YzAsNTEuOCwzMi40LDkzLjksODYuNiwxMTEuNwoJYzAuOCwwLDEuNiwwLjgsMi40LDAuOGMwLjgsMCwxLjYsMCwyLjQtMC44YzU0LjItMTcuOCw4Ni42LTU5LjEsODYuNi0xMTEuN1YzNS4zQzE4OSwzMiwxODYuNiwyOC44LDE4My4zLDI3LjJ6IE0xNDYuMSw4MS40CglsLTQ4LjUsNDguNWMtMS42LDEuNi0zLjIsMi40LTUuNywyLjRjLTIuNCwwLTQtMC44LTUuNy0yLjRMNjIsMTA1LjdjLTMuMi0zLjItMy4yLTguMSwwLTExLjNjMy4yLTMuMiw4LjEtMy4yLDExLjMsMGwxOC42LDE4LjYKCWw0Mi45LTQyLjljMy4yLTMuMiw4LjEtMy4yLDExLjMsMEMxNDkuNCw3My4zLDE0OS40LDc4LjIsMTQ2LjEsODEuNEwxNDYuMSw4MS40eiIvPgo8L3N2Zz4K"
alt="成功">
</div>
<span>人机验证已通过</span>
</div>
</div>
</template>
</el-popover>
</div>
</div>
</template>
<script>
import GoCaptcha from './GoCaptcha.vue'
import {captchaV2, checkCaptchaV2} from "/@/api/login";
import {ElMessage} from "element-plus";
export default {
name: 'GoCaptchaBtn',
components: {GoCaptcha},
props: {
modelValue: {
type: String,
default: 'default',
validator: value => ['default', 'check', 'error', 'over', 'success'].indexOf(value) > -1
},
width: String,
height: String,
maxDot: {
type: Number,
default: 5
}
},
emits:['update:modelValue','handleConfirm'],
data() {
return {
popoverVisible: false,
captStatus: 'default',
captchaData: {
captBase64: '',
captThumbBase64: '',
captKey: '',
captStatus: 'default',
}
}
},
watch: {
popoverVisible(val) {
if (val) {
this.captStatus = 'check'
}else if(this.captStatus!=='success'){
this.captStatus = 'default'
}
},
modelValue(val) {
window.console.log(val)
if (this.captStatus !== 'check') {
this.captStatus = val
}
if (val === 'over' || val === 'success') {
setTimeout(() => {
this.popoverVisible = false
}, 0)
}
},
captStatus(val) {
if (val !== 'check' && this.value !== val) {
this.$emit('update:modelValue', val)
}
}
},
computed: {
style() {
return `width:${this.width}; height:${this.height};`
},
activeClass() {
let activeClass = this.captStatus
return `wg-cap-active__${activeClass}`
}
},
methods: {
handleBtnEvent() {
this.popoverVisible = false
setTimeout(() => {
this.popoverVisible = true
this.handleRefreshEvent()
}, 200)
},
handleRefreshEvent() {
this.captStatus = 'check'
this.captchaData.captBase64 = ''
this.captchaData.captThumbBase64 = ''
this.captchaData.captKey = ''
captchaV2().then(res=>{
if(res.code === 0) {
this.captchaData.captBase64 = res.data['img'] || ''
this.captchaData.captThumbBase64 = res.data['thumb'] || ''
this.captchaData.captKey = res.data['key'] || ''
}
})
},
handleConfirmEvent(dots) {
if (dots.length <= 0) {
ElMessage.error('请进行人机验证再操作')
return
}
const str = btoa(encodeURIComponent(JSON.stringify(dots)))
checkCaptchaV2({key:this.captchaData.captKey,dots:str}).then(res=>{
if(res.code === 0) {
ElMessage.success('人机验证成功')
this.captStatus='success'
this.popoverVisible = false
this.$emit('handleConfirm',{key:this.captchaData.captKey,dots:str})
}
}).catch(err=>{
window.console.log(err)
this.captStatus='error'
})
},
handleCloseEvent() {
this.popoverVisible = false
}
}
}
</script>
<style scoped>
.wg-cap-btn {
width: 100%;
height: 48px;
}
.wg-cap-btn .wg-cap-btn__inner{
width: 100%;
height: 48px;
position: relative;
letter-spacing: 1px;
}
.wg-cap-btn .wg-cap-state__default,.wg-cap-btn .wg-cap-state__check, .wg-cap-btn .wg-cap-state__error, .wg-cap-btn .wg-cap-state__success, .wg-cap-btn .wg-cap-state__over{
width: 100%;
height: 48px;
cursor: pointer;
}
.wg-cap-btn .wg-cap-state__default{
color: #3e7cff;
border: 1px solid #50a1ff;
background: #ecf5ff;
box-shadow: 0 0 20px rgba(62, 124, 255, 0.1);
-webkit-box-shadow: 0 0 20px rgba(62, 124, 255, 0.1);
-moz-box-shadow: 0 0 20px rgba(62, 124, 255, 0.1);
}
.wg-cap-btn .wg-cap-state__check{
cursor: default;
color: #ffa000;
background: #fdf6ec;
border: 1px solid #ffbe09;
pointer-events: none;
}
.wg-cap-btn .wg-cap-state__error{
color: #ed4630;
background: #fef0f0;
border: 1px solid #ff5a34;
}
.wg-cap-btn .wg-cap-state__over{
color: #ed4630;
background: #fef0f0;
border: 1px solid #ff5a34;
}
.wg-cap-btn .wg-cap-state__success{
color: #5eaa2f;
background: #f0f9eb;
border: 1px solid #8bc640;
pointer-events: none;
}
.wg-cap-btn .wg-cap-active__default .wg-cap-state__default, .wg-cap-btn .wg-cap-active__error .wg-cap-state__error,.wg-cap-btn .wg-cap-active__over .wg-cap-state__over ,.wg-cap-btn .wg-cap-active__success .wg-cap-state__success,.wg-cap-btn .wg-cap-active__check .wg-cap-state__check {
visibility: visible;
}
.wg-cap-btn .wg-cap-state__inner{
display:-webkit-box;
display:-webkit-flex;
display:-ms-flexbox;
display:flex;
-webkit-box-align:center;
-webkit-align-items:center;
-ms-flex-align:center;
align-items: center;
justify-content: center;
justify-items: center;
}
.wg-cap-btn .wg-cap-state__inner em{
padding-left: 5px;
color: #3e7cff;
font-style: normal;
}
.wg-cap-btn .wg-cap-btn__inner .wg-cap-btn__ico{
position: relative;
width: 24px;
height: 24px;
margin-right: 12px;
font-size: 14px;
display: inline-block;
float: left;
flex: 0;
}
.wg-cap-btn .wg-cap-btn__inner .wg-cap-btn__ico img{
width: 24px;
height: 24px;
float: left;
position: relative;
z-index: 10;
}
@keyframes ripple {
0% { opacity: 0; }
5% { opacity: 0.05; }
20% { opacity: 0.35; }
65% { opacity: 0.01; }
100% {
transform: scaleX(2) scaleY(2);
opacity: 0;
}
}
@-webkit-keyframes ripple {
0% { opacity: 0; }
5% { opacity: 0.05; }
20% { opacity: 0.35; }
65% { opacity: 0.01; }
100% {
transform: scaleX(2) scaleY(2);
opacity: 0;
}
}
.wg-cap-btn .wg-cap-btn__inner .wg-cap-btn__verify::after {
background: #409eff;
-webkit-border-radius: 50px;
-moz-border-radius: 50px;
border-radius: 50px;
content: "";
display: block;
width: 24px;
height: 24px;
opacity: 0;
position: absolute;
top: 0;
left: 0;
z-index: 9;
animation: ripple 1.3s infinite;
-moz-animation: ripple 1.3s infinite;
-webkit-animation: ripple 1.3s infinite;
animation-delay: 2s;
-moz-animation-delay: 2s;
-webkit-animation-delay: 2s;
}
.wg-cap-tip{
padding: 50px 20px 100px;
font-size: 13px;
color: #76839b;
text-align: center;
line-height: 180%;
width: 100%;
max-width: 680px;
}
</style>

View File

@ -0,0 +1,331 @@
<template>
<div class="wg-cap-btn" :style="style">
<div class="wg-cap-btn__inner" :class="activeClass">
<!-- wg-cap-active__default wg-cap-active__error wg-cap-active__over wg-cap-active__success -->
<template>
<div @click="handleBtnEvent" class="wg-cap-state__default">
<!-- 初始状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico wg-cap-btn__verify">
<img
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSLlm77lsYJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiCgkgdmlld0JveD0iMCAwIDIwMCAyMDAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDIwMCAyMDA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojM0U3Q0ZGO30KCS5zdDF7ZmlsbDojRkZGRkZGO30KPC9zdHlsZT4KPGNpcmNsZSBjbGFzcz0ic3QwIiBjeD0iMTAwIiBjeT0iMTAwIiByPSI5Ni4zIi8+CjxwYXRoIGNsYXNzPSJzdDEiIGQ9Ik0xNDAuOCw2NC40bC0zOS42LTExLjloLTIuNEw1OS4yLDY0LjRjLTEuNiwwLjgtMi44LDIuNC0yLjgsNHYyNC4xYzAsMjUuMywxNS44LDQ1LjksNDIuMyw1NC42CgljMC40LDAsMC44LDAuNCwxLjIsMC40YzAuNCwwLDAuOCwwLDEuMi0wLjRjMjYuNS04LjcsNDIuMy0yOC45LDQyLjMtNTQuNlY2OC4zQzE0My41LDY2LjgsMTQyLjMsNjUuMiwxNDAuOCw2NC40eiIvPgo8L3N2Zz4K">
</div>
<span class="wg-cap-btn__text">点击按键进行人机验证</span>
</div>
</div>
<div @click="()=>false" class="wg-cap-state__check">
<!-- 验证状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico">
<img
src="data:image/svg+xml;base64,PHN2ZyB0PSIxNjI3MDU1NTg2NTk0IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEyMTEiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMTIwLjI1OTQ1NiA1MTIuMDAxMDIzbS0xMTcuOTIzNzYgMGExMTUuMjM4IDExNS4yMzggMCAxIDAgMjM1Ljg0NzUxOSAwIDExNS4yMzggMTE1LjIzOCAwIDEgMC0yMzUuODQ3NTE5IDBaIiBwLWlkPSIxMjEyIiBmaWxsPSIjZmZhMDAwIj48L3BhdGg+PHBhdGggZD0iTTUxMS45OTk0ODggNTEyLjAwMTAyM20tMTE3LjkyMTcxMyAwYTExNS4yMzYgMTE1LjIzNiAwIDEgMCAyMzUuODQzNDI2IDAgMTE1LjIzNiAxMTUuMjM2IDAgMSAwLTIzNS44NDM0MjYgMFoiIHAtaWQ9IjEyMTMiIGZpbGw9IiNmZmEwMDAiPjwvcGF0aD48cGF0aCBkPSJNOTAzLjczOTUyMSA1MTIuMDAxMDIzbS0xMTcuOTIzNzYgMGExMTUuMjM4IDExNS4yMzggMCAxIDAgMjM1Ljg0NzUxOSAwIDExNS4yMzggMTE1LjIzOCAwIDEgMC0yMzUuODQ3NTE5IDBaIiBwLWlkPSIxMjE0IiBmaWxsPSIjZmZhMDAwIj48L3BhdGg+PC9zdmc+"
alt="">
</div>
<span class="wg-cap-btn__text">正在进行人机验证...</span>
</div>
</div>
<div @click="handleBtnEvent" class="wg-cap-state__error">
<!-- 验证失败状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico">
<img
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6I0VENDYzMDt9Cjwvc3R5bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xODQsMjYuNkwxMDIuNCwyLjFoLTQuOUwxNiwyNi42Yy0zLjMsMS42LTUuNyw0LjktNS43LDguMnY0OS44YzAsNTIuMiwzMi42LDk0LjcsODcuMywxMTIuNgoJYzAuOCwwLDEuNiwwLjgsMi40LDAuOHMxLjYsMCwyLjQtMC44YzU0LjctMTgsODcuMy01OS42LDg3LjMtMTEyLjZWMzQuN0MxODkuOCwzMS41LDE4Ny4zLDI4LjIsMTg0LDI2LjZ6IE0xMzQuNSwxMjMuMQoJYzMuMSwzLjEsMy4xLDguMiwwLDExLjNjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zcy00LjEtMC44LTUuNy0yLjNMMTAwLDExMS4zbC0yMy4xLDIzLjFjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zCgljLTIsMC00LjEtMC44LTUuNy0yLjNjLTMuMS0zLjEtMy4xLTguMiwwLTExLjNMODguNywxMDBMNjUuNSw3Ni45Yy0zLjEtMy4xLTMuMS04LjIsMC0xMS4zYzMuMS0zLjEsOC4yLTMuMSwxMS4zLDBMMTAwLDg4LjcKCWwyMy4xLTIzLjFjMy4xLTMuMSw4LjItMy4xLDExLjMsMGMzLjEsMy4xLDMuMSw4LjIsMCwxMS4zTDExMS4zLDEwMEwxMzQuNSwxMjMuMXoiLz4KPC9zdmc+Cg=="
alt="失败">
</div>
<span>人机验证失败 <em>点击重试</em></span>
</div>
</div>
<div @click="handleBtnEvent" class="wg-cap-state__over">
<!-- 验证次数过多状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico">
<img
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6I0VENDYzMDt9Cjwvc3R5bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xODQsMjYuNkwxMDIuNCwyLjFoLTQuOUwxNiwyNi42Yy0zLjMsMS42LTUuNyw0LjktNS43LDguMnY0OS44YzAsNTIuMiwzMi42LDk0LjcsODcuMywxMTIuNgoJYzAuOCwwLDEuNiwwLjgsMi40LDAuOHMxLjYsMCwyLjQtMC44YzU0LjctMTgsODcuMy01OS42LDg3LjMtMTEyLjZWMzQuN0MxODkuOCwzMS41LDE4Ny4zLDI4LjIsMTg0LDI2LjZ6IE0xMzQuNSwxMjMuMQoJYzMuMSwzLjEsMy4xLDguMiwwLDExLjNjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zcy00LjEtMC44LTUuNy0yLjNMMTAwLDExMS4zbC0yMy4xLDIzLjFjLTEuNiwxLjYtMy42LDIuMy01LjcsMi4zCgljLTIsMC00LjEtMC44LTUuNy0yLjNjLTMuMS0zLjEtMy4xLTguMiwwLTExLjNMODguNywxMDBMNjUuNSw3Ni45Yy0zLjEtMy4xLTMuMS04LjIsMC0xMS4zYzMuMS0zLjEsOC4yLTMuMSwxMS4zLDBMMTAwLDg4LjcKCWwyMy4xLTIzLjFjMy4xLTMuMSw4LjItMy4xLDExLjMsMGMzLjEsMy4xLDMuMSw4LjIsMCwxMS4zTDExMS4zLDEwMEwxMzQuNSwxMjMuMXoiLz4KPC9zdmc+Cg=="
alt="失败">
</div>
<span>点击次数过多 <em>点击重试</em></span>
</div>
</div>
<div @click="()=>false" class="wg-cap-state__success">
<!-- 验证成功状态 -->
<div class="wg-cap-state__inner">
<div class="wg-cap-btn__ico">
<img
src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6IzVFQUEyRjt9Cjwvc3R5bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0xODMuMywyNy4yTDEwMi40LDIuOWgtNC45TDE2LjcsMjcuMkMxMy40LDI4LjgsMTEsMzIsMTEsMzUuM3Y0OS40YzAsNTEuOCwzMi40LDkzLjksODYuNiwxMTEuNwoJYzAuOCwwLDEuNiwwLjgsMi40LDAuOGMwLjgsMCwxLjYsMCwyLjQtMC44YzU0LjItMTcuOCw4Ni42LTU5LjEsODYuNi0xMTEuN1YzNS4zQzE4OSwzMiwxODYuNiwyOC44LDE4My4zLDI3LjJ6IE0xNDYuMSw4MS40CglsLTQ4LjUsNDguNWMtMS42LDEuNi0zLjIsMi40LTUuNywyLjRjLTIuNCwwLTQtMC44LTUuNy0yLjRMNjIsMTA1LjdjLTMuMi0zLjItMy4yLTguMSwwLTExLjNjMy4yLTMuMiw4LjEtMy4yLDExLjMsMGwxOC42LDE4LjYKCWw0Mi45LTQyLjljMy4yLTMuMiw4LjEtMy4yLDExLjMsMEMxNDkuNCw3My4zLDE0OS40LDc4LjIsMTQ2LjEsODEuNEwxNDYuMSw4MS40eiIvPgo8L3N2Zz4K"
alt="成功">
</div>
<span>人机验证已通过</span>
</div>
</div>
</template>
<el-dialog
:visible.sync="popoverVisible"
:close-on-click-modal="false"
append-to-body
:center="true"
title="人机校验"
:show-close="false"
z-index="999999"
width="360px"
>
<go-captcha
v-model="popoverVisible"
width="300px"
height="240px"
:max-dot="maxDot"
:image-base64="imageBase64"
:thumb-base64="thumbBase64"
@close="handleCloseEvent"
@refresh="handleRefreshEvent"
@confirm="handleConfirmEvent"
/>
</el-dialog>
</div>
</div>
</template>
<script>
import GoCaptcha from './GoCaptcha.vue'
export default {
name: 'GoCaptchaBtnDialog',
components: {GoCaptcha},
props: {
value: {
type: String,
default: 'default',
validator: value => ['default', 'check', 'error', 'over', 'success'].indexOf(value) > -1
},
width: String,
height: String,
maxDot: {
type: Number,
default: 5
},
imageBase64: String,
thumbBase64: String
},
data() {
return {
popoverVisible: false,
captStatus: 'default'
}
},
watch: {
popoverVisible(val) {
if (val) {
this.captStatus = 'check'
this.$emit('refresh')
} else if (this.captStatus === 'check') {
this.captStatus = this.value
}
},
value(val) {
if (this.captStatus !== 'check') {
this.captStatus = val
}
if (val === 'over' || val === 'success') {
setTimeout(() => {
this.popoverVisible = false
}, 0)
}
},
captStatus(val) {
if (val !== 'check' && this.value !== val) {
this.$emit('input', val)
}
}
},
computed: {
style() {
return `width:${this.width}; height:${this.height};`
},
activeClass() {
let activeClass = this.captStatus
return `wg-cap-active__${activeClass}`
}
},
methods: {
handleBtnEvent() {
setTimeout(() => {
this.popoverVisible = true
}, 0)
},
handleRefreshEvent() {
this.captStatus = 'check'
this.$emit('refresh')
},
handleConfirmEvent(data) {
this.$emit('confirm', data)
},
handleCloseEvent() {
this.popoverVisible = false
}
}
}
</script>
<style>
.wg-cap-btn {
width: 100%;
height: 48px;
}
.wg-cap-btn .wg-cap-btn__inner{
width: 100%;
height: 48px;
position: relative;
letter-spacing: 1px;
}
.wg-cap-btn .wg-cap-state__default,.wg-cap-btn .wg-cap-state__check, .wg-cap-btn .wg-cap-state__error, .wg-cap-btn .wg-cap-state__success, .wg-cap-btn .wg-cap-state__over{
position: absolute;
width: 100%;
height: 48px;
font-size: 13px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
display: inline-block;
line-height: 1;
white-space: nowrap;
cursor: pointer;
-webkit-appearance: none;
box-sizing: border-box;
outline: none;
margin: 0;
transition: .1s;
font-weight: 500;
-moz-user-select: none;
-webkit-user-select: none;
display:-webkit-box;
display:-webkit-flex;
display:-ms-flexbox;
display:flex;
-webkit-box-align:center;
-webkit-align-items:center;
-ms-flex-align:center;
align-items: center;
justify-content: center;
justify-items: center;
visibility: hidden;
}
.wg-cap-btn .wg-cap-state__default{
color: #3e7cff;
border: 1px solid #50a1ff;
background: #ecf5ff;
box-shadow: 0 0 20px rgba(62, 124, 255, 0.1);
-webkit-box-shadow: 0 0 20px rgba(62, 124, 255, 0.1);
-moz-box-shadow: 0 0 20px rgba(62, 124, 255, 0.1);
}
.wg-cap-btn .wg-cap-state__check{
cursor: default;
color: #ffa000;
background: #fdf6ec;
border: 1px solid #ffbe09;
}
.wg-cap-btn .wg-cap-state__error{
color: #ed4630;
background: #fef0f0;
border: 1px solid #ff5a34;
}
.wg-cap-btn .wg-cap-state__over{
color: #ed4630;
background: #fef0f0;
border: 1px solid #ff5a34;
}
.wg-cap-btn .wg-cap-state__success{
color: #5eaa2f;
background: #f0f9eb;
border: 1px solid #8bc640
}
.wg-cap-btn .wg-cap-active__default .wg-cap-state__default, .wg-cap-btn .wg-cap-active__error .wg-cap-state__error,.wg-cap-btn .wg-cap-active__over .wg-cap-state__over ,.wg-cap-btn .wg-cap-active__success .wg-cap-state__success,.wg-cap-btn .wg-cap-active__check .wg-cap-state__check {
visibility: visible;
}
.wg-cap-btn .wg-cap-state__inner{
display:-webkit-box;
display:-webkit-flex;
display:-ms-flexbox;
display:flex;
-webkit-box-align:center;
-webkit-align-items:center;
-ms-flex-align:center;
align-items: center;
justify-content: center;
justify-items: center;
}
.wg-cap-btn .wg-cap-state__inner em{
padding-left: 5px;
color: #3e7cff;
font-style: normal;
}
.wg-cap-btn .wg-cap-btn__inner .wg-cap-btn__ico{
position: relative;
width: 24px;
height: 24px;
margin-right: 12px;
font-size: 14px;
display: inline-block;
float: left;
flex: 0;
}
.wg-cap-btn .wg-cap-btn__inner .wg-cap-btn__ico img{
width: 24px;
height: 24px;
float: left;
position: relative;
z-index: 10;
}
@keyframes ripple {
0% { opacity: 0; }
5% { opacity: 0.05; }
20% { opacity: 0.35; }
65% { opacity: 0.01; }
100% {
transform: scaleX(2) scaleY(2);
opacity: 0;
}
}
@-webkit-keyframes ripple {
0% { opacity: 0; }
5% { opacity: 0.05; }
20% { opacity: 0.35; }
65% { opacity: 0.01; }
100% {
transform: scaleX(2) scaleY(2);
opacity: 0;
}
}
.wg-cap-btn .wg-cap-btn__inner .wg-cap-btn__verify::after {
background: #409eff;
-webkit-border-radius: 50px;
-moz-border-radius: 50px;
border-radius: 50px;
content: "";
display: block;
width: 24px;
height: 24px;
opacity: 0;
position: absolute;
top: 0;
left: 0;
z-index: 9;
animation: ripple 1.3s infinite;
-moz-animation: ripple 1.3s infinite;
-webkit-animation: ripple 1.3s infinite;
animation-delay: 2s;
-moz-animation-delay: 2s;
-webkit-animation-delay: 2s;
}
.wg-cap-tip{
padding: 50px 20px 100px;
font-size: 13px;
color: #76839b;
text-align: center;
line-height: 180%;
width: 100%;
max-width: 680px;
}
</style>

View File

@ -33,28 +33,50 @@ export function handleTree(data:any[], id:string, parentId:string, children:stri
id = id || 'id'
parentId = parentId || 'parentId'
children = children || 'children'
rootId = rootId || 0
let rootIds:any = []
if(typeof rootId === 'boolean' && rootId){
//自动获取rootId
let idSet:any = {}
data.map((item:any)=>{
idSet[item[id]] = true
})
data.map((item:any)=>{
if(!idSet[item[parentId]]){
rootIds.push(item[parentId])
}
})
}else{
rootId = rootId || 0
rootIds = [rootId]
}
rootIds = [...new Set(rootIds)]
let treeData:any = []
//对源数据深度克隆
const cloneData = JSON.parse(JSON.stringify(data))
//循环所有项
const treeData = cloneData.filter((father:any) => {
let branchArr = cloneData.filter((child:any) => {
//返回每一项的子级数组
return father[id] === child[parentId]
rootIds.map((rItem:any)=>{
//循环所有项
const td = cloneData.filter((father:any) => {
let branchArr = cloneData.filter((child:any) => {
//返回每一项的子级数组
return father[id] === child[parentId]
});
branchArr.length > 0 ? father[children] = branchArr : '';
//返回第一层
switch (typeof father[parentId]){
case 'string':
if(father[parentId]===''&&rItem===0){
return true
}
return father[parentId]===rItem.toString();
case 'number':
return father[parentId] === rItem;
}
return false;
});
branchArr.length > 0 ? father[children] = branchArr : '';
//返回第一层
switch (typeof father[parentId]){
case 'string':
if(father[parentId]===''&&rootId===0){
return true
}
return father[parentId]===rootId.toString();
case 'number':
return father[parentId] === rootId;
if(td.length>0){
treeData = [...treeData,...td]
}
return false;
});
})
return treeData != '' ? treeData : data;
}

View File

@ -10,7 +10,7 @@ const service: AxiosInstance = axios.create({
headers: { 'Content-Type': 'application/json' },
paramsSerializer: {
serialize(params) {
return qs.stringify(params, { allowDots: true });
return qs.stringify(params, { allowDots: true,arrayFormat: 'brackets' });
},
},
});

View File

@ -33,7 +33,16 @@
</el-input>
</el-form-item>
<el-form-item class="login-animation3" prop="verifyCode">
<el-col :span="15">
<el-col :span="24">
<GoCaptchaBtn
class="go-captcha-btn"
v-model="checkCaptchaResult"
width="100%"
height="50px"
@handleConfirm="handleVerifyCodeConfirm"
/>
</el-col>
<el-col :span="15" v-if="false">
<el-input
type="text"
maxlength="4"
@ -48,8 +57,8 @@
</template>
</el-input>
</el-col>
<el-col :span="1"></el-col>
<el-col :span="8">
<el-col :span="1" v-if="false"></el-col>
<el-col :span="8" v-if="false">
<div class="login-content-code">
<img
class="login-content-code-img"
@ -57,7 +66,7 @@
width="130"
height="38"
:src="captchaSrc"
style="cursor: pointer"
style="cursor: pointer;display: none"
/>
</div>
</el-col>
@ -91,9 +100,11 @@ import { initBackEndControlRoutes } from '/@/router/backEnd';
import { Session } from '/@/utils/storage';
import { formatAxis } from '/@/utils/formatTime';
import { NextLoading } from '/@/utils/loading';
import {login,captcha} from "/@/api/login";
import {login} from "/@/api/login";
import GoCaptchaBtn from "/@/components/goCaptcha/GoCaptchaBtn.vue";
export default defineComponent({
name: 'loginAccount',
components: {GoCaptchaBtn},
setup() {
const { t } = useI18n();
const {proxy} = <any>getCurrentInstance();
@ -102,6 +113,7 @@ export default defineComponent({
const route = useRoute();
const router = useRouter();
const loginForm = ref(null)
const checkCaptchaResult = ref('default')
const state = reactive({
isShowPassword: false,
ruleForm: {
@ -117,7 +129,7 @@ export default defineComponent({
password: [
{ required: true, trigger: "blur", message: "密码不能为空" }
],
verifyCode: [{ required: true, trigger: "blur", message: "验证码不能为空" }]
verifyCode: [{ required: true, trigger: "blur", message: "请先进行人机验证" }]
},
loading: {
signIn: false,
@ -128,10 +140,11 @@ export default defineComponent({
getCaptcha();
});
const getCaptcha = () => {
captcha().then((res:any)=>{
state.captchaSrc = res.data.img
state.ruleForm.verifyKey = res.data.key
})
// V1
// captcha().then((res:any)=>{
// state.captchaSrc = res.data.img
// state.ruleForm.verifyKey = res.data.key
// })
};
//
const currentTime = computed(() => {
@ -173,7 +186,11 @@ export default defineComponent({
}
}).catch(()=>{
state.loading.signIn = false;
getCaptcha();
state.ruleForm.verifyKey = ''
state.ruleForm.verifyCode = ''
checkCaptchaResult.value = 'default'
// V1
//getCaptcha();
})
}
})
@ -200,9 +217,17 @@ export default defineComponent({
// loading
NextLoading.start();
};
const handleVerifyCodeConfirm = (data:{key:string,dots:string})=>{
state.ruleForm.verifyCode = data.dots
state.ruleForm.verifyKey = data.key
}
return {
onSignIn,
getCaptcha,
checkCaptchaResult,
handleVerifyCodeConfirm,
loginForm,
...toRefs(state),
};

View File

@ -3,6 +3,23 @@
<el-dialog :title="(formData.id===0?'添加':'修改')+'角色'" v-model="isShowDialog" width="769px">
<el-form ref="formRef" :model="formData" :rules="rules" size="default" label-width="90px">
<el-row :gutter="35">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="上级角色">
<el-cascader
:options="roleData"
:props="{ checkStrictly: true,emitPath: false, value: 'id', label: 'name' }"
placeholder="请选择上级"
clearable
class="w100"
v-model="formData.pid"
>
<template #default="{ node, data }">
<span>{{ data.name }}</span>
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
</template>
</el-cascader>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="角色名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入角色名称" clearable></el-input>
@ -72,6 +89,7 @@ interface MenuDataTree {
}
interface DialogRow {
id:number;
pid:number;
name: string;
status: number;
listOrder: number;
@ -96,6 +114,12 @@ interface RoleState {
export default defineComponent({
name: 'systemEditRole',
props:{
roleData:{
type:Array,
default:()=>[]
}
},
setup(props,{emit}) {
const {proxy} = getCurrentInstance() as any;
const formRef = ref<HTMLElement | null>(null);
@ -105,6 +129,7 @@ export default defineComponent({
isShowDialog: false,
formData: {
id:0,
pid:0,
name: '',
status: 1,
listOrder: 0,
@ -198,6 +223,7 @@ export default defineComponent({
state.menuNodeAll = false;
state.formData = {
id:0,
pid:0,
name: '',
status: 1,
listOrder: 0,

View File

@ -28,8 +28,10 @@
</el-form-item>
</el-form>
</div>
<el-table :data="tableData.data" style="width: 100%">
<el-table-column type="index" label="序号" width="60" />
<el-table :data="tableData.data" style="width: 100%"
row-key="id"
default-expand-all
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }">
<el-table-column prop="name" label="角色名称" show-overflow-tooltip></el-table-column>
<el-table-column prop="listOrder" label="排序" show-overflow-tooltip></el-table-column>
<el-table-column prop="userCnt" label="用户数量" align="center">
@ -54,15 +56,8 @@
</template>
</el-table-column>
</el-table>
<pagination
v-show="tableData.total>0"
:total="tableData.total"
v-model:page="tableData.param.pageNum"
v-model:limit="tableData.param.pageSize"
@pagination="roleList"
/>
</el-card>
<EditRole ref="editRoleRef" @getRoleList="roleList"/>
<EditRole ref="editRoleRef" @getRoleList="roleList" :roleData="tableData.data"/>
<DataScope ref="dataScopeRef" @getRoleList="roleList"/>
<el-dialog :title="selectRow.name+'-用户列表'" v-model="isShowDialog" width="70vw">
@ -83,6 +78,7 @@ import UserList from '/@/views/system/user/component/userList.vue';
//
interface TableData {
id:number;
pid:number;
status: number;
listOrder: number;
name: string;
@ -151,6 +147,7 @@ export default defineComponent({
list.map((item:TableData)=>{
data.push({
id:item.id,
pid:item.pid,
status: item.status,
listOrder: item.listOrder,
name: item.name,
@ -160,8 +157,7 @@ export default defineComponent({
createdAt: item.createdAt,
});
})
state.tableData.data = data;
state.tableData.total = res.data.total;
state.tableData.data = proxy.handleTree(data??[], "id","pid","children",true);
})
};
//

View File

@ -20,15 +20,19 @@
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="关联角色" prop="roleIds">
<el-select v-model="ruleForm.roleIds" placeholder="请选择" clearable class="w100" multiple>
<el-option
v-for="role in roleList"
:key="'role-'+role.id"
:label="role.name"
:value="role.id"
:disabled="role.disabled">
</el-option>
</el-select>
<el-cascader
:options="roleList"
:props="{ checkStrictly: true,emitPath: false, value: 'id', label: 'name',multiple: true }"
placeholder="请选择角色"
clearable
class="w100"
v-model="ruleForm.roleIds"
>
<template #default="{ node, data }">
<span>{{ data.name }}</span>
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
</template>
</el-cascader>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
@ -116,7 +120,7 @@
</template>
<script lang="ts">
import { reactive, toRefs, onMounted, defineComponent,ref,unref } from 'vue';
import {reactive, toRefs, onMounted, defineComponent, ref, unref, getCurrentInstance} from 'vue';
import {getParams, addUser, editUser, getEditUser} from "/@/api/system/user";
import {ElMessage} from "element-plus";
@ -134,6 +138,7 @@ export default defineComponent({
}
},
setup(prop,{emit}) {
const {proxy} = getCurrentInstance() as any;
const roleList = ref([]);
const postList = ref([]);
const formRef = ref<HTMLElement | null>(null);
@ -253,7 +258,7 @@ export default defineComponent({
item.disabled = true
}
})
roleList.value = roles
roleList.value = proxy.handleTree(roles??[], "id","pid","children",true);
postList.value = res.data.posts??[];
});
};