style: 修改每日任务和引导任务的样式
This commit is contained in:
parent
a0dae45edb
commit
dd838fccc4
@ -82,9 +82,8 @@
|
||||
<!-- 任务弹窗 -->
|
||||
<DailyTasks :visible="showTaskModal" @close="showTaskModal = false" />
|
||||
|
||||
<!-- 新手引导列表弹窗 -->
|
||||
<GuideListModal :visible="showGuideListModal" @start-guide="handleStartGuide"
|
||||
@claim-success="handleClaimSuccess" @close="showGuideListModal = false" />
|
||||
<!-- 新手引导弹窗 -->
|
||||
<GuideModal :visible="showGuideModal" @close="showGuideModal = false" @updated="handleGuideUpdated" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -93,7 +92,7 @@ import { computed, ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import Avatar from './Avatar.vue';
|
||||
import DailyTasks from '@/pages/tasks/daily-tasks.vue';
|
||||
import GuideListModal from '@/components/GuideListModal.vue';
|
||||
import GuideModal from '@/pages/tasks/GuideModal.vue';
|
||||
import { getActivityListApi } from '@/utils/api.js';
|
||||
import { reportEvent } from '@/utils/task-api.js';
|
||||
|
||||
@ -135,7 +134,7 @@ const avatarKey = ref(0); // 用于强制刷新Avatar组件
|
||||
const showTaskModal = ref(false);
|
||||
|
||||
// 新手引导弹窗显示状态
|
||||
const showGuideListModal = ref(false);
|
||||
const showGuideModal = ref(false);
|
||||
|
||||
// 从本地存储读取用户信息
|
||||
const loadUserInfo = () => {
|
||||
@ -308,9 +307,13 @@ const handleStarActivityClick = async () => {
|
||||
|
||||
// 点击新手引导
|
||||
const handleGuideClick = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/tasks/guide'
|
||||
});
|
||||
showGuideModal.value = true;
|
||||
};
|
||||
|
||||
// 引导更新回调
|
||||
const handleGuideUpdated = () => {
|
||||
// 可以在这里刷新相关数据
|
||||
console.log('[Header] Guide updated');
|
||||
};
|
||||
|
||||
// 执行引导
|
||||
|
||||
744
frontend/pages/tasks/GuideModal.vue
Normal file
744
frontend/pages/tasks/GuideModal.vue
Normal file
@ -0,0 +1,744 @@
|
||||
<template>
|
||||
<view v-if="visible" class="guide-modal-wrapper" @touchmove.stop.prevent="handlePreventMove">
|
||||
<!-- 背景遮罩 -->
|
||||
<view class="modal-mask" @click="handleClose"></view>
|
||||
|
||||
<!-- 弹窗容器 -->
|
||||
<view class="modal-container" @click.stop>
|
||||
<!-- 背景图片 -->
|
||||
<image class="modal-background" src="/static/nft/beijingban.png" mode="aspectFill"></image>
|
||||
|
||||
<!-- 蒙层 -->
|
||||
<view class="modal-overlay"></view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="modal-content">
|
||||
<!-- 顶部区域:返回按钮和标题 -->
|
||||
<view class="modal-header">
|
||||
<!-- 返回按钮 -->
|
||||
<view class="back-button" @click="handleClose">
|
||||
<image class="back-icon" src="/static/icon/back.png" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<view class="modal-title">新手引导</view>
|
||||
</view>
|
||||
|
||||
<!-- 引导列表 -->
|
||||
<scroll-view class="modal-body" scroll-y :show-scrollbar="false">
|
||||
<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>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部统计 -->
|
||||
<view class="modal-footer">
|
||||
<view class="footer-stats">
|
||||
已完成 {{ doneCount }}/{{ totalCount }}
|
||||
</view>
|
||||
<view v-if="claimableCount > 0" class="footer-claim-tip">
|
||||
有 {{ claimableCount }} 个奖励可领取
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { getOnboardingStatus, claimOnboardingReward, completeGuide } from '@/utils/task-api.js'
|
||||
import {
|
||||
getGuideConfig,
|
||||
getGuideStatusList,
|
||||
getStepProgress,
|
||||
hasGuideProgress,
|
||||
getNextIncompleteStep,
|
||||
getStepPage,
|
||||
clearSubStepProgress,
|
||||
resetGuide,
|
||||
claimGuideReward,
|
||||
markGuideDone,
|
||||
onboardingStages
|
||||
} from '@/utils/guideConfig.js'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'updated'])
|
||||
|
||||
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)
|
||||
|
||||
// 保存滚动位置和页面状态
|
||||
let scrollTop = 0
|
||||
let bodyOverflow = ''
|
||||
let pageScrollEnabled = true
|
||||
|
||||
// 锁定背景页面滚动
|
||||
function lockBodyScroll() {
|
||||
// 获取当前页面滚动位置
|
||||
uni.createSelectorQuery().selectViewport().scrollOffset(res => {
|
||||
if (res) {
|
||||
scrollTop = res.scrollTop || 0
|
||||
}
|
||||
}).exec()
|
||||
|
||||
// #ifdef H5
|
||||
// H5 平台:锁定 body 滚动
|
||||
const body = document.body
|
||||
bodyOverflow = body.style.overflow
|
||||
body.style.overflow = 'hidden'
|
||||
body.style.position = 'fixed'
|
||||
body.style.top = `-${scrollTop}px`
|
||||
body.style.width = '100%'
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
// 小程序平台:禁用页面滚动
|
||||
uni.pageScrollTo({
|
||||
scrollTop: scrollTop,
|
||||
duration: 0
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// APP 平台:禁用页面滚动
|
||||
const currentWebview = plus.webview.currentWebview()
|
||||
if (currentWebview) {
|
||||
pageScrollEnabled = currentWebview.isScrollEnabled()
|
||||
currentWebview.setStyle({
|
||||
scrollIndicator: 'none',
|
||||
bounce: 'none'
|
||||
})
|
||||
// 尝试禁用滚动
|
||||
try {
|
||||
currentWebview.setStyle({ scrollEnabled: false })
|
||||
} catch (e) {
|
||||
console.log('APP 平台禁用滚动失败:', e)
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 解锁背景页面滚动
|
||||
function unlockBodyScroll() {
|
||||
// #ifdef H5
|
||||
// H5 平台:恢复 body 滚动
|
||||
const body = document.body
|
||||
body.style.overflow = bodyOverflow
|
||||
body.style.position = ''
|
||||
body.style.top = ''
|
||||
body.style.width = ''
|
||||
// 恢复滚动位置
|
||||
window.scrollTo(0, scrollTop)
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
// 小程序平台:恢复页面滚动
|
||||
uni.pageScrollTo({
|
||||
scrollTop: scrollTop,
|
||||
duration: 0
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// APP 平台:恢复页面滚动
|
||||
const currentWebview = plus.webview.currentWebview()
|
||||
if (currentWebview) {
|
||||
currentWebview.setStyle({
|
||||
scrollIndicator: 'auto',
|
||||
bounce: 'vertical'
|
||||
})
|
||||
// 恢复滚动
|
||||
try {
|
||||
currentWebview.setStyle({ scrollEnabled: pageScrollEnabled })
|
||||
} catch (e) {
|
||||
console.log('APP 平台恢复滚动失败:', e)
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
initBackend()
|
||||
// 锁定背景页面滚动
|
||||
lockBodyScroll()
|
||||
} else {
|
||||
// 解锁背景页面滚动
|
||||
unlockBodyScroll()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 刷新引导列表
|
||||
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('[GuideModal] handleStartGuide:', key)
|
||||
clearSubStepProgress(key)
|
||||
resetGuide(key)
|
||||
const targetPage = getStepPage(key, 0)
|
||||
|
||||
if (targetPage) {
|
||||
emit('close')
|
||||
uni.navigateTo({
|
||||
url: targetPage + '?guide_key=' + key + '&guide_step=0'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 继续引导
|
||||
function handleContinueGuide(key) {
|
||||
console.log('[GuideModal] handleContinueGuide:', key)
|
||||
const resumeStep = getNextIncompleteStep(key)
|
||||
const targetPage = getStepPage(key, resumeStep)
|
||||
|
||||
if (targetPage) {
|
||||
emit('close')
|
||||
uni.navigateTo({
|
||||
url: targetPage + '?guide_key=' + key + '&guide_step=' + resumeStep
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 完成引导并同步后端
|
||||
async function completeGuideAndSync(key) {
|
||||
try {
|
||||
markGuideDone(key)
|
||||
await completeGuide(key, onboardingStages)
|
||||
console.log('[GuideModal] completeGuideAndSync success:', key)
|
||||
} catch (err) {
|
||||
console.error('[GuideModal] 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 {
|
||||
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)
|
||||
refreshList()
|
||||
emit('updated')
|
||||
} 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 {
|
||||
for (const stage of data.stages) {
|
||||
if (stage.allTasksCompleted && stage.required_task_keys) {
|
||||
for (const taskKey of stage.required_task_keys) {
|
||||
if (!markGuideDone(taskKey)) {
|
||||
console.log('[GuideModal] 从后端同步任务完成状态:', taskKey)
|
||||
markGuideDone(taskKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshList()
|
||||
await checkAndSyncCompletedGuides()
|
||||
} catch (err) {
|
||||
console.error('initBackend error:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 阻止移动事件
|
||||
const handlePreventMove = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.guide-modal-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
-webkit-overflow-scrolling: auto;
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1;
|
||||
touch-action: none;
|
||||
-webkit-overflow-scrolling: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
position: relative;
|
||||
width: 748rpx;
|
||||
min-height: 66vh;
|
||||
border-radius: 40rpx;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
touch-action: pan-y;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.modal-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
background: #00000021;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 64rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 85vh;
|
||||
overflow: hidden;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
/* 顶部区域 */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 返回按钮 */
|
||||
.back-button {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
.modal-title {
|
||||
flex: 1;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 列表区域 */
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
max-height: 800rpx;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 20rpx;
|
||||
touch-action: pan-y;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.guide-item {
|
||||
background: linear-gradient(45deg, rgba(255, 136, 109, 0.6) 0%, rgba(202, 88, 180, 0.6) 33%, rgba(235, 230, 178, 0.6) 100%);
|
||||
border-radius: 24rpx;
|
||||
padding: 16rpx 30rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.guide-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
}
|
||||
|
||||
.guide-status-badge.not_started {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.4);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.guide-status-badge.in_progress {
|
||||
background: rgba(74, 144, 226, 0.6);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.5);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.guide-status-badge.completed {
|
||||
background: linear-gradient(135deg, rgba(144, 238, 144, 0.6) 0%, rgba(60, 179, 113, 0.6) 100%);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.5);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.guide-status-badge.reward_claimed {
|
||||
background: rgba(150, 150, 150, 0.5);
|
||||
border: 2rpx solid rgba(150, 150, 150, 0.6);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.guide-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.guide-item-body {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.guide-desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.guide-progress {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
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: rgba(255, 255, 255, 0.8);
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.guide-item-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.guide-btn {
|
||||
min-width: 120rpx;
|
||||
width: 120rpx;
|
||||
height: 50rpx;
|
||||
padding: 0;
|
||||
color: #fff;
|
||||
border-radius: 25rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
background: linear-gradient(to bottom right, #F0E4B1 0%, #F08399 50%, #B94E73 100%);
|
||||
box-shadow:
|
||||
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
|
||||
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4),
|
||||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.continue-btn {
|
||||
background: linear-gradient(to bottom right, #F0E4B1 0%, #F08399 50%, #B94E73 100%);
|
||||
box-shadow:
|
||||
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
|
||||
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4),
|
||||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.claim-btn {
|
||||
background: linear-gradient(to bottom right, #F0E4B1 0%, #F08399 50%, #B94E73 100%);
|
||||
box-shadow:
|
||||
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
|
||||
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4),
|
||||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.disabled-btn {
|
||||
background: rgba(150, 150, 150, 0.5);
|
||||
border: 2rpx solid rgba(150, 150, 150, 0.6);
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 底部统计 */
|
||||
.modal-footer {
|
||||
padding: 24rpx 0 0;
|
||||
border-top: 2rpx solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.footer-stats {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
margin-bottom: 8rpx;
|
||||
font-weight: bold;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.footer-claim-tip {
|
||||
font-size: 26rpx;
|
||||
color: #FFD700;
|
||||
text-align: center;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
</style>
|
||||
@ -1,27 +1,37 @@
|
||||
<template>
|
||||
<view v-if="visible" class="modal-wrapper" @touchmove.stop.prevent @click.stop>
|
||||
<view v-if="visible" class="modal-wrapper" @touchmove.stop.prevent="handlePreventMove" @click.stop>
|
||||
<transition name="fade">
|
||||
<view v-if="visible" class="modal-mask" @touchstart.stop="handleMaskTouchStart" @click.stop>
|
||||
<view v-if="visible" class="modal-mask" @touchstart.stop="handleMaskTouchStart" @touchmove.stop.prevent="handlePreventMove" @click.stop>
|
||||
</view>
|
||||
</transition>
|
||||
|
||||
<transition name="scale">
|
||||
<view v-if="visible" class="modal-container" @click.stop>
|
||||
<view v-if="visible" class="modal-container" @click.stop @touchmove.stop="handleContainerMove">
|
||||
<!-- 背景图片 -->
|
||||
<image class="modal-background" src="/static/background/friend-list-bg.png" mode="aspectFill"></image>
|
||||
<image class="modal-background" src="/static/nft/beijingban.png" mode="aspectFill"></image>
|
||||
|
||||
<!-- 蒙层 -->
|
||||
<view class="modal-overlay"></view>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<view class="close-button" @touchstart.stop="handleCloseTouchStart" @touchend.stop="handleCloseTouchEnd" @click="handleCloseClick">
|
||||
<text class="close-icon">×</text>
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="modal-content" @touchstart.stop @touchend.stop @click.stop>
|
||||
<!-- 标题 -->
|
||||
<text class="modal-title">每日任务</text>
|
||||
<view class="modal-content" @touchstart.stop @touchmove.stop @touchend.stop @click.stop>
|
||||
<!-- 顶部区域:返回按钮和Tab -->
|
||||
<view class="top-bar">
|
||||
<!-- 返回按钮 -->
|
||||
<view class="back-button" @touchstart.stop="handleCloseTouchStart" @touchend.stop="handleCloseTouchEnd" @click="handleCloseClick">
|
||||
<image class="back-icon" src="/static/icon/back.png" mode="aspectFit" />
|
||||
</view>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="tab-bar">
|
||||
<view class="tab-item active">
|
||||
<text class="tab-text">每日任务</text>
|
||||
</view>
|
||||
<view class="tab-item">
|
||||
<text class="tab-text">每周任务</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view v-if="loading" class="loading-state">
|
||||
@ -36,44 +46,58 @@
|
||||
</view>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<view v-else class="task-list">
|
||||
<scroll-view v-else class="task-list" scroll-y :show-scrollbar="false">
|
||||
<!-- 空状态 -->
|
||||
<view v-if="tasks.length === 0" class="empty-state">
|
||||
<view v-if="sortedTasks.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无每日任务</text>
|
||||
</view>
|
||||
|
||||
<!-- 任务项 -->
|
||||
<view v-for="task in tasks" :key="task.task_key" class="task-item">
|
||||
<view v-for="task in sortedTasks" :key="task.task_key" class="task-item">
|
||||
<view class="task-info">
|
||||
<text class="task-name">{{ task.name }}</text>
|
||||
<text class="task-desc">{{ task.description }}</text>
|
||||
<view class="task-reward">
|
||||
<text class="reward-icon">💎</text>
|
||||
<text class="reward-value">+{{ task.crystal_reward }}</text>
|
||||
<text class="reward-sep">|</text>
|
||||
<text class="reward-icon">⭐</text>
|
||||
<text class="reward-value">+{{ task.exp_reward }}</text>
|
||||
</view>
|
||||
<text class="task-progress" v-if="task.current_count !== undefined">({{ task.current_count }}/{{ task.target_count }})</text>
|
||||
</view>
|
||||
|
||||
<view class="task-action">
|
||||
<!-- 状态标签 -->
|
||||
<view v-if="!task.can_claim" class="status-badge" :class="getStatusClass(task.status)">
|
||||
<text class="status-text">{{ getStatusText(task.status) }}</text>
|
||||
<view class="task-right">
|
||||
<!-- 礼盒图标 - 点击展开/收起 -->
|
||||
<view class="reward-gift" @click.stop="toggleReward(task.task_key)">
|
||||
<image
|
||||
class="gift-icon"
|
||||
:class="{ 'gift-open': expandedTaskKey === task.task_key }"
|
||||
:src="expandedTaskKey === task.task_key ? '/static/nft/lihe_kaiqi.png' : '/static/nft/lihe.png'"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
|
||||
<!-- 奖励详情弹出 -->
|
||||
<view v-if="expandedTaskKey === task.task_key" class="reward-popup">
|
||||
<view class="reward-item">
|
||||
<image class="reward-icon-img" src="/static/icon/crystal.png" mode="aspectFit"></image>
|
||||
<text class="reward-value">{{ task.crystal_reward }}</text>
|
||||
</view>
|
||||
<view class="reward-item">
|
||||
<image class="reward-icon-img" src="/static/nft/jingyanzhi.png" mode="aspectFit"></image>
|
||||
<text class="reward-value">{{ task.exp_reward }}</text>
|
||||
</view>
|
||||
<view class="reward-item">
|
||||
<image class="reward-icon-img" src="/static/nft/huoyuezhitubiao.png" mode="aspectFit"></image>
|
||||
<text class="reward-value">{{ task.activity_reward || 10 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 领取按钮 -->
|
||||
<!-- 状态按钮/标签 -->
|
||||
<button
|
||||
v-if="task.can_claim"
|
||||
class="claim-btn"
|
||||
:class="getStatusClass(task.status)"
|
||||
:loading="claimingTask === task.task_key"
|
||||
@click="handleClaim(task)"
|
||||
:disabled="!task.can_claim"
|
||||
@click="task.can_claim && handleClaim(task)"
|
||||
>
|
||||
领取
|
||||
{{ task.can_claim ? '待领取' : getStatusText(task.status) }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部进度区域和一键领取 -->
|
||||
<view v-if="!loading && !errorMessage" class="bottom-section">
|
||||
@ -86,15 +110,33 @@
|
||||
class="progress-node"
|
||||
:class="{ 'active': index < completedCount }"
|
||||
>
|
||||
<view class="node-circle"></view>
|
||||
<text class="node-label">{{ milestone }}</text>
|
||||
<!-- 奖励图标 -->
|
||||
<image
|
||||
class="milestone-icon"
|
||||
:src="[20, 40, 80].includes(milestone.value) ? '/static/icon/crystal.png' : '/static/nft/lihe.png'"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<!-- 进度节点 -->
|
||||
<image
|
||||
class="node-circle"
|
||||
:class="{ 'node-inactive': index >= completedCount }"
|
||||
src="/static/nft/huoyuezhi_jiedian.png"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<text class="node-label">{{ milestone.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 一键领取按钮 -->
|
||||
<view v-if="hasClaimableTasks" class="claim-all-bar">
|
||||
<button class="claim-all-btn" :loading="claimingAll" @click="handleClaimAll">
|
||||
<view class="claim-all-bar">
|
||||
<button
|
||||
class="claim-all-btn"
|
||||
:class="{ 'disabled': !hasClaimableTasks }"
|
||||
:loading="claimingAll"
|
||||
:disabled="!hasClaimableTasks"
|
||||
@click="handleClaimAll"
|
||||
>
|
||||
一键领取 ({{ claimableCount }})
|
||||
</button>
|
||||
</view>
|
||||
@ -124,12 +166,43 @@ const tasks = ref([])
|
||||
const claimingTask = ref('')
|
||||
const claimingAll = ref(false)
|
||||
const starId = ref(1)
|
||||
const expandedTaskKey = ref('') // 记录当前展开的任务
|
||||
|
||||
// 排序后的任务列表:待领取 > 进行中 > 已完成
|
||||
const sortedTasks = computed(() => {
|
||||
const tasksCopy = [...tasks.value]
|
||||
return tasksCopy.sort((a, b) => {
|
||||
// 定义优先级:can_claim(待领取) > pending(进行中) > claimed(已完成)
|
||||
const getPriority = (task) => {
|
||||
if (task.can_claim) return 1
|
||||
if (task.status === 'pending') return 2
|
||||
if (task.status === 'claimed') return 3
|
||||
return 4
|
||||
}
|
||||
return getPriority(a) - getPriority(b)
|
||||
})
|
||||
})
|
||||
|
||||
const hasClaimableTasks = computed(() => tasks.value.some(t => t.can_claim))
|
||||
const claimableCount = computed(() => tasks.value.filter(t => t.can_claim).length)
|
||||
|
||||
// 切换奖励展开/收起
|
||||
function toggleReward(taskKey) {
|
||||
if (expandedTaskKey.value === taskKey) {
|
||||
expandedTaskKey.value = ''
|
||||
} else {
|
||||
expandedTaskKey.value = taskKey
|
||||
}
|
||||
}
|
||||
|
||||
// 进度里程碑
|
||||
const milestones = ref(['任务1', '任务2', '任务3', '任务4'])
|
||||
const milestones = ref([
|
||||
{ value: 20 },
|
||||
{ value: 40 },
|
||||
{ value: 60 },
|
||||
{ value: 80 },
|
||||
{ value: 100 }
|
||||
])
|
||||
|
||||
// 已完成的里程碑数量
|
||||
const completedCount = computed(() => {
|
||||
@ -137,10 +210,105 @@ const completedCount = computed(() => {
|
||||
return tasks.value.filter(t => t.status === 'claimed').length
|
||||
})
|
||||
|
||||
// 保存滚动位置和页面状态
|
||||
let scrollTop = 0
|
||||
let bodyOverflow = ''
|
||||
let pageScrollEnabled = true
|
||||
|
||||
// 锁定背景页面滚动
|
||||
function lockBodyScroll() {
|
||||
// 获取当前页面滚动位置
|
||||
uni.createSelectorQuery().selectViewport().scrollOffset(res => {
|
||||
if (res) {
|
||||
scrollTop = res.scrollTop || 0
|
||||
}
|
||||
}).exec()
|
||||
|
||||
// #ifdef H5
|
||||
// H5 平台:锁定 body 滚动
|
||||
const body = document.body
|
||||
bodyOverflow = body.style.overflow
|
||||
body.style.overflow = 'hidden'
|
||||
body.style.position = 'fixed'
|
||||
body.style.top = `-${scrollTop}px`
|
||||
body.style.width = '100%'
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
// 小程序平台:禁用页面滚动
|
||||
uni.pageScrollTo({
|
||||
scrollTop: scrollTop,
|
||||
duration: 0
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// APP 平台:禁用页面滚动
|
||||
const currentWebview = plus.webview.currentWebview()
|
||||
if (currentWebview) {
|
||||
pageScrollEnabled = currentWebview.isScrollEnabled()
|
||||
currentWebview.setStyle({
|
||||
scrollIndicator: 'none',
|
||||
bounce: 'none'
|
||||
})
|
||||
// 尝试禁用滚动
|
||||
try {
|
||||
currentWebview.setStyle({ scrollEnabled: false })
|
||||
} catch (e) {
|
||||
console.log('APP 平台禁用滚动失败:', e)
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 解锁背景页面滚动
|
||||
function unlockBodyScroll() {
|
||||
// #ifdef H5
|
||||
// H5 平台:恢复 body 滚动
|
||||
const body = document.body
|
||||
body.style.overflow = bodyOverflow
|
||||
body.style.position = ''
|
||||
body.style.top = ''
|
||||
body.style.width = ''
|
||||
// 恢复滚动位置
|
||||
window.scrollTo(0, scrollTop)
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
// 小程序平台:恢复页面滚动
|
||||
uni.pageScrollTo({
|
||||
scrollTop: scrollTop,
|
||||
duration: 0
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
// APP 平台:恢复页面滚动
|
||||
const currentWebview = plus.webview.currentWebview()
|
||||
if (currentWebview) {
|
||||
currentWebview.setStyle({
|
||||
scrollIndicator: 'auto',
|
||||
bounce: 'vertical'
|
||||
})
|
||||
// 恢复滚动
|
||||
try {
|
||||
currentWebview.setStyle({ scrollEnabled: pageScrollEnabled })
|
||||
} catch (e) {
|
||||
console.log('APP 平台恢复滚动失败:', e)
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 监听 visible 变化,自动加载数据
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
loadTasks()
|
||||
// 锁定背景页面滚动
|
||||
lockBodyScroll()
|
||||
} else {
|
||||
// 解锁背景页面滚动
|
||||
unlockBodyScroll()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
@ -155,9 +323,9 @@ function getStatusClass(status) {
|
||||
|
||||
function getStatusText(status) {
|
||||
switch (status) {
|
||||
case 'pending': return '进行中'
|
||||
case 'pending': return '前往'
|
||||
case 'completed': return '可领取'
|
||||
case 'claimed': return '已领取'
|
||||
case 'claimed': return '已完成'
|
||||
default: return status
|
||||
}
|
||||
}
|
||||
@ -214,13 +382,23 @@ async function handleClaimAll() {
|
||||
}
|
||||
|
||||
// 遮罩层触摸处理
|
||||
let maskTouchStartTime = 0
|
||||
const handleMaskTouchStart = (e) => {
|
||||
maskTouchStartTime = Date.now()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
// 阻止移动事件(完全阻止背景滚动)
|
||||
const handlePreventMove = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}
|
||||
|
||||
// 容器移动处理(允许内部滚动)
|
||||
const handleContainerMove = (e) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
// 关闭按钮触摸处理
|
||||
let closeTouchStartTime = 0
|
||||
let closeTouchLocked = false
|
||||
@ -260,6 +438,9 @@ const handleCloseClick = (e) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
-webkit-overflow-scrolling: auto;
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
@ -270,15 +451,21 @@ const handleCloseClick = (e) => {
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1;
|
||||
touch-action: none;
|
||||
-webkit-overflow-scrolling: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
position: relative;
|
||||
width: 580rpx;
|
||||
width: 748rpx;
|
||||
max-height: 85vh;
|
||||
border-radius: 40rpx;
|
||||
overflow: hidden;
|
||||
z-index: 2;
|
||||
touch-action: pan-y;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.modal-background {
|
||||
@ -302,7 +489,7 @@ const handleCloseClick = (e) => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: #00000021;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
@ -327,21 +514,72 @@ const handleCloseClick = (e) => {
|
||||
.modal-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 60rpx 40rpx 40rpx;
|
||||
padding: 64rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 40rpx;
|
||||
/* 顶部区域 */
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 返回按钮 */
|
||||
.back-button {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
/* Tab 样式 */
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
padding: 24rpx 32rpx;
|
||||
border-radius: 50rpx;
|
||||
background-image: url('/static/nft/dingbutubiao_an.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
background-image: url('/static/nft/dingbutubiao_liang.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 20rpx rgba(240, 131, 153, 0.3);
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
color: #e6e6e6;
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
color: #fff;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
@ -400,74 +638,128 @@ const handleCloseClick = (e) => {
|
||||
}
|
||||
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
max-height: 448rpx;
|
||||
margin-bottom: 30rpx;
|
||||
touch-action: pan-y;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.task-list .task-item {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.task-list .task-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
|
||||
padding: 16rpx 30rpx;
|
||||
background: linear-gradient(45deg, rgba(255, 136, 109, 0.6) 0%, rgba(202, 88, 180, 0.6) 33%, rgba(235, 230, 178, 0.6) 100%);
|
||||
/* backdrop-filter: blur(10rpx); */
|
||||
border-radius: 24rpx;
|
||||
/* border: 2rpx solid rgba(255, 255, 255, 0.3); */
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.task-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-size: 30rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #e6e6e6;
|
||||
color: #fff;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.task-desc {
|
||||
.task-progress {
|
||||
font-size: 24rpx;
|
||||
color: rgba(230, 230, 230, 0.7);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.task-reward {
|
||||
.task-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-top: 8rpx;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.reward-icon {
|
||||
font-size: 24rpx;
|
||||
.reward-gift {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gift-icon {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.gift-icon.gift-open {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.reward-popup {
|
||||
position: absolute;
|
||||
right: 60rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
background: linear-gradient(135deg, rgba(255, 182, 193, 0.95) 0%, rgba(173, 216, 230, 0.95) 100%);
|
||||
backdrop-filter: blur(10rpx);
|
||||
border-radius: 30rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
|
||||
z-index: 10;
|
||||
animation: popupShow 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes popupShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%) scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.reward-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.reward-icon-img {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
.reward-value {
|
||||
font-size: 26rpx;
|
||||
color: #FFB800;
|
||||
font-size: 22rpx;
|
||||
color: #FFD700;
|
||||
font-weight: bold;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.reward-sep {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
margin: 0 8rpx;
|
||||
}
|
||||
|
||||
.task-action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10rpx;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
min-width: 30rpx;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
min-width: 120rpx;
|
||||
min-width: 100rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 25rpx;
|
||||
display: flex;
|
||||
@ -477,69 +769,106 @@ const handleCloseClick = (e) => {
|
||||
}
|
||||
|
||||
.status-badge.status-pending {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.status-badge.status-completed {
|
||||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||||
background: linear-gradient(135deg, rgba(144, 238, 144, 0.6) 0%, rgba(60, 179, 113, 0.6) 100%);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.status-badge.status-claimed {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 24rpx;
|
||||
color: #e6e6e6;
|
||||
color: #fff;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.claim-btn {
|
||||
min-width: 140rpx;
|
||||
min-width: 120rpx;
|
||||
width: 120rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 25rpx;
|
||||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||||
/* 渐变:左浅橙粉 → 右柔粉红 */
|
||||
background: linear-gradient(to bottom right,
|
||||
#F0E4B1 0%, /* 左:浅橙粉 */
|
||||
#F08399 50%,
|
||||
#B94E73 100% /* 右:柔粉红 */
|
||||
);
|
||||
|
||||
/* 立体感核心:多层阴影 + 内阴影模拟凸起 */
|
||||
box-shadow:
|
||||
/* 外层投影 - 让按钮浮起 */
|
||||
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
|
||||
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
|
||||
|
||||
/* 内阴影 - 模拟顶部受光 + 底部凹陷 */
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4), /* 顶部高光 */
|
||||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05); /* 底部暗部 */
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.5);
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.claim-all-bar {
|
||||
margin-top: 20rpx;
|
||||
.claim-btn.status-pending {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.4);
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.claim-btn.status-claimed {
|
||||
background: rgba(150, 150, 150, 0.5);
|
||||
border: 2rpx solid rgba(150, 150, 150, 0.6);
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bottom-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
width: 100%;
|
||||
padding: 30rpx 20rpx;
|
||||
/* backdrop-filter: blur(10rpx); */
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
height: 60rpx;
|
||||
padding: 0 30rpx;
|
||||
padding: 0 10rpx;
|
||||
}
|
||||
|
||||
.progress-bar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50rpx;
|
||||
right: 50rpx;
|
||||
height: 4rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-50%);
|
||||
bottom: 40rpx;
|
||||
left: 0;
|
||||
right: 10%;
|
||||
height: 32rpx;
|
||||
background-image: url('/static/nft/huoyuezhi_changtiao.png');
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@ -549,40 +878,63 @@ const handleCloseClick = (e) => {
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
gap: 8rpx;
|
||||
/* flex: 1; */
|
||||
}
|
||||
|
||||
.milestone-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.node-circle {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-node.active .node-circle {
|
||||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||||
border-color: #F08399;
|
||||
.node-circle.node-inactive {
|
||||
filter: grayscale(100%) brightness(0.6);
|
||||
/* opacity: 0.5; */
|
||||
}
|
||||
|
||||
.progress-node.active .milestone-icon {
|
||||
filter: drop-shadow(0 4rpx 12rpx rgba(240, 131, 153, 0.6));
|
||||
}
|
||||
|
||||
.node-label {
|
||||
font-size: 22rpx;
|
||||
color: rgba(230, 230, 230, 0.7);
|
||||
margin-top: 8rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.claim-all-bar {
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.claim-all-btn {
|
||||
width: 100%;
|
||||
padding: 24rpx 0;
|
||||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||||
background: linear-gradient(135deg, rgba(240, 228, 177, 0.8) 0%, rgba(240, 131, 153, 0.8) 50%, rgba(185, 78, 115, 0.8) 100%);
|
||||
/* backdrop-filter: blur(10rpx); */
|
||||
/* border: 2rpx solid rgba(255, 255, 255, 0.5); */
|
||||
color: #fff;
|
||||
border-radius: 50rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 8rpx 20rpx rgba(240, 131, 153, 0.4);
|
||||
}
|
||||
|
||||
.claim-all-btn.disabled {
|
||||
background: rgba(150, 150, 150, 0.5);
|
||||
border: 2rpx solid rgba(150, 150, 150, 0.6);
|
||||
box-shadow: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
|
||||
@ -1,540 +0,0 @@
|
||||
<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>
|
||||
@ -239,21 +239,13 @@ export function getSentFriendRequestsApi(page = 1, pageSize = 10) {
|
||||
}
|
||||
|
||||
// 发送好友请求(添加好友)
|
||||
// friendUserId: 好友用户ID(可选)
|
||||
// nickname: 昵称(可选,与 friendUserId 二选一)
|
||||
// searchMode: 是否为搜索模式(true=仅搜索返回匹配用户,false=正常发送请求)
|
||||
export function sendFriendRequestApi(friendUserId = null, nickname = '', searchMode = true) {
|
||||
const data = {}
|
||||
if (searchMode && nickname) {
|
||||
data.nickname = nickname
|
||||
data.search_mode = true
|
||||
} else if (friendUserId) {
|
||||
data.friend_user_id = friendUserId
|
||||
}
|
||||
export function sendFriendRequestApi(friendUserId) {
|
||||
return request({
|
||||
url: '/api/v1/social/friend-requests',
|
||||
method: 'POST',
|
||||
data
|
||||
data: {
|
||||
friend_user_id: friendUserId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user