Jelajahi Sumber

登录页:谷歌一键登录、区号选号

urbanu 1 bulan lalu
induk
melakukan
a8cad1259a
4 mengubah file dengan 193 tambahan dan 119 penghapusan
  1. 20 0
      public/google-callback.html
  2. 4 1
      src/App.vue
  3. 119 74
      src/views/login/index.vue
  4. 50 44
      src/views/sign-in/index.vue

+ 20 - 0
public/google-callback.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Google Login</title>
+</head>
+<body>
+<script>
+  // 从 URL hash 中获取 id_token
+  const hash = window.location.hash.substring(1);
+  const params = new URLSearchParams(hash);
+  const idToken = params.get('id_token');
+
+  if (idToken && window.opener) {
+    // 发送 token 给主窗口
+    window.opener.postMessage({ type: 'google-login', idToken: idToken }, window.location.origin);
+  }
+  window.close();
+</script>
+</body>
+</html>

+ 4 - 1
src/App.vue

@@ -51,6 +51,7 @@ body {
   overscroll-behavior-y: none;
   height: 100%;
   background: #161617;
+  overflow: hidden;
 }
 
 #app {
@@ -58,7 +59,9 @@ body {
   max-width: 500px;
   margin: 0 auto;
   height: 100%;
-  overflow: hidden;
+  overflow-x: hidden;
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
   overscroll-behavior: contain;
   color: #ffffff;
 }

+ 119 - 74
src/views/login/index.vue

@@ -160,19 +160,18 @@
     </div>
 
     <!-- 区号选择弹窗 -->
-    <van-popup v-model:show="showAreaCode" position="bottom" round>
-      <van-picker
-        :columns="areaCodes"
-        @confirm="onAreaCodeConfirm"
-        @cancel="showAreaCode = false"
-        show-toolbar
-      />
-    </van-popup>
+    <van-action-sheet
+      v-model:show="showAreaCode"
+      :actions="areaCodeActions"
+      @select="onAreaCodeSelect"
+      cancel-text="Cancel"
+      close-on-click-action
+    />
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted, nextTick } from 'vue';
+import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue';
 import { useRouter } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import { toast } from 'vue-sonner';
@@ -212,8 +211,13 @@ const areaCodes = [
   { text: '+1 USA', value: '1' }
 ];
 
-const onAreaCodeConfirm = ({ selectedValues }) => {
-  areaCode.value = selectedValues[0];
+const areaCodeActions = areaCodes.map(item => ({
+  name: item.text,
+  value: item.value
+}));
+
+const onAreaCodeSelect = (action: { name: string; value: string }) => {
+  areaCode.value = action.value;
   showAreaCode.value = false;
 };
 
@@ -455,35 +459,71 @@ const handleGoogleLogin = () => {
     return;
   }
 
-  if (window.google) {
-    // 使用 Google One Tap 或弹窗登录
-    window.google.accounts.id.prompt((notification: any) => {
-      if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
-        // 如果 One Tap 不显示,使用传统弹窗方式
-        window.google.accounts.oauth2.initCodeClient({
-          client_id: googleClientId.value,
-          scope: 'email profile',
-          callback: (response: any) => {
-            if (response.code) {
-              // 这里可以用 code 换取 token,但我们使用 ID Token 方式更简单
-            }
-          }
-        });
-      }
+  // 使用 OAuth 2.0 隐式授权流程,通过弹窗方式
+  const redirectUri = window.location.origin + '/google-callback.html';
+  const scope = 'openid email profile';
+  const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleClientId.value}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=id_token&scope=${encodeURIComponent(scope)}&nonce=${Date.now()}`;
+
+  // 打开弹窗
+  const width = 500;
+  const height = 600;
+  const left = (screen.width - width) / 2;
+  const top = (screen.height - height) / 2;
+  const popup = window.open(authUrl, 'google-login', `width=${width},height=${height},left=${left},top=${top}`);
+
+  // 监听弹窗返回
+  const timer = setInterval(() => {
+    if (popup?.closed) {
+      clearInterval(timer);
+      googleLoading.value = false;
+    }
+  }, 500);
+
+  googleLoading.value = true;
+};
+
+// 监听 Google 弹窗回传的消息
+const onGoogleMessage = async (event: MessageEvent) => {
+  if (event.origin !== window.location.origin) return;
+  if (event.data?.type !== 'google-login' || !event.data?.idToken) return;
+
+  googleLoading.value = true;
+  try {
+    const payload = JSON.parse(atob(event.data.idToken.split('.')[1]));
+    const res = await requestOAuthLogin({
+      provider: 'google',
+      openId: payload.sub,
+      nickname: payload.name,
+      avatar: payload.picture,
+      email: payload.email
     });
-  } else {
-    toast.error('Google SDK 加载失败');
+    if (res.code === 200) {
+      localStorage.setItem('token', res.data.token);
+      userStore.setUserInfo(res.data.user);
+      toast.success(t('auth.loginSuccess'));
+      router.replace('/home');
+    }
+  } catch (error) {
+    console.error('Google login error:', error);
+    toast.error('Google 登录失败');
+  } finally {
+    googleLoading.value = false;
   }
 };
 
 // 组件挂载时初始化 OAuth
 onMounted(() => {
-  // 先检查 Zalo 回调(需要在 OAuth 配置加载前检查,因为 codeVerifier 在 sessionStorage 中)
+  window.addEventListener('message', onGoogleMessage);
+  // 先检查 Zalo 回调
   initZaloLogin();
   // 再初始化 OAuth 配置
   initOAuth();
 });
 
+onUnmounted(() => {
+  window.removeEventListener('message', onGoogleMessage);
+});
+
 // 生成随机字符串
 const generateRandomString = (length: number): string => {
   const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
@@ -607,62 +647,64 @@ const goRegister = () => {
 .login-page {
   min-height: 100vh;
   background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
-  padding: 60px 24px 40px;
+  padding: min(6vw, 30px) min(5.333vw, 25.6px) min(10vw, 48px);
+  max-width: 500px;
+  margin: 0 auto;
 }
 
 .logo-section {
   text-align: center;
-  margin-bottom: 40px;
+  margin-bottom: min(5.333vw, 25.6px);
 
   .logo {
-    width: 80px;
-    height: 80px;
-    margin: 0 auto 16px;
+    width: min(17.067vw, 80px);
+    height: min(17.067vw, 80px);
+    margin: 0 auto min(2.667vw, 12.8px);
     background: linear-gradient(135deg, #ffc300 0%, #ff9500 100%);
-    border-radius: 20px;
+    border-radius: min(4.267vw, 20.48px);
     display: flex;
     align-items: center;
     justify-content: center;
 
     .logo-img {
-      width: 60px;
-      height: 60px;
+      width: min(12.8vw, 60px);
+      height: min(12.8vw, 60px);
     }
   }
 
   .title {
-    font-size: 28px;
+    font-size: min(5.867vw, 28px);
     font-weight: bold;
     color: #fff;
-    margin-bottom: 8px;
+    margin-bottom: min(1.067vw, 5.12px);
   }
 
   .subtitle {
-    font-size: 14px;
+    font-size: min(3.733vw, 17.92px);
     color: rgba(255, 255, 255, 0.6);
   }
 }
 
 .form-section {
   background: rgba(255, 255, 255, 0.05);
-  border-radius: 16px;
-  padding: 24px;
+  border-radius: min(4.267vw, 20.48px);
+  padding: min(5.333vw, 25.6px);
 }
 
 .login-tabs {
   display: flex;
-  margin-bottom: 24px;
+  margin-bottom: min(5.333vw, 25.6px);
   background: rgba(255, 255, 255, 0.1);
-  border-radius: 8px;
-  padding: 4px;
+  border-radius: min(2.133vw, 10.24px);
+  padding: min(1.067vw, 5.12px);
 
   .tab-item {
     flex: 1;
     text-align: center;
-    padding: 10px 0;
+    padding: min(2.667vw, 12.8px) 0;
     color: rgba(255, 255, 255, 0.6);
-    font-size: 14px;
-    border-radius: 6px;
+    font-size: min(3.733vw, 17.92px);
+    border-radius: min(1.6vw, 7.68px);
     cursor: pointer;
     transition: all 0.3s;
 
@@ -676,15 +718,16 @@ const goRegister = () => {
 
 .form-content {
   .input-group {
-    margin-bottom: 16px;
+    margin-bottom: min(3.2vw, 15.36px);
 
     :deep(.van-field) {
       background: rgba(255, 255, 255, 0.1);
-      border-radius: 8px;
-      padding: 12px 16px;
+      border-radius: min(2.133vw, 10.24px);
+      padding: min(3.2vw, 15.36px) min(4.267vw, 20.48px);
 
       .van-field__control {
         color: #fff;
+        font-size: min(3.733vw, 17.92px);
 
         &::placeholder {
           color: rgba(255, 255, 255, 0.4);
@@ -693,17 +736,19 @@ const goRegister = () => {
 
       .van-icon {
         color: rgba(255, 255, 255, 0.6);
+        font-size: min(4.267vw, 20.48px);
       }
     }
 
     .area-code {
       display: flex;
       align-items: center;
-      gap: 4px;
+      gap: min(1.067vw, 5.12px);
       color: #fff;
-      padding-right: 12px;
+      font-size: min(3.733vw, 17.92px);
+      padding-right: min(3.2vw, 15.36px);
       border-right: 1px solid rgba(255, 255, 255, 0.2);
-      margin-right: 12px;
+      margin-right: min(3.2vw, 15.36px);
       cursor: pointer;
     }
   }
@@ -712,27 +757,27 @@ const goRegister = () => {
 .forgot-password {
   text-align: right;
   color: #ffc300;
-  font-size: 13px;
-  margin-bottom: 24px;
+  font-size: min(3.467vw, 16.64px);
+  margin-bottom: min(5.333vw, 25.6px);
   cursor: pointer;
 }
 
 .login-btn {
-  height: 48px;
-  border-radius: 8px;
-  font-size: 16px;
+  height: min(12.8vw, 61.44px);
+  border-radius: min(2.133vw, 10.24px);
+  font-size: min(4.267vw, 20.48px);
   font-weight: 500;
   background: linear-gradient(135deg, #ffc300 0%, #ff9500 100%);
   border: none;
 }
 
 .third-party-section {
-  margin-top: 32px;
+  margin-top: min(6.4vw, 30.72px);
 
   .divider {
     display: flex;
     align-items: center;
-    margin-bottom: 20px;
+    margin-bottom: min(4.267vw, 20.48px);
 
     &::before,
     &::after {
@@ -743,9 +788,9 @@ const goRegister = () => {
     }
 
     span {
-      padding: 0 16px;
+      padding: 0 min(4.267vw, 20.48px);
       color: rgba(255, 255, 255, 0.4);
-      font-size: 12px;
+      font-size: min(3.2vw, 15.36px);
     }
   }
 
@@ -753,14 +798,14 @@ const goRegister = () => {
     display: flex;
     align-items: center;
     justify-content: center;
-    gap: 10px;
-    height: 48px;
-    border-radius: 8px;
-    font-size: 15px;
+    gap: min(2.667vw, 12.8px);
+    height: min(12.8vw, 61.44px);
+    border-radius: min(2.133vw, 10.24px);
+    font-size: min(4vw, 19.2px);
     font-weight: 500;
     cursor: pointer;
     transition: all 0.3s;
-    margin-bottom: 12px;
+    margin-bottom: min(3.2vw, 15.36px);
 
     &.loading {
       pointer-events: none;
@@ -803,13 +848,13 @@ const goRegister = () => {
 
 .register-entry {
   text-align: center;
-  margin-top: 24px;
+  margin-top: min(5.333vw, 25.6px);
   color: rgba(255, 255, 255, 0.6);
-  font-size: 14px;
+  font-size: min(3.733vw, 17.92px);
 
   .link {
     color: #ffc300;
-    margin-left: 4px;
+    margin-left: min(1.067vw, 5.12px);
     cursor: pointer;
   }
 }
@@ -817,11 +862,11 @@ const goRegister = () => {
 .telegram-widget-wrapper {
   display: flex;
   justify-content: center;
-  margin-bottom: 12px;
-  min-height: 48px;
+  margin-bottom: min(3.2vw, 15.36px);
+  min-height: min(12.8vw, 61.44px);
 
   iframe {
-    border-radius: 8px !important;
+    border-radius: min(2.133vw, 10.24px) !important;
     width: 100% !important;
     max-width: 100% !important;
   }

+ 50 - 44
src/views/sign-in/index.vue

@@ -108,27 +108,32 @@ const rewardList = computed(() => {
   ];
 });
 
-// 判断某天是否已签到(基于连续天数在8天循环内的位置)
+// 判断某天是否已签到(基于累计天数在8天循环内的位置)
 const isSignedDay = (day: number) => {
-  const continuousDays = signInfo.value?.continuousDays || 0;
-  const cycleDay = continuousDays % 8 || (continuousDays > 0 ? 8 : 0);
-  return day <= cycleDay && signInfo.value?.todaySigned;
+  const totalDays = signInfo.value?.totalDays || 0;
+  const cycleDay = totalDays % 8 || (totalDays > 0 ? 8 : 0);
+  return day <= cycleDay;
 };
 
 // 判断是否是今天要签到的天数
 const isTodayDay = (day: number) => {
   if (signInfo.value?.todaySigned) return false;
-  const continuousDays = signInfo.value?.continuousDays || 0;
-  const nextDay = (continuousDays % 8) + 1;
+  const totalDays = signInfo.value?.totalDays || 0;
+  const nextDay = (totalDays % 8) + 1;
   return day === nextDay;
 };
 
 // 获取今日奖励
 const getTodayReward = () => {
-  const continuousDays = signInfo.value?.continuousDays || 0;
-  const nextDayIndex = continuousDays % 8;
-  const rewards = rewardList.value;
-  return rewards[nextDayIndex]?.reward || 0.10;
+  const totalDays = signInfo.value?.totalDays || 0;
+  if (signInfo.value?.todaySigned) {
+    // 已签到,显示今天的奖励
+    const currentDayIndex = ((totalDays - 1) % 8);
+    return rewardList.value[currentDayIndex]?.reward || 0.10;
+  }
+  // 未签到,显示下一天的奖励
+  const nextDayIndex = totalDays % 8;
+  return rewardList.value[nextDayIndex]?.reward || 0.10;
 };
 
 const getSignInfo = async () => {
@@ -175,20 +180,20 @@ onMounted(() => {
 .sign-in-page {
   min-height: 100vh;
   background: #121212;
-  padding-bottom: 80px;
+  padding-bottom: min(21.333vw, 102.4px);
 }
 
 .header {
   display: flex;
   align-items: center;
   justify-content: space-between;
-  padding: 16px;
+  padding: min(4.267vw, 20.48px);
   color: #fff;
-  font-size: 16px;
+  font-size: min(4.267vw, 20.48px);
   font-weight: 600;
 
   .van-icon {
-    font-size: 20px;
+    font-size: min(5.333vw, 25.6px);
   }
 }
 
@@ -196,30 +201,30 @@ onMounted(() => {
   display: flex;
   justify-content: center;
   align-items: center;
-  padding: 24px 16px;
+  padding: min(6.4vw, 30.72px) min(4.267vw, 20.48px);
 
   .stats-item {
     text-align: center;
-    padding: 0 32px;
+    padding: 0 min(8.533vw, 40.96px);
 
     .value {
       display: block;
-      font-size: 36px;
+      font-size: min(9.6vw, 46.08px);
       font-weight: bold;
       color: #ffc300;
     }
 
     .label {
       display: block;
-      font-size: 12px;
+      font-size: min(3.2vw, 15.36px);
       color: rgba(255, 255, 255, 0.6);
-      margin-top: 4px;
+      margin-top: min(1.067vw, 5.12px);
     }
   }
 
   .stats-divider {
     width: 1px;
-    height: 40px;
+    height: min(10.667vw, 51.2px);
     background: rgba(255, 255, 255, 0.2);
   }
 }
@@ -227,21 +232,21 @@ onMounted(() => {
 .sign-calendar {
   display: grid;
   grid-template-columns: repeat(4, 1fr);
-  gap: 10px;
-  padding: 0 16px;
+  gap: min(2.667vw, 12.8px);
+  padding: 0 min(4.267vw, 20.48px);
 
   .calendar-day {
     display: flex;
     flex-direction: column;
     align-items: center;
-    padding: 12px 4px;
+    padding: min(3.2vw, 15.36px) min(1.067vw, 5.12px);
     background: rgba(255, 255, 255, 0.03);
-    border-radius: 12px;
+    border-radius: min(3.2vw, 15.36px);
     border: 2px solid transparent;
 
     .day-icon {
-      width: 28px;
-      height: 28px;
+      width: min(7.467vw, 35.84px);
+      height: min(7.467vw, 35.84px);
       display: flex;
       align-items: center;
       justify-content: center;
@@ -249,21 +254,21 @@ onMounted(() => {
       background: rgba(255, 255, 255, 0.1);
 
       .van-icon {
-        font-size: 16px;
+        font-size: min(4.267vw, 20.48px);
         color: rgba(255, 255, 255, 0.5);
       }
     }
 
     .day-text {
-      font-size: 10px;
+      font-size: min(2.667vw, 12.8px);
       color: rgba(255, 255, 255, 0.5);
-      margin-top: 6px;
+      margin-top: min(1.6vw, 7.68px);
     }
 
     .day-reward {
-      font-size: 10px;
+      font-size: min(2.667vw, 12.8px);
       color: rgba(255, 255, 255, 0.4);
-      margin-top: 2px;
+      margin-top: min(0.533vw, 2.56px);
     }
 
     &.signed {
@@ -308,30 +313,31 @@ onMounted(() => {
   display: flex;
   justify-content: center;
   align-items: center;
-  gap: 12px;
-  padding: 24px 16px;
+  gap: min(3.2vw, 15.36px);
+  padding: min(6.4vw, 30.72px) min(4.267vw, 20.48px);
 
   .label {
-    font-size: 14px;
+    font-size: min(3.733vw, 17.92px);
     color: rgba(255, 255, 255, 0.7);
   }
 
   .value {
-    font-size: 24px;
+    font-size: min(6.4vw, 30.72px);
     font-weight: bold;
     color: #ffc300;
   }
 }
 
 .sign-action {
-  padding: 0 16px 20px;
+  padding: 0 min(4.267vw, 20.48px) min(5.333vw, 25.6px);
 
   .van-button--primary {
     background: linear-gradient(135deg, #ffc300 0%, #ff9500 100%);
     border: none;
     color: #000;
     font-weight: 600;
-    height: 48px;
+    height: min(12.8vw, 61.44px);
+    font-size: min(4.267vw, 20.48px);
   }
 
   .van-button--disabled {
@@ -341,23 +347,23 @@ onMounted(() => {
 }
 
 .sign-rules {
-  margin: 0 16px;
-  padding: 16px;
+  margin: 0 min(4.267vw, 20.48px);
+  padding: min(4.267vw, 20.48px);
   background: rgba(255, 255, 255, 0.03);
-  border-radius: 12px;
+  border-radius: min(3.2vw, 15.36px);
 
   .rules-title {
-    font-size: 14px;
+    font-size: min(3.733vw, 17.92px);
     font-weight: 600;
     color: #fff;
-    margin-bottom: 12px;
+    margin-bottom: min(3.2vw, 15.36px);
   }
 
   .rules-content {
     p {
-      font-size: 13px;
+      font-size: min(3.467vw, 16.64px);
       color: rgba(255, 255, 255, 0.5);
-      margin-bottom: 8px;
+      margin-bottom: min(2.133vw, 10.24px);
       line-height: 1.4;
 
       &:last-child {