feat:新增微信扫码登录

This commit is contained in:
cyonjan 2025-05-14 13:24:05 +08:00
parent fc69f53dc9
commit c0f4b7db9b
5 changed files with 259 additions and 183 deletions

View File

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/order.svg" />

24
package-lock.json generated
View File

@ -23,6 +23,7 @@
"vite": "^6.3.5",
"vite-plugin-vue-devtools": "^7.7.6",
"vite-plugin-windicss": "^1.9.4",
"vue-next-wxlogin": "^1.0.4",
"windicss": "^3.5.6"
}
},
@ -2392,6 +2393,18 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/core-js": {
"version": "3.42.0",
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.42.0.tgz",
"integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -4507,6 +4520,17 @@
}
}
},
"node_modules/vue-next-wxlogin": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/vue-next-wxlogin/-/vue-next-wxlogin-1.0.4.tgz",
"integrity": "sha512-gJR8Zyp0tDWGcHPDkIJmTD50oblPbu7kPODmucj5d/sHYJw3VegD7xDsgRCSmT6+sT2lPBXFr8OTy2TQuMMmNw==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-js": "^3.6.5",
"vue": "^3.0.0"
}
},
"node_modules/vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz",

View File

@ -24,6 +24,7 @@
"vite": "^6.3.5",
"vite-plugin-vue-devtools": "^7.7.6",
"vite-plugin-windicss": "^1.9.4",
"vue-next-wxlogin": "^1.0.4",
"windicss": "^3.5.6"
}
}

View File

@ -1,30 +1,5 @@
<template>
<div class="wx-login-container">
<!-- 模态框模式 -->
<el-dialog
v-model="dialogVisible"
title="微信扫码登录"
width="380px"
:close-on-click-modal="false"
v-if="isDialog"
>
<login-content />
</el-dialog>
<!-- 嵌入式模式 -->
<div class="inline-login" v-else>
<login-content />
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
const LoginContent = {
template: `
<div class="login-content">
<div class="qrcode-container">
<el-image
v-loading="loading"
@ -46,7 +21,7 @@
:closable="false"
/>
<div v-else-if="!loading" class="scan-tip">
微信扫一扫登录
请使用微信扫一扫登录
</div>
</div>
@ -64,126 +39,150 @@
</el-button>
</div>
</div>
`,
props: ['getQrcodeUrl', 'checkLoginStatus'],
setup(props, { emit }) {
const loading = ref(true)
const refreshing = ref(false)
const qrcodeUrl = ref('')
const errorMessage = ref('')
let pollTimer = null
</template>
//
const fetchQrcode = async () => {
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
const props = defineProps({
// appid
appid: {
type: String,
required: true
},
// appsecret
appsecret: {
type: String,
required: true
}
})
const emit = defineEmits(['success'])
const loading = ref(true)
const refreshing = ref(false)
const qrcodeUrl = ref('')
const errorMessage = ref('')
let pollTimer = null
let ticket = ref('')
// 访
const getAccessToken = async () => {
try {
const response = await axios.get(`/api/wechat/token?appid=${props.appid}&secret=${props.appsecret}`)
return response.data.access_token
} catch (error) {
throw new Error('获取访问令牌失败')
}
}
//
const generateQrcode = async (access_token) => {
try {
const response = await axios.post(`/api/wechat/qrcode/create?access_token=${access_token}`, {
expire_seconds: 600,
action_name: 'QR_STR_SCENE',
action_info: {
scene: {
scene_str: 'login_' + Date.now()
}
}
})
return response.data
} catch (error) {
throw new Error('生成二维码失败')
}
}
//
const fetchQrcode = async () => {
try {
errorMessage.value = ''
const response = await fetch(props.getQrcodeUrl)
const data = await response.json()
qrcodeUrl.value = data.qrcodeUrl
startPolling(data.ticket)
} catch (e) {
errorMessage.value = '二维码获取失败,请重试'
loading.value = true
// 访
const access_token = await getAccessToken()
//
const qrcodeData = await generateQrcode(access_token)
ticket.value = qrcodeData.ticket
qrcodeUrl.value = `https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=${qrcodeData.ticket}`
//
startPolling()
} catch (error) {
errorMessage.value = error.message || '二维码获取失败,请重试'
} finally {
loading.value = false
refreshing.value = false
}
}
}
//
const startPolling = (ticket) => {
//
const startPolling = () => {
clearInterval(pollTimer)
pollTimer = setInterval(async () => {
try {
const response = await fetch(`${props.checkLoginStatus}?ticket=${ticket}`)
const res = await response.json()
const response = await axios.get(`/api/wechat/scan/check?ticket=${ticket.value}`)
const data = response.data
if (res.status === 'success') {
if (data.status === 'success') {
clearInterval(pollTimer)
emit('success', res.data)
} else if (res.status === 'expired') {
emit('success', data)
} else if (data.status === 'expired') {
errorMessage.value = '二维码已过期,请刷新'
clearInterval(pollTimer)
}
} catch (e) {
} catch (error) {
errorMessage.value = '网络异常,请检查连接'
clearInterval(pollTimer)
}
}, 2000)
}
}
//
const refreshQrcode = () => {
//
const refreshQrcode = () => {
refreshing.value = true
fetchQrcode()
}
}
onMounted(fetchQrcode)
onBeforeUnmount(() => clearInterval(pollTimer))
return {
loading,
refreshing,
qrcodeUrl,
errorMessage,
refreshQrcode
}
}
}
//
const props = defineProps({
// API
getQrcodeUrl: {
type: String,
required: true
},
// API
checkLoginStatus: {
type: String,
required: true
},
//
isDialog: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['success'])
const dialogVisible = ref(true)
onMounted(fetchQrcode)
onBeforeUnmount(() => clearInterval(pollTimer))
</script>
<style scoped>
.wx-login-container {
.wx-login-container {
text-align: center;
}
}
.qrcode-container {
.qrcode-container {
width: 300px;
margin: 0 auto;
}
}
.qrcode-image {
.qrcode-image {
width: 100%;
height: 300px;
border: 1px solid #eee;
}
}
.scan-tip {
.scan-tip {
color: #666;
margin: 15px 0;
font-size: 14px;
}
}
.action-area {
.action-area {
margin-top: 20px;
}
}
.image-error {
.image-error {
height: 300px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
}
}
</style>

View File

@ -21,7 +21,7 @@
<div class="flex flex-col items-center justify-center">
<el-input
v-model="username"
class="w-240px mt-3"
class="w-240px mt-4"
size="large"
placeholder="请输入用户名"
>
@ -32,7 +32,7 @@
<el-input
type="password"
v-model="password"
class="w-240px mt-3"
class="w-240px mt-4"
size="large"
placeholder="请输入密码..."
>
@ -58,7 +58,7 @@
</div>
</template>
<div class="flex flex-col justify-center items-center">
<div class="flex items-center w-240px mt-3">
<div class="flex items-center w-240px mt-4">
<el-input
v-model="mobile"
class="w-160px"
@ -83,7 +83,7 @@
style="width: 240px"
size="large"
placeholder="短信验证码"
class="mt-3"
class="mt-4"
>
<template #prefix>
<el-icon><Grid /></el-icon>
@ -107,7 +107,7 @@
</div>
</template>
<div class="flex flex-col justify-center items-center">
<div class="flex items-center w-240px mt-3">
<div class="flex items-center w-240px mt-4">
<el-input
type="email"
v-model="email"
@ -133,7 +133,7 @@
style="width: 240px"
size="large"
placeholder="邮件验证码"
class="mt-3"
class="mt-4"
>
<template #prefix>
<el-icon><Grid /></el-icon>
@ -149,7 +149,30 @@
</el-button>
</div>
</el-tab-pane>
<el-tab-pane label="" name="fourth">
<template #label>
<div class="flex items-center">
<i class="fa fa-qrcode"></i>
<span class="font-bold ml-2">微信扫码</span>
</div>
</template>
<div class="flex flex-col justify-center items-center">
<wxlogin
:appid="appId"
:scope="scope"
:redirect_uri="redirect_uri"
:state="state"
:self_redirect="self_redirect"
:href="style_href"
class='w-300px h-300px'
>
</wxlogin>
</div>
</el-tab-pane>
</el-tabs>
<div class="w-full flex items-center justify-center mt-4">
<el-link type="primary">找回密码</el-link>
</div>
<el-divider class="mt-6">
<span class="text-xs text-gray-400">成都机联云维科技有限公司 ©版权所有</span>
</el-divider>
@ -160,6 +183,10 @@
<script setup>
import { ref } from 'vue'
import wxlogin from 'vue-next-wxlogin'
import { useRouter } from 'vue-router'
const router = useRouter()
const activeName = ref('first')
const username = ref('')
const password = ref('')
@ -167,6 +194,22 @@
const code = ref('')
const email = ref('')
const ecode = ref('')
const appId = ref('wxa0a92798871387ba')
// wxd18da60b377a9b40
// wxa0a92798871387ba
const impower_style = `
.impowerBox qrcode {width: 200px;margin-top:10px;border: 0;}
.impowerBox .title {display: none;}
.status_icon {display: none;}
.impowerBox .info {width: 200px;margin: -10px auto;display: none;}
.impowerBox .status {text-align: center;padding: 0;}
`
const style_href = ref(`data:text/css;base64,${window.btoa(unescape(encodeURIComponent(impower_style)))}`)
const self_redirect = ref('false')
const scope = ref("snsapi_login")
const redirect_uri = encodeURIComponent("https://api.jifuyun.cn/")
const state = ref(`${parseInt(new Date().getTime() / 1000)}`)
const handleClick = (tab, event) => {
console.log(tab, event)
}
@ -179,6 +222,15 @@
const email_login = () => {
console.log(mobile.value, code.value)
}
const get_code = () => {
console.log('获取验证码')
}
const handleWechatLoginSuccess = (data) => {
console.log('微信登录成功:', data)
router.push('/home')
}
</script>
<style lang="scss" scoped>