feat: login component
This commit is contained in:
@@ -18,8 +18,6 @@ const onClickChangeLocal = (newLanguage: string) => {
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="btn btn-sm btn-ghost gap-1 px-1.5 font-bold"
|
||||
aria-label="Language"
|
||||
title="Change Language"
|
||||
>
|
||||
<svg
|
||||
class="text-base-content/70 size-4"
|
||||
|
||||
69
src/components/button/UserAuthNavButton.vue
Normal file
69
src/components/button/UserAuthNavButton.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { renderAuthDialog } from "@/hooks/dialog/renderAuthDialog";
|
||||
|
||||
const dropdownVisible = ref(true);
|
||||
|
||||
const closeDropdown = () => {
|
||||
dropdownVisible.value = false;
|
||||
setTimeout(() => {
|
||||
dropdownVisible.value = true;
|
||||
}, 300);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown dropdown-hover dropdown-end">
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class="btn btn-circle btn-sm btn-ghost px-3 w-16"
|
||||
>
|
||||
<span class="text-sm text-base-content/70">{{ $t("nav.login") }}</span>
|
||||
</div>
|
||||
<ul
|
||||
v-show="dropdownVisible"
|
||||
tabindex="-1"
|
||||
class="dropdown-content z-1 w-100 p-2"
|
||||
>
|
||||
<div class="card bg-base-200 w-96 mt-4 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">现在加入!</h2>
|
||||
<p>登录我们的系统</p>
|
||||
<div class="card-actions justify-end">
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
@click="
|
||||
renderAuthDialog('register');
|
||||
closeDropdown();
|
||||
"
|
||||
>
|
||||
{{ $t("nav.register") }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
@click="
|
||||
renderAuthDialog('login');
|
||||
closeDropdown();
|
||||
"
|
||||
>
|
||||
{{ $t("nav.login") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<!-- 登录弹窗 -->
|
||||
<dialog id="my_modal" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">Hello!</h3>
|
||||
<p class="py-4">Press ESC key or click outside to close</p>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
336
src/components/dialog/AuthDialog.vue
Normal file
336
src/components/dialog/AuthDialog.vue
Normal file
@@ -0,0 +1,336 @@
|
||||
<script setup lang="ts">
|
||||
import { closeAuthDialog } from "@/hooks/dialog/renderAuthDialog";
|
||||
import { AnimatePresence, motion } from "motion-v";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
defaultAuthFormType: {
|
||||
type: String as PropType<"login" | "register">,
|
||||
default: "login",
|
||||
},
|
||||
});
|
||||
|
||||
const authFormType = ref<"login" | "register">(props.defaultAuthFormType);
|
||||
|
||||
const changeAuthFormType = (type: "login" | "register") => {
|
||||
authFormType.value = "";
|
||||
setTimeout(() => {
|
||||
authFormType.value = type;
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const isNotClose = ref(true);
|
||||
const closeAuthDialogWaitAnim = () => {
|
||||
isNotClose.value = false;
|
||||
setTimeout(() => {
|
||||
closeAuthDialog();
|
||||
}, 500);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="fixed inset-0 flex items-center justify-center z-50">
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
v-show="isNotClose"
|
||||
class="wrapper"
|
||||
:initial="{ opacity: 0, scale: 0 }"
|
||||
:animate="{ opacity: 1, scale: 1 }"
|
||||
:transition="{
|
||||
duration: 0.4,
|
||||
scale: { type: 'spring', visualDuration: 0.4, bounce: 0.5 },
|
||||
}"
|
||||
:exit="{
|
||||
opacity: 0,
|
||||
scale: 0.2,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
class="icon-close bg-primary text-primary-content"
|
||||
@click="closeAuthDialogWaitAnim"
|
||||
>
|
||||
<span class="icon-[material-symbols--close-small]" />
|
||||
</span>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
v-if="authFormType === 'login'"
|
||||
class="form-box login"
|
||||
:initial="{
|
||||
y: -100,
|
||||
opacity: 0,
|
||||
}"
|
||||
:animate="{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
}"
|
||||
:transition="{
|
||||
duration: 0.3,
|
||||
scale: { type: 'spring', visualDuration: 0.3, bounce: 0.5 },
|
||||
}"
|
||||
:exit="{
|
||||
y: 100,
|
||||
opacity: 0,
|
||||
}"
|
||||
>
|
||||
<h2>
|
||||
{{ t("auth.login") }}
|
||||
</h2>
|
||||
<form action="#">
|
||||
<div class="input-box">
|
||||
<span class="icon">
|
||||
<span class="icon-[material-symbols--mail-outline]" />
|
||||
</span>
|
||||
<input type="text" required />
|
||||
<label>{{ t("auth.email") }}</label>
|
||||
</div>
|
||||
<div class="input-box">
|
||||
<span class="icon">
|
||||
<span class="icon-[material-symbols--lock-outline]" />
|
||||
</span>
|
||||
<input type="password" required />
|
||||
<label>{{ t("auth.password") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="remember-forgot">
|
||||
<label class="label">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" />
|
||||
{{ t("auth.remember_me") }}
|
||||
</label>
|
||||
<a class="label" href="#">{{ t("auth.forget_password") }}</a>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn bg-primary text-primary-content"
|
||||
>
|
||||
{{ t("auth.login") }}
|
||||
</button>
|
||||
<div
|
||||
class="login-register"
|
||||
@click="changeAuthFormType('register')"
|
||||
>
|
||||
<p>
|
||||
{{ t("auth.no_account") }}
|
||||
<a class="register-link cursor-pointer">
|
||||
{{ t("auth.register") }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
<!-- 注册表单 -->
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
v-if="authFormType === 'register'"
|
||||
class="form-box register"
|
||||
:initial="{
|
||||
y: -100,
|
||||
opacity: 0,
|
||||
}"
|
||||
:animate="{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
}"
|
||||
:transition="{
|
||||
duration: 0.3,
|
||||
scale: { type: 'spring', visualDuration: 0.3, bounce: 0.5 },
|
||||
}"
|
||||
:exit="{
|
||||
y: 100,
|
||||
opacity: 0,
|
||||
}"
|
||||
>
|
||||
<h2>{{ t("auth.register") }}</h2>
|
||||
<form action="#">
|
||||
<div class="input-box">
|
||||
<span class="icon">
|
||||
<span class="icon-[material-symbols--mail-outline]" />
|
||||
</span>
|
||||
<input type="text" required />
|
||||
<label>{{ t("auth.email") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="input-box">
|
||||
<span class="icon">
|
||||
<span class="icon-[material-symbols--lock-outline]" />
|
||||
</span>
|
||||
<input type="password" required />
|
||||
<label>{{ t("auth.password") }}</label>
|
||||
</div>
|
||||
|
||||
<div class="remember-forgot">
|
||||
<label class="label">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" />
|
||||
{{ t("auth.agree_terms") }}
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn bg-primary text-primary-content"
|
||||
>
|
||||
{{ t("auth.register") }}
|
||||
</button>
|
||||
<div
|
||||
class="login-register"
|
||||
@click="changeAuthFormType('login')"
|
||||
>
|
||||
<p>
|
||||
{{ t("auth.have_account") }}
|
||||
<a class="login-link cursor-pointer">
|
||||
{{ t("auth.login") }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800;900&display=swap");
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
height: 440px;
|
||||
background: transparent;
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wrapper .form-box {
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.wrapper .icon-close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 2em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-bottom-left-radius: 20px;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.form-box h2 {
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
border-bottom: 2px solid;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.input-box label {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 5px;
|
||||
transform: translateY(-50%);
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
pointer-events: none;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.input-box input:focus ~ label,
|
||||
.input-box input:valid ~ label {
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
.input-box input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
padding: 0 35px 0 5px;
|
||||
}
|
||||
|
||||
.input-box .icon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
font-size: 1.2rem;
|
||||
line-height: 57px;
|
||||
}
|
||||
|
||||
.remember-forgot {
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
margin: -15px 0 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.remember-forgot label input {
|
||||
margin-right: 3px;
|
||||
}
|
||||
.remember-forgot a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.remember-forgot a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-register {
|
||||
font-size: 0.9em;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
margin: 25px 0 10px;
|
||||
}
|
||||
|
||||
.login-register p a {
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-register p a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@ import { useI18n } from "vue-i18n";
|
||||
import { throttle } from "radash";
|
||||
import { AnimatePresence, motion } from "motion-v";
|
||||
import { navigateTo } from "@/utils/navigator";
|
||||
import UserAuthNavButton from "../button/UserAuthNavButton.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -82,6 +83,7 @@ window.addEventListener("scroll", handleThrottleScroll);
|
||||
<div class="navbar-end">
|
||||
<ChangeThemeDropdownButton />
|
||||
<ChangeLanguageDropdownButton />
|
||||
<UserAuthNavButton />
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
22
src/hooks/dialog/renderAuthDialog.ts
Normal file
22
src/hooks/dialog/renderAuthDialog.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import AuthDialog from "@/components/dialog/AuthDialog.vue";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
export const renderAuthDialog = (defaultAuthFormType: "login" | "register") => {
|
||||
const authDialogContainer = document.createElement("div");
|
||||
authDialogContainer.id = "auth-dialog-container";
|
||||
|
||||
document.body.appendChild(authDialogContainer);
|
||||
|
||||
createApp(AuthDialog, {
|
||||
defaultAuthFormType,
|
||||
})
|
||||
.use(i18n)
|
||||
.mount(authDialogContainer);
|
||||
};
|
||||
|
||||
export const closeAuthDialog = () => {
|
||||
const authDialogContainer = document.getElementById("auth-dialog-container");
|
||||
if (authDialogContainer) {
|
||||
authDialogContainer.remove();
|
||||
}
|
||||
};
|
||||
@@ -6,5 +6,18 @@ export const en_USMessages: messagesInterface = {
|
||||
about: "About",
|
||||
theme: "Theme",
|
||||
locale: "Locale",
|
||||
login: "Login",
|
||||
register: "Register",
|
||||
},
|
||||
auth: {
|
||||
login: "Login",
|
||||
register: "Register",
|
||||
email: "Email",
|
||||
password: "Password",
|
||||
remember_me: "Remember Me",
|
||||
forget_password: "Forget Password",
|
||||
no_account: "Don't have an account?",
|
||||
agree_terms: "Agree to the Terms & Conditions",
|
||||
have_account: "Already have an account?",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,6 +8,19 @@ export interface messagesInterface {
|
||||
about: string;
|
||||
theme: string;
|
||||
locale: string;
|
||||
login: string;
|
||||
register: string;
|
||||
};
|
||||
auth: {
|
||||
login: string;
|
||||
register: string;
|
||||
email: string;
|
||||
password: string;
|
||||
remember_me: string;
|
||||
forget_password: string;
|
||||
no_account: string;
|
||||
agree_terms: string;
|
||||
have_account: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,5 +6,18 @@ export const zh_CNMessages: messagesInterface = {
|
||||
about: "关于",
|
||||
theme: "主题",
|
||||
locale: "语言",
|
||||
login: "登录",
|
||||
register: "注册",
|
||||
},
|
||||
auth: {
|
||||
login: "登录",
|
||||
register: "注册",
|
||||
email: "邮箱",
|
||||
password: "密码",
|
||||
remember_me: "记住我",
|
||||
forget_password: "忘记密码",
|
||||
no_account: "没有账号?",
|
||||
agree_terms: "同意条款和条件",
|
||||
have_account: "已经有账号?",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@plugin "@iconify/tailwind4";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@plugin "daisyui" {
|
||||
|
||||
Reference in New Issue
Block a user