1248 lines
28 KiB
Vue
1248 lines
28 KiB
Vue
<template>
|
||
<div class="play-container">
|
||
<!-- 加载状态 -->
|
||
<div v-if="state.loading" class="loading-overlay">
|
||
<div class="loading-spinner"></div>
|
||
<p>加载中...</p>
|
||
</div>
|
||
|
||
<!-- 选区界面 -->
|
||
<div v-else-if="state.status === 'NEW' && !state.needRefresh" class="select-region-page">
|
||
<div class="page-header">
|
||
<h1 class="title">请选择您的账号类型</h1>
|
||
</div>
|
||
|
||
<div class="region-buttons">
|
||
<button
|
||
@click="selectRegion('Q')"
|
||
class="region-btn qq-btn"
|
||
:disabled="state.submitting"
|
||
>
|
||
<div class="btn-icon">Q</div>
|
||
<span class="btn-text">QQ区</span>
|
||
</button>
|
||
|
||
<button
|
||
@click="selectRegion('V')"
|
||
class="region-btn wx-btn"
|
||
:disabled="state.submitting"
|
||
>
|
||
<div class="btn-icon">V</div>
|
||
<span class="btn-text">微信区</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="notice-text">
|
||
<p>代理技术代练平台操作中</p>
|
||
<p>绝对安全保障!请耐心等待</p>
|
||
<p>温馨提示: 请选择正确区域</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 扫码界面 -->
|
||
<div v-else-if="state.status === 'USING'" class="scan-page">
|
||
<div class="page-header">
|
||
<h1 class="title">请选择您的账号类型</h1>
|
||
<div class="selected-region">{{ getRegionName() }}</div>
|
||
</div>
|
||
|
||
<!-- 等待二维码 -->
|
||
<div v-if="state.isWaitingQr" class="qr-waiting">
|
||
<div class="loading-spinner"></div>
|
||
<p class="waiting-text">正在准备二维码...</p>
|
||
<p class="waiting-desc">预计等待 {{ state.qrDelaySeconds }} 秒</p>
|
||
</div>
|
||
|
||
<!-- 二维码区域 -->
|
||
<div v-else-if="state.qrInfo && countdown > 0" class="qr-container">
|
||
<div class="qr-wrapper">
|
||
<img :src="state.qrInfo.url" class="qr-code" alt="扫码登录" />
|
||
</div>
|
||
<div class="countdown-timer">{{ formatTime(countdown) }}</div>
|
||
</div>
|
||
|
||
<!-- 二维码过期 -->
|
||
<div v-else class="qr-expired">
|
||
<div class="warning-icon">⚠️</div>
|
||
<p class="expired-text">扫码超时{{ state.qrInfo.url}}</p>
|
||
<p class="expired-desc">请手动刷新页面重新获取二维码</p>
|
||
<img :src="state.qrInfo.url" class="qr-code" alt="扫码登录" />
|
||
<button
|
||
@click="handlePageRefresh"
|
||
class="refresh-btn"
|
||
>
|
||
刷新页面
|
||
</button>
|
||
</div>
|
||
|
||
<div class="notice-text">
|
||
<p>代理技术代练平台操作中</p>
|
||
<p>绝对安全保障!请耐心等待</p>
|
||
<p>温馨提示: 请选择正确区域</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 二界面 -->
|
||
<div v-else-if="state.status === 'LOGGED_IN'" class="game-page">
|
||
<div class="game-header">
|
||
<div class="browser-bar">
|
||
<span class="url">{{ getCurrentUrl() }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="game-content">
|
||
<!-- 大厅选择显示 -->
|
||
<div class="hall-info">
|
||
<span class="hall-label">选择大厅:</span>
|
||
<span class="hall-value">{{ getRegionName() }}</span>
|
||
</div>
|
||
|
||
<!-- 状态显示 -->
|
||
<div class="status-info">
|
||
<span class="status-label">状态:</span>
|
||
<span class="status-value" :class="getStatusClass()">{{ getGameStatus() }}</span>
|
||
</div>
|
||
|
||
<!-- 目标点数进度 -->
|
||
<div class="progress-section">
|
||
<div class="progress-header">
|
||
<span class="progress-label">目标点数</span>
|
||
<span class="progress-value">{{ state.currentPoints || 0 }}/{{ state.totalPoints || 1000 }}</span>
|
||
</div>
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" :style="{ width: getProgressPercent() + '%' }"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 游戏截图区域 -->
|
||
<div class="game-images" v-if="state.assets">
|
||
<div class="image-container">
|
||
<img
|
||
v-if="getCurrentGameImage()"
|
||
:src="getCurrentGameImage()"
|
||
class="game-screenshot"
|
||
alt="游戏截图"
|
||
/>
|
||
<div v-else class="placeholder-image">
|
||
<p>正在上号中,请稍等...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮区域 -->
|
||
<div class="game-actions">
|
||
<button class="action-btn completed-btn">已完成</button>
|
||
<button class="action-btn current-btn">进行中</button>
|
||
<button class="action-btn pending-btn">未开始</button>
|
||
</div>
|
||
|
||
<!-- 底部信息 -->
|
||
<div class="bottom-info">
|
||
<p class="safe-text">安全可靠的代练服务</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 刷新等待界面 -->
|
||
<div v-else-if="state.needRefresh" 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="handleRefresh"
|
||
class="refresh-btn"
|
||
:disabled="refreshCooldown > 0"
|
||
>
|
||
{{ refreshCooldown > 0 ? `请等待 ${refreshCooldown}s` : '确定' }}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="notice-text">
|
||
<p>代理技术代练平台操作中</p>
|
||
<p>绝对安全保障!请耐心等待</p>
|
||
<p>温馨提示: 请选择正确区域</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 错误/不可用界面 -->
|
||
<div v-else class="error-page">
|
||
<div class="error-container">
|
||
<div class="error-icon">❌</div>
|
||
<h2 class="error-title">{{ getErrorTitle() }}</h2>
|
||
<p class="error-message">{{ getErrorMessage() }}</p>
|
||
<button @click="handleRetry" class="retry-btn">重新尝试</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { reactive, ref, onMounted, onUnmounted } from 'vue'
|
||
import { useRoute } 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 default {
|
||
name: 'Play',
|
||
setup() {
|
||
const route = useRoute()
|
||
|
||
// 状态管理
|
||
const state = reactive({
|
||
code: '',
|
||
status: 'NEW',
|
||
loading: true,
|
||
submitting: false,
|
||
needRefresh: false,
|
||
region: null,
|
||
qrInfo: null,
|
||
assets: null,
|
||
currentPoints: 0,
|
||
totalPoints: 1000,
|
||
error: null,
|
||
qrDelaySeconds: 0,
|
||
isWaitingQr: false
|
||
})
|
||
|
||
// 计时器
|
||
const countdown = ref(0)
|
||
const refreshCooldown = ref(0)
|
||
const timers = reactive({
|
||
loginPoll: null,
|
||
countdown: null,
|
||
refreshCooldown: null
|
||
})
|
||
|
||
// 初始化
|
||
onMounted(() => {
|
||
const code = route.query.code
|
||
if (!code) {
|
||
state.error = 'INVALID_CODE'
|
||
state.loading = false
|
||
return
|
||
}
|
||
|
||
state.code = code
|
||
initializePage()
|
||
})
|
||
|
||
// 清理定时器
|
||
onUnmounted(() => {
|
||
clearAllTimers()
|
||
})
|
||
|
||
// 初始化页面
|
||
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)
|
||
|
||
// 根据状态启动相应的定时器
|
||
if (state.status === 'USING' && state.qrInfo) {
|
||
startCountdown()
|
||
startLoginPolling()
|
||
}
|
||
|
||
} 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'
|
||
state.assets = gameData.assets
|
||
|
||
// 从游戏接口数据中更新总点数和当前进度
|
||
if (gameData.assets && gameData.assets.totalPoints) {
|
||
state.totalPoints = gameData.assets.totalPoints
|
||
}
|
||
// 初始化当前点数为0(代表刚开始)
|
||
state.currentPoints = 0
|
||
|
||
clearTimer('loginPoll')
|
||
clearTimer('countdown')
|
||
ElMessage.success('登录成功,正在进入游戏界面...')
|
||
|
||
} catch (error) {
|
||
console.error('获取游戏界面数据失败:', error)
|
||
ElMessage.error('获取游戏数据失败,请稍后重试')
|
||
}
|
||
}
|
||
|
||
// 更新状态
|
||
const updateStateFromResponse = async (data, skipQrProcessing = false) => {
|
||
// 如果状态是LOGGED_IN,调用游戏接口获取详细数据
|
||
if (data.status === 'LOGGED_IN') {
|
||
await handleLoggedInStatus()
|
||
return
|
||
}
|
||
|
||
state.status = data.status
|
||
state.needRefresh = data.needRefresh || false
|
||
state.region = data.region
|
||
state.assets = data.assets
|
||
|
||
// 如果有游戏数据,更新点数信息
|
||
if (data.assets && data.assets.totalPoints) {
|
||
state.totalPoints = data.assets.totalPoints
|
||
// 如果没有当前进度,初始化为0
|
||
if (state.currentPoints === undefined) {
|
||
state.currentPoints = 0
|
||
}
|
||
}
|
||
|
||
// 调试信息
|
||
console.log('updateStateFromResponse:', {
|
||
status: data.status,
|
||
region: data.region,
|
||
qrCodeUrl: data.qrCodeUrl,
|
||
skipQrProcessing
|
||
})
|
||
|
||
// 如果需要跳过二维码处理(延迟处理),则不立即设置 qrInfo
|
||
if (skipQrProcessing) {
|
||
return
|
||
}
|
||
|
||
// 处理二维码信息
|
||
if (data.qrCodeUrl) {
|
||
state.qrInfo = {
|
||
url: data.qrCodeUrl,
|
||
createdAt: data.qrCreatedAt,
|
||
expireAt: data.qrExpireAt
|
||
}
|
||
|
||
const now = Date.now()
|
||
const expireTime = new Date(data.qrExpireAt).getTime()
|
||
countdown.value = Math.max(0, Math.floor((expireTime - now) / 1000))
|
||
} else if (data.qr) {
|
||
// 兼容旧的响应格式
|
||
state.qrInfo = data.qr
|
||
|
||
if (data.qr.expireAt) {
|
||
const now = Date.now()
|
||
const expireTime = data.qr.expireAt
|
||
countdown.value = Math.max(0, Math.floor((expireTime - now) / 1000))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 延迟获取二维码
|
||
const fetchQrCodeAfterDelay = async (qrCodeUrl, qrCreatedAt, qrExpireAt) => {
|
||
try {
|
||
// 这里可以添加额外的验证或处理逻辑
|
||
// 比如检查 URL 是否可访问
|
||
|
||
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))
|
||
|
||
ElMessage.success('二维码已准备就绪,请扫码登录')
|
||
} catch (error) {
|
||
console.error('获取二维码失败:', error)
|
||
ElMessage.error('二维码获取失败,请重试')
|
||
}
|
||
}
|
||
|
||
// 选择区域
|
||
const selectRegion = async (region) => {
|
||
if (state.submitting) return
|
||
|
||
state.submitting = true
|
||
|
||
try {
|
||
const response = await selectRegionAPI({ code: state.code, region })
|
||
const data = response.data
|
||
|
||
console.log('selectRegion 响应数据:', 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.status === 'USING') {
|
||
startCountdown()
|
||
startLoginPolling()
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
handleError(error)
|
||
} finally {
|
||
state.submitting = false
|
||
}
|
||
}
|
||
|
||
// 刷新
|
||
const handleRefresh = async () => {
|
||
if (refreshCooldown.value > 0) return
|
||
|
||
try {
|
||
const response = await refreshLinkAPI(state.code)
|
||
const data = response.data
|
||
|
||
if (data.waitSeconds) {
|
||
startRefreshCooldown(data.waitSeconds)
|
||
}
|
||
|
||
// 刷新成功后,重置状态
|
||
state.needRefresh = false
|
||
state.status = 'NEW'
|
||
clearTimer('loginPoll')
|
||
|
||
} catch (error) {
|
||
handleError(error)
|
||
}
|
||
}
|
||
|
||
// 手动刷新页面
|
||
const handlePageRefresh = () => {
|
||
window.location.reload()
|
||
}
|
||
|
||
// 开始倒计时
|
||
const startCountdown = () => {
|
||
clearTimer('countdown')
|
||
|
||
timers.countdown = setInterval(() => {
|
||
if (countdown.value > 0) {
|
||
countdown.value--
|
||
} else {
|
||
clearTimer('countdown')
|
||
clearTimer('loginPoll')
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
// 开始登录轮询
|
||
const startLoginPolling = () => {
|
||
clearTimer('loginPoll')
|
||
|
||
timers.loginPoll = setInterval(async () => {
|
||
try {
|
||
const response = await pollLoginStatus(state.code)
|
||
const data = response.data
|
||
|
||
console.log('poll-login 响应数据:', data)
|
||
|
||
// 处理登录成功的情况(无论success是true还是false)
|
||
if (data.status === 'LOGGED_IN') {
|
||
await handleLoggedInStatus()
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('轮询错误:', error)
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
// 开始进度轮询
|
||
const startProgressPolling = () => {
|
||
const pollProgress = async () => {
|
||
try {
|
||
const response = await getGameProgress(state.code)
|
||
const data = response.data
|
||
state.currentPoints = data.currentPoints || state.currentPoints
|
||
state.totalPoints = data.totalPoints || state.totalPoints
|
||
} catch (error) {
|
||
console.error('进度轮询错误:', error)
|
||
}
|
||
}
|
||
|
||
// 立即执行一次
|
||
pollProgress()
|
||
|
||
// 每5秒轮询一次进度
|
||
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)
|
||
}
|
||
|
||
// 清理定时器
|
||
const clearTimer = (name) => {
|
||
if (timers[name]) {
|
||
clearInterval(timers[name])
|
||
timers[name] = null
|
||
}
|
||
}
|
||
|
||
const clearAllTimers = () => {
|
||
Object.keys(timers).forEach(clearTimer)
|
||
}
|
||
|
||
// 错误处理
|
||
const handleError = (error) => {
|
||
console.error('API错误:', error)
|
||
|
||
// 获取HTTP状态码
|
||
const status = error?.response?.status
|
||
|
||
// 根据HTTP状态码设置错误状态
|
||
if (status === 400 || status === 401 || status === 403) {
|
||
state.error = 'INVALID_CODE'
|
||
} else if (status === 410) {
|
||
state.error = 'EXPIRED'
|
||
} else {
|
||
// 对于其他错误,显示通用错误状态
|
||
state.error = 'NETWORK_ERROR'
|
||
}
|
||
|
||
// HTTP拦截器已经处理了错误消息显示,这里只需要设置页面错误状态
|
||
}
|
||
|
||
// 重试
|
||
const handleRetry = () => {
|
||
state.error = null
|
||
state.loading = true
|
||
initializePage()
|
||
}
|
||
|
||
// 工具函数
|
||
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 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,
|
||
countdown,
|
||
refreshCooldown,
|
||
selectRegion,
|
||
handleRefresh,
|
||
handlePageRefresh,
|
||
handleRetry,
|
||
formatTime,
|
||
getRegionName,
|
||
getCurrentUrl,
|
||
getGameStatus,
|
||
getStatusClass,
|
||
getProgressPercent,
|
||
getCurrentGameImage,
|
||
getErrorTitle,
|
||
getErrorMessage
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.play-container {
|
||
min-height: 100vh;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.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); }
|
||
}
|
||
|
||
/* 页面头部 */
|
||
.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);
|
||
}
|
||
|
||
/* 选区界面 */
|
||
.select-region-page {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 扫码界面 */
|
||
.scan-page {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.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 {
|
||
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;
|
||
}
|
||
|
||
.expired-text {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
margin: 0 0 8px 0;
|
||
}
|
||
|
||
.expired-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;
|
||
}
|
||
|
||
/* 二界面 */
|
||
.game-page {
|
||
flex: 1;
|
||
background: white;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.game-header {
|
||
background: #f8f9fa;
|
||
padding: 12px;
|
||
border-bottom: 1px solid #e9ecef;
|
||
}
|
||
|
||
.browser-bar {
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 20px;
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
text-align: center;
|
||
}
|
||
|
||
.game-content {
|
||
flex: 1;
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.hall-info, .status-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid #e9ecef;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.hall-label, .status-label {
|
||
color: #666;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.hall-value {
|
||
color: #333;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.status-value {
|
||
font-weight: 600;
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.status-completed {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.status-playing {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
|
||
.status-idle {
|
||
background: #d1ecf1;
|
||
color: #0c5460;
|
||
}
|
||
|
||
.progress-section {
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.progress-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.progress-label {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.progress-value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #667eea;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 8px;
|
||
background: #e9ecef;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||
border-radius: 4px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.game-images {
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.image-container {
|
||
background: #f8f9fa;
|
||
border: 1px solid #e9ecef;
|
||
border-radius: 12px;
|
||
min-height: 200px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.game-screenshot {
|
||
max-width: 100%;
|
||
max-height: 300px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.placeholder-image {
|
||
color: #666;
|
||
text-align: center;
|
||
padding: 40px;
|
||
}
|
||
|
||
.game-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.action-btn {
|
||
flex: 1;
|
||
padding: 12px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.completed-btn {
|
||
background: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.current-btn {
|
||
background: #ffc107;
|
||
color: #212529;
|
||
}
|
||
|
||
.pending-btn {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
|
||
.bottom-info {
|
||
text-align: center;
|
||
padding: 20px 0;
|
||
border-top: 1px solid #e9ecef;
|
||
margin-top: auto;
|
||
}
|
||
|
||
.safe-text {
|
||
color: #666;
|
||
font-size: 14px;
|
||
margin: 0;
|
||
}
|
||
|
||
/* 刷新等待界面 */
|
||
.refresh-wait-page {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.refresh-container {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
padding: 0 20px;
|
||
color: white;
|
||
text-align: center;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 错误界面 */
|
||
.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);
|
||
}
|
||
|
||
/* 通知文本 */
|
||
.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;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@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;
|
||
}
|
||
|
||
.qr-code {
|
||
width: 150px;
|
||
height: 150px;
|
||
}
|
||
|
||
.game-actions {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
</style>
|