522 lines
11 KiB
Vue
522 lines
11 KiB
Vue
<template>
|
||
<view v-if="visible" class="modal-wrapper" @touchmove.stop.prevent @click.stop>
|
||
<transition name="fade">
|
||
<view v-if="visible" class="modal-mask" @touchstart.stop="handleMaskTouchStart" @click.stop>
|
||
</view>
|
||
</transition>
|
||
|
||
<transition name="scale">
|
||
<view v-if="visible" class="modal-container" @click.stop>
|
||
<!-- 背景图片 -->
|
||
<image class="modal-background" src="/static/background/friend-list-bg.png" mode="aspectFill"></image>
|
||
|
||
<!-- 蒙层 -->
|
||
<view class="modal-overlay"></view>
|
||
|
||
<!-- 关闭按钮 -->
|
||
<view class="close-button" @touchstart.stop="handleCloseTouchStart" @touchend.stop="handleCloseTouchEnd" @click="handleCloseClick">
|
||
<text class="close-icon">×</text>
|
||
</view>
|
||
|
||
<!-- 内容区域 -->
|
||
<view class="modal-content" @touchstart.stop @touchend.stop @click.stop>
|
||
<!-- 标题 -->
|
||
<text class="modal-title">每日任务</text>
|
||
|
||
<!-- 加载状态 -->
|
||
<view 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">
|
||
<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>
|
||
</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>
|
||
|
||
<!-- 领取按钮 -->
|
||
<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>
|
||
</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 hasClaimableTasks = computed(() => tasks.value.some(t => t.can_claim))
|
||
const claimableCount = computed(() => tasks.value.filter(t => t.can_claim).length)
|
||
|
||
// 监听 visible 变化,自动加载数据
|
||
watch(() => props.visible, (newVal) => {
|
||
if (newVal) {
|
||
loadTasks()
|
||
}
|
||
}, { 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)
|
||
await loadTasks()
|
||
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)
|
||
await loadTasks()
|
||
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
|
||
}
|
||
}
|
||
|
||
// 遮罩层触摸处理
|
||
let maskTouchStartTime = 0
|
||
const handleMaskTouchStart = (e) => {
|
||
maskTouchStartTime = Date.now()
|
||
e.preventDefault()
|
||
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;
|
||
}
|
||
|
||
.modal-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
z-index: 1;
|
||
}
|
||
|
||
.modal-container {
|
||
position: relative;
|
||
width: 580rpx;
|
||
max-height: 85vh;
|
||
border-radius: 40rpx;
|
||
overflow: hidden;
|
||
z-index: 2;
|
||
}
|
||
|
||
.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: rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.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: 60rpx 40rpx 40rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-height: 85vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 40rpx;
|
||
font-weight: bold;
|
||
color: #e6e6e6;
|
||
text-align: center;
|
||
margin-bottom: 40rpx;
|
||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.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 {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.task-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx 0;
|
||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.task-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.task-name {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #e6e6e6;
|
||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.task-desc {
|
||
font-size: 24rpx;
|
||
color: rgba(230, 230, 230, 0.7);
|
||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.task-reward {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.reward-icon {
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.reward-value {
|
||
font-size: 26rpx;
|
||
color: #FFB800;
|
||
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;
|
||
}
|
||
|
||
.status-badge {
|
||
min-width: 120rpx;
|
||
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.2);
|
||
}
|
||
|
||
.status-badge.status-completed {
|
||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||
}
|
||
|
||
.status-badge.status-claimed {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.status-text {
|
||
font-size: 24rpx;
|
||
color: #e6e6e6;
|
||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
text-shadow: 0 4rpx 4rpx rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.claim-btn {
|
||
min-width: 140rpx;
|
||
height: 50rpx;
|
||
border-radius: 25rpx;
|
||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||
color: #fff;
|
||
font-size: 26rpx;
|
||
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.claim-all-bar {
|
||
margin-top: 20rpx;
|
||
}
|
||
|
||
.claim-all-btn {
|
||
width: 100%;
|
||
padding: 24rpx 0;
|
||
background: linear-gradient(165deg, #F0E4B1 0%, #F08399 50%, #B94E73 90%, #834B9E 100%);
|
||
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);
|
||
}
|
||
|
||
/* 动画效果 */
|
||
.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>
|