add rotate captcha

This commit is contained in:
landaiqing
2024-08-12 22:24:16 +08:00
parent fea8b9df2d
commit 431e690f02
18 changed files with 257 additions and 38 deletions

23
src/api/captcha/index.ts Normal file
View File

@@ -0,0 +1,23 @@
import {service} from "@/utils/alova/service.ts";
export const getRotatedCaptchaData = () => {
return service.Get('/api/captcha/rotate/get', {
meta: {
ignoreToken: true
},
});
};
export const checkRotatedCaptcha = (angle: any, key: any) => {
return service.Post('/api/captcha/rotate/check', {
angle: angle,
key: key,
},
{
meta: {
ignoreToken: true
},
}
);
};

View File

@@ -29,6 +29,11 @@ export default {
repasswordValidate: "please enter the confirmation password",
forgetPassword: "forget password",
resetPassword: "reset password",
captchaSuccess: "captcha success",
captchaError: "captcha failed",
rotateCaptchaTitle: "Please drag the slider to complete the puzzle",
systemError: "System error, please try again later",
captchaExpired: "captcha expired, please try again",
},
error: {
networkError: 'Network error, please try again later',

View File

@@ -29,6 +29,12 @@ export default {
repasswordValidate: "请输入确认密码",
forgetPassword: "忘记密码",
resetPassword: "重置密码",
captchaSuccess: "验证成功!",
captchaError: "验证失败!",
rotateCaptchaTitle: "请拖动滑块完成拼图",
systemError: "系统错误!请稍后再试!",
captchaExpired: "验证码已过期,请重新获取!",
},
error: {
networkError: '网络连接失败!',

View File

@@ -5,9 +5,12 @@ import '@/assets/styles/global.scss';
import i18n from "@/locales/index.ts";
import {setupStore} from "@/store/pinia.ts";
import router from "@/router/router.ts";
import "go-captcha-vue/dist/style.css"
import GoCaptcha from "go-captcha-vue"
const app = createApp(App);
setupStore(app);
app.use(router);
app.use(i18n);
app.use(GoCaptcha)
app.mount('#app');

View File

@@ -5,13 +5,13 @@ import variables from '@/assets/styles/colors.module.scss';
import {parse, stringify} from "zipson/lib";
/**
* theme 配置 开启持久化
* theme 配置
*/
export const useThemeStore = defineStore(
'theme',
() => {
const themeName = ref('green'); // 主题名称
const darkMode = ref('light'); // 颜色模式
const themeName = ref<string>('green'); // 主题名称
const darkMode = ref<string>('light'); // 颜色模式
const darkModeComp = computed(() => {
document.documentElement.setAttribute('data-dark', darkMode.value);
return darkMode.value;

View File

@@ -17,10 +17,10 @@ export const service = createAlova({
requestAdapter: axiosRequestAdapter(),
l2Cache: localforageStorageAdapter,
cacheFor: {
GET: {
mode: 'restore',
expire: 10 * 1000
},
// GET: {
// mode: 'restore',
// expire: 10 * 1000
// },
},
cacheLogger: import.meta.env.VITE_NODE_ENV === 'development',
// 设置全局的请求拦截器

View File

@@ -15,7 +15,7 @@
import {GithubOutlined, QqOutlined, QrcodeOutlined} from "@ant-design/icons-vue";
import {useI18n} from "vue-i18n";
import {h} from "vue";
import {useRouter} from 'vue-router'
import {useRouter} from 'vue-router';
const router = useRouter();
const {t} = useI18n();

View File

@@ -33,7 +33,8 @@
<AFlex :vertical="true">
<span class="login-card-span">{{ t("login.phoneCaptcha") }}</span>
<AFlex :vertical="false" align="center" justify="center">
<AInput v-model:value="phoneLoginForm.captcha" size="large" :placeholder=captchaValidate allow-clear>
<AInput v-model:value="phoneLoginForm.captcha" size="large" :placeholder=captchaValidate
allow-clear>
<template #prefix>
<SafetyOutlined/>
</template>
@@ -126,6 +127,18 @@
</ATooltip>
</ACard>
</div>
<div v-if="showRotateCaptcha" class="mask">
<!-- 滑动验证码 -->
<gocaptcha-rotate
class="gocaptcha-rotate"
v-if="showRotateCaptcha"
:data="captchaData"
:config="{
title: t('login.rotateCaptchaTitle'),
}"
:events="rotateEvent"
/>
</div>
</div>
</template>
<script setup lang="ts">
@@ -136,11 +149,27 @@ import {useI18n} from "vue-i18n";
import BoxDog from "@/components/BoxDog/BoxDog.vue";
import LoginFooter from "@/views/Login/LoginFooter.vue";
import {useRouter} from "vue-router";
import {checkRotatedCaptcha, getRotatedCaptchaData} from "@/api/captcha";
import {message} from "ant-design-vue";
const router = useRouter();
const {t} = useI18n();
const accountLoginFormRef = ref<any>();
const phoneLoginFormRef = ref<any>();
const showRotateCaptcha = ref<boolean>(false);
const captchaData = reactive({angle: 0, image: "", thumb: "", key: ""});
const captchaErrorCount = ref<number>(0);
const rotateEvent = {
confirm: (angle: number) => {
checkCaptcha(angle);
},
close: () => {
closeRotateCaptcha();
},
refresh: () => {
getRotateCaptcha();
},
};
// 账号登录表单数据
const accountLoginForm: UnwrapRef<AccountLogin> = reactive({
account: '',
@@ -185,11 +214,12 @@ const state = reactive({
countDownTime: 60,
showCountDown: false,
});
/**
* 验证码发送倒计时
*/
const countDown = () => {
const startTime = localStorage.getItem('startTimeSendCaptcha');
const startTime = localStorage.getItem('start_time_send_captcha');
const nowTime = new Date().getTime();
let surplus: number = 60;
let timer: any;
@@ -197,17 +227,15 @@ const countDown = () => {
surplus = 60 - Math.floor((nowTime - Number(startTime)) / 1000);
surplus = surplus <= 0 ? 0 : surplus;
} else {
localStorage.setItem('startTimeSendCaptcha', String(nowTime));
localStorage.setItem('start_time_send_captcha', String(nowTime));
}
state.countDownTime = surplus;
if (timer) {
clearInterval(timer);
}
timer = setInterval(() => {
if (state.countDownTime <= 0) {
localStorage.removeItem('startTimeSendCaptcha');
localStorage.removeItem('start_time_send_captcha');
clearInterval(timer);
state.countDownTime = 60;
state.showCountDown = false;
@@ -218,7 +246,7 @@ const countDown = () => {
}, 1000);
};
onMounted(() => {
const sendEndTime = localStorage.getItem('startTimeSendCaptcha');
const sendEndTime = localStorage.getItem('start_time_send_captcha');
if (sendEndTime) {
state.showCountDown = true;
countDown();
@@ -232,8 +260,8 @@ async function sendCaptcha() {
phoneLoginFormRef.value
.validateFields("phone")
.then(() => {
countDown();
console.log('values');
showRotateCaptcha.value = true;
getRotateCaptcha();
})
.catch((error: any) => {
console.log('error', error);
@@ -268,6 +296,52 @@ async function phoneLoginSubmit() {
});
}
/**
* 获取旋转验证码数据
*/
async function getRotateCaptcha() {
const data: any = await getRotatedCaptchaData();
if (data.code === 0 && data.data) {
captchaData.image = data.data.image;
captchaData.thumb = data.data.thumb;
captchaData.key = data.data.key;
} else {
message.error(t('login.systemError'));
}
}
/**
* 验证旋转验证码
* @param angle
*/
async function checkCaptcha(angle: number) {
if (captchaErrorCount.value >= 1) {
message.error(t('login.captchaError'));
getRotateCaptcha().then();
captchaErrorCount.value = 0;
} else {
const result: any = await checkRotatedCaptcha(angle, captchaData.key);
if (result.code === 0 && result.success) {
message.success(t('login.captchaSuccess'));
showRotateCaptcha.value = false;
countDown();
} else if (result.code === 1011) {
message.error(t('login.captchaExpired'));
getRotateCaptcha().then();
captchaErrorCount.value = 0;
} else {
captchaErrorCount.value++;
message.error(t('login.captchaError'));
}
}
}
/**
* 关闭旋转验证码
*/
async function closeRotateCaptcha() {
showRotateCaptcha.value = false;
}
</script>
<style src="./index.scss" scoped>
@import "@/assets/styles/global.scss";

View File

@@ -84,4 +84,22 @@
}
}
.gocaptcha-rotate {
width: 330px;
height: 350px;
position: absolute;
border-radius: 15px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.mask { /* 遮罩层的写法 */
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
}
}

View File

@@ -39,7 +39,7 @@
import {useI18n} from "vue-i18n";
import BoxDog from "@/components/BoxDog/BoxDog.vue";
import QRLoginFooter from "@/views/QRLogin/QRLoginFooter.vue";
import {useRouter} from 'vue-router'
import {useRouter} from 'vue-router';
const {t} = useI18n();

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="test">
<AButton @click="handleClick"> {{ t('login.test') }}</AButton>
<AButton @click="changeLang('zh')"> 切换中文</AButton>
<AButton @click="changeLang('en')"> 切换英文</AButton>
@@ -32,5 +32,13 @@ async function changeLang(language: any) {
}
</script>
<style scoped>
<style lang="scss" scoped>
@import "@/assets/styles/theme.scss";
.test {
@include useTheme {
background: getModeVar('primary');
color: getColor('info');
}
}
</style>

View File

@@ -17,6 +17,10 @@
</AButtonGroup>
<div class="test">test</div>
</AConfigProvider>
<AButton type="primary" @click="()=>{
}">获取验证码
</AButton>
</div>
</template>
@@ -25,10 +29,8 @@
import variables from "@/assets/styles/colors.module.scss";
import useStore from "@/store/index.ts";
const app = useStore().theme;
</script>
<style lang="scss" scoped>
@import "@/assets/styles/theme.scss";