فهرست منبع

docs: add family member cards design specs and plan

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User 4 ماه پیش
والد
کامیت
1195852702

+ 868 - 0
docs/superpowers/plans/2026-06-15-family-member-cards.md

@@ -0,0 +1,868 @@
+# 家庭成员卡片 Implementation Plan
+
+> **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 (`- [ ]`) for tracking.
+
+**Goal:** Add horizontal-scrolling family member card strip to four tab pages (home/body/mind/action), with per-member data filtering and show_to_family visibility control.
+
+**Architecture:** Add `show_to_family` column to `children` and `users` tables via DatabaseInitializer. Create `FamilyMemberCard` + `FamilyMemberStrip` components. Two new API endpoints under `FamilyUserController` for visible members query and visibility toggle. Integrate strip into all four dimension pages above FamilyEnergyBar.
+
+**Tech Stack:** Spring Boot 2.7.18 / MyBatis-Plus / uni-app Vue 2 (Options API, no `?.` operator, no CSS Grid)
+
+---
+
+### Task 1: Database migration — add show_to_family column
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+
+- [ ] **Step 1: Add migration block after existing family_member_attributes migration (~line 1556)**
+
+```java
+// ===== 迁移: children + users 表添加 show_to_family 字段(v1.3 家庭成员可见性) =====
+try {
+    String checkChildCol = "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'children' AND COLUMN_NAME = 'show_to_family'";
+    if (jdbcTemplate.queryForObject(checkChildCol, Integer.class) == 0) {
+        jdbcTemplate.execute("ALTER TABLE children ADD COLUMN show_to_family TINYINT DEFAULT 1 COMMENT '是否对家庭成员可见: 1=是, 0=否'");
+        log.info("已添加 show_to_family 列到 children 表");
+    } else {
+        log.info("children.show_to_family 已存在,跳过");
+    }
+} catch (Exception e) {
+    log.warn("添加 children.show_to_family 列失败: {}", e.getMessage());
+}
+
+try {
+    String checkUserCol = "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'show_to_family'";
+    if (jdbcTemplate.queryForObject(checkUserCol, Integer.class) == 0) {
+        jdbcTemplate.execute("ALTER TABLE users ADD COLUMN show_to_family TINYINT DEFAULT 1 COMMENT '是否对家庭成员可见: 1=是, 0=否'");
+        log.info("已添加 show_to_family 列到 users 表");
+    } else {
+        log.info("users.show_to_family 已存在,跳过");
+    }
+} catch (Exception e) {
+    log.warn("添加 users.show_to_family 列失败: {}", e.getMessage());
+}
+```
+
+- [ ] **Step 2: Verify compilation**
+
+Run: `cd cfc-backend && mvn clean compile -q`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
+git commit -m "feat: add show_to_family column migration to children and users tables"
+```
+
+---
+
+### Task 2: Update DTOs — add showToFamily field
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/dto/UpdateChildDTO.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/dto/ChildInfoDTO.java`
+
+- [ ] **Step 1: Add `showToFamily` to UpdateChildDTO**
+
+```java
+// After line 17 (private String theme;)
+    private Integer showToFamily; // 是否对家庭成员可见: 1=是, 0=否
+```
+
+- [ ] **Step 2: Add `showToFamily` to ChildInfoDTO**
+
+```java
+// After line 21 (private Integer focusRemaining;)
+    private Integer showToFamily; // 是否对家庭成员可见: 1=是, 0=否
+```
+
+- [ ] **Step 3: Verify compilation**
+
+Run: `cd cfc-backend && mvn clean compile -q`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/UpdateChildDTO.java cfc-backend/src/main/java/com/etotem/cfc/dto/ChildInfoDTO.java
+git commit -m "feat: add showToFamily field to ChildInfoDTO and UpdateChildDTO"
+```
+
+---
+
+### Task 3: UserService — add getVisibleMembers and updateVisibility methods
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java`
+
+- [ ] **Step 1: Create FamilyMemberVO DTO**
+
+```java
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class FamilyMemberVO {
+    private Long id;
+    private String nickname;
+    private String avatar;
+    private String memberType; // 'child' 或 'parent'
+    private String roleLabel;  // '孩子' 或 '家长'
+    private String danLevel;   // 段位(仅孩子)
+    private Integer totalPoints;
+    private Integer showToFamily; // 1=可见, 0=不可见
+}
+```
+
+- [ ] **Step 2: Add `getVisibleMembers` method to UserService**
+
+Insert after `getChildren()` method (after line 749):
+
+```java
+public List<FamilyMemberVO> getVisibleMembers(Long userId) {
+    User user = userMapper.selectById(userId);
+    if (user == null || user.getFamilyId() == null) {
+        return Collections.emptyList();
+    }
+
+    Long familyId = user.getFamilyId();
+    List<FamilyMemberVO> members = new ArrayList<>();
+
+    // 查询可见的家长成员
+    List<User> parents = userMapper.selectList(
+        new LambdaQueryWrapper<User>()
+            .eq(User::getFamilyId, familyId)
+            .eq(User::getRole, "parent")
+            .eq(User::getShowToFamily, 1)
+    );
+    for (User p : parents) {
+        FamilyMemberVO vo = new FamilyMemberVO();
+        vo.setId(p.getId());
+        vo.setNickname(p.getNickname());
+        vo.setAvatar(p.getAvatar());
+        vo.setMemberType("parent");
+        vo.setRoleLabel("家长");
+        vo.setTotalPoints(p.getTotalPoints());
+        vo.setShowToFamily(p.getShowToFamily());
+        members.add(vo);
+    }
+
+    // 查询可见的孩子成员
+    List<Child> children = childMapper.selectList(
+        new LambdaQueryWrapper<Child>()
+            .eq(Child::getFamilyId, familyId)
+            .eq(Child::getShowToFamily, 1)
+    );
+    for (Child c : children) {
+        FamilyMemberVO vo = new FamilyMemberVO();
+        vo.setId(c.getId());
+        vo.setNickname(c.getNickname());
+        vo.setAvatar(null);
+        vo.setMemberType("child");
+        vo.setRoleLabel("孩子");
+        vo.setDanLevel(c.getDanLevel());
+        vo.setTotalPoints(c.getTotalPoints());
+        vo.setShowToFamily(c.getShowToFamily());
+        members.add(vo);
+    }
+
+    return members;
+}
+```
+
+Add imports at top of file:
+```java
+import com.etotem.cfc.dto.FamilyMemberVO;
+import java.util.ArrayList;
+import java.util.Collections;
+```
+
+- [ ] **Step 3: Add `updateChildVisibility` method to UserService**
+
+Insert after `getVisibleMembers`:
+
+```java
+public boolean updateVisibility(Long userId, Long memberId, String memberType, Integer showToFamily) {
+    User user = userMapper.selectById(userId);
+    if (user == null) return false;
+
+    if ("child".equals(memberType)) {
+        // 只有家长可以修改孩子的可见性
+        if (!"parent".equals(user.getRole())) return false;
+        Child child = childMapper.selectById(memberId);
+        if (child == null || !child.getFamilyId().equals(user.getFamilyId())) return false;
+        child.setShowToFamily(showToFamily);
+        child.setUpdatedAt(new Date());
+        childMapper.updateById(child);
+        return true;
+    } else if ("parent".equals(memberType)) {
+        // 用户只能修改自己的可见性
+        if (!memberId.equals(userId)) return false;
+        user.setShowToFamily(showToFamily);
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return true;
+    }
+    return false;
+}
+```
+
+- [ ] **Step 4: Update `toChildInfoDTO` to include showToFamily**
+
+In the existing `toChildInfoDTO` method (line 802+), add after line ~819:
+```java
+dto.setShowToFamily(child.getShowToFamily());
+```
+
+- [ ] **Step 5: Update `updateChild` method to handle showToFamily**
+
+In `updateChild` method (line 751), add before `child.setUpdatedAt(new Date())`:
+```java
+if (dto.getShowToFamily() != null) {
+    child.setShowToFamily(dto.getShowToFamily());
+}
+```
+
+- [ ] **Step 6: Verify compilation**
+
+Run: `cd cfc-backend && mvn clean compile -q`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
+git commit -m "feat: add getVisibleMembers and updateVisibility to UserService"
+```
+
+---
+
+### Task 4: FamilyUserController — add two new API endpoints
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java`
+
+- [ ] **Step 1: Add `getVisibleFamilyMembers` endpoint**
+
+Insert before `getFamilyMembers` (before line 167):
+
+```java
+@Operation(summary = "获取对当前用户可见的家庭成员列表")
+@PostMapping("/members/visible")
+public Result<List<FamilyMemberVO>> getVisibleFamilyMembers(javax.servlet.http.HttpServletRequest request) {
+    Long userId = getUserId(request);
+    List<FamilyMemberVO> members = userService.getVisibleMembers(userId);
+    return Result.success(members);
+}
+```
+
+- [ ] **Step 2: Add `updateMemberVisibility` endpoint**
+
+Insert after the above:
+
+```java
+@Operation(summary = "更新家庭成员可见性")
+@PostMapping("/members/update-visibility")
+public Result<Boolean> updateMemberVisibility(javax.servlet.http.HttpServletRequest request,
+                                               @RequestBody Map<String, Object> body) {
+    Long userId = getUserId(request);
+    Long memberId = Long.valueOf(body.get("memberId").toString());
+    String memberType = (String) body.get("memberType");
+    Integer showToFamily = Integer.valueOf(body.get("showToFamily").toString());
+    boolean success = userService.updateVisibility(userId, memberId, memberType, showToFamily);
+    return Result.success(success);
+}
+```
+
+- [ ] **Step 3: Add missing import**
+
+Ensure `FamilyMemberVO` is imported via the existing `import com.etotem.cfc.dto.*;` on line 5 — it's already a wildcard import, so `FamilyMemberVO` will be auto-included.
+
+- [ ] **Step 4: Verify compilation**
+
+Run: `cd cfc-backend && mvn clean compile -q`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyUserController.java
+git commit -m "feat: add /family/user/members/visible and /members/update-visibility endpoints"
+```
+
+---
+
+### Task 5: Frontend API — add new API functions
+
+**Files:**
+- Modify: `cfc-frontend/utils/api.js`
+
+- [ ] **Step 1: Add `getVisibleFamilyMembers` and `updateMemberVisibility` exports
+
+Insert after `getFamilyMembers` (after line 146):
+
+```javascript
+// 获取对当前用户可见的家庭成员列表(已过滤 show_to_family=0 的成员)
+export const getVisibleFamilyMembers = () => {
+  return request('/api/family/user/members/visible', 'POST')
+}
+
+// 更新成员可见性
+export const updateMemberVisibility = (memberId, memberType, showToFamily) => {
+  return request('/api/family/user/members/update-visibility', 'POST', { memberId, memberType, showToFamily })
+}
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add cfc-frontend/utils/api.js
+git commit -m "feat: add getVisibleFamilyMembers and updateMemberVisibility API functions"
+```
+
+---
+
+### Task 6: FamilyMemberCard component
+
+**Files:**
+- Create: `cfc-frontend/components/FamilyMemberCard.vue`
+
+- [ ] **Step 1: Create component**
+
+```vue
+<template>
+  <view class="family-member-card" :class="{ 'is-active': isSelected }" @click="onSelect">
+    <view class="member-avatar-wrap">
+      <image v-if="member.avatar" class="member-avatar" :src="member.avatar" mode="aspectFill" />
+      <view v-else class="member-avatar-default">
+        <text class="avatar-letter">{{ displayLetter }}</text>
+      </view>
+      <text class="member-role-badge">{{ member.memberType === 'parent' ? '家长' : '孩子' }}</text>
+    </view>
+    <text class="member-name">{{ member.nickname }}</text>
+    <text class="member-dan" v-if="member.danLevel">{{ member.danLevel }}</text>
+    <view class="selected-check" v-if="isSelected">
+      <text class="check-icon">✓</text>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  props: {
+    member: { type: Object, required: true },
+    isSelected: { type: Boolean, default: false }
+  },
+  computed: {
+    displayLetter: function() {
+      if (!this.member.nickname) return '?'
+      return this.member.nickname[0]
+    }
+  },
+  methods: {
+    onSelect: function() {
+      this.$emit('select', this.member)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.family-member-card {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 16rpx 20rpx;
+  margin-right: 16rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  min-width: 130rpx;
+  position: relative;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+  border: 3rpx solid transparent;
+  transition: all 0.2s;
+}
+.family-member-card.is-active {
+  border-color: #F97316;
+  background: #FFF7ED;
+}
+.member-avatar-wrap {
+  position: relative;
+  margin-bottom: 8rpx;
+}
+.member-avatar {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+}
+.member-avatar-default {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  background: #FDE68A;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.avatar-letter {
+  font-size: 32rpx;
+  color: #92400E;
+  font-weight: bold;
+}
+.member-role-badge {
+  position: absolute;
+  bottom: -4rpx;
+  right: -8rpx;
+  font-size: 18rpx;
+  color: #fff;
+  background: #F97316;
+  padding: 2rpx 8rpx;
+  border-radius: 12rpx;
+  line-height: 1.4;
+  transform: scale(0.85);
+}
+.member-name {
+  font-size: 24rpx;
+  color: #333;
+  font-weight: 500;
+  text-align: center;
+  max-width: 100rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.member-dan {
+  font-size: 18rpx;
+  color: #F97316;
+  margin-top: 2rpx;
+}
+.selected-check {
+  position: absolute;
+  top: 4rpx;
+  right: 4rpx;
+  width: 32rpx;
+  height: 32rpx;
+  background: #F97316;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.check-icon {
+  color: #fff;
+  font-size: 20rpx;
+  font-weight: bold;
+}
+</style>
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add cfc-frontend/components/FamilyMemberCard.vue
+git commit -m "feat: add FamilyMemberCard component"
+```
+
+---
+
+### Task 7: FamilyMemberStrip component
+
+**Files:**
+- Create: `cfc-frontend/components/FamilyMemberStrip.vue`
+
+- [ ] **Step 1: Create component**
+
+```vue
+<template>
+  <view class="member-strip" v-if="members && members.length > 0">
+    <scroll-view class="strip-scroll" scroll-x :show-scrollbar="false" :enhanced="true">
+      <view class="strip-inner">
+        <FamilyMemberCard
+          v-for="m in members"
+          :key="m.memberType + '-' + m.id"
+          :member="m"
+          :isSelected="m.id === selectedMemberId && m.memberType === selectedMemberType"
+          @select="onSelectMember"
+        />
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import FamilyMemberCard from './FamilyMemberCard.vue'
+
+export default {
+  components: { FamilyMemberCard },
+  props: {
+    members: { type: Array, default: function() { return [] } },
+    selectedMemberId: { type: Number, default: null },
+    selectedMemberType: { type: String, default: '' }
+  },
+  methods: {
+    onSelectMember: function(member) {
+      this.$emit('select', member)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.member-strip {
+  margin: 0 30rpx 16rpx;
+}
+.strip-scroll {
+  width: 100%;
+  white-space: nowrap;
+}
+.strip-inner {
+  display: flex;
+  flex-direction: row;
+  padding: 4rpx 0;
+}
+</style>
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add cfc-frontend/components/FamilyMemberStrip.vue
+git commit -m "feat: add FamilyMemberStrip horizontal scrolling component"
+```
+
+---
+
+### Task 8: Integrate FamilyMemberStrip into parent-index.vue
+
+**Files:**
+- Modify: `cfc-frontend/pages/index/parent-index.vue`
+
+- [ ] **Step 1: Add component import (after line ~361)**
+
+```javascript
+import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
+```
+
+- [ ] **Step 2: Add to components block (around line ~375)**
+
+```javascript
+FamilyMemberStrip,
+```
+
+- [ ] **Step 3: Add data properties (in data() around line ~390)**
+
+```javascript
+familyMembersVisible: [],
+selectedMemberId: null,
+selectedMemberType: 'child',
+```
+
+- [ ] **Step 4: Insert FamilyMemberStrip in template after PageBanner**
+
+Insert between `PageBanner` (line ~10) and the `children-row` section (line ~13):
+
+```html
+<!-- ===== 家庭成员卡片条 ===== -->
+<FamilyMemberStrip
+  v-if="isLoggedIn"
+  :members="familyMembersVisible"
+  :selectedMemberId="selectedMemberId"
+  :selectedMemberType="selectedMemberType"
+  @select="onFamilyMemberSelect" />
+```
+
+- [ ] **Step 5: Add data loading in loadData() method**
+
+After the existing `getFamilyMembers()` call (~line 488), add:
+
+```javascript
+try {
+  const visibleRes = await getVisibleFamilyMembers()
+  const visibleMembers = visibleRes.data || []
+  this.familyMembersVisible = visibleMembers
+  // 默认选中第一个孩子
+  var firstChild = visibleMembers.find(function(m) { return m.memberType === 'child' })
+  if (firstChild) {
+    this.selectedMemberId = firstChild.id
+    this.selectedMemberType = firstChild.memberType
+  }
+} catch (e) {
+  console.log('获取可见家庭成员失败', e)
+}
+```
+
+- [ ] **Step 6: Add `onFamilyMemberSelect` method**
+
+```javascript
+onFamilyMemberSelect: function(member) {
+  this.selectedMemberId = member.id
+  this.selectedMemberType = member.memberType
+  // 切换选中孩子以刷新数据
+  if (member.memberType === 'child') {
+    this.selectedChild = { id: member.id, nickname: member.nickname }
+    this.loadChildData(member.id)
+  }
+  // 如果是家长,切换为家长视图
+}
+```
+
+- [ ] **Step 7: Add import for `getVisibleFamilyMembers`**
+
+Find the existing import line ~354:
+```javascript
+import { getFamilyMembers, getChildren, ... } from '../../utils/api.js'
+```
+Change to:
+```javascript
+import { getFamilyMembers, getChildren, getVisibleFamilyMembers, getPendingReviewTasks, ... } from '../../utils/api.js'
+```
+
+- [ ] **Step 8: Commit**
+
+```bash
+git add cfc-frontend/pages/index/parent-index.vue
+git commit -m "feat: integrate FamilyMemberStrip into parent-index page"
+```
+
+---
+
+### Task 9: Integrate FamilyMemberStrip into body/index.vue
+
+**Files:**
+- Modify: `cfc-frontend/pages/body/index.vue`
+
+- [ ] **Step 1: Add component import after existing imports (~line 1-5)**
+
+```javascript
+import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
+import { getVisibleFamilyMembers } from '../../utils/api.js'
+```
+
+- [ ] **Step 2: Add to components block**
+
+```javascript
+FamilyMemberStrip,
+```
+
+- [ ] **Step 3: Add data properties (in data())**
+
+```javascript
+familyMembersVisible: [],
+selectedMemberId: null,
+selectedMemberType: '',
+```
+
+- [ ] **Step 4: Insert FamilyMemberStrip in template after PageBanner (after line 6)**
+
+```html
+<!-- ===== 家庭成员卡片条 ===== -->
+<FamilyMemberStrip
+  v-if="isLoggedIn"
+  :members="familyMembersVisible"
+  :selectedMemberId="selectedMemberId"
+  :selectedMemberType="selectedMemberType"
+  @select="onFamilyMemberSelect" />
+```
+
+- [ ] **Step 5: Add data loading and onFamilyMemberSelect method**
+
+In `onShow` or a dedicated `loadFamilyMembers` method:
+```javascript
+loadVisibleMembers: function() {
+  var self = this
+  getVisibleFamilyMembers().then(function(res) {
+    if (res && res.data) {
+      self.familyMembersVisible = res.data
+    }
+  }).catch(function(e) {
+    console.log('获取可见家庭成员失败', e)
+  })
+},
+onFamilyMemberSelect: function(member) {
+  this.selectedMemberId = member.id
+  this.selectedMemberType = member.memberType
+  // 切换该成员的健康数据
+  this.loadHealthData(member)
+}
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add cfc-frontend/pages/body/index.vue
+git commit -m "feat: integrate FamilyMemberStrip into body page"
+```
+
+---
+
+### Task 10: Integrate FamilyMemberStrip into mind/index.vue
+
+**Files:**
+- Modify: `cfc-frontend/pages/mind/index.vue`
+
+- [ ] **Step 1: Add import + component + data + template insertion**
+
+Same pattern as Task 9 — insert `FamilyMemberStrip` after `PageBanner`, load `getVisibleFamilyMembers`, add `onFamilyMemberSelect`.
+
+Find the template block and add after `PageBanner` closing tag (after line ~6):
+```html
+<FamilyMemberStrip
+  v-if="isLoggedIn"
+  :members="familyMembersVisible"
+  :selectedMemberId="selectedMemberId"
+  :selectedMemberType="selectedMemberType"
+  @select="onFamilyMemberSelect" />
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add cfc-frontend/pages/mind/index.vue
+git commit -m "feat: integrate FamilyMemberStrip into mind page"
+```
+
+---
+
+### Task 11: Integrate FamilyMemberStrip into action/index.vue
+
+**Files:**
+- Modify: `cfc-frontend/pages/action/index.vue`
+
+- [ ] **Step 1: Same pattern as Task 9/10**
+
+Insert after `PageBanner` (~line 6):
+```html
+<FamilyMemberStrip
+  v-if="isLoggedIn"
+  :members="familyMembersVisible"
+  :selectedMemberId="selectedMemberId"
+  :selectedMemberType="selectedMemberType"
+  @select="onFamilyMemberSelect" />
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add cfc-frontend/pages/action/index.vue
+git commit -m "feat: integrate FamilyMemberStrip into action page"
+```
+
+---
+
+### Task 12: Visibility toggle UI — child edit page
+
+**Files:**
+- Modify: `cfc-frontend/pages/profile/edit-child.vue`
+
+- [ ] **Step 1: Add visibility toggle switch after the theme picker (after line ~65)**
+
+```html
+<!-- 对家庭成员可见 -->
+<view class="form-item">
+  <text class="label">对家庭成员可见</text>
+  <switch :checked="form.showToFamily !== 0" @change="onVisibilityChange" color="#F97316" />
+  <text class="hint">关闭后其他家庭成员看不到该孩子</text>
+</view>
+```
+
+- [ ] **Step 2: Add change handler and form default**
+
+```javascript
+onVisibilityChange: function(e) {
+  this.form.showToFamily = e.detail.value ? 1 : 0
+}
+```
+
+In `data()`, set form default:
+```javascript
+form: {
+  // ... existing fields
+  showToFamily: 1
+}
+```
+
+In `onLoad`, when loading child data:
+```javascript
+// after setting form fields
+self.form.showToFamily = child.showToFamily !== undefined ? child.showToFamily : 1
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add cfc-frontend/pages/profile/edit-child.vue
+git commit -m "feat: add showToFamily toggle to child edit page"
+```
+
+---
+
+### Task 13: Visibility toggle UI — profile page (for self)
+
+**Files:**
+- Modify: `cfc-frontend/pages/profile/profile.vue`
+
+- [ ] **Step 1: Add visibility toggle in settings section**
+
+Find the profile settings area and add:
+```html
+<view class="setting-item">
+  <text class="setting-label">对家庭成员可见</text>
+  <switch :checked="showToFamily !== 0" @change="onSelfVisibilityChange" color="#F97316" />
+</view>
+```
+
+- [ ] **Step 2: Add handler**
+
+```javascript
+onSelfVisibilityChange: function(e) {
+  var show = e.detail.value ? 1 : 0
+  var self = this
+  updateMemberVisibility(self.userId, 'parent', show).then(function(res) {
+    if (res && res.code === 200) {
+      uni.showToast({ title: '已更新', icon: 'success' })
+    }
+  })
+}
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add cfc-frontend/pages/profile/profile.vue
+git commit -m "feat: add showToFamily toggle to profile page"
+```
+
+---
+
+### Task 14: Final verification
+
+- [ ] **Step 1: Backend compilation**
+
+Run: `cd cfc-backend && mvn clean compile -q`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 2: Quick LSP check on modified frontend files**
+
+Check for template syntax issues (no `?.`, no CSS Grid).
+
+- [ ] **Step 3: Verify file checklist**
+
+- [ ] `DatabaseInitializer.java` — migration added
+- [ ] `UpdateChildDTO.java` — showToFamily field
+- [ ] `ChildInfoDTO.java` — showToFamily field
+- [ ] `FamilyMemberVO.java` — new DTO
+- [ ] `UserService.java` — getVisibleMembers + updateVisibility
+- [ ] `FamilyUserController.java` — 2 new endpoints
+- [ ] `api.js` — 2 new API functions
+- [ ] `FamilyMemberCard.vue` — new component
+- [ ] `FamilyMemberStrip.vue` — new component
+- [ ] `parent-index.vue` — strip integrated
+- [ ] `body/index.vue` — strip integrated
+- [ ] `mind/index.vue` — strip integrated
+- [ ] `action/index.vue` — strip integrated
+- [ ] `edit-child.vue` — visibility toggle
+- [ ] `profile.vue` — visibility toggle (self)

+ 224 - 0
docs/superpowers/specs/2026-06-15-family-member-cards-design.md

@@ -0,0 +1,224 @@
+# 家庭成员卡片跨页面展示 + 可见性控制 — 设计文档
+
+**日期:** 2026-06-15
+**状态:** 草案(待审批)
+
+## 1. 概述
+
+在首页、身体、心智、行动四个 Tab 页面顶部增加家庭成员卡片条,点击卡片切换当前查看的成员,页面数据随之过滤为该成员在对应维度的内容。成员(家长/孩子)可控制自己是否对其他家庭成员可见。
+
+## 2. 数据模型
+
+### 2.1 现有模型
+
+- `children` 表 (`Child.java`): nickname, age, gender, danLevel, totalPoints, familyId
+- `users` 表 (`User.java`): nickname, avatar, role, familyId
+- `getFamilyMembers()` API (`/api/family/user/family-members`): 返回 `{ parents: [...], children: [...] }`
+
+### 2.2 新增字段
+
+`children` 表和 `users` 表各新增:
+
+```sql
+show_to_family TINYINT DEFAULT 1 COMMENT '是否对家庭成员可见: 1=是, 0=否'
+```
+
+通过 `DatabaseInitializer` 自动迁移:
+
+```java
+// 检查并添加 show_to_family 列
+String checkChildCol = "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'children' AND COLUMN_NAME = 'show_to_family'";
+if (jdbcTemplate.queryForObject(checkChildCol, Integer.class) == 0) {
+    jdbcTemplate.execute("ALTER TABLE children ADD COLUMN show_to_family TINYINT DEFAULT 1 COMMENT '是否对家庭成员可见'");
+}
+// 同样处理 users 表
+```
+
+### 2.3 新增 DTO: `FamilyMemberVO`
+
+```java
+@Data
+public class FamilyMemberVO {
+    private Long id;           // 成员ID (child.id 或 user.id)
+    private String nickname;   // 昵称
+    private String avatar;     // 头像URL (用户表)
+    private String memberType; // 'child' 或 'parent'
+    private String roleLabel;  // '孩子' 或 '家长'
+    private String danLevel;   // 段位 (仅孩子)
+    private Integer totalPoints;
+    private Boolean showToFamily;
+}
+```
+
+## 3. API 设计
+
+### 3.1 `POST /api/family/members/visible`
+
+获取当前家庭中所有 `show_to_family = 1` 的成员。
+
+**请求体:** 无(通过 JWT 获取 userId)
+
+**响应:**
+```json
+{
+  "code": 200,
+  "data": [
+    {
+      "id": 1,
+      "nickname": "小明",
+      "avatar": "",
+      "memberType": "child",
+      "roleLabel": "孩子",
+      "danLevel": "DAN3",
+      "totalPoints": 120,
+      "showToFamily": true
+    },
+    {
+      "id": 2,
+      "nickname": "妈妈",
+      "avatar": "https://...",
+      "memberType": "parent",
+      "roleLabel": "家长",
+      "danLevel": null,
+      "totalPoints": 500,
+      "showToFamily": true
+    }
+  ]
+}
+```
+
+### 3.2 `POST /api/family/members/update-visibility`
+
+切换当前用户的可见性。
+
+**请求体:**
+```json
+{
+  "memberId": 1,
+  "memberType": "child",
+  "showToFamily": false
+}
+```
+
+**约束:** 只有家长可以修改孩子的可见性;用户只能修改自己的可见性。
+
+## 4. 前端组件
+
+### 4.1 `FamilyMemberCard.vue`
+
+单个成员卡片组件。
+
+**Props:**
+- `member: Object` — 成员数据 (FamilyMemberVO)
+- `isSelected: Boolean` — 是否当前选中
+
+**Events:**
+- `@select(member)` — 点击选中
+
+**样式:**
+- 尺寸: 120rpx × 160rpx 竖卡
+- 头像(圆形)+ 昵称 + 角色标签
+- 选中态: 橙色边框 + 底部对勾标记
+- 不可见成员不展示(由父组件过滤)
+
+### 4.2 `FamilyMemberStrip.vue`
+
+水平滚动成员卡片条。
+
+**Props:**
+- `members: Array` — 成员列表
+- `selectedMemberId: Number` — 当前选中ID
+
+**Events:**
+- `@select(member)` — 卡片点击
+- `@visibilityChange(memberId, show)` — 可见性变更
+
+**行为:**
+- 少于 4 个成员:居中均匀排列
+- 4 个及以上:scroll-view 水平滚动
+- 点击卡片 → 触发 select 事件
+
+## 5. 页面集成
+
+### 5.1 组件层级(四页统一)
+
+```
+PageBanner
+  ↓
+FamilyMemberStrip ← 新增(水平滚动,高亮选中成员)
+  ↓
+FamilyEnergyBar(已有,数据响应 selectedMemberId)
+  ↓
+UserQuickEntry(已有,保持不变)
+  ↓
+各维度专属内容(随 selectedMemberId 切换)
+```
+
+### 5.2 各页面差异
+
+| 页面 | 选中成员后的数据切换逻辑 |
+|------|------------------------|
+| `parent-index.vue` | 能量沙盘、成长数据统计切换为该成员的数据;孩子卡片区域保持不变 |
+| `body/index.vue` | 健康报告、功能入口切换为该成员 |
+| `mind/index.vue` | EMI/DAN 测评数据切换为该成员 |
+| `action/index.vue` | 任务、活动、商品切换为该成员 |
+
+### 5.3 数据流
+
+```
+FamilyMemberStrip
+  @select(member)
+    → 父页面设置 selectedMemberId
+    → 重新请求该成员在对应维度的数据
+    → 下发给 FamilyEnergyBar / 内容组件
+```
+
+已有模式参考:`UserQuickEntry` 的 `@childChanged` → `onChildChanged` → 重新加载数据。
+
+## 6. 可见性控制 UI
+
+### 6.1 家长端
+
+- **孩子编辑页**(`pages/family/child-edit.vue` 或新增设置页):新增开关"对其他家庭成员可见"
+- **个人设置页**(`pages/profile/profile.vue`):新增开关"对家庭成员可见"
+
+### 6.2 孩子端
+
+- 孩子本人不可修改可见性(由家长控制)
+
+## 7. 向后兼容
+
+- `show_to_family` 默认 `1`,现有成员不受影响
+- 新增 API `/api/family/members/visible` 不影响现有 `getFamilyMembers`
+- 各页面插 `FamilyMemberStrip` 用 `v-if="isLoggedIn"` 控制,不登录不影响
+
+## 8. 实施计划
+
+### Phase 1: 后端
+1. `DatabaseInitializer` 添加 `show_to_family` 字段迁移
+2. `FamilyMemberVO` DTO
+3. `FamilyService.getVisibleMembers(userId)` 查询逻辑
+4. `FamilyService.updateVisibility(memberId, memberType, showToFamily)` 更新逻辑
+5. `FamilyController` 新增两个端点
+6. `mvn clean compile` 验证
+
+### Phase 2: 前端组件
+1. `FamilyMemberCard.vue` 组件
+2. `FamilyMemberStrip.vue` 组件
+3. `api.js` 添加 `getVisibleFamilyMembers` 和 `updateMemberVisibility`
+
+### Phase 3: 页面集成
+1. `parent-index.vue` — 插 FamilyMemberStrip
+2. `body/index.vue` — 插 FamilyMemberStrip
+3. `mind/index.vue` — 插 FamilyMemberStrip
+4. `action/index.vue` — 插 FamilyMemberStrip
+
+### Phase 4: 可见性控制UI
+1. 孩子编辑页添加 showToFamily 开关
+2. 个人设置页添加 showToFamily 开关
+
+## 9. 未纳入范围
+
+- 本设计不涉及家长端/孩子端之间数据权限的细粒度控制(如"只对爸爸可见不对妈妈可见"),当前仅用 `show_to_family` 二值控制
+- 不涉及首页 child-index/teacher-index 的成员卡片(仅 parent-index + body/mind/action 三个维度页)
+- 不修改现有 `FamilyEnergyBar` 和 `wuxing-sandbox` 的行为