feat: login component

This commit is contained in:
2026-02-16 02:12:51 +08:00
parent 193cbe74ba
commit 235c130a6d
13 changed files with 558 additions and 2 deletions

View File

@@ -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"

View 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>

View 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>

View File

@@ -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>

View 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();
}
};

View File

@@ -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?",
},
};

View File

@@ -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;
};
}

View File

@@ -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: "已经有账号?",
},
};

View File

@@ -1,5 +1,6 @@
@import "tailwindcss";
@import "tw-animate-css";
@plugin "@iconify/tailwind4";
@custom-variant dark (&:is(.dark *));
@plugin "daisyui" {