<think>我们使用Vue2和AntV X6来创建一个图形编辑应用。AntV X6是一个图编辑引擎,提供了丰富的图形编辑和交互功能。
首先,我们需要安装依赖:
- vue2
- @antv/x6
步骤:
1. 创建一个Vue组件。
2. 在组件中,初始化X6的Graph实例。
3. 在mounted生命周期中挂载Graph到DOM元素。
4. 添加一些节点和边。
注意:在Vue中使用X6,我们需要确保在mounted之后初始化图形,因为此时DOM已经渲染完成。
下面是一个简单的示例:
</think>
# Vue2 与 AntV X6 图形编辑应用
我将创建一个基于 Vue2 和 AntV X6 的图形编辑应用,展示如何初始化图形画布、添加节点和边,并实现基本交互功能。
## 解决方案思路
1. 创建 Vue2 应用并集成 AntV X6
2. 初始化图形画布并设置基本配置
3. 添加各种类型的节点和边
4. 实现节点拖拽添加功能
5. 添加基本交互功能(如框选、撤销/重做)
## 完整代码实现
```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue2 + AntV X6 图形编辑应用</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script src="https://unpkg.com/@antv/x6/dist/x6.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
color: #333;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 20px;
}
header {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
text-align: center;
}
h1 {
color: white;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
margin-bottom: 10px;
}
.subtitle {
color: #e0e0ff;
font-size: 1.2em;
max-width: 800px;
margin: 0 auto;
}
.app-content {
display: flex;
gap: 20px;
}
.toolbar {
background: rgba(255, 255, 255, 0.85);
border-radius: 12px;
padding: 20px;
width: 250px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
.toolbar h2 {
margin-bottom: 15px;
color: #2c3e50;
border-bottom: 2px solid #2c3e50;
padding-bottom: 8px;
}
.tool-item {
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
cursor: grab;
border: 2px solid #e0e0e0;
transition: all 0.3s ease;
text-align: center;
font-weight: 500;
}
.tool-item:hover {
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
border-color: #4e9af1;
}
.tool-item.rect {
background: linear-gradient(135deg, #a8edea, #fed6e3);
}
.tool-item.circle {
background: linear-gradient(135deg, #d4fc79, #96e6a1);
}
.tool-item.ellipse {
background: linear-gradient(135deg, #f6d365, #fda085);
}
.tool-item.cylinder {
background: linear-gradient(135deg, #c2e59c, #64b3f4);
}
.graph-container {
flex: 1;
background: rgba(255, 255, 255, 0.85);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
position: relative;
}
#graph-container {
width: 100%;
height: 600px;
}
.controls {
display: flex;
gap: 12px;
margin-top: 20px;
flex-wrap: wrap;
}
.control-btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
background: #3498db;
color: white;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
min-width: 120px;
}
.control-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.control-btn.redo {
background: #f39e12;
}
.control-btn.undo {
background: #9b59b6;
}
.control-btn.clear {
background: #e74c3c;
}
.control-btn.layout {
background: #2ecc71;
}
.control-btn.export {
background: #34495e;
}
.status-bar {
background: rgba(255, 255, 255, 0.85);
border-radius: 12px;
padding: 15px 20px;
text-align: center;
font-size: 14px;
color: #2c3e50;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
}
.info-panel {
background: rgba(255, 255, 255, 0.85);
border-radius: 12px;
padding: 20px;
width: 250px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
.info-panel h2 {
margin-bottom: 15px;
color: #2c3e50;
border-bottom: 2px solid #2c3e50;
padding-bottom: 8px;
}
.info-content {
background: #f8f9fa;
border-radius: 8px;
padding:command: 15px;
min-height: 200px;
font-size: 14px;
line-height: 1.6;
}
footer {
text-align: center;
color: #e0e0ff;
padding: 20px;
font-size: 14px;
}
.highlight {
color: #e74c3c;
font-weight: bold;
}
@media (max-width: 1200px) {
.app-content {
flex-direction: column;
}
.toolbar, .info-panel {
width: 100%;
}
}
</style>
</head>
<body>
<div id="app" class="container">
<header>
<h1>Vue2 + AntV X6 图形编辑应用</h1>
<p class="subtitle">使用Vue2集成AntV X6图形库实现流程图、ER图、拓扑图等可视化编辑功能</p>
</header>
<div class="app-content">
<div class="toolbar">
<h2>图形组件</h2>
<div class="tool-item rect" draggable="true" @dragstart="dragStart($event, 'rect')">矩形节点</div>
<div class="tool-item circle" draggable="true" @dragstart="dragStart($event, 'circle')">圆形节点</div>
<div class="tool-item ellipse" draggable="true" @dragstart="dragStart($event, 'ellipse')">椭圆形节点</div>
<div class="tool-item cylinder" draggable="true" @dragstart="dragStart($event, 'cylinder')">圆柱形节点</div>
<div class="controls">
<button class="control-btn undo" @click="undo">撤销</button>
<button class="control-btn redo" @click="redo">重做</button>
<button class="control-btn clear" @click="clearGraph">清空</button>
<button class="control-btn layout" @click="autoLayout">自动布局</button>
<button class="control-btn export" @click="exportGraph">导出JSON</button>
</div>
</div>
<div class="graph-container">
<div id="graph-container"></div>
</div>
<div class="info-panel">
<h2>操作说明</h2>
<div class="info-content">
<p>1. <span class="highlight">拖拽</span>左侧组件到画布创建节点</p>
<p>2. 点击节点并<span class="highlight">拖动连接点</span>创建连线</p>
<p>3. <span class="highlight">框选</span>多个节点进行批量操作</p>
<p>4. 使用<span class="highlight">滚轮</span>缩放画布</p>
<p>5. <span class="highlight">Ctrl+Z</span>撤销操作,<span class="highlight">Ctrl+Y</span>重做操作</p>
<p>6. 双击节点<span class="highlight">编辑文本</span></p>
<hr style="margin: 15px 0; border-color: #eee;">
<p>当前节点数: {{ nodeCount }}</p>
<p>当前边数: {{ edgeCount }}</p>
</div>
</div>
</div>
<div class="status-bar">
<div>当前模式: {{ interactionMode }} | 状态: {{ statusMessage }}</div>
<div>AntV X6 v{{ x6Version }} | Vue v{{ vueVersion }}</div>
</div>
<footer>
Vue2 与 AntV X6 集成示例 | 设计:高级前端开发专家
</footer>
</div>
<script>
new Vue({
el: '#app',
data: {
graph: null,
statusMessage: '就绪',
interactionMode: '选择模式',
nodeCount: 0,
edgeCount: 0,
history: [],
historyIndex: -1,
x6Version: X6.version,
vueVersion: Vue.version
},
mounted() {
this.initGraph();
this.setupKeyboardShortcuts();
},
methods: {
initGraph() {
// 初始化画布
this.graph = new X6.Graph({
container: document.getElementById('graph-container'),
width: '100%',
height: '100%',
grid: {
visible: true,
type: 'doubleMesh',
args: [
{ color: '#e0e0e0', thickness: 1 },
{ color: '#d0d0d0', thickness: 1, factor: 4 }
]
},
zoom: 0.9,
panning: {
enabled: true,
modifiers: 'shift'
},
mouse: {
wheel: {
enabled: true,
modifiers: ['ctrl', 'meta']
}
},
highlighting: {
magnetAvailable: {
name: 'stroke',
args: {
attrs: {
fill: '#fff',
stroke: '#285fd3',
strokeWidth: 2
}
}
}
},
connecting: {
snap: true,
allowBlank: false,
allowLoop: false,
allowEdge: false,
highlight: true,
router: {
name: 'manhattan',
args: {
style: {
corner: 'rounded',
cornerRadius: 8
}
}
},
connector: {
name: 'rounded',
args: {
radius: 8
}
},
createEdge() {
return new X6.Shape.Edge({
attrs: {
line: {
stroke: '#5F95FF',
strokeWidth: 2,
targetMarker: {
name: 'block',
size: 6
}
}
},
labels: [
{
position: 0.3,
attrs: {
text: {
text: '连线'
}
}
}
],
zIndex: 0
});
}
}
});
// 初始化示例节点
this.addSampleNodes();
// 注册事件监听
this.graph.on('node:change:position', this.updateHistory.bind(this));
this.graph.on('edge:connected', this.updateHistory.bind(this));
this.graph.on('node:removed', this.updateHistory.bind(this));
this.graph.on('edge:removed', this.updateHistory.bind(this));
this.graph.on('node:added', this.updateCounts.bind(this));
this.graph.on('node:removed', this.updateCounts.bind(this));
this.graph.on('edge:added', this.updateCounts.bind(this));
this.graph.on('edge:removed', this.updateCounts.bind(this));
// 初始化计数
this.updateCounts();
},
addSampleNodes() {
// 创建示例节点
const rect = this.graph.addNode({
shape: 'rect',
x: 100,
y: 100,
width: 80,
height: 40,
label: '开始',
attrs: {
body: {
fill: '#a8edea',
stroke: '#5F95FF',
strokeWidth: 2
},
label: {
fill: '#333',
fontSize: 14,
fontWeight: 'bold'
}
}
});
const circle = this.graph.addNode({
shape: 'circle',
x: 300,
y: 100,
width: 60,
height: 60,
label: '处理',
attrs: {
body: {
fill: '#d4fc79',
stroke: '#5F95FF',
strokeWidth: 2
},
label: {
fill: '#333',
fontSize: 14,
fontWeight: 'bold'
}
}
});
const ellipse = this.graph.addNode({
shape: 'ellipse',
x: 100,
y: 200,
width: 100,
height: 60,
label: '决策',
attrs: {
body: {
fill: '#f6d365',
stroke: '#5F95FF',
strokeWidth: 2
},
label: {
fill: '#333',
fontSize: 14,
fontWeight: 'bold'
}
}
});
// 创建连接线
this.graph.addEdge({
source: rect,
target: circle,
vertices: [
{ x: 200, y: 120 }
],
attrs: {
line: {
stroke: '#5F95FF'
}
}
});
},
dragStart(event, shape) {
event.dataTransfer.setData('shape', shape);
},
setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.ctrlKey) {
if (e.key === 'z' || e.key === 'Z') {
this.undo();
e.preventDefault();
} else if (e.key === 'y' || e.key === 'Y') {
this.redo();
e.preventDefault();
}
}
});
// 监听画布上的拖放事件
const container = document.getElementById('graph-container');
container.addEventListener('dragover', (e) => {
e.preventDefault();
this.interactionMode = '添加节点';
});
container.addEventListener('dragleave', () => {
this.interactionMode = '选择模式';
});
container.addEventListener('drop', (e) => {
e.preventDefault();
this.interactionMode = '选择模式';
const shape = e.dataTransfer.getData('shape');
if (shape) {
const point = this.graph.clientToLocal({ x: e.clientX, y: e.clientY });
this.addNodeByType(shape, point.x, point.y);
}
});
},
addNodeByType(type, x, y) {
const baseConfig = {
x: x - 40,
y: y - 20,
width: 80,
height: 40,
label: this.getLabelByType(type),
attrs: {
body: {
stroke: '#5F95FF',
strokeWidth: 2
},
label: {
fill: '#333',
fontSize: 14,
fontWeight: 'bold'
}
}
};
switch(type) {
case 'rect':
this.graph.addNode({
...baseConfig,
shape: 'rect',
attrs: {
...baseConfig.attrs,
body: {
...baseConfig.attrs.body,
fill: '#a8edea'
}
}
});
break;
case 'circle':
this.graph.addNode({
...baseConfig,
shape: 'circle',
width: 60,
height: 60,
x: x - 30,
y: y - 30,
attrs: {
...baseConfig.attrs,
body: {
...baseConfig.attrs.body,
fill: '#d4fc79'
}
}
});
break;
case 'ellipse':
this.graph.addNode({
...baseConfig,
shape: 'ellipse',
width: 100