فهرست منبع

Add email login functionality and enhance login page UI

- Implemented `loginWithEmail` method in `userApi` for test environment email login.
- Updated `useAuth` composable to include the new email login method.
- Enhanced `login.vue` to support email login with a modal for test email input, including error handling and loading states.
- Added a new SCSS mixin for converting pixel values to viewport width for responsive design.
0es 4 ماه پیش
والد
کامیت
4dfd7416ed
4فایلهای تغییر یافته به همراه238 افزوده شده و 4 حذف شده
  1. 9 0
      app/api/user.ts
  2. 7 0
      app/assets/css/mixins.scss
  3. 21 0
      app/composables/useAuth.ts
  4. 201 4
      app/pages/login.vue

+ 9 - 0
app/api/user.ts

@@ -50,6 +50,15 @@ export const userApi = {
     return http.post<UserLoginVO>('/user/login/google/enter', params)
   },
 
+  /**
+   * Test environment email login
+   * @param email - User email for test login
+   * @returns Login response with user info and token
+   */
+  loginWithEmail(email: string) {
+    return http.post<UserLoginVO>('/user/login/email/enter', { email })
+  },
+
   /**
    * User logout
    * @returns Empty response

+ 7 - 0
app/assets/css/mixins.scss

@@ -3,6 +3,13 @@
 // Design base width - 设计稿基准宽度
 $design-base-width: 375;
 
+// Convert px value from design to vw
+// $px: number in px from design draft, e.g. 16 for 16px
+// $base-width: design base width, default is 375
+@function px2vw($px, $base-width: $design-base-width) {
+  @return math.div($px, $base-width) * 100 * 1vw;
+}
+
 // Basic size mixin - 固定宽高
 @mixin size($width, $height: $width) {
   width: $width;

+ 21 - 0
app/composables/useAuth.ts

@@ -65,6 +65,26 @@ export const useAuth = () => {
     }
   }
 
+  const loginWithEmail = async (email: string) => {
+    try {
+      const result = await userApi.loginWithEmail(email)
+
+      if (result) {
+        authStore.setAuth(result.token)
+        authStore.setUserProfile(result.userProfile)
+        authStore.setWallet(result.wallet)
+
+        return result
+      }
+
+      return null
+    }
+    catch (error) {
+      console.error('Failed to login with email:', error)
+      return null
+    }
+  }
+
   const logout = () => {
     authStore.logout()
   }
@@ -82,6 +102,7 @@ export const useAuth = () => {
     refreshUser,
     refreshPlaymateInfo,
     login,
+    loginWithEmail,
     logout,
   }
 }

+ 201 - 4
app/pages/login.vue

@@ -10,7 +10,12 @@ definePageMeta({
 })
 
 const router = useRouter()
-const { login, isAuthenticated } = useAuth()
+const { login, loginWithEmail, isAuthenticated } = useAuth()
+
+const showTestLogin = ref(false)
+const testEmail = ref('')
+const testLoginLoading = ref(false)
+const testLoginError = ref('')
 
 onMounted(() => {
   watch(isAuthenticated, (newIsAuthenticated) => {
@@ -28,16 +33,54 @@ const handleLoginSuccess = async (response: CredentialResponse) => {
 const handleLoginError = () => {
   console.error('Login failed')
 }
+
+const openTestLogin = () => {
+  testEmail.value = ''
+  testLoginError.value = ''
+  showTestLogin.value = true
+}
+
+const closeTestLogin = () => {
+  if (testLoginLoading.value)
+    return
+
+  showTestLogin.value = false
+}
+
+const handleTestLogin = async () => {
+  if (!testEmail.value) {
+    testLoginError.value = 'Email is required'
+    return
+  }
+
+  testLoginError.value = ''
+  testLoginLoading.value = true
+  const result = await loginWithEmail(testEmail.value.trim())
+  testLoginLoading.value = false
+
+  if (!result) {
+    testLoginError.value = 'Login failed, please check email'
+    return
+  }
+
+  showTestLogin.value = false
+}
 </script>
 
 <template>
-  <div class="login-container min-h-screen flex flex-col justify-between items-center px-8">
+  <div class="login-container min-h-screen flex flex-col justify-end items-center px-8">
     <img
       src="~/assets/images/auth/logo.png"
       alt="logo"
       class="login-container__logo"
     >
-    <div class="login-container__btn">
+    <div class="login-container__btn flex flex-col items-center">
+      <div
+        class="login-container__btn-test"
+        @click="openTestLogin"
+      >
+        !!! TEST ENV LOGIN !!!
+      </div>
       <GoogleSignInButton
         size="large"
         @success="handleLoginSuccess"
@@ -53,18 +96,172 @@ const handleLoginError = () => {
         class="text-white"
       >privacy policy</a>
     </p>
+
+    <div
+      v-if="showTestLogin"
+      class="login-test-modal"
+    >
+      <div
+        class="login-test-modal__mask"
+        @click="closeTestLogin"
+      />
+      <div class="login-test-modal__content">
+        <h3 class="login-test-modal__title">
+          Test Email Login
+        </h3>
+        <input
+          v-model="testEmail"
+          type="email"
+          class="login-test-modal__input"
+          placeholder="Enter test email"
+        >
+        <p
+          v-if="testLoginError"
+          class="login-test-modal__error"
+        >
+          {{ testLoginError }}
+        </p>
+        <div class="login-test-modal__actions">
+          <button
+            type="button"
+            class="login-test-modal__btn login-test-modal__btn--cancel"
+            @click="closeTestLogin"
+          >
+            Cancel
+          </button>
+          <button
+            type="button"
+            class="login-test-modal__btn login-test-modal__btn--confirm"
+            :disabled="testLoginLoading"
+            @click="handleTestLogin"
+          >
+            <span v-if="!testLoginLoading">Login</span>
+            <span v-else>Logging in...</span>
+          </button>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 .login-container {
-  @include bg('~/assets/images/auth/bg.png', 100%);
+  @include bg('~/assets/images/auth/bg.png', 100vw auto);
+  position: relative;
 
   &__logo {
     width: 113px;
+    position: absolute;
+    top: px2vw(200);
+  }
+
+  &__btn {
+    gap: 16px;
+
+    &-test {
+      display: flex;
+      cursor: pointer;
+      height: 40px;
+      padding: 8px 24px;
+      justify-content: center;
+      align-items: center;
+      background: #1D2129;
+      border-radius: 100px;
+      color: #FFF;
+    }
   }
 }
 .login-footer {
   color: #C7C7C7;
 }
+
+.login-test-modal {
+  position: fixed;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+
+  &__mask {
+    position: absolute;
+    inset: 0;
+    background: rgba(0, 0, 0, 0.5);
+  }
+
+  &__content {
+    position: relative;
+    width: 80%;
+    max-width: 360px;
+    background: #111827;
+    border-radius: 16px;
+    padding: 20px 16px 16px;
+    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
+    color: #fff;
+  }
+
+  &__title {
+    margin-bottom: 12px;
+    font-size: 16px;
+    font-weight: 600;
+    text-align: center;
+  }
+
+  &__input {
+    width: 100%;
+    padding: 10px 12px;
+    border-radius: 8px;
+    border: 1px solid #4B5563;
+    background: #1F2937;
+    color: #F9FAFB;
+    font-size: 14px;
+    outline: none;
+
+    &::placeholder {
+      color: #6B7280;
+    }
+
+    &:focus {
+      border-color: #6366F1;
+    }
+  }
+
+  &__error {
+    margin-top: 8px;
+    font-size: 12px;
+    color: #FCA5A5;
+    text-align: left;
+  }
+
+  &__actions {
+    margin-top: 16px;
+    display: flex;
+    justify-content: flex-end;
+    gap: 8px;
+  }
+
+  &__btn {
+    min-width: 80px;
+    height: 32px;
+    border-radius: 999px;
+    font-size: 14px;
+    border: none;
+    cursor: pointer;
+
+    &--cancel {
+      background: #374151;
+      color: #E5E7EB;
+    }
+
+    &--confirm {
+      background: #6366F1;
+      color: #F9FAFB;
+
+      &:disabled {
+        opacity: 0.7;
+        cursor: default;
+      }
+    }
+  }
+}
 </style>