✨ feat: 添加验证码
This commit is contained in:
parent
16cb7f1859
commit
92799887d7
5
components.d.ts
vendored
5
components.d.ts
vendored
@ -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']
|
||||
}
|
||||
}
|
||||
|
76
src/App.vue
76
src/App.vue
@ -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>
|
||||
|
@ -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
BIN
src/assets/avatar1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
5
src/assets/icons/Validate.svg
Normal file
5
src/assets/icons/Validate.svg
Normal 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 |
115
src/components/VerifyInput.vue
Normal file
115
src/components/VerifyInput.vue
Normal 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>
|
@ -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;
|
||||
|
@ -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;
|
||||
})
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user