|
|
@@ -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 中所有模板引用统一替换。
|