2026-06-22-family-relation-graph-redesign.md 6.8 KB

FamilyRelationGraph 重构设计

日期: 2026-06-22
状态: 待审核
范围: cfc-frontend/components/FamilyRelationGraph.vue 完整重构


1. 背景

现有 FamilyRelationGraph 使用 Canvas 2D 极坐标布局绘制家庭成员关系图。存在以下问题:

  • 极坐标布局按角度均匀分布成员,不区分人数,2人和10人用同一套逻辑
  • 布局规则不直观:用户期望2人横排、3人自己居中、多人紧凑排列
  • 节点大小固定(self vs other 两档),无法直观表达能量差异
  • Canvas 点击命中检测复杂,需要手动计算距离

2. 目标

用 View + Canvas 混合架构替换纯 Canvas 实现,实现:

  • 条件渲染:1人时不显示组件
  • 自适应布局:2人/3人横排,4+人多行紧凑排列
  • 能量可视化:节点大小映射能量值
  • 保留连线:Canvas 画 self→其他成员的关系线
  • 原生点击:View 元素原生 tap 事件,零命中检测

3. 已确认决策

决策项 选择 理由
与 FamilyRelationGraph 关系 完全替换(同名同接口) 避免上层调用方改动
渲染架构 View(布局+交互)+ Canvas(只画线) 方案A,flex 布局天然适配多人数规则
每个成员展示 头像 + 昵称 能量值用节点大小体现
能量可视化方式 节点大小(不用环形进度条) 用户明确要求
连线 保留,Canvas 绘制 信任度→颜色,沟通度→粗细

4. 组件接口

Props(保持兼容)

Prop 类型 默认值 说明
dimensionCode String 必填 维度编码:body/mind/action/home,决定主题色
selfId Number|String null 当前用户 ID
members Array [] 成员列表 [{id, nickname, memberType}]
energyMap Object {} 能量数据 {memberId: {bodyScore, mindScore, actionScore}}
intimacyMap Object {} 亲密度 {memberId: {closeness, communication, trust}}
interactive Boolean true 是否可点击

Events

事件 参数 触发条件
memberTap {memberId, memberType, nickname} interactive=true 时点击非 self 成员

5. 布局规则

5.1 条件渲染

  • members.length === 0:渲染空状态(文字"暂无家庭成员")
  • members.length === 1不渲染(组件返回空 v-if,非 v-show 隐藏)
  • members.length >= 2:按规则渲染

5.2 人数对应布局

2 人

容器: flex, flex-direction: row, justify-content: space-around, align-items: center
[我]        [对方]

self 在左,对方在右。单行横排。

3 人

容器: flex, flex-direction: row, justify-content: space-between, align-items: center
数据重排: [其他, self, 其他]
[对方]     [我]     [对方]

self 在中间,其余两人分列左右。

4+ 人

两行结构:
  第一行: flex, justify-content: center — 只有 [我]
  第二行+: flex, flex-wrap: wrap, justify-content: center — 其他成员

          [我]
  [爸] [妈] [大宝] [二宝]

核心原则:用宽度,省高度。其他成员尽量排在一行,排不下才换行。

5.3 节点排列算法

  1. members 中分离出 self 和 others
  2. layoutMode 排列:
    • layoutMode = 'row' (2-3人): self 和 others 在同一行,按 [other1, self, other2, ...] 排列
    • layoutMode = 'multi-row' (4+人): self 占第一行居中,others 在第二行 flex-wrap 排列

6. 节点样式

6.1 头像尺寸

能量值映射到头像直径:

//能量值来自当前维度的 score(如 dimensionCode='body' 则取 bodyScore)
//energy = energyMap[member.id][dimensionCode + 'Score'] || 0
//范围 0-100

size = 64 + (Math.max(0, Math.min(100, energy)) / 100) * 56  // 单位: rpx, 范围 64-120,clamped to 0-100
  • 最小直径:64rpx(能量 0)
  • 最大直径:120rpx(能量 100)
  • self 额外 +16rpx,且不小于 96rpx

6.2 头像形状

memberType 形状 CSS
parent 圆角方形 border-radius: 16rpx
child 圆形 border-radius: 50%
partner/spouse 圆形 + 底部心形装饰 圆形 + ::after 心形

6.3 头像内容

  • 背景色:主题色 15% 透明度
  • 文字:昵称首字,居中
  • self:白色背景 + 主题色边框(2rpx) + 主题色文字
  • other:主题色淡背景 + 灰色文字

6.4 昵称

  • 位置:头像正下方
  • 字号:22rpx
  • 颜色:self 用主题色+加粗,other 用 #94A3B8
  • 截断:单行,overflow: hidden, text-overflow: ellipsis

7. 连线

7.1 Canvas 层

  • 绝对定位在 View 节点层下方(z-index: 1)
  • 尺寸与容器一致
  • 只负责画线,不画节点

7.2 线条属性

数据维度 映射目标 映射规则
信任度 trust 线颜色 0 → hsl(0°,75%,45%) 红色; 100 → hsl(120°,75%,45%) 绿色
沟通度 communication 线粗细 0 → 2px; 100 → 10px; 公式: 2 + (comm/100)*8
全局 透明度 α = 0.6

7.3 线条起止点

  • 起点:self 头像边缘(非中心)
  • 终点:other 头像边缘(非中心)
  • 计算方式:从 self 中心到 other 中心的向量,沿向量方向缩短一个半径距离到边缘

7.4 兼容性

  • 优先使用 type=2d Canvas API
  • 降级为 CanvasContext(保留 drawLegacy 模式)

8. 渲染流程

mounted / watch(members, energyMap, intimacyMap):
  1. Vue 渲染 View 节点(flex 自动布局完成)
  2. $nextTick → 
     a. 遍历每个节点,通过 getBoundingClientRect 读取屏幕坐标
     b. 计算 self 中心坐标和每个 other 中心坐标
     c. 初始化 Canvas(type=2d),设置 dpr
     d. 清除 Canvas → drawLines() 画所有 self→other 连线

9. 交互

  • interactive=true(默认):View 节点绑定 @tap$emit('memberTap', {memberId, memberType, nickname})
  • interactive=false:不绑定 tap 事件
  • Canvas 层 pointer-events: none,所有交互由 View 处理

10. 受影响的文件

文件 改动类型 说明
components/FamilyRelationGraph.vue 重写 View+Canvas 替换纯 Canvas
所有引用 FamilyRelationGraph 的页面 无改动 Props 接口不变,drop-in 替换

引用方列表(无需改动):

  • pages/index/parent-index.vue
  • pages/body/index.vue
  • pages/mind/index.vue
  • pages/action/index.vue
  • pages/body/member-body-detail.vue
  • pages/mind/member-mind-detail.vue
  • pages/action/member-action-detail.vue
  • pages/index/member-home-detail.vue
  • pages/wealth/index.vue
  • pages/profile/profile.vue

11. 不做的事

  • 不改动上层调用方的 Props 传参方式
  • 不引入新依赖
  • 不改变 intimacyMap/energyMap 的数据格式
  • 不改变 memberTap 事件的参数结构