完成微信扫码登录
This commit is contained in:
parent
c2c4485d74
commit
4c6b82789b
|
|
@ -8,7 +8,7 @@
|
|||
"name": "wechat-pc-frontend",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@batiao/batiao-sdk-vue-vite": "^1.0.357",
|
||||
"@batiao/batiao-sdk-vue-vite": "^1.0.386",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.6.8",
|
||||
"body-parser": "^2.3.0",
|
||||
|
|
@ -80,9 +80,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@batiao/batiao-sdk-vue-vite": {
|
||||
"version": "1.0.357",
|
||||
"resolved": "http://maven.batiao8.com/repository/npm-releases/@batiao/batiao-sdk-vue-vite/-/batiao-sdk-vue-vite-1.0.357.tgz",
|
||||
"integrity": "sha512-CGp16f+z7kLJSZocrj8wuttwUI4LuhAM3F9EE8+LManpsaGG5GPeDCfIfmYI9am97vnn4Fl1J0JGSSZiafjZyg==",
|
||||
"version": "1.0.395",
|
||||
"resolved": "http://maven.batiao8.com/repository/npm-releases/@batiao/batiao-sdk-vue-vite/-/batiao-sdk-vue-vite-1.0.395.tgz",
|
||||
"integrity": "sha512-i4tdbKk4RAcmOyL4HeXr4p2tgzaAcJG9DJc2UyzKteOWtsaJbG9jAGbuaq/XxfWqCNP1nGRakrltVMpQzRbWOQ==",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"quickjs-emscripten": "^0.32.0"
|
||||
|
|
|
|||
24
src/App.vue
24
src/App.vue
|
|
@ -17,24 +17,22 @@ import AppFooter from '@/components/common/AppFooter.vue'
|
|||
import PaymentDialog from '@/components/PaymentDialog.vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import api from '@/api'
|
||||
import { handleWxLoginCallback } from '@/utils/wxLogin'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const paymentDialog = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 1. 尝试处理微信登录回调
|
||||
const isWxLoginSuccess = await handleWxLoginCallback()
|
||||
if (isWxLoginSuccess) {
|
||||
// 微信登录成功后拉取用户信息
|
||||
userStore.fetchProfile().catch(() => { })
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 正常 token 验证
|
||||
if (localStorage.getItem('token') && !userStore.tokenChecked) {
|
||||
// 用静默验证:401时不跳转登录页(避免注册/登录页被拦截)
|
||||
// api.auth.profileSilent().then(res => {
|
||||
// userStore.username = res.username
|
||||
// userStore.isVip = res.is_vip
|
||||
// userStore.avatarUrl = res.avatar_url
|
||||
// userStore.membershipExpireAt = res.membership_expire_at
|
||||
// userStore.tokenChecked = true
|
||||
// userStore.tokenValid = true
|
||||
// }).catch(() => {
|
||||
// userStore.tokenChecked = true
|
||||
// userStore.tokenValid = false
|
||||
// })
|
||||
userStore.fetchProfile().catch(() => { })
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { get, postJson } from "@/utils/requestService";
|
||||
import appConfig from "@/utils/appConfig";
|
||||
import config from "@/config.json";
|
||||
|
||||
export default {
|
||||
/**
|
||||
|
|
@ -29,7 +30,21 @@ export default {
|
|||
},
|
||||
|
||||
/**
|
||||
* 3. 获取微信扫码登录二维码
|
||||
* 3. 微信开放平台 code 登录
|
||||
*/
|
||||
async loginByWxCode(code) {
|
||||
return await postJson(appConfig.baseURL, "/user/login", {
|
||||
type: "weixin",
|
||||
data: {
|
||||
// appid: config.wxAppId,
|
||||
code: code,
|
||||
code_type: "wx-web"
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 4. 获取微信扫码登录二维码 (可能废弃)
|
||||
*/
|
||||
async getLoginCode(data) {
|
||||
return await postJson(appConfig.baseURL, "/weixin/qrcode", data);
|
||||
|
|
|
|||
|
|
@ -29,18 +29,11 @@
|
|||
|
||||
<template v-else>
|
||||
<div class="qrcode-wrapper">
|
||||
<div class="qrcode-img">
|
||||
<img v-if="qrcode" :src="qrcode" style="width: 100%; height: 100%; object-fit: cover;" alt="微信扫码登录" />
|
||||
</div>
|
||||
<!-- <img class="qrcode-img" src="/src/public/login/qrcode.png" alt="微信登录二维码" /> -->
|
||||
<div class="qr-text">
|
||||
请使用微信扫描二维码登录<br>
|
||||
“装样大师” 网页版
|
||||
</div>
|
||||
<div id="wechat-login-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="agreement-row" @click="form.agreed = !form.agreed"
|
||||
<div v-if="loginType === 'phone'" class="agreement-row" @click="form.agreed = !form.agreed"
|
||||
:style="{ 'margin-top': loginType === 'phone' ? '54px' : '0px' }">
|
||||
<div class="check-icon" :class="{ 'is-checked': form.agreed }">
|
||||
<img v-if="form.agreed" class="empty-circle" src="/src/public/login/agreement.png">
|
||||
|
|
@ -49,7 +42,8 @@
|
|||
<span class="agreement-text">我已阅读并同意<a @click.stop>《用户协议》</a>和<a @click.stop>《隐私政策》</a></span>
|
||||
</div>
|
||||
|
||||
<button class="submit-btn" :class="{ 'is-loading': loading }" @click="handleLogin">登录</button>
|
||||
<button v-if="loginType === 'phone'" class="submit-btn" :class="{ 'is-loading': loading }"
|
||||
@click="handleLogin">登录</button>
|
||||
|
||||
<div class="other-methods">
|
||||
<div class="line"></div>
|
||||
|
|
@ -74,6 +68,7 @@ import api from '@/api'
|
|||
import authService from '@/api/authService'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import { initWechatLogin } from '@/utils/wxLogin'
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
const userStore = useUserStore()
|
||||
|
|
@ -83,9 +78,6 @@ const loading = ref(false)
|
|||
const sendingCode = ref(false)
|
||||
const countdown = ref(0)
|
||||
let timer = null
|
||||
let qrTimer = null
|
||||
const qrcode = ref('')
|
||||
const qrKey = ref('')
|
||||
const showError = ref(false)
|
||||
const loginType = ref('phone')
|
||||
|
||||
|
|
@ -101,50 +93,14 @@ function open() {
|
|||
// fetchQrCode()
|
||||
}
|
||||
|
||||
async function fetchQrCode() {
|
||||
try {
|
||||
// 确保请求二维码前本地有 Token (若缺失则自动拉取游客 Token)
|
||||
if (!localStorage.getItem('token')) {
|
||||
const configRes = await authService.getUserConfig()
|
||||
if (configRes && configRes.data && configRes.data.token) {
|
||||
localStorage.setItem('token', configRes.data.token)
|
||||
}
|
||||
}
|
||||
const res = await authService.getLoginCode()
|
||||
if (res && res.data) {
|
||||
qrKey.value = res.data.key
|
||||
qrcode.value = res.data.qrcode
|
||||
|
||||
if (qrTimer) clearInterval(qrTimer)
|
||||
qrTimer = setInterval(checkQrLogin, 2000)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取微信登录二维码失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkQrLogin() {
|
||||
if (loginType.value !== 'wechat' || !visible.value) return
|
||||
try {
|
||||
const res = await authService.askLoginCode({ key: qrKey.value })
|
||||
if (res && res.code === 0 && res.data && res.data.token) {
|
||||
localStorage.setItem('token', res.data.token)
|
||||
if (qrTimer) {
|
||||
clearInterval(qrTimer)
|
||||
qrTimer = null
|
||||
}
|
||||
visible.value = false
|
||||
emit('success')
|
||||
ElMessage.success('微信登录成功')
|
||||
window.location.reload()
|
||||
}
|
||||
} catch (err) {
|
||||
// 忽略轮询中的错误
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLoginType() {
|
||||
loginType.value = loginType.value === 'phone' ? 'wechat' : 'phone'
|
||||
if (loginType.value === 'wechat') {
|
||||
// 初始化微信开放平台登录
|
||||
setTimeout(() => {
|
||||
initWechatLogin('wechat-login-container')
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
|
|
@ -554,7 +510,15 @@ defineExpose({ open })
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
|
||||
|
||||
#wechat-login-container {
|
||||
height: 312px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.qrcode-img {
|
||||
width: 150px;
|
||||
|
|
|
|||
|
|
@ -24,5 +24,7 @@
|
|||
"ignoreCodeErrorList": ["GET&/api/weixin/qrcode","GET&/api/app/code", "GET&/api/order"],
|
||||
"hideConsole": true,
|
||||
"downloadSuccessCode":[200,201,202,203,204,205,206],
|
||||
"configIgnoreParams": ["inputvalue"]
|
||||
"configIgnoreParams": ["inputvalue"],
|
||||
"wxAppId": "wx099ee8f75a70b571",
|
||||
"wxRedirectUri": "https://dashi666.com/"
|
||||
}
|
||||
|
|
@ -39,6 +39,7 @@ const setheader = (dataType) => {
|
|||
header["x-channel"] = "official";
|
||||
header["x-version"] = "1.0.0";
|
||||
header["x-platform"] = "web";
|
||||
header["x-app-id"] = "10037"; // 添加 appid 以满足微信鉴权后端要求
|
||||
let deviceId = localStorage.getItem("device_id");
|
||||
if (!deviceId) {
|
||||
deviceId = appUtils.generateUUID();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import config from '@/config.json'
|
||||
|
||||
function loadWXSDK() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.WxLogin) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js'
|
||||
script.onload = resolve
|
||||
script.onerror = () => reject(new Error('微信SDK加载失败'))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
// 生成 UUID (简单实现)
|
||||
function generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = (Math.random() * 16) | 0,
|
||||
v = c == 'x' ? r : (r & 0x3) | 0x8
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
export async function initWechatLogin(containerId = 'wechat-login-container') {
|
||||
await loadWXSDK()
|
||||
|
||||
const stateUUID = generateUUID()
|
||||
// 将 state 存入 localStorage 用于防范 CSRF
|
||||
localStorage.setItem('wx_state', stateUUID)
|
||||
|
||||
// 动态获取当前的完整基础 URL 作为重定向地址(包括端口号),这样无论本地用什么端口,都能精准跳回来
|
||||
const WXLOGIN_APPID = config.wxAppId
|
||||
const WXLOGIN_REDIRECT_URI = config.wxRedirectUri
|
||||
|
||||
try {
|
||||
const customCss = '.impowerBox .title { opacity: 0 !important; margin-top:20px !important;}.impowerBox .info { margin-bottom:20px !important;} .impowerBox .qrcode { width: 250px !important; height: 250px !important; margin: 0 auto !important; } .impowerBox .qrcode img { width: 250px !important; height: 250px !important; }';
|
||||
|
||||
new window.WxLogin({
|
||||
self_redirect: true, // 本地调试时改为 true,让 iframe 自己跳转,避免跨域阻止
|
||||
id: containerId,
|
||||
appid: WXLOGIN_APPID,
|
||||
scope: 'snsapi_login',
|
||||
redirect_uri: encodeURIComponent(WXLOGIN_REDIRECT_URI),
|
||||
state: stateUUID,
|
||||
style: 'black',
|
||||
href: 'data:text/css;base64,' + btoa(customCss)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('初始化微信登录失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleWxLoginCallback() {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const code = urlParams.get('code')
|
||||
const state = urlParams.get('state')
|
||||
|
||||
if (code && state) {
|
||||
const savedState = localStorage.getItem('wx_state')
|
||||
if (savedState !== state) {
|
||||
console.warn('微信登录 state 校验失败,可能存在跨站请求伪造 (CSRF) 攻击风险。')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const authService = (await import('@/api/authService')).default
|
||||
const res = await authService.loginByWxCode(code)
|
||||
if (res && res.code === 0 && res.data && res.data.token) {
|
||||
localStorage.setItem('token', res.data.token)
|
||||
// 清理 URL 参数
|
||||
const url = new URL(window.location.href)
|
||||
url.search = ''
|
||||
window.history.replaceState({}, document.title, url.toString())
|
||||
return true
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('微信扫码登录失败:', err)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Loading…
Reference in New Issue