重构 Play.vue 组件,拆分为多个子组件,提升代码可维护性和可读性。
主要变更: - 将 Play.vue 拆分为 LoadingOverlay、SelectRegion、ScanPage、GamePage、RefreshWaitPage、ErrorPage 等组件 - 新增 composables 目录,分离业务逻辑(usePlayState、useTimers、useQrCode) - 优化代码结构,提升开发效率和维护性 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(git add:*)"
|
||||||
|
],
|
||||||
|
"deny": [],
|
||||||
|
"ask": []
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/components/play/ErrorPage.vue
Normal file
83
src/components/play/ErrorPage.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div class="error-page">
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="error-icon">❌</div>
|
||||||
|
<h2 class="error-title">{{ errorTitle }}</h2>
|
||||||
|
<p class="error-message">{{ errorMessage }}</p>
|
||||||
|
<button @click="$emit('retry')" class="retry-btn">重新尝试</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ErrorPage',
|
||||||
|
props: {
|
||||||
|
errorTitle: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['retry']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.error-page {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
background: white;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
text-align: center;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 32px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn:hover {
|
||||||
|
background: #5a6fd8;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
238
src/components/play/GamePage.vue
Normal file
238
src/components/play/GamePage.vue
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
<template>
|
||||||
|
<div class="game-page">
|
||||||
|
<!-- 顶部标签页 -->
|
||||||
|
<div class="tab-header">
|
||||||
|
<div class="tab-item active">代练大区</div>
|
||||||
|
<div class="tab-item status-tab">状态</div>
|
||||||
|
<div class="tab-item target-tab">目标点数</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-header">{{ region }}
|
||||||
|
<div class="tab-item active" v-if="region === 'Q'">QQ</div>
|
||||||
|
<div class="tab-item active" v-if="region === 'V'">微信</div>
|
||||||
|
<div class="tab-item status-tab">{{ displayStatus }}</div>
|
||||||
|
<div class="tab-item target-tab">{{ completedPoints || 0 }}/{{ totalPoints || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<!-- 状态提示 -->
|
||||||
|
<div class="status-message" :class="statusMessageClass">
|
||||||
|
{{ statusMessage }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 游戏截图展示区域 -->
|
||||||
|
<div class="image-gallery">
|
||||||
|
<div class="image-item" v-if="assets?.homepageUrl">
|
||||||
|
<img :src="assets.homepageUrl" alt="首次主页" class="game-image" />
|
||||||
|
<div class="image-label">首次主页</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-item" v-if="assets?.firstRewardUrl">
|
||||||
|
<img :src="assets.firstRewardUrl" alt="首次赏金" class="game-image" style="transform: rotate(-90deg);" />
|
||||||
|
<div class="image-label">首次赏金</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-item" v-if="assets?.midRewardUrl">
|
||||||
|
<img :src="assets.midRewardUrl" alt="中途赏金" class="game-image" />
|
||||||
|
<div class="image-label">中途赏金</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-item" v-if="assets?.endRewardUrl">
|
||||||
|
<img :src="assets.endRewardUrl" alt="结束赏金" class="game-image" />
|
||||||
|
<div class="image-label">结束赏金</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部状态显示 -->
|
||||||
|
<div class="bottom-status">
|
||||||
|
ss{{ currentPoints || 0 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'GamePage',
|
||||||
|
props: {
|
||||||
|
region: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
displayStatus: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
completedPoints: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
totalPoints: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
statusMessage: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
statusMessageClass: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
currentPoints: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.game-page {
|
||||||
|
flex: 1;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-header {
|
||||||
|
display: flex;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border-bottom-color: #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.status-tab {
|
||||||
|
background: #2196F3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.target-tab {
|
||||||
|
color: #f44336;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
border-left: 4px solid #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message-completed {
|
||||||
|
background: #d4edda !important;
|
||||||
|
color: #155724 !important;
|
||||||
|
border-left: 4px solid #28a745 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-image {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 150px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-status {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
margin: 0 -16px -16px -16px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.tab-item {
|
||||||
|
padding: 10px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-item {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-image {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-label {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
43
src/components/play/LoadingOverlay.vue
Normal file
43
src/components/play/LoadingOverlay.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="loading-overlay">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>加载中...</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'LoadingOverlay'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
137
src/components/play/RefreshWaitPage.vue
Normal file
137
src/components/play/RefreshWaitPage.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<div class="refresh-wait-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="title">请选择您的账号类型</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="refresh-container">
|
||||||
|
<div class="warning-icon">⚠️</div>
|
||||||
|
<p class="refresh-text">页面需要刷新</p>
|
||||||
|
<p class="refresh-desc">请等待后重新选择区域</p>
|
||||||
|
<button
|
||||||
|
@click="$emit('refresh')"
|
||||||
|
class="refresh-btn"
|
||||||
|
:disabled="refreshCooldown > 0"
|
||||||
|
>
|
||||||
|
{{ refreshCooldown > 0 ? `请等待 ${refreshCooldown}s` : '确定' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notice-text">
|
||||||
|
<p>代理技术代练平台操作中</p>
|
||||||
|
<p>绝对安全保障!请耐心等待</p>
|
||||||
|
<p>温馨提示: 请选择正确区域</p>
|
||||||
|
<p v-if="mecmachineId" class="machine-id-info">机器ID: {{ mecmachineId }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'RefreshWaitPage',
|
||||||
|
props: {
|
||||||
|
refreshCooldown: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
mecmachineId: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['refresh']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.refresh-wait-page {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-text {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-desc {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
background: white;
|
||||||
|
color: #667eea;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 32px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover:not(:disabled) {
|
||||||
|
background: #f0f0f0;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-text {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-text p {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-id-info {
|
||||||
|
color: #4CAF50 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
295
src/components/play/ScanPage.vue
Normal file
295
src/components/play/ScanPage.vue
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
<template>
|
||||||
|
<div class="scan-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="title">请选择您的账号类型</h1>
|
||||||
|
<div class="selected-region">{{ regionName }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 等待二维码 -->
|
||||||
|
<div v-if="isWaitingQr" class="qr-waiting">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p class="waiting-text">正在准备二维码...</p>
|
||||||
|
<p class="waiting-desc">预计等待 {{ qrDelaySeconds }} 秒</p>
|
||||||
|
<p v-if="qrRetryCount > 0" class="retry-info">重试中... ({{ qrRetryCount }}/{{ maxQrRetries }})</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 二维码区域 -->
|
||||||
|
<div v-else-if="qrInfo && countdown > 0" class="qr-container">
|
||||||
|
<div class="qr-wrapper">
|
||||||
|
<img :src="qrInfo.url" class="qr-code" alt="扫码登录" @error="$emit('qrImageError', $event)" />
|
||||||
|
</div>
|
||||||
|
<div class="countdown-timer">{{ formatTime(countdown) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 二维码获取失败 -->
|
||||||
|
<div v-else-if="qrError" class="qr-error">
|
||||||
|
<div class="error-icon">❌</div>
|
||||||
|
<p class="error-text">二维码获取失败</p>
|
||||||
|
<p class="error-desc">{{ qrError }}</p>
|
||||||
|
<button
|
||||||
|
@click="$emit('retryQrCode')"
|
||||||
|
class="retry-btn"
|
||||||
|
:disabled="submitting"
|
||||||
|
>
|
||||||
|
重新获取
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 二维码过期 -->
|
||||||
|
<div v-else class="qr-expired">
|
||||||
|
<div class="warning-icon">⚠️</div>
|
||||||
|
<p class="expired-text">扫码超时{{ qrInfo?.url }}</p>
|
||||||
|
<p class="expired-desc">请手动刷新页面重新获取二维码</p>
|
||||||
|
<img :src="qrInfo?.url" class="qr-code" alt="扫码登录" />
|
||||||
|
<button
|
||||||
|
@click="$emit('pageRefresh')"
|
||||||
|
class="refresh-btn"
|
||||||
|
>
|
||||||
|
刷新页面
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notice-text">
|
||||||
|
<p>代理技术代练平台操作中</p>
|
||||||
|
<p>绝对安全保障!请耐心等待</p>
|
||||||
|
<p>温馨提示: 请选择正确区域</p>
|
||||||
|
<p v-if="mecmachineId" class="machine-id-info">机器ID: {{ mecmachineId }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ScanPage',
|
||||||
|
props: {
|
||||||
|
regionName: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isWaitingQr: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
qrInfo: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
countdown: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
qrDelaySeconds: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
qrRetryCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
maxQrRetries: {
|
||||||
|
type: Number,
|
||||||
|
default: 3
|
||||||
|
},
|
||||||
|
qrError: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
submitting: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
mecmachineId: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['qrImageError', 'retryQrCode', 'pageRefresh'],
|
||||||
|
methods: {
|
||||||
|
formatTime(seconds) {
|
||||||
|
const mins = Math.floor(seconds / 60)
|
||||||
|
const secs = seconds % 60
|
||||||
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.scan-page {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-region {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-waiting {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-text {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-desc {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-info {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
color: #ffd700;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-wrapper {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.countdown-timer {
|
||||||
|
color: white;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-expired, .qr-error {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-icon, .error-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-text, .error-text {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expired-desc, .error-desc {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn, .retry-btn {
|
||||||
|
background: white;
|
||||||
|
color: #667eea;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 32px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover:not(:disabled) {
|
||||||
|
background: #f0f0f0;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-text {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-text p {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-id-info {
|
||||||
|
color: #4CAF50 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.qr-code {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
215
src/components/play/SelectRegion.vue
Normal file
215
src/components/play/SelectRegion.vue
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<template>
|
||||||
|
<div class="select-region-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="title">请选择您的账号类型</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="region-buttons">
|
||||||
|
<button
|
||||||
|
@click="$emit('selectRegion', 'Q')"
|
||||||
|
class="region-btn qq-btn"
|
||||||
|
:disabled="submitting"
|
||||||
|
:class="{ 'loading': submitting }"
|
||||||
|
>
|
||||||
|
<div v-if="submitting" class="loading-spinner small"></div>
|
||||||
|
<div v-else class="btn-icon">Q</div>
|
||||||
|
<span class="btn-text">{{ submitting ? '正在连接...' : 'QQ区' }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="$emit('selectRegion', 'V')"
|
||||||
|
class="region-btn wx-btn"
|
||||||
|
:disabled="submitting"
|
||||||
|
:class="{ 'loading': submitting }"
|
||||||
|
>
|
||||||
|
<div v-if="submitting" class="loading-spinner small"></div>
|
||||||
|
<div v-else class="btn-icon">V</div>
|
||||||
|
<span class="btn-text">{{ submitting ? '正在连接...' : '微信区' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notice-text">
|
||||||
|
<p>代理技术代练平台操作中</p>
|
||||||
|
<p>绝对安全保障!请耐心等待</p>
|
||||||
|
<p>温馨提示: 请选择正确区域</p>
|
||||||
|
<p v-if="mecmachineId" class="machine-id-info">机器ID: {{ mecmachineId }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'SelectRegion',
|
||||||
|
props: {
|
||||||
|
submitting: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
mecmachineId: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['selectRegion']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.select-region-page {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-buttons {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 40px;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-btn {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: none;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-btn:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-btn.loading {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-btn.loading:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qq-btn .btn-icon {
|
||||||
|
background: #12B7F5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wx-btn .btn-icon {
|
||||||
|
background: #07C160;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #667eea;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner.small {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #f3f3f3;
|
||||||
|
border-top: 2px solid #667eea;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-text {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-text p {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-id-info {
|
||||||
|
color: #4CAF50 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.region-buttons {
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.region-btn {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
437
src/composables/usePlayState.js
Normal file
437
src/composables/usePlayState.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
185
src/composables/useQrCode.js
Normal file
185
src/composables/useQrCode.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/composables/useTimers.js
Normal file
108
src/composables/useTimers.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
1738
src/views/Play.vue
1738
src/views/Play.vue
File diff suppressed because it is too large
Load Diff
1696
src/views/oldplay.vue
Normal file
1696
src/views/oldplay.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user