Prechádzať zdrojové kódy

docs: add homepage risk fix plan — 10 issues across parent-index/child-index

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
liaoxg 4 mesiacov pred
rodič
commit
bd9ec5f69a

+ 666 - 0
docs/superpowers/plans/2026-06-19-homepage-risk-fix.md

@@ -0,0 +1,666 @@
+# 小程序首页功能修复计划
+
+> **For agentic workers:** 使用 superpowers:subagent-driven-development 或 superpowers:executing-plans 来执行此计划。
+
+**目标:** 修复小程序首页(parent-index / child-index)的 10 个实现问题
+
+**问题总览:**
+
+| # | 严重度 | 问题 | 文件 | 影响 |
+|---|---|---|---|---|
+| A1 | 🔴高 | FamilyEnergyBar 硬编码 3 个未注册页面路径,点击成员行→404 | `FamilyEnergyBar.vue:134-139` | 运行时导航失败 |
+| A2 | 🟡中 | DimensionActivities 组件自加载 API,父组件 prop 数据被忽略 | `DimensionActivities.vue:47-65` | 重复 API 调用,prop 冗余 |
+| A3 | 🟡中 | DimensionProducts 组件自加载 API,父组件 prop 数据被忽略 | `DimensionProducts.vue:39-53` | 同 A2 |
+| A4 | 🟡中 | `onDomainClick` 导航 URL 缺少 `childId` 参数 | `child-index.vue:580-585` | 能量详情页无法加载孩子数据 |
+| A5 | 🟡中 | 成就徽章 3/4/5 无解锁触发逻辑 | `child-index.vue:499-504` | 运动/艺术/家务徽章永不解锁 |
+| A6 | 🟡中 | 模板残留孤立标签 `@moreProducts` | `parent-index.vue:229` | 微信 XML 解析警告,事件绑定丢失 |
+| A7 | 🟢低 | preview 模式每次 mount 重算随机值,视觉不稳定 | `wuxing-sandbox.vue:304-310` | 沙盘数值闪烁 |
+| A8 | 🟢低 | FamilyMemberCard `roleLabel` 依赖后端,可能未填充 | `FamilyMemberCard.vue:9` | 角色标签空白 |
+| A9 | 🟢低 | 降级路径 `!c.role` 条件混入非孩子角色 | `parent-index.vue:432-433` | 成员过滤不精确 |
+| A10 | 🟢低 | FamilyMemberStrip scroll-view 无固定高度 | `FamilyMemberStrip.vue:109-112` | 小屏设备布局风险 |
+
+**优先级排序:** A1 → A6 → A4 → A5 → A2 → A3 → A7 → A8 → A9 → A10
+
+**架构:** 10 个任务,每个只改一个文件。Task 1-6 为高/中优先级,Task 7-10 为低优先级可并行处理。
+
+---
+
+## Task 1: 修复 FamilyEnergyBar 硬编码未注册页面路径(A1)
+
+**优先级:** 🔴 高(运行时 404)
+
+**文件:** `cfc-frontend/components/FamilyEnergyBar.vue`
+
+### 问题定位
+
+L134-139:
+
+```javascript
+var detailPages = {
+  body:   '/pages/body/member-body-detail',
+  mind:   '/pages/mind/member-mind-detail',
+  wisdom: '/pages/mind/member-mind-detail',   // ⚠️ wisdom 和 mind 共用同一页
+  action: '/pages/action/member-action-detail'
+}
+```
+
+`member-body-detail`、`member-mind-detail`、`member-action-detail` 这 3 个页面**未在 `pages.json` 中注册**,点击成员行直接 404。
+
+### 修复方案
+
+**选项 A(推荐):** 在 `pages.json` 中注册这 3 个成员详情页面(需要评估是否有对应的 Vue 文件存在)。
+
+**选项 B(保守):** 将 `onMemberClick` 方法中的跳转改为 `uni.showToast({ title: '功能即将上线', icon: 'none' })`,阻止 404。
+
+**选项 C:** 若这些页面存在但未注册,补全 `pages.json` 注册条目。
+
+- [ ] **Step 1: 确认目标页面文件是否存在于 `pages/` 目录**
+
+```bash
+ls /root/cfc/cfc-frontend/pages/body/member-body-detail* 2>/dev/null || echo "NOT FOUND"
+ls /root/cfc/cfc-frontend/pages/mind/member-mind-detail* 2>/dev/null || echo "NOT FOUND"
+ls /root/cfc/cfc-frontend/pages/action/member-action-detail* 2>/dev/null || echo "NOT FOUND"
+```
+
+- [ ] **Step 2: 根据 Step 1 结果选择修复路径**
+
+**若文件存在但未注册(选项 A):** 在 `pages.json` 中补充注册:
+
+```json
+{
+  "path": "pages/body/member-body-detail/index",
+  "style": { "navigationBarTitleText": "身体详情" }
+},
+{
+  "path": "pages/mind/member-mind-detail/index",
+  "style": { "navigationBarTitleText": "心智详情" }
+},
+{
+  "path": "pages/action/member-action-detail/index",
+  "style": { "navigationBarTitleText": "行动详情" }
+}
+```
+
+然后检查对应组件是否完整实现,若不完整则标记后续任务。
+
+**若文件不存在(选项 B):** 将 L133-145 替换为:
+
+```javascript
+onMemberClick: function(member) {
+  // TODO: 注册 member-xxx-detail 页面后替换为真实跳转
+  uni.showToast({
+    title: '家庭成员详情功能即将上线',
+    icon: 'none',
+    duration: 2000
+  })
+},
+```
+
+- [ ] **Step 3: 验证 pages.json 中无重复注册**
+
+```bash
+grep -n "member-body-detail\|member-mind-detail\|member-action-detail" /root/cfc/cfc-frontend/pages.json
+```
+
+---
+
+## Task 2: 修复 parent-index.vue 模板孤立标签(A6)
+
+**优先级:** 🟡 中
+
+**文件:** `cfc-frontend/pages/index/parent-index.vue`
+
+- [ ] **Step 1: 确认问题行**
+
+文件 L221–229 区域当前状态:
+
+```vue
+221:       <!-- ===== 推荐商品 ===== -->
+222:       <DimensionProducts
+223:         dimensionCode="all"
+224:         :products="recommendedProducts"
+225:         @productClick="goProductDetail" />   ← 自闭合,@moreProducts 丢失
+226:       <view class="error-state" v-if="errorProducts">
+227:         <text class="error-state-text">商品推荐加载失败</text>
+228:       </view>
+229:         @moreProducts="goMoreProducts" />    ← 孤立残留行,必须删除
+```
+
+- [ ] **Step 2: 将 `@moreProducts` 合并到 DimensionProducts 组件上,删除孤立行**
+
+将 L222–225 改为:
+
+```vue
+222:       <DimensionProducts
+223:         dimensionCode="all"
+224:         :products="recommendedProducts"
+225:         @productClick="goProductDetail"
+226:         @moreProducts="goMoreProducts" />
+```
+
+删除 L229(`@moreProducts="goMoreProducts" />`)。
+
+- [ ] **Step 3: 验证 `goMoreProducts` 方法存在**
+
+L687-688:
+
+```javascript
+goMoreProducts() {
+  uni.switchTab({ url: '/pages/discover/index' })
+},
+```
+
+已存在,无需修改。
+
+- [ ] **Step 4: lint 检查**
+
+```bash
+cd /root/cfc/cfc-frontend && npx eslint pages/index/parent-index.vue --no-error-on-unmatched-pattern 2>&1 | head -20
+```
+
+---
+
+## Task 3: 修复 child-index.vue onDomainClick 缺少 childId(A4)
+
+**优先级:** 🟡 中
+
+**文件:** `cfc-frontend/pages/index/child-index.vue`
+
+- [ ] **Step 1: 确认当前错误实现**
+
+L580–585:
+
+```javascript
+onDomainClick(evt) {
+  var code = evt && evt.code
+  if (!code) return
+  uni.navigateTo({
+    url: '/pages/energy/detail?code=' + code   // ❌ 缺少 childId
+  })
+},
+```
+
+- [ ] **Step 2: 修复 onDomainClick,追加 childId**
+
+替换 L580–585 为:
+
+```javascript
+onDomainClick(evt) {
+  var code = evt && evt.code
+  if (!code || !this.childId) return
+  uni.navigateTo({
+    url: '/pages/energy/detail?childId=' + this.childId + '&code=' + code
+  })
+},
+```
+
+- [ ] **Step 3: 验证与 parent-index 逻辑一致**
+
+parent-index.vue L656–665 的 `goToDomain` 方法:
+
+```javascript
+goToDomain(code) {
+  var childId = this.currentChildId
+  if (!childId || !code) return
+  uni.navigateTo({
+    url: '/pages/energy/detail?childId=' + childId + '&code=' + code
+  })
+},
+```
+
+child-index 修复后行为与 parent-index 一致。
+
+- [ ] **Step 4: lint 检查**
+
+```bash
+cd /root/cfc/cfc-frontend && npx eslint pages/index/child-index.vue --no-error-on-unmatched-pattern 2>&1 | head -20
+```
+
+---
+
+## Task 4: 补充成就徽章解锁触发逻辑(A5)
+
+**优先级:** 🟡 中
+
+**文件:** `cfc-frontend/pages/index/child-index.vue`
+
+### 徽章 → 解锁条件
+
+| 徽章 ID | 名称 | 解锁条件 |
+|---|---|---|
+| 1 | 🌟 早起鸟 | `streakDays >= 7` ✅ 已有 |
+| 2 | 📚 阅读小达人 | `totalPoints >= 1000` ✅ 已有 |
+| 3 | 🏃 运动健将 | 完成 `运动类` 任务 ≥ 5 次 |
+| 4 | 🎨 小艺术家 | 完成 `艺术类` 任务 ≥ 5 次 |
+| 5 | 🧹 家务小能手 | 完成 `家务类` 任务 ≥ 5 次 |
+
+- [ ] **Step 1: 确认任务数据位置**
+
+L492–496:
+
+```javascript
+const tasksRes = await getTodayTasks(this.childId)
+const rawTasks = tasksRes.data || []
+this.todayTasks = rawTasks
+this.completedCount = rawTasks.filter(function(t) { return t.status === 'completed' }).length
+this.totalTaskCount = rawTasks.length
+```
+
+- [ ] **Step 2: 替换 L498–504 解锁逻辑区域**
+
+当前代码(L498–504):
+
+```javascript
+// 解锁成就
+if (this.streakDays >= 7) {
+  this.unlockBadge(1)
+}
+if (this.totalPoints >= 1000) {
+  this.unlockBadge(2)
+}
+```
+
+替换为:
+
+```javascript
+// 解锁成就
+if (this.streakDays >= 7) {
+  this.unlockBadge(1)
+}
+if (this.totalPoints >= 1000) {
+  this.unlockBadge(2)
+}
+
+// 按类别统计已完成任务次数,解锁对应徽章
+var categoryCompletedCount = { '运动类': 0, '艺术类': 0, '家务类': 0 }
+for (var i = 0; i < rawTasks.length; i++) {
+  var task = rawTasks[i]
+  if (task.status === 'completed') {
+    var cat = task.category || '其他'
+    if (cat === '运动类') {
+      categoryCompletedCount['运动类']++
+    } else if (cat === '艺术类') {
+      categoryCompletedCount['艺术类']++
+    } else if (cat === '家务类') {
+      categoryCompletedCount['家务类']++
+    }
+  }
+}
+if (categoryCompletedCount['运动类'] >= 5) {
+  this.unlockBadge(3)
+}
+if (categoryCompletedCount['艺术类'] >= 5) {
+  this.unlockBadge(4)
+}
+if (categoryCompletedCount['家务类'] >= 5) {
+  this.unlockBadge(5)
+}
+```
+
+> **注意**:`task.status === 'completed'` — 与 L495 的过滤条件保持一致,不使用 `|| task.status === 'done'`。
+
+- [ ] **Step 3: lint 检查**
+
+```bash
+cd /root/cfc/cfc-frontend && npx eslint pages/index/child-index.vue --no-error-on-unmatched-pattern 2>&1 | head -20
+```
+
+---
+
+## Task 5: 修复 DimensionActivities 重复 API 调用(A2)
+
+**优先级:** 🟡 中(架构问题)
+
+**文件:** `cfc-frontend/components/DimensionActivities.vue`
+
+### 问题分析
+
+**双重调用链路:**
+
+```
+parent-index / child-index:
+  loadData() → getActivityList() → dimensionActivities → :activities prop
+                              ↑
+               ↑(第一次调用,prop 数据被忽略)
+
+DimensionActivities 组件:
+  created() → getActivityList() → activitiesData → displayActivities
+                              ↑
+               ↑(第二次调用,实际生效)
+```
+
+`displayActivities` 优先级:`activitiesData.length > 0 ? activitiesData : activities`,组件自加载的 `activitiesData` 永远有值,父组件 prop 永远被忽略。
+
+### 修复方案
+
+**选项 A(推荐):** 移除组件 `created()` 中的自加载逻辑,改为完全依赖 prop 数据(消除重复调用)。
+
+**选项 B:** 父组件不调用 API,直接传空数组,组件自加载生效(保持现状但承认架构)。
+
+- [ ] **Step 1: 确认 DimensionActivities 的调用方传入了什么**
+
+parent-index L212-216:
+
+```vue
+<DimensionActivities
+  dimensionCode="all"
+  :activities="recommendedActivities"
+  @activityClick="goActivityDetail"
+  @moreActivities="goMoreActivities" />
+```
+
+child-index L269-274:
+
+```vue
+<DimensionActivities
+  dimensionCode="child"
+  :activities="dimensionActivities"
+  @activityClick="goActivityDetail"
+  @moreActivities="goMoreProducts" />
+```
+
+**两个调用方都已经在 loadData 中调用了 `getActivityList` 并通过 prop 传入**,组件自加载是冗余的。
+
+- [ ] **Step 2: 移除组件 `created()` 自加载逻辑**
+
+删除 `DimensionActivities.vue` L47-65 的 `created()` 钩子和 `loadActivities` 方法:
+
+删除:
+
+```javascript
+created: function() {
+  this.loadActivities()
+},
+methods: {
+  loadActivities: function() {
+    var self = this
+    import('@/utils/api.js').then(function(api) {
+      api.getActivityList({ dimensionCode: self.dimensionCode, page: 1, size: 6 }).then(function(res) {
+        var records = (res.data && res.data.records) ? res.data.records : []
+        self.activitiesData = records
+      }).catch(function() {
+        // silently fail
+      })
+    })
+  }
+}
+```
+
+同时删除 `data()` 中的 `activitiesData: []`(L38-39),因为不再需要内部状态。
+
+`displayActivities` computed 简化为:
+
+```javascript
+displayActivities: function() {
+  return this.activities
+}
+```
+
+- [ ] **Step 3: 验证修改后 dimensionCode prop 仍传递给父组件(事件链路不变)**
+
+`dimensionCode` 仅用于传给父组件的事件参数,不需要在组件内部使用。
+
+- [ ] **Step 4: lint 检查**
+
+```bash
+cd /root/cfc/cfc-frontend && npx eslint components/DimensionActivities.vue --no-error-on-unmatched-pattern 2>&1 | head -20
+```
+
+---
+
+## Task 6: 修复 DimensionProducts 重复 API 调用(A3)
+
+**优先级:** 🟡 中(架构问题)
+
+**文件:** `cfc-frontend/components/DimensionProducts.vue`
+
+### 问题分析
+
+与 Task 5 完全相同:
+- 父组件在 `loadData` 中调用 `getProductsByDomain` 并通过 prop 传入
+- 组件 `created()` 自加载,`activitiesData` 优先级覆盖 prop
+- 造成重复 API 调用
+
+- [ ] **Step 1: 确认调用方传入数据**
+
+parent-index L222-225:
+
+```vue
+<DimensionProducts
+  dimensionCode="all"
+  :products="recommendedProducts"
+  @productClick="goProductDetail"
+  @moreProducts="goMoreProducts" />
+```
+
+child-index L277-282:
+
+```vue
+<DimensionProducts
+  dimensionCode="child"
+  :products="dimensionProducts"
+  @productClick="goProductDetail"
+  @moreProducts="goMoreProducts" />
+```
+
+- [ ] **Step 2: 移除组件自加载(删除 `created()`、`loadProducts` 方法、`productsData` 状态)**
+
+与 Task 5 完全对称的处理,删除 L39-53:
+
+```javascript
+data: function() {
+  return {
+    productsData: []   // 删除
+  }
+},
+created: function() {
+  this.loadProducts()  // 删除
+},
+methods: {
+  loadProducts: function() { ... }  // 删除
+}
+```
+
+`displayProducts` computed 简化为:
+
+```javascript
+displayProducts: function() {
+  return this.products
+}
+```
+
+- [ ] **Step 3: lint 检查**
+
+```bash
+cd /root/cfc/cfc-frontend && npx eslint components/DimensionProducts.vue --no-error-on-unmatched-pattern 2>&1 | head -20
+```
+
+---
+
+## Task 7: 优化 wuxing-sandbox preview 模式随机值抖动(A7)
+
+**优先级:** 🟢 低
+
+**文件:** `cfc-frontend/components/wuxing-sandbox.vue`
+
+### 问题分析
+
+L304-310,每次组件重新 mount 时 `Math.random()` 生成新的随机分数,导致数值闪烁:
+
+```javascript
+if (this.isPreview) {
+  var scores = []
+  for (var i = 0; i < 5; i++) {
+    scores.push(Math.floor(Math.random() * 51) + 35)  // 35-85 随机
+  }
+  this.previewScores = scores   // ⚠️ 每次 mount 重算
+}
+```
+
+### 修复方案
+
+将预览数据持久化到 Storage,首次生成后缓存,后续 mount 读取缓存。
+
+- [ ] **Step 1: 在 mounted 中加入缓存逻辑**
+
+将 L304-310 替换为:
+
+```javascript
+if (this.isPreview) {
+  var cacheKey = '_wuxing_preview_scores'
+  var cached = uni.getStorageSync(cacheKey)
+  if (cached && Array.isArray(cached) && cached.length === 5) {
+    this.previewScores = cached
+  } else {
+    var scores = []
+    for (var i = 0; i < 5; i++) {
+      scores.push(Math.floor(Math.random() * 51) + 35)
+    }
+    this.previewScores = scores
+    uni.setStorageSync(cacheKey, scores)
+  }
+}
+```
+
+**权衡**:这意味着游客第一次访问生成的随机值会一直保持,直到清除缓存。若希望"仅在当前 session 稳定"而非"永久稳定",将 `uni.setStorageSync` 替换为 `uni.setStorageSync(cacheKey, scores)` 并在 `onUnload` 清除。
+
+---
+
+## Task 8: 确认 FamilyMemberCard roleLabel 后端填充(A8)
+
+**优先级:** 🟢 低(需后端确认)
+
+**文件:** `cfc-frontend/components/FamilyMemberCard.vue`
+
+- [ ] **Step 1: 搜索后端 FamilyMemberVO 是否填充了 roleLabel**
+
+```bash
+grep -n "roleLabel" /root/cfc/cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java
+grep -n "roleLabel" /root/cfc/cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java | head -20
+```
+
+- [ ] **Step 2: 根据搜索结果处理**
+
+**若后端已填充**:在 `FamilyMemberCard.vue` 增加兜底显示:
+
+L9:
+
+```vue
+<text class="member-card__role">{{ member.roleLabel || (member.memberType === 'child' ? '孩子' : '家长') }}</text>
+```
+
+**若后端未填充**:建议在后端 `UserService.getVisibleMembers()` 中填充 `roleLabel` 字段,此任务标记为"需后端配合"。
+
+---
+
+## Task 9: 收紧 parent-index 降级路径成员过滤条件(A9)
+
+**优先级:** 🟢 低
+
+**文件:** `cfc-frontend/pages/index/parent-index.vue`
+
+### 问题分析
+
+L432-433:
+
+```javascript
+var filteredChildren = dashChildren.filter(function(c) {
+  return c && c.id && c.id !== currentUserId && (c.role === 'child' || c.memberType === 'child' || !c.role)
+})
+```
+
+`!c.role` 的意图是"若成员没有 role 字段也保留",但这可能导致非孩子角色被误包含。
+
+### 修复方案
+
+收紧条件,明确只允许孩子角色:
+
+- [ ] **Step 1: 替换 L432-433 过滤条件**
+
+```javascript
+var filteredChildren = dashChildren.filter(function(c) {
+  return c && c.id && c.id !== currentUserId && (c.role === 'child' || c.memberType === 'child')
+})
+```
+
+删除 `|| !c.role`。
+
+---
+
+## Task 10: 修复 FamilyMemberStrip scroll-view 高度(A10)
+
+**优先级:** 🟢 低
+
+**文件:** `cfc-frontend/components/FamilyMemberStrip.vue`
+
+### 问题分析
+
+L109-112:
+
+```css
+.member-strip__scroll {
+  white-space: nowrap;
+  width: 100%;
+}
+```
+
+`scroll-view` 组件若无固定高度,在部分 Android 机型上可能无法正确滚动。
+
+### 修复方案
+
+- [ ] **Step 1: 在 scroll-view 上添加固定高度**
+
+L6-13 区域,将 `<scroll-view>` 标签添加 style:
+
+```vue
+<scroll-view
+  v-if="allMembers.length >= 4"
+  class="member-strip__scroll"
+  scroll-x
+  :scroll-with-animation="true"
+  :enhanced="true"
+  :show-scrollbar="false"
+  style="height: 160rpx;"
+>
+```
+
+160rpx 基于成员卡片高度(约 104rpx avatar 72rpx + padding 16rpx + 名称行 22rpx × 2)估算,留出足够空间。
+
+---
+
+## 执行验证
+
+### 编译验证
+```bash
+cd /root/cfc/cfc-backend && mvn clean compile -q
+```
+
+### 前端语法检查
+```bash
+cd /root/cfc/cfc-frontend && npm run build:mp-weixin 2>&1 | grep -E "ERROR|error|Warning" | head -30
+```
+
+### 微信开发者工具验证
+
+| 操作 | 预期 |
+|---|---|
+| 家长账号 → 首页 → 点击能量横条成员行 | 不再 404(Toast 提示或正常跳转) |
+| 家长账号 → 首页 → 推荐商品 → 查看更多 | 跳转发现页 |
+| 孩子账号 → 首页 → 点击沙盘点 | 跳转能量详情页,正常显示孩子数据 |
+| 孩子完成 5 个运动类任务 → 返回首页 | 运动健将徽章解锁 |
+| 孩子完成 5 个艺术类任务 → 返回首页 | 小艺术家徽章解锁 |
+| 孩子完成 5 个家务类任务 → 返回首页 | 家务小能手徽章解锁 |
+
+### 回退方案
+
+```bash
+git checkout cfc-frontend/pages/index/parent-index.vue \
+           cfc-frontend/pages/index/child-index.vue \
+           cfc-frontend/components/FamilyEnergyBar.vue \
+           cfc-frontend/components/DimensionActivities.vue \
+           cfc-frontend/components/DimensionProducts.vue \
+           cfc-frontend/components/wuxing-sandbox.vue \
+           cfc-frontend/components/FamilyMemberCard.vue \
+           cfc-frontend/components/FamilyMemberStrip.vue \
+           cfc-frontend/pages.json
+```