feat: 用户中心新增每日任务组件并在头像菜单中集成
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getTodayTaskStatus, claimTaskReward } from '@/api/dailyTask';
|
||||
import type { DailyTaskStatusOutput, DailyTaskItem } from '@/api/dailyTask/types';
|
||||
|
||||
const taskData = ref<DailyTaskStatusOutput | null>(null);
|
||||
const loading = ref(false);
|
||||
const claiming = ref<{ [key: number]: boolean }>({});
|
||||
|
||||
onMounted(() => {
|
||||
fetchTaskStatus();
|
||||
});
|
||||
|
||||
async function fetchTaskStatus() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTodayTaskStatus();
|
||||
taskData.value = res.data;
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '获取任务状态失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClaim(task: DailyTaskItem) {
|
||||
if (task.status !== 1) return;
|
||||
|
||||
claiming.value[task.level] = true;
|
||||
try {
|
||||
await claimTaskReward({ taskLevel: task.level });
|
||||
ElMessage.success(`恭喜!获得 ${formatTokenDisplay(task.rewardTokens)} token`);
|
||||
|
||||
// 刷新任务状态
|
||||
await fetchTaskStatus();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.message || '领取奖励失败');
|
||||
} finally {
|
||||
claiming.value[task.level] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化 Token 显示(单位:万)
|
||||
function formatTokenDisplay(tokens: number): string {
|
||||
return `${(tokens / 10000).toFixed(0)}w`;
|
||||
}
|
||||
|
||||
// 获取任务状态文本
|
||||
function getStatusText(task: DailyTaskItem): string {
|
||||
switch (task.status) {
|
||||
case 0:
|
||||
return '未达成';
|
||||
case 1:
|
||||
return `领取 ${formatTokenDisplay(task.rewardTokens)}`;
|
||||
case 2:
|
||||
return '✓ 已领取';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取按钮样式类
|
||||
function getButtonClass(task: DailyTaskItem): string {
|
||||
switch (task.status) {
|
||||
case 0:
|
||||
return 'btn-disabled';
|
||||
case 1:
|
||||
return 'btn-claimable';
|
||||
case 2:
|
||||
return 'btn-claimed';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取进度条颜色
|
||||
function getProgressColor(task: DailyTaskItem): string {
|
||||
if (task.status === 2) return '#FFD700'; // 已完成:金色
|
||||
if (task.status === 1) return '#67C23A'; // 可领取:绿色
|
||||
return '#409EFF'; // 进行中:蓝色
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="daily-task-container">
|
||||
<div class="task-header">
|
||||
<h2>每日任务</h2>
|
||||
<p class="task-desc">完成每日任务,领取额外尊享包 Token 奖励,可累加重复</p>
|
||||
</div>
|
||||
|
||||
<div v-if="taskData" class="task-content">
|
||||
<!-- 今日消耗统计 -->
|
||||
<div class="consumption-card">
|
||||
<div class="consumption-icon">🔥</div>
|
||||
<div class="consumption-info">
|
||||
<div class="consumption-label">今日尊享包消耗</div>
|
||||
<div class="consumption-value">
|
||||
{{ formatTokenDisplay(taskData.todayConsumedTokens) }} Tokens
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<div class="task-list">
|
||||
<div
|
||||
v-for="task in taskData.tasks"
|
||||
:key="task.level"
|
||||
class="task-item"
|
||||
:class="{
|
||||
'task-completed': task.status === 2,
|
||||
'task-claimable': task.status === 1
|
||||
}"
|
||||
>
|
||||
<div class="task-icon">
|
||||
<span v-if="task.status === 2">🎁</span>
|
||||
<span v-else-if="task.status === 1">✨</span>
|
||||
<span v-else>📦</span>
|
||||
</div>
|
||||
|
||||
<div class="task-main">
|
||||
<div class="task-title">
|
||||
<span class="task-name">{{ task.name }}</span>
|
||||
<span class="task-badge" :class="`badge-status-${task.status}`">
|
||||
{{ task.status === 0 ? '未完成' : task.status === 1 ? '可领取' : '已完成' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="task-description">
|
||||
{{ task.description }}
|
||||
</div>
|
||||
|
||||
<div class="task-progress-section">
|
||||
<div class="progress-info">
|
||||
<span class="progress-text">
|
||||
{{ formatTokenDisplay(taskData.todayConsumedTokens) }} / {{ formatTokenDisplay(task.requiredTokens) }}
|
||||
</span>
|
||||
<span class="progress-percent">{{ task.progress.toFixed(0) }}%</span>
|
||||
</div>
|
||||
<el-progress
|
||||
:percentage="Math.min(task.progress, 100)"
|
||||
:color="getProgressColor(task)"
|
||||
:show-text="false"
|
||||
:stroke-width="8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="task-reward">
|
||||
<span class="reward-label">奖励:</span>
|
||||
<span class="reward-value">{{ formatTokenDisplay(task.rewardTokens) }} Tokens</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-action">
|
||||
<el-button
|
||||
:class="getButtonClass(task)"
|
||||
:disabled="task.status !== 1 || claiming[task.level]"
|
||||
:loading="claiming[task.level]"
|
||||
size="large"
|
||||
@click="handleClaim(task)"
|
||||
>
|
||||
{{ getStatusText(task) }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="task-tips">
|
||||
<el-alert
|
||||
title="温馨提示"
|
||||
type="info"
|
||||
:closable="false"
|
||||
>
|
||||
<template #default>
|
||||
<ul>
|
||||
<li>任务每日 0 点自动重置</li>
|
||||
<li>使用尊享包模型消耗的 Token 计入任务进度</li>
|
||||
<li>完成任务后立即领取奖励,奖励直接发放到您的尊享包账户</li>
|
||||
</ul>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.daily-task-container {
|
||||
padding: 20px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.task-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.task-header h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.task-desc {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.task-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 消耗统计卡片 */
|
||||
.consumption-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.consumption-icon {
|
||||
font-size: 48px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.consumption-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.consumption-label {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.consumption-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 任务列表 */
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding: 20px;
|
||||
background: #ffffff;
|
||||
border: 2px solid #e4e7ed;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.task-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.task-item.task-claimable {
|
||||
border-color: #67C23A;
|
||||
background: linear-gradient(to right, rgba(103, 194, 58, 0.05) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.task-item.task-completed {
|
||||
border-color: #FFD700;
|
||||
background: linear-gradient(to right, rgba(255, 215, 0, 0.05) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.task-icon {
|
||||
font-size: 48px;
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.task-badge {
|
||||
padding: 2px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-status-0 {
|
||||
background: #f4f4f5;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.badge-status-1 {
|
||||
background: #f0f9ff;
|
||||
color: #67C23A;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.badge-status-2 {
|
||||
background: #fffbf0;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.task-description {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.task-progress-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.progress-percent {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.task-reward {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.reward-label {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.reward-value {
|
||||
color: #F56C6C;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.task-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-disabled {
|
||||
background: #f4f4f5;
|
||||
border-color: #e4e7ed;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.btn-claimable {
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
.btn-claimable:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 16px rgba(255, 215, 0, 0.5);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 20px rgba(255, 215, 0, 0.6);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 40px rgba(255, 215, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-claimed {
|
||||
background: #f4f4f5;
|
||||
border-color: #e4e7ed;
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
/* 提示信息 */
|
||||
.task-tips {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.task-tips ul {
|
||||
margin: 8px 0 0 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.task-tips li {
|
||||
margin: 4px 0;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user