Prechádzať zdrojové kódy

zalo登录.找回密码

urbanu 1 mesiac pred
rodič
commit
8b29d53f0c

+ 23 - 0
index.html

@@ -10,6 +10,29 @@
 
 <body>
 <div id="app"></div>
+<script>
+  // 在 Vue 加载前处理 Zalo OAuth 回调参数
+  // 因为使用 hash 路由,当 Zalo 回调到 /login?code=xxx 时
+  // 页面会重定向到 /#/login,导致 query 参数丢失
+  // 所以需要在页面加载时立即保存这些参数
+  (function() {
+    var urlParams = new URLSearchParams(window.location.search);
+    var code = urlParams.get('code');
+    var state = urlParams.get('state');
+
+    if (code) {
+      // 保存 Zalo 回调的 code 到 sessionStorage
+      sessionStorage.setItem('zalo_oauth_code', code);
+      if (state) {
+        sessionStorage.setItem('zalo_oauth_state', state);
+      }
+      console.log('Zalo OAuth code saved:', code);
+
+      // 重定向到 hash 路由的登录页,去掉 URL 中的参数
+      window.location.href = window.location.origin + '/#/login';
+    }
+  })();
+</script>
 <script type="module" src="/src/main.ts"></script>
 </body>
 

+ 2 - 1
src/locales/en.json

@@ -67,7 +67,8 @@
     "telegramLoginFailed": "Telegram login failed",
     "telegramNotConfigured": "Telegram login not configured",
     "zaloLoginFailed": "Zalo login failed",
-    "zaloNotConfigured": "Zalo login not configured"
+    "zaloNotConfigured": "Zalo login not configured",
+    "logoutConfirm": "Are you sure you want to log out?"
   },
   "home": {
     "checkIn": "Check in",

+ 2 - 1
src/locales/vi.json

@@ -67,7 +67,8 @@
     "telegramLoginFailed": "Đăng nhập Telegram thất bại",
     "telegramNotConfigured": "Đăng nhập Telegram chưa được cấu hình",
     "zaloLoginFailed": "Đăng nhập Zalo thất bại",
-    "zaloNotConfigured": "Đăng nhập Zalo chưa được cấu hình"
+    "zaloNotConfigured": "Đăng nhập Zalo chưa được cấu hình",
+    "logoutConfirm": "Bạn có chắc chắn muốn đăng xuất?"
   },
   "home": {
     "checkIn": "Điểm danh",

+ 2 - 1
src/locales/zh.json

@@ -67,7 +67,8 @@
     "telegramLoginFailed": "Telegram 登录失败",
     "telegramNotConfigured": "Telegram 登录未配置",
     "zaloLoginFailed": "Zalo 登录失败",
-    "zaloNotConfigured": "Zalo 登录未配置"
+    "zaloNotConfigured": "Zalo 登录未配置",
+    "logoutConfirm": "确定要退出登录吗?"
   },
   "home": {
     "checkIn": "签到",

+ 7 - 0
src/router/index.js

@@ -4,6 +4,7 @@ import Layout from "../components/Layout/index.vue";
 // 页面懒加载
 const loadLogin = () => import("../views/login/index.vue");
 const loadRegister = () => import("../views/register/index.vue");
+const loadForgotPassword = () => import("../views/forgot-password/index.vue");
 const loadHome = () => import("../views/home/index.vue");
 const loadHall = () => import("../views/hall/index.vue");
 const loadGameShow = () => import("../views/game-show/index.vue");
@@ -48,6 +49,12 @@ const router = createRouter({
       component: loadRegister,
       meta: { requiresAuth: false }
     },
+    {
+      path: "/forgot-password",
+      name: "forgot-password",
+      component: loadForgotPassword,
+      meta: { requiresAuth: false }
+    },
     // 主布局(带底部导航)
     {
       path: "/",

+ 10 - 0
src/styles/index.css

@@ -43,3 +43,13 @@
   font-weight: 600;
   font-style: normal;
 }
+
+/* Vant Dialog 按钮样式修复 - 深色主题下按钮文字看不清 */
+.van-dialog__confirm,
+.van-dialog__cancel {
+  color: #1989fa !important;
+}
+
+.van-dialog__cancel {
+  color: #666 !important;
+}

+ 286 - 0
src/views/forgot-password/index.vue

@@ -0,0 +1,286 @@
+<template>
+  <div class="forgot-password-page">
+    <!-- 顶部导航 -->
+    <van-nav-bar
+      :title="$t('auth.resetPassword')"
+      left-arrow
+      @click-left="goBack"
+    />
+
+    <!-- 表单 -->
+    <div class="form-section">
+      <div class="form-content">
+        <div class="input-group">
+          <van-field
+            v-model="form.phone"
+            type="tel"
+            :placeholder="$t('auth.pleaseInputPhone')"
+            clearable
+          >
+            <template #left-icon>
+              <div class="area-code" @click="showAreaCode = true">
+                +{{ areaCode }}
+                <van-icon name="arrow-down" />
+              </div>
+            </template>
+          </van-field>
+        </div>
+
+        <div class="input-group">
+          <van-field
+            v-model="form.code"
+            type="number"
+            :placeholder="$t('auth.pleaseInputVerifyCode')"
+            maxlength="6"
+          >
+            <template #button>
+              <van-button
+                size="small"
+                type="primary"
+                :disabled="countdown > 0"
+                @click="sendCode"
+              >
+                {{ countdown > 0 ? `${countdown}s` : $t('auth.getCode') }}
+              </van-button>
+            </template>
+          </van-field>
+        </div>
+
+        <div class="input-group">
+          <van-field
+            v-model="form.newPassword"
+            :type="showPassword ? 'text' : 'password'"
+            :placeholder="$t('auth.newPassword')"
+          >
+            <template #right-icon>
+              <van-icon
+                :name="showPassword ? 'eye-o' : 'closed-eye'"
+                @click="showPassword = !showPassword"
+              />
+            </template>
+          </van-field>
+        </div>
+
+        <div class="input-group">
+          <van-field
+            v-model="form.confirmPassword"
+            :type="showConfirmPassword ? 'text' : 'password'"
+            :placeholder="$t('auth.confirmPassword')"
+          >
+            <template #right-icon>
+              <van-icon
+                :name="showConfirmPassword ? 'eye-o' : 'closed-eye'"
+                @click="showConfirmPassword = !showConfirmPassword"
+              />
+            </template>
+          </van-field>
+        </div>
+
+        <van-button
+          class="submit-btn"
+          type="primary"
+          block
+          :loading="loading"
+          @click="handleSubmit"
+        >
+          {{ $t('auth.resetPassword') }}
+        </van-button>
+      </div>
+    </div>
+
+    <!-- 区号选择弹窗 -->
+    <van-popup v-model:show="showAreaCode" position="bottom" round>
+      <van-picker
+        :columns="areaCodes"
+        @confirm="onAreaCodeConfirm"
+        @cancel="showAreaCode = false"
+        show-toolbar
+      />
+    </van-popup>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue';
+import { useRouter } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+import { toast } from 'vue-sonner';
+import { requestSendCode, requestResetPassword } from '@/api/auth';
+
+const { t } = useI18n();
+const router = useRouter();
+
+const loading = ref(false);
+const showPassword = ref(false);
+const showConfirmPassword = ref(false);
+const showAreaCode = ref(false);
+const areaCode = ref('84');
+const countdown = ref(0);
+
+const form = reactive({
+  phone: '',
+  code: '',
+  newPassword: '',
+  confirmPassword: ''
+});
+
+const areaCodes = [
+  { text: '+84 Vietnam', value: '84' },
+  { text: '+62 Indonesia', value: '62' },
+  { text: '+86 China', value: '86' },
+  { text: '+1 USA', value: '1' }
+];
+
+const onAreaCodeConfirm = ({ selectedValues }) => {
+  areaCode.value = selectedValues[0];
+  showAreaCode.value = false;
+};
+
+const goBack = () => {
+  router.back();
+};
+
+// 发送验证码
+const sendCode = async () => {
+  if (!form.phone) {
+    toast.error(t('auth.pleaseInputPhone'));
+    return;
+  }
+
+  try {
+    const res = await requestSendCode({
+      type: 'phone',
+      account: form.phone,
+      areaCode: areaCode.value,
+      scene: 'reset'
+    });
+    if (res.code === 200) {
+      toast.success(t('common.success') || '验证码已发送');
+      countdown.value = 60;
+      const timer = setInterval(() => {
+        countdown.value--;
+        if (countdown.value <= 0) {
+          clearInterval(timer);
+        }
+      }, 1000);
+    }
+  } catch (error) {
+    console.error('Send code error:', error);
+  }
+};
+
+// 提交重置密码
+const handleSubmit = async () => {
+  if (!form.phone) {
+    toast.error(t('auth.pleaseInputPhone'));
+    return;
+  }
+  if (!form.code) {
+    toast.error(t('auth.pleaseInputVerifyCode'));
+    return;
+  }
+  if (!form.newPassword) {
+    toast.error(t('auth.pleaseInputPassword'));
+    return;
+  }
+  if (form.newPassword.length < 6) {
+    toast.error(t('auth.passwordTooShort') || '密码至少6位');
+    return;
+  }
+  if (form.newPassword !== form.confirmPassword) {
+    toast.error(t('auth.passwordNotMatch'));
+    return;
+  }
+
+  loading.value = true;
+  try {
+    const res = await requestResetPassword({
+      type: 'phone',
+      account: form.phone,
+      code: form.code,
+      newPassword: form.newPassword,
+      areaCode: areaCode.value
+    });
+    if (res.code === 200) {
+      toast.success(t('auth.resetSuccess') || '密码重置成功');
+      router.replace('/login');
+    }
+  } finally {
+    loading.value = false;
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.forgot-password-page {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
+
+  :deep(.van-nav-bar) {
+    background: transparent;
+
+    .van-nav-bar__title {
+      color: #fff;
+    }
+
+    .van-nav-bar__arrow {
+      color: #fff;
+    }
+  }
+}
+
+.form-section {
+  padding: 40px 24px;
+}
+
+.form-content {
+  .input-group {
+    margin-bottom: 16px;
+
+    :deep(.van-field) {
+      background: rgba(255, 255, 255, 0.1);
+      border-radius: 8px;
+      padding: 12px 16px;
+
+      .van-field__control {
+        color: #fff;
+
+        &::placeholder {
+          color: rgba(255, 255, 255, 0.4);
+        }
+      }
+
+      .van-icon {
+        color: rgba(255, 255, 255, 0.6);
+      }
+
+      .van-button--primary {
+        background: linear-gradient(135deg, #ffc300 0%, #ff9500 100%);
+        border: none;
+        color: #000;
+      }
+    }
+
+    .area-code {
+      display: flex;
+      align-items: center;
+      gap: 4px;
+      color: #fff;
+      padding-right: 12px;
+      border-right: 1px solid rgba(255, 255, 255, 0.2);
+      margin-right: 12px;
+      cursor: pointer;
+    }
+  }
+}
+
+.submit-btn {
+  margin-top: 32px;
+  height: 48px;
+  border-radius: 8px;
+  font-size: 16px;
+  font-weight: 500;
+  background: linear-gradient(135deg, #ffc300 0%, #ff9500 100%);
+  border: none;
+}
+</style>

+ 22 - 6
src/views/login/index.vue

@@ -478,6 +478,9 @@ const handleGoogleLogin = () => {
 
 // 组件挂载时初始化 OAuth
 onMounted(() => {
+  // 先检查 Zalo 回调(需要在 OAuth 配置加载前检查,因为 codeVerifier 在 sessionStorage 中)
+  initZaloLogin();
+  // 再初始化 OAuth 配置
   initOAuth();
 });
 
@@ -506,17 +509,25 @@ const generatePKCE = async () => {
 
 // 初始化 Zalo 登录
 const initZaloLogin = () => {
-  // 检查 URL 是否有 Zalo 回调参数
-  const urlParams = new URLSearchParams(window.location.search);
-  const code = urlParams.get('code');
+  // 从 sessionStorage 读取 Zalo OAuth 回调的 code
+  // (由 index.html 中的脚本在页面加载时保存)
+  const code = sessionStorage.getItem('zalo_oauth_code');
   const codeVerifier = sessionStorage.getItem('zalo_code_verifier');
 
+  console.log('Zalo initZaloLogin - code:', code ? 'exists' : 'null', 'codeVerifier:', codeVerifier ? 'exists' : 'null');
+
   if (code && codeVerifier) {
-    // 清除 URL 参数
-    window.history.replaceState({}, document.title, window.location.pathname + window.location.hash);
+    // 清除保存的参数
+    sessionStorage.removeItem('zalo_oauth_code');
+    sessionStorage.removeItem('zalo_oauth_state');
     sessionStorage.removeItem('zalo_code_verifier');
     // 使用 code 获取用户信息
     handleZaloCallback(code, codeVerifier);
+  } else if (code && !codeVerifier) {
+    // 有 code 但没有 codeVerifier,可能是用户直接访问了带 code 的 URL
+    console.warn('Zalo OAuth code found but no codeVerifier, clearing...');
+    sessionStorage.removeItem('zalo_oauth_code');
+    sessionStorage.removeItem('zalo_oauth_state');
   }
 };
 
@@ -564,9 +575,14 @@ const handleZaloLogin = async () => {
     sessionStorage.setItem('zalo_code_verifier', codeVerifier);
 
     // 构建 Zalo OAuth URL
-    const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
+    // 对于 hash 路由,使用不带 hash 的路径作为回调地址
+    // Zalo 回调后 URL 会变成: https://domain.com/login?code=xxx
+    // 然后我们在 initZaloLogin 中处理这个参数
+    const redirectUri = encodeURIComponent(window.location.origin + '/login');
     const state = generateRandomString(16);
 
+    console.log('Zalo login - redirectUri:', window.location.origin + '/login');
+
     const authUrl = `https://oauth.zaloapp.com/v4/permission?app_id=${zaloAppId.value}&redirect_uri=${redirectUri}&code_challenge=${codeChallenge}&state=${state}`;
 
     // 跳转到 Zalo 授权页面

+ 3 - 1
src/views/mine/index.vue

@@ -100,7 +100,9 @@ const handleLogout = async () => {
   try {
     await showConfirmDialog({
       title: t('auth.logout'),
-      message: '确定要退出登录吗?'
+      message: t('auth.logoutConfirm'),
+      confirmButtonText: t('common.confirm'),
+      cancelButtonText: t('common.cancel')
     });
     userStore.clearUserInfo();
     router.replace('/login');