|
@@ -1,6 +1,10 @@
|
|
|
<template>
|
|
<template>
|
|
|
<!-- 全 Canvas 关系图谱 - Force-directed 物理布局 -->
|
|
<!-- 全 Canvas 关系图谱 - Force-directed 物理布局 -->
|
|
|
- <view class="family-graph-wrapper" v-if="members && members.length >= 1">
|
|
|
|
|
|
|
+ <view class="family-graph-wrapper" v-if="members && members.length >= 1" :style="{ minHeight: canvasHeight + 'px' }">
|
|
|
|
|
+ <!-- 右上角小齿轮 → 成员管理 -->
|
|
|
|
|
+ <view class="gear-icon" @click="onManageTap" @tap.stop="onManageTap">
|
|
|
|
|
+ <text class="gear-text">⚙</text>
|
|
|
|
|
+ </view>
|
|
|
<canvas
|
|
<canvas
|
|
|
class="family-graph-canvas"
|
|
class="family-graph-canvas"
|
|
|
canvas-id="forceGraphCanvas"
|
|
canvas-id="forceGraphCanvas"
|
|
@@ -10,6 +14,10 @@
|
|
|
@touchstart="onTouchStart"
|
|
@touchstart="onTouchStart"
|
|
|
@touchmove="onTouchMove"
|
|
@touchmove="onTouchMove"
|
|
|
@touchend="onTouchEnd" />
|
|
@touchend="onTouchEnd" />
|
|
|
|
|
+ <!-- + 添加成员提示(触摸自身节点时显示) -->
|
|
|
|
|
+ <view class="add-hint" :class="{ 'add-hint-visible': touchingSelf }" :style="addHintPosition">
|
|
|
|
|
+ <text class="add-hint-text">+</text>
|
|
|
|
|
+ </view>
|
|
|
</view>
|
|
</view>
|
|
|
<!-- 空状态 -->
|
|
<!-- 空状态 -->
|
|
|
<view class="family-graph-empty" v-else-if="members && members.length === 0">
|
|
<view class="family-graph-empty" v-else-if="members && members.length === 0">
|
|
@@ -71,9 +79,6 @@ export default {
|
|
|
canvasHeight: 280,
|
|
canvasHeight: 280,
|
|
|
dpr: 1,
|
|
dpr: 1,
|
|
|
|
|
|
|
|
- // 物理模拟节点
|
|
|
|
|
- simNodes: [],
|
|
|
|
|
-
|
|
|
|
|
// 物理参数
|
|
// 物理参数
|
|
|
physics: {
|
|
physics: {
|
|
|
repulsion: 800, // 节点间斥力强度
|
|
repulsion: 800, // 节点间斥力强度
|
|
@@ -89,6 +94,13 @@ export default {
|
|
|
dragging: null, // { nodeIndex, offsetX, offsetY }
|
|
dragging: null, // { nodeIndex, offsetX, offsetY }
|
|
|
dragStartPos: null, // 用于判断是否真的拖拽了
|
|
dragStartPos: null, // 用于判断是否真的拖拽了
|
|
|
|
|
|
|
|
|
|
+ // 触摸/悬停状态(显示 + 号)
|
|
|
|
|
+ touchingSelf: false,
|
|
|
|
|
+ hoveringSelf: false,
|
|
|
|
|
+
|
|
|
|
|
+ // 是否 PC 环境(h5 编译)
|
|
|
|
|
+ isPC: false,
|
|
|
|
|
+
|
|
|
// 动画
|
|
// 动画
|
|
|
animFrameId: null,
|
|
animFrameId: null,
|
|
|
isSimulating: false,
|
|
isSimulating: false,
|
|
@@ -108,6 +120,16 @@ export default {
|
|
|
return this.themeColors[this.dimensionCode] || '#8D6E63'
|
|
return this.themeColors[this.dimensionCode] || '#8D6E63'
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ // + 提示位置(跟随中心节点偏移)
|
|
|
|
|
+ addHintPosition: function() {
|
|
|
|
|
+ var selfNode = this.nodeData.find(function(n) { return n.isSelf })
|
|
|
|
|
+ if (!selfNode) return {}
|
|
|
|
|
+ return {
|
|
|
|
|
+ left: (selfNode.x + selfNode.radius * 0.7) + 'px',
|
|
|
|
|
+ top: (selfNode.y - selfNode.radius * 0.5) + 'px'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
// 将 members 转换为 simNodes
|
|
// 将 members 转换为 simNodes
|
|
|
nodeData: function() {
|
|
nodeData: function() {
|
|
|
var self = this
|
|
var self = this
|
|
@@ -546,6 +568,7 @@ export default {
|
|
|
var touch = e.touches ? e.touches[0] : e.detail
|
|
var touch = e.touches ? e.touches[0] : e.detail
|
|
|
if (!touch) return
|
|
if (!touch) return
|
|
|
|
|
|
|
|
|
|
+ var self = this
|
|
|
var rect = {}
|
|
var rect = {}
|
|
|
var query = uni.createSelectorQuery().in(this)
|
|
var query = uni.createSelectorQuery().in(this)
|
|
|
query.select('#forceGraphCanvas').boundingClientRect()
|
|
query.select('#forceGraphCanvas').boundingClientRect()
|
|
@@ -556,16 +579,21 @@ export default {
|
|
|
var y = touch.clientY - rect.top
|
|
var y = touch.clientY - rect.top
|
|
|
var node = self.getTouchNode(x, y)
|
|
var node = self.getTouchNode(x, y)
|
|
|
if (node) {
|
|
if (node) {
|
|
|
- self.dragging = {
|
|
|
|
|
- node: node,
|
|
|
|
|
- offsetX: x - node.x,
|
|
|
|
|
- offsetY: y - node.y
|
|
|
|
|
|
|
+ if (node.isSelf) {
|
|
|
|
|
+ // 自身节点 → 显示 + 号
|
|
|
|
|
+ self.touchingSelf = true
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 其他节点 → 拖拽
|
|
|
|
|
+ self.dragging = {
|
|
|
|
|
+ node: node,
|
|
|
|
|
+ offsetX: x - node.x,
|
|
|
|
|
+ offsetY: y - node.y
|
|
|
|
|
+ }
|
|
|
|
|
+ self.dragStartPos = { x: x, y: y }
|
|
|
|
|
+ node.fx = node.x
|
|
|
|
|
+ node.fy = node.y
|
|
|
|
|
+ node.fixed = true
|
|
|
}
|
|
}
|
|
|
- self.dragStartPos = { x: x, y: y }
|
|
|
|
|
- // 固定节点
|
|
|
|
|
- node.fx = node.x
|
|
|
|
|
- node.fy = node.y
|
|
|
|
|
- node.fixed = true
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}.bind(this))
|
|
}.bind(this))
|
|
@@ -597,6 +625,18 @@ export default {
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
onTouchEnd: function(e) {
|
|
onTouchEnd: function(e) {
|
|
|
|
|
+ // 如果正在触摸自身节点 → 跳转添加成员
|
|
|
|
|
+ if (this.touchingSelf) {
|
|
|
|
|
+ this.touchingSelf = false
|
|
|
|
|
+ if (this.interactive) {
|
|
|
|
|
+ this.$emit('addMemberTap')
|
|
|
|
|
+ this.goAddMember()
|
|
|
|
|
+ }
|
|
|
|
|
+ this.dragging = null
|
|
|
|
|
+ this.dragStartPos = null
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (!this.dragging) return
|
|
if (!this.dragging) return
|
|
|
|
|
|
|
|
var wasDragged = this.dragStartPos && (
|
|
var wasDragged = this.dragStartPos && (
|
|
@@ -622,6 +662,20 @@ export default {
|
|
|
this.dragStartPos = null
|
|
this.dragStartPos = null
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ // 跳转到添加成员页面
|
|
|
|
|
+ goAddMember: function() {
|
|
|
|
|
+ uni.navigateTo({
|
|
|
|
|
+ url: '/pages/family/add-member'
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 点击右上角小齿轮 → 跳转成员管理页
|
|
|
|
|
+ onManageTap: function() {
|
|
|
|
|
+ uni.navigateTo({
|
|
|
|
|
+ url: '/pages/profile/family-members'
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
// ===== 工具方法 =====
|
|
// ===== 工具方法 =====
|
|
|
|
|
|
|
|
getEnergy: function(memberId) {
|
|
getEnergy: function(memberId) {
|
|
@@ -644,11 +698,12 @@ export default {
|
|
|
|
|
|
|
|
updateCanvasSize: function() {
|
|
updateCanvasSize: function() {
|
|
|
var self = this
|
|
var self = this
|
|
|
|
|
+ var cnt = this.members ? this.members.length : 0
|
|
|
|
|
+ self.canvasHeight = Math.max(200, cnt * 90)
|
|
|
var query = uni.createSelectorQuery().in(this)
|
|
var query = uni.createSelectorQuery().in(this)
|
|
|
query.select('.family-graph-wrapper').boundingClientRect(function(rect) {
|
|
query.select('.family-graph-wrapper').boundingClientRect(function(rect) {
|
|
|
if (rect) {
|
|
if (rect) {
|
|
|
self.canvasWidth = rect.width || 340
|
|
self.canvasWidth = rect.width || 340
|
|
|
- self.canvasHeight = rect.height || 280
|
|
|
|
|
}
|
|
}
|
|
|
}).exec()
|
|
}).exec()
|
|
|
}
|
|
}
|
|
@@ -660,7 +715,8 @@ export default {
|
|
|
.family-graph-wrapper {
|
|
.family-graph-wrapper {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
- height: 280px;
|
|
|
|
|
|
|
+ min-height: 200px;
|
|
|
|
|
+ height: auto;
|
|
|
margin: 10rpx 0;
|
|
margin: 10rpx 0;
|
|
|
background: #FFFFFF;
|
|
background: #FFFFFF;
|
|
|
border-radius: 24rpx;
|
|
border-radius: 24rpx;
|
|
@@ -672,6 +728,51 @@ export default {
|
|
|
left: 0;
|
|
left: 0;
|
|
|
z-index: 1;
|
|
z-index: 1;
|
|
|
}
|
|
}
|
|
|
|
|
+/* 右上角小齿轮 */
|
|
|
|
|
+.gear-icon {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 12rpx;
|
|
|
|
|
+ right: 12rpx;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+ width: 56rpx;
|
|
|
|
|
+ height: 56rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
|
|
|
|
+}
|
|
|
|
|
+.gear-text {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ color: #94A3B8;
|
|
|
|
|
+}
|
|
|
|
|
+/* + 添加成员提示 */
|
|
|
|
|
+.add-hint {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ z-index: 5;
|
|
|
|
|
+ width: 40rpx;
|
|
|
|
|
+ height: 40rpx;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #10B981;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transform: scale(0.5);
|
|
|
|
|
+ transition: opacity 0.15s, transform 0.15s;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+}
|
|
|
|
|
+.add-hint.add-hint-visible {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ transform: scale(1);
|
|
|
|
|
+}
|
|
|
|
|
+.add-hint-text {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #FFFFFF;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
.family-graph-empty {
|
|
.family-graph-empty {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|