# 家庭成员关系图谱组件设计 > **Status:** Draft > **Date:** 2026-06-19 > **Scope:** 新增 Canvas 关系图谱组件,集成到身/心/行三个维度页面,以可视化方式展示家庭成员关系与能量值 --- ## 1. 五行匹配 | 维度 | 五行 | 属性 | 对应关系图谱 | |:----:|:----:|------|-------------| | 身(Body) | 土 | 承载·包容 | 家庭成员是身体的根基支撑 | | 心(Mind) | 火 | 明丽·温暖 | 情感连接的温度可视化 | | 行(Action)| 木 | 生发·仁爱 | 关系根系图谱,家族连接网络 | 关系图谱的核心是呈现家庭成员之间的远近亲疏,天然对应行的「根系网络」属性,同时兼顾各维度的能量可视化。 --- ## 2. 组件定位 ### 2.1 解决什么问题 现有 `FamilyMemberStrip` 和 `FamilyMemberCard` 以横条/列表形式展示成员,无法表达: - 成员之间的**亲疏距离**(太近 vs 太远) - **沟通频率**(多 vs 少) - **信任程度**(高 vs 低) - 各维度**能量值**在成员身上的分布 ### 2.2 与现有功能的关系 | 已有功能 | 关系图谱 | 关系 | |:---------|:--------|:-----| | `FamilyMemberStrip` | 家庭成员列表 | 图谱的数据来源之一,图谱替代 strip | | `ContactCard`/联系人 | 非家庭外部联系人 | 不冲突,Contact 管理外部关系 | | `FamilyEnergySandbox` | 家庭能量沙盘 | 图谱使用的能量数据来源 | | `wuxing-sandbox` | 五行沙盘(全局) | 图谱是成员层面的能量展示 | --- ## 3. 技术选型 **Canvas 自绘制**,原因: - 需要自由绘制圆、弧、线、渐变、填充百分比 - 成员数量 2~8 人,Canvas 性能无压力 - 命中检测通过坐标反算实现 - 避免 CSS DOM 爆炸(每个连线 + 形状都需 DOM) ### 3.1 兼容性 uni-app Canvas 在微信小程序使用 ``(基础库 2.9.0+),需确保 `uni.canvasToTempFilePath` / `uni.createCanvasContext` 可用。 --- ## 4. 组件接口 ``` 文件: cfc-frontend/components/FamilyRelationGraph.vue ``` ### Props | 参数 | 类型 | 必需 | 默认 | 说明 | |:-----|:----|:----:|:----|:-----| | `dimensionCode` | String | ✓ | — | 当前维度 `body` / `mind` / `action` | | `selfId` | Number/String | ✗ | store中的currentChildId | 自我成员ID | | `members` | Array | ✓ | — | `FamilyMemberVO[]` 家庭可见成员 | | `energyMap` | Object | ✓ | — | `{ memberId: { bodyScore, mindScore, actionScore } }` | | `intimacyMap` | Object | ✓ | — | `{ memberId: { closeness, communication, trust } }` | ### Events | 事件 | 参数 | 说明 | |:-----|:-----|:-----| | `@memberTap` | `{ memberId, memberType, nickname }` | 成员被点击,由父页面处理跳转 | ### Size | 环境 | 尺寸 | |:-----|:----| | 画布宽高 | `320 × 320 px`(通过 `uni.upx2px(690)` 动态适配) | | 中心坐标 | `(width/2, height/2)` | | 内外半径 | `R_inner = 80px`, `R_outer = 220px`(可配置) | | self 形状 | `72 × 72px` | | 成员形状 | `56 × 56px` | --- ## 5. 布局算法:极坐标 + 亲密度调半径 ``` N = members.length for i in 0..N-1: angle = 2π * i / N radius = R_inner + (100 - closeness_i) / 100 * (R_outer - R_inner) x = centerX + radius * cos(angle - π/2) y = centerY + radius * sin(angle - π/2) ``` | 亲密度 | 半径 | 视觉效果 | |:------:|:----:|:---------| | 100 | 80px | 紧贴self | | 75 | 115px | 较近 | | 50 | 150px | 中等 | | 25 | 185px | 较远 | | 0 | 220px | 边缘 | 角度减 `π/2` 使第一个成员在正上方(12点钟方向)。 --- ## 6. 连线编码 从 self 中心绘制到每个成员中心。 ### 6.1 颜色 = 信任度(HSL 色相渐变) ``` trust ∈ [0, 100] hue = 120 * trust / 100 // 0°(红) → 60°(黄) → 120°(绿) strokeStyle = hsl(hue, 80%, 45%) ``` | trust范围 | 色相 | 显示颜色 | 含义 | |:---------:|:----:|:--------:|:----| | 80-100 | 96-120° | 翠绿 | 充分信任 | | 60-79 | 72-96° | 黄绿 | 基本信任 | | 40-59 | 48-72° | 橙黄 | 一般 | | 20-39 | 24-48° | 橙红 | 缺乏信任 | | 0-19 | 0-24° | 殷红 | 不信任 | ### 6.2 线宽 = 沟通程度(2~10px) ``` communication ∈ [0, 100] width = 2 + communication / 100 * 8 // 2px ~ 10px ``` | 沟通程度 | 线宽 | 视觉效果 | |:--------:|:----:|:---------| | 0~20 | 2~3.6px | 细线 | | 40~60 | 5.2~6.8px | 中等 | | 80~100 | 8.4~10px | 粗线 | ### 6.3 长度 = 亲密度(由 §5 半径表达) 布局算法已实现,无需额外绘制。 ### 6.4 辅助视觉 - 连线默认**实线** - self 端可选小圆点装饰(节点连接点) - 连线末端在成员形状外边缘停止(避免穿过形状) --- ## 7. 成员形状编码 ### 7.1 形状 → 角色类型 | 角色 (`memberType`) | Canvas 形状 | 尺寸 | 说明 | |:-------------------:|:-----------|:----:|:-----| | `self` | 双层圆环(外描边+内实心) | 72×72 | 明显大于其他,外圈发光或高亮描边 | | `parent` | 圆角正方形(`roundRect`) | 56×56 | 圆角半径 10px,稳重 | | `child` | 圆形(`arc`) | 56×56 | 圆润柔和 | | `partner` | 心形(双弧线+三角形拼接) | 56×56 | 特殊形状突出亲密关系 | ### 7.2 形状颜色 | 部分 | 颜色 | 说明 | |:-----|:-----|:-----| | 描边 | 维度主题色 | body=#8D6E63, mind=#FF6B35, action=#4CAF50 | | 底填充 | `rgba(维度色, 0.15)` | 极淡底色,表示形状边界 | | 文字/头像 | 白色 + 成员nickname首字 | — | ### 7.3 能量填充 — 环形进度条(方案A) ``` score = energyMap[memberId][dimensionCode + 'Score'] // bodyScore/mindScore/actionScore fillRatio = Math.min(score / 100, 1) // 绘制环形进度条 // 以形状中心为圆心,在形状外围绘制 arc 扇形 ctx.beginPath() // 从 -π/2(顶部)开始,顺时针扫过 fillRatio * 2π 角度 ctx.arc(cx, cy, outerR, -Math.PI/2, -Math.PI/2 + fillRatio * 2 * Math.PI) ctx.lineWidth = 4 ctx.strokeStyle = dimensionColor // 维度主题色(半透明处理) ctx.stroke() // 剩余部分(灰色圆圈) ctx.beginPath() ctx.arc(cx, cy, outerR, -Math.PI/2 + fillRatio * 2 * Math.PI, -Math.PI/2 + 2 * Math.PI) ctx.strokeStyle = 'rgba(200, 200, 200, 0.3)' ctx.stroke() ``` | 能量值 | 环形进度 | 视觉效果 | |:------:|:--------:|:---------| | 100 | 满圈(360°) | 完整闭环 | | 75 | 270° | 显示3/4 | | 50 | 180° | 半圈 | | 25 | 90° | 1/4圈 | | 0 | 无 | 只有灰色底圈 | ### 7.4 self 特殊处理 self 的能量填充绑定到**整体能量概览**(非member维度单项),或者不显示能量环,保持中心突出。 --- ## 8. 交互与导航 ### 8.1 Canvas 命中检测 ``` onCanvasTap(touchEvent) { const rect = canvas.getBoundingClientRect() const tapX = touchEvent.touches[0].clientX - rect.left const tapY = touchEvent.touches[0].clientY - rect.top for member in positionedMembers: const dx = tapX - member.canvasX const dy = tapY - member.canvasY if (dx*dx + dy*dy < member.hitRadius^2): this.$emit('memberTap', { memberId: member.id, ... }) return } ``` ### 8.2 跳转路由 由父页面处理 `@memberTap` 事件: | 当前维度 | 跳转目标 | |:--------:|:---------| | `body` | `/pages/body/member-body-detail?childId={id}` | | `mind` | `/pages/mind/member-mind-detail?childId={id}` | | `action` | `/pages/action/member-action-detail?childId={id}` | ### 8.3 触摸反馈 - 点击命中时:形状缩放动画(`0.95 → 1.0`)或描边加亮 - 实现方式:重绘 canvas(requestAnimationFrame 过渡) --- ## 9. 跨页面集成方式 ```html ``` ### 父页面数据准备 ```javascript // 页面 onLoad loadFamilyData() { // 1. 获取家庭成员 getVisibleFamilyMembers().then(res => { this.familyMembers = res.data }) // 2. 获取能量沙盘 getFamilyEnergySandbox().then(res => { const sandbox = res.data this.energyMap = {} sandbox.members.forEach(m => { this.energyMap[m.memberId] = { bodyScore: m.bodyScore, mindScore: m.mindScore, actionScore: m.actionScore } }) }) // 3. 获取亲密度数据(从 Contact 服务或新增 API) getFamilyIntimacy().then(res => { this.intimacyMap = {} res.data.forEach(c => { this.intimacyMap[c.contactId] = { closeness: c.intimacyLevel, communication: c.contactCount, trust: c.intimacyLevel // 暂时复用亲密度 } }) }) } ``` --- ## 10. 边界状态 | 状态 | 处理 | |:-----|:-----| | **空成员 (members=[])** | 居中显示提示文字「暂无家庭成员」,不绘制图谱 | | **单人 (1个成员+self)** | 单点连接,角度随便 | | **成员过多 (>8)** | canvas 内压缩半径范围,或显示滚动提示 | | **能量数据缺失** | 环形进度条不显示(灰色底圈),形状正常展示 | | **亲密数据缺失** | 默认半径中等(R_mid=150px),连线灰色默认粗细 | | **self不在members中** | 从store或props.selfId获取self信息,强制作为中心 | --- ## 11. 与现有组件的替换关系 | 页面 | 现有组件 | 替换为 | |:-----|:---------|:-------| | body/index.vue | `FamilyMemberStrip`(成员快速切换) | 保留 strip 作为成员切换器,图谱作为独立区块在其下方 | | mind/index.vue | — | 图谱新增 | | action/index.vue | `ContactCard` 横向列表(联系人) | 图谱在上方(家庭关系),ContactCard 在下方(外部关系),两者共存 | --- ## 12. 依赖接口 | API | 方法 | 响应 | 用途 | |:----|:----|:-----|:-----| | `getVisibleFamilyMembers` | post | `FamilyMemberVO[]` | 获取家庭可见成员列表 | | `getFamilyEnergySandbox` | post | `EnergySandboxDTO` | 获取各成员五维能量值 | | `getFamilyIntimacy`(新增/复用) | post | `[{contactId, intimacyLevel, contactCount}]` | 获取与各成员的关系亲密度 | --- ## 13. 开放问题 1. **亲密度数据源**:目前 Contact 实体有 intimacyLevel(0-100)和 contactCount,可用于 line 长度和粗细。但 Contact 管理的是「外部联系人」,家庭成员的亲密度是否需新增 `FamilyIntimacy` 表或字段? 2. **self 的身份**:当前页面的用户是家长视角(查看孩子数据)还是孩子视角?self 应显示为「当前孩子本人」还是「家长自己」? 3. **触摸反馈动画**:Canvas 重绘实现缩放动画,需要 requestAnimationFrame 兼容性确认。