feat: 添加验证码

This commit is contained in:
Litrix 2024-04-18 17:57:35 +08:00
parent 16cb7f1859
commit 92799887d7
8 changed files with 198 additions and 52 deletions

5
components.d.ts vendored
View File

@ -19,14 +19,19 @@ declare module 'vue' {
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
Game2048: typeof import('./src/components/Game2048.vue')['default']
IconCsLock: typeof import('~icons/cs/lock')['default']
IconCsValidate: typeof import('~icons/cs/validate')['default']
IconEpLoading: typeof import('~icons/ep/loading')['default']
IconEpUserFilled: typeof import('~icons/ep/user-filled')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
VerifyInput: typeof import('./src/components/VerifyInput.vue')['default']
VerifyInputImage: typeof import('./src/components/VerifyInputImage.vue')['default']
}
}

View File

@ -60,16 +60,7 @@
</template>
</el-input>
</el-form-item>
<el-form-item style="margin-bottom: 0">
<el-button
type="primary"
style="width: 100%"
@click="submitLoginForm"
:loading="logining"
>
登录
</el-button>
</el-form-item>
<verify-input v-model="loginFormData" :disabled="logining" />
</el-form>
</el-tab-pane>
<el-tab-pane label="注册" name="register">
@ -136,6 +127,7 @@
display: flex;
align-items: center;
}
.title-container {
font-size: 1.2em;
}
@ -145,15 +137,22 @@
</style>
<script setup lang="ts">
import axiosInstance from '@/api';
import { loginResponseSchema, registerResponseSchema } from '@/schemas';
import { getAvailableRoutes } from '@/router';
import {
loginResponseSchema,
registerResponseSchema,
userInfoResponseSchema,
verifyResponseSchema
} from '@/schemas';
import { useUserStore } from '@/stores';
import { errorMessage, timeout } from '@/utils';
import { errorMessage } from '@/utils';
import { Avatar, CloseBold } from '@element-plus/icons-vue';
import { AxiosError } from 'axios';
import { type FormInstance, type FormRules, ElMessage } from 'element-plus';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { partial, pick } from 'lodash-es';
import { onMounted, reactive, ref, watch, onBeforeMount } from 'vue';
import { onMounted, reactive, ref, watch } from 'vue';
import { RouterView, useRoute, useRouter } from 'vue-router';
import { type VerifyImagePath } from '@/components/VerifyInput.vue';
import { z } from 'zod';
const router = useRouter();
const route = useRoute();
@ -167,11 +166,15 @@ watch(showLoginRegisterDialog, (value) => {
registerFormRef.value?.resetFields();
}
});
const loginRegisterDialogActiveName = ref('login');
const loginFormRef = ref<FormInstance>();
const loginFormData = reactive({
username: 'wubaopu2',
password: '123456'
password: '123456',
verifyImage: 'none' as VerifyImagePath,
verifyCode: '',
lastVerifyTime: undefined as number | undefined
});
const loginFormRules = reactive<FormRules<typeof loginFormData>>({
username: [
@ -188,7 +191,10 @@ const registerFormRef = ref<FormInstance>();
const registerFormData = reactive({
username: '',
password: '',
confirmPassword: ''
confirmPassword: '',
verifyImage: 'none' as VerifyImagePath,
verifyCode: '',
lastVerifyTime: undefined as number | undefined
});
const registerFormRules = reactive<FormRules<typeof registerFormData>>({
...loginFormRules,
@ -216,6 +222,7 @@ interface LoginParams {
username: string;
password: string;
}
async function login(params: LoginParams = pick(loginFormData, 'username', 'password')) {
try {
const loginResp = loginResponseSchema.parse(
@ -229,8 +236,9 @@ async function login(params: LoginParams = pick(loginFormData, 'username', 'pass
} catch (e) {
if (e instanceof AxiosError) {
ElMessage.error(loginErrorMessage(e.code, e.message));
} else if (e instanceof z.ZodError) {
ElMessage.error('登录失败: 响应数据错误');
} else {
ElMessage.error('error');
console.log(e);
}
return false;
}
@ -276,8 +284,9 @@ async function register() {
} catch (e) {
if (e instanceof AxiosError) {
ElMessage.error(registerErrorMessage(e.code, e.message));
} else if (e instanceof z.ZodError) {
ElMessage.error('注册失败: 响应数据错误');
} else {
ElMessage.error('error');
console.log(e);
}
console.log('error', e);
return false;
@ -306,4 +315,31 @@ async function logout() {
await router.push('/');
window.location.reload();
}
onMounted(async () => {
try {
console.log((await axiosInstance.get('/api/user/info')).data);
const userInfoResponse = userInfoResponseSchema.parse(
(await axiosInstance.get('/api/user/info')).data
);
console.log(userInfoResponse);
if (userInfoResponse.type === 'error') {
ElMessage.error(
errorMessage('获取用户信息失败', userInfoResponse.code, userInfoResponse.msg)
);
return false;
}
// .
if (userStore.token !== null) {
userStore.updateUserInfo(userInfoResponse);
}
getAvailableRoutes(userInfoResponse.data.auth.permissions[0].routers);
} catch (e) {
console.log(e);
if (e instanceof AxiosError) {
ElMessage.error(errorMessage('获取用户信息失败', e.code, e.message));
}
} finally {
userStore.initialized = true;
}
});
</script>

View File

@ -1,7 +1,7 @@
import axios from 'axios';
import { useUserStore } from '@/stores';
const axiosInstance = axios.create({
baseURL: 'http://wzpmc.cn:18080/'
baseURL: 'http://pc.wzpmc.cn:18080/'
});
// 自动添加token到请求中.
axiosInstance.interceptors.request.use((config) => {

BIN
src/assets/avatar1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 14 14">
<path fill="currentColor" fill-rule="evenodd"
d="M8 3a3 3 0 1 1-6 0a3 3 0 0 1 6 0m-1.95 9.5H.5A.5.5 0 0 1 0 12a5 5 0 0 1 9.725-1.64l-.076.11l-.601-.404A2 2 0 0 0 6.05 12.5m7.8-3.3a.75.75 0 0 0-1.2-.9l-2.67 3.9l-1.65-1.11a.75.75 0 1 0-.9 1.2l2.25 1.56a.75.75 0 0 0 1.05-.15z"
clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 401 B

View File

@ -0,0 +1,115 @@
<template>
<el-form-item>
<el-input v-model="model.verifyCode" placeholder="请输入验证码" :disabled="props.disabled">
<template #prepend>
<el-icon><icon-cs-validate /></el-icon>
</template>
<template #append>
<el-button v-if="model.verifyImage === 'none'" class="verify" @click="getVerifyImage">
获取验证码
</el-button>
<el-icon v-else-if="model.verifyImage === 'fetching'" class="is-loading verify">
<icon-ep-loading />
</el-icon>
<template v-else>
<el-popover v-if="model.lastVerifyTime !== undefined" :content="popOverMessage">
<template #reference>
<el-image
class="verify"
:src="model.verifyImage.img"
@click="getVerifyImage"
></el-image>
</template>
</el-popover>
</template>
</template>
</el-input>
</el-form-item>
</template>
<style scoped lang="scss">
.verify {
height: 30px;
}
</style>
<script setup lang="ts">
import axiosInstance from '@/api';
import { verifyResponseSchema } from '@/schemas';
import { errorMessage, timeout } from '@/utils';
import { AxiosError } from 'axios';
import { ElMessage } from 'element-plus';
import { partial } from 'lodash-es';
import { toRef, watchEffect } from 'vue';
import { watch } from 'vue';
import { computed, ref } from 'vue';
export type VerifyImagePath =
| 'none'
| 'fetching'
| {
key: string;
img: string;
};
const model = defineModel<{
verifyImage: VerifyImagePath;
verifyCode: string;
lastVerifyTime: number | undefined;
}>({ required: true });
const props = defineProps<{ disabled: boolean }>();
const canRefresh = ref(true);
const popOverMessage = ref('');
const refreshCooldown = ref(0);
const verifyErrorMessage = partial(errorMessage, '获取验证码');
watchEffect(() => {
const lastVerifyTime = toRef(model.value, 'lastVerifyTime');
if (lastVerifyTime.value !== undefined) {
if (refreshCooldown.value > 0) {
popOverMessage.value = `${refreshCooldown.value.toFixed(0)}秒再试`;
} else {
popOverMessage.value = '看不清? 点击换一张';
}
}
});
async function cooldown() {
canRefresh.value = false;
for (refreshCooldown.value = 10; refreshCooldown.value > 0; refreshCooldown.value--) {
await timeout(1000);
}
canRefresh.value = true;
}
async function getVerifyImage() {
if (model.value.verifyImage === 'fetching' || !canRefresh.value) {
return;
}
let succeed = false;
const original = model.value.verifyImage;
model.value.verifyImage = 'fetching';
try {
try {
const verifyResponse = verifyResponseSchema.parse(
(await axiosInstance.get('/api/user/verify')).data
);
console.log(verifyResponse);
if (verifyResponse.type === 'error') {
ElMessage.error(verifyErrorMessage(verifyResponse.code, verifyResponse.msg));
return;
}
const { img, key } = verifyResponse.data;
model.value.verifyImage = {
key,
img
};
model.value.lastVerifyTime = Date.now();
cooldown();
succeed = true;
} catch (e) {
if (e instanceof AxiosError) {
ElMessage.error(verifyErrorMessage(e.code, e.message));
}
}
} finally {
if (!succeed) {
model.value.verifyImage = original;
}
}
}
</script>

View File

@ -1,10 +1,6 @@
import axiosInstance from '@/api';
import { userInfoResponseSchema, type Route } from '@/schemas';
import { type Route } from '@/schemas';
import { useUserStore } from '@/stores';
import { errorMessage, timeout } from '@/utils';
import { AxiosError } from 'axios';
import { ElMessage } from 'element-plus';
import { type Component } from 'vue';
import { watch, type Component } from 'vue';
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
@ -39,7 +35,7 @@ function _getAvailableRoutes(routeResponses: Route[], root: boolean, pathList: s
}
return res;
}
function getAvailableRoutes(routeResponse: Route[]) {
export function getAvailableRoutes(routeResponse: Route[]) {
_getAvailableRoutes(routeResponse, true, []);
}
router.beforeEach(async (to) => {
@ -48,29 +44,7 @@ router.beforeEach(async (to) => {
// await timeout(1000);
console.log(userStore.initialized);
if (!userStore.initialized) {
try {
const userInfoResponse = userInfoResponseSchema.parse(
(await axiosInstance.get('/api/user/info')).data
);
console.log(userInfoResponse);
if (userInfoResponse.type === 'error') {
ElMessage.error(
errorMessage('获取用户信息失败', userInfoResponse.code, userInfoResponse.msg)
);
return false;
}
// 判断是否已登录.
if (userStore.token !== null) {
userStore.updateUserInfo(userInfoResponse);
}
getAvailableRoutes(userInfoResponse.data.auth.permissions[0].routers);
} catch (e) {
if (e instanceof AxiosError) {
ElMessage.error(errorMessage('获取用户信息失败', e.code, e.message));
}
} finally {
userStore.initialized = true;
}
await new Promise((resolve) => watch(() => userStore.initialized, resolve));
}
if (userStore.addedRouteSet.has(to.fullPath) || to.fullPath === '/404') {
return true;

View File

@ -44,9 +44,20 @@ const routeResponsesSchema: z.ZodSchema<Route[]> = z.lazy(() =>
);
export const userInfoResponseSchema = createResponseSchema(
idAndNameSchema.extend({
avatar: z.string(),
avatar: z.nullable(z.string()),
auth: idAndNameSchema.extend({
permissions: idAndNameSchema.extend({ routers: routeResponsesSchema }).array()
})
})
);
export const verifyResponseSchema = createResponseSchema(
z
.object({
img: z.string(),
key: z.string()
})
.transform((raw) => {
raw.img = `data:image/png;base64,${raw.img}`;
return raw;
})
);