feat(login): 全面增强登录组件用户体验
✨ 新增功能: - 支持回车键登录,在任意输入框按回车即可触发登录 - 登录失败时自动刷新验证码并清空验证码输入框 - 重定向访问时显示优雅的loading状态和身份过期提示 - 智能检测URL重定向参数,提供个性化用户引导 🎨 界面优化: - 新增专业的loading界面,包含旋转器和动态点阵动画 - 优化过渡动画效果,提供平滑的视觉体验 - 使用项目主题色,确保视觉统一性 - 支持暗色模式适配 🔧 技术改进: - 增强错误处理机制,防止重复提交 - 优化验证码刷新逻辑,提升用户操作便利性 - 改进loading时序控制,确保用户看到重要提示信息
This commit is contained in:
parent
92c886346c
commit
d415592762
@ -1,94 +1,119 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- 头部问候语 -->
|
<!-- 重定向Loading状态 -->
|
||||||
<div class="text-center">
|
<div v-if="isRedirectLoading" class="text-center space-y-6 py-8">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
<div class="flex justify-center">
|
||||||
欢迎回来 👋
|
<n-spin size="large" stroke="var(--primary-color)" />
|
||||||
</h1>
|
</div>
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
<div class="space-y-2">
|
||||||
请输入您的详细信息以开始管理您的帐户
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
</p>
|
正在检查登录状态...
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
请稍候,系统正在为您准备登录界面
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center space-x-1">
|
||||||
|
<div class="loading-dot" style="animation-delay: 0s;" />
|
||||||
|
<div class="loading-dot" style="animation-delay: 0.1s;" />
|
||||||
|
<div class="loading-dot" style="animation-delay: 0.2s;" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 登录表单 -->
|
<!-- 正常登录状态 -->
|
||||||
<n-form ref="formRef" :rules="rules" :model="formValue" :show-label="false" size="large" class="space-y-4">
|
<div v-else>
|
||||||
<!-- 账号输入 -->
|
<!-- 头部问候语 -->
|
||||||
<n-form-item path="account">
|
<div class="text-center">
|
||||||
<n-input
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
v-model:value="formValue.account"
|
欢迎回来 👋
|
||||||
clearable
|
</h1>
|
||||||
:placeholder="$t('login.accountPlaceholder')"
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
class="login-input"
|
请输入您的详细信息以开始管理您的帐户
|
||||||
/>
|
</p>
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<!-- 密码输入 -->
|
|
||||||
<n-form-item path="pwd">
|
|
||||||
<n-input
|
|
||||||
v-model:value="formValue.pwd"
|
|
||||||
type="password"
|
|
||||||
:placeholder="$t('login.passwordPlaceholder')"
|
|
||||||
clearable
|
|
||||||
show-password-on="click"
|
|
||||||
class="login-input"
|
|
||||||
>
|
|
||||||
<template #password-invisible-icon>
|
|
||||||
<icon-park-outline-preview-close-one />
|
|
||||||
</template>
|
|
||||||
<template #password-visible-icon>
|
|
||||||
<icon-park-outline-preview-open />
|
|
||||||
</template>
|
|
||||||
</n-input>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<!-- 验证码输入 -->
|
|
||||||
<n-form-item path="securityCode">
|
|
||||||
<div class="flex w-full gap-3">
|
|
||||||
<n-input
|
|
||||||
v-model:value="formValue.securityCode"
|
|
||||||
placeholder="请输入验证码"
|
|
||||||
clearable
|
|
||||||
class="flex-1 login-input"
|
|
||||||
maxlength="5"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="w-32 h-12 cursor-pointer rounded-lg border-2 border-gray-200 flex items-center justify-center overflow-hidden bg-white dark:bg-gray-700 dark:border-gray-600 transition-colors duration-200 captcha-button"
|
|
||||||
title="点击刷新验证码"
|
|
||||||
@click="getCaptcha"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
v-if="captchaImage"
|
|
||||||
:src="captchaImage"
|
|
||||||
alt="验证码"
|
|
||||||
class="w-full h-full object-contain"
|
|
||||||
>
|
|
||||||
<span v-else class="text-sm text-gray-500 dark:text-gray-400">点击获取验证码</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<!-- 记住密码和忘记密码 -->
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<n-checkbox v-model:checked="isRemember" class="text-sm">
|
|
||||||
{{ $t('login.rememberMe') }}
|
|
||||||
</n-checkbox>
|
|
||||||
<n-button type="primary" text class="text-sm" @click="toOtherForm('resetPwd')">
|
|
||||||
{{ $t('login.forgotPassword') }}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 登录按钮 -->
|
<!-- 登录表单 -->
|
||||||
<n-button
|
<n-form ref="formRef" :rules="rules" :model="formValue" :show-label="false" size="large" class="space-y-4" @keyup.enter="handleLogin">
|
||||||
block
|
<!-- 账号输入 -->
|
||||||
type="primary"
|
<n-form-item path="account">
|
||||||
size="large"
|
<n-input
|
||||||
:loading="isLoading"
|
v-model:value="formValue.account"
|
||||||
:disabled="isLoading"
|
clearable
|
||||||
class="login-button"
|
:placeholder="$t('login.accountPlaceholder')"
|
||||||
@click="handleLogin"
|
class="login-input"
|
||||||
>
|
@keyup.enter="handleLogin"
|
||||||
{{ $t('login.signIn') }}
|
/>
|
||||||
</n-button>
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 密码输入 -->
|
||||||
|
<n-form-item path="pwd">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formValue.pwd"
|
||||||
|
type="password"
|
||||||
|
:placeholder="$t('login.passwordPlaceholder')"
|
||||||
|
clearable
|
||||||
|
show-password-on="click"
|
||||||
|
class="login-input"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
>
|
||||||
|
<template #password-invisible-icon>
|
||||||
|
<icon-park-outline-preview-close-one />
|
||||||
|
</template>
|
||||||
|
<template #password-visible-icon>
|
||||||
|
<icon-park-outline-preview-open />
|
||||||
|
</template>
|
||||||
|
</n-input>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 验证码输入 -->
|
||||||
|
<n-form-item path="securityCode">
|
||||||
|
<div class="flex w-full gap-3">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formValue.securityCode"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
clearable
|
||||||
|
class="flex-1 login-input"
|
||||||
|
maxlength="5"
|
||||||
|
@keyup.enter="handleLogin"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="w-32 h-12 cursor-pointer rounded-lg border-2 border-gray-200 flex items-center justify-center overflow-hidden bg-white dark:bg-gray-700 dark:border-gray-600 transition-colors duration-200 captcha-button"
|
||||||
|
title="点击刷新验证码"
|
||||||
|
@click="getCaptcha"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="captchaImage"
|
||||||
|
:src="captchaImage"
|
||||||
|
alt="验证码"
|
||||||
|
class="w-full h-full object-contain"
|
||||||
|
>
|
||||||
|
<span v-else class="text-sm text-gray-500 dark:text-gray-400">点击获取验证码</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 记住密码和忘记密码 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<n-checkbox v-model:checked="isRemember" class="text-sm">
|
||||||
|
{{ $t('login.rememberMe') }}
|
||||||
|
</n-checkbox>
|
||||||
|
<n-button type="primary" text class="text-sm" @click="toOtherForm('resetPwd')">
|
||||||
|
{{ $t('login.forgotPassword') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录按钮 -->
|
||||||
|
<n-button
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:loading="isLoading"
|
||||||
|
:disabled="isLoading"
|
||||||
|
class="login-button"
|
||||||
|
@click="handleLogin"
|
||||||
|
>
|
||||||
|
{{ $t('login.signIn') }}
|
||||||
|
</n-button>
|
||||||
|
|
||||||
<!-- 注册链接 -->
|
<!-- 注册链接 -->
|
||||||
<!-- <div class="text-center"> -->
|
<!-- <div class="text-center"> -->
|
||||||
@ -97,7 +122,8 @@
|
|||||||
<!-- {{ $t('login.signUp') }} -->
|
<!-- {{ $t('login.signUp') }} -->
|
||||||
<!-- </n-button> -->
|
<!-- </n-button> -->
|
||||||
<!-- </div> -->
|
<!-- </div> -->
|
||||||
</n-form>
|
</n-form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -106,6 +132,7 @@ import type { FormInst } from 'naive-ui'
|
|||||||
import { useAppStore, useAuthStore } from '@/store'
|
import { useAppStore, useAuthStore } from '@/store'
|
||||||
import { fetchCaptchaPng } from '@/service/api/auth'
|
import { fetchCaptchaPng } from '@/service/api/auth'
|
||||||
import { local } from '@/utils'
|
import { local } from '@/utils'
|
||||||
|
import { coiMsgWarning } from '@/utils/coi'
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
@ -148,6 +175,7 @@ const formValue = ref({
|
|||||||
})
|
})
|
||||||
const isRemember = ref(false)
|
const isRemember = ref(false)
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
const isRedirectLoading = ref(false)
|
||||||
|
|
||||||
// 验证码相关
|
// 验证码相关
|
||||||
const captchaImage = ref('')
|
const captchaImage = ref('')
|
||||||
@ -169,6 +197,10 @@ async function getCaptcha() {
|
|||||||
|
|
||||||
const formRef = ref<FormInst | null>(null)
|
const formRef = ref<FormInst | null>(null)
|
||||||
function handleLogin() {
|
function handleLogin() {
|
||||||
|
// 防止重复提交
|
||||||
|
if (isLoading.value)
|
||||||
|
return
|
||||||
|
|
||||||
formRef.value?.validate(async (errors) => {
|
formRef.value?.validate(async (errors) => {
|
||||||
if (errors)
|
if (errors)
|
||||||
return
|
return
|
||||||
@ -180,14 +212,38 @@ function handleLogin() {
|
|||||||
local.set('loginAccount', { account, pwd })
|
local.set('loginAccount', { account, pwd })
|
||||||
else local.remove('loginAccount')
|
else local.remove('loginAccount')
|
||||||
|
|
||||||
await authStore.login(account, pwd, captchaKey.value, securityCode, isRemember.value)
|
try {
|
||||||
isLoading.value = false
|
await authStore.login(account, pwd, captchaKey.value, securityCode, isRemember.value)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// 登录失败时刷新验证码并清空验证码输入框
|
||||||
|
await getCaptcha()
|
||||||
|
formValue.value.securityCode = ''
|
||||||
|
console.warn('[Login Failed]:', error)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkUserAccount()
|
checkUserAccount()
|
||||||
getCaptcha()
|
getCaptcha()
|
||||||
|
|
||||||
|
// 检查是否有重定向参数,显示身份过期提示和loading
|
||||||
|
const route = useRoute()
|
||||||
|
if (route.query.redirect) {
|
||||||
|
isRedirectLoading.value = true
|
||||||
|
// 显示loading状态,然后显示提示
|
||||||
|
setTimeout(() => {
|
||||||
|
coiMsgWarning('账号身份过期,请你重新登录')
|
||||||
|
// 延迟隐藏loading,让过渡更自然
|
||||||
|
setTimeout(() => {
|
||||||
|
isRedirectLoading.value = false
|
||||||
|
}, 200)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function checkUserAccount() {
|
function checkUserAccount() {
|
||||||
@ -257,6 +313,40 @@ function checkUserAccount() {
|
|||||||
--n-text-color: #f3f4f6;
|
--n-text-color: #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 过渡动画 */
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading点动画 */
|
||||||
|
.loading-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: v-bind(primaryColor);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: bounce 1.4s ease-in-out infinite both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 80%, 100% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.login-input {
|
.login-input {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user