# 五维 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
全家
{{ familyScore }}
{{ member.name }}
{{ getMemberScore(member) }}
›
```
**Step 2: 编写组件脚本**
```vue
```
**Step 3: 编写组件样式**
```vue
```
---
### Task 2: UserQuickEntry 组件
**Files:**
- Create: `cfc-frontend/components/UserQuickEntry.vue`
**Step 1: 编写组件**
```vue
👤 当前用户:{{ userName }}({{ roleName }})
切换孩子:{{ currentChildName || '选择' }} ▼
📋
今日任务
🔥
相关活动
🛍️
推荐商品
```
---
### 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
{{ prod.name }}
¥{{ prod.price }}
```
**Step 3b: DimensionActivities.vue**
```vue
{{ act.title }}
{{ act.startTime }} · {{ act.location }}
```
**Step 3c: DimensionTasks.vue**
```vue
{{ task.title }}
+{{ task.points || 0 }}分
```
---
### Task 4: 改造身体页面
**Files:**
- Modify: `cfc-frontend/pages/body/index.vue`
**改造要点:**
1. 引入 5 个新组件
2. 移除原有静态占位数据(硬编码步数/饮水/睡眠/健康小贴士/规划师推荐)
3. 替换为真实数据组件
```vue
{{ reportTypeLabel(latestReport.reportType) }}
{{ latestReport.createdAt ? latestReport.createdAt.slice(0,10) : '' }}
{{ latestReport.overallScore || 0 }}
综合评分
查看完整报告 ›
菌群检测
{{ gutReport.createdAt ? gutReport.createdAt.slice(0,10) : '' }}
{{ gutReport.gutHealthScore || 0 }}
菌群健康
查看完整报告 ›
{{ ind.indicatorName }}: {{ ind.indicatorValue }}
还没有健康检测报告
上传体检报告或菌群检测结果,了解身体维度健康状况
上传报告
6,582
🚶 步数
1.2L
💧 饮水
8.5h
😴 睡眠
(对接智能设备后自动同步)
今日已打卡:{{ checkinSummary }}
⚡ +{{ checkinEnergy }} 能量
去打卡 ›
🏃
运动
🍎
饮食
😴
作息
🔬
菌群
```
组件脚本中需添加的数据和 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
{{ energyScore }}
/100
{{ memberName }} 的身体能量
累计获得
+{{ totalEarned }}
累计消耗
-{{ totalSpent }}
当前余额
{{ balance }}
{{ healthReport.overallScore || 0 }}
综合评分
{{ healthReport.createdAt }}
暂无健康报告数据
{{ log.description }}
{{ log.amount > 0 ? '+' : '' }}{{ log.amount }}
```
**心智详情页差异(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 ─╯→ 可并行创建
```