# 家庭成员关系卡片 — 修复方案 ## 总体目标 让 `FamilyMemberCard` 从"简单的成员选择器"升级为"包含关系标签、交互操作、丰富信息的家庭关系卡片"。 ## 方案概览 | 层 | 改动量 | 核心变更 | |----|--------|---------| | 后端 DTO | 小 | FamilyMemberDTO 增加 relationship 字段 | | 后端 Service | 中 | FamilyMembersController.list() 增加关系计算逻辑 | | 前端组件 | 中 | FamilyMemberCard 增加关系标签/操作按钮/交互 | | 前端容器 | 小 | FamilyMemberStrip 增加长按菜单支持 | --- ## 第一步:后端 — FamilyMemberDTO 增加关系字段 ### 修改 FamilyMemberDTO ```java // DTO 中增加以下字段(从 family_relationships 表计算) private String relationship; // 与当前用户的关系: 爸爸/妈妈/儿子/女儿/配偶 private Integer generation; // 辈分: 1/2/3 private Boolean isSpouse; // 是否配偶 private String relativeLabel; // 关系标签显示名(如"爸爸 · 二代") ``` ### 修改 FamilyMembersController.list() ```java @PostMapping("/list") public Result> list(@RequestAttribute("userId") Long userId) { User user = userMapper.selectById(userId); if (user == null || user.getFamilyId() == null) { return Result.error("用户未加入家庭"); } List members = familyMemberMapper.selectList( new LambdaQueryWrapper() .eq(FamilyMember::getFamilyId, user.getFamilyId()) ); // 为每个成员计算关系标签 List result = members.stream().map(m -> { FamilyMemberVO vo = FamilyMemberVO.from(m); // 计算关系 String relation = calculateRelation(userId, m.getUserId(), m.getGeneration(), m.getIsSpouse()); vo.setRelationship(relation); vo.setRelativeLabel(buildRelativeLabel(relation, m.getGeneration())); return vo; }).collect(Collectors.toList()); return Result.success(result); } ``` ### 关系计算逻辑 ```java /** * 计算当前用户与目标成员的关系 * 依赖 family_relationships 表中的双向关系数据 */ private String calculateRelation(Long currentUserId, Long memberUserId, Integer generation, Boolean isSpouse) { if (isSpouse != null && isSpouse) return "配偶"; // 查询 family_relationships 表获取关系 FamilyRelationship rel = relationshipMapper.selectOne( new LambdaQueryWrapper() .eq(FamilyRelationship::getMemberId, currentUserId) .eq(FamilyRelationship::getRelatedMemberId, memberUserId) ); if (rel != null && rel.getRelationshipType() != null) { return rel.getRelationshipType(); // 如 "爸爸"/"妈妈"/"儿子"/"女儿" } // Fallback: 根据 generation 推断 if (generation != null) { if (generation == 1) return "长辈"; if (generation == 2) return "同辈"; if (generation == 3) return "晚辈"; } return "家庭成员"; } /** * 构建关系标签显示文本 */ private String buildRelativeLabel(String relation, Integer generation) { String gen = generation != null ? " · 第" + generation + "代" : ""; return relation + gen; } ``` ### 新建 FamilyMemberVO ```java @Data public class FamilyMemberVO { private Long id; private Long userId; private String nickname; private String avatar; private String phone; private String gender; private Integer generation; private Boolean isSpouse; private String relationship; // 关系: 爸爸/妈妈/儿子/女儿/配偶 private String relativeLabel; // 显示标签: "爸爸 · 二代" private Integer totalPoints; private BigDecimal trustScore; private BigDecimal intimacyScore; private Date lastInteractionAt; private Integer streakDays; public static FamilyMemberVO from(FamilyMember m) { FamilyMemberVO vo = new FamilyMemberVO(); BeanUtils.copyProperties(m, vo); return vo; } } ``` --- ## 第二步:后端 — 关系数据查询 ### 查询 family_relationships 表 ```sql -- family_relationships 表结构(已存在) CREATE TABLE family_relationships ( id BIGINT AUTO_INCREMENT PRIMARY KEY, family_id BIGINT NOT NULL, member_id BIGINT NOT NULL COMMENT '关系主体', related_member_id BIGINT NOT NULL COMMENT '关系客体', relationship_type VARCHAR(20) NOT NULL COMMENT '关系类型: 爸爸/妈妈/儿子/女儿/丈夫/妻子', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); ``` ### 注入 RelationshipMapper ```java @Resource private FamilyRelationshipMapper familyRelationshipMapper; ``` ### 迁移109:family_members 表补充关系索引 ```java // 迁移109: family_members 表添加关系查询索引 try { jdbcTemplate.execute("ALTER TABLE family_members ADD INDEX idx_family_user (family_id, user_id)"); log.info("已添加family_members关系查询索引"); } catch (Exception e) { // 索引已存在,忽略 } ``` --- ## 第三步:前端 — FamilyMemberCard 增强 ### 改造后的模板 ```vue ``` ### 改造后的脚本 ```javascript export default { props: { member: { type: Object, required: true }, isSelected: { type: Boolean, default: false }, energyScore: { type: Number, default: null }, showActions: { type: Boolean, default: false } // 是否显示操作按钮 }, computed: { defaultAvatar: function() { return '/static/default-avatar.png' }, relationColor: function() { var rel = this.member && this.member.relationship if (rel === '爸爸' || rel === '妈妈') return '#F97316' // 父母-橙色 if (rel === '儿子' || rel === '女儿') return '#0EA5E9' // 子女-蓝色 if (rel === '配偶') return '#EC4899' // 配偶-粉色 if (rel === '爷爷' || rel === '奶奶') return '#6B7280' // 祖辈-灰色 var role = this.member && this.member.effectiveRole if (role === 'parent') return '#F97316' if (role === 'child') return '#0EA5E9' if (role === 'teacher') return '#8B5CF6' return '#9CA3AF' } }, methods: { onClick: function() { this.$emit('select', this.member) }, onLongPress: function() { this.$emit('longpress', this.member) }, onCall: function() { if (this.member.phone) { uni.makePhoneCall({ phoneNumber: this.member.phone }) } } } } ``` ### 改造后的样式(关键部分) ```css .card-relation-badge { position: absolute; bottom: -4rpx; left: 50%; transform: translateX(-50%); padding: 2rpx 10rpx; border-radius: 12rpx; white-space: nowrap; max-width: 100rpx; } .card-relation-text { font-size: 16rpx; color: #FFFFFF; font-weight: 500; } .card-spouse-mark { position: absolute; top: -4rpx; right: -4rpx; width: 28rpx; height: 28rpx; } .card-relation-line { display: flex; align-items: center; justify-content: center; margin-top: 2rpx; } .card-relation-label { font-size: 18rpx; color: #9CA3AF; } .card-contact-row { display: flex; align-items: center; justify-content: center; margin-top: 4rpx; } .contact-icon { font-size: 20rpx; } .contact-phone { font-size: 18rpx; color: #3B82F6; margin-left: 4rpx; } ``` --- ## 第四步:前端 — FamilyMemberStrip 增加长按菜单 ```vue ``` ```javascript export default { data() { return { longPressedMember: null, isParent: false } }, methods: { onLongPress: function(member) { this.longPressedMember = member this.$refs.actionMenu.open() }, switchToMember: function() { this.closeMenu() if (this.longPressedMember) { uni.navigateTo({ url: '/pages/home-pages/child-index?memberId=' + this.longPressedMember.id }) } }, editMember: function() { this.closeMenu() if (this.longPressedMember) { uni.navigateTo({ url: '/pages/profile-extra/family-members?editId=' + this.longPressedMember.id }) } }, removeMember: function() { this.closeMenu() // 调用 /api/family/member/kick }, closeMenu: function() { this.$refs.actionMenu.close() } } } ``` --- ## 第五步:适配现有页面 现有9个使用 `FamilyMemberStrip` 的页面需要适配: | 页面 | 改动 | 说明 | |------|------|------| | parent-index | 不需要改 | 直接使用增强后的组件 | | child-index | 不需要改 | 直接使用增强后的组件 | | mind/body/wisdom/wealth/action/index | 不需要改 | 直接使用增强后的组件 | | index-home | 不需要改 | 直接使用增强后的组件 | 组件增强后,现有页面无需修改即可获得关系标签、联系按钮等新功能。 --- ## 实施路线图 | 步骤 | 内容 | 涉及文件 | 估算工时 | |------|------|---------|---------| | 1 | 创建 FamilyMemberVO + 修改 FamilyMembersController.list() | 后端 2 文件 | 1h | | 2 | 实现关系计算逻辑 | 后端 1 文件 | 1h | | 3 | 迁移109:添加索引 | 后端 1 文件 | 0.5h | | 4 | 增强 FamilyMemberCard 组件 | 前端 1 文件 | 1.5h | | 5 | 增强 FamilyMemberStrip 组件(长按菜单) | 前端 1 文件 | 1h | | 6 | 测试验证 | 前后端 | 1h | | | **合计** | **7 文件** | **~6h** | ## 风险与注意事项 1. **兼容性**:增强后的 FamilyMemberCard 新增 props 都有默认值,现有页面无需修改即可使用 2. **关系数据**:如果 `family_relationships` 表中没有数据,Fallback 逻辑使用 `generation` 推断 3. **小程序限制**:`uni.makePhoneCall` 需要用户授权,`uni-popup` 需要 uni-ui 组件库 4. **权限控制**:移除成员操作需要校验当前用户角色(仅家长可操作)