2026-08-15-daily-task-overview.md 34 KB

「今日任务」首页卡片 + 只读聚合接口 实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 首页新增「今日任务」卡片,聚合打卡/任务/阅读活动/测评报告四类今日状态,点击跳转对应功能页,返回自动刷新。

Architecture: 后端新增 1 个只读聚合接口 POST /api/daily-task/overview(DailyTaskController + DailyOverviewService + DailyOverviewVO),复用现有 service/mapper 查询逻辑,零表结构改动;前端新增 components/daily-task-card.vue 组件(自带 onShow 拉取),挂载到首页 index-home。

Tech Stack: Spring Boot 2.7.18 + MyBatis-Plus + Java 8;uni-app Vue 2(Options API)微信小程序。

Global Constraints

  • 接口统一 @PostMapping,禁止 @GetMapping/@PutMapping/@DeleteMapping(AGENTS.md)
  • 响应统一 Result<T>(code=200/message/data),Result.noFamily code=5001(Result.java)
  • DI 用 @Resource,字段名与类型默认 Bean Name 一致(AGENTS.md)
  • 认证:JWT Bearer Token;当前用户 = @RequestAttribute("userId"),当前家庭成员 = @RequestAttribute(value="currentMemberId", required=false),家庭 = @RequestAttribute(value="familyId", required=false);禁止使用 @RequestAttribute("memberId")(代码库无此 attribute)(JwtInterceptor.java:131-165)
  • memberId 回退链:请求体 memberId → currentMemberId(JwtInterceptor 注入)→ 成长任务层用 userId(GrowthTaskService.getTaskList 以 userId 为参数)
  • 聚合接口只读:不写库、不改任何表结构(spec §4.2)
  • 小程序限制:禁止可选链 ?.(用 &&/逐字段判断)、禁止 CSS Grid(用 flexbox)、禁止 :key 表达式(用方法调用 :key="getXxxKey(item)")、禁止 new Date(string)(用 utils/format.js 的 parseDate())、数组 props 必须 default: function() { return [] }、:class 绑定禁用方法调用(cfc-frontend/AGENTS.md)
  • 前端打包禁止 npm build 命令,只做 node --check 语法校验,打包走 HBuilderX(cfc-frontend/AGENTS.md)
  • 后端验证:cd cfc-backend && mvn clean compile;测试:mvn test(无 H2/Testcontainers,靠 @MockBean 隔离 DB)

Task 1: 后端聚合核心 — DailyOverviewVO + DailyOverviewService

Files:

  • Create: cfc-backend/src/main/java/com/etotem/cfc/dto/DailyOverviewVO.java
  • Create: cfc-backend/src/main/java/com/etotem/cfc/service/DailyOverviewService.java
  • Test: cfc-backend/src/test/java/com/etotem/cfc/service/DailyOverviewServiceTest.java

Interfaces:

  • Consumes: 现有 service 方法(本文件列出签名,任务内逐一使用)
  • Produces: DailyOverviewService.aggregate(Long userId, Long memberId, Long familyId) → DailyOverviewVO(Task 2 的 Controller 调用)
  • Produces: DailyOverviewVO 结构(checkins/tasks/activities/assessments/hasMore),Task 3 前端按此渲染

前提校验步骤(开始编码前执行):

  • [ ] 步骤 0: 确认实体字段名(若与下方代码不同则替换字段名,方法签名不变):

    • Resource: HealthCheckin.getCheckinDate()(java.util.Date,列 checkin_date,MicroActionService.java:187-193 已用 DATE(checkin_date))
    • Resource: FinanceCheckin.getCheckinDate()(java.util.Date,同列名约定)
    • Resource: EmotionCheckinVO.getCheckinDate()(java.util.Date,dto/EmotionCheckinVO.java:18,与 health/finance 统一用 checkinDate)
    • Resource: ArticleReadingRecord.getChildId()(列 child_id)与 getReadAt()(列 read_at,ArticleService.java:321 已用 DATE(read_at)=CURDATE())
    • Resource: AssessmentRecord.getStatus()/getChildId()、DanReportUploadVO.getDraftStatus()(dto/DanReportUploadVO.java:26,常量 DanReportUpload.DRAFT_PENDING="pending",entity/DanReportUpload.java:71)
    • 若某字段名不同,改下方代码中对应字段引用,其余不动
  • [ ] Step 1: 写失败测试

创建 DailyOverviewServiceTest.java:

package com.etotem.cfc.service;

import com.etotem.cfc.common.Result;
import com.etotem.cfc.dto.DailyOverviewVO;
import com.etotem.cfc.dto.DanReportUploadVO;
import com.etotem.cfc.entity.AssessmentRecord;
import com.etotem.cfc.entity.Task;
import com.etotem.cfc.mapper.ArticleReadingRecordMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import javax.annotation.Resource;

import java.util.*;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;

@SpringBootTest
public class DailyOverviewServiceTest {

    @Resource
    private DailyOverviewService service;

    @MockBean
    private HealthCheckinService healthCheckinService;
    @MockBean
    private FinanceCheckinService financeCheckinService;
    @MockBean
    private EmotionCheckinService emotionCheckinService;
    @MockBean
    private GrowthTaskService growthTaskService;
    @MockBean
    private TaskService taskService;
    @MockBean
    private ArticleReadingRecordMapper articleReadingRecordMapper;
    @MockBean
    private AssessmentService assessmentService;
    @MockBean
    private DanReportUploadService danReportUploadService;

    @Test
    public void aggregate_四类汇总_全部正确() {
        // 打卡:全部空列表 → 均未完成
        when(healthCheckinService.getCheckins(any(), any(), any())).thenReturn(Collections.emptyList());
        when(financeCheckinService.getCheckins(any(), any(), any())).thenReturn(Collections.emptyList());
        when(emotionCheckinService.getCheckinList(any(), anyInt(), anyInt(), any(), anyBoolean()))
                .thenReturn(Collections.emptyList());

        // 任务:待接受 1 条;成长任务 已完成1 + 待完成1
        when(taskService.getMyPendingTasks(10L))
                .thenReturn(Collections.singletonList(new Task()));
        Map<String, Object> doneTask = new HashMap<>();
        doneTask.put("completed", 1);
        Map<String, Object> todoTask = new HashMap<>();
        todoTask.put("completed", 0);
        when(growthTaskService.getTaskList(eq(1L), anyMap()))
                .thenReturn(Arrays.asList(doneTask, todoTask));

        // 阅读:今日已读 2 条
        when(articleReadingRecordMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(2);

        // 测评:进行中 1(pending 且 childId 匹配);待上传报告 0
        AssessmentRecord rec = new AssessmentRecord();
        rec.setStatus("pending");
        rec.setChildId(10L);
        when(assessmentService.getFamilyRecords(100L))
                .thenReturn(Collections.singletonList(rec));
        when(danReportUploadService.listByMember(10L, null))
                .thenReturn(Result.success(Collections.<DanReportUploadVO>emptyList()));

        DailyOverviewVO vo = service.aggregate(1L, 10L, 100L);

        assertEquals(10L, vo.getMemberId().longValue());
        assertEquals(3, vo.getCheckins().size());
        assertFalse(vo.getCheckins().get(0).getDone());

        assertEquals(Integer.valueOf(1), vo.getTasks().getPendingAccept());
        assertEquals(Integer.valueOf(1), vo.getTasks().getTodoToday());
        assertEquals(Integer.valueOf(1), vo.getTasks().getDoneToday());

        assertEquals(Integer.valueOf(2), vo.getActivities().getArticleReadToday());

        assertEquals(Integer.valueOf(1), vo.getAssessments().getOngoing());
        assertEquals(Integer.valueOf(0), vo.getAssessments().getReportPending());
    }
}
  • [ ] Step 2: 运行测试确认失败

    cd cfc-backend && mvn test -Dtest=DailyOverviewServiceTest
    

Expected: FAIL(DailyOverviewService 不存在,编译错误)。

  • Step 3: 创建 DailyOverviewVO

创建 cfc-backend/src/main/java/com/etotem/cfc/dto/DailyOverviewVO.java:

package com.etotem.cfc.dto;

import lombok.Data;

import java.util.List;

/**
 * 今日任务聚合视图(只读)
 */
@Data
public class DailyOverviewVO {

    private Long memberId;
    private String date; // yyyy-MM-dd
    private List<CheckinItem> checkins;
    private TaskSummary tasks;
    private ActivitySummary activities;
    private AssessmentSummary assessments;
    private Boolean hasMore = false; // 二期「查看全部」扩展位

    @Data
    public static class CheckinItem {
        private String key;      // health / wealth / mind
        private String label;    // 健康打卡 / 财商打卡 / 情绪打卡
        private Boolean done;
        private String route;    // 由后端下发,前端直接 navigateTo
    }

    @Data
    public static class TaskSummary {
        private Integer pendingAccept; // 家庭任务待接受数
        private Integer todoToday;     // 今日待完成数(成长任务 DAILY)
        private Integer doneToday;     // 今日已完成数
        private String route;          // /pages/tasks/tasks
    }

    @Data
    public static class ActivitySummary {
        private Integer articleReadToday; // 今日已读文章数
        private Integer activityAvailable; // v1 恒为 0(预留)
        private String articleRoute;       // /pages/article-center/index
        private String activityRoute;      // /pages/activity/index
    }

    @Data
    public static class AssessmentSummary {
        private Integer ongoing;      // 进行中测评数
        private Integer reportPending; // 待上传/待确认报告数
        private String route;         // /pages/assessment/results
    }
}
  • Step 4: 创建 DailyOverviewService

创建 cfc-backend/src/main/java/com/etotem/cfc/service/DailyOverviewService.java:

package com.etotem.cfc.service;

import com.etotem.cfc.dto.DailyOverviewVO;
import com.etotem.cfc.entity.ArticleReadingRecord;
import com.etotem.cfc.entity.AssessmentRecord;
import com.etotem.cfc.entity.Task;
import com.etotem.cfc.mapper.ArticleReadingRecordMapper;
import com.etotem.cfc.dto.DanReportUploadVO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 今日任务聚合服务(只读,零新业务规则,全部复用现有查询)
 */
@Service
public class DailyOverviewService {

    @Resource
    private HealthCheckinService healthCheckinService;
    @Resource
    private FinanceCheckinService financeCheckinService;
    @Resource
    private EmotionCheckinService emotionCheckinService;
    @Resource
    private GrowthTaskService growthTaskService;
    @Resource
    private TaskService taskService;
    @Resource
    private ArticleReadingRecordMapper articleReadingRecordMapper;
    @Resource
    private AssessmentService assessmentService;
    @Resource
    private DanReportUploadService danReportUploadService;

    private static final String TODAY_FMT = "yyyy-MM-dd";

    public DailyOverviewVO aggregate(Long userId, Long memberId, Long familyId) {
        String today = new SimpleDateFormat(TODAY_FMT).format(new Date());
        DailyOverviewVO vo = new DailyOverviewVO();
        vo.setMemberId(memberId);
        vo.setDate(today);
        vo.setCheckins(buildCheckins(userId, memberId, today));
        vo.setTasks(buildTasks(userId, memberId));
        vo.setActivities(buildActivities(memberId));
        vo.setAssessments(buildAssessments(memberId, familyId));
        vo.setHasMore(false);
        return vo;
    }

    // ---------- 打卡类(最多 3 项:健康/财商/情绪) ----------

    private List<DailyOverviewVO.CheckinItem> buildCheckins(Long userId, Long memberId, String today) {
        List<DailyOverviewVO.CheckinItem> list = new ArrayList<>();
        list.add(buildCheckin("health", "健康打卡", "/pages/health/daily-checkin",
                hasTodayRecord(healthCheckinService.getCheckins(userId, memberId, null), today,
                        o -> ((com.etotem.cfc.entity.HealthCheckin) o).getCheckinDate())));
        list.add(buildCheckin("wealth", "财商打卡", "/pages/wealth-sub/checkin",
                hasTodayRecord(financeCheckinService.getCheckins(userId, memberId, null), today,
                        o -> ((com.etotem.cfc.entity.FinanceCheckin) o).getCheckinDate())));
        list.add(buildCheckin("mind", "情绪打卡", "/pages/mind-detail/emotion-checkin",
                hasTodayRecord(emotionCheckinService.getCheckinList(memberId, 1, 1, userId, true), today,
                        o -> ((com.etotem.cfc.dto.EmotionCheckinVO) o).getCheckinDate())));
        return list;
    }

    private DailyOverviewVO.CheckinItem buildCheckin(String key, String label, String route, boolean done) {
        DailyOverviewVO.CheckinItem item = new DailyOverviewVO.CheckinItem();
        item.setKey(key);
        item.setLabel(label);
        item.setRoute(route);
        item.setDone(done);
        return item;
    }

    /** 今日过滤:dateGetter 返回 java.util.Date 或 String,统一格式化为 yyyy-MM-dd 后与 today 比较 */
    private boolean hasTodayRecord(List<?> records, String today, java.util.function.Function<Object, Object> dateGetter) {
        if (records == null || records.isEmpty()) return false;
        SimpleDateFormat fmt = new SimpleDateFormat(TODAY_FMT);
        for (Object rec : records) {
            Object d = dateGetter.apply(rec);
            if (d == null) continue;
            String ds;
            if (d instanceof java.util.Date) {
                ds = fmt.format((java.util.Date) d);
            } else {
                ds = String.valueOf(d);
            }
            if (ds != null && ds.startsWith(today)) return true;
        }
        return false;
    }

    // ---------- 任务类 ----------

    private DailyOverviewVO.TaskSummary buildTasks(Long userId, Long memberId) {
        DailyOverviewVO.TaskSummary s = new DailyOverviewVO.TaskSummary();

        List<Task> pending = taskService.getMyPendingTasks(memberId);
        s.setPendingAccept(pending == null ? 0 : pending.size());

        Map<String, String> params = new HashMap<>();
        params.put("type", "DAILY");
        List<Map<String, Object>> dailyList = growthTaskService.getTaskList(userId, params);
        int todo = 0;
        int done = 0;
        if (dailyList != null) {
            for (Map<String, Object> t : dailyList) {
                Object c = t.get("completed");
                boolean isDone = c != null && ("1".equals(String.valueOf(c)) || Boolean.TRUE.equals(c));
                if (isDone) done++; else todo++;
            }
        }
        s.setTodoToday(todo);
        s.setDoneToday(done);
        s.setRoute("/pages/tasks/tasks");
        return s;
    }

    // ---------- 阅读活动类 ----------

    private DailyOverviewVO.ActivitySummary buildActivities(Long memberId) {
        DailyOverviewVO.ActivitySummary s = new DailyOverviewVO.ActivitySummary();
        Integer count = articleReadingRecordMapper.selectCount(
                new LambdaQueryWrapper<ArticleReadingRecord>()
                        .eq(ArticleReadingRecord::getChildId, memberId)
                        .apply("DATE(read_at) = CURDATE()"));
        s.setArticleReadToday(count == null ? 0 : count);
        s.setActivityAvailable(0); // v1 无现成可参与活动数查询,预留
        s.setArticleRoute("/pages/article-center/index");
        s.setActivityRoute("/pages/activity/index");
        return s;
    }

    // ---------- 测评报告类 ----------

    private DailyOverviewVO.AssessmentSummary buildAssessments(Long memberId, Long familyId) {
        DailyOverviewVO.AssessmentSummary s = new DailyOverviewVO.AssessmentSummary();

        int ongoing = 0;
        if (familyId != null) {
            List<AssessmentRecord> records = assessmentService.getFamilyRecords(familyId);
            if (records != null) {
                for (AssessmentRecord r : records) {
                    if ("pending".equals(r.getStatus()) && memberId.equals(r.getChildId())) {
                        ongoing++;
                    }
                }
            }
        }
        s.setOngoing(ongoing);

        int reportPending = 0;
        try {
            com.etotem.cfc.common.Result<List<DanReportUploadVO>> uploadRes =
                    danReportUploadService.listByMember(memberId, null);
            if (uploadRes != null && uploadRes.getCode() == 200 && uploadRes.getData() != null) {
                for (DanReportUploadVO vo : uploadRes.getData()) {
                    if ("pending".equals(vo.getDraftStatus())) reportPending++;
                }
            }
        } catch (Exception e) {
            reportPending = 0; // DAN 报告子模块异常不阻断聚合
        }
        s.setReportPending(reportPending);
        s.setRoute("/pages/assessment/results");
        return s;
    }
}
  • [ ] Step 5: 运行测试确认通过

    cd cfc-backend && mvn test -Dtest=DailyOverviewServiceTest
    

Expected: PASS(若个别 field 名与步骤 0 校验不符,按步骤 0 结论修正引用后重跑)。

  • [ ] Step 6: 提交

    git add cfc-backend/src/main/java/com/etotem/cfc/dto/DailyOverviewVO.java \
        cfc-backend/src/main/java/com/etotem/cfc/service/DailyOverviewService.java \
        cfc-backend/src/test/java/com/etotem/cfc/service/DailyOverviewServiceTest.java
    git commit -m "feat(backend): 新增今日任务聚合服务 DailyOverviewService + DailyOverviewVO
    - 聚合打卡/任务/阅读活动/测评报告四类今日状态
    - 纯只读,全部复用现有 service/mapper 查询逻辑
    - 单元测试 DailyOverviewServiceTest(@SpringBootTest + @MockBean)"
    

Task 2: 后端聚合接口 — DailyTaskController

Files:

  • Create: cfc-backend/src/main/java/com/etotem/cfc/controller/DailyTaskController.java
  • Test: cfc-backend/src/test/java/com/etotem/cfc/controller/DailyTaskControllerTest.java

Interfaces:

  • Consumes: DailyOverviewService.aggregate(userId, memberId, familyId)(Task 1)
  • Produces: POST /api/daily-task/overview → Result<DailyOverviewVO>(Task 3 前端调用)

  • [ ] Step 1: 前置检查(AGENTS.md 强制)

    cd cfc-backend && mvn clean compile
    # 检查路由重复(确认 /api/daily-task/overview 未被占用)
    grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | Select-String 'daily-task'
    # 检查 Bean 命名冲突(确认无同名 DailyTaskController/DailyOverviewService)
    Get-ChildItem -Recurse cfc-backend/src/main/java -Filter 'DailyTaskController.java' | Select-Object FullName
    Get-ChildItem -Recurse cfc-backend/src/main/java -Filter 'DailyOverviewService.java' | Select-Object FullName
    

Expected: 编译通过;路由无占用;Bean 无重名。

  • Step 2: 写失败测试

创建 cfc-backend/src/test/java/com/etotem/cfc/controller/DailyTaskControllerTest.java:

package com.etotem.cfc.controller;

import com.etotem.cfc.common.Result;
import com.etotem.cfc.dto.DailyOverviewVO;
import com.etotem.cfc.service.DailyOverviewService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

@SpringBootTest
public class DailyTaskControllerTest {

    @Resource
    private DailyTaskController controller;

    @MockBean
    private DailyOverviewService dailyOverviewService;

    @Test
    public void overview_成功_返回聚合视图() {
        DailyOverviewVO vo = new DailyOverviewVO();
        vo.setMemberId(10L);
        vo.setCheckins(Collections.emptyList());
        when(dailyOverviewService.aggregate(eq(1L), eq(10L), eq(100L))).thenReturn(vo);

        Map<String, Object> params = new HashMap<>();
        params.put("memberId", 10L);

        Result<DailyOverviewVO> result = controller.overview(params, 1L, 10L, 100L);

        assertEquals(200, result.getCode());
        assertEquals(10L, result.getData().getMemberId().longValue());
    }

    @Test
    public void overview_memberId为null_返回错误() {
        Map<String, Object> params = new HashMap<>();

        Result<DailyOverviewVO> result = controller.overview(params, 1L, null, 100L);

        assertEquals(500, result.getCode());
        assertEquals("memberId不能为空", result.getMessage());
    }
}
  • [ ] Step 3: 运行测试确认失败

    cd cfc-backend && mvn test -Dtest=DailyTaskControllerTest
    

Expected: FAIL(DailyTaskController 不存在)。

  • Step 4: 创建 DailyTaskController

创建 cfc-backend/src/main/java/com/etotem/cfc/controller/DailyTaskController.java(参考 TaskController.getTodayTasks 与 DailyCheckinController.list 的骨架):

package com.etotem.cfc.controller;

import com.etotem.cfc.common.Result;
import com.etotem.cfc.dto.DailyOverviewVO;
import com.etotem.cfc.service.DailyOverviewService;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Map;

/**
 * 今日任务聚合接口(只读)
 */
@RestController
@RequestMapping("/api/daily-task")
public class DailyTaskController {

    @Resource
    private DailyOverviewService dailyOverviewService;

    @Operation(summary = "今日任务总览(打卡/任务/阅读活动/测评报告四类)")
    @PostMapping("/overview")
    public Result<DailyOverviewVO> overview(
            @RequestBody(required = false) Map<String, Object> params,
            @RequestAttribute("userId") Long userId,
            @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId,
            @RequestAttribute(value = "familyId", required = false) Long familyId) {
        Long memberId = (params != null && params.get("memberId") != null)
                ? Long.valueOf(params.get("memberId").toString()) : currentMemberId;
        if (memberId == null) {
            return Result.error("memberId不能为空");
        }
        return Result.success(dailyOverviewService.aggregate(userId, memberId, familyId));
    }
}
  • [ ] Step 5: 运行测试确认通过

    cd cfc-backend && mvn test -Dtest=DailyTaskControllerTest
    

Expected: PASS。

  • [ ] Step 6: 全量验证

    cd cfc-backend && mvn clean compile && mvn test
    

Expected: 编译通过 + 全部测试通过(含既有 70 个测试文件的回归)。

  • [ ] Step 7: 提交

    git add cfc-backend/src/main/java/com/etotem/cfc/controller/DailyTaskController.java \
        cfc-backend/src/test/java/com/etotem/cfc/controller/DailyTaskControllerTest.java
    git commit -m "feat(backend): 新增今日任务聚合接口 POST /api/daily-task/overview
    - memberId 回退链:请求体 memberId → currentMemberId → userId(成长任务)
    - 统一 @PostMapping + Result<T>,只读不写库
    - 单元测试 DailyTaskControllerTest"
    

Task 3: 前端 API 封装 + 今日任务卡片组件

Files:

  • Modify: cfc-frontend/utils/api.js(末尾追加导出函数)
  • Create: cfc-frontend/components/daily-task-card.vue
  • Test: node --check(语法校验,AGENTS.md:禁止 npm build)

Interfaces:

  • Consumes: 后端 POST /api/daily-task/overview(Task 2),响应 DailyOverviewVO 结构
  • Produces: getDailyTaskOverview()(api.js)、<DailyTaskCard /> 组件(Task 4 挂载首页)

  • [ ] Step 1: api.js 追加导出函数

在 cfc-frontend/utils/api.js 末尾追加(参考现有 getTodayTasks 第 463-465 行风格):

/**
 * 今日任务总览(首页卡片聚合:打卡/任务/阅读活动/测评报告)
 */
export const getDailyTaskOverview = () => {
  return request('/api/daily-task/overview', 'POST', {})
}
  • Step 2: 创建 daily-task-card.vue

创建 cfc-frontend/components/daily-task-card.vue(参考 ProfileGrowth.vue 的 onShow 拉数据 + uni.navigateTo 模式):

<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>
  • [ ] Step 3: 语法校验

    # 提取 script 块做 node --check(AGENTS.md:不打包)
    node -e "const fs=require('fs');const s=fs.readFileSync('cfc-frontend/components/daily-task-card.vue','utf8');const m=s.match(/<script>([\s\S]*?)<\/script>/);if(!m){throw new Error('no script block')};new Function(m[1].replace(/import .*?from ['\"][^'\"]+['\"];?\s*/g,'').replace(/export default/,'const __comp ='));console.log('syntax OK')"
    

Expected: 输出 syntax OK(new Function 只做语法解析不执行)。

  • [ ] Step 4: 提交

    git add cfc-frontend/utils/api.js cfc-frontend/components/daily-task-card.vue
    git commit -m "feat(frontend): 新增今日任务卡片组件 daily-task-card.vue + 聚合接口封装
    - utils/api.js 新增 getDailyTaskOverview()
    - 组件自带 onShow 拉取刷新,点击项 uni.navigateTo 跳转
    - BaseLoading/BaseEmpty 风格与小程序限制(无 ?. 可选链等)全部遵从"
    

Task 4: 首页集成 — 挂载卡片到 index-home

Files:

  • Modify: cfc-frontend/pages/index-home/index.vue(import 第 347 行附近、components 注册第 353-362 行、模板第 61-77 行之间)

Interfaces:

  • Consumes: <DailyTaskCard /> 组件(Task 3)
  • Produces: 首页登录态(v-if="currentRole" 容器内)渲染今日任务卡片

  • [ ] Step 1: import

在 cfc-frontend/pages/index-home/index.vue 第 347 行(import JourneyCard from '../../components/journey-card.vue')后追加:

import DailyTaskCard from '../../components/daily-task-card.vue'
  • Step 2: components 注册

在第 353-362 行的 components: { ... } 对象中追加:

DailyTaskCard,

(放在 JourneyCard 后,保持字母序)

  • Step 3: 模板挂载

在模板第 64 行 </JourneyCard> 之后、第 67 行 <WuxingSandbox 之前插入:

    <!-- 今日任务卡片 — 每日任务统一入口 -->
    <DailyTaskCard />
  • [ ] Step 4: 语法校验

    node -e "const fs=require('fs');const s=fs.readFileSync('cfc-frontend/pages/index-home/index.vue','utf8');const m=s.match(/<script>([\s\S]*?)<\/script>/);if(!m){throw new Error('no script block')};new Function(m[1].replace(/import .*?from ['\"][^'\"]+['\"];?\s*/g,'').replace(/export default/,'const __comp ='));console.log('syntax OK')"
    

Expected: 输出 syntax OK。

  • [ ] Step 5: 确认模板顺序

    Select-String -Path cfc-frontend/pages/index-home/index.vue -Pattern 'DailyTaskCard|JourneyCard|WuxingSandbox'
    

Expected: DailyTaskCard 出现在 import/components/模板三处,且模板中位于 JourneyCard 与 WuxingSandbox 之间。

  • [ ] Step 6: 提交

    git add cfc-frontend/pages/index-home/index.vue
    git commit -m "feat(frontend): 首页集成今日任务卡片(JourneyCard 与 WuxingSandbox 之间)"
    

验收核对(对应 spec §7)

spec 验收标准 验证 task/step
首页登录态渲染卡片,未登录不显示 Task 4 Step 3(v-if="currentRole" 容器内)+ 组件 onShow 无 token 直接 return
打卡/任务/阅读/测评四分组正确展示 Task 1 Step 4(buildCheckins/buildTasks/buildActivities/buildAssessments)
点击动作项跳转到对应功能页 Task 1 VO 下发 route + Task 3 go() 方法
完成操作返回后卡片自动刷新 ✓ Task 3 组件 onShow() 每次拉取(避开 index-home loggedInInitDone 单次守卫)
聚合接口只读,零写操作 Task 1(纯查询)+ Task 2(只调 aggregate)
memberId 回退链正确 Task 2 Step 4(body → currentMemberId → aggregate 内 userId 用于成长任务)
接口失败降级 + 重试 Task 3 error 态 + 点击重试
node --check 语法校验,不打包 Task 3 Step 3 / Task 4 Step 4