Преглед на файлове

feat(frontend): 首页新增今日任务卡片 daily-task-card + 聚合接口封装

- utils/api.js 新增 getDailyTaskOverview()
- daily-task-card.vue 自带 onShow 拉取刷新,四分组渲染 + 全部完成空态 + 失败重试
- index-home 挂载到 JourneyCard 与 WuxingSandbox 之间(登录态容器内,未登录不显示)
- 遵从小程序限制:无 ?. 可选链、:key 用方法调用
asus преди 1 месец
родител
ревизия
3fec8e0948
променени са 3 файла, в които са добавени 221 реда и са изтрити 1 реда
  1. 210 0
      cfc-frontend/components/daily-task-card.vue
  2. 6 1
      cfc-frontend/pages/index-home/index.vue
  3. 5 0
      cfc-frontend/utils/api.js

+ 210 - 0
cfc-frontend/components/daily-task-card.vue

@@ -0,0 +1,210 @@
+<template>
+  <view class="daily-task-card" v-if="overview">
+    <view class="daily-task-head">
+      <text class="daily-task-title">今日任务</text>
+      <text class="daily-task-date">{{ overview.date }}</text>
+    </view>
+
+    <view v-if="loading" class="daily-task-state">
+      <text class="daily-task-state-text">加载中...</text>
+    </view>
+
+    <block v-else-if="error">
+      <view class="daily-task-state" @click="loadOverview">
+        <text class="daily-task-state-text">加载失败,点击重试</text>
+      </view>
+    </block>
+
+    <block v-else>
+      <!-- 打卡类 -->
+      <view class="daily-task-group" v-if="overview.checkins && overview.checkins.length > 0">
+        <view class="daily-task-group-title">打卡</view>
+        <view class="daily-task-item" v-for="item in overview.checkins" :key="getCheckinKey(item)" @click="go(item.route)">
+          <text class="daily-task-item-label">{{ item.label }}</text>
+          <text v-if="item.done" class="daily-task-done">✓</text>
+          <text v-else class="daily-task-go">去完成</text>
+        </view>
+      </view>
+
+      <!-- 任务类 -->
+      <view class="daily-task-group" v-if="overview.tasks">
+        <view class="daily-task-group-title">任务</view>
+        <view class="daily-task-item" @click="go(overview.tasks.route)">
+          <text class="daily-task-item-label">待完成 {{ overview.tasks.todoToday }} · 已完成 {{ overview.tasks.doneToday }}</text>
+          <text v-if="overview.tasks.todoToday > 0" class="daily-task-go">去完成</text>
+          <text v-else class="daily-task-done">✓</text>
+        </view>
+      </view>
+
+      <!-- 阅读活动 -->
+      <view class="daily-task-group" v-if="overview.activities">
+        <view class="daily-task-group-title">阅读活动</view>
+        <view class="daily-task-item" @click="go(overview.activities.articleRoute)">
+          <text class="daily-task-item-label">今日已读 {{ overview.activities.articleReadToday }} 篇</text>
+          <text v-if="overview.activities.articleReadToday > 0" class="daily-task-done">✓</text>
+          <text v-else class="daily-task-go">去阅读</text>
+        </view>
+      </view>
+
+      <!-- 测评报告 -->
+      <view class="daily-task-group" v-if="overview.assessments">
+        <view class="daily-task-group-title">测评报告</view>
+        <view class="daily-task-item" @click="go(overview.assessments.route)">
+          <text class="daily-task-item-label">进行中 {{ overview.assessments.ongoing }} · 待处理 {{ overview.assessments.reportPending }}</text>
+          <text v-if="overview.assessments.ongoing > 0 || overview.assessments.reportPending > 0" class="daily-task-go">去处理</text>
+          <text v-else class="daily-task-done">✓</text>
+        </view>
+      </view>
+
+      <!-- 全部完成空态 -->
+      <view class="daily-task-all" v-if="allDone()">
+        <text class="daily-task-all-text">今日任务已全部完成 🎉</text>
+      </view>
+    </block>
+  </view>
+</template>
+
+<script>
+import { getDailyTaskOverview } from '../utils/api.js'
+
+export default {
+  name: 'DailyTaskCard',
+  data() {
+    return {
+      overview: null,
+      loading: false,
+      error: false
+    }
+  },
+  onShow() {
+    if (!uni.getStorageSync('token')) return
+    this.loadOverview()
+  },
+  methods: {
+    loadOverview: function() {
+      var self = this
+      self.loading = true
+      self.error = false
+      getDailyTaskOverview().then(function(res) {
+        self.loading = false
+        if (res.code === 200 && res.data) {
+          self.overview = res.data
+        } else {
+          self.overview = null
+          self.error = true
+        }
+      }).catch(function() {
+        self.loading = false
+        self.overview = null
+        self.error = true
+      })
+    },
+    getCheckinKey: function(item) {
+      return item.key || item.label || ''
+    },
+    go: function(route) {
+      if (!route) return
+      uni.navigateTo({ url: route })
+    },
+    allDone: function() {
+      var o = this.overview
+      if (!o) return false
+      var all = true
+      if (o.checkins) {
+        var i
+        for (i = 0; i < o.checkins.length; i++) {
+          if (!o.checkins[i].done) all = false
+        }
+      }
+      if (o.tasks && o.tasks.todoToday > 0) all = false
+      if (o.activities && o.activities.articleReadToday === 0) all = false
+      if (o.assessments && (o.assessments.ongoing > 0 || o.assessments.reportPending > 0)) all = false
+      return all
+    }
+  }
+}
+</script>
+
+<style scoped>
+.daily-task-card {
+  margin: 24rpx 24rpx 0;
+  padding: 28rpx;
+  border-radius: 24rpx;
+  background: #FFFFFF;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
+}
+.daily-task-head {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+.daily-task-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #1F2937;
+}
+.daily-task-date {
+  font-size: 24rpx;
+  color: #9CA3AF;
+}
+.daily-task-state {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  padding: 24rpx 0;
+}
+.daily-task-state-text {
+  font-size: 26rpx;
+  color: #9CA3AF;
+}
+.daily-task-group {
+  display: flex;
+  flex-direction: column;
+  margin-bottom: 16rpx;
+}
+.daily-task-group-title {
+  font-size: 24rpx;
+  color: #9CA3AF;
+  margin-bottom: 8rpx;
+}
+.daily-task-item {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  padding: 18rpx 0;
+  border-bottom: 1rpx solid #F3F4F6;
+}
+.daily-task-item:last-child {
+  border-bottom: none;
+}
+.daily-task-item-label {
+  font-size: 28rpx;
+  color: #374151;
+}
+.daily-task-go {
+  font-size: 26rpx;
+  color: #F97316;
+  font-weight: 500;
+}
+.daily-task-done {
+  font-size: 26rpx;
+  color: #10B981;
+  font-weight: 600;
+}
+.daily-task-all {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  padding: 12rpx 0;
+}
+.daily-task-all-text {
+  font-size: 28rpx;
+  color: #10B981;
+  font-weight: 500;
+}
+</style>

+ 6 - 1
cfc-frontend/pages/index-home/index.vue

@@ -63,6 +63,9 @@
       :intent="userIntent"
       @action="handleJourneyAction" />
 
+    <!-- 今日任务卡片 — 每日任务统一入口 -->
+    <DailyTaskCard />
+
     <!-- 定向科普内容 — F17 B类问卷结果定向展示 -->
     <view v-if="targetedArticles.length > 0" class="section">
       <view class="section-header">
@@ -359,6 +362,7 @@ import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
 import IntentPicker from '../../components/intent-picker.vue'
 import JourneyCard from '../../components/journey-card.vue'
+import DailyTaskCard from '../../components/daily-task-card.vue'
 import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getChildren, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory, getNotices, getUserIntent, chooseUserIntent, getContentByTags } from '../../utils/api.js'
 import nav from '../../utils/nav.js'
 import config from '@/config.js'
@@ -372,7 +376,8 @@ export default {
     ArticleBookshelf,
     FamilyChallengeCard,
     IntentPicker,
-    JourneyCard
+    JourneyCard,
+    DailyTaskCard
   },
   data() {
     // 从 storage 预取登录态/角色,确保首次渲染即正确(WeChat 渲染层/逻辑层分离架构下 onLoad 执行前已渲染)

+ 5 - 0
cfc-frontend/utils/api.js

@@ -2701,3 +2701,8 @@ export const updateLogistics = (data) => request('/api/logistics/update', 'POST'
 export const getMyLogistics = () => request('/api/logistics/my', 'POST', {})
 // 内容标签过滤
 export const getContentByTags = (data) => request('/api/content/by-tags', 'POST', data)
+
+// ===== 今日任务总览(首页卡片聚合:打卡/任务/阅读活动/测评报告) =====
+export const getDailyTaskOverview = () => {
+  return request('/api/daily-task/overview', 'POST', {})
+}