977 lines
22 KiB
Vue
977 lines
22 KiB
Vue
<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">
|
||
<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">
|
||
<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>
|
||
<text class="task-progress" v-if="task.current_count !== undefined">({{ task.current_count }}/{{ task.target_count }})</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'
|
||
|
||
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) {
|
||
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 })
|
||
|
||
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
|
||
}
|
||
|
||
emit('updated')
|
||
uni.$emit('balanceUpdated', { crystal_balance: res.data?.crystal_balance, experience: res.data?.experience })
|
||
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')
|
||
uni.$emit('balanceUpdated', { crystal_balance: res.data?.crystal_balance, experience: res.data?.experience })
|
||
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;
|
||
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: #fff;
|
||
font-family: 'ZaoZiGongFangJianHei-1', 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: 'ZaoZiGongFangJianHei-1', 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: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
}
|
||
|
||
.empty-state {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 60rpx 0;
|
||
}
|
||
|
||
.empty-text {
|
||
color: #e6e6e6;
|
||
font-size: 28rpx;
|
||
font-family: 'ZaoZiGongFangJianHei-1', 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: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.task-progress {
|
||
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);
|
||
}
|
||
|
||
.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: 'ZaoZiGongFangJianHei-1', 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: 'ZaoZiGongFangJianHei-1', 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: '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-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: 'ZaoZiGongFangJianHei-1', 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: 'ZaoZiGongFangJianHei-1', 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>
|