2026-06-19-family-relation-graph-design.md 11 KB

家庭成员关系图谱组件设计

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 在微信小程序使用 <canvas canvas-id="..." type="2d">(基础库 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. 跨页面集成方式

<!-- pages/body/index.vue — 身体页 -->
<FamilyRelationGraph
  dimensionCode="body"
  :members="familyMembers"
  :energyMap="energyMap"
  :intimacyMap="intimacyMap"
  @memberTap="goMemberDetail" />

<!-- pages/mind/index.vue — 心智页 -->
<FamilyRelationGraph
  dimensionCode="mind"
  :members="familyMembers"
  :energyMap="energyMap"
  :intimacyMap="intimacyMap"
  @memberTap="goMemberDetail" />

<!-- pages/action/index.vue — 行远页 -->
<FamilyRelationGraph
  dimensionCode="action"
  :members="familyMembers"
  :energyMap="energyMap"
  :intimacyMap="intimacyMap"
  @memberTap="goMemberDetail" />

父页面数据准备

// 页面 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 兼容性确认。