|
@@ -140,6 +140,9 @@
|
|
|
<van-loading v-if="googleLoading" size="16px" color="#fff" />
|
|
<van-loading v-if="googleLoading" size="16px" color="#fff" />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- Telegram 官方 Widget 按钮 -->
|
|
|
|
|
+ <div id="telegram-login-container" class="telegram-widget-wrapper" v-if="telegramBotName"></div>
|
|
|
|
|
+
|
|
|
<!-- Zalo 登录按钮 -->
|
|
<!-- Zalo 登录按钮 -->
|
|
|
<div class="oauth-btn zalo-btn" @click="handleZaloLogin">
|
|
<div class="oauth-btn zalo-btn" @click="handleZaloLogin">
|
|
|
<svg class="zalo-icon" viewBox="0 0 24 24" width="20" height="20">
|
|
<svg class="zalo-icon" viewBox="0 0 24 24" width="20" height="20">
|
|
@@ -169,7 +172,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive, onMounted } from 'vue';
|
|
|
|
|
|
|
+import { ref, reactive, onMounted, nextTick } from 'vue';
|
|
|
import { useRouter } from 'vue-router';
|
|
import { useRouter } from 'vue-router';
|
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useI18n } from 'vue-i18n';
|
|
|
import { toast } from 'vue-sonner';
|
|
import { toast } from 'vue-sonner';
|
|
@@ -181,6 +184,10 @@ const router = useRouter();
|
|
|
const userStore = useUserStore();
|
|
const userStore = useUserStore();
|
|
|
const googleLoading = ref(false);
|
|
const googleLoading = ref(false);
|
|
|
const googleClientId = ref('');
|
|
const googleClientId = ref('');
|
|
|
|
|
+const telegramLoading = ref(false);
|
|
|
|
|
+const telegramBotName = ref('');
|
|
|
|
|
+const zaloLoading = ref(false);
|
|
|
|
|
+const zaloAppId = ref('');
|
|
|
|
|
|
|
|
const loginType = ref<'phone' | 'email'>('phone');
|
|
const loginType = ref<'phone' | 'email'>('phone');
|
|
|
const showPassword = ref(false);
|
|
const showPassword = ref(false);
|
|
@@ -267,36 +274,146 @@ const handleEmailLogin = async () => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-// 获取OAuth配置并初始化 Google Sign-In
|
|
|
|
|
-const initGoogleSignIn = async () => {
|
|
|
|
|
|
|
+// 获取OAuth配置并初始化
|
|
|
|
|
+const initOAuth = async () => {
|
|
|
try {
|
|
try {
|
|
|
// 从后端获取OAuth配置
|
|
// 从后端获取OAuth配置
|
|
|
const configRes = await requestOAuthConfig();
|
|
const configRes = await requestOAuthConfig();
|
|
|
- if (configRes.code === 200 && configRes.data.googleClientId) {
|
|
|
|
|
- googleClientId.value = configRes.data.googleClientId;
|
|
|
|
|
-
|
|
|
|
|
- // 动态加载 Google Identity Services SDK
|
|
|
|
|
- const script = document.createElement('script');
|
|
|
|
|
- script.src = 'https://accounts.google.com/gsi/client';
|
|
|
|
|
- script.async = true;
|
|
|
|
|
- script.defer = true;
|
|
|
|
|
- script.onload = () => {
|
|
|
|
|
- if (window.google && googleClientId.value) {
|
|
|
|
|
- window.google.accounts.id.initialize({
|
|
|
|
|
- client_id: googleClientId.value,
|
|
|
|
|
- callback: handleGoogleCallback,
|
|
|
|
|
- auto_select: false,
|
|
|
|
|
- cancel_on_tap_outside: true
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
- document.head.appendChild(script);
|
|
|
|
|
|
|
+ if (configRes.code === 200) {
|
|
|
|
|
+ // 初始化 Google Sign-In
|
|
|
|
|
+ if (configRes.data.googleClientId) {
|
|
|
|
|
+ googleClientId.value = configRes.data.googleClientId;
|
|
|
|
|
+ const script = document.createElement('script');
|
|
|
|
|
+ script.src = 'https://accounts.google.com/gsi/client';
|
|
|
|
|
+ script.async = true;
|
|
|
|
|
+ script.defer = true;
|
|
|
|
|
+ script.onload = () => {
|
|
|
|
|
+ if (window.google && googleClientId.value) {
|
|
|
|
|
+ window.google.accounts.id.initialize({
|
|
|
|
|
+ client_id: googleClientId.value,
|
|
|
|
|
+ callback: handleGoogleCallback,
|
|
|
|
|
+ auto_select: false,
|
|
|
|
|
+ cancel_on_tap_outside: true
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ document.head.appendChild(script);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化 Telegram Login
|
|
|
|
|
+ if (configRes.data.telegramBotName) {
|
|
|
|
|
+ telegramBotName.value = configRes.data.telegramBotName;
|
|
|
|
|
+ // 等待 DOM 更新后再初始化
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ initTelegramLogin();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化 Zalo Login
|
|
|
|
|
+ if (configRes.data.zaloAppId) {
|
|
|
|
|
+ zaloAppId.value = configRes.data.zaloAppId;
|
|
|
|
|
+ initZaloLogin();
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.warn('Failed to load OAuth config:', error);
|
|
console.warn('Failed to load OAuth config:', error);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+// 初始化 Telegram 登录
|
|
|
|
|
+const initTelegramLogin = () => {
|
|
|
|
|
+ console.log('initTelegramLogin called, botName:', telegramBotName.value);
|
|
|
|
|
+
|
|
|
|
|
+ // 设置全局回调函数
|
|
|
|
|
+ (window as any).onTelegramAuth = (user: any) => {
|
|
|
|
|
+ console.log('Telegram auth callback:', user);
|
|
|
|
|
+ handleTelegramCallback(user);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 动态创建 Telegram Widget 脚本
|
|
|
|
|
+ const container = document.getElementById('telegram-login-container');
|
|
|
|
|
+ console.log('Container found:', container);
|
|
|
|
|
+
|
|
|
|
|
+ if (container) {
|
|
|
|
|
+ container.innerHTML = '';
|
|
|
|
|
+ const script = document.createElement('script');
|
|
|
|
|
+ script.async = true;
|
|
|
|
|
+ script.src = 'https://telegram.org/js/telegram-widget.js?22';
|
|
|
|
|
+ script.setAttribute('data-telegram-login', telegramBotName.value);
|
|
|
|
|
+ script.setAttribute('data-size', 'large');
|
|
|
|
|
+ script.setAttribute('data-radius', '8');
|
|
|
|
|
+ script.setAttribute('data-onauth', 'onTelegramAuth(user)');
|
|
|
|
|
+ script.setAttribute('data-request-access', 'write');
|
|
|
|
|
+ script.onload = () => console.log('Telegram widget script loaded');
|
|
|
|
|
+ script.onerror = (e) => console.error('Telegram widget script error:', e);
|
|
|
|
|
+ container.appendChild(script);
|
|
|
|
|
+ console.log('Script appended to container');
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// Telegram 登录回调
|
|
|
|
|
+const handleTelegramCallback = async (user: any) => {
|
|
|
|
|
+ if (!user || !user.id) {
|
|
|
|
|
+ toast.error(t('auth.telegramLoginFailed') || 'Telegram 登录失败');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ telegramLoading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await requestOAuthLogin({
|
|
|
|
|
+ provider: 'telegram',
|
|
|
|
|
+ openId: String(user.id),
|
|
|
|
|
+ nickname: user.first_name + (user.last_name ? ' ' + user.last_name : ''),
|
|
|
|
|
+ avatar: user.photo_url || ''
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ 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('Telegram login error:', error);
|
|
|
|
|
+ toast.error(t('auth.telegramLoginFailed') || 'Telegram 登录失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ telegramLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 手动触发 Telegram 登录 - 点击隐藏的 Widget 按钮
|
|
|
|
|
+const handleTelegramLogin = () => {
|
|
|
|
|
+ if (!telegramBotName.value) {
|
|
|
|
|
+ toast.error(t('auth.telegramNotConfigured') || 'Telegram 登录未配置');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 查找并点击 Telegram Widget 生成的按钮
|
|
|
|
|
+ const container = document.getElementById('telegram-login-container');
|
|
|
|
|
+ const widgetBtn = container?.querySelector('iframe');
|
|
|
|
|
+
|
|
|
|
|
+ if (widgetBtn) {
|
|
|
|
|
+ // Widget 已加载,模拟点击
|
|
|
|
|
+ widgetBtn.style.position = 'fixed';
|
|
|
|
|
+ widgetBtn.style.top = '50%';
|
|
|
|
|
+ widgetBtn.style.left = '50%';
|
|
|
|
|
+ widgetBtn.style.zIndex = '9999';
|
|
|
|
|
+ widgetBtn.click();
|
|
|
|
|
+
|
|
|
|
|
+ // 稍后隐藏回去
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ widgetBtn.style.position = '';
|
|
|
|
|
+ widgetBtn.style.top = '';
|
|
|
|
|
+ widgetBtn.style.left = '';
|
|
|
|
|
+ widgetBtn.style.zIndex = '';
|
|
|
|
|
+ }, 100);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Widget 未加载,重新初始化
|
|
|
|
|
+ initTelegramLogin();
|
|
|
|
|
+ toast.info('请稍后再试');
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
// Google 登录回调处理
|
|
// Google 登录回调处理
|
|
|
const handleGoogleCallback = async (response: any) => {
|
|
const handleGoogleCallback = async (response: any) => {
|
|
|
if (!response.credential) {
|
|
if (!response.credential) {
|
|
@@ -359,14 +476,106 @@ const handleGoogleLogin = () => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-// 组件挂载时初始化 Google Sign-In
|
|
|
|
|
|
|
+// 组件挂载时初始化 OAuth
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
- initGoogleSignIn();
|
|
|
|
|
|
|
+ initOAuth();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-const handleZaloLogin = () => {
|
|
|
|
|
- // TODO: Zalo OAuth登录
|
|
|
|
|
- toast.info('Zalo Login - Coming Soon');
|
|
|
|
|
|
|
+// 生成随机字符串
|
|
|
|
|
+const generateRandomString = (length: number): string => {
|
|
|
|
|
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
|
|
|
+ let result = '';
|
|
|
|
|
+ for (let i = 0; i < length; i++) {
|
|
|
|
|
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 生成 PKCE code verifier 和 code challenge
|
|
|
|
|
+const generatePKCE = async () => {
|
|
|
|
|
+ const codeVerifier = generateRandomString(43);
|
|
|
|
|
+ const encoder = new TextEncoder();
|
|
|
|
|
+ const data = encoder.encode(codeVerifier);
|
|
|
|
|
+ const digest = await crypto.subtle.digest('SHA-256', data);
|
|
|
|
|
+ const base64 = btoa(String.fromCharCode(...new Uint8Array(digest)))
|
|
|
|
|
+ .replace(/\+/g, '-')
|
|
|
|
|
+ .replace(/\//g, '_')
|
|
|
|
|
+ .replace(/=+$/, '');
|
|
|
|
|
+ return { codeVerifier, codeChallenge: base64 };
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 初始化 Zalo 登录
|
|
|
|
|
+const initZaloLogin = () => {
|
|
|
|
|
+ // 检查 URL 是否有 Zalo 回调参数
|
|
|
|
|
+ const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
+ const code = urlParams.get('code');
|
|
|
|
|
+ const codeVerifier = sessionStorage.getItem('zalo_code_verifier');
|
|
|
|
|
+
|
|
|
|
|
+ if (code && codeVerifier) {
|
|
|
|
|
+ // 清除 URL 参数
|
|
|
|
|
+ window.history.replaceState({}, document.title, window.location.pathname + window.location.hash);
|
|
|
|
|
+ sessionStorage.removeItem('zalo_code_verifier');
|
|
|
|
|
+ // 使用 code 获取用户信息
|
|
|
|
|
+ handleZaloCallback(code, codeVerifier);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// Zalo 登录回调处理
|
|
|
|
|
+const handleZaloCallback = async (code: string, codeVerifier: string) => {
|
|
|
|
|
+ zaloLoading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 调用后端接口,让后端用 code 换取 access_token 和用户信息
|
|
|
|
|
+ const res = await requestOAuthLogin({
|
|
|
|
|
+ provider: 'zalo',
|
|
|
|
|
+ openId: code, // 先传 code,后端会处理
|
|
|
|
|
+ nickname: '',
|
|
|
|
|
+ avatar: '',
|
|
|
|
|
+ extra: codeVerifier // 传递 code_verifier 给后端
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ localStorage.setItem('token', res.data.token);
|
|
|
|
|
+ userStore.setUserInfo(res.data.user);
|
|
|
|
|
+ toast.success(t('auth.loginSuccess'));
|
|
|
|
|
+ router.replace('/home');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ toast.error(res.msg || t('auth.zaloLoginFailed') || 'Zalo 登录失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Zalo login error:', error);
|
|
|
|
|
+ toast.error(t('auth.zaloLoginFailed') || 'Zalo 登录失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ zaloLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 触发 Zalo 登录
|
|
|
|
|
+const handleZaloLogin = async () => {
|
|
|
|
|
+ if (!zaloAppId.value) {
|
|
|
|
|
+ toast.error(t('auth.zaloNotConfigured') || 'Zalo 登录未配置');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ zaloLoading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 生成 PKCE
|
|
|
|
|
+ const { codeVerifier, codeChallenge } = await generatePKCE();
|
|
|
|
|
+ // 保存 code_verifier 到 sessionStorage
|
|
|
|
|
+ sessionStorage.setItem('zalo_code_verifier', codeVerifier);
|
|
|
|
|
+
|
|
|
|
|
+ // 构建 Zalo OAuth URL
|
|
|
|
|
+ const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
|
|
|
|
|
+ const state = generateRandomString(16);
|
|
|
|
|
+
|
|
|
|
|
+ const authUrl = `https://oauth.zaloapp.com/v4/permission?app_id=${zaloAppId.value}&redirect_uri=${redirectUri}&code_challenge=${codeChallenge}&state=${state}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 跳转到 Zalo 授权页面
|
|
|
|
|
+ window.location.href = authUrl;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Zalo login error:', error);
|
|
|
|
|
+ toast.error('Zalo 登录失败');
|
|
|
|
|
+ zaloLoading.value = false;
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const goForgotPassword = () => {
|
|
const goForgotPassword = () => {
|
|
@@ -542,7 +751,7 @@ const goRegister = () => {
|
|
|
opacity: 0.7;
|
|
opacity: 0.7;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- .google-icon, .zalo-icon {
|
|
|
|
|
|
|
+ .google-icon, .zalo-icon, .telegram-icon {
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -564,6 +773,15 @@ const goRegister = () => {
|
|
|
background: #0055cc;
|
|
background: #0055cc;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ &.telegram-btn {
|
|
|
|
|
+ background: #0088cc;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+
|
|
|
|
|
+ &:active {
|
|
|
|
|
+ background: #0077b5;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -579,4 +797,17 @@ const goRegister = () => {
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+.telegram-widget-wrapper {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ min-height: 48px;
|
|
|
|
|
+
|
|
|
|
|
+ iframe {
|
|
|
|
|
+ border-radius: 8px !important;
|
|
|
|
|
+ width: 100% !important;
|
|
|
|
|
+ max-width: 100% !important;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|