index.vue 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. <template>
  2. <div class="my-task-page">
  3. <!-- 返回按钮 -->
  4. <div class="header">
  5. <van-icon name="arrow-left" @click="goBack" />
  6. <span>{{ $t('home.taskCenter') }}</span>
  7. <span></span>
  8. </div>
  9. <!-- 状态Tab -->
  10. <div class="status-tabs">
  11. <div
  12. class="tab-item"
  13. v-for="item in statusTabs"
  14. :key="item.value"
  15. :class="{ active: status === item.value }"
  16. @click="status = item.value"
  17. >
  18. {{ item.label }}
  19. </div>
  20. </div>
  21. <!-- 任务列表 -->
  22. <div class="task-list">
  23. <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
  24. <van-list
  25. v-model:loading="loading"
  26. :finished="finished"
  27. :finished-text="$t('common.noData')"
  28. @load="loadMore"
  29. >
  30. <div
  31. class="task-card"
  32. v-for="item in taskList"
  33. :key="item.id"
  34. @click="goDetail(item)"
  35. >
  36. <div class="task-img">
  37. <img :src="item.task?.cover || item.task?.imageMaterial?.[0] || defaultIcon" />
  38. </div>
  39. <div class="task-info">
  40. <div class="task-title">{{ item.taskTitle || item.task?.title }}</div>
  41. <div class="task-meta">
  42. <span class="time">{{ formatTime(item.createdAt) }}</span>
  43. <span class="status" :class="getStatusClass(item.status)">
  44. {{ getStatusText(item.status) }}
  45. </span>
  46. </div>
  47. <div class="task-price">+{{ formatReward(item.rewardAmount || item.task?.rewardAmount) }} USDT</div>
  48. </div>
  49. </div>
  50. </van-list>
  51. </van-pull-refresh>
  52. </div>
  53. </div>
  54. </template>
  55. <script setup lang="ts">
  56. import { ref, computed, watch, onMounted } from "vue";
  57. import { useRouter } from "vue-router";
  58. import { useI18n } from "vue-i18n";
  59. import dayjs from "dayjs";
  60. import { requestMyTaskList } from "@/api/task";
  61. import defaultIcon from "@/assets/images/common/no-data.svg";
  62. const { t } = useI18n();
  63. const router = useRouter();
  64. const status = ref<number>(-99);
  65. const taskList = ref<TaskApplyInfo[]>([]);
  66. const loading = ref(false);
  67. const finished = ref(false);
  68. const refreshing = ref(false);
  69. const page = ref(1);
  70. const pageSize = 10;
  71. // 后端状态: -2=已放弃 -1=审核失败 0=进行中 1=待审核 2=已完成 5=已打回
  72. const statusTabs = computed(() => [
  73. { label: t('taskStatus.all'), value: -99 },
  74. { label: t('taskStatus.inProgress'), value: 0 },
  75. { label: t('taskStatus.pendingReview'), value: 1 },
  76. { label: t('taskStatus.completed'), value: 2 },
  77. { label: t('taskStatus.returnBack'), value: 5 },
  78. { label: t('taskStatus.rejected'), value: -1 }
  79. ]);
  80. // 后端状态: -2=已放弃 -1=审核失败 0=进行中 1=待审核 2=已完成 5=已打回
  81. const getStatusClass = (s: number) => {
  82. const map: Record<number, string> = {
  83. [-2]: 'abandoned',
  84. [-1]: 'rejected',
  85. 0: 'in-progress',
  86. 1: 'pending',
  87. 2: 'completed',
  88. 5: 'return-back'
  89. };
  90. return map[s] || '';
  91. };
  92. const getStatusText = (s: number) => {
  93. const map: Record<number, string> = {
  94. [-2]: t('taskStatus.abandoned'),
  95. [-1]: t('taskStatus.rejected'),
  96. 0: t('taskStatus.inProgress'),
  97. 1: t('taskStatus.pendingReview'),
  98. 2: t('taskStatus.completed'),
  99. 5: t('taskStatus.returnBack')
  100. };
  101. return map[s] || '';
  102. };
  103. const formatTime = (time: string | number) => {
  104. if (!time) return '';
  105. // 如果是时间戳(秒)
  106. if (typeof time === 'number') {
  107. return dayjs.unix(time).format('YYYY-MM-DD HH:mm');
  108. }
  109. return dayjs(time).format('YYYY-MM-DD HH:mm');
  110. };
  111. // 格式化奖励金额
  112. const formatReward = (amount: number | string | undefined) => {
  113. if (amount === undefined || amount === null) return '0.00';
  114. const num = typeof amount === 'string' ? parseFloat(amount) : amount;
  115. if (num > 100) {
  116. return (num / 100).toFixed(2);
  117. }
  118. return num.toFixed(2);
  119. };
  120. const getTaskList = async (isRefresh = false) => {
  121. if (isRefresh) {
  122. page.value = 1;
  123. finished.value = false;
  124. }
  125. try {
  126. const res = await requestMyTaskList({
  127. current: page.value,
  128. size: pageSize,
  129. status: status.value === -99 ? undefined : status.value
  130. });
  131. if (res.code === 200) {
  132. const list = res.data?.list || [];
  133. if (isRefresh) {
  134. taskList.value = list;
  135. } else {
  136. taskList.value = [...taskList.value, ...list];
  137. }
  138. if (list.length < pageSize) {
  139. finished.value = true;
  140. }
  141. }
  142. } catch (e) {
  143. // 模拟数据
  144. const mockStatuses = status.value ? [status.value] : [1, 2, 3, 4];
  145. const mockList = Array(pageSize).fill(null).map((_, i) => ({
  146. id: (page.value - 1) * pageSize + i + 1,
  147. taskId: i + 1,
  148. status: mockStatuses[Math.floor(Math.random() * mockStatuses.length)],
  149. createdAt: dayjs().subtract(i, 'day').toISOString(),
  150. task: {
  151. id: i + 1,
  152. title: `Task ${(page.value - 1) * pageSize + i + 1} - Complete social media interaction`,
  153. price: (Math.random() * 10 + 2).toFixed(2),
  154. imageMaterial: []
  155. }
  156. })) as TaskApplyInfo[];
  157. if (isRefresh) {
  158. taskList.value = mockList;
  159. } else {
  160. taskList.value = [...taskList.value, ...mockList];
  161. }
  162. if (page.value >= 2) {
  163. finished.value = true;
  164. }
  165. }
  166. loading.value = false;
  167. refreshing.value = false;
  168. };
  169. const onRefresh = () => {
  170. getTaskList(true);
  171. };
  172. const loadMore = () => {
  173. page.value++;
  174. getTaskList();
  175. };
  176. const goBack = () => {
  177. router.back();
  178. };
  179. const goDetail = (item: TaskApplyInfo) => {
  180. if (item.status === 0 || item.status === 5 || item.status === 3 || item.status === -1) {
  181. // 进行中(0)、已拒绝(3/-1)或已打回(5) - 跳转提交页
  182. router.push(`/task/submit/${item.id}`);
  183. } else {
  184. // 其他状态 - 跳转详情页
  185. router.push(`/task/detail/${item.taskId}`);
  186. }
  187. };
  188. watch(status, () => {
  189. getTaskList(true);
  190. });
  191. onMounted(() => {
  192. getTaskList(true);
  193. });
  194. </script>
  195. <style lang="scss" scoped>
  196. .my-task-page {
  197. min-height: 100vh;
  198. background: #121212;
  199. padding-bottom: 80px;
  200. }
  201. .header {
  202. display: flex;
  203. align-items: center;
  204. justify-content: space-between;
  205. padding: 16px;
  206. color: #fff;
  207. font-size: 16px;
  208. font-weight: 600;
  209. .van-icon {
  210. font-size: 20px;
  211. }
  212. }
  213. .status-tabs {
  214. display: flex;
  215. gap: 8px;
  216. padding: 0 16px 16px;
  217. overflow-x: auto;
  218. &::-webkit-scrollbar {
  219. display: none;
  220. }
  221. .tab-item {
  222. flex-shrink: 0;
  223. padding: 8px 14px;
  224. background: rgba(255, 255, 255, 0.05);
  225. border-radius: 16px;
  226. font-size: 12px;
  227. color: rgba(255, 255, 255, 0.6);
  228. &.active {
  229. background: linear-gradient(135deg, #ffc300 0%, #ff9500 100%);
  230. color: #000;
  231. font-weight: 600;
  232. }
  233. }
  234. }
  235. .task-list {
  236. padding: 0 16px;
  237. }
  238. .task-card {
  239. display: flex;
  240. background: rgba(255, 255, 255, 0.05);
  241. border-radius: 12px;
  242. overflow: hidden;
  243. margin-bottom: 12px;
  244. .task-img {
  245. width: 90px;
  246. height: 90px;
  247. flex-shrink: 0;
  248. img {
  249. width: 100%;
  250. height: 100%;
  251. object-fit: cover;
  252. }
  253. }
  254. .task-info {
  255. flex: 1;
  256. padding: 12px;
  257. display: flex;
  258. flex-direction: column;
  259. justify-content: space-between;
  260. .task-title {
  261. font-size: 14px;
  262. color: #fff;
  263. line-height: 1.3;
  264. display: -webkit-box;
  265. -webkit-line-clamp: 2;
  266. -webkit-box-orient: vertical;
  267. overflow: hidden;
  268. }
  269. .task-meta {
  270. display: flex;
  271. align-items: center;
  272. gap: 12px;
  273. .time {
  274. font-size: 11px;
  275. color: rgba(255, 255, 255, 0.4);
  276. }
  277. .status {
  278. font-size: 11px;
  279. padding: 2px 8px;
  280. border-radius: 10px;
  281. &.in-progress {
  282. background: rgba(33, 150, 243, 0.2);
  283. color: #2196f3;
  284. }
  285. &.pending {
  286. background: rgba(255, 152, 0, 0.2);
  287. color: #ff9800;
  288. }
  289. &.completed {
  290. background: rgba(76, 175, 80, 0.2);
  291. color: #4caf50;
  292. }
  293. &.rejected {
  294. background: rgba(244, 67, 54, 0.2);
  295. color: #f44336;
  296. }
  297. &.abandoned {
  298. background: rgba(158, 158, 158, 0.2);
  299. color: #9e9e9e;
  300. }
  301. &.return-back {
  302. background: rgba(255, 152, 0, 0.2);
  303. color: #ff9800;
  304. }
  305. }
  306. }
  307. .task-price {
  308. font-size: 15px;
  309. font-weight: bold;
  310. color: #ffc300;
  311. }
  312. }
  313. }
  314. </style>