优化二维码获取逻辑,新增URL验证和重试机制,提升用户体验;更新Play.vue界面,增加重试信息显示和区域选择处理逻辑。

This commit is contained in:
zyh
2025-08-28 22:20:28 +08:00
parent b9752b9a51
commit 37e56978b1
2 changed files with 172 additions and 47 deletions

View File

@@ -38,9 +38,13 @@ http.interceptors.request.use(
'/api/link/status', '/api/link/status',
'/api/link/select-region', '/api/link/select-region',
'/api/link/poll-login', '/api/link/poll-login',
'/api/link/progress' '/api/link/progress',
'/api/link/refresh',
'/api/link/qr.png'
] ]
const isPublicAPI = publicAPIs.some(api => config.url?.includes(api)) // 游戏界面接口使用动态路径,需要特殊处理
const isGameInterfaceAPI = /\/api\/link\/[^\/]+\/game-interface/.test(config.url || '')
const isPublicAPI = publicAPIs.some(api => config.url?.includes(api)) || isGameInterfaceAPI
if (!isPublicAPI) { if (!isPublicAPI) {
const token = getAccessToken() const token = getAccessToken()
@@ -81,11 +85,20 @@ http.interceptors.response.use(
'/api/link/status', '/api/link/status',
'/api/link/select-region', '/api/link/select-region',
'/api/link/poll-login', '/api/link/poll-login',
'/api/link/progress' '/api/link/progress',
'/api/link/refresh',
'/api/link/qr.png'
] ]
const isPublicAPI = publicAPIs.some(api => url?.includes(api)) // 游戏界面接口使用动态路径,需要特殊处理
const isGameInterfaceAPI = /\/api\/link\/[^\/]+\/game-interface/.test(url || '')
const isPublicAPI = publicAPIs.some(api => url?.includes(api)) || isGameInterfaceAPI
if (status === 401 && !isAuthPath && !isPublicAPI) { if (status === 401 && !isAuthPath && !isPublicAPI) {
// 阻止浏览器显示基本认证弹窗
if (error.response && error.response.headers) {
delete error.response.headers['www-authenticate']
}
if (!isRefreshing) { if (!isRefreshing) {
isRefreshing = true isRefreshing = true
try { try {

View File

@@ -51,6 +51,7 @@
<div class="loading-spinner"></div> <div class="loading-spinner"></div>
<p class="waiting-text">正在准备二维码...</p> <p class="waiting-text">正在准备二维码...</p>
<p class="waiting-desc">预计等待 {{ state.qrDelaySeconds }} </p> <p class="waiting-desc">预计等待 {{ state.qrDelaySeconds }} </p>
<p v-if="state.qrRetryCount > 0" class="retry-info">重试中... ({{ state.qrRetryCount }}/{{ state.maxQrRetries }})</p>
</div> </div>
<!-- 二维码区域 --> <!-- 二维码区域 -->
@@ -172,7 +173,7 @@
<script> <script>
import { reactive, ref, onMounted, onUnmounted } from 'vue' import { reactive, ref, onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { import {
getLinkStatus, getLinkStatus,
@@ -187,6 +188,7 @@ export default {
name: 'Play', name: 'Play',
setup() { setup() {
const route = useRoute() const route = useRoute()
const router = useRouter()
// 状态管理 // 状态管理
const state = reactive({ const state = reactive({
@@ -202,7 +204,10 @@ export default {
totalPoints: 1000, totalPoints: 1000,
error: null, error: null,
qrDelaySeconds: 0, qrDelaySeconds: 0,
isWaitingQr: false isWaitingQr: false,
qrRetryCount: 0,
maxQrRetries: 3,
qrRetryDelay: 2000
}) })
// 计时器 // 计时器
@@ -431,12 +436,38 @@ export default {
} }
} }
// 延迟获取二维码 // 验证二维码URL是否可访问
const fetchQrCodeAfterDelay = async (qrCodeUrl, qrCreatedAt, qrExpireAt) => { const validateQrCodeUrl = async (url) => {
try { try {
// 这里可以添加额外的验证或处理逻辑 const controller = new AbortController()
// 比如检查 URL 是否可访问 const timeoutId = setTimeout(() => controller.abort(), 5000)
const response = await fetch(url, {
method: 'HEAD',
signal: controller.signal
})
clearTimeout(timeoutId)
return response.ok
} catch (error) {
console.warn('二维码URL验证失败:', error)
return false
}
}
// 延迟获取二维码(带重试机制)
const fetchQrCodeAfterDelay = async (qrCodeUrl, qrCreatedAt, qrExpireAt, retryCount = 0) => {
try {
console.log(`尝试获取二维码 (第${retryCount + 1}次):`, qrCodeUrl)
// 验证二维码URL是否可访问
const isUrlValid = await validateQrCodeUrl(qrCodeUrl)
if (!isUrlValid) {
throw new Error('二维码URL无法访问')
}
// URL验证通过设置二维码信息
state.qrInfo = { state.qrInfo = {
url: qrCodeUrl, url: qrCodeUrl,
createdAt: qrCreatedAt, createdAt: qrCreatedAt,
@@ -447,10 +478,104 @@ export default {
const expireTime = new Date(qrExpireAt).getTime() const expireTime = new Date(qrExpireAt).getTime()
countdown.value = Math.max(0, Math.floor((expireTime - now) / 1000)) countdown.value = Math.max(0, Math.floor((expireTime - now) / 1000))
// 重置重试计数
state.qrRetryCount = 0
ElMessage.success('二维码已准备就绪,请扫码登录') ElMessage.success('二维码已准备就绪,请扫码登录')
console.log('二维码获取成功')
} catch (error) { } catch (error) {
console.error('获取二维码失败:', error) console.error(`二维码获取失败 (第${retryCount + 1}次):`, error)
ElMessage.error('二维码获取失败,请重试')
// 如果还有重试次数,则进行重试
if (retryCount < state.maxQrRetries) {
state.qrRetryCount = retryCount + 1
const delay = state.qrRetryDelay * Math.pow(2, retryCount) // 指数退避
console.log(`${delay}ms后进行第${retryCount + 2}次重试`)
ElMessage.warning(`二维码获取失败,${delay/1000}秒后重试...`)
setTimeout(() => {
fetchQrCodeAfterDelay(qrCodeUrl, qrCreatedAt, qrExpireAt, retryCount + 1)
}, delay)
} else {
// 重试次数用完,重新请求新的二维码
console.error('二维码获取重试次数用完,重新请求区域选择')
ElMessage.error('二维码获取失败,正在重新请求...')
state.qrRetryCount = 0
// 重新调用区域选择API
if (state.region) {
retrySelectRegion(state.region)
}
}
}
}
// 重试选择区域内部使用不显示loading状态
const retrySelectRegion = async (region) => {
try {
console.log('重试选择区域:', region)
const response = await selectRegionAPI({ code: state.code, region })
const data = response.data
console.log('retrySelectRegion 响应数据:', data)
// 处理响应数据,使用重试逻辑
await processSelectRegionResponse(data)
} catch (error) {
console.error('重试选择区域失败:', error)
ElMessage.error('重新请求二维码失败,请手动刷新页面')
}
}
// 处理选择区域响应的通用逻辑
const processSelectRegionResponse = async (data) => {
// 如果返回了延迟时间和二维码URL需要延迟处理
if (data.qrDelaySeconds && data.qrDelaySeconds > 0 && data.qrCodeUrl) {
console.log('进入延迟分支')
// 跳过二维码处理,只更新基本状态
await updateStateFromResponse(data, true)
state.qrDelaySeconds = data.qrDelaySeconds
state.isWaitingQr = true
console.log('设置状态:', { status: state.status, isWaitingQr: state.isWaitingQr })
ElMessage.info(`正在准备二维码,请等待 ${data.qrDelaySeconds} 秒...`)
setTimeout(async () => {
state.isWaitingQr = false
// 延迟后获取二维码(带重试机制)
await fetchQrCodeAfterDelay(data.qrCodeUrl, data.qrCreatedAt, data.qrExpireAt)
if (state.status === 'USING') {
startCountdown()
startLoginPolling()
}
}, data.qrDelaySeconds * 1000)
} else {
console.log('进入立即处理分支')
// 没有延迟时间,立即处理二维码
await updateStateFromResponse(data)
state.isWaitingQr = false
console.log('设置状态:', { status: state.status, isWaitingQr: state.isWaitingQr, qrInfo: !!state.qrInfo })
// 对于立即处理的二维码,也要进行验证
if (state.qrInfo && state.qrInfo.url) {
const isUrlValid = await validateQrCodeUrl(state.qrInfo.url)
if (!isUrlValid) {
console.warn('立即获取的二维码URL无法访问尝试重试')
// 清除当前二维码信息,触发重试
state.qrInfo = null
await fetchQrCodeAfterDelay(data.qrCodeUrl, data.qrCreatedAt, data.qrExpireAt)
}
}
if (state.status === 'USING') {
startCountdown()
startLoginPolling()
}
} }
} }
@@ -459,6 +584,7 @@ export default {
if (state.submitting) return if (state.submitting) return
state.submitting = true state.submitting = true
state.qrRetryCount = 0 // 重置重试计数
try { try {
const response = await selectRegionAPI({ code: state.code, region }) const response = await selectRegionAPI({ code: state.code, region })
@@ -466,40 +592,8 @@ export default {
console.log('selectRegion 响应数据:', data) console.log('selectRegion 响应数据:', data)
// 如果返回了延迟时间和二维码URL需要延迟处理 // 使用通用处理逻辑
if (data.qrDelaySeconds && data.qrDelaySeconds > 0 && data.qrCodeUrl) { await processSelectRegionResponse(data)
console.log('进入延迟分支')
// 跳过二维码处理,只更新基本状态
await updateStateFromResponse(data, true)
state.qrDelaySeconds = data.qrDelaySeconds
state.isWaitingQr = true
console.log('设置状态:', { status: state.status, isWaitingQr: state.isWaitingQr })
ElMessage.info(`正在准备二维码,请等待 ${data.qrDelaySeconds} 秒...`)
setTimeout(async () => {
state.isWaitingQr = false
// 延迟后获取二维码
await fetchQrCodeAfterDelay(data.qrCodeUrl, data.qrCreatedAt, data.qrExpireAt)
if (state.status === 'USING') {
startCountdown()
startLoginPolling()
}
}, data.qrDelaySeconds * 1000)
} else {
console.log('进入立即处理分支')
// 没有延迟时间,立即处理二维码
await updateStateFromResponse(data)
state.isWaitingQr = false
console.log('设置状态:', { status: state.status, isWaitingQr: state.isWaitingQr, qrInfo: !!state.qrInfo })
if (state.status === 'USING') {
startCountdown()
startLoginPolling()
}
}
} catch (error) { } catch (error) {
handleError(error) handleError(error)
@@ -628,8 +722,18 @@ export default {
// 获取HTTP状态码 // 获取HTTP状态码
const status = error?.response?.status const status = error?.response?.status
// 如果是认证相关错误,跳转到登录页面
if (status === 401) {
console.log('检测到401错误跳转到登录页面')
router.replace({
name: 'Login',
query: { redirect: route.fullPath }
})
return
}
// 根据HTTP状态码设置错误状态 // 根据HTTP状态码设置错误状态
if (status === 400 || status === 401 || status === 403) { if (status === 400 || status === 403) {
state.error = 'INVALID_CODE' state.error = 'INVALID_CODE'
} else if (status === 410) { } else if (status === 410) {
state.error = 'EXPIRED' state.error = 'EXPIRED'
@@ -956,6 +1060,14 @@ export default {
opacity: 0.8; opacity: 0.8;
} }
.retry-info {
font-size: 14px;
margin: 8px 0 0 0;
opacity: 0.9;
color: #ffd700;
font-weight: 500;
}
.qr-wrapper { .qr-wrapper {
background: white; background: white;
padding: 20px; padding: 20px;