feat: vue3 add aj-captcha

This commit is contained in:
xingyu 2022-08-02 17:07:05 +08:00
parent 02224ad6b5
commit 414671e881
13 changed files with 1331 additions and 57 deletions

View File

@ -38,6 +38,7 @@
"element-plus": "2.2.12",
"intro.js": "^6.0.0",
"jsencrypt": "^3.2.1",
"crypto-js": "^4.1.1",
"lodash-es": "^4.17.21",
"mitt": "^3.0.0",
"nprogress": "^0.2.0",

View File

@ -18,11 +18,6 @@ export interface SmsLoginVO {
code: string
}
// 获取验证码
export const getCodeImgApi = () => {
return request.get({ url: '/system/captcha/get-image' })
}
// 登录
export const loginApi = (data: UserLoginVO) => {
return request.post({ url: '/system/auth/login', data })

View File

@ -1,8 +1,6 @@
export type UserLoginVO = {
username: string
password: string
code: string
uuid: string
}
export type TokenType = {

View File

@ -0,0 +1,3 @@
import Verify from './src/Verify.vue'
export { Verify }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,279 @@
<template>
<div style="position: relative">
<div class="verify-img-out">
<div
class="verify-img-panel"
:style="{
width: setSize.imgWidth,
height: setSize.imgHeight,
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
'margin-bottom': vSpace + 'px'
}"
>
<div class="verify-refresh" style="z-index: 3" @click="refresh" v-show="showRefresh">
<i class="iconfont icon-refresh"></i>
</div>
<img
:src="'data:image/png;base64,' + pointBackImgBase"
ref="canvas"
alt=""
style="width: 100%; height: 100%; display: block"
@click="bindingClick ? canvasClick($event) : undefined"
/>
<div
v-for="(tempPoint, index) in tempPoints"
:key="index"
class="point-area"
:style="{
'background-color': '#1abd6c',
color: '#fff',
'z-index': 9999,
width: '20px',
height: '20px',
'text-align': 'center',
'line-height': '20px',
'border-radius': '50%',
position: 'absolute',
top: parseInt(tempPoint.y - 10) + 'px',
left: parseInt(tempPoint.x - 10) + 'px'
}"
>
{{ index + 1 }}
</div>
</div>
</div>
<!-- 'height': this.barSize.height, -->
<div
class="verify-bar-area"
:style="{
width: setSize.imgWidth,
color: barAreaColor,
'border-color': barAreaBorderColor,
'line-height': barSize.height
}"
>
<span class="verify-msg">{{ text }}</span>
</div>
</div>
</template>
<script type="text/babel">
/**
* VerifyPoints
* @description 点选
* */
import { resetSize } from './../utils/util'
import { aesEncrypt } from './../utils/ase'
import { reqGet, reqCheck } from './../api/index'
import { onMounted, reactive, ref, nextTick, toRefs, getCurrentInstance } from 'vue'
export default {
name: 'VerifyPoints',
props: {
//popfixed
mode: {
type: String,
default: 'fixed'
},
captchaType: {
type: String
},
//
vSpace: {
type: Number,
default: 5
},
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px'
}
}
},
barSize: {
type: Object,
default() {
return {
width: '310px',
height: '40px'
}
}
}
},
setup(props) {
const { mode, captchaType } = toRefs(props)
const { proxy } = getCurrentInstance()
let secretKey = ref(''), //ase
checkNum = ref(3), //
fontPos = reactive([]), //
checkPosArr = reactive([]), //
num = ref(1), //
pointBackImgBase = ref(''), //
poinTextList = reactive([]), //
backToken = ref(''), //token
setSize = reactive({
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0
}),
tempPoints = reactive([]),
text = ref(''),
barAreaColor = ref(undefined),
barAreaBorderColor = ref(undefined),
showRefresh = ref(true),
bindingClick = ref(true)
const init = () => {
//
fontPos.splice(0, fontPos.length)
checkPosArr.splice(0, checkPosArr.length)
num.value = 1
getPictrue()
nextTick(() => {
let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
setSize.imgHeight = imgHeight
setSize.imgWidth = imgWidth
setSize.barHeight = barHeight
setSize.barWidth = barWidth
proxy.$parent.$emit('ready', proxy)
})
}
onMounted(() => {
//
init()
proxy.$el.onselectstart = function () {
return false
}
})
const canvas = ref(null)
const canvasClick = (e) => {
checkPosArr.push(getMousePos(canvas, e))
if (num.value == checkNum.value) {
num.value = createPoint(getMousePos(canvas, e))
//
let arr = pointTransfrom(checkPosArr, setSize)
checkPosArr.length = 0
checkPosArr.push(...arr)
//
setTimeout(() => {
// var flag = this.comparePos(this.fontPos, this.checkPosArr);
//
var captchaVerification = secretKey.value
? aesEncrypt(backToken.value + '---' + JSON.stringify(checkPosArr), secretKey.value)
: backToken.value + '---' + JSON.stringify(checkPosArr)
let data = {
captchaType: captchaType.value,
pointJson: secretKey.value
? aesEncrypt(JSON.stringify(checkPosArr), secretKey.value)
: JSON.stringify(checkPosArr),
token: backToken.value
}
reqCheck(data).then((res) => {
if (res.repCode == '0000') {
barAreaColor.value = '#4cae4c'
barAreaBorderColor.value = '#5cb85c'
text.value = '验证成功'
bindingClick.value = false
if (mode.value == 'pop') {
setTimeout(() => {
proxy.$parent.clickShow = false
refresh()
}, 1500)
}
proxy.$parent.$emit('success', { captchaVerification })
} else {
proxy.$parent.$emit('error', proxy)
barAreaColor.value = '#d9534f'
barAreaBorderColor.value = '#d9534f'
text.value = '验证失败'
setTimeout(() => {
refresh()
}, 700)
}
})
}, 400)
}
if (num.value < checkNum.value) {
num.value = createPoint(getMousePos(canvas, e))
}
}
//
const getMousePos = function (obj, e) {
var x = e.offsetX
var y = e.offsetY
return { x, y }
}
//
const createPoint = function (pos) {
tempPoints.push(Object.assign({}, pos))
return num.value + 1
}
const refresh = function () {
tempPoints.splice(0, tempPoints.length)
barAreaColor.value = '#000'
barAreaBorderColor.value = '#ddd'
bindingClick.value = true
fontPos.splice(0, fontPos.length)
checkPosArr.splice(0, checkPosArr.length)
num.value = 1
getPictrue()
text.value = '验证失败'
showRefresh.value = true
}
//
function getPictrue() {
let data = {
captchaType: captchaType.value
}
reqGet(data).then((res) => {
if (res.repCode == '0000') {
pointBackImgBase.value = res.repData.originalImageBase64
backToken.value = res.repData.token
secretKey.value = res.repData.secretKey
poinTextList.value = res.repData.wordList
text.value = '请依次点击【' + poinTextList.value.join(',') + '】'
} else {
text.value = res.repMsg
}
})
}
//
const pointTransfrom = function (pointArr, imgSize) {
var newPointArr = pointArr.map((p) => {
let x = Math.round((310 * p.x) / parseInt(imgSize.imgWidth))
let y = Math.round((155 * p.y) / parseInt(imgSize.imgHeight))
return { x, y }
})
return newPointArr
}
return {
secretKey,
checkNum,
fontPos,
checkPosArr,
num,
pointBackImgBase,
poinTextList,
backToken,
setSize,
tempPoints,
text,
barAreaColor,
barAreaBorderColor,
showRefresh,
bindingClick,
init,
canvas,
canvasClick,
getMousePos,
createPoint,
refresh,
getPictrue,
pointTransfrom
}
}
}
</script>

View File

@ -0,0 +1,422 @@
<template>
<div style="position: relative">
<div
v-if="type === '2'"
class="verify-img-out"
:style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }"
>
<div class="verify-img-panel" :style="{ width: setSize.imgWidth, height: setSize.imgHeight }">
<img
:src="'data:image/png;base64,' + backImgBase"
alt=""
style="width: 100%; height: 100%; display: block"
/>
<div class="verify-refresh" @click="refresh" v-show="showRefresh"
><i class="iconfont icon-refresh"></i>
</div>
<transition name="tips">
<span class="verify-tips" v-if="tipWords" :class="passFlag ? 'suc-bg' : 'err-bg'">{{
tipWords
}}</span>
</transition>
</div>
</div>
<!-- 公共部分 -->
<div
class="verify-bar-area"
:style="{ width: setSize.imgWidth, height: barSize.height, 'line-height': barSize.height }"
>
<span class="verify-msg" v-text="text"></span>
<div
class="verify-left-bar"
:style="{
width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
height: barSize.height,
'border-color': leftBarBorderColor,
transaction: transitionWidth
}"
>
<span class="verify-msg" v-text="finishText"></span>
<div
class="verify-move-block"
@touchstart="start"
@mousedown="start"
:style="{
width: barSize.height,
height: barSize.height,
'background-color': moveBlockBackgroundColor,
left: moveBlockLeft,
transition: transitionLeft
}"
>
<i :class="['verify-icon iconfont', iconClass]" :style="{ color: iconColor }"></i>
<div
v-if="type === '2'"
class="verify-sub-block"
:style="{
width: Math.floor((parseInt(setSize.imgWidth) * 47) / 310) + 'px',
height: setSize.imgHeight,
top: '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight
}"
>
<img
:src="'data:image/png;base64,' + blockBackImgBase"
alt=""
style="width: 100%; height: 100%; display: block; -webkit-user-drag: none"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script type="text/babel">
/**
* VerifySlide
* @description 滑块
* */
import { aesEncrypt } from './../utils/ase'
import { resetSize } from './../utils/util'
import { reqGet, reqCheck } from './../api/index'
import {
computed,
onMounted,
reactive,
ref,
watch,
nextTick,
toRefs,
getCurrentInstance
} from 'vue'
// "captchaType":"blockPuzzle",
export default {
name: 'VerifySlide',
props: {
captchaType: {
type: String
},
type: {
type: String,
default: '1'
},
//popfixed
mode: {
type: String,
default: 'fixed'
},
vSpace: {
type: Number,
default: 5
},
explain: {
type: String,
default: '向右滑动完成验证'
},
imgSize: {
type: Object,
default() {
return {
width: '310px',
height: '155px'
}
}
},
blockSize: {
type: Object,
default() {
return {
width: '50px',
height: '50px'
}
}
},
barSize: {
type: Object,
default() {
return {
width: '310px',
height: '40px'
}
}
}
},
setup(props) {
const { mode, captchaType, type, blockSize, explain } = toRefs(props)
const { proxy } = getCurrentInstance()
let secretKey = ref(''), //ase
passFlag = ref(''), //
backImgBase = ref(''), //
blockBackImgBase = ref(''), //
backToken = ref(''), //token
startMoveTime = ref(''), //
endMovetime = ref(''), //
tipsBackColor = ref(''), //
tipWords = ref(''),
text = ref(''),
finishText = ref(''),
setSize = reactive({
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0
}),
top = ref(0),
left = ref(0),
moveBlockLeft = ref(undefined),
leftBarWidth = ref(undefined),
//
moveBlockBackgroundColor = ref(undefined),
leftBarBorderColor = ref('#ddd'),
iconColor = ref(undefined),
iconClass = ref('icon-right'),
status = ref(false), //
isEnd = ref(false), //
showRefresh = ref(true),
transitionLeft = ref(''),
transitionWidth = ref(''),
startLeft = ref(0)
const barArea = computed(() => {
return proxy.$el.querySelector('.verify-bar-area')
})
function init() {
text.value = explain.value
getPictrue()
nextTick(() => {
let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
setSize.imgHeight = imgHeight
setSize.imgWidth = imgWidth
setSize.barHeight = barHeight
setSize.barWidth = barWidth
proxy.$parent.$emit('ready', proxy)
})
window.removeEventListener('touchmove', function (e) {
move(e)
})
window.removeEventListener('mousemove', function (e) {
move(e)
})
//
window.removeEventListener('touchend', function () {
end()
})
window.removeEventListener('mouseup', function () {
end()
})
window.addEventListener('touchmove', function (e) {
move(e)
})
window.addEventListener('mousemove', function (e) {
move(e)
})
//
window.addEventListener('touchend', function () {
end()
})
window.addEventListener('mouseup', function () {
end()
})
}
watch(type, () => {
init()
})
onMounted(() => {
//
init()
proxy.$el.onselectstart = function () {
return false
}
})
//
function start(e) {
e = e || window.event
if (!e.touches) {
//PC
var x = e.clientX
} else {
//
var x = e.touches[0].pageX
}
console.log(barArea)
startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left)
startMoveTime.value = +new Date() //
if (isEnd.value == false) {
text.value = ''
moveBlockBackgroundColor.value = '#337ab7'
leftBarBorderColor.value = '#337AB7'
iconColor.value = '#fff'
e.stopPropagation()
status.value = true
}
}
//
function move(e) {
e = e || window.event
if (status.value && isEnd.value == false) {
if (!e.touches) {
//PC
var x = e.clientX
} else {
//
var x = e.touches[0].pageX
}
var bar_area_left = barArea.value.getBoundingClientRect().left
var move_block_left = x - bar_area_left //left
if (
move_block_left >=
barArea.value.offsetWidth - parseInt(parseInt(blockSize.value.width) / 2) - 2
) {
move_block_left =
barArea.value.offsetWidth - parseInt(parseInt(blockSize.value.width) / 2) - 2
}
if (move_block_left <= 0) {
move_block_left = parseInt(parseInt(blockSize.value.width) / 2)
}
//left
moveBlockLeft.value = move_block_left - startLeft.value + 'px'
leftBarWidth.value = move_block_left - startLeft.value + 'px'
}
}
//
function end() {
endMovetime.value = +new Date()
//
if (status.value && isEnd.value == false) {
var moveLeftDistance = parseInt((moveBlockLeft.value || '').replace('px', ''))
moveLeftDistance = (moveLeftDistance * 310) / parseInt(setSize.imgWidth)
let data = {
captchaType: captchaType.value,
pointJson: secretKey.value
? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value)
: JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
token: backToken.value
}
reqCheck(data).then((res) => {
if (res.repCode == '0000') {
moveBlockBackgroundColor.value = '#5cb85c'
leftBarBorderColor.value = '#5cb85c'
iconColor.value = '#fff'
iconClass.value = 'icon-check'
showRefresh.value = false
isEnd.value = true
if (mode.value == 'pop') {
setTimeout(() => {
proxy.$parent.clickShow = false
refresh()
}, 1500)
}
passFlag.value = true
tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(
2
)}s验证成功`
var captchaVerification = secretKey.value
? aesEncrypt(
backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
secretKey.value
)
: backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 })
setTimeout(() => {
tipWords.value = ''
proxy.$parent.closeBox()
proxy.$parent.$emit('success', { captchaVerification })
}, 1000)
} else {
moveBlockBackgroundColor.value = '#d9534f'
leftBarBorderColor.value = '#d9534f'
iconColor.value = '#fff'
iconClass.value = 'icon-close'
passFlag.value = false
setTimeout(function () {
refresh()
}, 1000)
proxy.$parent.$emit('error', proxy)
tipWords.value = '验证失败'
setTimeout(() => {
tipWords.value = ''
}, 1000)
}
})
status.value = false
}
}
const refresh = () => {
showRefresh.value = true
finishText.value = ''
transitionLeft.value = 'left .3s'
moveBlockLeft.value = 0
leftBarWidth.value = undefined
transitionWidth.value = 'width .3s'
leftBarBorderColor.value = '#ddd'
moveBlockBackgroundColor.value = '#fff'
iconColor.value = '#000'
iconClass.value = 'icon-right'
isEnd.value = false
getPictrue()
setTimeout(() => {
transitionWidth.value = ''
transitionLeft.value = ''
text.value = explain.value
}, 300)
}
//
function getPictrue() {
let data = {
captchaType: captchaType.value
}
reqGet(data).then((res) => {
if (res.repCode == '0000') {
backImgBase.value = res.repData.originalImageBase64
blockBackImgBase.value = res.repData.jigsawImageBase64
backToken.value = res.repData.token
secretKey.value = res.repData.secretKey
} else {
tipWords.value = res.repMsg
}
})
}
return {
secretKey, //ase
passFlag, //
backImgBase, //
blockBackImgBase, //
backToken, //token
startMoveTime, //
endMovetime, //
tipsBackColor, //
tipWords,
text,
finishText,
setSize,
top,
left,
moveBlockLeft,
leftBarWidth,
//
moveBlockBackgroundColor,
leftBarBorderColor,
iconColor,
iconClass,
status, //
isEnd, //
showRefresh,
transitionLeft,
transitionWidth,
barArea,
refresh,
start
}
}
}
</script>

View File

@ -0,0 +1,4 @@
import VerifySlide from './VerifySlide.vue'
import VerifyPoints from './VerifyPoints.vue'
export { VerifySlide, VerifyPoints }

View File

@ -0,0 +1,24 @@
/**
* axios
*/
import request from './../utils/axios' //组件内部封装的axios
// import request from "@/api/axios.js" //调用项目封装的axios
//获取验证图片 以及token
export function reqGet(data) {
return request({
url: '/captcha/get',
method: 'post',
data
})
}
//滑动或者点选验证
export function reqCheck(data) {
return request({
url: '/captcha/check',
method: 'post',
data
})
}

View File

@ -0,0 +1,14 @@
import CryptoJS from 'crypto-js'
/**
* @word
* @keyWord String
* */
export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') {
const key = CryptoJS.enc.Utf8.parse(keyWord)
const srcs = CryptoJS.enc.Utf8.parse(word)
const encrypted = CryptoJS.AES.encrypt(srcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
return encrypted.toString()
}

View File

@ -0,0 +1,26 @@
import axios from 'axios'
axios.defaults.baseURL = import.meta.env.VITE_BASE_URL
const service = axios.create({
timeout: 40000,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json; charset=UTF-8'
}
})
service.interceptors.request.use(
(config) => {
return config
},
(error) => {
Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use((response) => {
const res = response.data
return res
})
export default service

View File

@ -0,0 +1,97 @@
export function resetSize(vm) {
let img_width, img_height, bar_width, bar_height //图片的宽度、高度,移动条的宽度、高度
const EmployeeWindow = window as any
const parentWidth = vm.$el.parentNode.offsetWidth || EmployeeWindow.offsetWidth
const parentHeight = vm.$el.parentNode.offsetHeight || EmployeeWindow.offsetHeight
if (vm.imgSize.width.indexOf('%') != -1) {
img_width = (parseInt(vm.imgSize.width) / 100) * parentWidth + 'px'
} else {
img_width = vm.imgSize.width
}
if (vm.imgSize.height.indexOf('%') != -1) {
img_height = (parseInt(vm.imgSize.height) / 100) * parentHeight + 'px'
} else {
img_height = vm.imgSize.height
}
if (vm.barSize.width.indexOf('%') != -1) {
bar_width = (parseInt(vm.barSize.width) / 100) * parentWidth + 'px'
} else {
bar_width = vm.barSize.width
}
if (vm.barSize.height.indexOf('%') != -1) {
bar_height = (parseInt(vm.barSize.height) / 100) * parentHeight + 'px'
} else {
bar_height = vm.barSize.height
}
return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height }
}
export const _code_chars = [
1,
2,
3,
4,
5,
6,
7,
8,
9,
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z'
]
export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0']
export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC']

View File

@ -30,6 +30,7 @@ import { required } from '@/utils/formRules'
import { Icon } from '@/components/Icon'
import { LoginStateEnum, useLoginState, useFormValid } from './useLogin'
import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
import { Verify } from '@/components/Verifition'
const { currentRoute, addRoute, push } = useRouter()
const permissionStore = usePermissionStore()
@ -46,12 +47,10 @@ const { t } = useI18n()
const iconHouse = useIcon({ icon: 'ep:house' })
const iconAvatar = useIcon({ icon: 'ep:avatar' })
const iconLock = useIcon({ icon: 'ep:lock' })
const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
const LoginCaptchaRules = {
tenantName: [required],
username: [required],
password: [required],
code: [required]
password: [required]
}
const LoginRules = {
tenantName: [required],
@ -60,7 +59,6 @@ const LoginRules = {
}
const loginLoading = ref(false)
const loginData = reactive({
codeImg: '',
isShowPassword: false,
captchaEnable: true,
tenantEnable: true,
@ -72,20 +70,16 @@ const loginData = reactive({
tenantName: '芋道源码',
username: 'admin',
password: 'admin123',
rememberMe: false,
code: '',
uuid: ''
rememberMe: false
}
})
// blockPuzzle clickWord
const verify = ref()
const captchaType = ref('blockPuzzle')
//
const getCode = async () => {
const res = await LoginApi.getCodeImgApi()
loginData.captchaEnable = res.enable
if (res.enable) {
loginData.codeImg = 'data:image/gif;base64,' + res.img
loginData.loginForm.uuid = res.uuid
}
verify.value.show()
}
//ID
const getTenantId = async () => {
@ -112,19 +106,12 @@ const handleLogin = async () => {
const data = await validForm()
if (!data) return
loginLoading.value = true
await LoginApi.loginApi(loginData.loginForm)
.then(async (res) => {
setToken(res)
const userInfo = await LoginApi.getInfoApi()
await userStore.getUserInfoAction(userInfo)
await getRoutes()
})
.catch(() => {
getCode()
})
.finally(() => {
loginLoading.value = false
})
const res = await LoginApi.loginApi(loginData.loginForm)
setToken(res)
const userInfo = await LoginApi.getInfoApi()
await userStore.getUserInfoAction(userInfo)
await getRoutes()
loginLoading.value = false
}
//
@ -159,8 +146,7 @@ watch(
immediate: true
}
)
onMounted(async () => {
await getCode()
onMounted(() => {
getCookie()
})
</script>
@ -207,31 +193,11 @@ onMounted(async () => {
type="password"
:placeholder="t('login.passwordPlaceholder')"
show-password
@keyup.enter="handleLogin"
@keyup.enter="getCode()"
:prefix-icon="iconLock"
/>
</el-form-item>
</el-col>
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item prop="code" v-if="loginData.captchaEnable">
<el-row justify="space-between" style="width: 100%">
<el-col :span="14">
<el-input
v-model="loginData.loginForm.code"
:placeholder="t('login.codePlaceholder')"
@keyup.enter="handleLogin"
:prefix-icon="iconCircleCheck"
style="width: 90%"
/>
</el-col>
<el-col :span="10">
<div class="login-code">
<img :src="loginData.codeImg" @click="getCode" class="login-code-img" alt="" />
</div>
</el-col>
</el-row>
</el-form-item>
</el-col>
<el-col
:span="24"
style="padding-left: 10px; padding-right: 10px; margin-top: -20px; margin-bottom: -20px"
@ -251,11 +217,18 @@ onMounted(async () => {
</el-col>
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item>
<el-button :loading="loginLoading" type="primary" class="w-[100%]" @click="handleLogin">
<el-button :loading="loginLoading" type="primary" class="w-[100%]" @click="getCode()">
{{ t('login.login') }}
</el-button>
</el-form-item>
</el-col>
<Verify
ref="verify"
mode="pop"
:captchaType="captchaType"
:imgSize="{ width: '400px', height: '200px' }"
@success="handleLogin"
/>
<el-col :span="24" style="padding-left: 10px; padding-right: 10px">
<el-form-item>
<el-row justify="space-between" style="width: 100%" :gutter="5">