feat(login): switch login to google oauth flow

- add typed OauthTokenRequest interface for oauth token API
- remove unused AgreePopup component and all agreement-related logic
- rewrite login page to implement full Google OAuth2 login flow including state generation, callback validation and redirect handling
This commit is contained in:
DEV_DSW
2026-05-27 14:42:06 +08:00
parent b1731ed919
commit fb635ce5dd
3 changed files with 48 additions and 209 deletions

View File

@@ -1,7 +1,15 @@
import { request } from "../utils/request";
// 获取oauth token
export function oauthToken(args: any) {
export interface OauthTokenRequest {
clientId: string;
grant_type?: string;
openIdCode?: string[];
scope?: string;
[property: string]: any;
}
export function oauthToken(args: OauthTokenRequest) {
return request({
url: "/auth/oauth2/token",
method: "post",

View File

@@ -1,76 +0,0 @@
<template>
<van-popup ref="popup" position="center" v-model:show="show">
<div class="w-[327px] bg-white rounded-[12px]">
<!-- 弹窗头部 -->
<div class="relative px-[20px] pt-[20px]">
<div class="text-[18px] font-[600] text-[#333] text-center leading-[24px]">{{ title }}</div>
<div class="absolute top-[16px] right-[16px] w-[28px] h-[28px] flex items-center justify-center"
@click="handleClose">
<van-icon name="cross" size="24" color="#999" />
</div>
</div>
<!-- 弹窗内容 -->
<div class="p-[12px] max-h-[400px] overflow-y-auto">
<div class="content-span">
<vue3-markdown-it :source="agreement" :html="true" :linkify="true" />
</div>
</div>
<!-- 确认按钮 -->
<div class="p-[20px] flex justify-center items-center">
<div
class="w-[148px] h-[44px] bg-[#22a7ff] rounded-[50px] flex items-center justify-center text-white text-[16px] font-[500]"
@click="handleClose">我知道了</div>
</div>
</div>
</van-popup>
</template>
<script setup>
import { ref, watch, defineProps, defineEmits, defineExpose } from "vue";
import Vue3MarkdownIt from 'vue3-markdown-it';
// Props定义
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "温馨提示",
},
agreement: {
type: String,
default: "",
},
});
// Events定义
const emits = defineEmits(["agree", "close", "cancel"]);
// 响应式数据
const popup = ref(null);
const show = ref(false)
// 方法定义
const open = () => {
show.value = true;
};
const close = () => {
show.value = false
};
const handleClose = () => {
close();
emits("close");
};
// 暴露方法给父组件
defineExpose({
open,
close,
});
</script>

View File

@@ -1,149 +1,56 @@
<template>
<div class="h-screen flex flex-col items-center justify-center">
<!-- 卡通形象 -->
<img class="w-[200px] h-[200px]" :src="logo" />
<!-- 协议勾选 -->
<div class="w-[304px] mt-[80px]">
<van-checkbox v-model="isAgree">
<span class="text-[12px] text-ink-600">我已阅读并同意</span>
<span class="text-[12px] text-[#2D91FF] mx-[4px]" @click.stop="handleAgreeClick('service')">服务协议</span>
<span class="text-[12px] text-ink-600"></span>
<span class="text-[12px] text-[#2D91FF] mx-[4px]" @click.stop="handleAgreeClick('privacy')">隐私协议</span>
<span class="text-[12px] text-ink-600 ml-[30px]">授权与账号关联操作</span>
</van-checkbox>
</div>
<!-- 按钮区域 -->
<div class="w-[304px] mt-[20px]">
<button class="w-full h-[44px] bg-[#0ccd58] text-white rounded-[10px] text-center" @click="getPhoneNumber">
手机号快捷登录
<button class="w-full h-[44px] bg-[#0ccd58] text-white rounded-[10px] text-center" @click="loginWithGoogle">
Continue with Google
</button>
</div>
<AgreePopup ref="agreePopup" :agreement="computedAgreement" />
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import { getServiceAgreement, getPrivacyAgreement } from "@/api/login";
import { onLogin, goBack, onCheckPhoneLogin, onAppleLogin } from "@/hooks/useGoLogin";
import AgreePopup from "./components/AgreePopup/index.vue";
import { oauthToken } from '@/api/login'
// 是否需要微信手机号授权登录
const needWxAuthLogin = ref(false);
const isAgree = ref(false);
const visible = ref(false);
const serviceAgreement = ref("");
const privacyAgreement = ref("");
// 协议类型
const AgreeType = ref("service");
const logo = ref('');
const isAppleLogin = ref(false);
const generateState = () => {
const array = new Uint32Array(4);
window.crypto.getRandomValues(array);
// 同意隐私协议并获取手机号
const handleAgreeAndGetPhone = () => {
// 如果需要微信登录,直接返回
if (needWxAuthLogin.value && !isAppleLogin.value) {
return Array.from(array, dec => dec.toString(16).padStart(8, '0')).join('');
}
// 用Google登录
const loginWithGoogle = () => {
const client_id = '你的Google客户端ID';
const redirect_uri = encodeURIComponent('http://10.10.3.1:5174/login');
const scope = encodeURIComponent('openid email profile');
const response_type = 'code'; // 推荐 code 模式(授权码模式,更安全);如果不需要后端参与,可填 'token' (隐式模式)
const state = generateState();
sessionStorage.setItem('oauth_state', state);
const authUrl = `https://google.com{client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
window.location.href = authUrl;
};
onMounted(() => {
// 1. 从 URL 中获取谷歌传回的 state
const urlParams = new URLSearchParams(window.location.search);
const returnedState = urlParams.get('state');
// 2. 从本地缓存取出之前存的 state
const originalState = sessionStorage.getItem('oauth_state');
// 3. 严格比对
if (!returnedState || returnedState !== originalState) {
// 清除缓存并拒绝后续登录逻辑
sessionStorage.removeItem('oauth_state');
return;
}
if (!isAgree.value) {
showToast({
title: "请先同意服务协议和隐私协议",
icon: "none",
});
return;
}
if (isAppleLogin.value) {
onAppleLogin()
.then((res) => {
if (res) {
loginSuccess();
}
})
.catch(() => {
showToast({
title: "Apple ID登录失败",
icon: "none",
});
});
} else {
onCheckPhoneLogin()
.then((res) => {
if (!res) {
loginSuccess();
}
})
.catch(() => {
showToast({
title: "手机号登录失败",
icon: "none",
});
});
}
};
/// 获取授权后绑定手机号登录
const getPhoneNumber = (e) => {
if (!isAgree.value) {
showToast({
title: "请先同意服务协议和隐私协议",
icon: "none",
});
return;
}
onLogin(e)
.then(() => loginSuccess())
.catch(() => {
showToast({
title: "获取登录手机号失败",
icon: "none",
});
});
};
/// 登录成功返回上一页
const loginSuccess = () => {
showToast({
title: "登录成功",
icon: "success",
});
setTimeout(() => {
goBack();
}, 500);
};
// 处理同意协议点击事件
const handleAgreeClick = (type) => {
visible.value = true;
AgreeType.value = type;
};
// 计算协议类型
const computedAgreement = computed(() => {
if (AgreeType.value === "service") {
return serviceAgreement.value;
} else {
return privacyAgreement.value;
}
});
// 获取服务协议数据
const getServiceAgreementData = async () => {
const { data } = await getServiceAgreement();
serviceAgreement.value = data;
};
getServiceAgreementData();
// 获取隐私协议数据
const getPrivacyAgreementData = async () => {
const { data } = await getPrivacyAgreement();
privacyAgreement.value = data;
};
getPrivacyAgreementData();
// 4. 比对成功,清除使用过的 state继续执行后续拿 code 换 token 的业务
sessionStorage.removeItem('oauth_state');
const code = urlParams.get('code');
})
</script>