topfans/frontend/pages/tasks/daily-tasks.vue
zerosaturation 83999995f5 refactor(frontend): consolidate user storage read via getStoredUser helper
uni.getStorageSync('user') 在 key 从未写过的 UniApp 平台下默认返回 ""(空串)
而非 null/undefined,旧式 `JSON.parse(uni.getStorageSync('user')) || {}` 在
parse 阶段直接抛 `SyntaxError: Unexpected end of JSON input`,导致 quickLogin
setup 崩溃,并触发 Vue 二次告警 `Invalid vnode type: undefined`。

新增 utils/getStoredUser.js 单点封装,统一处理:
  - 空值/损坏 JSON 兜底(返回 null,不抛)
  - 与 store/modules/user.js 初始化块同源守卫
  - 失败 warn 级别日志,不影响业务

替换全库 16 处不安全 `JSON.parse(userStr)` 调用:
  composables/useLaserMint.js                  composables/useShare.js
  utils/guideConfig.js                         utils/preloadApi/core.js
  pages/castlove/lenticular/lenticular-result.vue
  pages/support-activity/index.vue             pages/support-activity/components/{TopRanking,ActionBar}.vue
  pages/components/{Header,ShareModal,ShareReportButtons}.vue
  pages/tasks/{GuideModal,daily-tasks}.vue
  pages/asset-detail/asset-detail.vue          pages/exhibition/exhibition.vue
  pages/profile/profile.vue                    pages/login/quickLogin.vue(原崩点)

净 -18 行,17 文件改动 + 1 文件新增。后续如需支持多 key(如 'adminUser'),
把 getStoredUser 扩成泛型 getStoredJSON(key) 即可。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:16:49 +08:00

982 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<view 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"
@touchmove.stop.prevent="handlePreventMove" @click.stop>
</view>
</transition>
<transition name="scale">
<view v-if="visible" class="modal-container" @click.stop @touchmove.stop="handleContainerMove">
<!-- 背景图片 -->
<image class="modal-background" src="/static/nft/beijingban.png" mode="aspectFill"></image>
<!-- 蒙层 -->
<view class="modal-overlay"></view>
<!-- 内容区域 -->
<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">
<!-- <text class="back-icon">←</text> -->
<image class="back-icon" src="/static/starbookcontent/tuichu.png" mode="aspectFit"></image>
</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">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 错误状态 -->
<view v-else-if="errorMessage" class="error-state">
<text class="error-text">{{ errorMessage }}</text>
<button class="retry-btn" @click="loadTasks">重试</button>
</view>
<!-- 任务列表 -->
<scroll-view v-else class="task-list" scroll-y :show-scrollbar="false">
<!-- 空状态 -->
<view v-if="sortedTasks.length === 0" class="empty-state">
<text class="empty-text">暂无每日任务</text>
</view>
<!-- 任务项 -->
<view v-for="task in sortedTasks" :key="task.task_key" class="task-item">
<view class="task-info">
<text class="task-name">{{ task.name }}</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 class="claim-btn" :class="getStatusClass(task.status)"
:loading="claimingTask === task.task_key" :disabled="!task.can_claim"
@click="task.can_claim && handleClaim(task)">
{{ task.can_claim ? '待领取' : getStatusText(task.status) }}
</button>
</view>
</view>
</scroll-view>
<!-- 底部进度区域和一键领取 -->
<view v-if="!loading && !errorMessage" class="bottom-section">
<!-- 进度条 -->
<view class="progress-section">
<view class="progress-bar">
<view v-for="(milestone, index) in milestones" :key="index" class="progress-node"
:class="{ 'active': index < completedCount }">
<!-- 奖励图标 -->
<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 class="claim-all-bar">
<button class="claim-all-btn" :class="{ 'disabled': !hasClaimableTasks }"
:loading="claimingAll" :disabled="!hasClaimableTasks" @click="handleClaimAll">
一键领取 ({{ claimableCount }})
</button>
</view>
</view>
</view>
</view>
</transition>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { getDailyTasks, claimDailyTask, claimAllDailyTasks } from '@/utils/task-api.js'
import { getStoredUser } from '@/utils/getStoredUser.js'
const props = defineProps({
visible: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['close', 'updated'])
const loading = ref(true)
const errorMessage = ref('')
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([
{ value: 20 },
{ value: 40 },
{ value: 60 },
{ value: 80 },
{ value: 100 }
])
// 已完成的里程碑数量
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) {
// plus.webview.WebviewObject 没有 isScrollEnabled() 方法HTML5+ 标准 API 仅 setStyle/getStyle
// webview 默认 scrollEnabled=true这里直接假设初值为 true由 modal 生命周期负责还原。
pageScrollEnabled = true
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 })
function getStatusClass(status) {
switch (status) {
case 'pending': return 'status-pending'
case 'completed': return 'status-completed'
case 'claimed': return 'status-claimed'
default: return ''
}
}
function getStatusText(status) {
switch (status) {
case 'pending': return '未完成'
case 'completed': return '可领取'
case 'claimed': return '已完成'
default: return status
}
}
async function loadTasks() {
try {
loading.value = true
errorMessage.value = ''
starId.value = uni.getStorageSync('star_id')
const res = await getDailyTasks(starId.value)
tasks.value = res.data?.tasks || []
} catch (err) {
console.error('loadTasks error:', err)
errorMessage.value = err.message || '加载失败'
} finally {
loading.value = false
}
}
async function handleClaim(task) {
if (claimingTask.value) return
try {
claimingTask.value = task.task_key
const res = await claimDailyTask(task.task_key, starId.value)
// 直接更新本地状态,而不是重新加载整个列表
const index = tasks.value.findIndex(t => t.task_key === task.task_key)
if (index !== -1) {
tasks.value[index].status = 'claimed'
tasks.value[index].can_claim = false
}
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
const user = getStoredUser()
if (user) {
emit('updated')
// 更新余额,确保是数字类型
user.crystal_balance = Number(res.data?.crystal_balance) || 0
// 保存回存储,确保是字符串格式
uni.setStorageSync('user', JSON.stringify(user))
uni.$emit('balanceUpdated', { crystal_balance: user.crystal_balance })
}
uni.showToast({ title: '领取成功', icon: 'success' })
} catch (err) {
console.error('handleClaim error:', err)
uni.showToast({ title: err.message || '领取失败', icon: 'none' })
} finally {
claimingTask.value = ''
}
}
async function handleClaimAll() {
if (claimingAll.value) return
try {
claimingAll.value = true
const res = await claimAllDailyTasks(starId.value)
// 直接更新所有已领取的任务为 claimed 状态
tasks.value.forEach(task => {
if (task.can_claim) {
task.status = 'claimed'
task.can_claim = false
}
})
emit('updated')
// ★ 2026-07-27 refactor:委托 @/utils/getStoredUser,统一空值/损坏 JSON 守卫
const user = getStoredUser()
if (user) {
emit('updated')
// 更新余额,确保是数字类型
user.crystal_balance = Number(res.data?.crystal_balance) || 0
// 保存回存储,确保是字符串格式
uni.setStorageSync('user', JSON.stringify(user))
uni.$emit('balanceUpdated', { crystal_balance: user.crystal_balance })
}
uni.showToast({ title: '领取成功', icon: 'success' })
} catch (err) {
console.error('handleClaimAll error:', err)
uni.showToast({ title: err.message || '领取失败', icon: 'none' })
} finally {
claimingAll.value = false
}
}
// 遮罩层触摸处理
const handleMaskTouchStart = (e) => {
e.preventDefault()
e.stopPropagation()
}
// 阻止移动事件(完全阻止背景滚动)
const handlePreventMove = (e) => {
e.preventDefault()
e.stopPropagation()
return false
}
// 容器移动处理(允许内部滚动)
const handleContainerMove = (e) => {
e.stopPropagation()
}
// 关闭按钮触摸处理
let closeTouchStartTime = 0
let closeTouchLocked = false
const handleCloseTouchStart = (e) => {
closeTouchStartTime = Date.now()
closeTouchLocked = false
}
const handleCloseTouchEnd = (e) => {
if (closeTouchLocked) return
closeTouchLocked = true
const touchDuration = Date.now() - closeTouchStartTime
if (touchDuration < 300) {
e.preventDefault()
e.stopPropagation()
emit('close')
}
}
const handleCloseClick = (e) => {
e.preventDefault()
e.stopPropagation()
emit('close')
}
</script>
<style scoped>
.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;
max-height: 85vh;
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;
}
.close-button {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.close-icon {
font-size: 50rpx;
color: #e6e6e6;
line-height: 1;
font-weight: 300;
}
.modal-content {
position: relative;
z-index: 2;
padding: 64rpx;
display: flex;
flex-direction: column;
max-height: 85vh;
overflow: hidden;
touch-action: pan-y;
}
/* 顶部区域 */
.top-bar {
display: flex;
align-items: center;
margin-bottom: 30rpx;
position: relative;
}
/* 返回按钮 */
.back-button {
position: absolute;
right: 0;
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.back-icon {
/* font-size: 48rpx;
font-weight: bold;
color: #fff; */
width: 32rpx;
height: 32rpx;
}
/* 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: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4);
}
.loading-state,
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-top-color: #F08399;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text,
.error-text {
color: #e6e6e6;
font-size: 28rpx;
font-family: 'yt', sans-serif;
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
}
.retry-btn {
margin-top: 20rpx;
padding: 16rpx 40rpx;
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
color: #fff;
border-radius: 50rpx;
font-size: 28rpx;
font-family: 'yt', sans-serif;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 0;
}
.empty-text {
color: #e6e6e6;
font-size: 28rpx;
font-family: 'yt', sans-serif;
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
}
.task-list {
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: 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;
align-items: center;
gap: 8rpx;
}
.task-name {
font-size: 28rpx;
font-weight: bold;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
.task-right {
display: flex;
align-items: center;
gap: 12rpx;
}
.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: 22rpx;
color: #FFD700;
font-weight: bold;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
min-width: 30rpx;
}
.status-badge {
min-width: 100rpx;
height: 50rpx;
border-radius: 25rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 20rpx;
}
.status-badge.status-pending {
background: rgba(255, 255, 255, 0.3);
border: 2rpx solid rgba(255, 255, 255, 0.4);
}
.status-badge.status-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);
}
.status-badge.status-claimed {
background: rgba(255, 255, 255, 0.2);
border: 2rpx solid rgba(255, 255, 255, 0.3);
}
.status-text {
font-size: 24rpx;
color: #fff;
font-family: 'yt', sans-serif;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
.claim-btn {
min-width: 120rpx;
width: 120rpx;
height: 50rpx;
border-radius: 25rpx;
/* 渐变:左浅橙粉 → 右柔粉红 */
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: 24rpx;
font-weight: bold;
font-family: 'yt', sans-serif;
display: flex;
align-items: center;
justify-content: center;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
padding: 0;
}
.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-evenly;
align-items: flex-end;
position: relative;
padding: 0 10rpx;
}
.progress-bar::before {
content: '';
position: absolute;
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;
}
.progress-node {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 1;
gap: 8rpx;
/* flex: 1; */
}
.milestone-icon {
width: 60rpx;
height: 60rpx;
margin-bottom: 8rpx;
}
.node-circle {
width: 50rpx;
height: 50rpx;
transition: all 0.3s ease;
}
.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: #fff;
font-weight: bold;
font-family: 'yt', sans-serif;
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(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: 'yt', sans-serif;
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;
}
/* 动画效果 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.scale-enter-active,
.scale-leave-active {
transition: all 0.3s ease;
}
.scale-enter-from,
.scale-leave-to {
opacity: 0;
transform: scale(0.8);
}
</style>