Jelajahi Sumber

feat: 大厅页面优化及我的任务页面修复

功能改进:
- 大厅页面新增"任务中心"入口,点击可跳转查看已领取的任务
- 大厅任务列表显示任务封面图和分类图标
- 任务列表页面修复图片显示

Bug修复:
- 修复 task.taskList 国际化key未翻译的问题
- 修复我的任务页面图片显示问号的问题
- 修复我的任务页面价格只显示"+ USDT"没有金额的问题
- 修复时间格式化函数处理时间戳的问题
urbanu 1 bulan lalu
induk
melakukan
17badf06a6

+ 4 - 0
src/locales/en.json

@@ -138,7 +138,11 @@
     "currentlyCompletedTasks": "Currently Completed Tasks",
     "unit": "Unit"
   },
+  "hall": {
+    "viewMyTasks": "View My Tasks"
+  },
   "task": {
+    "taskList": "Task List",
     "taskDetail": "Task Detail",
     "taskPrice": "Reward",
     "taskDifficulty": "Difficulty",

+ 4 - 0
src/locales/id.json

@@ -138,7 +138,11 @@
     "currentlyCompletedTasks": "Tugas yang diselesaikan",
     "unit": "Unit"
   },
+  "hall": {
+    "viewMyTasks": "Lihat Tugas Saya"
+  },
   "task": {
+    "taskList": "Daftar Tugas",
     "taskDetail": "Detail Tugas",
     "taskPrice": "Hadiah",
     "taskDifficulty": "Kesulitan",

+ 4 - 0
src/locales/vi.json

@@ -138,7 +138,11 @@
     "currentlyCompletedTasks": "Nhiệm vụ đã hoàn thành",
     "unit": "Đơn vị"
   },
+  "hall": {
+    "viewMyTasks": "Xem nhiệm vụ của tôi"
+  },
   "task": {
+    "taskList": "Danh sách nhiệm vụ",
     "taskDetail": "Chi tiết nhiệm vụ",
     "taskPrice": "Phần thưởng",
     "taskDifficulty": "Độ khó",

+ 4 - 0
src/locales/zh.json

@@ -138,7 +138,11 @@
     "currentlyCompletedTasks": "当前已完成任务",
     "unit": "单位"
   },
+  "hall": {
+    "viewMyTasks": "查看我的任务"
+  },
   "task": {
+    "taskList": "任务列表",
     "taskDetail": "任务详情",
     "taskPrice": "任务奖励",
     "taskDifficulty": "任务难度",

+ 122 - 22
src/views/hall/index.vue

@@ -14,6 +14,18 @@
       </div>
     </div>
 
+    <!-- 我的任务入口 -->
+    <div class="my-task-entry" @click="goMyTask">
+      <div class="entry-left">
+        <span class="entry-icon">📋</span>
+        <span class="entry-text">{{ $t('home.taskCenter') }}</span>
+      </div>
+      <div class="entry-right">
+        <span class="entry-hint">{{ $t('hall.viewMyTasks') }}</span>
+        <van-icon name="arrow" />
+      </div>
+    </div>
+
     <!-- 任务列表 -->
     <div class="task-section">
       <div class="section-title">{{ $t('home.taskList') }}</div>
@@ -26,13 +38,21 @@
           @click="goTaskDetail(task.id)"
         >
           <div class="task-header">
-            <div class="category-tag">{{ task.categoryName || 'TikTok' }}</div>
+            <div class="category-tag">
+              <img v-if="task.categoryIcon" :src="task.categoryIcon" class="category-icon" alt="" />
+              <span>{{ task.categoryName || 'TikTok' }}</span>
+            </div>
           </div>
-          <div class="task-body">
-            <div class="task-title">{{ task.title }}</div>
-            <div class="task-footer">
-              <span class="price">+{{ formatReward(task.rewardAmount ?? task.price) }}</span>
-              <span class="remaining">{{ $t('task.remaining') }}: {{ task.remainCount ?? (task.maxNum - task.applyNum) }}</span>
+          <div class="task-content">
+            <div class="task-cover" v-if="task.cover">
+              <img :src="task.cover" alt="" />
+            </div>
+            <div class="task-body">
+              <div class="task-title">{{ task.title }}</div>
+              <div class="task-footer">
+                <span class="price">+{{ formatReward(task.rewardAmount ?? task.price) }}</span>
+                <span class="remaining">{{ $t('task.remaining') }}: {{ task.remainCount ?? (task.maxNum - task.applyNum) }}</span>
+              </div>
             </div>
           </div>
         </div>
@@ -144,6 +164,11 @@ const goTaskDetail = (id: number) => {
   router.push(`/task/detail/${id}`);
 };
 
+// 跳转我的任务
+const goMyTask = () => {
+  router.push('/my-task');
+};
+
 onMounted(() => {
   getPlatformStats();
   getTaskList(true);
@@ -158,6 +183,49 @@ onMounted(() => {
   padding-bottom: min(21.333vw, 102.4px);
 }
 
+.my-task-entry {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  background: linear-gradient(135deg, rgba(33, 150, 243, 0.15) 0%, rgba(33, 150, 243, 0.08) 100%);
+  border: 1px solid rgba(33, 150, 243, 0.3);
+  border-radius: min(2.667vw, 12px);
+  padding: min(3.2vw, 14px) min(4vw, 18px);
+  margin-bottom: min(4vw, 18px);
+  cursor: pointer;
+
+  .entry-left {
+    display: flex;
+    align-items: center;
+    gap: min(2.133vw, 10px);
+
+    .entry-icon {
+      font-size: min(5.333vw, 24px);
+    }
+
+    .entry-text {
+      font-size: min(3.733vw, 16px);
+      font-weight: 600;
+      color: #fff;
+    }
+  }
+
+  .entry-right {
+    display: flex;
+    align-items: center;
+    gap: min(1.333vw, 6px);
+    color: rgba(255, 255, 255, 0.6);
+
+    .entry-hint {
+      font-size: min(3.2vw, 14px);
+    }
+
+    .van-icon {
+      font-size: min(3.2vw, 14px);
+    }
+  }
+}
+
 .stats-section {
   display: flex;
   gap: min(3.2vw, 15.36px);
@@ -211,37 +279,69 @@ onMounted(() => {
       margin-bottom: min(1.6vw, 8px);
 
       .category-tag {
-        display: inline-block;
+        display: inline-flex;
+        align-items: center;
+        gap: min(1.067vw, 5px);
         background: rgba(255, 195, 0, 0.2);
         color: #ffc300;
         padding: min(0.8vw, 4px) min(2.133vw, 10px);
         border-radius: min(0.8vw, 4px);
         font-size: min(2.933vw, 14px);
+
+        .category-icon {
+          width: min(3.733vw, 18px);
+          height: min(3.733vw, 18px);
+          object-fit: contain;
+        }
       }
     }
 
-    .task-body {
-      .task-title {
-        font-size: min(3.467vw, 16px);
-        color: #fff;
-        margin-bottom: min(1.6vw, 8px);
-        line-height: 1.3;
+    .task-content {
+      display: flex;
+      gap: min(2.667vw, 12px);
+
+      .task-cover {
+        flex-shrink: 0;
+        width: min(16vw, 70px);
+        height: min(16vw, 70px);
+        border-radius: min(1.6vw, 8px);
+        overflow: hidden;
+        background: rgba(255, 255, 255, 0.1);
+
+        img {
+          width: 100%;
+          height: 100%;
+          object-fit: cover;
+        }
       }
 
-      .task-footer {
+      .task-body {
+        flex: 1;
         display: flex;
+        flex-direction: column;
         justify-content: space-between;
-        align-items: center;
 
-        .price {
-          font-size: min(4.267vw, 20px);
-          font-weight: bold;
-          color: #ffc300;
+        .task-title {
+          font-size: min(3.467vw, 16px);
+          color: #fff;
+          line-height: 1.3;
         }
 
-        .remaining {
-          font-size: min(2.933vw, 14px);
-          color: rgba(255, 255, 255, 0.5);
+        .task-footer {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+
+          .price {
+            font-size: min(4.267vw, 20px);
+            font-weight: bold;
+            color: #ffc300;
+          }
+
+          .remaining {
+            font-size: min(2.933vw, 14px);
+            color: rgba(255, 255, 255, 0.5);
+          }
         }
       }
     }

+ 19 - 4
src/views/my-task/index.vue

@@ -36,17 +36,17 @@
             @click="goDetail(item)"
           >
             <div class="task-img">
-              <img :src="item.task?.imageMaterial?.[0] || defaultIcon" />
+              <img :src="item.task?.cover || item.task?.imageMaterial?.[0] || defaultIcon" />
             </div>
             <div class="task-info">
-              <div class="task-title">{{ item.task?.title }}</div>
+              <div class="task-title">{{ item.taskTitle || item.task?.title }}</div>
               <div class="task-meta">
                 <span class="time">{{ formatTime(item.createdAt) }}</span>
                 <span class="status" :class="getStatusClass(item.status)">
                   {{ getStatusText(item.status) }}
                 </span>
               </div>
-              <div class="task-price">+{{ item.task?.price }} USDT</div>
+              <div class="task-price">+{{ formatReward(item.rewardAmount || item.task?.rewardAmount) }} USDT</div>
             </div>
           </div>
         </van-list>
@@ -109,10 +109,25 @@ const getStatusText = (s: number) => {
   return map[s] || '';
 };
 
-const formatTime = (time: string) => {
+const formatTime = (time: string | number) => {
+  if (!time) return '';
+  // 如果是时间戳(秒)
+  if (typeof time === 'number') {
+    return dayjs.unix(time).format('YYYY-MM-DD HH:mm');
+  }
   return dayjs(time).format('YYYY-MM-DD HH:mm');
 };
 
+// 格式化奖励金额
+const formatReward = (amount: number | string | undefined) => {
+  if (amount === undefined || amount === null) return '0.00';
+  const num = typeof amount === 'string' ? parseFloat(amount) : amount;
+  if (num > 100) {
+    return (num / 100).toFixed(2);
+  }
+  return num.toFixed(2);
+};
+
 const getTaskList = async (isRefresh = false) => {
   if (isRefresh) {
     page.value = 1;

+ 1 - 1
src/views/task/list.vue

@@ -54,7 +54,7 @@
             @click="goDetail(task.id)"
           >
             <div class="task-img">
-              <img :src="task.imageMaterial?.[0] || defaultIcon" />
+              <img :src="task.cover || task.imageMaterial?.[0] || defaultIcon" />
               <div class="task-tag" :class="getDifficultyClass(task.difficulty)">
                 {{ getDifficultyText(task.difficulty) }}
               </div>