topfans/frontend/pages/tasks/guide.vue

541 lines
12 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">
<!-- 背景图片 -->
<image class="background-image" src="/static/background/starbook.jpg" mode="aspectFill"></image>
<!-- 左上角返回按钮 -->
<view class="back-button" :style="{ top: backButtonTop }" @click="handleBack">
<image class="back-icon" src="/static/icon/back.png" mode="aspectFit" />
</view>
<!-- 页面标题 -->
<view class="page-title">新手引导</view>
<!-- 引导列表 -->
<view class="guide-list">
<view
v-for="item in guideList"
:key="item.key"
class="guide-item"
:class="{ 'in-progress': item.status === 'in_progress' }"
>
<view class="guide-item-header">
<view class="guide-status-badge" :class="item.status">
{{ item.statusText }}
</view>
<text class="guide-name">{{ item.name }}</text>
</view>
<view class="guide-item-body">
<text class="guide-desc">{{ item.desc }}</text>
</view>
<!-- 进度条(进行中状态显示) -->
<view v-if="item.status === 'in_progress'" class="guide-progress">
<view class="progress-bar">
<view
class="progress-fill"
:style="{ width: item.progress.percentage + '%' }"
></view>
</view>
<text class="progress-text">{{ item.progress.completed }}/{{ item.progress.total }} 步骤</text>
</view>
<view class="guide-item-footer">
<!-- 未开始 -->
<view
v-if="item.buttonType === 'start'"
class="guide-btn start-btn"
@click="handleStartGuide(item.key)"
>
{{ item.buttonText }}
</view>
<!-- 进行中 -->
<view
v-else-if="item.buttonType === 'continue'"
class="guide-btn continue-btn"
@click="handleContinueGuide(item.key)"
>
{{ item.buttonText }}
</view>
<!-- 已完成(可领取) -->
<view
v-else-if="item.buttonType === 'claim'"
class="guide-btn claim-btn"
@click="handleClaimReward(item.key)"
>
{{ item.buttonText }}
</view>
<!-- 已领取 -->
<view
v-else
class="guide-btn disabled-btn"
>
{{ item.buttonText }}
</view>
</view>
</view>
</view>
<!-- 底部统计 -->
<view class="guide-footer">
<view class="footer-stats">
已完成 {{ doneCount }}/{{ totalCount }}
</view>
<view v-if="claimableCount > 0" class="footer-claim-tip">
{{ claimableCount }} 个奖励可领取
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { getOnboardingStatus, advanceStage, claimOnboardingReward, completeGuide } from '@/utils/task-api.js'
import {
guideConfig,
getGuideConfig,
getGuideStatusList,
getStepProgress,
hasGuideProgress,
getNextIncompleteStep,
getStepPage,
clearSubStepProgress,
resetGuide,
claimGuideReward,
markGuideDone,
isGuideDone,
isGuideRewardClaimed,
onboardingStages
} from '@/utils/guideConfig.js'
const isAndroid = ref(false)
const backButtonTop = computed(() => {
if (isAndroid.value) {
return 'calc(env(safe-area-inset-top) + 80rpx)'
}
return 'calc(env(safe-area-inset-top) + 32rpx)'
})
const handleBack = () => {
// 获取页面栈
const pages = getCurrentPages();
if (pages.length > 1) {
// 有上一页,执行返回
uni.navigateBack();
} else {
uni.removeStorageSync('is_new_user')
// 没有上一页跳转到square页面
uni.reLaunch({
url: '/pages/square/square'
});
}
}
const loading = ref(true)
const guideList = ref([])
// 统计数据
const totalCount = computed(() => guideList.value.length)
const doneCount = computed(() => guideList.value.filter(item =>
item.status === 'completed' || item.status === 'reward_claimed'
).length)
const claimableCount = computed(() => guideList.value.filter(item => item.status === 'completed').length)
// 刷新引导列表
function refreshList() {
const rawList = getGuideStatusList()
guideList.value = rawList.map(item => {
const progress = getStepProgress(item.key)
const status = calculateStatus(item.key, item.done, item.claimed)
return {
...item,
progress,
status,
statusText: getStatusText(status, progress),
buttonText: getButtonText(status),
buttonType: getButtonType(status)
}
})
}
function calculateStatus(key, done, claimed) {
if (claimed) return 'reward_claimed'
if (done && !claimed) return 'completed'
if (hasGuideProgress(key)) return 'in_progress'
return 'not_started'
}
function getStatusText(status, progress) {
if (status === 'in_progress') {
return `${progress.completed}/${progress.total} 步骤`
}
const map = {
not_started: '未开始',
completed: '已完成',
reward_claimed: '已领取'
}
return map[status] || '未开始'
}
function getButtonText(status) {
const map = {
not_started: '开始',
in_progress: '继续',
completed: '领取奖励',
reward_claimed: '已领取'
}
return map[status] || '开始'
}
function getButtonType(status) {
const map = {
not_started: 'start',
in_progress: 'continue',
completed: 'claim',
reward_claimed: 'claimed'
}
return map[status] || 'start'
}
// 开始引导
function handleStartGuide(key) {
console.log('[guide] handleStartGuide:', key)
// 清除进度从头开始
clearSubStepProgress(key)
// 重置引导"已显示"状态,允许重新开始
resetGuide(key)
const targetPage = getStepPage(key, 0)
if (targetPage) {
uni.navigateTo({
url: targetPage + '?guide_key=' + key + '&guide_step=0'
})
}
}
// 继续引导
function handleContinueGuide(key) {
console.log('[guide] handleContinueGuide:', key)
// 从第一个未完成步骤继续
const resumeStep = getNextIncompleteStep(key)
const targetPage = getStepPage(key, resumeStep)
console.log('[guide] handleContinueGuide, resumeStep:', resumeStep, 'targetPage:', targetPage)
if (targetPage) {
uni.navigateTo({
url: targetPage + '?guide_key=' + key + '&guide_step=' + resumeStep
})
}
}
// 完成引导并同步后端
async function completeGuideAndSync(key) {
try {
// 标记本地已完成
markGuideDone(key)
// 同步到后端
await completeGuide(key, onboardingStages)
console.log('[guide] completeGuideAndSync success:', key)
} catch (err) {
console.error('[guide] completeGuideAndSync error:', err)
}
}
// 检查并同步所有完成的引导到后端
async function checkAndSyncCompletedGuides() {
const rawList = getGuideStatusList()
for (const item of rawList) {
if (item.done && !item.claimed) {
// 引导已完成但后端未同步
await completeGuideAndSync(item.key)
}
}
}
// 领取奖励
async function handleClaimReward(key) {
// 调用后端接口领取奖励
try {
// 找到对应的 stage
const config = getGuideConfig(key)
if (!config) return
// 从 onboardingStages 找到对应 stage
let stageNum = -1
for (const s of onboardingStages) {
if (s.required_task_keys.includes(key)) {
stageNum = s.stage
break
}
}
if (stageNum >= 0) {
const res = await claimOnboardingReward(stageNum)
// 更新本地存储的余额并通知 Header 组件
if (res.data?.crystal_balance !== undefined) {
try {
const userStr = uni.getStorageSync('user')
if (userStr) {
const user = JSON.parse(userStr)
user.crystal_balance = parseInt(res.data.crystal_balance)
uni.setStorageSync('user', JSON.stringify(user))
}
} catch (e) {
console.error('更新本地存储失败:', e)
}
uni.$emit('balanceUpdated', { crystal_balance: res.data.crystal_balance, experience: res.data.experience })
}
}
// 本地标记已领取
claimGuideReward(key)
// uni.showToast({
// title: `领取成功!${config.reward?.exp || 0}经验 ${config.reward?.diamond || 0}钻`,
// icon: 'none'
// })
refreshList()
} catch (err) {
console.error('handleClaimReward error:', err)
uni.showToast({ title: '领取失败', icon: 'none' })
}
}
// 初始化时同步后端
async function initBackend() {
try {
loading.value = true
// 检查后端是否有配置
const res = await getOnboardingStatus()
const data = res.data || {}
if (!data.stages || data.stages.length === 0) {
// 后端没有配置,用前端配置初始化
await completeGuide('init', onboardingStages)
} else {
// 后端有配置,根据后端的 stage 状态更新本地存储
// 遍历后端返回的 stages根据 allTasksCompleted 状态更新本地
for (const stage of data.stages) {
if (stage.allTasksCompleted && stage.required_task_keys) {
for (const taskKey of stage.required_task_keys) {
// 如果后端显示该任务已完成,标记本地为已完成
if (!isGuideDone(taskKey)) {
console.log('[Guide] 从后端同步任务完成状态:', taskKey)
markGuideDone(taskKey)
}
}
}
}
}
// 刷新列表
refreshList()
} catch (err) {
console.error('initBackend error:', err)
} finally {
loading.value = false
}
}
onMounted(() => {
const systemInfo = uni.getSystemInfoSync()
isAndroid.value = systemInfo.platform === 'android'
initBackend().then(() => {
// 初始化后检查并同步已完成的引导
checkAndSyncCompletedGuides()
refreshList()
})
})
</script>
<style scoped>
.guide-container {
position: relative;
min-height: 100vh;
padding: 20rpx;
padding-bottom: 200rpx;
}
.background-image {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.back-button {
position: fixed;
left: 32rpx;
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 101;
}
.back-icon {
width: 64rpx;
height: 64rpx;
}
.page-title {
position: relative;
z-index: 1;
font-size: 36rpx;
font-weight: bold;
color: #fff;
text-align: center;
padding: 20rpx 0;
}
.guide-list {
position: relative;
z-index: 1;
}
.guide-item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
}
.guide-item.in-progress {
border-left: 4rpx solid #4a90e2;
}
.guide-item-header {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.guide-status-badge {
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-size: 22rpx;
margin-right: 12rpx;
}
.guide-status-badge.not_started {
background: #f0f0f0;
color: #999;
}
.guide-status-badge.in_progress {
background: #e6f0ff;
color: #4a90e2;
}
.guide-status-badge.completed {
background: #fff3e6;
color: #ff9500;
}
.guide-status-badge.reward_claimed {
background: #e8f5e9;
color: #4caf50;
}
.guide-name {
font-size: 30rpx;
font-weight: 500;
color: #333;
flex: 1;
}
.guide-desc {
font-size: 26rpx;
color: #666;
margin-bottom: 16rpx;
}
.guide-progress {
margin-bottom: 16rpx;
}
.progress-bar {
height: 8rpx;
background: #eee;
border-radius: 4rpx;
overflow: hidden;
margin-bottom: 8rpx;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
border-radius: 4rpx;
transition: width 0.3s ease;
}
.progress-text {
font-size: 22rpx;
color: #999;
}
.guide-item-footer {
display: flex;
justify-content: flex-end;
}
.guide-btn {
padding: 12rpx 32rpx;
color: #fff;
border-radius: 30rpx;
font-size: 26rpx;
}
.start-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.continue-btn {
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%);
}
.claim-btn {
background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
}
.disabled-btn {
background: #ddd;
color: #999;
}
.guide-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 24rpx 32rpx;
background: #fff;
border-top: 1rpx solid #eee;
z-index: 10;
}
.footer-stats {
font-size: 28rpx;
color: #333;
text-align: center;
margin-bottom: 8rpx;
}
.footer-claim-tip {
font-size: 26rpx;
color: #ff6b6b;
text-align: center;
}
</style>