# 五维 TabBar 页面统一改造 — 实施计划 > **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:** 按设计原则改造身体/心智/行动3个 TabBar 页,新增通用组件和成员详情页 **Architecture:** 前端新增5个可复用组件(能量柱状图/用户快捷入口/商品/活动/任务区块),改造3个 TabBar 页面融入统一布局,新增3个成员维度详情页。后端新增 Activity 实体+CRUD,给 Product/Task 接口增加 dimension 筛选参数。 **Tech Stack:** uni-app Vue 2, 微信小程序, Spring Boot 2.7.18, MyBatis-Plus --- ## 文件结构 ### 新增文件 | 文件 | 类型 | 说明 | |------|:----:|------| | `cfc-frontend/components/FamilyEnergyBar.vue` | component | 家庭能量横放柱状图 | | `cfc-frontend/components/UserQuickEntry.vue` | component | 用户信息+快捷入口 | | `cfc-frontend/components/DimensionProducts.vue` | component | 按维度商品展示 | | `cfc-frontend/components/DimensionActivities.vue` | component | 按维度活动展示 | | `cfc-frontend/components/DimensionTasks.vue` | component | 按维度任务展示 | | `cfc-frontend/pages/body/member-body-detail.vue` | page | 成员身体维度详情 | | `cfc-frontend/pages/mind/member-mind-detail.vue` | page | 成员心智维度详情 | | `cfc-frontend/pages/action/member-action-detail.vue` | page | 成员行动维度详情 | | `cfc-backend/.../entity/Activity.java` | entity | 活动实体 | | `cfc-backend/.../mapper/ActivityMapper.java` | mapper | 活动 Mapper | | `cfc-backend/.../service/ActivityService.java` | service | 活动服务 | | `cfc-backend/.../controller/ActivityController.java` | controller | 活动 CRUD 接口 | | `cfc-backend/.../dto/ActivityDTO.java` | dto | 活动数据传输对象 | ### 修改文件 | 文件 | 类型 | 说明 | |------|:----:|------| | `cfc-frontend/pages/body/index.vue` | page | 改造为统一布局 | | `cfc-frontend/pages/mind/index.vue` | page | 改造为统一布局 | | `cfc-frontend/pages/action/index.vue` | page | 改造为统一布局 | | `cfc-frontend/pages.json` | config | 注册3个成员详情页面路径 | | `cfc-frontend/utils/api.js` | api | 新增活动/商品/任务维度筛选 API | | `cfc-backend/.../controller/product/ProductController.java` | controller | 增加 dimensionCode 参数 | | `cfc-backend/.../controller/task/TaskController.java` | controller | 增加 dimensionCode 参数 | --- ## 实现任务 ### Task 1: FamilyEnergyBar 组件 **Files:** - Create: `cfc-frontend/components/FamilyEnergyBar.vue` **Step 1: 编写组件模板** ```vue ``` **Step 2: 编写组件脚本** ```vue ``` **Step 3: 编写组件样式** ```vue ``` --- ### Task 2: UserQuickEntry 组件 **Files:** - Create: `cfc-frontend/components/UserQuickEntry.vue` **Step 1: 编写组件** ```vue ``` --- ### Task 3: DimensionProducts / DimensionActivities / DimensionTasks 组件 **Files:** - Create: `cfc-frontend/components/DimensionProducts.vue` - Create: `cfc-frontend/components/DimensionActivities.vue` - Create: `cfc-frontend/components/DimensionTasks.vue` **Step 3a: DimensionProducts.vue** ```vue ``` **Step 3b: DimensionActivities.vue** ```vue ``` **Step 3c: DimensionTasks.vue** ```vue ``` --- ### Task 4: 改造身体页面 **Files:** - Modify: `cfc-frontend/pages/body/index.vue` **改造要点:** 1. 引入 5 个新组件 2. 移除原有静态占位数据(硬编码步数/饮水/睡眠/健康小贴士/规划师推荐) 3. 替换为真实数据组件 ```vue ``` 组件脚本中需添加的数据和 API 调用: ```javascript import PageBanner from '../../components/PageBanner.vue' import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue' import UserQuickEntry from '../../components/UserQuickEntry.vue' import DimensionProducts from '../../components/DimensionProducts.vue' import DimensionActivities from '../../components/DimensionActivities.vue' import { getFamilyEnergySandbox, getLatestHealthReport, getHealthIndicatorList } from '../../utils/api.js' export default { components: { PageBanner, FamilyEnergyBar, UserQuickEntry, DimensionProducts, DimensionActivities }, data() { return { isLoggedIn: false, currentChildId: null, userName: '', currentChildName: '', familyMembers: [], sandboxData: null, latestReport: null, gutReport: null, indicators: [], checkinSummary: '运动 ✔ 喝水 ✔', checkinEnergy: 15 } }, onShow() { this.initUser() if (this.isLoggedIn) { this.loadEnergyData() this.loadHealthData() } }, methods: { initUser: function() { /* 从 storage 读取 token/role/userName/currentChildId */ }, loadEnergyData: function() { getFamilyEnergySandbox().then(function(res) { if (res.code === 200) this.sandboxData = res.data }) }, loadHealthData: function() { // 获取最近两份报告(体检+菌群) getLatestHealthReport({ childId: this.currentChildId, type: 'physical_exam' }).then(r => { if (r.code === 200) this.latestReport = r.data }) getLatestHealthReport({ childId: this.currentChildId, type: 'gut_flora' }).then(r => { if (r.code === 200) this.gutReport = r.data }) getHealthIndicatorList({ childId: this.currentChildId }).then(r => { if (r.code === 200) this.indicators = r.data }) }, goReportDetail: function(id) { uni.navigateTo({ url: '/pages/body/health-report?id=' + id }) }, goCheckin: function() { uni.navigateTo({ url: '/pages/body/checkin' }) }, goUploadReport: function() { /* 跳转上传页面 */ }, onScrollTo: function(section) { /* 滚动到对应区域 */ }, onChildChanged: function(child) { /* 刷新数据 */ }, goFunc: function(type) { /* 跳转功能页面 */ } } } ``` --- ### Task 5: 改造心智页面 **Files:** - Modify: `cfc-frontend/pages/mind/index.vue` **改造要点:** 1. 顶部插入 FamilyEnergyBar(双维度模式:mind + wisdom) 2. 插入 UserQuickEntry 3. 保留现有 心理Tab / 认知Tab 双结构 4. 心理Tab底部 + 认知Tab底部插入文章推荐列表 5. 通用区块:DimensionProducts + DimensionActivities **页面布局模板:** ```vue ``` 心智页需保留的现有代码块(不做删除,只新增组件插入): - EMI 心理四维条形图 (`emi-section / emi-card`) - 每日心理 (`daily_tip / tip-card`) - DAN 认知评分 (`cognitive-overview / cognitive-score-card`) - 六维雷达图 (`radar-section / radar-card`) - 认知训练入口 (`game-grid`) - 阅读统计 (`reading-card`) - 无数据占位引导 --- ### Task 6: 改造行动页面 **Files:** - Modify: `cfc-frontend/pages/action/index.vue` **改造要点:** 1. 顶部插入 FamilyEnergyBar(dimensionCode="action") 2. 插入 UserQuickEntry 3. 替换原有今日任务区块为 DimensionTasks 4. 替换原有推荐商品区块为 DimensionProducts 5. 替换原有热门活动区块为 DimensionActivities 6. 保留功能入口 Grid ```vue ``` 移除原有硬编码数据:`hotActivities`、`recommendedProducts` 数组和它们的 mock 数据。 --- ### Task 7: 成员维度详情页(3 页) **Files:** - Create: `cfc-frontend/pages/body/member-body-detail.vue` - Create: `cfc-frontend/pages/mind/member-mind-detail.vue` - Create: `cfc-frontend/pages/action/member-action-detail.vue` **通用模板(以 body 为例):** ```vue ``` **心智详情页差异(member-mind-detail.vue):** - 能量构成字段不变 - 身体专属 "健康报告" 区块 → 替换为 "心理评估" 和 "认知评估" 两个区块 - 心理评估用 `getEmiReport(childId)` → 展示情绪商数/心理韧性/压力应对/自我认知 - 认知评估用 `getAssessmentLatestResult(childId)` → 展示 DAN 等级 + 六维评分 **行动详情页差异(member-action-detail.vue):** - 能量构成字段不变 - 身体专属 "健康报告" 区块 → 替换为 "任务统计" - 任务统计:本周完成数/完成率/连续打卡天数 --- ### Task 8: 后端 Activity 实体 + CRUD **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java` - Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/ActivityMapper.java` - Create: `cfc-backend/src/main/java/com/etotem/cfc/service/ActivityService.java` - Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/ActivityController.java` - Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/ActivityDTO.java` **Activity.java** ```java package com.etotem.cfc.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.math.BigDecimal; import java.util.Date; @Data @TableName("activities") public class Activity implements Serializable { @TableId(type = IdType.AUTO) private Long id; private String title; private String description; private String coverImage; /** body/mind/wisdom/action/wealth */ private String dimensionCode; /** offline/online/campaign */ private String activityType; /** draft/published/ended */ private String status; private Date startTime; private Date endTime; private String location; private Integer maxParticipants; private Integer currentParticipants; private BigDecimal price; private Long vendorId; private String vendorName; private Date createdAt; private Date updatedAt; } ``` **ActivityDTO.java** ```java package com.etotem.cfc.dto; import lombok.Data; import java.math.BigDecimal; import java.util.Date; @Data public class ActivityDTO { private Long id; private String title; private String description; private String coverImage; private String dimensionCode; private String activityType; private String status; private String startTime; private String endTime; private String location; private Integer maxParticipants; private Integer currentParticipants; private BigDecimal price; private String vendorName; } ``` **ActivityMapper.java** ```java package com.etotem.cfc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.etotem.cfc.entity.Activity; public interface ActivityMapper extends BaseMapper { } ``` **ActivityService.java** ```java package com.etotem.cfc.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.etotem.cfc.dto.ActivityDTO; import com.etotem.cfc.entity.Activity; import com.etotem.cfc.mapper.ActivityMapper; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; @Service public class ActivityService extends ServiceImpl { public Page getActivityList(String dimensionCode, Integer page, Integer size) { LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(Activity::getStatus, "published") .orderByAsc(Activity::getStartTime); if (dimensionCode != null && !dimensionCode.isEmpty()) { query.eq(Activity::getDimensionCode, dimensionCode); } Page pageResult = this.page(new Page<>(page, size), query); Page dtoPage = new Page<>(pageResult.getCurrent(), pageResult.getSize(), pageResult.getTotal()); dtoPage.setRecords(pageResult.getRecords().stream().map(this::toDTO).collect(Collectors.toList())); return dtoPage; } public ActivityDTO getDetail(Long id) { Activity activity = this.getById(id); return activity != null ? toDTO(activity) : null; } private ActivityDTO toDTO(Activity activity) { ActivityDTO dto = new ActivityDTO(); dto.setId(activity.getId()); dto.setTitle(activity.getTitle()); dto.setDescription(activity.getDescription()); dto.setCoverImage(activity.getCoverImage()); dto.setDimensionCode(activity.getDimensionCode()); dto.setActivityType(activity.getActivityType()); dto.setStatus(activity.getStatus()); dto.setStartTime(activity.getStartTime() != null ? activity.getStartTime().toString() : null); dto.setEndTime(activity.getEndTime() != null ? activity.getEndTime().toString() : null); dto.setLocation(activity.getLocation()); dto.setMaxParticipants(activity.getMaxParticipants()); dto.setCurrentParticipants(activity.getCurrentParticipants()); dto.setPrice(activity.getPrice()); dto.setVendorName(activity.getVendorName()); return dto; } } ``` **ActivityController.java** ```java package com.etotem.cfc.controller; import com.etotem.cfc.common.Result; import com.etotem.cfc.dto.ActivityDTO; import com.etotem.cfc.service.ActivityService; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.Map; @Tag(name = "活动管理", description = "五维活动列表和详情") @RestController @RequestMapping("/api/activity") public class ActivityController { @Resource private ActivityService activityService; @Operation(summary = "获取活动列表") @PostMapping("/list") public Result> list(@RequestBody Map params) { String dimensionCode = (String) params.get("dimensionCode"); Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1; Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10; return Result.success(activityService.getActivityList(dimensionCode, page, size)); } @Operation(summary = "获取活动详情") @PostMapping("/detail") public Result detail(@RequestBody Map params) { Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null; if (id == null) return Result.error("id不能为空"); ActivityDTO dto = activityService.getDetail(id); return dto != null ? Result.success(dto) : Result.error("活动不存在"); } } ``` --- ### Task 9: 后端 Product/Task 维度筛选 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductController.java` - Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/task/TaskController.java` **ProductController 改造** — 在 `list` 接口中增加 `dimensionCode` 参数: ```java // 在现有 list 方法参数中增加 @Operation(summary = "获取商品列表") @PostMapping("/list") public Result> list(@RequestBody Map params) { String dimensionCode = (String) params.get("dimensionCode"); // ... 现有分页参数解析 ... LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(Product::getStatus, "approved"); if (dimensionCode != null && !dimensionCode.isEmpty()) { // 通过 energy_dimension_id 关联筛选(需 Product 实体有 energyDimensionId 字段) // query.eq(Product::getEnergyDimensionId, dimensionId); // 若暂无 energyDimensionId,先按 domain 模糊匹配 query.like(Product::getDomain, dimensionCode); } // ... 现有分页查询 ... } ``` **Product 实体增加字段**(若尚无): ```java /** 关联能量维度ID,对应 energy_dimension.id */ private Long energyDimensionId; ``` **TaskController 改造** — 增加 `dimensionCode` 筛选: ```java // 在现有任务列表接口中增加 String dimensionCode = (String) params.get("dimensionCode"); if (dimensionCode != null && !dimensionCode.isEmpty()) { // 按任务分类筛选(Task.category 或关联维度) query.eq(Task::getCategory, dimensionCode); // 或按 Task 现有 category 字段做模糊匹配 } ``` --- ### Task 10: 配置注册 **Files:** - Modify: `cfc-frontend/pages.json` - Modify: `cfc-frontend/utils/api.js` **pages.json** — 在 pages 数组中新增 3 个成员详情页: ```json { "path": "pages/body/member-body-detail", "style": { "navigationBarTitleText": "成员身体详情", "navigationStyle": "custom" } }, { "path": "pages/mind/member-mind-detail", "style": { "navigationBarTitleText": "成员心智详情", "navigationStyle": "custom" } }, { "path": "pages/action/member-action-detail", "style": { "navigationBarTitleText": "成员行动详情", "navigationStyle": "custom" } } ``` **api.js** — 新增接口封装: ```javascript // 家庭能量沙盘 export function getFamilyEnergySandbox() { return request('/api/energy/sandbox', 'POST', {}) } // 活动列表(按维度) export function getActivityList(params) { return request('/api/activity/list', 'POST', params) } // 活动详情 export function getActivityDetail(id) { return request('/api/activity/detail', 'POST', { id: id }) } // 商品列表(按维度) export function getProductList(params) { return request('/api/product/list', 'POST', params) } // 任务列表(按维度) export function getTaskList(params) { return request('/api/tasks/list', 'POST', params) } // 获取最新健康报告 export function getLatestHealthReport(params) { return request('/api/health/report/latest', 'POST', params) } // 健康指标列表 export function getHealthIndicatorList(params) { return request('/api/health/indicator/list', 'POST', params) } ``` --- ## Spec 覆盖率检查 | 设计原则 | 对应任务 | |:--------:|:--------:| | 原则1: 三个页面都包含商品、活动、任务 | Task 3 (组件), Task 4/5/6 (集成到各页), Task 8/9 (后端) | | 原则2: 文章都在心智里 | Task 5 (心智页保留文章,身体/行动页不包含) | | 原则3: 身体包含各种体检数据 | Task 4 (健康报告+指标+打卡) | | 原则4: 心智包括DAN测评+认知6维度 | Task 5 (保留双Tab结构+六维雷达图) | | 原则5: 每页显示用户信息+快捷入口 | Task 2 (UserQuickEntry 组件) | | 原则6: 顶部横放柱状图+成员跳转 | Task 1 (FamilyEnergyBar 组件) + Task 7 (成员详情页) | | 后端 Activity 实体 | Task 8 | | 后端维度筛选 | Task 9 | | pages.json / api.js 注册 | Task 10 | --- ## 执行顺序建议 ``` Task 8 (后端 Activity) → Task 9 (维度筛选) ↓ Task 1 (FamilyEnergyBar) → Task 2 (UserQuickEntry) → Task 3 (通用区块) ↓ Task 10 (pages.json + api.js) ↓ Task 4 (身体页) ┐ Task 5 (心智页) ├→ 可并行改造 Task 6 (行动页) ┘ ↓ Task 7 (成员详情页) ┐ body/mind/action ─╯→ 可并行创建 ```