Selaa lähdekoodia

docs: 家庭成员关系条增强实现计划(FamilyMemberStrip)

Sisyphus 1 kuukausi sitten
vanhempi
sitoutus
d378693a2b

+ 2 - 1
docs/superpowers/PROJECT-OVERVIEW.md

@@ -2,7 +2,7 @@
 
 **文档版本:** v2.5
 **日期:** 2026-08-04
-**状态:** 已确认(v2.1 Phase 2-4 全栈完成)+ 虚拟支付改造(Tasks 1-12 已完成,退款闭环实施中)+ 新用户注册引导(12 Tasks 全栈完成)
+**状态:** 已确认(v2.1 Phase 2-4 全栈完成)+ 虚拟支付改造(Tasks 1-12 已完成,退款闭环实施中)+ 新用户注册引导(12 Tasks 全栈完成)+ 家庭成员关系条增强(✅ 已实施)
 **维护:** 所有需求变更需更新本文档
 
 ---
@@ -68,6 +68,7 @@
 | 家庭成员卡片(跨页展示+可见性) | ✅ 已上线 | `specs/2026-06-15-family-member-cards-design.md` + `plans/2026-06-15-family-member-cards.md` |
 | 成员详情页维度专属内容区 | ✅ 已实施(member-detail.vue 505行 + 4维度section组件 879行) | `specs/2026-07-14-member-detail-dimension-sections.md` |
 | 关系问卷与互动记录功能补全 | ✅ 已实施(3表迁移154-156+评分算法+LangGraph接入+跨家庭校验+编辑bug修复) | `specs/2026-08-04-relationship-feedback-fix-design.md` + `plans/2026-08-04-relationship-feedback-fix.md` |
+| 家庭成员关系条(FamilyMemberStrip)关系展示增强 | ✅ 已实施(后端:FamilyRelationship 三列 + 迁移180 + pairwise 接口 + VO 补齐;前端:FamilyMemberCard 三色点 + FamilyMemberStrip 空状态引导 + Action Sheet 升级 + pairwise API + centerMemberId 视角模式 + member-home-detail.vue 接入) | `specs/2026-08-05-family-relationship-strip-design.md` + `plans/2026-08-05-family-relationship-strip.md` |
 
 ---
 

+ 1022 - 0
docs/superpowers/plans/2026-08-05-family-relationship-strip.md

@@ -0,0 +1,1022 @@
+# FamilyMemberStrip 关系展示增强 实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 在 FamilyMemberStrip/FamilyMemberCard 上直接呈现关系类型与关系现状(信任度/亲密度/沟通质量),长按菜单升级为带关系现状区块的 Action Sheet,支持未创建家庭引导 + 当前用户添加/邀请成员入口,并新增 pairwise 接口支撑成员详情页以任意成员为中心的视图。
+
+**架构:** 后端在 `family_relationships` 表增加评分三列并新增 pairwise 接口(问卷提交时同步写入);前端在 FamilyMemberCard 徽章下增加三个迷你色点,FamilyMemberStrip 增加空状态引导与 `centerMemberId` prop,长按菜单升级为分区 Action Sheet。
+
+**技术栈:** Spring Boot 2.7.18 + MyBatis-Plus(后端)/ uni-app Vue 2 Options API(前端)
+
+**关联设计文档:** `docs/superpowers/specs/2026-08-05-family-relationship-strip-design.md`
+
+---
+
+## 文件结构
+
+**后端(cfc-backend/src/main/java/com/etotem/cfc/):**
+- 修改 `entity/FamilyRelationship.java` — 增加 trustScore/intimacyScore/communicationScore 三字段
+- 修改 `config/DatabaseInitializer.java` — 迁移154:family_relationships 表加三列
+- 修改 `resources/schema.sql` — family_relationships CREATE TABLE 补三列
+- 修改 `service/RelationshipQuestionnaireService.java` — submitQuestionnaire 增加 pairwise 落库
+- 修改 `dto/FamilyMemberVO.java` — 补齐 communicationScore 字段
+- 修改 `service/FamilyMemberService.java` — listMembers 补齐 communicationScore
+- 修改 `service/RelationshipQualityService.java` — 新增 getPairwiseRelationships()
+- 修改 `controller/family/RelationshipQualityController.java` — 新增 POST /pairwise
+
+**前端(cfc-frontend/):**
+- 修改 `components/FamilyMemberCard.vue` — 徽章下加三色点
+- 修改 `components/FamilyMemberStrip.vue` — 空状态引导 + centerMemberId prop + Action Sheet 升级
+- 修改 `utils/api.js` — 新增 getPairwiseRelationships()
+- 修改 `pages/home-pages/member-home-detail.vue` — 接入 centerMemberId 模式
+
+**测试:**
+- 无现成后端测试框架(mvn test 只有 1 个测试类),以 `mvn clean compile` 验证
+- 前端以 `npm run build:mp-weixin` 验证
+
+---
+
+### 任务 1:后端实体 + 数据库迁移
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyRelationship.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+- 修改:`cfc-backend/src/main/resources/schema.sql`
+
+- [ ] **步骤 1:实体加字段**
+
+在 `FamilyRelationship.java` 的 `relationshipKey` 字段后追加:
+
+```java
+    /** 信任度(答卷人视角) 0-100 */
+    private BigDecimal trustScore;
+
+    /** 亲密度(答卷人视角) 0-100 */
+    private BigDecimal intimacyScore;
+
+    /** 沟通质量(答卷人视角) 0-100 */
+    private BigDecimal communicationScore;
+```
+
+并补充 import(若未引入 `java.math.BigDecimal`,加在 `import java.io.Serializable;` 之后)。
+
+- [ ] **步骤 2:DatabaseInitializer 加迁移**
+
+找到 `// 迁移153:` 注释块,在其后追加(用 `ensureColumn` 三次调用):
+
+```java
+        // 迁移154: family_relationships表添加关系质量评分三列(关系条展示增强)
+        ensureColumn("family_relationships", "trust_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '信任度(答卷人视角)'");
+        ensureColumn("family_relationships", "intimacy_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '亲密度(答卷人视角)'");
+        ensureColumn("family_relationships", "communication_score", "DECIMAL(5,2) DEFAULT 0 COMMENT '沟通质量(答卷人视角)'");
+```
+
+- [ ] **步骤 3:同步 schema.sql**
+
+`schema.sql` 的 `family_relationships` CREATE TABLE(约 1818 行)在 `relationship_key` 行后加三列:
+
+```sql
+    trust_score        DECIMAL(5,2) DEFAULT 0 COMMENT '信任度(答卷人视角)',
+    intimacy_score     DECIMAL(5,2) DEFAULT 0 COMMENT '亲密度(答卷人视角)',
+    communication_score DECIMAL(5,2) DEFAULT 0 COMMENT '沟通质量(答卷人视角)',
+```
+
+- [ ] **步骤 4:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyRelationship.java cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql
+git commit -m "feat(backend): family_relationships表增加关系质量评分三列"
+```
+
+---
+
+### 任务 2:问卷提交同步写入 pairwise 关系评分
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/RelationshipQuestionnaireService.java`
+
+- [ ] **步骤 1:注入 FamilyRelationshipMapper 并补 import**
+
+该文件已有 `@Resource private FamilyMemberMapper familyMemberMapper;`(line 38)与全部所需 import(LambdaQueryWrapper/FamilyMember/BigDecimal/Date/ArrayList)。只需新增 mapper 注入:
+
+```java
+    @Resource
+    private com.etotem.cfc.mapper.FamilyRelationshipMapper familyRelationshipMapper;
+```
+
+并在 import 区(`import com.etotem.cfc.entity.RelationshipQuestionnaireSnapshot;` 之后)补:
+
+```java
+import com.etotem.cfc.entity.FamilyRelationship;
+import com.etotem.cfc.mapper.FamilyRelationshipMapper;
+```
+
+- [ ] **步骤 2:修改 submitQuestionnaire,提交后调用 pairwise 落库**
+
+在 `submitQuestionnaire` 中 `updateMemberScores(snapshot.getFamilyMemberId(), scores);` 之后追加:
+
+```java
+        // 同步写入 pairwise 关系评分(答卷人视角 → 被评成员)
+        syncPairwiseScores(snapshot.getRespondentId(), snapshot.getFamilyMemberId(), scores);
+```
+
+- [ ] **步骤 3:新增 syncPairwiseScores 方法**
+
+在 `updateMemberScores` 方法后新增:
+
+```java
+    /**
+     * 将问卷评分写入 family_relationships 表(答卷人视角)
+     * 答卷人无 family_member 记录或关系行不存在时,静默跳过(不阻塞问卷保存)
+     */
+    private void syncPairwiseScores(Long respondentUserId, Long targetMemberId, BigDecimal[] scores) {
+        try {
+            if (respondentUserId == null || targetMemberId == null || scores == null) return;
+            // 找到答卷人对应的 family_member 记录
+            LambdaQueryWrapper<FamilyMember> respWrapper = new LambdaQueryWrapper<>();
+            respWrapper.eq(FamilyMember::getUserId, respondentUserId);
+            FamilyMember respondent = familyMemberMapper.selectOne(respWrapper);
+            if (respondent == null) {
+                log.warn("答卷人用户{}无family_member记录,跳过pairwise评分同步", respondentUserId);
+                return;
+            }
+            // 查 from→to 关系行(有则更新,无则插入)
+            LambdaQueryWrapper<FamilyRelationship> relWrapper = new LambdaQueryWrapper<>();
+            relWrapper.eq(FamilyRelationship::getFromMemberId, respondent.getId())
+                    .eq(FamilyRelationship::getToMemberId, targetMemberId);
+            FamilyRelationship rel = familyRelationshipMapper.selectOne(relWrapper);
+            Date now = new Date();
+            if (rel != null) {
+                rel.setTrustScore(scores[0]);
+                rel.setIntimacyScore(scores[1]);
+                rel.setCommunicationScore(scores[2]);
+                rel.setUpdatedAt(now);
+                familyRelationshipMapper.updateById(rel);
+            } else {
+                FamilyMember target = familyMemberMapper.selectById(targetMemberId);
+                if (target == null) return;
+                FamilyRelationship newRel = new FamilyRelationship();
+                newRel.setFamilyId(target.getFamilyId());
+                newRel.setFromMemberId(respondent.getId());
+                newRel.setToMemberId(targetMemberId);
+                int respGen = respondent.getGeneration() != null ? respondent.getGeneration() : 0;
+                int targetGen = target.getGeneration() != null ? target.getGeneration() : 0;
+                newRel.setGenerationValue(respGen - targetGen);
+                newRel.setTrustScore(scores[0]);
+                newRel.setIntimacyScore(scores[1]);
+                newRel.setCommunicationScore(scores[2]);
+                newRel.setCreatedAt(now);
+                newRel.setUpdatedAt(now);
+                familyRelationshipMapper.insert(newRel);
+            }
+        } catch (Exception e) {
+            log.warn("pairwise评分同步失败: {}", e.getMessage());
+        }
+    }
+```
+
+需要新增 import:`com.etotem.cfc.entity.FamilyRelationship`(文件顶部 `import com.etotem.cfc.entity.RelationshipQuestionnaireSnapshot;` 之后)。
+
+- [ ] **步骤 4:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/RelationshipQuestionnaireService.java
+git commit -m "feat(backend): 问卷提交时同步pairwise关系评分到family_relationships"
+```
+
+---
+
+### 任务 3:后端 VO 补齐 communicationScore + pairwise 接口
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/RelationshipQualityService.java`
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/family/RelationshipQualityController.java`
+
+- [ ] **步骤 1:FamilyMemberVO 增加 communicationScore 字段**
+
+`FamilyMemberVO.java` 现有 `intimacyScore`(line 65)与 `lastInteractionAt`(line 68)之间补:
+
+```java
+    /** 沟通质量分数 */
+    private java.math.BigDecimal communicationScore;
+```
+
+- [ ] **步骤 2:FamilyMemberService.listMembers 补齐 communicationScore 与 isSelf**
+
+`FamilyMemberService.java` 现有 `vo.setTrustScore(fm.getTrustScore());`(line 226)与 `vo.setIntimacyScore(fm.getIntimacyScore());`(line 227),在其后补 communicationScore;同时该方法**从未设置 `isSelf`**(已核实,全文件无 `setIsSelf`),需一并补齐——否则首页 8 处现有 FamilyMemberStrip 调用中"添加成员/邀请成员"入口永不显示,且现有 `canRemove` 的 `!actionMember.isSelf` 判断恒为 true(对自己也显示移除按钮):
+
+```java
+        vo.setCommunicationScore(fm.getCommunicationScore());
+        vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
+```
+
+> 说明:`viewerMember` 在该方法 line 175-181 已计算(当前查看者在 family_members 中的记录),直接复用。`isSelf` 语义 = 该成员就是当前登录用户在家庭中的记录。
+
+- [ ] **步骤 3:RelationshipQualityService 新增 getPairwiseRelationships 方法**
+
+该类已 import `LambdaQueryWrapper / FamilyMember / FamilyMemberMapper / BigDecimal / ArrayList / Date`,且已有 private `computeEffectiveRole(FamilyMember)`(line 211)可复用。新增注入:
+
+```java
+    @Resource
+    private com.etotem.cfc.service.FamilyRelationshipService familyRelationshipService;
+```
+
+在 `getScores` 方法后新增:
+
+```java
+    /**
+     * 获取以指定成员为中心的所有成员关系(关系类型 + 三项评分)
+     */
+    public List<FamilyMemberVO> getPairwiseRelationships(Long centerMemberId, Long familyId) {
+        List<FamilyMemberVO> result = new ArrayList<>();
+        FamilyMember center = familyMemberMapper.selectById(centerMemberId);
+        if (center == null || (familyId != null && !familyId.equals(center.getFamilyId()))) {
+            throw new RuntimeException("成员不存在或不属于该家庭");
+        }
+        if (familyId == null) {
+            familyId = center.getFamilyId();
+        }
+
+        List<FamilyMember> members = familyMemberMapper.selectList(
+                new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId));
+
+        // 中心成员 → 各成员的关系行(评分与关系类型来源)
+        List<FamilyRelationship> rels = familyRelationshipService.getRelationsForMember(centerMemberId);
+        Map<Long, FamilyRelationship> relMap = new java.util.HashMap<>();
+        for (FamilyRelationship rel : rels) {
+            relMap.put(rel.getToMemberId(), rel);
+        }
+
+        for (FamilyMember fm : members) {
+            FamilyMemberVO vo = new FamilyMemberVO();
+            vo.setId(fm.getId());
+            vo.setSource("family_member");
+            vo.setUserId(fm.getUserId());
+            vo.setNickname(fm.getNickname());
+            vo.setPhone(fm.getPhone());
+            vo.setAvatar(fm.getAvatar());
+            vo.setGender(fm.getGender());
+            vo.setGeneration(fm.getGeneration());
+            vo.setEffectiveRole(computeEffectiveRole(fm));
+            vo.setRoleLabel(computeEffectiveRole(fm));
+            vo.setIsSelf(centerMemberId.equals(fm.getId()));
+            vo.setAge(fm.getAge());
+
+            FamilyRelationship rel = relMap.get(fm.getId());
+            if (rel != null) {
+                vo.setGenerationValue(rel.getGenerationValue());
+                String label = familyRelationshipService.computeRelationshipLabel(rel.getGenerationValue(), fm.getGender());
+                vo.setRelativeLabel(label);
+                vo.setRelationship(label);
+                vo.setTrustScore(rel.getTrustScore());
+                vo.setIntimacyScore(rel.getIntimacyScore());
+                vo.setCommunicationScore(rel.getCommunicationScore());
+            } else if (centerMemberId.equals(fm.getId())) {
+                vo.setRelationship("我");
+                vo.setRelativeLabel("我");
+            } else {
+                vo.setTrustScore(BigDecimal.ZERO);
+                vo.setIntimacyScore(BigDecimal.ZERO);
+                vo.setCommunicationScore(BigDecimal.ZERO);
+            }
+            result.add(vo);
+        }
+        return result;
+    }
+```
+
+需要新增 import:`com.etotem.cfc.entity.FamilyRelationship`、`com.etotem.cfc.dto.FamilyMemberVO`、`java.util.Map`。
+
+> **注意(已核实):** `FamilyMember` 实体自带 `age` 字段(line 67),直接 `vo.setAge(fm.getAge())`,无需调 `FamilyMemberService.calculateAge`(那是 private 方法)。`computeEffectiveRole` 是本类 private 方法,可直接调用。
+
+- [ ] **步骤 4:Controller 新增 pairwise 端点**
+
+`RelationshipQualityController` 已有 `getFamilyIdFromRequest(userId, body)`(line 30)与 `@PostMapping("/scores")` 模式。在 `getScores` 方法后新增:
+
+```java
+    @Operation(summary = "获取以指定成员为中心的所有成员关系(pairwise)")
+    @PostMapping("/pairwise")
+    public Result<List<FamilyMemberVO>> getPairwise(@RequestBody Map<String, Object> body,
+                                                    @RequestAttribute("userId") Long userId) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        Long centerMemberId = body.get("centerMemberId") != null
+                ? Long.valueOf(body.get("centerMemberId").toString()) : null;
+        if (centerMemberId == null) {
+            return Result.error("centerMemberId不能为空");
+        }
+        Long familyId = getFamilyIdFromRequest(userId, body);
+        try {
+            List<FamilyMemberVO> members = relationshipQualityService.getPairwiseRelationships(centerMemberId, familyId);
+            return Result.success(members);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+```
+
+需要新增 import:`com.etotem.cfc.dto.FamilyMemberVO`。
+
+- [ ] **步骤 5:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.java cfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.java cfc-backend/src/main/java/com/etotem/cfc/service/RelationshipQualityService.java cfc-backend/src/main/java/com/etotem/cfc/controller/family/RelationshipQualityController.java
+git commit -m "feat(backend): 关系pairwise接口(以成员为中心) + VO补齐沟通评分"
+```
+
+---
+
+### 任务 4:前端 FamilyMemberCard 三色点
+
+**文件:**
+- 修改:`cfc-frontend/components/FamilyMemberCard.vue`
+
+- [ ] **步骤 1:模板加三色点(关系徽章下方)**
+
+`card-relation-badge` 的 `</view>` 之后、`card-spouse-mark` 之前插入:
+
+```html
+      <!-- 关系现状三色点:信任/亲密度/沟通 -->
+      <view class="card-relation-dots" v-if="hasRelationScores">
+        <view class="relation-dot" :style="{ background: dotColor(scoreKey('trust')) }"></view>
+        <view class="relation-dot" :style="{ background: dotColor(scoreKey('intimacy')) }"></view>
+        <view class="relation-dot" :style="{ background: dotColor(scoreKey('communication')) }"></view>
+      </view>
+      <view class="card-relation-dots" v-else>
+        <view class="relation-dot dot-empty"></view>
+        <view class="relation-dot dot-empty"></view>
+        <view class="relation-dot dot-empty"></view>
+      </view>
+```
+
+> **注意:** 模板中禁止带参数方法调用(`dotColor(scoreKey('trust'))` 嵌套调用在微信小程序模板编译器可能报错)。**改为计算属性返回三个色值数组:**
+
+```html
+      <view class="card-relation-dots">
+        <view class="relation-dot" v-for="(color, idx) in relationDotColors" :key="idx"
+              :style="{ background: color }"></view>
+      </view>
+```
+
+- [ ] **步骤 2:script 增加计算属性**
+
+在 `computed` 中 `relationColor` 后新增:
+
+```js
+    hasRelationScores: function() {
+      var m = this.member
+      if (!m) return false
+      var t = Number(m.trustScore) || 0
+      var i = Number(m.intimacyScore) || 0
+      var c = Number(m.communicationScore) || 0
+      return t > 0 || i > 0 || c > 0
+    },
+    relationDotColors: function() {
+      var m = this.member
+      var empty = '#E5E7EB'
+      if (!m) return [empty, empty, empty]
+      var self = this
+      var keys = ['trustScore', 'intimacyScore', 'communicationScore']
+      return keys.map(function(k) {
+        var v = Number(m[k]) || 0
+        if (v <= 0) return empty
+        if (v >= 70) return '#10B981'
+        if (v >= 40) return '#F59E0B'
+        return '#EF4444'
+      })
+    }
+```
+
+- [ ] **步骤 3:样式**
+
+`<style scoped>` 中 `.card-relation-badge` 后新增:
+
+```css
+.card-relation-dots {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  margin-top: 6rpx;
+  height: 12rpx;
+}
+.relation-dot {
+  width: 12rpx;
+  height: 12rpx;
+  border-radius: 50%;
+  margin: 0 4rpx;
+}
+.dot-empty {
+  background: #E5E7EB;
+}
+```
+
+> **注意:** 卡片宽度 140rpx,徽章在头像下方 bottom:-4rpx 处,三色点放徽章下方会与昵称挤压。**实现时需验证布局**:若重叠,将 `.card-relation-dots` 的 margin-top 调小(如 2rpx)并将 `.card-nickname` 的 margin-top 压缩;或把色点移到徽章内部右侧(`position:absolute; right:4rpx`)。以视觉不重叠为准。
+
+- [ ] **步骤 4:构建验证**
+
+运行:`cd cfc-frontend && npm run build:mp-weixin`
+预期:BUILD SUCCESS(无模板编译错误)
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-frontend/components/FamilyMemberCard.vue
+git commit -m "feat(frontend): 家庭成员卡片增加关系现状三色点"
+```
+
+---
+
+### 任务 5:前端 FamilyMemberStrip 空状态 + Action Sheet 升级
+
+**文件:**
+- 修改:`cfc-frontend/components/FamilyMemberStrip.vue`
+
+- [ ] **步骤 1:模板空状态分支(单根节点重构)**
+
+Vue 2 组件模板要求单根节点,当前根 `<view class="family-member-strip" v-if="...">` 无法直接挂 `v-else`。将根节点改为常渲染,内部用 `v-if/v-else` 二选一:
+
+```html
+<template>
+  <view class="family-member-strip">
+    <!-- 有成员:滚动条 / 居中模式 -->
+    <view v-if="renderedMembers && renderedMembers.length > 0">
+      <scroll-view
+        class="strip-scroll"
+        scroll-x
+        :scroll-with-animation="true"
+        :enhanced="true"
+        show-scrollbar="false"
+        v-if="renderedMembers.length >= 4"
+      >
+        <view class="strip-inner scroll-mode">
+          <FamilyMemberCard
+            v-for="member in renderedMembers"
+            :key="member.id"
+            :member="member"
+            :isSelected="member.id === selectedMemberId"
+            :energyScore="getMemberEnergy(member)"
+            @select="onSelect"
+            @longpress="onLongPress"
+          />
+        </view>
+      </scroll-view>
+      <view class="strip-inner center-mode" v-else>
+        <FamilyMemberCard
+          v-for="member in renderedMembers"
+          :key="member.id"
+          :member="member"
+          :isSelected="member.id === selectedMemberId"
+          :energyScore="getMemberEnergy(member)"
+          @select="onSelect"
+          @longpress="onLongPress"
+        />
+      </view>
+      <!-- 长按操作菜单:原样保留,见步骤 3 升级 -->
+    </view>
+    <!-- 未创建家庭引导 -->
+    <view class="family-strip-empty" v-else>
+      <view class="empty-content">
+        <text class="empty-icon">🏠</text>
+        <text class="empty-title">还未创建家庭</text>
+        <text class="empty-desc">创建家庭,开启全家幸福之旅</text>
+        <view class="empty-btn" @click="onCreateFamily">+ 创建家庭</view>
+      </view>
+    </view>
+  </view>
+</template>
+```
+
+> **注意:** 此步骤同时引入 `renderedMembers`(见任务 6 步骤 3 的 data/watch 改造)。模板中所有 `members` 引用(含 action-sheet 内)统一改为 `renderedMembers`。`v-if` 里的空数组判断必须写 `renderedMembers && renderedMembers.length > 0`,避免空数组渲染异常。
+
+- [ ] **步骤 2:script 增加空状态创建家庭 / 添加成员 / 邀请成员(弹窗模式)**
+
+`import` 行追加(现有:`import { kickFamilyMemberById } from '@/utils/api'`):
+
+```js
+import { kickFamilyMemberById, createFamily, generateFamilyInviteQrCode } from '@/utils/api'
+import SharePoster from '@/components/SharePoster.vue'
+import parseDate from '@/utils/format'
+```
+
+`components` 注册追加:`SharePoster`。
+
+`data` 增加字段:
+
+```js
+      creatingFamily: false,
+      showPoster: false,
+      inviteCode: '',
+      inviteQrCode: '',
+      posterNickname: '',
+      posterAvatar: ''
+```
+
+`methods` 新增:
+
+```js
+    onCreateFamily: function() {
+      var self = this
+      if (this.creatingFamily) return
+      this.creatingFamily = true
+      // 家庭名称使用当前用户昵称 + '的家庭'(与 user-edit.vue createFamilyAction 一致)
+      var userInfo = uni.getStorageSync('userInfo')
+      var nickname = uni.getStorageSync('nickname') || (userInfo && userInfo.nickname) || '我的'
+      uni.showLoading({ title: '创建中...' })
+      createFamily(nickname + '的家庭')
+        .then(function(res) {
+          uni.hideLoading()
+          self.creatingFamily = false
+          if (res.code === 200) {
+            uni.showToast({ title: '家庭创建成功', icon: 'success' })
+            self.$emit('created')
+          } else {
+            uni.showToast({ title: res.message || '创建失败', icon: 'none' })
+          }
+        })
+        .catch(function(err) {
+          uni.hideLoading()
+          self.creatingFamily = false
+          uni.showToast({ title: (err && err.message) || '创建失败', icon: 'none' })
+        })
+    },
+    addMember: function() {
+      this.closeMenu()
+      uni.navigateTo({ url: '/pages/family/add-member' })
+    },
+    // 邀请成员:弹窗展示分享海报(与 family-members.vue onInvite 一致的模式)
+    inviteMember: function() {
+      var self = this
+      this.closeMenu()
+      var userInfo = uni.getStorageSync('userInfo')
+      this.posterNickname = uni.getStorageSync('nickname') || (userInfo && userInfo.nickname) || ''
+      this.posterAvatar = uni.getStorageSync('avatar') || (userInfo && userInfo.avatar) || ''
+      this.inviteCode = ''
+      this.inviteQrCode = ''
+      uni.showLoading({ title: '生成邀请...' })
+      generateFamilyInviteQrCode()
+        .then(function(res) {
+          uni.hideLoading()
+          if (res.code === 200 && res.data) {
+            self.inviteCode = res.data.inviteCode
+            self.inviteQrCode = res.data.qrCodeBase64
+            self.showPoster = true
+          } else {
+            uni.showToast({ title: res.message || '生成失败', icon: 'none' })
+          }
+        })
+        .catch(function() {
+          uni.hideLoading()
+          uni.showToast({ title: '生成失败', icon: 'none' })
+        })
+    }
+```
+
+模板中 action-sheet 内"添加成员 / 邀请成员"入口(isSelf 才显示)加在"切换视角"之前:
+
+```html
+      <view class="action-item" @click="addMember" v-if="actionMember && actionMember.isSelf">
+        <text class="action-item-icon">➕</text>
+        <text class="action-item-text">添加成员</text>
+      </view>
+      <view class="action-item" @click="inviteMember" v-if="actionMember && actionMember.isSelf">
+        <text class="action-item-icon">📨</text>
+        <text class="action-item-text">邀请成员</text>
+      </view>
+```
+
+SharePoster 挂载位置:**根节点 `.family-member-strip` 的直接子级**(与 v-if/v-else 两个分支平级,放在它们之后、根 `</view>` 闭合前),保证空态/有成员态下都能渲染:
+
+```html
+    </view>
+    <SharePoster
+      :show="showPoster"
+      :referralCode="inviteCode"
+      :qrCodeBase64="inviteQrCode"
+      :nickname="posterNickname"
+      :userAvatar="posterAvatar"
+      @close="showPoster = false" />
+  </view>
+</template>
+```
+
+- [ ] **步骤 3:Action Sheet 模板升级(头部 + 关系现状区块)**
+
+将 `action-sheet` 内头部替换为头像 + 关系标签,关系现状区块插在 header 之后、操作项之前。**已核实微信模板禁止 `:style` 内带参方法调用(`scoreWidth('trustScore')` 不可用)**,宽度/数值统一走计算属性 `scoreWidths[]` / `scoreValues[]`(下标 0=信任度 1=亲密度 2=沟通质量):
+
+```html
+    <view class="action-sheet" v-if="showActionMenu">
+      <view class="action-header">
+        <image class="action-avatar" :src="actionMember ? (actionMember.avatar || '/static/default-avatar.png') : ''" mode="aspectFill" />
+        <view class="action-header-info">
+          <text class="action-title">{{ actionMember ? actionMember.nickname : '' }}</text>
+          <text class="action-relation">{{ actionMember ? (actionMember.relationship || actionMember.relativeLabel || actionMember.roleLabel || '') : '' }}</text>
+        </view>
+      </view>
+      <!-- 关系现状区块(无数据时展示"未测评") -->
+      <view class="action-scores" v-if="actionMember">
+        <view class="score-row">
+          <text class="score-label">信任度</text>
+          <view class="score-bar-track">
+            <view class="score-bar-fill trust" :style="{ width: scoreWidths[0] }"></view>
+          </view>
+          <text class="score-value">{{ scoreValues[0] }}</text>
+        </view>
+        <view class="score-row">
+          <text class="score-label">亲密度</text>
+          <view class="score-bar-track">
+            <view class="score-bar-fill intimacy" :style="{ width: scoreWidths[1] }"></view>
+          </view>
+          <text class="score-value">{{ scoreValues[1] }}</text>
+        </view>
+        <view class="score-row">
+          <text class="score-label">沟通质量</text>
+          <view class="score-bar-track">
+            <view class="score-bar-fill communication" :style="{ width: scoreWidths[2] }"></view>
+          </view>
+          <text class="score-value">{{ scoreValues[2] }}</text>
+        </view>
+      </view>
+      <view class="action-divider"></view>
+      <!-- 添加成员/邀请成员入口已由步骤 2 加入(isSelf 时显示),位于切换视角之前 -->
+      <view class="action-item" @click="switchToMember" v-if="canSwitch">
+        ...(保留现有)
+      </view>
+      ...(其余操作项保留现有)
+```
+
+`computed` 新增:
+
+```js
+    actionScores: function() {
+      var m = this.actionMember
+      if (!m) return []
+      var keys = ['trustScore', 'intimacyScore', 'communicationScore']
+      return keys.map(function(k) {
+        var v = Number(m[k]) || 0
+        return { width: Math.max(0, Math.min(100, v)) + '%', value: Math.round(v) }
+      })
+    },
+    scoreWidths: function() {
+      return this.actionScores.map(function(s) { return s.width })
+    },
+    scoreValues: function() {
+      return this.actionScores.map(function(s) { return s.value })
+    },
+    // 上次填写时间(问卷提交时 updateMemberScores 已更新 lastInteractionAt)
+    lastQuestionnaireText: function() {
+      var m = this.actionMember
+      if (!m || !m.lastInteractionAt) return ''
+      var d = parseDate(m.lastInteractionAt)
+      if (!d) return ''
+      var now = new Date()
+      var diffMs = now.getTime() - d.getTime()
+      var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
+      if (diffDays === 0) return '今天'
+      if (diffDays === 1) return '昨天'
+      if (diffDays < 7) return diffDays + '天前'
+      if (diffDays < 30) return Math.floor(diffDays / 7) + '周前'
+      return Math.floor(diffDays / 30) + '个月前'
+    }
+```
+
+> **注意:** 需 `import parseDate from '@/utils/format'`(小程序规范:禁止 `new Date(string)` 解析日期,统一用 `parseDate()` 归一化格式,见 cfc-frontend/AGENTS.md)。
+
+模板中"关系问卷"操作项(保留现有,追加时间后缀):
+
+```html
+      <view class="action-item" @click="questionnaireMember">
+        <text class="action-item-icon">📋</text>
+        <text class="action-item-text">关系问卷</text>
+        <text class="action-time" v-if="lastQuestionnaireText">  ·  上次填写 {{ lastQuestionnaireText }}</text>
+      </view>
+```
+
+`.action-time` 样式追加到 `.action-item-text` 同级:
+
+```css
+.action-time {
+  font-size: 22rpx;
+  color: #9CA3AF;
+  margin-left: 4rpx;
+}
+```
+
+> **未测评展示:** 三项分数全为 0/空时,进度条宽度为 `0%`、数值为 `0`。如需"未测评"文案,可在 `scoreValues` 分支内判断(`v <= 0` 时返回 `--`),实现时按视觉效果取舍,不影响数据结构。
+
+- [ ] **步骤 4:样式**
+
+`<style scoped>` 追加:
+
+```css
+/* 空状态 */
+.family-strip-empty {
+  margin: 16rpx 0;
+  padding: 40rpx 30rpx;
+  background: #FFFFFF;
+  border-radius: 24rpx;
+  display: flex;
+  justify-content: center;
+}
+.empty-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.empty-icon { font-size: 56rpx; }
+.empty-title { font-size: 30rpx; font-weight: 600; color: #1F2937; margin-top: 12rpx; }
+.empty-desc { font-size: 24rpx; color: #9CA3AF; margin-top: 8rpx; }
+.empty-btn {
+  margin-top: 24rpx;
+  padding: 16rpx 48rpx;
+  background: #F97316;
+  color: #FFFFFF;
+  font-size: 28rpx;
+  border-radius: 40rpx;
+}
+/* Action Sheet 头部 */
+.action-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  padding: 24rpx 30rpx;
+  border-bottom: 1rpx solid #F3F4F6;
+}
+.action-avatar {
+  width: 88rpx;
+  height: 88rpx;
+  border-radius: 50%;
+  margin-right: 20rpx;
+  background: #F3F4F6;
+}
+.action-header-info {
+  display: flex;
+  flex-direction: column;
+}
+/* 关系现状 */
+.action-scores {
+  padding: 20rpx 30rpx;
+}
+.score-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 12rpx;
+}
+.score-label {
+  width: 110rpx;
+  font-size: 24rpx;
+  color: #6B7280;
+}
+.score-bar-track {
+  flex: 1;
+  height: 14rpx;
+  background: #F3F4F6;
+  border-radius: 7rpx;
+  overflow: hidden;
+}
+.score-bar-fill {
+  height: 100%;
+  border-radius: 7rpx;
+}
+.score-bar-fill.trust { background: #10B981; }
+.score-bar-fill.intimacy { background: #3B82F6; }
+.score-bar-fill.communication { background: #F59E0B; }
+.score-value {
+  width: 70rpx;
+  text-align: right;
+  font-size: 24rpx;
+  color: #1F2937;
+  font-weight: 600;
+}
+.action-divider {
+  height: 1rpx;
+  background: #F3F4F6;
+  margin: 0 30rpx;
+}
+```
+
+- [ ] **步骤 5:构建验证**
+
+运行:`cd cfc-frontend && npm run build:mp-weixin`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-frontend/components/FamilyMemberStrip.vue
+git commit -m "feat(frontend): 成员条空状态引导 + 长按菜单升级关系现状Action Sheet"
+```
+
+---
+
+### 任务 6:前端 pairwise API + 组件 centerMemberId 模式
+
+**文件:**
+- 修改:`cfc-frontend/utils/api.js`
+- 修改:`cfc-frontend/components/FamilyMemberStrip.vue`
+
+- [ ] **步骤 1:api.js 新增 pairwise 接口**
+
+在 `getVisibleFamilyMembers` 定义后新增:
+
+```js
+// 获取以指定成员为中心的所有成员关系(关系类型 + 信任/亲密度/沟通评分)
+export const getPairwiseRelationships = (centerMemberId) => {
+	return request('/api/family/relationship/pairwise', 'POST', { centerMemberId })
+}
+```
+
+- [ ] **步骤 2:FamilyMemberStrip 增加 centerMemberId prop**
+
+`props` 增加:
+
+```js
+    centerMemberId: { type: Number, default: null }
+```
+
+- [ ] **步骤 3:watch centerMemberId 自动拉取 + renderedMembers 数据代理**
+
+> **已核实:** 组件 `members` 是 prop 且为只读,不能直接赋值。引入 data `renderedMembers` 作为渲染数据源,`members` prop 变更时同步;`centerMemberId` 传入时组件内部拉取 pairwise 数据覆盖渲染。
+
+`data` 增加:
+
+```js
+      renderedMembers: []
+```
+
+`watch` 新增:
+
+```js
+  watch: {
+    members: {
+      handler: function(val) {
+        this.renderedMembers = val || []
+      },
+      immediate: true
+    },
+    centerMemberId: function(val) {
+      if (val) {
+        this.loadPairwise(val)
+      }
+    }
+  }
+```
+
+`methods` 新增:
+
+```js
+    loadPairwise: function(centerMemberId) {
+      var self = this
+      getPairwiseRelationships(centerMemberId).then(function(res) {
+        if (res.code === 200 && res.data) {
+          var arr = res.data instanceof Array ? res.data : ((res.data && res.data.list) || [])
+          self.renderedMembers = arr
+        }
+      }).catch(function() {})
+    }
+```
+
+模板中所有 `members` 引用(`v-if="members && members.length > 0"`、`v-for="member in members"`、`members.length >= 4`)统一改为 `renderedMembers`(任务 5 步骤 1 已含此改造,此处确认无遗漏)。
+
+`import` 增加 `getPairwiseRelationships`。
+
+- [ ] **步骤 4:构建验证**
+
+运行:`cd cfc-frontend && npm run build:mp-weixin`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-frontend/utils/api.js cfc-frontend/components/FamilyMemberStrip.vue
+git commit -m "feat(frontend): pairwise接口 + 成员条centerMemberId视角模式"
+```
+
+---
+
+### 任务 7:成员详情页接入 pairwise
+
+**文件:**
+- 修改:`cfc-frontend/pages/home-pages/member-home-detail.vue`
+
+- [ ] **步骤 1:引入组件**
+
+`import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'`,并加入 `components` 注册。
+
+- [ ] **步骤 2:模板插入关系条区块**
+
+在 `member-card` 之后、`FamilyRelationGraph` 之前插入:
+
+```html
+      <!-- 以该成员为中心的家庭成员关系条 -->
+      <FamilyMemberStrip
+        v-if="memberId"
+        :center-member-id="memberId"
+        :selected-member-id="memberId"
+        :is-parent="true" />
+```
+
+> **注意:** `centerMemberId` 是 Number 类型 prop,`options.memberId` 来自 URL 是 String。**需在 onLoad 中 `this.memberId = Number(options.memberId) || null`**,否则 Number prop 校验告警。
+
+- [ ] **步骤 3:onLoad 转 Number**
+
+`onLoad` 修改:
+
+```js
+  onLoad: function(options) {
+    this.memberId = Number(options.memberId) || null
+    this.memberType = options.memberType || 'child'
+  },
+```
+
+- [ ] **步骤 4:构建验证**
+
+运行:`cd cfc-frontend && npm run build:mp-weixin`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 5:Commit**
+
+```bash
+git add cfc-frontend/pages/home-pages/member-home-detail.vue
+git commit -m "feat(frontend): 成员详情页接入以成员为中心的关系条"
+```
+
+---
+
+### 任务 8:最终验证
+
+**文件:** 无
+
+- [ ] **步骤 1:后端全量编译**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 2:前端构建**
+
+运行:`cd cfc-frontend && npm run build:mp-weixin`
+预期:BUILD SUCCESS
+
+- [ ] **步骤 3:检查路由无重复**
+
+运行:`grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | grep pairwise`
+预期:`/api/family/relationship/pairwise` 出现一次
+
+- [ ] **步骤 4:git 状态确认**
+
+运行:`git status --short`
+预期:无未提交变更(或仅剩余待提交的临时文件)
+
+---
+
+## 自检记录
+
+**规格覆盖度:**
+- 2.1 表扩展三列 → 任务 1 ✅
+- 2.2 问卷同步 → 任务 2 ✅
+- 2.3 pairwise 接口 → 任务 3 ✅
+- 3.1 卡片三色点 → 任务 4 ✅
+- 3.2 空状态引导 → 任务 5 ✅
+- 3.3 Action Sheet 升级(含添加/邀请成员 + 上次填写时间)→ 任务 5 ✅
+- 3.4 centerMemberId prop → 任务 6 ✅
+- 3.5 成员详情页接入 → 任务 7 ✅
+- 5 错误处理边界(fallback、权限保留)→ 任务 5/6 ✅
+- 6 验证 → 任务 8 ✅
+
+**实现期风险(已核实并在任务内处理):**
+1. 微信模板禁止带参数方法调用 → 全部改用计算属性(任务 4/5)
+2. `members` 是 prop 不可赋值 → 引入 `renderedMembers` data(任务 6)
+3. URL 参数 String → Number 转换(任务 7)
+4. ~~`calculateAgeStatic` 不存在~~ → **已核实**:`FamilyMember` 实体自带 `age` 字段(line 67),直接 `vo.setAge(fm.getAge())`(任务 3 步骤 3)
+5. ~~`FamilyMemberVO.communicationScore` 缺失~~ → **已核实并修复**:任务 3 步骤 1 补字段 + 步骤 2 在 `listMembers` 补齐(否则首页 8 处现有调用第三点恒灰)
+6. ~~`FamilyRelationshipService.getRelationsForMember` 是假设~~ → **已核实存在**(line 88,返回 from 方向边),`computeRelationshipLabel(Integer, String)` 存在(line 118)
+7. ~~`computeEffectiveRole` 跨类不可调~~ → **已核实**:是 `RelationshipQualityService` 本类 private 方法(line 211),可直接调用
+8. ~~问卷服务需新增 FamilyMemberMapper~~ → **已核实**:`familyMemberMapper` 已注入(line 38),只需补 `familyRelationshipMapper`(任务 2 步骤 1)
+9. ~~`isSelf` 已有值~~ → **已核实反例**:`FamilyMemberService.listMembers` 全文件无 `setIsSelf`,若不加则首页模式"添加成员/邀请成员"入口永不显示、`canRemove` 对自己也显示 → 任务 3 步骤 2 补齐 `vo.setIsSelf(viewerMember.getId().equals(fm.getId()))`(复用已有 viewerMember)
+10. ~~邀请成员跳页~~ → **已核实改为弹窗**:与 family-members.vue `onInvite`(line 258)+ SharePoster 组件(props: show/referralCode/qrCodeBase64/nickname/userAvatar)一致,任务 5 步骤 2 已实现
+11. ~~Vue2 单根节点挂 v-else~~ → **已核实**:任务 5 步骤 1 改为根节点常渲染 + 内部 v-if/v-else 二选一,避免直接对根节点挂 v-else
+12. ~~generation_value 计算未知~~ → **已核实**:FamilyRelationshipService.createRelationsForNewMember(line 38-80)约定 `from→to: fromGen - toGen`,syncPairwiseScores 使用相同公式(respGen - targetGen)✅
+13. ~~上次填写时间无数据源~~ → **已核实**:`updateMemberScores`(line 210)在问卷提交时更新 `member.lastInteractionAt`,FamilyMemberVO 已有该字段;任务 5 步骤 3 用 `parseDate` 格式化为"X天前"展示
+14. 卡片三色点布局挤压 → 实现时视觉调优(任务 4 步骤 3 备注)
+
+**占位符扫描:** 无 TBD/TODO;所有代码块为完整实现代码,且关键 API 已对照真实源码核实(entity 字段、mapper、service 方法、controller 模式、Lombok setter)。
+
+**类型一致性:** `FamilyRelationship` 新增字段名(trustScore/intimacyScore/communicationScore)在任务 1/2/3 中一致;`FamilyMemberVO` 补齐 communicationScore 后,任务 3 的 setter 调用与任务 4/5 前端字段读取(member.communicationScore)对齐;`renderedMembers` 在任务 6 中所有模板引用统一替换。