面向 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 补齐 communicationScoreservice/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 clean compile 验证npm run build:mp-weixin 验证文件:
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyRelationship.javacfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java修改:cfc-backend/src/main/resources/schema.sql
[ ] 步骤 1:实体加字段
在 FamilyRelationship.java 的 relationshipKey 字段后追加:
/** 信任度(答卷人视角) 0-100 */
private BigDecimal trustScore;
/** 亲密度(答卷人视角) 0-100 */
private BigDecimal intimacyScore;
/** 沟通质量(答卷人视角) 0-100 */
private BigDecimal communicationScore;
并补充 import(若未引入 java.math.BigDecimal,加在 import java.io.Serializable; 之后)。
找到 // 迁移153: 注释块,在其后追加(用 ensureColumn 三次调用):
// 迁移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 '沟通质量(答卷人视角)'");
schema.sql 的 family_relationships CREATE TABLE(约 1818 行)在 relationship_key 行后加三列:
trust_score DECIMAL(5,2) DEFAULT 0 COMMENT '信任度(答卷人视角)',
intimacy_score DECIMAL(5,2) DEFAULT 0 COMMENT '亲密度(答卷人视角)',
communication_score DECIMAL(5,2) DEFAULT 0 COMMENT '沟通质量(答卷人视角)',
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 5:Commit
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表增加关系质量评分三列"
文件:
修改: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 注入:
@Resource
private com.etotem.cfc.mapper.FamilyRelationshipMapper familyRelationshipMapper;
并在 import 区(import com.etotem.cfc.entity.RelationshipQuestionnaireSnapshot; 之后)补:
import com.etotem.cfc.entity.FamilyRelationship;
import com.etotem.cfc.mapper.FamilyRelationshipMapper;
在 submitQuestionnaire 中 updateMemberScores(snapshot.getFamilyMemberId(), scores); 之后追加:
// 同步写入 pairwise 关系评分(答卷人视角 → 被评成员)
syncPairwiseScores(snapshot.getRespondentId(), snapshot.getFamilyMemberId(), scores);
在 updateMemberScores 方法后新增:
/**
* 将问卷评分写入 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; 之后)。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 5:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/RelationshipQuestionnaireService.java
git commit -m "feat(backend): 问卷提交时同步pairwise关系评分到family_relationships"
文件:
cfc-backend/src/main/java/com/etotem/cfc/dto/FamilyMemberVO.javacfc-backend/src/main/java/com/etotem/cfc/service/FamilyMemberService.javacfc-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)之间补:
/** 沟通质量分数 */
private java.math.BigDecimal communicationScore;
FamilyMemberService.java 现有 vo.setTrustScore(fm.getTrustScore());(line 226)与 vo.setIntimacyScore(fm.getIntimacyScore());(line 227),在其后补 communicationScore;同时该方法从未设置 isSelf(已核实,全文件无 setIsSelf),需一并补齐——否则首页 8 处现有 FamilyMemberStrip 调用中"添加成员/邀请成员"入口永不显示,且现有 canRemove 的 !actionMember.isSelf 判断恒为 true(对自己也显示移除按钮):
vo.setCommunicationScore(fm.getCommunicationScore());
vo.setIsSelf(viewerMember != null && viewerMember.getId().equals(fm.getId()));
说明:
viewerMember在该方法 line 175-181 已计算(当前查看者在 family_members 中的记录),直接复用。isSelf语义 = 该成员就是当前登录用户在家庭中的记录。
该类已 import LambdaQueryWrapper / FamilyMember / FamilyMemberMapper / BigDecimal / ArrayList / Date,且已有 private computeEffectiveRole(FamilyMember)(line 211)可复用。新增注入:
@Resource
private com.etotem.cfc.service.FamilyRelationshipService familyRelationshipService;
在 getScores 方法后新增:
/**
* 获取以指定成员为中心的所有成员关系(关系类型 + 三项评分)
*/
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 方法,可直接调用。
RelationshipQualityController 已有 getFamilyIdFromRequest(userId, body)(line 30)与 @PostMapping("/scores") 模式。在 getScores 方法后新增:
@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。
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
[ ] 步骤 6:Commit
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补齐沟通评分"
文件:
修改:cfc-frontend/components/FamilyMemberCard.vue
[ ] 步骤 1:模板加三色点(关系徽章下方)
card-relation-badge 的 </view> 之后、card-spouse-mark 之前插入:
<!-- 关系现状三色点:信任/亲密度/沟通 -->
<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'))嵌套调用在微信小程序模板编译器可能报错)。改为计算属性返回三个色值数组:
<view class="card-relation-dots">
<view class="relation-dot" v-for="(color, idx) in relationDotColors" :key="idx"
:style="{ background: color }"></view>
</view>
在 computed 中 relationColor 后新增:
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'
})
}
<style scoped> 中 .card-relation-badge 后新增:
.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)。以视觉不重叠为准。
运行:cd cfc-frontend && npm run build:mp-weixin
预期:BUILD SUCCESS(无模板编译错误)
[ ] 步骤 5:Commit
git add cfc-frontend/components/FamilyMemberCard.vue
git commit -m "feat(frontend): 家庭成员卡片增加关系现状三色点"
文件:
修改:cfc-frontend/components/FamilyMemberStrip.vue
[ ] 步骤 1:模板空状态分支(单根节点重构)
Vue 2 组件模板要求单根节点,当前根 <view class="family-member-strip" v-if="..."> 无法直接挂 v-else。将根节点改为常渲染,内部用 v-if/v-else 二选一:
<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,避免空数组渲染异常。
import 行追加(现有:import { kickFamilyMemberById } from '@/utils/api'):
import { kickFamilyMemberById, createFamily, generateFamilyInviteQrCode } from '@/utils/api'
import SharePoster from '@/components/SharePoster.vue'
import { parseDate } from '@/utils/format.js'
components 注册追加:SharePoster。
data 增加字段:
creatingFamily: false,
showPoster: false,
inviteCode: '',
inviteQrCode: '',
posterNickname: '',
posterAvatar: ''
methods 新增:
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 才显示)加在"切换视角"之前:
<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> 闭合前),保证空态/有成员态下都能渲染:
</view>
<SharePoster
:show="showPoster"
:referralCode="inviteCode"
:qrCodeBase64="inviteQrCode"
:nickname="posterNickname"
:userAvatar="posterAvatar"
@close="showPoster = false" />
</view>
</template>
将 action-sheet 内头部替换为头像 + 关系标签,关系现状区块插在 header 之后、操作项之前。已核实微信模板禁止 :style 内带参方法调用(scoreWidth('trustScore') 不可用),宽度/数值统一走计算属性 scoreWidths[] / scoreValues[](下标 0=信任度 1=亲密度 2=沟通质量):
<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 新增:
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.js'(小程序规范:禁止new Date(string)解析日期,统一用parseDate()归一化格式,见 cfc-frontend/AGENTS.md)。
模板中"关系问卷"操作项(保留现有,追加时间后缀):
<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 同级:
.action-time {
font-size: 22rpx;
color: #9CA3AF;
margin-left: 4rpx;
}
未测评展示: 三项分数全为 0/空时,进度条宽度为
0%、数值为0。如需"未测评"文案,可在scoreValues分支内判断(v <= 0时返回--),实现时按视觉效果取舍,不影响数据结构。
<style scoped> 追加:
/* 空状态 */
.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;
}
运行:cd cfc-frontend && npm run build:mp-weixin
预期:BUILD SUCCESS
[ ] 步骤 6:Commit
git add cfc-frontend/components/FamilyMemberStrip.vue
git commit -m "feat(frontend): 成员条空状态引导 + 长按菜单升级关系现状Action Sheet"
文件:
cfc-frontend/utils/api.js修改:cfc-frontend/components/FamilyMemberStrip.vue
[ ] 步骤 1:api.js 新增 pairwise 接口
在 getVisibleFamilyMembers 定义后新增:
// 获取以指定成员为中心的所有成员关系(关系类型 + 信任/亲密度/沟通评分)
export const getPairwiseRelationships = (centerMemberId) => {
return request('/api/family/relationship/pairwise', 'POST', { centerMemberId })
}
props 增加:
centerMemberId: { type: Number, default: null }
已核实: 组件
members是 prop 且为只读,不能直接赋值。引入 datarenderedMembers作为渲染数据源,membersprop 变更时同步;centerMemberId传入时组件内部拉取 pairwise 数据覆盖渲染。
data 增加:
renderedMembers: []
watch 新增:
watch: {
members: {
handler: function(val) {
this.renderedMembers = val || []
},
immediate: true
},
centerMemberId: function(val) {
if (val) {
this.loadPairwise(val)
}
}
}
methods 新增:
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。
运行:cd cfc-frontend && npm run build:mp-weixin
预期:BUILD SUCCESS
[ ] 步骤 5:Commit
git add cfc-frontend/utils/api.js cfc-frontend/components/FamilyMemberStrip.vue
git commit -m "feat(frontend): pairwise接口 + 成员条centerMemberId视角模式"
文件:
修改:cfc-frontend/pages/home-pages/member-home-detail.vue
[ ] 步骤 1:引入组件
import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue',并加入 components 注册。
在 member-card 之后、FamilyRelationGraph 之前插入:
<!-- 以该成员为中心的家庭成员关系条 -->
<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 校验告警。
onLoad 修改:
onLoad: function(options) {
this.memberId = Number(options.memberId) || null
this.memberType = options.memberType || 'child'
},
运行:cd cfc-frontend && npm run build:mp-weixin
预期:BUILD SUCCESS
[ ] 步骤 5:Commit
git add cfc-frontend/pages/home-pages/member-home-detail.vue
git commit -m "feat(frontend): 成员详情页接入以成员为中心的关系条"
文件: 无
运行:cd cfc-backend && mvn clean compile
预期:BUILD SUCCESS
运行:cd cfc-frontend && npm run build:mp-weixin
预期:BUILD SUCCESS
运行: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 出现一次
运行:git status --short
预期:无未提交变更(或仅剩余待提交的临时文件)
规格覆盖度:
实现期风险(已核实并在任务内处理):
members 是 prop 不可赋值 → 引入 renderedMembers data(任务 6)calculateAgeStatic 不存在FamilyMember 实体自带 age 字段(line 67),直接 vo.setAge(fm.getAge())(任务 3 步骤 3)FamilyMemberVO.communicationScore 缺失listMembers 补齐(否则首页 8 处现有调用第三点恒灰)FamilyRelationshipService.getRelationsForMember 是假设computeRelationshipLabel(Integer, String) 存在(line 118)computeEffectiveRole 跨类不可调RelationshipQualityService 本类 private 方法(line 211),可直接调用familyMemberMapper 已注入(line 38),只需补 familyRelationshipMapper(任务 2 步骤 1)isSelf 已有值FamilyMemberService.listMembers 全文件无 setIsSelf,若不加则首页模式"添加成员/邀请成员"入口永不显示、canRemove 对自己也显示 → 任务 3 步骤 2 补齐 vo.setIsSelf(viewerMember.getId().equals(fm.getId()))(复用已有 viewerMember)onInvite(line 258)+ SharePoster 组件(props: show/referralCode/qrCodeBase64/nickname/userAvatar)一致,任务 5 步骤 2 已实现from→to: fromGen - toGen,syncPairwiseScores 使用相同公式(respGen - targetGen)✅updateMemberScores(line 210)在问卷提交时更新 member.lastInteractionAt,FamilyMemberVO 已有该字段;任务 5 步骤 3 用 parseDate 格式化为"X天前"展示占位符扫描: 无 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 中所有模板引用统一替换。