家庭成员关系图谱组件设计
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. 开放问题
- 亲密度数据源:目前 Contact 实体有 intimacyLevel(0-100)和 contactCount,可用于 line 长度和粗细。但 Contact 管理的是「外部联系人」,家庭成员的亲密度是否需新增
FamilyIntimacy 表或字段?
- self 的身份:当前页面的用户是家长视角(查看孩子数据)还是孩子视角?self 应显示为「当前孩子本人」还是「家长自己」?
- 触摸反馈动画:Canvas 重绘实现缩放动画,需要 requestAnimationFrame 兼容性确认。