style: 修改每日任务和引导任务的样式

This commit is contained in:
zerosaturation 2026-04-22 15:08:05 +08:00
parent a0dae45edb
commit dd838fccc4
5 changed files with 1229 additions and 678 deletions

View File

@ -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');
};
//

View 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>

View File

@ -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 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>
<!-- 内容区域 -->
<view class="modal-content" @touchstart.stop @touchend.stop @click.stop>
<!-- 标题 -->
<text class="modal-title">每日任务</text>
<!-- 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>
<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;
}
/* 动画效果 */

View File

@ -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>

View File

@ -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
}
})
}