完成微信扫码登录
This commit is contained in:
parent
c2c4485d74
commit
4c6b82789b
|
|
@ -8,7 +8,7 @@
|
||||||
"name": "wechat-pc-frontend",
|
"name": "wechat-pc-frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@batiao/batiao-sdk-vue-vite": "^1.0.357",
|
"@batiao/batiao-sdk-vue-vite": "^1.0.386",
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"body-parser": "^2.3.0",
|
"body-parser": "^2.3.0",
|
||||||
|
|
@ -80,9 +80,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@batiao/batiao-sdk-vue-vite": {
|
"node_modules/@batiao/batiao-sdk-vue-vite": {
|
||||||
"version": "1.0.357",
|
"version": "1.0.395",
|
||||||
"resolved": "http://maven.batiao8.com/repository/npm-releases/@batiao/batiao-sdk-vue-vite/-/batiao-sdk-vue-vite-1.0.357.tgz",
|
"resolved": "http://maven.batiao8.com/repository/npm-releases/@batiao/batiao-sdk-vue-vite/-/batiao-sdk-vue-vite-1.0.395.tgz",
|
||||||
"integrity": "sha512-CGp16f+z7kLJSZocrj8wuttwUI4LuhAM3F9EE8+LManpsaGG5GPeDCfIfmYI9am97vnn4Fl1J0JGSSZiafjZyg==",
|
"integrity": "sha512-i4tdbKk4RAcmOyL4HeXr4p2tgzaAcJG9DJc2UyzKteOWtsaJbG9jAGbuaq/XxfWqCNP1nGRakrltVMpQzRbWOQ==",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"quickjs-emscripten": "^0.32.0"
|
"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 PaymentDialog from '@/components/PaymentDialog.vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
|
import { handleWxLoginCallback } from '@/utils/wxLogin'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const paymentDialog = ref(null)
|
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) {
|
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(() => { })
|
userStore.fetchProfile().catch(() => { })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { get, postJson } from "@/utils/requestService";
|
import { get, postJson } from "@/utils/requestService";
|
||||||
import appConfig from "@/utils/appConfig";
|
import appConfig from "@/utils/appConfig";
|
||||||
|
import config from "@/config.json";
|
||||||
|
|
||||||
export default {
|
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) {
|
async getLoginCode(data) {
|
||||||
return await postJson(appConfig.baseURL, "/weixin/qrcode", data);
|
return await postJson(appConfig.baseURL, "/weixin/qrcode", data);
|
||||||
|
|
|
||||||
|
|
@ -29,18 +29,11 @@
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="qrcode-wrapper">
|
<div class="qrcode-wrapper">
|
||||||
<div class="qrcode-img">
|
<div id="wechat-login-container"></div>
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</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' }">
|
:style="{ 'margin-top': loginType === 'phone' ? '54px' : '0px' }">
|
||||||
<div class="check-icon" :class="{ 'is-checked': form.agreed }">
|
<div class="check-icon" :class="{ 'is-checked': form.agreed }">
|
||||||
<img v-if="form.agreed" class="empty-circle" src="/src/public/login/agreement.png">
|
<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>
|
<span class="agreement-text">我已阅读并同意<a @click.stop>《用户协议》</a>和<a @click.stop>《隐私政策》</a></span>
|
||||||
</div>
|
</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="other-methods">
|
||||||
<div class="line"></div>
|
<div class="line"></div>
|
||||||
|
|
@ -74,6 +68,7 @@ import api from '@/api'
|
||||||
import authService from '@/api/authService'
|
import authService from '@/api/authService'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
|
import { initWechatLogin } from '@/utils/wxLogin'
|
||||||
|
|
||||||
const emit = defineEmits(['success'])
|
const emit = defineEmits(['success'])
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
@ -83,9 +78,6 @@ const loading = ref(false)
|
||||||
const sendingCode = ref(false)
|
const sendingCode = ref(false)
|
||||||
const countdown = ref(0)
|
const countdown = ref(0)
|
||||||
let timer = null
|
let timer = null
|
||||||
let qrTimer = null
|
|
||||||
const qrcode = ref('')
|
|
||||||
const qrKey = ref('')
|
|
||||||
const showError = ref(false)
|
const showError = ref(false)
|
||||||
const loginType = ref('phone')
|
const loginType = ref('phone')
|
||||||
|
|
||||||
|
|
@ -101,50 +93,14 @@ function open() {
|
||||||
// fetchQrCode()
|
// 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() {
|
function toggleLoginType() {
|
||||||
loginType.value = loginType.value === 'phone' ? 'wechat' : 'phone'
|
loginType.value = loginType.value === 'phone' ? 'wechat' : 'phone'
|
||||||
|
if (loginType.value === 'wechat') {
|
||||||
|
// 初始化微信开放平台登录
|
||||||
|
setTimeout(() => {
|
||||||
|
initWechatLogin('wechat-login-container')
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startCountdown() {
|
function startCountdown() {
|
||||||
|
|
@ -554,7 +510,15 @@ defineExpose({ open })
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
margin-bottom: 20px;
|
|
||||||
|
|
||||||
|
|
||||||
|
#wechat-login-container {
|
||||||
|
height: 312px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.qrcode-img {
|
.qrcode-img {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,7 @@
|
||||||
"ignoreCodeErrorList": ["GET&/api/weixin/qrcode","GET&/api/app/code", "GET&/api/order"],
|
"ignoreCodeErrorList": ["GET&/api/weixin/qrcode","GET&/api/app/code", "GET&/api/order"],
|
||||||
"hideConsole": true,
|
"hideConsole": true,
|
||||||
"downloadSuccessCode":[200,201,202,203,204,205,206],
|
"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-channel"] = "official";
|
||||||
header["x-version"] = "1.0.0";
|
header["x-version"] = "1.0.0";
|
||||||
header["x-platform"] = "web";
|
header["x-platform"] = "web";
|
||||||
|
header["x-app-id"] = "10037"; // 添加 appid 以满足微信鉴权后端要求
|
||||||
let deviceId = localStorage.getItem("device_id");
|
let deviceId = localStorage.getItem("device_id");
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
deviceId = appUtils.generateUUID();
|
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