feat: 完成验证码功能

This commit is contained in:
Litrix2 2024-04-19 20:52:42 +08:00
parent 291c3210ac
commit d1484708fc
11 changed files with 120 additions and 88 deletions

3
auto-imports.d.ts vendored
View File

@ -5,5 +5,6 @@
// Generated by unplugin-auto-import
export {}
declare global {
const IconEpAvatar: typeof import('~icons/ep/avatar')['default']
const IconEpCloseBold: typeof import('~icons/ep/close-bold')['default']
}

2
components.d.ts vendored
View File

@ -26,12 +26,12 @@ declare module 'vue' {
ElTabs: typeof import('element-plus/es')['ElTabs']
Game2048: typeof import('./src/components/Game2048.vue')['default']
IconCsLock: typeof import('~icons/cs/lock')['default']
IconCsUser: typeof import('~icons/cs/user')['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

@ -8,7 +8,7 @@
{{ userStore.userInfo.name }}
</div>
<el-dropdown v-if="userStore.userInfo !== null">
<el-avatar :icon="userStore.userInfo.avatar || Avatar" :size="40"></el-avatar>
<el-avatar :icon="userStore.userInfo.avatar ?? Avatar" :size="40"></el-avatar>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :icon="CloseBold" @click="logout">退出登录</el-dropdown-item>
@ -44,7 +44,7 @@
:disabled="logining"
>
<template #prepend>
<el-icon><icon-ep-user-filled /></el-icon>
<el-icon><icon-cs-user /></el-icon>
</template>
</el-input>
</el-form-item>
@ -60,12 +60,14 @@
</template>
</el-input>
</el-form-item>
<verify-input v-model="loginFormData" :disabled="logining" />
<el-form-item prop="verifyCode">
<verify-input v-model="loginFormData" :disabled="logining" />
</el-form-item>
<el-form-item style="margin-bottom: 0">
<el-button
type="primary"
style="width: 100%"
@click="submitRegisterForm"
@click="submitLoginForm"
:loading="logining"
>
登录
@ -82,7 +84,7 @@
:disabled="registering"
>
<template #prepend>
<el-icon><icon-ep-user-filled /></el-icon>
<el-icon><icon-cs-user /></el-icon>
</template>
</el-input>
</el-form-item>
@ -110,6 +112,9 @@
</template>
</el-input>
</el-form-item>
<el-form-item prop="verifyCode">
<verify-input v-model="registerFormData" :disabled="registering" />
</el-form-item>
<el-form-item style="margin-bottom: 0">
<el-button
type="primary"
@ -152,21 +157,22 @@ import { getAvailableRoutes } from '@/router';
import { loginResponseSchema, registerResponseSchema, userInfoResponseSchema } from '@/schemas';
import { useUserStore } from '@/stores';
import { errorMessage } from '@/utils';
import { Avatar, CloseBold } from '@element-plus/icons-vue';
import { AxiosError } from 'axios';
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { partial, pick } from 'lodash-es';
import { partial } from 'lodash-es';
import { onMounted, reactive, ref, watch } from 'vue';
import { RouterView, useRoute, useRouter } from 'vue-router';
import { RouterView, useRouter } from 'vue-router';
import { Avatar, CloseBold } from '@element-plus/icons-vue';
const router = useRouter();
const route = useRoute();
const userStore = useUserStore();
const showLoginRegisterDialog = ref(false);
watch(showLoginRegisterDialog, (value) => {
if (value) {
logining.value = false;
loginFormData.verifyImage = 'none';
loginFormRef.value?.resetFields();
registering.value = false;
registerFormData.verifyImage = 'none';
registerFormRef.value?.resetFields();
}
});
@ -177,17 +183,20 @@ const loginFormData = reactive({
username: 'wubaopu2',
password: '123456',
verifyImage: 'none' as VerifyImagePath,
verifyCode: '',
lastVerifyTime: undefined as number | undefined
verifyCode: ''
});
const loginFormRules = reactive<FormRules<typeof loginFormData>>({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 6, message: '用户名长度不能小于3位', trigger: 'blur' }
{ required: true, message: '请输入用户名' },
{ min: 6, message: '用户名长度不能小于3位' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
{ required: true, message: '请输入密码' },
{ min: 6, message: '密码长度不能小于6位' }
],
verifyCode: [
{ required: true, message: '请输入验证码' },
{ pattern: /[0-9A-Za-z]{4}/, message: '验证码不符合格式' }
]
});
const logining = ref(false);
@ -197,16 +206,13 @@ const registerFormData = reactive({
password: '',
confirmPassword: '',
verifyImage: 'none' as VerifyImagePath,
verifyCode: '',
lastVerifyTime: undefined as number | undefined
verifyCode: ''
});
const registerFormRules = reactive<FormRules<typeof registerFormData>>({
...loginFormRules,
confirmPassword: [
{
trigger: 'blur',
validator: (_, value, callback) => {
console.log(value);
if (value === '') {
callback('请输入密码');
} else if (value !== registerFormData.password) {
@ -244,8 +250,8 @@ async function login(params: LoginParams) {
ElMessage.error(loginErrorMessage(e.code, e.message));
} else {
ElMessage.error('error');
console.log(e);
}
console.log(e);
return false;
}
}
@ -258,7 +264,22 @@ async function submitLoginForm() {
} catch (e) {
return;
}
succeed = await login();
if (typeof loginFormData.verifyImage === 'string') {
ElMessage.error('请获取验证码');
return;
}
const {
username,
password,
verifyImage: { key },
verifyCode: code
} = loginFormData;
succeed = await login({
username,
password,
key,
code
});
} finally {
if (succeed) {
window.location.reload();
@ -268,16 +289,13 @@ async function submitLoginForm() {
}
}
const registerErrorMessage = partial(errorMessage, '注册');
async function register() {
interface RegisterParams extends LoginParams {
auth: number;
}
async function register(params: RegisterParams) {
try {
const registerResp = registerResponseSchema.parse(
(
await axiosInstance.put('/api/user/create', {
username: registerFormData.username,
password: registerFormData.password,
auth: 1
})
).data
(await axiosInstance.put('/api/user/create', params)).data
);
if (registerResp.type === 'error') {
ElMessage.error({
@ -285,16 +303,14 @@ async function register() {
});
return false;
}
console.log(registerResp);
return true;
} catch (e) {
if (e instanceof AxiosError) {
ElMessage.error(registerErrorMessage(e.code, e.message));
} else {
ElMessage.error('error');
console.log(e);
}
console.log('error', e);
console.log(e);
return false;
}
}
@ -307,7 +323,17 @@ async function submitRegisterForm() {
} catch {
return;
}
succeed = await register();
if (typeof registerFormData.verifyImage === 'string') {
ElMessage.error('请输入验证码');
return;
}
const {
username,
password,
verifyImage: { key },
verifyCode: code
} = registerFormData;
succeed = await register({ username, password, key, code, auth: 1 });
} finally {
if (succeed) {
window.location.reload();
@ -323,11 +349,9 @@ async function logout() {
}
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)
@ -340,7 +364,6 @@ onMounted(async () => {
}
getAvailableRoutes(userInfoResponse.data.auth.permissions[0].routers);
} catch (e) {
console.log(e);
if (e instanceof AxiosError) {
ElMessage.error(errorMessage('获取用户信息失败', e.code, e.message));
}

View File

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

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="0.88em" height="1em" viewBox="0 0 448 512">
<path fill="currentColor"
d="M224 256a128 128 0 1 0 0-256a128 128 0 1 0 0 256m-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512h388.6c16.4 0 29.7-13.3 29.7-29.7c0-98.5-79.8-178.3-178.3-178.3z" />
</svg>

After

Width:  |  Height:  |  Size: 310 B

View File

@ -1,34 +1,32 @@
<template>
<el-form-item>
<el-input v-model="model.verifyCode" placeholder="请输入验证码" :disabled="props.disabled">
<template #prepend>
<el-icon><icon-cs-validate /></el-icon>
<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'" @click="getVerifyImage">获取验证码</el-button>
<el-icon v-else-if="model.verifyImage === 'fetching'" class="is-loading">
<icon-ep-loading />
</el-icon>
<template v-else>
<el-popover popper-style="text-align: center" :content="popOverMessage">
<template #reference>
<img
class="verify-image"
draggable="false"
:src="model.verifyImage.img"
@click="getVerifyImage"
/>
</template>
</el-popover>
</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>
</el-input>
</template>
<style scoped lang="scss">
.verify {
.verify-image {
height: 30px;
user-select: none;
}
</style>
<script setup lang="ts">
@ -38,8 +36,7 @@ import { errorMessage, timeout } from '@/utils';
import { AxiosError } from 'axios';
import { ElMessage } from 'element-plus';
import { partial } from 'lodash-es';
import { ref, toRef, watchEffect } from 'vue';
import { computed, ref, watchEffect } from 'vue';
export type VerifyImagePath =
| 'none'
| 'fetching'
@ -50,29 +47,28 @@ export type VerifyImagePath =
const model = defineModel<{
verifyImage: VerifyImagePath;
verifyCode: string;
lastVerifyTime: number | undefined;
}>({ required: true });
const coolDownDuration = 5;
const props = defineProps<{ disabled: boolean }>();
const canRefresh = ref(true);
const canRefresh = computed(() => refreshCoolDown.value === 0);
const popOverMessage = ref('');
const refreshCooldown = ref(0);
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 = '看不清? 点击换一张';
}
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--) {
for (
refreshCoolDown.value = coolDownDuration;
refreshCoolDown.value > 0;
refreshCoolDown.value--
) {
await timeout(1000);
}
canRefresh.value = true;
}
async function getVerifyImage() {
if (model.value.verifyImage === 'fetching' || !canRefresh.value) {
@ -96,13 +92,13 @@ async function getVerifyImage() {
key,
img
};
model.value.lastVerifyTime = Date.now();
cooldown();
succeed = true;
} catch (e) {
if (e instanceof AxiosError) {
ElMessage.error(verifyErrorMessage(e.code, e.message));
}
console.log(e);
}
} finally {
if (!succeed) {

View File

@ -3,7 +3,7 @@ function createResponseSchema<T extends z.ZodTypeAny>(data: T) {
return z.union([
z
.object({
code: z.literal(200),
code: z.number().min(200).max(299),
msg: z.string(),
time: z.coerce.date()
})
@ -26,7 +26,7 @@ function createResponseSchema<T extends z.ZodTypeAny>(data: T) {
)
]);
}
export type SucceedResponse<T> = T extends { type: 'success' } ? T : never;
export type SucceedResponse<T> = Extract<T, { type: 'success' }>;
export type ErrorResponse<T> = Exclude<SucceedResponse<T>, T>;
const ordinaryResponse = createResponseSchema(z.literal(true));
export const loginResponseSchema = ordinaryResponse;

View File

@ -29,8 +29,8 @@ export const useBackgroundStore = defineStore('background', () => {
options: BackgroundOptions = { resolveTiming: 'transitionEnd' }
) {
const addFuture = Promise.withResolvers<void>();
addFuture.promise.then(() => console.log('resolve', getFuturesMap));
addFuture.promise.catch(() => console.log('reject', getFuturesMap));
// addFuture.promise.then(() => console.log('resolve', getFuturesMap));
// addFuture.promise.catch(() => console.log('reject', getFuturesMap));
const task: BackgroundTask = {
url,
options,

View File

@ -27,5 +27,13 @@ export const useUserStore = defineStore('user', () => {
addedRouteSet.clear();
isInitialized.value = false;
}
return { token, userInfo, routeMap, addedRouteSet, initialized: isInitialized, updateUserInfo, $reset };
return {
token,
userInfo,
routeMap,
addedRouteSet,
initialized: isInitialized,
updateUserInfo,
$reset
};
});

View File

@ -41,7 +41,7 @@ export class IdDispenser {
export function useRefresh() {
const dispenser = new IdDispenser();
const key = ref(dispenser.getId());
watch(key, (value) => console.log(value));
// watch(key, (value) => console.log(value));
return {
key,
refresh() {

View File

@ -17,7 +17,7 @@ export default defineConfig({
resolvers: [
ElementPlusResolver(),
IconsResolver({
prefix: 'Icon',
prefix: 'icon',
enabledCollections: ['ep'],
customCollections: ['cs']
})