From 74978176403d9095c9af584ed3bfbca6cf6c081c Mon Sep 17 00:00:00 2001 From: zyh Date: Fri, 29 Aug 2025 19:03:56 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=20Play.vue=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E6=8B=86=E5=88=86=E4=B8=BA=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E5=AD=90=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=8F=90=E5=8D=87=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=8F=AF=E7=BB=B4=E6=8A=A4=E6=80=A7=E5=92=8C=E5=8F=AF?= =?UTF-8?q?=E8=AF=BB=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要变更: - 将 Play.vue 拆分为 LoadingOverlay、SelectRegion、ScanPage、GamePage、RefreshWaitPage、ErrorPage 等组件 - 新增 composables 目录,分离业务逻辑(usePlayState、useTimers、useQrCode) - 优化代码结构,提升开发效率和维护性 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .claude/settings.local.json | 9 + src/components/play/ErrorPage.vue | 83 ++ src/components/play/GamePage.vue | 238 ++++ src/components/play/LoadingOverlay.vue | 43 + src/components/play/RefreshWaitPage.vue | 137 ++ src/components/play/ScanPage.vue | 295 ++++ src/components/play/SelectRegion.vue | 215 +++ src/composables/usePlayState.js | 437 ++++++ src/composables/useQrCode.js | 185 +++ src/composables/useTimers.js | 108 ++ src/views/Play.vue | 1744 ++--------------------- src/views/oldplay.vue | 1696 ++++++++++++++++++++++ 12 files changed, 3578 insertions(+), 1612 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 src/components/play/ErrorPage.vue create mode 100644 src/components/play/GamePage.vue create mode 100644 src/components/play/LoadingOverlay.vue create mode 100644 src/components/play/RefreshWaitPage.vue create mode 100644 src/components/play/ScanPage.vue create mode 100644 src/components/play/SelectRegion.vue create mode 100644 src/composables/usePlayState.js create mode 100644 src/composables/useQrCode.js create mode 100644 src/composables/useTimers.js create mode 100644 src/views/oldplay.vue diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..5557eae --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(git add:*)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/src/components/play/ErrorPage.vue b/src/components/play/ErrorPage.vue new file mode 100644 index 0000000..1e6c94b --- /dev/null +++ b/src/components/play/ErrorPage.vue @@ -0,0 +1,83 @@ + + + + + \ No newline at end of file diff --git a/src/components/play/GamePage.vue b/src/components/play/GamePage.vue new file mode 100644 index 0000000..f33a2a7 --- /dev/null +++ b/src/components/play/GamePage.vue @@ -0,0 +1,238 @@ + + + + + \ No newline at end of file diff --git a/src/components/play/LoadingOverlay.vue b/src/components/play/LoadingOverlay.vue new file mode 100644 index 0000000..cea63bc --- /dev/null +++ b/src/components/play/LoadingOverlay.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/src/components/play/RefreshWaitPage.vue b/src/components/play/RefreshWaitPage.vue new file mode 100644 index 0000000..71301d8 --- /dev/null +++ b/src/components/play/RefreshWaitPage.vue @@ -0,0 +1,137 @@ + + + + + \ No newline at end of file diff --git a/src/components/play/ScanPage.vue b/src/components/play/ScanPage.vue new file mode 100644 index 0000000..b286cc6 --- /dev/null +++ b/src/components/play/ScanPage.vue @@ -0,0 +1,295 @@ + + + + + \ No newline at end of file diff --git a/src/components/play/SelectRegion.vue b/src/components/play/SelectRegion.vue new file mode 100644 index 0000000..c3d787a --- /dev/null +++ b/src/components/play/SelectRegion.vue @@ -0,0 +1,215 @@ + + + + + \ No newline at end of file diff --git a/src/composables/usePlayState.js b/src/composables/usePlayState.js new file mode 100644 index 0000000..4a6ca86 --- /dev/null +++ b/src/composables/usePlayState.js @@ -0,0 +1,437 @@ +import { reactive } from 'vue' +import { useRoute, useRouter } from 'vue-router' +import { ElMessage } from 'element-plus' +import { + getLinkStatus, + selectRegion as selectRegionAPI, + refreshLink as refreshLinkAPI, + pollLoginStatus, + getGameProgress, + getGameInterface as getGameInterfaceAPI +} from '@/api/play' + +export function usePlayState() { + const route = useRoute() + const router = useRouter() + + const state = reactive({ + code: '', + status: 'NEW', + loading: true, + submitting: false, + needRefresh: false, + region: null, + qrInfo: null, + assets: null, + currentPoints: 0, + totalPoints: 1000, + completedPoints: 0, + error: null, + qrDelaySeconds: 0, + isWaitingQr: false, + qrRetryCount: 0, + maxQrRetries: 3, + qrRetryDelay: 2000, + qrError: null, + mecmachineId: null + }) + + const initializePage = async () => { + try { + await fetchStatus() + } catch (error) { + handleError(error) + } finally { + state.loading = false + } + } + + const fetchStatus = async () => { + try { + const response = await getLinkStatus(state.code) + const data = response.data + await updateStateFromResponse(data) + } catch (error) { + throw error + } + } + + const getGameInterface = async () => { + try { + console.log('调用游戏界面接口,code:', state.code) + const response = await getGameInterfaceAPI(state.code) + console.log('游戏界面接口响应:', response.data) + return response + } catch (error) { + console.error('获取游戏界面数据失败:', error) + ElMessage.error('获取游戏界面数据失败') + throw error + } + } + + const handleLoggedInStatus = async () => { + try { + console.log('检测到LOGGED_IN状态,获取游戏界面数据') + const gameResponse = await getGameInterface() + const gameData = gameResponse.data + + console.log('游戏界面数据:', gameData) + + state.status = 'LOGGED_IN' + + if(gameData.status == "COMPLETED"){ + state.assets = { + homepageUrl: gameData.homepageUrl, + firstRewardUrl: gameData.firstRewardUrl, + midRewardUrl: gameData.midRewardUrl, + endRewardUrl: gameData.endRewardUrl, + qrCodeUrl: gameData.mecmachineId ? `https://2.uzi0.cc/image/${gameData.mecmachineId}/二维码.png?t=${new Date().getTime()}` : null, + ...(gameData.assets || {}) + } + } else { + state.assets = { + homepageUrl: gameData.mecmachineId ? `https://2.uzi0.cc/image/${gameData.mecmachineId}/首次主页.png?t=${new Date().getTime()}` : null, + firstRewardUrl: gameData.mecmachineId ? `https://2.uzi0.cc/image/${gameData.mecmachineId}/首次赏金.png?t=${new Date().getTime()}` : null, + midRewardUrl: gameData.mecmachineId ? `https://2.uzi0.cc/image/${gameData.mecmachineId}/中途赏金.png?t=${new Date().getTime()}` : null, + endRewardUrl: gameData.mecmachineId ? `https://2.uzi0.cc/image/${gameData.mecmachineId}/结束赏金.png?t=${new Date().getTime()}` : null, + qrCodeUrl: gameData.mecmachineId ? `https://2.uzi0.cc/image/${gameData.mecmachineId}/二维码.png?t=${new Date().getTime()}` : null, + ...(gameData.assets || {}) + } + } + + console.log('更新区域信息:', { + gameDataRegion: gameData.region, + originalStateRegion: state.region + }) + if (gameData.region) { + state.region = gameData.region + console.log('已设置 state.region =', state.region) + } else { + console.log('gameData.region 为空,未更新 state.region') + } + + if (gameData.mecmachineId) { + state.mecmachineId = gameData.mecmachineId + } + + if (gameData.totalPoints) { + state.totalPoints = gameData.totalPoints + } else if (gameData.assets && gameData.assets.totalPoints) { + state.totalPoints = gameData.assets.totalPoints + } + + state.completedPoints = gameData.completedPoints || 0 + state.currentPoints = 0 + + console.log('handleLoggedInStatus 执行完成,最终状态:', { + status: state.status, + region: state.region, + totalPoints: state.totalPoints, + completedPoints: state.completedPoints, + mecmachineId: state.mecmachineId + }) + + ElMessage.success('登录成功,正在进入游戏界面...') + + } catch (error) { + console.error('获取游戏界面数据失败:', error) + ElMessage.error('获取游戏数据失败,请稍后重试') + } + } + + const handleCompletedStatus = async () => { + try { + const gameResponse = await getGameInterfaceAPI(state.code) + const gameData = gameResponse.data + + console.log('已完成状态 - 游戏界面数据:', gameData) + + state.status = 'COMPLETED' + + state.assets = { + homepageUrl: gameData.homepageUrl, + firstRewardUrl: gameData.firstRewardUrl, + midRewardUrl: gameData.midRewardUrl, + endRewardUrl: gameData.endRewardUrl, + qrCodeUrl: gameData.mecmachineId ? `https://2.uzi0.cc/image/${gameData.mecmachineId}/二维码.png?t=${Date.now()}` : null + } + + state.totalPoints = gameData.totalPoints || 50 + state.completedPoints = gameData.completedPoints || state.totalPoints + state.currentPoints = state.totalPoints + + console.log('已完成状态更新完成:', { + status: state.status, + totalPoints: state.totalPoints, + completedPoints: state.completedPoints, + currentPoints: state.currentPoints, + assets: !!state.assets + }) + + } catch (error) { + console.error('获取已完成状态游戏数据失败:', error) + state.status = 'COMPLETED' + ElMessage.error('获取游戏数据失败,但订单已完成') + } + } + + const updateStateFromResponse = async (data, skipQrProcessing = false) => { + if (data.status === 'LOGGED_IN') { + await handleLoggedInStatus() + return + } + + if (data.status === 'COMPLETED') { + await handleCompletedStatus() + return + } + + state.status = data.status + state.needRefresh = data.needRefresh || false + state.region = data.region + state.assets = data.assets + state.mecmachineId = data.mecmachineId || null + + if (data.totalPoints) { + state.totalPoints = data.totalPoints + } + + if (data.completedPoints !== undefined) { + state.completedPoints = data.completedPoints + } + + if (data.assets && data.assets.totalPoints) { + state.totalPoints = data.assets.totalPoints + if (state.currentPoints === undefined) { + state.currentPoints = 0 + } + } + + console.log('updateStateFromResponse:', { + status: data.status, + dataRegion: data.region, + stateRegion: state.region, + mecmachineId: data.mecmachineId, + totalPoints: state.totalPoints, + completedPoints: state.completedPoints, + skipQrProcessing + }) + + if (skipQrProcessing) { + return + } + + if (data.mecmachineId) { + const qrUrl = `https://2.uzi0.cc/image/${data.mecmachineId}/二维码.png?t=${Date.now()}` + state.qrInfo = { + url: qrUrl, + createdAt: data.qrCreatedAt, + expireAt: data.qrExpireAt + } + } else if (data.qr) { + state.qrInfo = data.qr + } + } + + const selectRegion = async (region) => { + if (state.submitting) return + + state.submitting = true + state.qrRetryCount = 0 + + try { + const response = await selectRegionAPI({ code: state.code, region }) + const data = response.data + + console.log('selectRegion 响应数据:', data) + return data + + } catch (error) { + handleError(error) + throw error + } finally { + state.submitting = false + } + } + + const handleRefresh = async (clearAllTimers) => { + try { + const response = await refreshLinkAPI(state.code) + const data = response.data + + if (clearAllTimers) { + clearAllTimers() + } + + state.needRefresh = false + state.status = 'NEW' + + return data + + } catch (error) { + handleError(error) + throw error + } + } + + const handlePageRefresh = () => { + window.location.reload() + } + + const handleRetry = () => { + state.error = null + state.loading = true + initializePage() + } + + const handleError = (error) => { + console.error('API错误:', error) + + const status = error?.response?.status + + if (status === 401) { + console.log('检测到401错误,跳转到登录页面') + router.replace({ + name: 'Login', + query: { redirect: route.fullPath } + }) + return + } + + if (status === 400 || status === 403) { + state.error = 'INVALID_CODE' + } else if (status === 410) { + state.error = 'EXPIRED' + } else { + state.error = 'NETWORK_ERROR' + } + } + + const formatTime = (seconds) => { + const mins = Math.floor(seconds / 60) + const secs = seconds % 60 + return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` + } + + const getRegionName = () => { + return state.region === 'Q' ? 'QQ区' : state.region === 'V' ? '微信区' : '' + } + + const getCurrentUrl = () => { + return window.location.href + } + + const getGameStatus = () => { + if (state.currentPoints >= state.totalPoints) { + return '已打完' + } else if (state.currentPoints > 0) { + return '代练中' + } else { + return '空闲' + } + } + + const getStatusClass = () => { + const status = getGameStatus() + return { + 'status-completed': status === '已打完', + 'status-playing': status === '代练中', + 'status-idle': status === '空闲' + } + } + + const getDisplayStatus = () => { + if (state.status === 'COMPLETED') { + return '已完成' + } else if (state.status === 'LOGGED_IN') { + return '代练中' + } else { + return '状态' + } + } + + const getStatusMessage = () => { + if (state.status === 'COMPLETED') { + return '代练已完成!感谢您的使用,订单已结束。' + } else if (state.status === 'LOGGED_IN') { + return '正在代练中,期间请勿操号,耐心等待代练完成......' + } else { + return '正在代练中,期间请勿操号,耐心等待代练完成......' + } + } + + const getStatusMessageClass = () => { + if (state.status === 'COMPLETED') { + return 'status-message-completed' + } else { + return '' + } + } + + const getProgressPercent = () => { + if (!state.totalPoints) return 0 + return Math.min(100, (state.currentPoints / state.totalPoints) * 100) + } + + const getCurrentGameImage = () => { + if (!state.assets) return null + + const progress = getProgressPercent() + if (progress === 0) { + return state.assets.homepageUrl + } else if (progress < 50) { + return state.assets.firstRewardUrl + } else if (progress < 100) { + return state.assets.midRewardUrl + } else { + return state.assets.endRewardUrl + } + } + + const getErrorTitle = () => { + const titles = { + 'INVALID_CODE': '链接无效', + 'EXPIRED': '链接已过期', + 'REFUNDED': '订单已退单', + 'NETWORK_ERROR': '网络错误' + } + return titles[state.error] || '出现错误' + } + + const getErrorMessage = () => { + const messages = { + 'INVALID_CODE': '请联系商家获取有效链接', + 'EXPIRED': '请联系商家重新获取链接', + 'REFUNDED': '该订单已被退单,无法继续使用', + 'NETWORK_ERROR': '网络连接失败,请检查网络后重试' + } + return messages[state.error] || '请稍后重试或联系客服' + } + + + return { + state, + initializePage, + fetchStatus, + updateStateFromResponse, + handleLoggedInStatus, + handleCompletedStatus, + selectRegion, + handleRefresh, + handlePageRefresh, + handleRetry, + handleError, + formatTime, + getRegionName, + getCurrentUrl, + getGameStatus, + getStatusClass, + getDisplayStatus, + getStatusMessage, + getStatusMessageClass, + getProgressPercent, + getCurrentGameImage, + getErrorTitle, + getErrorMessage + } +} \ No newline at end of file diff --git a/src/composables/useQrCode.js b/src/composables/useQrCode.js new file mode 100644 index 0000000..50a0148 --- /dev/null +++ b/src/composables/useQrCode.js @@ -0,0 +1,185 @@ +import { ElMessage } from 'element-plus' +import { selectRegion as selectRegionAPI } from '@/api/play' + +export function useQrCode() { + + const validateQrCodeUrl = async (url) => { + try { + console.log('开始验证二维码URL:', url) + + if (!url || typeof url !== 'string') { + console.error('无效的二维码URL:', url) + return false + } + + if (process.env.NODE_ENV === 'production') { + console.log('生产环境跳过URL验证') + return true + } + + const controller = new AbortController() + const timeoutId = setTimeout(() => { + controller.abort() + console.log('URL验证超时') + }, 3000) + + const response = await fetch(url, { + method: 'HEAD', + signal: controller.signal, + mode: 'no-cors' + }) + + clearTimeout(timeoutId) + console.log('URL验证响应:', response.status) + return true + } catch (error) { + console.warn('二维码URL验证失败:', error.name, error.message) + return true + } + } + + const fetchQrCodeAfterDelay = async (state, countdown, mecmachineId, qrCreatedAt, qrExpireAt, retryCount = 0) => { + try { + const qrCodeUrl = `https://2.uzi0.cc/image/${mecmachineId}/二维码.png?t=${Date.now()}` + console.log(`尝试获取二维码 (第${retryCount + 1}次):`, qrCodeUrl) + + const isUrlValid = await validateQrCodeUrl(qrCodeUrl) + console.log('URL验证结果:', isUrlValid) + + if (!isUrlValid) { + throw new Error('二维码URL无法访问') + } + + console.log('设置二维码信息:', { qrCodeUrl, qrCreatedAt, qrExpireAt }) + state.qrInfo = { + url: qrCodeUrl, + createdAt: qrCreatedAt, + expireAt: qrExpireAt + } + + const now = Date.now() + const expireTime = new Date(qrExpireAt).getTime() + countdown.value = Math.max(0, Math.floor((expireTime - now) / 1000)) + + state.qrRetryCount = 0 + state.isWaitingQr = false + + console.log('二维码设置完成,countdown:', countdown.value) + ElMessage.success('二维码已准备就绪,请扫码登录') + + } catch (error) { + console.error(`二维码获取失败 (第${retryCount + 1}次):`, error.message || 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(state, countdown, mecmachineId, qrCreatedAt, qrExpireAt, retryCount + 1) + }, delay) + } else { + console.error('二维码获取重试次数用完,显示错误状态') + state.qrRetryCount = 0 + state.isWaitingQr = false + state.qrError = `二维码获取失败,已重试${state.maxQrRetries}次。可能是网络问题或服务器繁忙,请稍后重试。` + ElMessage.error('二维码获取失败,请点击重新获取按钮') + } + } + } + + const retrySelectRegion = async (state, code, region) => { + try { + console.log('重试选择区域:', region) + const response = await selectRegionAPI({ code, region }) + const data = response.data + + console.log('retrySelectRegion 响应数据:', data) + return data + + } catch (error) { + console.error('重试选择区域失败:', error) + ElMessage.error('重新请求二维码失败,请手动刷新页面') + throw error + } + } + + const processSelectRegionResponse = async (state, countdown, data, startCountdown, startLoginPolling, updateStateFromResponse) => { + if (data.qrDelaySeconds && data.qrDelaySeconds > 0 && data.mecmachineId) { + 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(state, countdown, data.mecmachineId, 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(state, countdown, data.mecmachineId, data.qrCreatedAt, data.qrExpireAt) + } + } + + if (state.status === 'USING') { + startCountdown() + startLoginPolling() + } + } + } + + const handleQrImageError = (state, event) => { + console.error('二维码图片加载失败:', event) + state.qrError = '二维码图片加载失败,可能是网络问题' + ElMessage.error('二维码图片加载失败') + } + + const retryGetQrCode = async (state, selectRegion) => { + if (state.submitting) return + + state.submitting = true + state.qrError = null + state.isWaitingQr = true + state.qrRetryCount = 0 + + try { + if (state.region) { + await selectRegion(state.region) + } + } catch (error) { + console.error('重新获取二维码失败:', error) + state.qrError = '重新获取失败,请刷新页面重试' + } finally { + state.submitting = false + } + } + + return { + validateQrCodeUrl, + fetchQrCodeAfterDelay, + retrySelectRegion, + processSelectRegionResponse, + handleQrImageError, + retryGetQrCode + } +} \ No newline at end of file diff --git a/src/composables/useTimers.js b/src/composables/useTimers.js new file mode 100644 index 0000000..f601dc6 --- /dev/null +++ b/src/composables/useTimers.js @@ -0,0 +1,108 @@ +import { ref, reactive } from 'vue' +import { pollLoginStatus, getGameProgress } from '@/api/play' + +export function useTimers() { + const countdown = ref(0) + const refreshCooldown = ref(0) + + const timers = reactive({ + loginPoll: null, + countdown: null, + refreshCooldown: null, + progressPoll: null + }) + + const clearTimer = (name) => { + if (timers[name]) { + clearInterval(timers[name]) + timers[name] = null + } + } + + const clearAllTimers = () => { + Object.keys(timers).forEach(clearTimer) + } + + const startCountdown = (initialValue = null) => { + clearTimer('countdown') + + if (initialValue !== null) { + countdown.value = initialValue + } + + timers.countdown = setInterval(() => { + if (countdown.value > 0) { + countdown.value-- + } else { + clearTimer('countdown') + clearTimer('loginPoll') + } + }, 1000) + } + + const startLoginPolling = (code, onLoggedIn, onCompleted) => { + clearTimer('loginPoll') + + timers.loginPoll = setInterval(async () => { + try { + const response = await pollLoginStatus(code) + const data = response.data + + console.log('poll-login 响应数据:', data) + + if (data.status === 'LOGGED_IN') { + await onLoggedIn() + } else if (data.status === 'COMPLETED') { + await onCompleted() + clearTimer('loginPoll') + } + + } catch (error) { + console.error('轮询错误:', error) + } + }, 1000) + } + + const startProgressPolling = (code, onProgressUpdate) => { + clearTimer('progressPoll') + + const pollProgress = async () => { + try { + const response = await getGameProgress(code) + const data = response.data + onProgressUpdate(data) + } catch (error) { + console.error('进度轮询错误:', error) + } + } + + pollProgress() + + timers.progressPoll = setInterval(pollProgress, 5000) + } + + const startRefreshCooldown = (seconds) => { + refreshCooldown.value = seconds + clearTimer('refreshCooldown') + + timers.refreshCooldown = setInterval(() => { + if (refreshCooldown.value > 0) { + refreshCooldown.value-- + } else { + clearTimer('refreshCooldown') + } + }, 1000) + } + + return { + countdown, + refreshCooldown, + timers, + clearTimer, + clearAllTimers, + startCountdown, + startLoginPolling, + startProgressPolling, + startRefreshCooldown + } +} \ No newline at end of file diff --git a/src/views/Play.vue b/src/views/Play.vue index fe19b41..b3fdae0 100644 --- a/src/views/Play.vue +++ b/src/views/Play.vue @@ -1,251 +1,130 @@ + +