# 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 头像尺寸 能量值映射到头像直径: ```javascript //能量值来自当前维度的 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 事件的参数结构