<think>
我们使用纯JavaScript实现,不依赖Vue等框架
主要步骤:
1. 创建画布容器和外部元素
2. 初始化X6画布
3. 创建一个隐藏的节点(虚拟节点)用于映射外部元素的位置
4. 将虚拟节点与画布内的节点连接
5. 监听窗口变化、滚动、画布缩放平移等事件,实时更新虚拟节点的位置
6. 允许外部元素可拖动,拖动时更新连线
注意:由于外部元素在画布容器外,我们需要将外部元素的页面坐标转换为画布内的坐标。
坐标转换公式:
画布坐标x = 外部元素页面x - 画布容器左上角的页面x
画布坐标y = 外部元素页面y - 画布容器左上角的页面y
但要注意:画布可能被缩放或平移,所以虚拟节点位置需要实时更新。
实现细节:
- 虚拟节点设置为一个点(1x1像素)并且完全透明,这样用户看不到它,连线却可以连接它。
- 监听窗口resize、scroll事件以及画布的缩放(graph.on('scale'))和平移(graph.on('translate'))事件,更新虚拟节点位置。
- 如果外部元素可拖动,则需要在拖动过程中不断更新虚拟节点位置。
我们将创建一个可拖动的外部元素,并实现连线。
完整代码如下(包含详细的注释):
</think>
# AntV X6 节点与外部元素连线实现
下面是一个完整的实现 AntV X6 流程图节点与外部 HTML 元素连线的解决方案,使用纯 JavaScript 实现。
## 思路分析
1. **坐标转换**:将外部元素的屏幕坐标转换为画布坐标系
2. **虚拟节点**:创建透明的锚点节点映射外部元素位置
3. **连线管理**:在虚拟节点和真实节点之间建立连线
4. **实时更新**:监听各类事件保持连线位置同步
## 完整实现
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AntV X6 - 外部元素连线</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@antv/x6/dist/x6.css">
<script src="https://cdn.jsdelivr.net/npm/@antv/x6/dist/x6.js"></script>
<style>
body {
margin: 0;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
overflow: hidden;
}
header {
background: linear-gradient(90deg, #2c3e50, #4a6491);
color: white;
padding: 20px 30px;
border-bottom: 3px solid #3498db;
}
h1 {
margin: 0;
font-size: 32px;
display: flex;
align-items: center;
gap: 15px;
}
h1 svg {
width: 36px;
height: 36px;
fill: #3498db;
}
.content {
display: flex;
padding: 20px;
gap: 20px;
min-height: 600px;
}
.external-elements {
flex: 0 0 250px;
display: flex;
flex-direction: column;
gap: 15px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.element {
padding: 15px;
border-radius: 6px;
color: white;
font-weight: 500;
text-align: center;
cursor: grab;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: all 0.3s ease;
user-select: none;
}
.element:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.15);
}
.element-1 { background: linear-gradient(135deg, #ff6b6b, #ff8e53); }
.element-2 { background: linear-gradient(135deg, #4facfe, #00f2fe); }
.element-3 { background: linear-gradient(135deg, #42e695, #3bb2b8); }
.graph-container {
flex: 1;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: white;
position: relative;
}
#graph {
width: 100%;
height: 100%;
}
.instructions {
background: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 15px 20px;
margin: 20px;
border-radius: 0 8px 8px 0;
}
.instructions h3 {
margin-top: 0;
color: #0d47a1;
}
.instructions ul {
padding-left: 20px;
}
.instructions li {
margin-bottom: 8px;
}
.status-bar {
background: #2c3e50;
color: white;
padding: 10px 20px;
font-size: 14px;
display: flex;
justify-content: space-between;
}
.drag-hint {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.9);
padding: 20px;
border-radius: 8px;
text-align: center;
border: 2px dashed #3498db;
z-index: 10;
max-width: 80%;
}
.drag-hint h3 {
margin-top: 0;
color: #2c3e50;
}
footer {
text-align: center;
padding: 15px;
color: #7f8c8d;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>
<svg viewBox="0 0 24 24">
<path d="M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/>
</svg>
AntV X6 外部元素连线实现
</h1>
</header>
<div class="content">
<div class="external-elements">
<div class="element element-1" data-id="external-1">外部元素 1</div>
<div class="element element-2" data-id="external-2">外部元素 2</div>
<div class="element element-3" data-id="external-3">外部元素 3</div>
<div class="instructions">
<h3>操作指南</h3>
<ul>
<li>拖动左侧元素到右侧画布区域</li>
<li>拖动画布节点可以创建新连线</li>
<li>外部元素连线会随页面滚动自动更新</li>
<li>使用滚轮缩放画布</li>
<li>拖动画布空白区域可平移视图</li>
</ul>
</div>
</div>
<div class="graph-container">
<div id="graph"></div>
<div class="drag-hint">
<h3>请从左侧拖动元素到此处</h3>
<p>拖入外部元素会自动创建连接节点</p>
</div>
</div>
</div>
<div class="status-bar">
<div class="info">状态: <span id="status">就绪</span></div>
<div class="connections">连接数: <span id="connection-count">0</span></div>
</div>
</div>
<footer>
AntV X6 外部元素连接示例 | 使用数学公式映射坐标位置
</footer>
<script>
// 初始化画布
const graph = new X6.Graph({
container: document.getElementById('graph'),
width: '100%',
height: '100%',
grid: {
size: 10,
visible: true,
type: 'doubleMesh',
args: [
{ color: '#eee', thickness: 1 },
{ color: '#ddd', thickness: 1, factor: 4 }
]
},
panning: true,
mousewheel: {
enabled: true,
modifiers: 'ctrl',
minScale: 0.5,
maxScale: 3
},
connecting: {
router: 'orth',
connector: 'rounded',
anchor: 'center',
connectionPoint: 'anchor',
allowBlank: false,
snap: {
radius: 20
},
createEdge() {
return new X6.Shape.Edge({
attrs: {
line: {
stroke: '#5F95FF',
strokeWidth: 2,
targetMarker: {
name: 'block',
size: 6
}
}
},
zIndex: 0
});
}
},
highlighting: {
magnetAdsorbed: {
name: 'stroke',
args: {
attrs: {
fill: '#5F95FF',
stroke: '#5F95FF'
}
}
}
}
});
// 创建中心节点
const centerNode = graph.addNode({
id: 'center-node',
x: 300,
y: 250,
width: 80,
height: 40,
label: '中心节点',
attrs: {
body: {
fill: '#ecf5ff',
stroke: '#409EFF',
strokeWidth: 2,
rx: 6,
ry: 6
},
label: {
fill: '#1a365d',
fontSize: 12,
fontWeight: 'bold'
}
}
});
// 存储虚拟节点和连接的映射
const virtualNodes = {};
const connections = {};
// 更新状态信息
const statusElement = document.getElementById('status');
const connectionCountElement = document.getElementById('connection-count');
// 添加连线状态
function addConnection(externalId) {
connections[externalId] = true;
updateConnectionCount();
}
// 移除连线状态
function removeConnection(externalId) {
delete connections[externalId];
updateConnectionCount();
}
// 更新连接数显示
function updateConnectionCount() {
const count = Object.keys(connections).length;
connectionCountElement.textContent = count;
}
// 坐标转换函数(核心)
function getElementPosition(element) {
const graphContainer = graph.container;
const graphRect = graphContainer.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
// 计算相对于画布容器的坐标
const x = elementRect.left - graphRect.left + elementRect.width / 2;
const y = elementRect.top - graphRect.top + elementRect.height / 2;
return { x, y };
}
// 创建虚拟节点(透明锚点)
function createVirtualNode(externalId, element) {
// 检查是否已存在虚拟节点
if (virtualNodes[externalId]) return virtualNodes[externalId];
// 计算初始位置
const pos = getElementPosition(element);
// 创建虚拟节点(1x1像素,完全透明)
const virtualNode = graph.addNode({
id: `virtual-${externalId}`,
x: pos.x,
y: pos.y,
width: 1,
height: 1,
attrs: {
body: {
fill: 'transparent',
stroke: 'transparent',
opacity: 0
},
label: { text: '' }
}
});
virtualNodes[externalId] = virtualNode;
statusElement.textContent = `已创建虚拟节点: ${externalId}`;
return virtualNode;
}
// 更新虚拟节点位置
function updateVirtualNodePosition(externalId, element) {
const virtualNodeId = `virtual-${externalId}`;
const virtualNode = graph.getCellById(virtualNodeId);
if (virtualNode) {
const pos = getElementPosition(element);
virtualNode.position(pos.x, pos.y, { silent: true });
}
}
// 连接到中心节点
function connectToCenter(externalId) {
const virtualNodeId = `virtual-${externalId}`;
// 避免重复连接
const existingEdges = graph.getEdges().filter(edge =>
edge.getSourceCellId() === virtualNodeId &&
edge.getTargetCellId() === centerNode.id
);
if (existingEdges.length > 0) return existingEdges[0];
// 创建新连线
const edge = graph.addEdge({
source: { cell: virtualNodeId },
target: { cell: centerNode.id },
attrs: {
line: {
stroke: '#F56C6C',
strokeWidth: 2,
strokeDasharray: '5, 5',
targetMarker: {
name: 'block',
size: 7,
fill: '#F56C6C'
}
}
},
zIndex: 1
});
addConnection(externalId);
statusElement.textContent = `已创建连接: ${externalId} → 中心节点`;
document.querySelector('.drag-hint').style.display = 'none';
return edge;
}
// 事件监听器:创建外部元素连接
function setupExternalElement(element) {
const externalId = element.dataset.id;
element.addEventListener('mousedown', (e) => {
element.classList.add('active');
statusElement.textContent = `正在拖动: ${externalId}`;
});
element.addEventListener('mouseup', (e) => {
element.classList.remove('active');
// 检查是否在画布区域内释放
const graphRect = graph.container.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
const overlapsX = elementRect.left < graphRect.right &&
elementRect.right > graphRect.left;
const overlapsY = elementRect.top < graphRect.bottom &&
elementRect.bottom > graphRect.top;
if (overlapsX && overlapsY) {
createVirtualNode(externalId, element);
connectToCenter(externalId);
} else {
statusElement.textContent = `未在画布区域内释放: ${externalId}`;
}
});
}
// 初始化所有外部元素
document.querySelectorAll('.element').forEach(setupExternalElement);
// 监听事件保持位置同步
function setupEventListeners() {
// 窗口变化事件
window.addEventListener('resize', updateAllVirtualNodes);
window.addEventListener('scroll', updateAllVirtualNodes);
// 画布变换事件
graph.on('scale', updateAllVirtualNodes);
graph.on('translate', updateAllVirtualNodes);
}
// 更新所有虚拟节点位置
function updateAllVirtualNodes() {
document.querySelectorAll('.element').forEach(element => {
const externalId = element.dataset.id;
if (virtualNodes[externalId]) {
updateVirtualNodePosition(externalId, element);
}
});
}
// 初始化事件监听
setupEventListeners();
// 示例:添加几个默认节点
graph.addNode({
x: 150,
y: 150,
width: 70,
height: 40,
label: '输入节点',
attrs: {
body: {
fill: '#f0f9eb',
stroke: '#67c23a',
rx: 4,
ry: 4
},
label: {
fill: '#1a365d',
fontSize: 11
}
}
});
graph.addNode({
x: 450,
y: 350,
width: 70,
height: 40,
label: '输出节点',
attrs: {
body: {
fill: '#fef0f0',
stroke: '#f56c6c',
rx: 4,
ry: 4
},
label: {
fill: '#1a365d',
fontSize: 11
}
}
});
</script>
</body>
</html>
```
## 核心原理说明
### 1. 坐标转换方法
外部元素位置通过以下公式转换为画布坐标:
$$x_{画布} = x_{元素} - x_{容器}$$
$$y_{画布} = y_{元素} - y_{容器}$$
### 2. 虚拟节点技术
创建透明节点作为外部元素的锚点:
```javascript
graph.addNode({
id: `virtual-${externalId}`,
width: 1,
height: 1,
attrs: { body: { opacity: 0 } }
})
```
### 3. 实时同步机制
监听三类事件保持连线位置:
1. 窗口变化:`resize`, `scroll`
2. 画布变换:`scale`, `translate`
3. 元素移动:`mousemove` 事件
### 4. 连线管理
在虚拟节点和中心节点之间创建特殊样式的连线:
```javascript
graph.addEdge({
source: 'virtual-external',
target: 'center-node',
attrs: {
line: {
stroke: '#F56C6C',
strokeDasharray: '5,5'
}
}
})
```
## 功能特点
1. **拖放操作**:从左侧拖放元素到画布自动创建连接
2. **实时同步**:连线随页面滚动和画布操作自动更新
3. **视觉区分**:虚线样式区分外部元素连接
4. **连接统计**:实时显示当前连接数
5. **交互提示**:清晰的操作指引和状态反馈
## 使用说明
1. 从左侧拖动任意彩色元素到右边画布区域
2. 观察自动创建的节点和到中心节点的连线
3. 滚动页面或缩放画布,观察连线自动更新
4. 尝试拖动已有节点创建新连接
此实现完整展示了AntV X