✨ feat: 完成验证码功能
This commit is contained in:
parent
291c3210ac
commit
d1484708fc
3
auto-imports.d.ts
vendored
3
auto-imports.d.ts
vendored
@ -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
2
components.d.ts
vendored
@ -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']
|
||||
}
|
||||
}
|
||||
|
95
src/App.vue
95
src/App.vue
@ -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));
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
4
src/assets/icons/User.svg
Normal file
4
src/assets/icons/User.svg
Normal 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 |
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
};
|
||||
});
|
||||
|
@ -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() {
|
||||
|
@ -17,7 +17,7 @@ export default defineConfig({
|
||||
resolvers: [
|
||||
ElementPlusResolver(),
|
||||
IconsResolver({
|
||||
prefix: 'Icon',
|
||||
prefix: 'icon',
|
||||
enabledCollections: ['ep'],
|
||||
customCollections: ['cs']
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user