refactor: ♻️ 重构登录页面并添加新功能

重新设计了登录页面布局和样式,添加了注册和重置密码功能组件
This commit is contained in:
zimo493
2025-04-03 15:05:03 +08:00
parent 6026e9cac0
commit c0718a089c
10 changed files with 742 additions and 276 deletions

View File

@@ -0,0 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" baseProfile="full" width="100%" height="100%" viewBox="0 0 1400 800">
<!-- 主波浪 (蓝色系) -->
<path d="M-200 450 Q100 400 400 450 T1000 430"
fill="none"
stroke="#409EFF"
stroke-width="8"
stroke-opacity="0.4"
stroke-linecap="round">
<animate attributeName="d"
values="M-200 450 Q100 400 400 450 T1000 430;
M-200 460 Q100 420 400 440 T1000 420;
M-200 450 Q100 400 400 450 T1000 430"
dur="12s"
repeatCount="indefinite"/>
</path>
<!-- 次级波浪 (金色系) -->
<path d="M-150 550 Q200 520 500 560 T1300 530"
fill="none"
stroke="#FF914D"
stroke-width="5"
stroke-opacity="0.3"
stroke-linecap="round">
<animate attributeName="d"
values="M-150 550 Q200 520 500 560 T1300 530;
M-150 540 Q200 530 500 550 T1300 520;
M-150 550 Q200 520 500 560 T1300 530"
dur="9s"
repeatCount="indefinite"/>
</path>
<!-- 动态流动线 -->
<path d="M-100 600 Q300 620 700 580 T1400 590"
fill="none"
stroke="#FF6B6B"
stroke-width="3"
stroke-opacity="0.25"
stroke-dasharray="20 10">
<animate attributeName="stroke-dashoffset"
from="0" to="100"
dur="15s"
repeatCount="indefinite"/>
</path>
<!-- 主园球 -->
<circle cx="220" cy="220" r="160" fill="#37B6FF" stroke="#37B6FF" stroke-width="8">
<animateMotion path="M 0 0 L 30 15 Z" dur="6s" repeatCount="indefinite"/>
</circle>
<!-- 半球形 -->
<path d="M 100 350 A 150 150 0 1 1 400 350 Q400 370 380 370 L 250 370 L 120 370 Q100 370 100 350" fill="#FF914D">
<animateMotion path="M 800 -200 L 800 -300 L 800 -200" dur="20s" begin="0s" repeatCount="indefinite"/>
<animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="30s" type="rotate" values="0 210 530 ; -30 210 530 ; 0 210 530" keyTimes="0 ; 0.5 ; 1" repeatCount="indefinite"/>
</path>
<!-- 旋转方块 -->
<rect x="1000" y="420" rx="20" ry="20" width="110" height="110" fill="#FABBD8" stroke="#FABBD8" stroke-width="3">
<animateTransform attributeType="XML" attributeName="transform"
begin="0s" dur="30s" type="rotate"
from="0 1450 550" to="360 1450 550"
repeatCount="indefinite"/>
</rect>
<!-- 动态光点 -->
<circle cx="1050" cy="580" r="24" fill="#FFD93D" opacity="0.9">
<animateMotion path="M 0 0 L -25 35 Z" dur="8s" repeatCount="indefinite"/>
<animate attributeName="r" values="24;28;24" dur="4s" repeatCount="indefinite"/>
</circle>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,21 @@
<template>
<div cursor-pointer flex-center rounded class="el" :class="padding">
<slot></slot>
</div>
</template>
<script setup lang="ts">
defineProps({
padding: {
type: String,
default: "p-2",
},
});
</script>
<style scoped lang="scss">
.el {
transition: 0.3s var(--el-transition-function-ease-in-out-bezier);
&:hover {
background-color: var(--el-fill-color);
}
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<el-dropdown trigger="click" @command="handleDarkChange">
<el-icon :size="20">
<component :is="settingsStore.theme === ThemeMode.DARK ? Moon : Sunny" />
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in theneList"
:key="item.value"
:command="item.value"
:disabled="settingsStore.theme === item.value"
>
<el-icon>
<component :is="item.component" />
</el-icon>
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup lang="ts">
import { useSettingsStore } from "@/store";
import { ThemeMode } from "@/enums";
import { Moon, Sunny } from "@element-plus/icons-vue";
const { t } = useI18n();
const settingsStore = useSettingsStore();
const theneList = [
{ label: t("login.light"), value: ThemeMode.LIGHT, component: Sunny },
{ label: t("login.dark"), value: ThemeMode.DARK, component: Moon },
];
const handleDarkChange = (theme: ThemeMode) => {
settingsStore.changeTheme(theme);
};
</script>

View File

@@ -1,6 +1,6 @@
<template>
<el-dropdown trigger="click" @command="handleLanguageChange">
<div class="i-svg:language" />
<div class="i-svg:language" :class="size" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item

View File

@@ -6,13 +6,17 @@ export default {
},
// 登录页面国际化
login: {
theneToggle: "Theme Switch",
languageToggle: "Language Switch",
dark: "Dark",
light: "Light",
username: "Username",
password: "Password",
login: "Login",
captchaCode: "Verify Code",
capsLock: "Caps Lock is On",
rememberMe: "Remember Me",
forgetPassword: "Forget Password",
forgetPassword: "Forget Password?",
message: {
username: {
required: "Please enter Username",
@@ -20,12 +24,23 @@ export default {
password: {
required: "Please enter Password",
min: "The password can not be less than 6 digits",
confirm: "Please confirm the password again",
inconformity: "The two password entries are inconsistent",
},
captchaCode: {
required: "Please enter Verify Code",
},
},
otherLoginMethods: "Other login methods",
otherLoginMethods: "Other",
resetPassword: "Reset password",
thinkOfPasswd: "Remember your password?",
register: "Register account",
agree: "I have read and agree to the",
userAgreement: "User Agreement",
haveAccount: "Already have an account?",
noAccount: "Don't have an account?",
quickFill: "Quick fill",
reg: "Register",
},
// 导航栏国际化
navbar: {

View File

@@ -6,13 +6,17 @@ export default {
},
// 登录页面国际化
login: {
theneToggle: "主题切换",
languageToggle: "语言切换",
dark: "暗黑",
light: "明亮",
username: "用户名",
password: "密码",
login: "登 录",
captchaCode: "验证码",
capsLock: "大写锁定已打开",
rememberMe: "记住我",
forgetPassword: "忘记密码",
forgetPassword: "忘记密码",
message: {
username: {
required: "请输入用户名",
@@ -20,12 +24,23 @@ export default {
password: {
required: "请输入密码",
min: "密码不能少于6位",
confirm: "请再次确认密码",
inconformity: "两次密码输入不一致",
},
captchaCode: {
required: "请输入验证码",
},
},
otherLoginMethods: "其他登录方式",
otherLoginMethods: "其他",
resetPassword: "重置密码",
thinkOfPasswd: "想起密码?",
register: "注册账号",
agree: "我已同意并阅读",
userAgreement: "用户协议",
haveAccount: "已有账号?",
noAccount: "您没有账号?",
quickFill: "快速填写",
reg: "注 册",
},
// 导航栏国际化
navbar: {

View File

@@ -0,0 +1,253 @@
<template>
<div>
<h3 text-center m-0 mb-20px>{{ t("login.login") }}</h3>
<el-form ref="loginFormRef" :model="loginFormData" :rules="loginRules" size="large">
<!-- 用户名 -->
<el-form-item prop="username">
<el-input v-model.trim="loginFormData.username" :placeholder="t('login.username')">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<!-- 密码 -->
<el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
<el-form-item prop="password">
<el-input
v-model.trim="loginFormData.password"
:placeholder="t('login.password')"
type="password"
show-password
@keyup="checkCapsLock"
@keyup.enter="handleLoginSubmit"
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
</el-tooltip>
<!-- 验证码 -->
<el-form-item prop="captchaCode">
<div flex>
<el-input
v-model.trim="loginFormData.captchaCode"
:placeholder="t('login.captchaCode')"
@keyup.enter="handleLoginSubmit"
>
<template #prefix>
<div class="i-svg:captcha" />
</template>
</el-input>
<div cursor-pointer h="[40px]" w="[120px]" flex-center ml-10px @click="getCaptcha">
<el-icon v-if="codeLoading" class="is-loading"><Loading /></el-icon>
<img
v-else
object-cover
border-rd-4px
p-1px
shadow="[0_0_0_1px_var(--el-border-color)_inset]"
:src="captchaBase64"
alt="code"
/>
</div>
</div>
</el-form-item>
<!-- 快捷登录 -->
<div flex-y-center gap-10px>
<el-text size="default">{{ t("login.quickFill") }}</el-text>
<el-link type="danger" @click="setLoginCredentials('root', '123456')">ROOT</el-link>
<el-link type="warning" @click="setLoginCredentials('admin', '123456')">ADMIN</el-link>
<el-link type="primary" @click="setLoginCredentials('test', '123456')">TEST</el-link>
</div>
<div class="flex-x-between w-full">
<el-checkbox v-model="loginFormData.rememberMe">{{ t("login.rememberMe") }}</el-checkbox>
<el-link type="primary" :underline="false" @click="toOtherForm('resetPwd')">
{{ t("login.forgetPassword") }}
</el-link>
</div>
<!-- 登录按钮 -->
<el-form-item>
<el-button :loading="loading" type="primary" class="w-full" @click="handleLoginSubmit">
{{ t("login.login") }}
</el-button>
</el-form-item>
</el-form>
<div flex-center gap-10px>
<el-text size="default">{{ t("login.noAccount") }}</el-text>
<el-link type="primary" :underline="false" @click="toOtherForm('register')">
{{ t("login.reg") }}
</el-link>
</div>
<!-- 第三方登录 -->
<el-divider>
<el-text size="small">{{ t("login.otherLoginMethods") }}</el-text>
</el-divider>
<div class="flex-center gap-x-5 w-full text-[var(--el-text-color-secondary)]">
<CommonWrapper>
<div text-20px class="i-svg:wechat" />
</CommonWrapper>
<CommonWrapper>
<div text-20px cursor-pointer class="i-svg:qq" />
</CommonWrapper>
<CommonWrapper>
<div text-20px cursor-pointer class="i-svg:github" />
</CommonWrapper>
<CommonWrapper>
<div text-20px cursor-pointer class="i-svg:gitee" />
</CommonWrapper>
</div>
</div>
</template>
<script setup lang="ts">
import type { FormInstance } from "element-plus";
import { LocationQuery, RouteLocationRaw, useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import AuthAPI, { type LoginFormData } from "@/api/auth.api";
import router from "@/router";
import { useUserStore } from "@/store";
import CommonWrapper from "@/components/CommonWrapper/index.vue";
const { t } = useI18n();
const userStore = useUserStore();
const route = useRoute();
onMounted(() => getCaptcha());
const loginFormRef = ref<FormInstance>();
const loading = ref(false); // 按钮 loading 状态
const isCapsLock = ref(false); // 是否大写锁定
const captchaBase64 = ref(); // 验证码图片Base64字符串
const loginFormData = ref<LoginFormData>({
username: "admin",
password: "123456",
captchaKey: "",
captchaCode: "",
rememberMe: false,
});
const loginRules = computed(() => {
return {
username: [
{
required: true,
trigger: "blur",
message: t("login.message.username.required"),
},
],
password: [
{
required: true,
trigger: "blur",
message: t("login.message.password.required"),
},
{
min: 6,
message: t("login.message.password.min"),
trigger: "blur",
},
],
captchaCode: [
{
required: true,
trigger: "blur",
message: t("login.message.captchaCode.required"),
},
],
};
});
// 获取验证码
const codeLoading = ref(false);
function getCaptcha() {
codeLoading.value = true;
AuthAPI.getCaptcha()
.then((data) => {
loginFormData.value.captchaKey = data.captchaKey;
captchaBase64.value = data.captchaBase64;
})
.finally(() => (codeLoading.value = false));
}
// 登录提交处理
async function handleLoginSubmit() {
try {
// 1. 表单验证
const valid = await loginFormRef.value?.validate();
if (!valid) return;
loading.value = true;
// 2. 执行登录
await userStore.login(loginFormData.value);
// 3. 获取用户信息
await userStore.getUserInfo();
// 4. 解析并跳转目标地址
const redirect = resolveRedirectTarget(route.query);
await router.push(redirect);
// TODO 5. 判断用户是否点击了记住我采用明文保存或使用jsencrypt库
} catch (error) {
// 5. 统一错误处理
getCaptcha(); // 刷新验证码
console.error("登录失败:", error);
} finally {
loading.value = false;
}
}
/**
* 解析重定向目标
* @param query 路由查询参数
* @returns 标准化后的路由地址对象
*/
function resolveRedirectTarget(query: LocationQuery): RouteLocationRaw {
// 默认跳转路径
const defaultPath = "/";
// 获取原始重定向路径
const rawRedirect = (query.redirect as string) || defaultPath;
try {
// 6. 使用Vue Router解析路径
const resolved = router.resolve(rawRedirect);
return {
path: resolved.path,
query: resolved.query,
};
} catch {
// 7. 异常处理:返回安全路径
return { path: defaultPath };
}
}
// 检查输入大小写
function checkCapsLock(event: KeyboardEvent) {
// 防止浏览器密码自动填充时报错
if (event instanceof KeyboardEvent) {
isCapsLock.value = event.getModifierState("CapsLock");
}
}
// 设置登录凭证
const setLoginCredentials = (username: string, password: string) => {
loginFormData.value.username = username;
loginFormData.value.password = password;
};
const emit = defineEmits(["update:modelValue"]);
function toOtherForm(type: "register" | "resetPwd") {
emit("update:modelValue", type);
}
</script>

View File

@@ -0,0 +1,203 @@
<template>
<div>
<h3 text-center m-0 mb-20px>{{ t("login.reg") }}</h3>
<el-form ref="formRef" :model="model" :rules="rules" size="large">
<!-- 用户名 -->
<el-form-item prop="username">
<el-input v-model.trim="model.username" :placeholder="t('login.username')">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<!-- 密码 -->
<el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
<el-form-item prop="password">
<el-input
v-model.trim="model.password"
:placeholder="t('login.password')"
type="password"
show-password
@keyup="checkCapsLock"
@keyup.enter="submit"
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
</el-tooltip>
<el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
<el-form-item prop="confirmPassword">
<el-input
v-model.trim="model.confirmPassword"
:placeholder="t('login.message.password.confirm')"
type="password"
show-password
@keyup="checkCapsLock"
@keyup.enter="submit"
>
<template #prefix>
<el-icon><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
</el-tooltip>
<!-- 验证码 -->
<el-form-item prop="captchaCode">
<div flex>
<el-input
v-model.trim="model.captchaCode"
:placeholder="t('login.captchaCode')"
@keyup.enter="submit"
>
<template #prefix>
<div class="i-svg:captcha" />
</template>
</el-input>
<div cursor-pointer h="[40px]" w="[120px]" flex-center ml-10px @click="getCaptcha">
<el-icon v-if="codeLoading" class="is-loading"><Loading /></el-icon>
<img
v-else
object-cover
border-rd-4px
p-1px
shadow="[0_0_0_1px_var(--el-border-color)_inset]"
:src="captchaBase64"
alt="code"
/>
</div>
</div>
</el-form-item>
<el-form-item>
<div class="flex-y-center w-full gap-10px">
<el-checkbox v-model="isRead">{{ t("login.agree") }}</el-checkbox>
<el-link type="primary" :underline="false">{{ t("login.userAgreement") }}</el-link>
</div>
</el-form-item>
<!-- 注册按钮 -->
<el-form-item>
<el-button :loading="loading" type="success" class="w-full" @click="submit">
{{ t("login.register") }}
</el-button>
</el-form-item>
</el-form>
<div flex-center gap-10px>
<el-text size="default">{{ t("login.haveAccount") }}</el-text>
<el-link type="primary" :underline="false" @click="toLogin">{{ t("login.login") }}</el-link>
</div>
</div>
</template>
<script setup lang="ts">
import type { FormInstance } from "element-plus";
import { Lock } from "@element-plus/icons-vue";
import { useI18n } from "vue-i18n";
import AuthAPI, { type LoginFormData } from "@/api/auth.api";
const { t } = useI18n();
const emit = defineEmits(["update:modelValue"]);
const toLogin = () => emit("update:modelValue", "login");
onMounted(() => getCaptcha());
const formRef = ref<FormInstance>();
const loading = ref(false); // 按钮 loading 状态
const isCapsLock = ref(false); // 是否大写锁定
const captchaBase64 = ref(); // 验证码图片Base64字符串
const isRead = ref(false);
interface Model extends LoginFormData {
confirmPassword: string;
}
const model = ref<Model>({
username: "admin",
password: "123456",
confirmPassword: "",
captchaKey: "",
captchaCode: "",
rememberMe: false,
});
const rules = computed(() => {
return {
username: [
{
required: true,
trigger: "blur",
message: t("login.message.username.required"),
},
],
password: [
{
required: true,
trigger: "blur",
message: t("login.message.password.required"),
},
{
min: 6,
message: t("login.message.password.min"),
trigger: "blur",
},
],
confirmPassword: [
{
required: true,
trigger: "blur",
message: t("login.message.password.required"),
},
{
min: 6,
message: t("login.message.password.min"),
trigger: "blur",
},
{
validator: (_: any, value: string) => {
return value === model.value.password;
},
trigger: "blur",
message: t("login.message.password.inconformity"),
},
],
captchaCode: [
{
required: true,
trigger: "blur",
message: t("login.message.captchaCode.required"),
},
],
};
});
// 获取验证码
const codeLoading = ref(false);
function getCaptcha() {
codeLoading.value = true;
AuthAPI.getCaptcha()
.then((data) => {
model.value.captchaKey = data.captchaKey;
captchaBase64.value = data.captchaBase64;
})
.finally(() => (codeLoading.value = false));
}
// 检查输入大小写
function checkCapsLock(event: KeyboardEvent) {
// 防止浏览器密码自动填充时报错
if (event instanceof KeyboardEvent) {
isCapsLock.value = event.getModifierState("CapsLock");
}
}
const submit = async () => {
await formRef.value?.validate();
ElMessage.warning("开发中 ...");
};
</script>

View File

@@ -0,0 +1,57 @@
<template>
<div>
<h3 text-center m-0 mb-20px>{{ t("login.resetPassword") }}</h3>
<el-form ref="formRef" :model="model" :rules="rules" size="large">
<!-- 用户名 -->
<el-form-item prop="username">
<el-input v-model.trim="model.username" :placeholder="t('login.username')">
<template #prefix>
<el-icon><User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="warning" class="w-full" @click="submit">
{{ t("login.resetPassword") }}
</el-button>
</el-form-item>
</el-form>
<div flex-center gap-10px>
<el-text size="default">{{ t("login.thinkOfPasswd") }}</el-text>
<el-link type="primary" :underline="false" @click="toLogin">{{ t("login.login") }}</el-link>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import type { FormInstance } from "element-plus";
const { t } = useI18n();
const emit = defineEmits(["update:modelValue"]);
const toLogin = () => emit("update:modelValue", "login");
const model = ref({
username: "",
});
const rules = computed(() => {
return {
username: [
{
required: true,
trigger: "blur",
message: t("login.message.username.required"),
},
],
};
});
const formRef = ref<FormInstance>();
const submit = async () => {
await formRef.value?.validate();
ElMessage.warning("开发中 ...");
};
</script>

View File

@@ -1,294 +1,86 @@
<template>
<div class="login flex-center wh-full relative overflow-scroll">
<!-- 登录页头部 -->
<div class="flex-x-end absolute top-0 w-full p-4">
<el-switch
v-model="isDark"
inline-prompt
active-icon="Moon"
inactive-icon="Sunny"
@change="toggleTheme"
/>
<lang-select class="ml-2 cursor-pointer" />
<div class="wh-full flex-center flex-col login">
<!-- 右侧切换主题语言按钮 -->
<div class="flex flex-col gap-4px fixed top-40px right-40px text-lg">
<el-tooltip :content="t('login.theneToggle')" placement="left">
<CommonWrapper>
<DarkModeSwitch />
</CommonWrapper>
</el-tooltip>
<el-tooltip :content="t('login.languageToggle')" placement="left">
<CommonWrapper>
<LangSelect size="text-20px" />
</CommonWrapper>
</el-tooltip>
</div>
<!-- 登录页主体 -->
<div flex-1 flex-center>
<div
class="p-4xl h-full w-full sm:w-450px border-rd-10px sm:h-680px shadow-[var(--el-box-shadow-light)] backdrop-blur-3px"
>
<div w-full flex flex-col items-center>
<!-- logo -->
<el-image :src="logo" style="width: 84px" />
<!-- 登录页内容 -->
<div
class="m-5 w-full min-w-[335px] max-w-[460px] p-5 shadow-[var(--el-box-shadow-light)] sm:p-10"
:class="[isDark ? 'bg-transparent' : 'bg-white']"
>
<div class="flex-center relative pb-5">
<h2>{{ defaultSettings.title }}</h2>
<el-dropdown class="absolute! right-0">
<div class="cursor-pointer">
<el-icon><arrow-down /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-tag>{{ defaultSettings.version }}</el-tag>
</el-dropdown-item>
<el-dropdown-item @click="setLoginCredentials('root', '123456')">
超级管理员: root/123456
</el-dropdown-item>
<el-dropdown-item @click="setLoginCredentials('admin', '123456')">
系统管理员: admin/123456
</el-dropdown-item>
<el-dropdown-item @click="setLoginCredentials('test', '123456')">
测试小游客: test/123456
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 标题 -->
<h2>
<el-badge :value="`v ${defaultSettings.version}`" type="success">
{{ defaultSettings.title }}
</el-badge>
</h2>
<!-- 组件切换 -->
<transition name="fade-slide" mode="out-in">
<component :is="formComponents[component]" v-model="component" class="w-90%" />
</transition>
</div>
</div>
<el-form ref="loginFormRef" :model="loginFormData" :rules="loginRules" size="large">
<!-- 用户名 -->
<el-form-item prop="username">
<el-input v-model.trim="loginFormData.username" :placeholder="t('login.username')">
<template #prefix>
<el-icon :color="isDark ? 'white' : 'black'"><User /></el-icon>
</template>
</el-input>
</el-form-item>
<!-- 密码 -->
<el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
<el-form-item prop="password">
<el-input
v-model.trim="loginFormData.password"
:placeholder="t('login.password')"
type="password"
show-password
@keyup="checkCapsLock"
@keyup.enter="handleLoginSubmit"
>
<template #prefix>
<el-icon :color="isDark ? 'white' : 'black'"><Lock /></el-icon>
</template>
</el-input>
</el-form-item>
</el-tooltip>
<!-- 验证码 -->
<el-form-item prop="captchaCode">
<el-input
v-model.trim="loginFormData.captchaCode"
:placeholder="t('login.captchaCode')"
@keyup.enter="handleLoginSubmit"
>
<template #prefix>
<div :class="['i-svg:captcha', isDark ? 'text-white' : 'text-black']" />
</template>
<template #suffix>
<el-image :src="captchaBase64" class="w-[138px] cursor-pointer" @click="getCaptcha" />
</template>
</el-input>
</el-form-item>
<div class="flex-x-between w-full">
<el-checkbox v-model="loginFormData.rememberMe">{{ t("login.rememberMe") }}</el-checkbox>
<el-link type="primary" @click="unfinished">{{ t("login.forgetPassword") }}</el-link>
</div>
<!-- 登录按钮 -->
<el-button :loading="loading" type="primary" class="w-full" @click="handleLoginSubmit">
{{ t("login.login") }}
</el-button>
<!-- 第三方登录 -->
<el-divider>
<el-text size="small">{{ t("login.otherLoginMethods") }}</el-text>
</el-divider>
<div class="flex-center gap-x-5 text-[var(--el-text-color-secondary)]">
<div class="i-svg:wechat" />
<div class="i-svg:qq" />
<div class="i-svg:github" />
<div class="i-svg:gitee" />
</div>
</el-form>
<!-- 登录页底部版权 -->
<el-text size="small" class="py-2.5! fixed bottom-0 text-center">
Copyright © 2021 - 2025 youlai.tech All Rights Reserved.
<a href="http://beian.miit.gov.cn/" target="_blank">皖ICP备20006496号-2</a>
</el-text>
</div>
<!-- 登录页底部 -->
<el-text size="small" class="py-2.5! fixed bottom-0 text-center">
Copyright © 2021 - 2025 youlai.tech All Rights Reserved.
<a href="http://beian.miit.gov.cn/" target="_blank">皖ICP备20006496号-2</a>
</el-text>
</div>
</template>
<script setup lang="ts">
import { LocationQuery, RouteLocationRaw, useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import AuthAPI, { type LoginFormData } from "@/api/auth.api";
import router from "@/router";
import type { FormInstance } from "element-plus";
import logo from "@/assets/logo.png";
import defaultSettings from "@/settings";
import { ThemeMode } from "@/enums/settings/theme.enum";
import CommonWrapper from "@/components/CommonWrapper/index.vue";
import DarkModeSwitch from "@/components/DarkModeSwitch/index.vue";
import { useSettingsStore, useUserStore } from "@/store";
type LayoutMap = "login" | "register" | "resetPwd";
const userStore = useUserStore();
const settingsStore = useSettingsStore();
const t = useI18n().t;
const route = useRoute();
const { t } = useI18n();
const loginFormRef = ref<FormInstance>();
const isDark = ref(settingsStore.theme === ThemeMode.DARK); // 是否暗黑模式
const loading = ref(false); // 按钮 loading 状态
const isCapsLock = ref(false); // 是否大写锁定
const captchaBase64 = ref(); // 验证码图片Base64字符串
const loginFormData = ref<LoginFormData>({
username: "admin",
password: "123456",
captchaKey: "",
captchaCode: "",
rememberMe: false,
});
const loginRules = computed(() => {
return {
username: [
{
required: true,
trigger: "change",
message: t("login.message.username.required"),
},
],
password: [
{
required: true,
trigger: "change",
message: t("login.message.password.required"),
},
{
min: 6,
message: t("login.message.password.min"),
trigger: "blur",
},
],
captchaCode: [
{
required: true,
trigger: "change",
message: t("login.message.captchaCode.required"),
},
],
};
});
// 获取验证码
function getCaptcha() {
AuthAPI.getCaptcha().then((data) => {
loginFormData.value.captchaKey = data.captchaKey;
captchaBase64.value = data.captchaBase64;
});
}
// 登录提交处理
async function handleLoginSubmit() {
try {
// 1. 表单验证
const valid = await loginFormRef.value?.validate();
if (!valid) return;
loading.value = true;
// 2. 执行登录
await userStore.login(loginFormData.value);
// 3. 获取用户信息
await userStore.getUserInfo();
// 4. 解析并跳转目标地址
const redirect = resolveRedirectTarget(route.query);
await router.push(redirect);
} catch (error) {
// 5. 统一错误处理
getCaptcha(); // 刷新验证码
console.error("登录失败:", error);
} finally {
loading.value = false;
}
}
/**
* 解析重定向目标
* @param query 路由查询参数
* @returns 标准化后的路由地址对象
*/
function resolveRedirectTarget(query: LocationQuery): RouteLocationRaw {
// 默认跳转路径
const defaultPath = "/";
// 获取原始重定向路径
const rawRedirect = (query.redirect as string) || defaultPath;
try {
// 6. 使用Vue Router解析路径
const resolved = router.resolve(rawRedirect);
return {
path: resolved.path,
query: resolved.query,
};
} catch {
// 7. 异常处理:返回安全路径
return { path: defaultPath };
}
}
// 主题切换
const toggleTheme = () => {
const newTheme = settingsStore.theme === ThemeMode.DARK ? ThemeMode.LIGHT : ThemeMode.DARK;
settingsStore.changeTheme(newTheme);
const component = ref<LayoutMap>("login"); // 切换显示的组件
const formComponents = {
login: defineAsyncComponent(() => import("./components/Login.vue")),
register: defineAsyncComponent(() => import("./components/Register.vue")),
resetPwd: defineAsyncComponent(() => import("./components/ResetPwd.vue")),
};
// 检查输入大小写
function checkCapsLock(event: KeyboardEvent) {
// 防止浏览器密码自动填充时报错
if (event instanceof KeyboardEvent) {
isCapsLock.value = event.getModifierState("CapsLock");
}
}
// 设置登录凭证
const setLoginCredentials = (username: string, password: string) => {
loginFormData.value.username = username;
loginFormData.value.password = password;
};
// 未实现功能提示
const unfinished = () => {
ElMessage.warning("该功能尚未完成,敬请期待!");
};
onMounted(() => getCaptcha());
</script>
<style lang="scss" scoped>
.login {
background: url("@/assets/images/login-bg.jpg") no-repeat center right;
background: url("@/assets/images/login-bg.svg");
background-size: cover;
}
html.dark {
.login {
background: url("@/assets/images/login-bg-dark.jpg") no-repeat center right;
.el-card {
background: transparent;
}
}
/* fade-slide */
.fade-slide-leave-active,
.fade-slide-enter-active {
transition: all 0.3s;
}
.el-form-item {
margin-bottom: 20px;
.fade-slide-enter-from {
opacity: 0;
transform: translateX(-30px);
}
:deep(.el-input) {
height: 48px;
.el-input__wrapper {
padding: 1px 10px;
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>