topfans/frontend/pages/tasks/daily-tasks.vue

354 lines
7.6 KiB
Vue

<template>
<view class="daily-tasks-container">
<!-- 顶部导航 -->
<Header :show-back="true" back-icon-color="#e6e6e6" :show-guide-icon="false" :show-task-icon="false" :show-star-activity-icon="false" />
<!-- 页面标题 -->
<view class="page-title">每日任务</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>
<!-- 任务列表 -->
<view v-else class="task-list">
<!-- 空状态 -->
<view v-if="tasks.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 class="task-info">
<view class="task-name">{{ task.name }}</view>
<view class="task-desc">{{ task.description }}</view>
<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>
</view>
<view class="task-action">
<!-- 状态标签 -->
<view class="status-badge" :class="getStatusClass(task.status)">
{{ getStatusText(task.status) }}
</view>
<!-- 领取按钮 -->
<button
v-if="task.can_claim"
class="claim-btn"
:loading="claimingTask === task.task_key"
@click="handleClaim(task)"
>
领取
</button>
</view>
</view>
</view>
<!-- 一键领取按钮 -->
<view v-if="!loading && !errorMessage && hasClaimableTasks" class="claim-all-bar">
<button class="claim-all-btn" :loading="claimingAll" @click="handleClaimAll">
一键领取 ({{ claimableCount }})
</button>
</view>
<!-- 下拉刷新 -->
<view v-if="!loading" class="pull-to-refresh" @click="handlePullRefresh">
<text class="refresh-text">{{ refreshing ? '刷新中...' : '下拉刷新' }}</text>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import Header from '../components/Header.vue'
import { getDailyTasks, claimDailyTask, claimAllDailyTasks } from '@/utils/task-api.js'
const loading = ref(true)
const refreshing = ref(false)
const errorMessage = ref('')
const tasks = ref([])
const claimingTask = ref('')
const claimingAll = ref(false)
const starId = ref(1)
const hasClaimableTasks = computed(() => tasks.value.some(t => t.can_claim))
const claimableCount = computed(() => tasks.value.filter(t => t.can_claim).length)
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 = ''
const fanProfile = uni.getStorageSync('fan_profile')
if (fanProfile && fanProfile.star_id) {
starId.value = fanProfile.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
refreshing.value = false
}
}
async function handleClaim(task) {
if (claimingTask.value) return
try {
claimingTask.value = task.task_key
await claimDailyTask(task.task_key, starId.value)
await loadTasks()
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
await claimAllDailyTasks(starId.value)
await loadTasks()
uni.showToast({ title: '领取成功', icon: 'success' })
} catch (err) {
console.error('handleClaimAll error:', err)
uni.showToast({ title: err.message || '领取失败', icon: 'none' })
} finally {
claimingAll.value = false
}
}
function handlePullRefresh() {
refreshing.value = true
loadTasks()
}
onMounted(() => {
loadTasks()
})
</script>
<style scoped>
.daily-tasks-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.page-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
text-align: center;
padding: 20rpx 0;
}
.loading-state,
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #e0e0e0;
border-top-color: #6c5ce7;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text,
.error-text {
color: #999;
font-size: 28rpx;
}
.retry-btn {
margin-top: 20rpx;
padding: 16rpx 40rpx;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
color: #fff;
border-radius: 50rpx;
font-size: 28rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 0;
}
.empty-text {
color: #999;
font-size: 28rpx;
}
.task-list {
padding: 20rpx;
}
.task-item {
background: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.task-info {
flex: 1;
}
.task-name {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.task-desc {
font-size: 24rpx;
color: #999;
margin-bottom: 10rpx;
}
.task-reward {
display: flex;
align-items: center;
gap: 8rpx;
}
.reward-icon {
font-size: 24rpx;
}
.reward-value {
font-size: 24rpx;
color: #6c5ce7;
}
.reward-sep {
color: #e0e0e0;
margin: 0 8rpx;
}
.task-action {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10rpx;
}
.status-badge {
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
}
.status-pending {
background: #e8f4ff;
color: #1890ff;
}
.status-completed {
background: #f6ffed;
color: #52c41a;
}
.status-claimed {
background: #f5f5f5;
color: #999;
}
.claim-btn {
padding: 12rpx 30rpx;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
color: #fff;
border-radius: 30rpx;
font-size: 26rpx;
min-width: 120rpx;
}
.claim-all-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 30rpx;
background: #fff;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.1);
}
.claim-all-btn {
width: 100%;
padding: 24rpx 0;
background: linear-gradient(135deg, #6c5ce7, #a29bfe);
color: #fff;
border-radius: 50rpx;
font-size: 32rpx;
font-weight: bold;
}
.pull-to-refresh {
display: flex;
justify-content: center;
padding: 30rpx 0;
}
.refresh-text {
color: #999;
font-size: 24rpx;
}
</style>