完成微信扫码登录

This commit is contained in:
tangxinyue 2026-08-13 16:55:42 +08:00
parent c2c4485d74
commit 4c6b82789b
7 changed files with 138 additions and 75 deletions

8
package-lock.json generated
View File

@ -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"

View File

@ -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(() => { })
} }
}) })

View File

@ -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);

View File

@ -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;

View File

@ -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/"
} }

View File

@ -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();

83
src/utils/wxLogin.js Normal file
View File

@ -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
}