Repository: AstrBotDevs/AstrBot
Pull request #1589: 🎈 perf: 优化了登录界面，解决了登录未自行跳转的问题
Author: login: Larch-C
Created: 2025-05-17T10:14:39Z | Merged: 2025-05-17T13:40:14Z

<!-- 如果有的话，指定这个 PR 要解决的 ISSUE -->
解决了 #XYZ

### Motivation

<!--解释为什么要改动-->

 - 优化了登录界面样式，使其更加美观
 - 解决了如果此前已经登录面板但未自行跳转的问题

### Modifications

<!--简单解释你的改动-->


### Check

<!--如果分支被合并，您的代码将服务于数万名用户！在提交前，请核查一下几点内容-->

- [x] 😊 我的 Commit Message 符合良好的[规范](https://www.conventionalcommits.org/en/v1.0.0/#summary)
- [x] 👀 我的更改经过良好的测试
- [x] 🤓 我确保没有引入新依赖库，或者引入了新依赖库的同时将其添加到了 `requirements.txt` 和 `pyproject.toml` 文件相应位置。
- [x] 😮 我的更改没有引入恶意代码

好的，这是将拉取请求摘要翻译成中文的结果：

## Sourcery 总结

通过彻底修改表单样式和动画、添加加载状态、提取 Logo 组件以及确保具有有效会话的用户重定向离开登录页面，优化登录体验

新功能：
- 为已通过身份验证的用户在访问登录页面时添加自动重定向
- 引入可重用的 Logo 组件，以实现一致的登录品牌形象

Bug 修复：
- 修复了登录视图上已通过身份验证的用户未重定向到目标页面的问题

增强功能：
- 使用更新的间距、图标、禁用状态和加载指示器改进登录表单外观
- 在登录页面上实现渐变背景和卡片显示动画
- 使用过渡和悬停效果设置表单输入、按钮和错误警报的样式

<details>
<summary>Original summary in English</summary>

## Summary by Sourcery

Optimize the login experience by overhauling the form styling and animations, adding a loading state, extracting a Logo component, and ensuring users with valid sessions are redirected away from the login page

New Features:
- Add automatic redirect for already authenticated users when accessing the login page
- Introduce a reusable Logo component for consistent login branding

Bug Fixes:
- Fix issue where authenticated users on the login view did not redirect to the intended page

Enhancements:
- Revamp login form appearance with updated spacing, icons, disabled states, and loading indicator
- Implement gradient background and card reveal animation on the login page
- Style form inputs, buttons, and error alerts with transitions and hover effects

</details>


Diff:
diff --git a/dashboard/src/assets/images/astrbot_logo_mini.webp b/dashboard/src/assets/images/astrbot_logo_mini.webp
new file mode 100644
index 0000000000..f6a868c543
Binary files /dev/null and b/dashboard/src/assets/images/astrbot_logo_mini.webp differ
diff --git a/dashboard/src/components/shared/Logo.vue b/dashboard/src/components/shared/Logo.vue
new file mode 100644
index 0000000000..294fae094e
--- /dev/null
+++ b/dashboard/src/components/shared/Logo.vue
@@ -0,0 +1,70 @@
+<template>
+  <div class="logo-container">
+    <div class="logo-content">
+      <div class="logo-image">
+        <img width="110" src="@/assets/images/astrbot_logo_mini.webp" alt="AstrBot Logo">
+      </div>
+      <div class="logo-text">
+        <h2 class="text-secondary">AstrBot 仪表盘</h2>
+        <h4 class="text-disabled">登录以继续</h4>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+// No props or other logic needed for this simple component
+</script>
+
+<style scoped>
+.logo-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  margin-bottom: 10px;
+}
+
+.logo-content {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  padding: 10px;
+}
+
+.logo-image {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.logo-image img {
+  transition: transform 0.3s ease;
+}
+
+.logo-image img:hover {
+  transform: scale(1.05);
+}
+
+.logo-text {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+}
+
+.logo-text h2 {
+  margin: 0;
+  font-size: 1.8rem;
+  font-weight: 600;
+  color: #5e35b1;
+  letter-spacing: 0.5px;
+}
+
+.logo-text h4 {
+  margin: 4px 0 0 0;
+  font-size: 1rem;
+  font-weight: 400;
+  color: #616161;
+  letter-spacing: 0.3px;
+}
+</style>
diff --git a/dashboard/src/router/index.ts b/dashboard/src/router/index.ts
index a56154f18c..c44e0249d6 100644
--- a/dashboard/src/router/index.ts
+++ b/dashboard/src/router/index.ts
@@ -24,6 +24,11 @@ router.beforeEach(async (to, from, next) => {
   const authRequired = !publicPages.includes(to.path);
   const auth: AuthStore = useAuthStore();
 
+  // 如果用户已登录且试图访问登录页面，则重定向到首页或之前尝试访问的页面
+  if (to.path === '/auth/login' && auth.has_token()) {
+    return next(auth.returnUrl || '/');
+  }
+
   if (to.matched.some((record) => record.meta.requiresAuth)) {
     if (authRequired && !auth.has_token()) {
       auth.returnUrl = to.fullPath;
diff --git a/dashboard/src/views/authentication/auth/LoginPage.vue b/dashboard/src/views/authentication/auth/LoginPage.vue
index 63a1f809c4..4bcefe07cd 100644
--- a/dashboard/src/views/authentication/auth/LoginPage.vue
+++ b/dashboard/src/views/authentication/auth/LoginPage.vue
@@ -1,23 +1,111 @@
 <script setup lang="ts">
 import AuthLogin from '../authForms/AuthLogin.vue';
+import Logo from '@/components/shared/Logo.vue';
+import { onMounted, ref } from 'vue';
+import { useAuthStore } from '@/stores/auth';
+import { useRouter } from 'vue-router';
+
+const cardVisible = ref(false);
+const router = useRouter();
+const authStore = useAuthStore();
+
+onMounted(() => {
+  // 检查用户是否已登录，如果已登录则重定向
+  if (authStore.has_token()) {
+    router.push(authStore.returnUrl || '/');
+    return;
+  }
+  
+  // 添加一个小延迟以获得更好的动画效果
+  setTimeout(() => {
+    cardVisible.value = true;
+  }, 100);
+});
 </script>
 
 <template>
-  <div style="display: flex; justify-content: center; flex-direction: column; align-items: center; height: 100vh; background-color: aliceblue;">
-    <v-card variant="outlined" style="max-width: 500px; box-shadow: 0 0 3px rgba(0, 0, 0, 0.1);">
-      <v-card-text class="pa-9">
-        <div class="text-center">
+  <div class="login-page-container">
+    <div class="login-background"></div>
+    <v-card 
+      variant="outlined" 
+      class="login-card" 
+      :class="{ 'card-visible': cardVisible }"
+    >
+      <v-card-text class="pa-10">
+        <div class="logo-wrapper">
           <Logo />
-          <h2 class="text-secondary text-h2 mt-4">AstrBot 仪表盘</h2>
-          <h4 class="text-disabled text-h4 mt-3">登录以继续</h4>
+        </div>
+        <div class="divider-container">
+          <v-divider class="custom-divider"></v-divider>
         </div>
         <AuthLogin />
       </v-card-text>
     </v-card>
   </div>
-
 </template>
+
 <style lang="scss">
+.login-page-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh;
+  width: 100%;
+  position: relative;
+  background: linear-gradient(135deg, #ebf5fd 0%, #e0e9f8 100%);
+  overflow: hidden;
+}
+
+.login-background {
+  position: absolute;
+  width: 200%;
+  height: 200%;
+  top: -50%;
+  left: -50%;
+  background: radial-gradient(circle, rgba(94, 53, 177, 0.03) 0%, rgba(30, 136, 229, 0.06) 70%);
+  z-index: 0;
+  animation: rotate 60s linear infinite;
+}
+
+@keyframes rotate {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+.login-card {
+  max-width: 520px;
+  width: 90%;
+  border-radius: 12px !important;
+  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.07) !important;
+  background-color: rgba(255, 255, 255, 0.98) !important;
+  transform: translateY(20px);
+  opacity: 0;
+  transition: all 0.5s ease;
+  z-index: 1;
+  
+  &.card-visible {
+    transform: translateY(0);
+    opacity: 1;
+  }
+}
+
+.logo-wrapper {
+  margin-bottom: 10px;
+}
+
+.divider-container {
+  margin: 20px 0;
+}
+
+.custom-divider {
+  border-color: rgba(0, 0, 0, 0.05) !important;
+  opacity: 0.8;
+}
+
 .loginBox {
   max-width: 475px;
   margin: 0 auto;
diff --git a/dashboard/src/views/authentication/authForms/AuthLogin.vue b/dashboard/src/views/authentication/authForms/AuthLogin.vue
index 55081f08a3..b2b356087b 100644
--- a/dashboard/src/views/authentication/authForms/AuthLogin.vue
+++ b/dashboard/src/views/authentication/authForms/AuthLogin.vue
@@ -8,9 +8,12 @@ const valid = ref(false);
 const show1 = ref(false);
 const password = ref('');
 const username = ref('');
+const loading = ref(false);
 
 /* eslint-disable @typescript-eslint/no-explicit-any */
 async function validate(values: any, { setErrors }: any) {
+  loading.value = true;
+  
   // md5加密
   let password_ = password.value;
   if (password.value != '') {
@@ -21,67 +24,154 @@ async function validate(values: any, { setErrors }: any) {
   const authStore = useAuthStore();
   return authStore.login(username.value, password_).then((res) => {
     console.log(res);
+    loading.value = false;
   }).catch((err) => {
     setErrors({ apiError: err });
+    loading.value = false;
   });
 }
 
 </script>
 
 <template>
-  <Form @submit="validate" class="mt-7 loginForm" v-slot="{ errors, isSubmitting }">
-    <v-text-field v-model="username" label="用户名" class="mt-4 mb-8" required density="comfortable"
-      hide-details="auto" variant="outlined" color="primary"></v-text-field>
-    <v-text-field v-model="password" label="密码" required density="comfortable" variant="outlined"
-      color="primary" hide-details="auto" :append-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'"
-      :type="show1 ? 'text' : 'password'" @click:append="show1 = !show1" class="pwdInput"></v-text-field>
+  <Form @submit="validate" class="mt-4 login-form" v-slot="{ errors, isSubmitting }">
+    <v-text-field 
+      v-model="username" 
+      label="用户名" 
+      class="mb-6 input-field" 
+      required 
+      density="comfortable"
+      hide-details="auto" 
+      variant="outlined" 
+      color="primary"
+      prepend-inner-icon="mdi-account"
+      :disabled="loading"
+    ></v-text-field>
+    
+    <v-text-field 
+      v-model="password" 
+      label="密码" 
+      required 
+      density="comfortable" 
+      variant="outlined"
+      color="primary" 
+      hide-details="auto" 
+      :append-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'"
+      :type="show1 ? 'text' : 'password'" 
+      @click:append="show1 = !show1" 
+      class="pwd-input"
+      prepend-inner-icon="mdi-lock"
+      :disabled="loading"
+    ></v-text-field>
 
-    <small>默认用户名和密码为 astrbot。</small>
-    <v-btn color="secondary" :loading="isSubmitting" block class="mt-8" variant="flat" size="large" :disabled="valid"
-      type="submit">
-      登录</v-btn>
-    <div v-if="errors.apiError" class="mt-2">
-      <v-alert color="error">{{ errors.apiError }}</v-alert>
+    <div class="mt-1 mb-5 hint-text">
+      <small>默认用户名和密码为 astrbot</small>
+    </div>
+    
+    <v-btn 
+      color="secondary" 
+      :loading="isSubmitting || loading" 
+      block 
+      class="login-btn" 
+      variant="flat" 
+      size="large" 
+      :disabled="valid"
+      type="submit"
+      elevation="2"
+    >
+      <span class="login-btn-text">登录</span>
+    </v-btn>
+    
+    <div v-if="errors.apiError" class="mt-4 error-container">
+      <v-alert 
+        color="error" 
+        variant="tonal" 
+        density="comfortable"
+        icon="mdi-alert-circle"
+        border="start"
+      >
+        {{ errors.apiError }}
+      </v-alert>
     </div>
   </Form>
-
 </template>
 
 <style lang="scss">
-.custom-devider {
-  border-color: rgba(0, 0, 0, 0.08) !important;
-}
-
-.googleBtn {
-  border-color: rgba(0, 0, 0, 0.08);
-  margin: 30px 0 20px 0;
-}
-
-.outlinedInput .v-field {
-  border: 1px solid rgba(0, 0, 0, 0.08);
-  box-shadow: none;
-}
+.login-form {
+  .v-text-field .v-field--active input {
+    font-weight: 500;
+  }
 
-.orbtn {
-  padding: 2px 40px;
-  border-color: rgba(0, 0, 0, 0.08);
-  margin: 20px 15px;
-}
+  .input-field, .pwd-input {
+    .v-field__field {
+      padding-top: 5px;
+      padding-bottom: 5px;
+    }
+    
+    .v-field__outline {
+      opacity: 0.7;
+    }
+    
+    &:hover .v-field__outline {
+      opacity: 0.9;
+    }
+    
+    .v-field--focused .v-field__outline {
+      opacity: 1;
+    }
+    
+    .v-field__prepend-inner {
+      padding-right: 8px;
+      opacity: 0.7;
+    }
+  }
 
-.pwdInput {
-  position: relative;
+  .pwd-input {
+    position: relative;
 
-  .v-input__append {
-    position: absolute;
-    right: 10px;
-    top: 50%;
-    transform: translateY(-50%);
+    .v-input__append {
+      position: absolute;
+      right: 10px;
+      top: 50%;
+      transform: translateY(-50%);
+      opacity: 0.7;
+      
+      &:hover {
+        opacity: 1;
+      }
+    }
+  }
+  
+  .login-btn {
+    margin-top: 12px;
+    height: 48px;
+    transition: all 0.3s ease;
+    letter-spacing: 0.5px;
+    
+    &:hover {
+      transform: translateY(-2px);
+      box-shadow: 0 5px 15px rgba(94, 53, 177, 0.2) !important;
+    }
+    
+    .login-btn-text {
+      font-size: 1.05rem;
+      font-weight: 500;
+    }
+  }
+  
+  .hint-text {
+    color: rgba(0, 0, 0, 0.5);
+    padding-left: 5px;
+  }
+  
+  .error-container {
+    .v-alert {
+      border-left-width: 4px !important;
+    }
   }
 }
 
-.loginForm {
-  .v-text-field .v-field--active input {
-    font-weight: 500;
-  }
+.custom-devider {
+  border-color: rgba(0, 0, 0, 0.08) !important;
 }
 </style>


Reviews:
user: login: sourcery-ai[bot]
body: 嘿 @Larch-C - 我已经查看了你的更改，它们看起来很棒！

<details>
<summary>这是我在审查期间查看的内容</summary>

- 🟡 **General issues**: 发现 3 个问题
- 🟢 **Testing**: 一切看起来都很好
- 🟢 **Complexity**: 一切看起来都很好
- 🟢 **Documentation**: 一切看起来都很好
</details>

***

<details>
<summary>Sourcery 对开源是免费的 - 如果你喜欢我们的评论，请考虑分享它们 ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
帮助我更有用！请点击每个评论上的 👍 或 👎，我将使用反馈来改进你的评论。
</sub>

<details>
<summary>Original comment in English</summary>

Hey @Larch-C - I've reviewed your changes and they look great!

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 3 issues found
- 🟢 **Testing**: all looks good
- 🟢 **Complexity**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>

</details>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T10:22:57Z
user: login: Soulter
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-17T13:37:00Z

Review comments:
diff_hunk: @@ -8,9 +8,12 @@ const valid = ref(false);
 const show1 = ref(false);
 const password = ref('');
 const username = ref('');
+const loading = ref(false);
path: dashboard/src/views/authentication/authForms/AuthLogin.vue
user: login: sourcery-ai[bot]
body: **suggestion:** 考虑在提前退出或出错时重置加载状态。

在 Promise 返回之前抛出的异常（例如，在 md5 逻辑中）将跳过重置 loading.value。将函数包装在 try/finally 中，或在每次提前返回时重置 loading.value，以确保它始终重置。

<details>
<summary>Original comment in English</summary>

**suggestion:** Consider resetting loading state on early exit or error.

An exception thrown before the promise returns (e.g. in the md5 logic) will skip resetting loading.value. Wrap the function in try/finally or reset loading.value on every early return to ensure it always resets.

</details>
created_at: 2025-05-17T10:22:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,67 +24,153 @@ async function validate(values: any, { setErrors }: any) {
   const authStore = useAuthStore();
   return authStore.login(username.value, password_).then((res) => {
     console.log(res);
+    loading.value = false;
   }).catch((err) => {
     setErrors({ apiError: err });
+    loading.value = false;
   });
 }
 
 </script>
 
 <template>
-  <Form @submit="validate" class="mt-7 loginForm" v-slot="{ errors, isSubmitting }">
-    <v-text-field v-model="username" label="用户名" class="mt-4 mb-8" required density="comfortable"
-      hide-details="auto" variant="outlined" color="primary"></v-text-field>
-    <v-text-field v-model="password" label="密码" required density="comfortable" variant="outlined"
-      color="primary" hide-details="auto" :append-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'"
-      :type="show1 ? 'text' : 'password'" @click:append="show1 = !show1" class="pwdInput"></v-text-field>
+  <Form @submit="validate" class="mt-4 login-form" v-slot="{ errors, isSubmitting }">
+    <v-text-field 
+      v-model="username" 
+      label="用户名" 
+      class="mb-6 input-field" 
+      required 
+      density="comfortable"
+      hide-details="auto" 
+      variant="outlined" 
+      color="primary"
+      prepend-inner-icon="mdi-account"
+      :disabled="loading"
+    ></v-text-field>
+    
+    <v-text-field 
+      v-model="password" 
+      label="密码" 
+      required 
+      density="comfortable" 
+      variant="outlined"
+      color="primary" 
+      hide-details="auto" 
+      :append-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'"
+      :type="show1 ? 'text' : 'password'" 
+      @click:append="show1 = !show1" 
+      class="pwd-input"
+      prepend-inner-icon="mdi-lock"
+      :disabled="loading"
+    ></v-text-field>
 
-    <small>默认用户名和密码为 astrbot。</small>
-    <v-btn color="secondary" :loading="isSubmitting" block class="mt-8" variant="flat" size="large" :disabled="valid"
-      type="submit">
-      登录</v-btn>
-    <div v-if="errors.apiError" class="mt-2">
-      <v-alert color="error">{{ errors.apiError }}</v-alert>
+    <div class="mt-1 mb-5 hint-text">
+      <small>默认用户名和密码为 astrbot</small>
+    </div>
+    
+    <v-btn 
+      color="secondary" 
+      :loading="isSubmitting || loading" 
+      block 
+      class="login-btn" 
+      variant="flat" 
+      size="large" 
+      :disabled="valid"
path: dashboard/src/views/authentication/authForms/AuthLogin.vue
user: login: sourcery-ai[bot]
body: **issue (bug_risk):** 重新评估使用 'valid' 变量来禁用登录按钮。

由于 'valid' 保持为 false 且此处未更新，因此 :disabled 不会反映表单状态。删除此绑定或在验证发生的地方更新 'valid'。

<details>
<summary>Original comment in English</summary>

**issue (bug_risk):** Re-evaluate the use of the 'valid' variable for disabling the login button.

Since 'valid' remains false and isn’t updated here, :disabled won’t reflect the form state. Remove this binding or update 'valid' where validation occurs.

</details>
created_at: 2025-05-17T10:22:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,6 +24,11 @@ router.beforeEach(async (to, from, next) => {
   const authRequired = !publicPages.includes(to.path);
   const auth: AuthStore = useAuthStore();
 
+  // 如果用户已登录且试图访问登录页面，则重定向到首页或之前尝试访问的页面
+  if (to.path === '/auth/login' && auth.has_token()) {
path: dashboard/src/router/index.ts
user: login: sourcery-ai[bot]
body: **suggestion:** 重定向逻辑可能无法处理查询参数或哈希片段。

此重定向会删除任何查询参数和哈希片段。要保留深度链接或查询数据，请将原始 URL 的搜索和哈希附加到重定向目标。

<details>
<summary>Original comment in English</summary>

**suggestion:** Redirection logic may not handle query parameters or hash fragments.

This redirect drops any query parameters and hash fragments. To preserve deep links or query data, append the original URL’s search and hash to the redirect target.

</details>
created_at: 2025-05-17T10:22:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-17T10:14:51Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

```reviewlanguage
## 审查者指南

此 PR 通过增强 AuthLogin 表单的加载状态和更新的 UI/样式来全面改进登录流程，并在登录页面组件和路由守卫中添加基于会话的重定向，以防止已登录用户看到登录屏幕，并使用动画、背景渐变和可重用的 Logo 组件来改进登录页面布局。

#### 顺序图：登录页面加载和自动重定向

```mermaid
sequenceDiagram
    actor User
    participant Browser
    participant LoginPageVue as "LoginPage.vue"
    participant AuthStore as "AuthStore"
    participant Router as "Vue Router"

    User->>Browser: Navigates to /auth/login
    Browser->>LoginPageVue: Mounts component
    activate LoginPageVue
    LoginPageVue->>LoginPageVue: onMounted() lifecycle hook
    LoginPageVue->>AuthStore: authStore.has_token()
    activate AuthStore
    AuthStore-->>LoginPageVue: boolean (tokenExists)
    deactivate AuthStore
    alt User is already logged in (tokenExists is true)
        LoginPageVue->>Router: router.push(authStore.returnUrl || '/')
        activate Router
        Router-->>Browser: Redirects to dashboard or returnUrl
        deactivate Router
    else User is not logged in (tokenExists is false)
        LoginPageVue->>LoginPageVue: cardVisible.value = true
        Note right of LoginPageVue: Login form becomes visible with animation
    end
    deactivate LoginPageVue
```

#### 顺序图：已登录用户的路由守卫自动重定向

```mermaid
sequenceDiagram
    actor User
    participant Browser
    participant RouterGuard as "router.beforeEach"
    participant AuthStore as "AuthStore"

    User->>Browser: Attempts to navigate to /auth/login
    Browser->>RouterGuard: Triggers beforeEach guard for /auth/login route
    activate RouterGuard
    RouterGuard->>AuthStore: auth.has_token()
    activate AuthStore
    AuthStore-->>RouterGuard: boolean (tokenExists)
    deactivate AuthStore
    alt User is already logged in (tokenExists is true) AND attempting to access /auth/login
        RouterGuard->>RouterGuard: next(auth.returnUrl || '/')
        Note right of RouterGuard: Redirects to dashboard or original intended URL
    else User is not logged in OR not accessing /auth/login
        RouterGuard->>RouterGuard: next()
        Note right of RouterGuard: Allows navigation to proceed
    end
    deactivate RouterGuard
```

#### 类图：AuthLogin.vue 组件更新

```mermaid
classDiagram
  class AuthLoginVue {
    +username: Ref<String>
    +password: Ref<String>
    +show1: Ref<Boolean>
    +valid: Ref<Boolean>
    +loading: Ref<Boolean>
    +validate(values, setErrors): Promise<void>
  }
  note for AuthLoginVue "New data property 'loading: Ref<Boolean>' to manage loading state.
Method 'validate()' updated to set 'loading' to true during API call and false on completion/error.
Template updated to bind 'loading' state to VBtn and disable fields, and new styling classes applied."
```

#### 类图：LoginPage.vue 组件更新和交互

```mermaid
classDiagram
  class LoginPageVue {
    +cardVisible: Ref<Boolean>
    +router: Router
    +authStore: AuthStore
    +onMounted(): void
  }
  class AuthLoginVue
  class LogoVue
  class AuthStore
  class Router

  LoginPageVue --> AuthLoginVue : uses
  LoginPageVue --> LogoVue : uses
  LoginPageVue --> AuthStore : uses (via useAuthStore())
  LoginPageVue --> Router : uses (via useRouter())

  note for LoginPageVue "New data properties: 'cardVisible', 'router', 'authStore'.
New lifecycle hook 'onMounted()' to check auth status using 'authStore.has_token()' and redirect using 'router.push()' if already logged in.
Template significantly revamped: new layout, background, animated card visibility ('cardVisible'), and incorporates the new 'Logo.vue' component."
```

#### 类图：路由导航守卫更新

```mermaid
classDiagram
  class VueRouterInstance {
    +beforeEach(guardFunction: Function): void
  }
  note for VueRouterInstance "The router.beforeEach() navigation guard in 'router/index.ts' has been modified.
New logic added: if the route 'to.path' is '/auth/login' AND 'auth.has_token()' is true, the user is redirected to 'auth.returnUrl' or '/' using 'next()'."
```

### 文件级别更改

| 变更 | 详情 | 文件 |
| ------ | ------- | ----- |
| 重构 AuthLogin.vue 以通过加载状态和样式增强 UX | <ul><li>添加了加载响应式标志，并在身份验证期间禁用了输入/按钮</li><li>使用前置图标和新类重构了输入字段</li><li>更新了带有加载微调器、高程和悬停效果的提交按钮标记</li><li>改进了错误警报样式、变体和位置</li></ul> | `dashboard/src/views/authentication/authForms/AuthLogin.vue` |
| 在 LoginPage.vue 中实现基于会话的重定向 | <ul><li>在组件挂载时检查 authStore.has_token()</li><li>如果已通过身份验证，则重定向到 returnUrl 或 '/'</li><li>延迟 cardVisible 切换以进行进入动画</li></ul> | `dashboard/src/views/authentication/auth/LoginPage.vue` |
| 重新设计登录页面布局和背景视觉效果 | <ul><li>添加了全屏渐变背景和旋转径向叠加</li><li>使用不透明度/平移动画和盒阴影设置了登录卡片的样式</li><li>插入了自定义分隔符和包装器类以实现有凝聚力的间距</li></ul> | `dashboard/src/views/authentication/auth/LoginPage.vue` |
| 阻止已通过身份验证的用户访问登录路由 | <ul><li>扩展 router.beforeEach 以在 has_token() 时检测 '/auth/login' 路径</li><li>将已登录用户重定向到 returnUrl 或根目录</li></ul> | `dashboard/src/router/index.ts` |
| 将标题提取到可重用的 Logo 组件中 | <ul><li>创建了带有图标和文本标记的 Logo.vue</li><li>在徽标图像上应用了范围悬停缩放效果</li></ul> | `dashboard/src/components/shared/Logo.vue` |

---

<details>
<summary>提示和命令</summary>

#### 与 Sourcery 互动

- **触发新的审查：** 在 pull request 上评论 `@sourcery-ai review`。
- **继续讨论：** 直接回复 Sourcery 的审查评论。
- **从审查评论生成 GitHub issue：** 通过回复审查评论，要求 Sourcery 从审查评论创建一个 issue。您也可以回复审查评论并使用 `@sourcery-ai issue` 从中创建一个 issue。
- **生成 pull request 标题：** 在 pull request 标题中的任何位置写入 `@sourcery-ai` 以随时生成标题。您也可以在 pull request 上评论 `@sourcery-ai title` 以随时（重新）生成标题。
- **生成 pull request 摘要：** 在 pull request 正文中的任何位置写入 `@sourcery-ai summary` 以随时在您想要的位置生成 PR 摘要。您也可以在 pull request 上评论 `@sourcery-ai summary` 以随时（重新）生成摘要。
- **生成审查者指南：** 在 pull request 上评论 `@sourcery-ai guide` 以随时（重新）生成审查者指南。
- **解决所有 Sourcery 评论：** 在 pull request 上评论 `@sourcery-ai resolve` 以解决所有 Sourcery 评论。如果您已经解决了所有评论并且不想再看到它们，这将非常有用。
- **驳回所有 Sourcery 审查：** 在 pull request 上评论 `@sourcery-ai dismiss` 以驳回所有现有的 Sourcery 审查。如果您想从新的审查开始，这将特别有用 - 不要忘记评论 `@sourcery-ai review` 以触发新的审查！

#### 自定义您的体验

访问您的 [dashboard](https://app.sourcery.ai) 以：
- 启用或禁用审查功能，例如 Sourcery 生成的 pull request 摘要、审查者指南等。
- 更改审查语言。
- 添加、删除或编辑自定义审查说明。
- 调整其他审查设置。

#### 获得帮助

- [联系我们的支持团队](mailto:support@sourcery.ai) 提出问题或反馈。
- 访问我们的 [documentation](https://docs.sourcery.ai) 获取详细指南和信息。
- 通过在 [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) 或 [GitHub](https://github.com/sourcery-ai) 上关注我们，与 Sourcery 团队保持联系。

</details>
```

<details>
<summary>Original review guide in English</summary>

## Reviewer's Guide

This PR overhauls the login flow by enhancing the AuthLogin form with loading state and updated UI/styling, adding session-based redirects to both the login page component and router guard to prevent logged-in users from seeing the login screen, and revamps the login page layout with animations, background gradients, and a reusable Logo component.

#### Sequence Diagram: Login Page Load and Auto-Redirection

```mermaid
sequenceDiagram
    actor User
    participant Browser
    participant LoginPageVue as "LoginPage.vue"
    participant AuthStore as "AuthStore"
    participant Router as "Vue Router"

    User->>Browser: Navigates to /auth/login
    Browser->>LoginPageVue: Mounts component
    activate LoginPageVue
    LoginPageVue->>LoginPageVue: onMounted() lifecycle hook
    LoginPageVue->>AuthStore: authStore.has_token()
    activate AuthStore
    AuthStore-->>LoginPageVue: boolean (tokenExists)
    deactivate AuthStore
    alt User is already logged in (tokenExists is true)
        LoginPageVue->>Router: router.push(authStore.returnUrl || '/')
        activate Router
        Router-->>Browser: Redirects to dashboard or returnUrl
        deactivate Router
    else User is not logged in (tokenExists is false)
        LoginPageVue->>LoginPageVue: cardVisible.value = true
        Note right of LoginPageVue: Login form becomes visible with animation
    end
    deactivate LoginPageVue
```

#### Sequence Diagram: Router Guard Auto-Redirection for Logged-in Users

```mermaid
sequenceDiagram
    actor User
    participant Browser
    participant RouterGuard as "router.beforeEach"
    participant AuthStore as "AuthStore"

    User->>Browser: Attempts to navigate to /auth/login
    Browser->>RouterGuard: Triggers beforeEach guard for /auth/login route
    activate RouterGuard
    RouterGuard->>AuthStore: auth.has_token()
    activate AuthStore
    AuthStore-->>RouterGuard: boolean (tokenExists)
    deactivate AuthStore
    alt User is already logged in (tokenExists is true) AND attempting to access /auth/login
        RouterGuard->>RouterGuard: next(auth.returnUrl || '/')
        Note right of RouterGuard: Redirects to dashboard or original intended URL
    else User is not logged in OR not accessing /auth/login
        RouterGuard->>RouterGuard: next()
        Note right of RouterGuard: Allows navigation to proceed
    end
    deactivate RouterGuard
```

#### Class Diagram: AuthLogin.vue Component Updates

```mermaid
classDiagram
  class AuthLoginVue {
    +username: Ref<String>
    +password: Ref<String>
    +show1: Ref<Boolean>
    +valid: Ref<Boolean>
    +loading: Ref<Boolean>
    +validate(values, setErrors): Promise<void>
  }
  note for AuthLoginVue "New data property 'loading: Ref<Boolean>' to manage loading state.
Method 'validate()' updated to set 'loading' to true during API call and false on completion/error.
Template updated to bind 'loading' state to VBtn and disable fields, and new styling classes applied."
```

#### Class Diagram: LoginPage.vue Component Updates and Interactions

```mermaid
classDiagram
  class LoginPageVue {
    +cardVisible: Ref<Boolean>
    +router: Router
    +authStore: AuthStore
    +onMounted(): void
  }
  class AuthLoginVue
  class LogoVue
  class AuthStore
  class Router

  LoginPageVue --> AuthLoginVue : uses
  LoginPageVue --> LogoVue : uses
  LoginPageVue --> AuthStore : uses (via useAuthStore())
  LoginPageVue --> Router : uses (via useRouter())

  note for LoginPageVue "New data properties: 'cardVisible', 'router', 'authStore'.
New lifecycle hook 'onMounted()' to check auth status using 'authStore.has_token()' and redirect using 'router.push()' if already logged in.
Template significantly revamped: new layout, background, animated card visibility ('cardVisible'), and incorporates the new 'Logo.vue' component."
```

#### Class Diagram: Router Navigation Guard Update

```mermaid
classDiagram
  class VueRouterInstance {
    +beforeEach(guardFunction: Function): void
  }
  note for VueRouterInstance "The router.beforeEach() navigation guard in 'router/index.ts' has been modified.
New logic added: if the route 'to.path' is '/auth/login' AND 'auth.has_token()' is true, the user is redirected to 'auth.returnUrl' or '/' using 'next()'."
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Refactor AuthLogin.vue to enhance UX with loading state and styling | <ul><li>Added loading reactive flag and disabled inputs/button during authentication</li><li>Restructured input fields with prepend icons and new classes</li><li>Updated submit button markup with loading spinner, elevation, and hover effects</li><li>Improved error alert styling, variant, and placement</li></ul> | `dashboard/src/views/authentication/authForms/AuthLogin.vue` |
| Implement session-based redirect in LoginPage.vue | <ul><li>Check authStore.has_token() on component mount</li><li>Redirect to returnUrl or '/' if already authenticated</li><li>Delay cardVisible toggle for entry animation</li></ul> | `dashboard/src/views/authentication/auth/LoginPage.vue` |
| Redesign login page layout and background visuals | <ul><li>Added full-screen gradient background and rotating radial overlay</li><li>Styled login card with opacity/translate animations and box-shadow</li><li>Inserted custom divider and wrapper classes for cohesive spacing</li></ul> | `dashboard/src/views/authentication/auth/LoginPage.vue` |
| Prevent authenticated users from accessing login route | <ul><li>Extend router.beforeEach to detect '/auth/login' path when has_token()</li><li>Redirect logged-in users to returnUrl or root</li></ul> | `dashboard/src/router/index.ts` |
| Extract header into reusable Logo component | <ul><li>Created Logo.vue with icon and text markup</li><li>Applied scoped hover scaling effect on logo image</li></ul> | `dashboard/src/components/shared/Logo.vue` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: Soulter
created_at: 2025-05-17T13:36:43Z
body: 变好看了很多！感谢 PR❤
author_association: MEMBER