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)微信小程序。
@PostMapping,禁止 @GetMapping/@PutMapping/@DeleteMapping(AGENTS.md)Result<T>(code=200/message/data),Result.noFamily code=5001(Result.java)@Resource,字段名与类型默认 Bean Name 一致(AGENTS.md)@RequestAttribute("userId"),当前家庭成员 = @RequestAttribute(value="currentMemberId", required=false),家庭 = @RequestAttribute(value="familyId", required=false);禁止使用 @RequestAttribute("memberId")(代码库无此 attribute)(JwtInterceptor.java:131-165)memberId → currentMemberId(JwtInterceptor 注入)→ 成长任务层用 userId(GrowthTaskService.getTaskList 以 userId 为参数)?.(用 &&/逐字段判断)、禁止 CSS Grid(用 flexbox)、禁止 :key 表达式(用方法调用 :key="getXxxKey(item)")、禁止 new Date(string)(用 utils/format.js 的 parseDate())、数组 props 必须 default: function() { return [] }、:class 绑定禁用方法调用(cfc-frontend/AGENTS.md)node --check 语法校验,打包走 HBuilderX(cfc-frontend/AGENTS.md)cd cfc-backend && mvn clean compile;测试:mvn test(无 H2/Testcontainers,靠 @MockBean 隔离 DB)Files:
cfc-backend/src/main/java/com/etotem/cfc/dto/DailyOverviewVO.javacfc-backend/src/main/java/com/etotem/cfc/service/DailyOverviewService.javacfc-backend/src/test/java/com/etotem/cfc/service/DailyOverviewServiceTest.javaInterfaces:
DailyOverviewService.aggregate(Long userId, Long memberId, Long familyId) → DailyOverviewVO(Task 2 的 Controller 调用)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 不存在,编译错误)。
创建 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
}
}
创建 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)"
Files:
cfc-backend/src/main/java/com/etotem/cfc/controller/DailyTaskController.javacfc-backend/src/test/java/com/etotem/cfc/controller/DailyTaskControllerTest.javaInterfaces:
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 无重名。
创建 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 不存在)。
创建 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"
Files:
cfc-frontend/utils/api.js(末尾追加导出函数)cfc-frontend/components/daily-task-card.vuenode --check(语法校验,AGENTS.md:禁止 npm build)Interfaces:
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', {})
}
创建 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 风格与小程序限制(无 ?. 可选链等)全部遵从"
Files:
cfc-frontend/pages/index-home/index.vue(import 第 347 行附近、components 注册第 353-362 行、模板第 61-77 行之间)Interfaces:
<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'
在第 353-362 行的 components: { ... } 对象中追加:
DailyTaskCard,
(放在 JourneyCard 后,保持字母序)
在模板第 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 验收标准 | 验证 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 |