优化注册和登录表单,移除调试日志,增强用户体验;添加用户面板功能,包括修改密码和删除账号对话框

This commit is contained in:
keqingmoe 2024-12-27 18:03:14 +08:00
parent 2432b9a0ce
commit 93df543bb3
5 changed files with 197 additions and 52 deletions

View File

@ -1,16 +1,19 @@
<template> <template>
<v-carousel v-model="loginMode" v-if="!authStore.isAuthenticated" height="100%" hide-delimiters show-arrows> <v-sheet class="d-flex align-center justify-center flex-wrap text-center mx-auto px-4"
<v-carousel-item> v-if="!authStore.isAuthenticated" width="640" height="670">
<v-sheet class="v-center" height="100%"> <v-carousel v-model="loginMode" height="100%" delimiter-icon="mdi-square" color="blue-darken-2"
<LoginForm id="aaaaa"></LoginForm> hide-delimiter-background show-arrows>
</v-sheet> <v-carousel-item>
</v-carousel-item> <v-sheet class="v-center" height="100%">
<v-carousel-item> <LoginForm></LoginForm>
<v-sheet class="v-center" height="100%"> </v-sheet>
<RegisterForm v-model="loginMode"></RegisterForm> </v-carousel-item>
</v-sheet> <v-carousel-item>
</v-carousel-item> <v-sheet class="v-center" height="100%">
</v-carousel> <RegisterForm v-model="loginMode"></RegisterForm>
</v-sheet>
</v-carousel-item>
</v-carousel></v-sheet>
<UserInfo v-else></UserInfo> <UserInfo v-else></UserInfo>
</template> </template>
@ -18,14 +21,12 @@
import { useAuthStore } from '@/store/auth'; import { useAuthStore } from '@/store/auth';
import LoginForm from './LoginForm.vue'; import LoginForm from './LoginForm.vue';
import { ref } from 'vue'; import { ref } from 'vue';
import { jwtDecode, type JwtPayload } from 'jwt-decode';
import RegisterForm from './RegisterForm.vue'; import RegisterForm from './RegisterForm.vue';
import UserInfo from './UserPanel.vue'; import UserInfo from './UserPanel.vue';
const loginMode = ref(0); const loginMode = ref(0);
const authStore = useAuthStore(); const authStore = useAuthStore();
const token = ref(authStore.token);
</script> </script>
@ -35,6 +36,5 @@ const token = ref(authStore.token);
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100vh; height: 100vh;
/* 设置容器高度为视口高度 */
} }
</style> </style>

View File

@ -1,41 +1,32 @@
<template> <template>
<div> <v-card class="mx-auto pa-12 pb-8" width="448" elevation="8" rounded="lg">
<v-form fast-fail @submit.prevent="submit"> <v-form fast-fail @submit.prevent="submit">
<!-- <v-img class="mx-auto my-6" max-width="228" <!-- <v-img class="mx-auto my-6" max-width="228"
src="https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-v3-slim-text-light.svg"></v-img> --> src="https://cdn.vuetifyjs.com/docs/images/logos/vuetify-logo-v3-slim-text-light.svg"></v-img> -->
<v-card class="mx-auto pa-12 pb-8" elevation="8" max-width="448" rounded="lg"> <v-text-field v-model="userId" prepend-inner-icon="mdi-account-circle" :rules="userIdRules"
<v-text-field v-model="userId" prepend-inner-icon="mdi-account-circle" :rules="userIdRules" label="账号"></v-text-field>
label="账号"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider> <v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-text-field v-model="password" :rules="passwordRules" :error-messages="passwordValidate" label="密码" <v-text-field v-model="password" :rules="passwordRules" :error-messages="passwordValidate" label="密码"
:append-inner-icon="visible ? 'mdi-eye' : 'mdi-eye-off'" :type="visible ? 'text' : 'password'" :append-inner-icon="visible ? 'mdi-eye' : 'mdi-eye-off'" :type="visible ? 'text' : 'password'"
@click:append-inner="visible = !visible" prepend-inner-icon="mdi-lock-outline"></v-text-field> @click:append-inner="visible = !visible" prepend-inner-icon="mdi-lock-outline"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider> <v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-card class="mb-12" color="surface-variant" variant="tonal"> <v-btn class="mb-8" color="blue" size="large" variant="tonal" type="submit" block>
<v-card-text class="text-medium-emphasis text-caption"> 登录
警告连续3次登录失败后您的帐户将被暂时锁定三个小时如果您必须立即登录您还可以点击忘记登录密码下面重置登录密码 </v-btn>
<br />
这只是个占位符文本不知道为什么没有这段话整个布局就会乱成一坨实际上我并没有做这个冻结功能
</v-card-text>
</v-card>
<v-btn class="mb-8" color="blue" size="large" variant="tonal" type="submit" block> <v-card-text class="text-center">
登录 <a class="text-red text-decoration-none"
</v-btn> @click="dialog('忘记密码', '请联系老师重置密码。')">
忘记密码<v-icon icon="mdi-chevron-right"></v-icon>
<v-card-text class="text-center"> </a>
<a class="text-red text-decoration-none" @click="dialog('没做呢。', '没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。没做呢。')"> </v-card-text>
忘记密码<v-icon icon="mdi-chevron-right"></v-icon>
</a>
</v-card-text>
</v-card>
</v-form> </v-form>
</div> </v-card>
<v-dialog v-model="dialogShow" width="auto"> <v-dialog v-model="dialogShow" width="auto">
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle"> <v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
<template v-slot:actions> <template v-slot:actions>
@ -79,7 +70,6 @@ const dialog = (title: string, text: string) => {
}; };
const login = async (userId: string, password: string) => { const login = async (userId: string, password: string) => {
console.log('login', userId, password);
try { try {
const formData = new FormData; const formData = new FormData;
formData.append("user_id", userId); formData.append("user_id", userId);

View File

@ -76,13 +76,11 @@ const dialog = (title: string, text: string) => {
}; };
const register = async (userId: string, password: string) => { const register = async (userId: string, password: string) => {
console.log('login', userId, password);
try { try {
const formData = new FormData; const formData = new FormData;
formData.append("user_id", userId); formData.append("user_id", userId);
formData.append("password", password); formData.append("password", password);
let res = await axios.post('/api/auth/register', formData); let res = await axios.post('/api/auth/register', formData);
console.log(res.data);
return res.data as RegisterResponse; return res.data as RegisterResponse;
} catch (e) { } catch (e) {
let ex = e as AxiosError; let ex = e as AxiosError;
@ -93,10 +91,9 @@ const register = async (userId: string, password: string) => {
const submit = async (event: SubmitEvent) => { const submit = async (event: SubmitEvent) => {
const results: any = await event; const results: any = await event;
if (results.valid) { if (results.valid) {
loading.value = true loading.value = true;
console.log('valid')
let res = await register(userId.value, password.value); let res = await register(userId.value, password.value);
loading.value = false loading.value = false;
if (res?.error) { if (res?.error) {
dialog('错误', `注册失败:${res.error}`); dialog('错误', `注册失败:${res.error}`);
} else { } else {
@ -104,7 +101,6 @@ const submit = async (event: SubmitEvent) => {
model.value = 0; model.value = 0;
} }
} }
console.log(2);
}; };
const userIdRules: any = [(value: string) => { const userIdRules: any = [(value: string) => {

View File

@ -0,0 +1,112 @@
<template>
<v-dialog v-model="dialogRepasswdShow" max-width="448">
<v-card class="mx-auto pa-12 pb-8" elevation="8" width="100%" rounded="lg">
<v-form fast-fail @submit.prevent="submit">
<v-text-field v-model="passwords[0]" :rules="passwordRules" label="原密码"
:append-inner-icon="visible[0] ? 'mdi-eye' : 'mdi-eye-off'" :type="visible[0] ? 'text' : 'password'"
@click:append-inner="visible[0] = !visible[0]" prepend-inner-icon="mdi-lock-outline"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-text-field v-model="passwords[1]" :rules="passwordRules" label="新密码"
:append-inner-icon="visible[1] ? 'mdi-eye' : 'mdi-eye-off'" :type="visible[1] ? 'text' : 'password'"
@click:append-inner="visible[1] = !visible[1]" prepend-inner-icon="mdi-lock-outline"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-text-field :rules="passwordRules2" label="重复输入新密码"
:append-inner-icon="visible[2] ? 'mdi-eye-off' : 'mdi-eye'" :type="visible[2] ? 'text' : 'password'"
@click:append-inner="visible[2] = !visible[2]" prepend-inner-icon="mdi-lock-outline"></v-text-field>
<v-divider :thickness="10" class="border-opacity-0"></v-divider>
<v-btn :loading="loading" class="mb-8" color="blue" size="large" type="submit" variant="tonal" block>
修改密码
</v-btn>
</v-form>
</v-card>
</v-dialog>
<v-dialog v-model="dialogShow" width="auto">
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
<template v-slot:actions>
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false, dialogClose()"></v-btn>
</template>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useAuthStore } from '@/store/auth';
import axios, { AxiosError } from 'axios';
const dialogShow = ref(false);
const dialogTitle = ref('');
const dialogText = ref('');
const dialogClose = ref(() => { });
const dialog = (title: string, text: string) => {
dialogTitle.value = title;
dialogText.value = text;
dialogShow.value = true;
return new Promise(res => {
dialogClose.value = res as () => void;
});
};
const dialogRepasswdShow = defineModel({ default: true });
const loading = ref(false);
const passwords = ref(['', '']);
const authStore = useAuthStore();
const token = ref(authStore.token);
const visible = ref([false, false, false]);
type RepasswdResponse = { success?: string, error?: string };
const requestRepasswd = async () => {
try {
const formData = new FormData;
formData.append("token", token.value);
formData.append("raw_passwd", passwords.value[0]);
formData.append("new_passwd", passwords.value[1]);
let res = await axios.post('/api/auth/repasswd', formData);
return res.data as RepasswdResponse;
} catch (e) {
let ex = e as AxiosError;
return ex.response?.data as RepasswdResponse;
}
}
const repasswd = async () => {
loading.value = true;
let res = await requestRepasswd();
loading.value = false;
if (res?.error) {
await dialog('错误', `修改密码失败:${res.error}`);
} else {
await dialog('信息', `修改密码成功,请重新登录。`);
authStore.clearToken();
}
}
const submit = async (event: SubmitEvent) => {
const results: any = await event;
if (results.valid) {
await repasswd();
}
};
const passwordRules: any = [(value: string) => {
if (value?.length > 0) return true;
return '密码不能为空';
}];
const passwordRules2: any = [(value: string) => {
if (value == passwords.value[1]) return true;
return '两次输入需保持一致';
}];
</script>

View File

@ -4,9 +4,15 @@
用户 ID: {{ userId }} 用户 ID: {{ userId }}
</v-card-item> </v-card-item>
<v-card-item> <v-card-item>
<v-chip @click="authStore.clearToken"> <v-chip class="chips" color="orange" @click="logout">
退出登录 退出登录
</v-chip> </v-chip>
<v-chip class="chips" color="blue" @click="dialogRepasswdShow = true">
修改密码
</v-chip>
<v-chip class="chips" color="red" @click="dialogDeleteAccountShow = true">
删除账号
</v-chip>
</v-card-item> </v-card-item>
<v-card-item> <v-card-item>
<v-expansion-panels> <v-expansion-panels>
@ -18,6 +24,15 @@
<Toy></Toy> <Toy></Toy>
</v-card-item> </v-card-item>
</v-card> </v-card>
<v-dialog v-model="dialogShow" width="auto">
<v-card max-width="400" prepend-icon="mdi-update" :text="dialogText" :title="dialogTitle">
<template v-slot:actions>
<v-btn class="ms-auto" text="Ok" @click="dialogShow = false, dialogClose()"></v-btn>
</template>
</v-card>
</v-dialog>
<RepasswdDialog v-model="dialogRepasswdShow"></RepasswdDialog>
<DeleteAccountDialog v-model="dialogDeleteAccountShow"></DeleteAccountDialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -25,6 +40,27 @@ import { ref, computed } from 'vue';
import { useAuthStore } from '@/store/auth'; import { useAuthStore } from '@/store/auth';
import { jwtDecode, type JwtPayload } from 'jwt-decode'; import { jwtDecode, type JwtPayload } from 'jwt-decode';
import Toy from './Toy.vue'; import Toy from './Toy.vue';
import axios, { AxiosError } from 'axios';
import DeleteAccountDialog from './DeleteAccountDialog.vue';
import RepasswdDialog from './RepasswdDialog.vue';
const dialogShow = ref(false);
const dialogTitle = ref('');
const dialogText = ref('');
const dialogClose = ref(() => { });
const dialogDeleteAccountShow = ref(false);
const dialogRepasswdShow = ref(false);
const dialog = (title: string, text: string) => {
dialogTitle.value = title;
dialogText.value = text;
dialogShow.value = true;
return new Promise(res => {
dialogClose.value = res as () => void;
});
};
const reveal = ref(false); const reveal = ref(false);
@ -41,6 +77,17 @@ const userId = computed(() => {
return data.user_id; return data.user_id;
} }
return ''; return '';
}) });
</script>
const logout = async () => {
authStore.clearToken();
}
</script>
<style lang="scss" scoped>
.chips {
margin: 5px;
}
</style>