基于Vue-Konva实现动态Rect形状连接器绑定及父子关系构建咨询
Vue-Konva实现矩形节点连接与父子关联方案
核心逻辑设计
- 数据结构设计:给每个矩形节点配置唯一ID,连接器单独存
sourceId(源节点ID)、targetId(目标节点ID)、标签文本三个核心字段,不直接写死坐标,所有连线坐标通过关联节点的位置动态计算 - 交互流程设计:点击选择源节点→拖拽过程中实时渲染虚线临时连线→释放鼠标时如果落点在其他节点范围内,生成正式连接器,同时绑定两个节点的父子关联关系
- 坐标自动更新:节点移动时依赖响应式特性自动重新计算所有关联连接器的起止坐标,保证连线始终跟随节点位置变化
具体实现步骤
1. 定义基础响应式数据结构
// 矩形节点列表 const rectNodes = ref([ { id: 'node_' + Date.now(), // 唯一标识不可重复 x: 100, y: 100, width: 120, height: 60, fill: '#ffffff', stroke: '#2d8cf0', strokeWidth: 2, parent: null, // 父节点ID children: [] // 子节点ID集合 } ]) // 连接器列表 const connectors = ref([]) // 拖拽过程中的临时连线状态 const tempLine = ref({ active: false, sourceId: '', startX: 0, startY: 0, endX: 0, endY: 0 }) // 画布基础配置 const stageConfig = reactive({ width: window.innerWidth, height: window.innerHeight })
2. 绑定源节点选择事件
给可拖拽矩形绑定mousedown事件,记录连线的起点信息:
const onRectMouseDown = (e, node) => { // 仅响应左键点击 if (e.evt.button !== 0) return // 记录临时连线起点为当前节点中心 tempLine.value = { active: true, sourceId: node.id, startX: node.x + node.width / 2, startY: node.y + node.height / 2, endX: node.x + node.width / 2, endY: node.y + node.height / 2 } }
3. 实现临时连线跟随鼠标逻辑
给画布Stage绑定mousemove事件,实时更新临时连线终点:
const onStageMouseMove = (e) => { if (!tempLine.value.active) return const pointerPos = e.currentTarget.getPointerPosition() tempLine.value.endX = pointerPos.x tempLine.value.endY = pointerPos.y }
4. 实现目标节点绑定与正式连接器生成
给画布Stage绑定mouseup事件,判断落点是否为有效目标节点,生成正式连接:
const onStageMouseUp = (e) => { if (!tempLine.value.active) return // 获取鼠标落点下的节点数据 const targetNode = e.target?.getAttr('nodeData') // 排除源节点本身和非节点区域的点击 if (targetNode && targetNode.id !== tempLine.value.sourceId) { // 新增正式连接器 connectors.value.push({ id: 'conn_' + Date.now(), sourceId: tempLine.value.sourceId, targetId: targetNode.id, label: '父子关联' }) // 绑定父子关联关系 const sourceNode = rectNodes.value.find(item => item.id === tempLine.value.sourceId) sourceNode.children.push(targetNode.id) targetNode.parent = tempLine.value.sourceId } // 清空临时连线状态 tempLine.value.active = false }
5. 动态计算连接器坐标
通过计算属性实时把连接器数据转换成可渲染的坐标信息:
const computedConnectors = computed(() => { return connectors.value.map(conn => { const source = rectNodes.value.find(item => item.id === conn.sourceId) const target = rectNodes.value.find(item => item.id === conn.targetId) if (!source || !target) return null // 取两个节点中心作为连线起止点,也可以根据需求调整为节点边缘坐标 const startX = source.x + source.width / 2 const startY = source.y + source.height / 2 const endX = target.x + target.width / 2 const endY = target.y + target.height / 2 // 标签取连线中点位置 const labelX = (startX + endX) / 2 const labelY = (startY + endY) / 2 return { ...conn, points: [startX, startY, endX, endY], labelX, labelY } }).filter(Boolean) })
6. 模板渲染
<template> <v-stage :config="stageConfig" @mousemove="onStageMouseMove" @mouseup="onStageMouseUp" > <v-layer> <!-- 正式连线:优先渲染保证在节点下层 --> <v-line v-for="conn in computedConnectors" :key="conn.id" :config="{ points: conn.points, stroke: '#555', strokeWidth: 2, lineCap: 'round', arrowHeadWidth: 8, arrowHeadLength: 10, pointerAtEnding: true // 末端加箭头标识方向 }" /> <!-- 连线标签 --> <v-text v-for="conn in computedConnectors" :key="'label_' + conn.id" :config="{ x: conn.labelX, y: conn.labelY, text: conn.label, fill: '#333', fontSize: 12, offsetX: conn.label.length * 3, offsetY: 6, background: '#fff', padding: 2 }" /> <!-- 临时虚线 --> <v-line v-if="tempLine.active" :config="{ points: [tempLine.startX, tempLine.startY, tempLine.endX, tempLine.endY], stroke: '#999', strokeWidth: 2, dash: [5, 5] }" /> <!-- 矩形节点 --> <v-rect v-for="node in rectNodes" :key="node.id" :config="node" :nodeData="node" @mousedown="(e) => onRectMouseDown(e, node)" draggable /> </v-layer> </v-stage> </template>
可选优化项
- 可以将连线起止点调整为节点边缘而非中心,避免连线穿过节点内部
- 增加连接器的点击选中、删除功能
- 增加路径自动避障逻辑,避免连线和节点重叠
内容的提问来源于stack exchange,提问作者BATMAN_2008
相关产品推荐
相关产品推荐

