初始版本
This commit is contained in:
360
app/pages/wx-scan.vue
Normal file
360
app/pages/wx-scan.vue
Normal file
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<div class="wx-scan-page">
|
||||
<div class="scan-container">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="status === 'loading'" class="status-box">
|
||||
<a-spin size="large" />
|
||||
<p class="status-text">{{ loadingText }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 扫码成功 -->
|
||||
<div v-else-if="status === 'success'" class="status-box success">
|
||||
<CheckCircleOutlined class="status-icon success-icon" />
|
||||
<h2 class="status-title">登录成功</h2>
|
||||
<p class="status-text">正在跳转...</p>
|
||||
<div class="user-info" v-if="userInfo">
|
||||
<a-avatar :size="64" :src="userInfo.avatar">
|
||||
<template v-if="!userInfo.avatar"><UserOutlined /></template>
|
||||
</a-avatar>
|
||||
<p class="username">{{ userInfo.nickname || userInfo.username }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 需要绑定 -->
|
||||
<div v-else-if="status === 'bind_required'" class="status-box warning">
|
||||
<ExclamationCircleOutlined class="status-icon warning-icon" />
|
||||
<h2 class="status-title">需要绑定手机号</h2>
|
||||
<p class="status-text">{{ message_text }}</p>
|
||||
<a-button type="primary" size="large" @click="goToBindPhone">去绑定手机号</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 扫码失败 -->
|
||||
<div v-else-if="status === 'error'" class="status-box error">
|
||||
<CloseCircleOutlined class="status-icon error-icon" />
|
||||
<h2 class="status-title">扫码失败</h2>
|
||||
<p class="status-text">{{ message_text }}</p>
|
||||
<a-button type="primary" size="large" @click="retry">重新扫码</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 未找到用户 -->
|
||||
<div v-else-if="status === 'not_bound'" class="status-box warning">
|
||||
<ExclamationCircleOutlined class="status-icon warning-icon" />
|
||||
<h2 class="status-title">账号未绑定</h2>
|
||||
<p class="status-text">{{ message_text }}</p>
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" size="large" @click="goToRegister">前往注册</a-button>
|
||||
<a-button size="large" @click="retry">重新扫码</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 底部安全提示 -->
|
||||
<div class="security-tip">
|
||||
<SafetyCertificateOutlined />
|
||||
<span>请确认是您本人操作,如非本人操作请忽略</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
UserOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import {
|
||||
checkQrCodeStatus,
|
||||
getWechatOAuthUrl,
|
||||
wechatScanConfirm,
|
||||
type WechatScanResponse
|
||||
} from '@/api/passport/qrLogin'
|
||||
import { setToken } from '@/utils/token-util'
|
||||
|
||||
definePageMeta({ layout: 'blank' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const status = ref<'loading' | 'success' | 'error' | 'bind_required' | 'not_bound' | 'not_wechat'>('loading')
|
||||
const message_text = ref('')
|
||||
const loadingText = ref('正在等待扫码...')
|
||||
const userInfo = ref<any>(null)
|
||||
|
||||
const token = computed(() => String(route.query.token || ''))
|
||||
const code = computed(() => String(route.query.code || ''))
|
||||
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const isWechatBrowser = computed(() => {
|
||||
if (!import.meta.client) return false
|
||||
return /MicroMessenger/i.test(window.navigator.userAgent)
|
||||
})
|
||||
|
||||
async function redirectToWechatOAuth() {
|
||||
if (!import.meta.client || !token.value) return
|
||||
|
||||
try {
|
||||
loadingText.value = '正在拉起微信授权...'
|
||||
const oauthUrl = await getWechatOAuthUrl(token.value)
|
||||
if (!oauthUrl) {
|
||||
throw new Error('未获取到微信授权地址')
|
||||
}
|
||||
window.location.replace(oauthUrl)
|
||||
} catch (error: unknown) {
|
||||
console.error('获取微信授权地址失败:', error)
|
||||
status.value = 'error'
|
||||
message_text.value = error instanceof Error ? error.message : '获取微信授权地址失败,请重试'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询检查登录状态(扫码关注登录模式)
|
||||
*/
|
||||
async function redirectToBindPhone(tip?: string) {
|
||||
status.value = 'bind_required'
|
||||
message_text.value = tip || '请先绑定手机号完成登录'
|
||||
message.info(message_text.value)
|
||||
await router.replace(`/bind-phone?token=${token.value}`)
|
||||
}
|
||||
|
||||
async function pollLoginStatus() {
|
||||
if (!token.value) return
|
||||
|
||||
try {
|
||||
const response = await checkQrCodeStatus(token.value)
|
||||
|
||||
if (response.status === 'confirmed') {
|
||||
// 登录成功
|
||||
stopPolling()
|
||||
handleLoginSuccess({
|
||||
status: 'success',
|
||||
accessToken: response.accessToken || response.access_token,
|
||||
userInfo: response.userInfo,
|
||||
tenantId: response.tenantId ? Number(response.tenantId) : undefined
|
||||
})
|
||||
} else if (response.status === 'bind_phone') {
|
||||
stopPolling()
|
||||
await redirectToBindPhone(response.message)
|
||||
} else if (response.status === 'expired') {
|
||||
// 二维码过期
|
||||
stopPolling()
|
||||
status.value = 'error'
|
||||
message_text.value = '二维码已过期,请刷新后重新扫码'
|
||||
} else if (response.status === 'scanned') {
|
||||
// 已扫码,等待公众号事件回写
|
||||
loadingText.value = '已识别扫码,请关注公众号后自动登录'
|
||||
} else {
|
||||
// pending 状态,继续等待
|
||||
loadingText.value = '等待扫码...'
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('检查登录状态失败:', error)
|
||||
// 轮询错误不影响,继续轮询
|
||||
}
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// 如果没有 token,显示错误
|
||||
if (!token.value) {
|
||||
status.value = 'error'
|
||||
message_text.value = '二维码参数错误'
|
||||
return
|
||||
}
|
||||
|
||||
// 如果有 code(微信授权回调),直接用 code 登录
|
||||
if (code.value) {
|
||||
await handleLoginWithCode()
|
||||
return
|
||||
}
|
||||
|
||||
// 微信内打开扫码页时,先拉起 OAuth 获取身份,再由回调继续登录
|
||||
if (isWechatBrowser.value) {
|
||||
await redirectToWechatOAuth()
|
||||
return
|
||||
}
|
||||
|
||||
// 非微信环境下保留轮询兜底,兼容服务端推送扫码状态的场景
|
||||
status.value = 'loading'
|
||||
loadingText.value = '等待扫码,请在微信中扫描二维码...'
|
||||
|
||||
// 立即检查一次
|
||||
await pollLoginStatus()
|
||||
// 每2秒轮询一次
|
||||
pollTimer = setInterval(pollLoginStatus, 2000)
|
||||
})
|
||||
|
||||
async function handleLoginWithCode() {
|
||||
status.value = 'loading'
|
||||
loadingText.value = '正在验证身份...'
|
||||
try {
|
||||
const response: WechatScanResponse = await wechatScanConfirm({
|
||||
token: token.value,
|
||||
code: code.value
|
||||
})
|
||||
|
||||
if ((response.status === 'success' || response.status === 'confirmed') && (response.accessToken || response.access_token)) {
|
||||
handleLoginSuccess({
|
||||
...response,
|
||||
accessToken: response.accessToken || response.access_token
|
||||
})
|
||||
} else if (response.status === 'bind_required' || response.status === 'bind_phone') {
|
||||
await redirectToBindPhone(response.message)
|
||||
} else {
|
||||
status.value = 'not_bound'
|
||||
message_text.value = response.message || '该微信未绑定平台账号'
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('微信扫码登录失败:', error)
|
||||
status.value = 'error'
|
||||
message_text.value = error instanceof Error ? error.message : '登录失败,请重试'
|
||||
}
|
||||
}
|
||||
|
||||
function handleLoginSuccess(response: WechatScanResponse) {
|
||||
status.value = 'success'
|
||||
userInfo.value = response.userInfo
|
||||
|
||||
// 保存 token
|
||||
if (response.accessToken) {
|
||||
setToken(response.accessToken, true)
|
||||
if (response.tenantId) {
|
||||
localStorage.setItem('TenantId', String(response.tenantId))
|
||||
}
|
||||
if (userInfo.value?.userId) {
|
||||
localStorage.setItem('UserId', String(userInfo.value.userId))
|
||||
}
|
||||
}
|
||||
|
||||
message.success('登录成功')
|
||||
|
||||
// 2秒后跳转到首页
|
||||
setTimeout(() => {
|
||||
router.push('/')
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
function goToBindPhone() {
|
||||
router.push(`/bind-phone?token=${token.value}`)
|
||||
}
|
||||
|
||||
function goToRegister() {
|
||||
router.push('/register')
|
||||
}
|
||||
|
||||
function retry() {
|
||||
router.replace('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wx-scan-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.scan-container {
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 40px 32px;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
min-height: 280px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 72px;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.security-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 32px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.security-tip :deep(.anticon) {
|
||||
color: #52c41a;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user