topfans/frontend/pages/tasks/guide.vue

425 lines
9.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="guide-container">
<!-- 顶部导航 -->
<Header :show-back="true" back-icon-color="#e6e6e6" :show-guide-icon="false" :show-task-icon="false" :show-star-activity-icon="false" />
<!-- 页面标题 -->
<view class="page-title">新手引导</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-state">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 错误状态 -->
<view v-else-if="errorMessage" class="error-state">
<text class="error-text">{{ errorMessage }}</text>
<button class="retry-btn" @click="loadStatus">重试</button>
</view>
<!-- 引导内容 -->
<view v-else class="guide-content">
<!-- 当前阶段信息 -->
<view class="current-stage-card">
<view class="stage-header">
<text class="stage-label">当前阶段</text>
<text class="stage-number">第 {{ currentStage }} 阶段</text>
</view>
<view class="stage-name">{{ currentStageName }}</view>
<view class="stage-reward" v-if="currentStageReward > 0">
<text class="reward-icon">💎</text>
<text class="reward-value">{{ currentStageReward }}</text>
<text class="reward-sep">|</text>
<text class="reward-icon">⭐</text>
<text class="reward-value">{{ currentStageExp }} 经验</text>
</view>
</view>
<!-- 阶段列表 -->
<view class="stages-list">
<view v-for="stage in stages" :key="stage.stage" class="stage-item" :class="{ 'is-current': stage.is_current, 'is-completed': stage.status === 'completed' }">
<view class="stage-item-header">
<view class="stage-item-name">
<text v-if="stage.status === 'completed'" class="check-icon">✓</text>
<text>{{ stage.name }}</text>
</view>
<view v-if="stage.is_current" class="current-tag">进行中</view>
</view>
<!-- 该阶段的任务列表 -->
<view class="task-list-mini">
<view v-for="taskKey in stage.required_task_keys" :key="taskKey" class="task-key-item">
<text class="task-check" :class="{ 'is-done': isTaskCompleted(stage, taskKey) }">
{{ isTaskCompleted(stage, taskKey) ? '✓' : '○' }}
</text>
<text class="task-key-name">{{ taskKey }}</text>
</view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-bar">
<!-- 进入下一阶段按钮 -->
<button
v-if="canAdvance"
class="advance-btn"
:loading="advancing"
@click="handleAdvance"
>
进入下一阶段
</button>
<view v-else-if="!allCurrentTasksDone" class="advance-hint">
完成当前阶段所有任务后可进入下一阶段
</view>
<!-- 领取奖励按钮 -->
<button
v-if="canClaimReward"
class="claim-btn"
:loading="claimingReward"
@click="handleClaimReward"
>
领取奖励
</button>
</view>
</view>
<!-- 下拉刷新 -->
<view v-if="!loading" class="pull-to-refresh" @click="handleRefresh">
<text class="refresh-text">{{ refreshing ? '刷新中...' : '下拉刷新' }}</text>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import Header from '../components/Header.vue'
import { getOnboardingStatus, advanceStage, claimOnboardingReward } from '@/utils/task-api.js'
const loading = ref(true)
const refreshing = ref(false)
const errorMessage = ref('')
const currentStage = ref(0)
const currentStatus = ref('')
const stages = ref([])
const advancing = ref(false)
const claimingReward = ref(false)
// 当前阶段配置
const currentStageConfig = computed(() => stages.value.find(s => s.is_current))
const currentStageName = computed(() => currentStageConfig.value?.name || '新手引导')
const currentStageReward = computed(() => currentStageConfig.value?.crystal_reward || 0)
const currentStageExp = computed(() => currentStageConfig.value?.exp_reward || 0)
// 当前阶段所有任务是否完成
const currentTasksDone = computed(() => {
if (!currentStageConfig.value) return false
return currentStageConfig.value.required_task_keys.every(key =>
isTaskCompleted(currentStageConfig.value, key)
)
})
const allCurrentTasksDone = computed(() => currentTasksDone.value)
const canAdvance = computed(() => currentStatus.value === 'in_progress' && currentTasksDone.value)
const canClaimReward = computed(() => currentStatus.value === 'in_progress' && currentTasksDone.value)
// 检查某个阶段的任务是否完成(简化:直接从 stages 状态判断)
function isTaskCompleted(stage, taskKey) {
// 这里需要根据实际数据结构判断
// 假设 completed 状态的任务 key 会被标记
// 暂时返回 false实际需要后端返回更详细的状态
return false
}
async function loadStatus() {
try {
loading.value = true
errorMessage.value = ''
const res = await getOnboardingStatus()
const data = res.data || {}
currentStage.value = data.current_stage || 0
currentStatus.value = data.status || 'pending'
stages.value = data.stages || []
} catch (err) {
console.error('loadStatus error:', err)
errorMessage.value = err.message || '加载失败'
} finally {
loading.value = false
refreshing.value = false
}
}
async function handleAdvance() {
if (advancing.value) return
try {
advancing.value = true
const nextStage = currentStage.value + 1
await advanceStage(nextStage)
await loadStatus()
uni.showToast({ title: '已进入下一阶段', icon: 'success' })
} catch (err) {
console.error('handleAdvance error:', err)
uni.showToast({ title: err.message || '进入下一阶段失败', icon: 'none' })
} finally {
advancing.value = false
}
}
async function handleClaimReward() {
if (claimingReward.value) return
try {
claimingReward.value = true
await claimOnboardingReward(currentStage.value)
await loadStatus()
uni.showToast({ title: '奖励领取成功', icon: 'success' })
} catch (err) {
console.error('handleClaimReward error:', err)
uni.showToast({ title: err.message || '领取失败', icon: 'none' })
} finally {
claimingReward.value = false
}
}
function handleRefresh() {
refreshing.value = true
loadStatus()
}
onMounted(() => {
loadStatus()
})
</script>
<style scoped>
.guide-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.page-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
text-align: center;
padding: 20rpx 0;
}
.loading-state,
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #e0e0e0;
border-top-color: #6c5ce7;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text,
.error-text {
color: #999;
font-size: 28rpx;
}
.retry-btn {
margin-top: 20rpx;
padding: 16rpx 40rpx;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
color: #fff;
border-radius: 50rpx;
font-size: 28rpx;
}
.guide-content {
padding: 20rpx;
}
.current-stage-card {
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
border-radius: 20rpx;
padding: 40rpx;
margin-bottom: 30rpx;
color: #fff;
}
.stage-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.stage-label {
font-size: 28rpx;
opacity: 0.8;
}
.stage-number {
font-size: 28rpx;
font-weight: bold;
}
.stage-name {
font-size: 40rpx;
font-weight: bold;
margin-bottom: 20rpx;
}
.stage-reward {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 28rpx;
}
.reward-icon {
font-size: 28rpx;
}
.reward-value {
font-weight: bold;
}
.reward-sep {
opacity: 0.5;
margin: 0 10rpx;
}
.stages-list {
margin-bottom: 30rpx;
}
.stage-item {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.stage-item.is-current {
border: 2rpx solid #6c5ce7;
}
.stage-item.is-completed {
opacity: 0.7;
}
.stage-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.stage-item-name {
display: flex;
align-items: center;
gap: 10rpx;
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.check-icon {
color: #52c41a;
font-size: 32rpx;
}
.current-tag {
padding: 6rpx 16rpx;
background: #6c5ce7;
color: #fff;
border-radius: 20rpx;
font-size: 22rpx;
}
.task-list-mini {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.task-key-item {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
background: #f5f5f5;
border-radius: 20rpx;
font-size: 24rpx;
}
.task-check {
color: #ddd;
font-size: 24rpx;
}
.task-check.is-done {
color: #52c41a;
}
.task-key-name {
color: #666;
}
.action-bar {
padding: 20rpx 0;
}
.advance-btn {
width: 100%;
padding: 24rpx 0;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
color: #fff;
border-radius: 50rpx;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
}
.advance-hint {
text-align: center;
color: #999;
font-size: 26rpx;
padding: 20rpx 0;
}
.claim-btn {
width: 100%;
padding: 24rpx 0;
background: linear-gradient(135deg, #ffd700, #ffb347);
color: #333;
border-radius: 50rpx;
font-size: 32rpx;
font-weight: bold;
}
.pull-to-refresh {
display: flex;
justify-content: center;
padding: 30rpx 0;
}
.refresh-text {
color: #999;
font-size: 24rpx;
}
</style>