feat(login): 实现滑动验证码功能

- 新增滑动验证码
This commit is contained in:
xingyy 2025-03-12 10:18:30 +08:00
parent 7675d6687b
commit e9896d86d6
25 changed files with 468 additions and 25 deletions

View File

@ -2,7 +2,7 @@
<div class="puzzle-container"> <div class="puzzle-container">
<div class="puzzle-box" :style="{ height: boxHeight + 'px' }"> <div class="puzzle-box" :style="{ height: boxHeight + 'px' }">
<!-- 背景图 --> <!-- 背景图 -->
<img :src="bgImageUrl" style="width: 320px;height: 189px;" ref="bgImage" @load="onImageLoad" @error="handleImageError"> <img :src="bgImageUrl" style="width: 320px;height: 191px;" ref="bgImage" @load="onImageLoad" @error="handleImageError">
<!-- 滑块 --> <!-- 滑块 -->
<img <img
class="slider-block" class="slider-block"
@ -10,9 +10,12 @@
:style="{ :style="{
top: `${blockY}px`, top: `${blockY}px`,
left: `${moveX}px`, left: `${moveX}px`,
visibility: loaded ? 'visible' : 'hidden' visibility: loaded ? 'visible' : 'hidden',
width: '50px',
height: '50px'
}" }"
></img> ></img>
<div v-if="verifySuccess || verifyError" :class="`text-#fff ${verifySuccess?'bg-#52C41A':'bg-#FF4D4F'} h-24px w-100% text-14px absolute left-0 bottom-0 text-center leading-24px`">{{ verifyTip }}</div>
</div> </div>
<!-- 滑动条 --> <!-- 滑动条 -->
@ -31,10 +34,7 @@
<div class="slider-icon"></div> <div class="slider-icon"></div>
</div> </div>
</div> </div>
<!-- 验证结果提示 -->
<div v-if="verifySuccess || verifyError" class="verify-result-bar" :class="{ 'success': verifySuccess, 'error': verifyError }">
{{ verifyTip }}
</div>
</div> </div>
</div> </div>
</template> </template>
@ -75,6 +75,15 @@ const verifySuccess = ref(false)
const verifyError = ref(false) const verifyError = ref(false)
const verifyTip = ref('') const verifyTip = ref('')
//
const reset = () => {
moveX.value = 0
verifySuccess.value = false
verifyError.value = false
verifyTip.value = ''
isDragging.value = false
}
// DOM // DOM
const bgImage = ref(null) const bgImage = ref(null)
@ -169,14 +178,18 @@ const handleMouseUp = async () => {
verifySuccess.value = false verifySuccess.value = false
verifyError.value = true verifyError.value = true
verifyTip.value = '验证失败' verifyTip.value = '验证失败'
moveX.value = 0 //
} }
}) })
} catch (error) { } catch (error) {
verifySuccess.value = false verifySuccess.value = false
verifyError.value = true verifyError.value = true
verifyTip.value = '验证失败' verifyTip.value = '验证失败'
moveX.value = 0 }finally{
setTimeout(() => {
reset()
}, 2000)
} }
} }
@ -222,8 +235,7 @@ onBeforeUnmount(() => {
} }
.puzzle-container { .puzzle-container {
width: 100%; position: relative;
max-width: 320px;
margin: 0 auto; margin: 0 auto;
background: #fff; background: #fff;
padding: 15px; padding: 15px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,418 @@
<template>
<view class="slider-verify-box" v-if="isShow">
<view :style="`background-image: url('../../static/image/slider-verify/${
$i18n.locale
}/${isError ? 'bg_error' : 'bg'}.png');`" class="verifyBox">
<!-- <view class="slider-title">{{ $t('authentication.title') }}</view> -->
<image class="slider-verify-box-close" src="/static/image/icon/close.png" @click.stop="hideSliderBox">
</image>
<view class="slide-content">
<view class="slider-pintu">
<!-- <u-icon
name="reload"
size="32"
color="#fff"
class="reload"
@tap="refreshVerify"
v-if="!isLoading"
></u-icon> -->
<image src="../../static/image/slider-verify/reload.png" mode="widthFix" style="width: 38rpx"
class="reload" @tap="refreshVerify" v-if="!isLoading"></image>
<view class="load" v-if="isLoading">
<van-loading type="spinner" />
<!-- <u-loading-icon text="" textSize="16" :vertical="true"></u-loading-icon> -->
</view>
<template v-else>
<image id="pintuImg" :src="canvasSrc" class="pintu"></image>
<view class="pintukuai" :style="{ top: '0px', left: oldx + 'px' }">
<image :src="blockSrc" :style="{
top: blockY + 'px',
left: oldx + 'px',
width: blockWidth + 'px',
height: blockHeight + 'px'
}"></image>
</view>
<view class="mark" v-if="isMess">
<image :src="`../../static/image/slider-verify/${$i18n.locale}/error.png`" mode="widthFix"
v-if="isError"></image>
<image :src="`../../static/image/slider-verify/${$i18n.locale}/success.png`" mode="widthFix"
v-else></image>
</view>
</template>
</view>
<view class="slider-movearea" @touchend="endTouchMove">
<movable-area :animation="true">
<movable-view :class="
isLoading
? 'movable-view btn_info'
: isError
? 'movable-view btn_error'
: 'movable-view btn_success'
" :x="x" direction="horizontal" @change="startMove"></movable-view>
</movable-area>
<view class="huadao">{{
$t('authentication.content')
}}</view>
<view :class="
isError ? 'huadao_done error_bg' : 'huadao_done'
" :style="'width:' + doneWindth + 'px;'"></view>
</view>
</view>
</view>
</view>
</template>
<script>
import {
postDataByParams
} from '../../utils/api.js'
export default {
name: 'slider-verify',
props: {
isShow: {
type: Boolean,
default: true
}
},
data() {
return {
x: 0, //
oldx: 0, //
top: 0, //top
canvasSrc: '',
blockSrc: '',
blockY: '',
nonceStr: '',
blockWidth: 50, //(blockWidth14)
blockHeight: 50,
isLoading: true,
doneWindth: 0,
isError: false,
isMess: false
}
},
watch: {
//
isShow(newValue, oldValue) {
if (newValue) {
this.refreshVerify() //
}
}
},
mounted() {
var that = this
// that.refreshVerify();
that.getCaptcha()
// console.log(this.$i18n.locale, 'this.$i18n.locale')
},
methods: {
async getCaptcha() {
this.isLoading = true
var that = this
let url = 'generate/captcha'
let params = {
canvasWidth: 320, //(canvasWidth41canvasWidth-10/2 - 1> blockWidth)
canvasHeight: 190, //(canvasHeight26 canvasHeight - blockHeight > 11
blockWidth: this.blockWidth, //(blockWidth14)
blockHeight: this.blockHeight, //(blockHeight14)
// blockRadius: 9, //
place: 0 //0URL1 0
}
postDataByParams(url, params)
.then((res) => {
if (res.status === 0) {
that.canvasSrc = 'data:image/jpg;base64,' + res.data.canvasSrc
that.blockSrc = 'data:image/jpg;base64,' + res.data.blockSrc
that.blockY = res.data.blockY
that.nonceStr = res.data.nonceStr
that.isLoading = false
} else {
this.$emit('sliderError', encodeURIComponent(JSON.stringify(res)))
}
})
.catch((err) => {
this.$emit('sliderError', encodeURIComponent(JSON.stringify(err)), true)
});
},
//
refreshVerify() {
this.x = 1
this.oldx = 1
setTimeout(() => {
this.x = 0
this.oldx = 0
this.doneWindth = 0
}, 300)
this.getCaptcha()
this.isError = false
this.isMess = false
},
/* 滑动中 */
startMove(e) {
// console.log(e.detail.x)
this.oldx = e.detail.x / 2 - 5
if (e.detail.x > 1) {
this.doneWindth = e.detail.x + 30
} else {
this.doneWindth = 0
}
},
/* 滑动结束 */
async endTouchMove() {
var that = this
// console.log('')
let url = 'validate/captcha'
let params = {
nonceStr: this.nonceStr,
blockX: this.oldx * 2 + 5
}
postDataByParams(url, params)
.then((res) => {
this.isMess = true
if (res.status == 0) {
setTimeout(() => {
that.$emit('touchSliderResult', res.data.nonceStr)
}, 1000)
} else {
this.isError = true
this.$emit('sliderError', encodeURIComponent(JSON.stringify(res)))
}
})
.catch((err) => {
this.$emit('sliderError', encodeURIComponent(JSON.stringify(err)), true)
});
},
/* 重置阴影位置 */
/* resetMove() {
this.x = 1;
this.oldx = 1;
setTimeout(() => {
this.x = 0;
this.oldx = 0;
}, 300);
}, */
//
closeSlider() {
this.$emit('touchSliderResult', false)
},
//
hideSliderBox() {
this.$emit('hideSliderBox')
}
}
}
</script>
<style lang="scss">
.slider-verify-box {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.error_box {
// background: url('../../static/image/slider-verify/bg_error.png') no-repeat !important;
background-size: 100% 100% !important;
}
.verifyBox {
// width: 588rpx;
// height: 662rpx;
padding: 218rpx 45rpx 30rpx 45rpx;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
// width: 85%;
// background-color: #fff;
background-repeat: no-repeat;
// background: url('../../static/image/slider-verify/bg.png') no-repeat;
background-size: 100% 100%;
border-radius: 20upx;
// box-shadow: 0 0 5upx rgba(0, 0, 0, 1);
.slider-verify-box-close {
width: 40rpx;
height: 40rpx;
position: absolute;
top: 0;
right: 0;
}
.slider-title {
font-size: 36upx;
text-align: center;
padding: 12rpx 0;
color: #000000;
}
.slide-content {
// width: 560rpx;
// padding: 0 ;
// margin: 0 auto;
.slide-tips {
font-size: 28rpx;
color: rgba(2, 20, 33, 0.45);
padding: 0.5em 0;
}
.slider-pintu {
position: relative;
width: 100%;
border-radius: 10rpx;
overflow: hidden;
.reload {
position: absolute;
right: 10rpx;
top: 10rpx;
z-index: 110;
}
.load {
width: 320px;
height: 190px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
background: #000000;
}
.pintu {
width: 320px;
height: 190px;
display: block;
margin: 0 auto;
}
.pintukuai {
position: absolute;
/* top: 0;
left: 0; */
z-index: 100;
box-shadow: 0 0 5upx rgba(0, 0, 0, 0.3);
image {
display: block;
position: absolute;
top: 0;
left: 0;
/* width: 560rpx;
height: 315rpx; */
}
}
.mark {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #000000;
z-index: 101;
display: flex;
align-items: center;
justify-content: center;
image {
width: 60%;
height: 50rpx;
}
}
}
.yinying {
position: absolute;
width: 120rpx;
height: 120rpx;
background-color: rgba(0, 0, 0, 0.5);
}
}
}
.slider-movearea {
position: relative;
height: 80upx;
width: 320px;
margin-top: 25rpx;
movable-area {
width: 100%;
height: 64rpx;
.movable-view {
width: 100upx;
height: 74rpx;
border-radius: 40upx;
// background-color: #699A70;
background-image: url(../../static/image/slider-verify/icon-button-normal.png);
background-repeat: no-repeat;
background-size: auto 30upx;
background-position: center;
position: relative;
z-index: 100;
border: 1px solid transparent;
}
}
}
.btn_info {
background-color: #878787 !important;
}
.btn_error {
background-color: #fd343c !important;
}
.btn_success {
background-color: #699a70 !important;
}
.error_bg {
background-color: #ffb7ba !important;
}
.success_bg {
background-color: #aad0b0 !important;
}
.huadao {
width: 100%;
// width: 320px;
height: 66upx;
line-height: 66upx;
background: #ededed;
// box-shadow: inset 0 0 5upx #EDEDED;
border-radius: 40rpx;
color: #bcbcbc;
text-align: center;
box-sizing: border-box;
position: absolute;
top: 7rpx;
left: 0;
font-size: 28rpx;
z-index: 99;
}
.huadao_done {
height: 66upx;
line-height: 66upx;
background: #aad0b0;
border-radius: 40rpx;
text-align: center;
box-sizing: border-box;
position: absolute;
top: 7rpx;
left: 0;
font-size: 28rpx;
z-index: 99;
}
</style>

View File

@ -3,7 +3,7 @@ import { useRouter, useRoute } from 'vue-router';
import Vcode from "vue3-puzzle-vcode"; import Vcode from "vue3-puzzle-vcode";
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import countryCode from '../countryRegion/data/index.js' import countryCode from '../countryRegion/data/index.js'
import {senCode, userLogin,userCaptcha,userCaptchaValidate} from "@/api/auth/index.js"; import {senCode, userLogin,userCaptcha,userCaptchaValidate,} from "@/api/auth/index.js";
import {authStore} from "@/stores/auth/index.js"; import {authStore} from "@/stores/auth/index.js";
import {message} from '@/components/x-message/useMessage.js' import {message} from '@/components/x-message/useMessage.js'
import {fddCheck} from "~/api/goods/index.js"; import {fddCheck} from "~/api/goods/index.js";
@ -14,7 +14,6 @@ const router = useRouter();
const route = useRoute(); const route = useRoute();
const { locale } = useI18n() const { locale } = useI18n()
const imgs=ref([zu6020]) const imgs=ref([zu6020])
console.log('zu6020');
definePageMeta({ definePageMeta({
name: 'login', name: 'login',
@ -93,7 +92,7 @@ const captchaUrl=ref('')
const captchaVerifyUrl=ref('') const captchaVerifyUrl=ref('')
const blockY=ref(0) const blockY=ref(0)
const getCode =async () => { const getCode =async () => {
loadingRef.value.loading1=true
const res=await userCaptcha({ const res=await userCaptcha({
"canvasWidth": 320, //(canvasWidth41canvasWidth-10/2 - 1> blockWidth) "canvasWidth": 320, //(canvasWidth41canvasWidth-10/2 - 1> blockWidth)
"canvasHeight": 191, //(canvasHeight26 canvasHeight - blockHeight > 11 "canvasHeight": 191, //(canvasHeight26 canvasHeight - blockHeight > 11
@ -106,8 +105,9 @@ const getCode =async () => {
captchaUrl.value=`data:image/png;base64,${res.data.canvasSrc}` captchaUrl.value=`data:image/png;base64,${res.data.canvasSrc}`
captchaVerifyUrl.value=`data:image/png;base64,${res.data.blockSrc}` captchaVerifyUrl.value=`data:image/png;base64,${res.data.blockSrc}`
blockY.value=res.data.blockY blockY.value=res.data.blockY
captcha.value=res.data.nonceStr captcha.value.nonceStr=res.data.nonceStr
isShow.value=true isShow.value=true
loadingRef.value.loading1=false
} }
// loadingRef.value.loading1=true // loadingRef.value.loading1=true
// const res=await senCode({ // const res=await senCode({
@ -184,16 +184,30 @@ const onClose=()=>{
isShow.value=false isShow.value=false
} }
const onLeave =async (moveX, callback) => { const onLeave =async (moveX, callback) => {
console.log('moveX',moveX); loadingRef.value.loading1=true
const res=await userCaptchaValidate({ const res=await senCode({
blockX:moveX, telNum:phoneNum.value,
nonceStr:captcha.value.nonceStr zone:selectedZone.value,
}) verifyCaptcha:{
if (res.status===0){ blockX:moveX,
callback(true) nonceStr:captcha.value.nonceStr
}else { }
callback(false) })
} loadingRef.value.loading1=false
if (res.status===408){
callback(false)
getCode()
}else{
callback(true)
setTimeout(() => {
pane.value = 1
vanSwipeRef.value?.swipeTo(pane.value)
startCountdown();
isShow.value=false
}, 1000)
}
} }
</script> </script>
@ -273,7 +287,6 @@ const onLeave =async (moveX, callback) => {
</div> </div>
<van-popup v-model:show="isShow" round> <van-popup v-model:show="isShow" round>
<YourPuzzleComponent <YourPuzzleComponent
:show="true"
:blockY="blockY" :blockY="blockY"
:bgImageUrl="captchaUrl" :bgImageUrl="captchaUrl"
:sliderImageUrl="captchaVerifyUrl" :sliderImageUrl="captchaVerifyUrl"