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", "pinia": "^2.0.34",
"print-js": "^1.6.0", "print-js": "^1.6.0",
"qrcodejs2-fixes": "^0.0.2", "qrcodejs2-fixes": "^0.0.2",
"qs": "^6.11.1",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",
@ -36,13 +37,12 @@
"vue": "^3.2.47", "vue": "^3.2.47",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-codemirror": "^6.1.1", "vue-codemirror": "^6.1.1",
"vue-demi": "^0.13.11",
"vue-grid-layout": "^3.0.0-beta1", "vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.2.2", "vue-i18n": "^9.2.2",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
"vue-simple-uploader": "^1.0.0-beta.5", "vue-simple-uploader": "^1.0.0-beta.5",
"vue-ueditor-wrap": "^3.0.8", "vue-ueditor-wrap": "^3.0.8"
"qs": "^6.11.1",
"vue-demi": "^0.13.11"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.15.11", "@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,11 +33,29 @@ export function handleTree(data:any[], id:string, parentId:string, children:stri
id = id || 'id' id = id || 'id'
parentId = parentId || 'parentId' parentId = parentId || 'parentId'
children = children || 'children' children = children || 'children'
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 rootId = rootId || 0
rootIds = [rootId]
}
rootIds = [...new Set(rootIds)]
let treeData:any = []
//对源数据深度克隆 //对源数据深度克隆
const cloneData = JSON.parse(JSON.stringify(data)) const cloneData = JSON.parse(JSON.stringify(data))
rootIds.map((rItem:any)=>{
//循环所有项 //循环所有项
const treeData = cloneData.filter((father:any) => { const td = cloneData.filter((father:any) => {
let branchArr = cloneData.filter((child:any) => { let branchArr = cloneData.filter((child:any) => {
//返回每一项的子级数组 //返回每一项的子级数组
return father[id] === child[parentId] return father[id] === child[parentId]
@ -46,15 +64,19 @@ export function handleTree(data:any[], id:string, parentId:string, children:stri
//返回第一层 //返回第一层
switch (typeof father[parentId]){ switch (typeof father[parentId]){
case 'string': case 'string':
if(father[parentId]===''&&rootId===0){ if(father[parentId]===''&&rItem===0){
return true return true
} }
return father[parentId]===rootId.toString(); return father[parentId]===rItem.toString();
case 'number': case 'number':
return father[parentId] === rootId; return father[parentId] === rItem;
} }
return false; return false;
}); });
if(td.length>0){
treeData = [...treeData,...td]
}
})
return treeData != '' ? treeData : data; return treeData != '' ? treeData : data;
} }

View File

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

View File

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

View File

@ -28,8 +28,10 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<el-table :data="tableData.data" style="width: 100%"> <el-table :data="tableData.data" style="width: 100%"
<el-table-column type="index" label="序号" width="60" /> 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="name" label="角色名称" show-overflow-tooltip></el-table-column>
<el-table-column prop="listOrder" 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"> <el-table-column prop="userCnt" label="用户数量" align="center">
@ -54,15 +56,8 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </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> </el-card>
<EditRole ref="editRoleRef" @getRoleList="roleList"/> <EditRole ref="editRoleRef" @getRoleList="roleList" :roleData="tableData.data"/>
<DataScope ref="dataScopeRef" @getRoleList="roleList"/> <DataScope ref="dataScopeRef" @getRoleList="roleList"/>
<el-dialog :title="selectRow.name+'-用户列表'" v-model="isShowDialog" width="70vw"> <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 { interface TableData {
id:number; id:number;
pid:number;
status: number; status: number;
listOrder: number; listOrder: number;
name: string; name: string;
@ -151,6 +147,7 @@ export default defineComponent({
list.map((item:TableData)=>{ list.map((item:TableData)=>{
data.push({ data.push({
id:item.id, id:item.id,
pid:item.pid,
status: item.status, status: item.status,
listOrder: item.listOrder, listOrder: item.listOrder,
name: item.name, name: item.name,
@ -160,8 +157,7 @@ export default defineComponent({
createdAt: item.createdAt, createdAt: item.createdAt,
}); });
}) })
state.tableData.data = data; state.tableData.data = proxy.handleTree(data??[], "id","pid","children",true);
state.tableData.total = res.data.total;
}) })
}; };
// //

View File

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