效果展示
1. 拖拽
2. 编辑节点内容和样式
3. 删除
4. 连线、拖拽以及删除连线
5. 单个节点进行配置面板
目前只研究到简单的属性配置,后续可能会有更高级的配置项
6. 概览
开发
1. 背景
文档地址:g6.antv.vision/zh/docs/des…
2. 环境与准备
1. NPM 包引入
npm install --save @antv/g6
3. 代码
home.vue
<template>
<div class="home-root">
<div class="header">G6-编辑器</div>
<!-- 左侧按钮 -->
<item-panel />
<!-- 挂载节点 -->
<div
id="canvasPanel"
ref="canvasPanel"
@dragover.prevent
/>
<!-- 配置面板 -->
<div
id="configPanel"
:class="{ hidden: !configVisible }"
>
<i
class="gb-toggle-btn"
@click="configVisible = !configVisible"
/>
<h2 class="panel-title">数据配置</h2>
<div class="config-data">
id: {{ config.id }}, data: {{ config.data }}
</div>
<h2 class="panel-title">节点样式配置</h2>
<div class="config-data">
<div class="config-item">
形状: <select v-model="configData.node.type">
<option
v-for="(item, index) in nodeShapes"
:key="index"
:value="item.shape"
>
{{ item.name }}
</option>
</select>
</div>
<div class="config-item">
背景色: <input v-model="configData.node.style.fill">
</div>
<div class="config-item">
边框虚线: <input v-model="configData.node.lineDash">
</div>
<div class="config-item">
边框颜色: <input v-model="configData.node.style.stroke">
</div>
<div class="config-item">
宽: <input v-model="configData.node.style.width">px
</div>
<div class="config-item">
高: <input v-model="configData.node.style.height">px
</div>
</div>
<h2 class="panel-title">文字样式配置</h2>
<div class="config-data">
<div class="config-item">
文字: <input v-model="configData.node.label">
</div>
<div class="config-item">
字体大小: <input v-model.number="configData.node.labelCfg.fontSize">
</div>
<div class="config-item">
颜色: <input v-model="configData.node.labelCfg.style.fill">
</div>
</div>
<button @click="configVisible = false">取消</button>
<button
class="save"
@click="save"
>
保存
</button>
</div>
</div>
</template>
<script>
import G6 from '@antv/g6';
import ItemPanel from './ItemPanel.vue';
import registerFactory from '../../components/graph/graph';
export default {
name: 'home',
components: {
ItemPanel,
},
data() {
return {
graph: {}, // 图表的载体
configVisible: false,
// 配置项
configData: {
node: {
id: '',
label: '',
lineDash: 'none',
type: 'rect-node',
style: {
width: 160,
height: 60,
fill: '#cccccc',
stroke: '',
lineWidth: '',
},
labelCfg: {
fontSize: 12,
style: {
fill: '#fff',
},
},
},
},
config: {},
// 形状选项
nodeShapes: [
{
name: '矩形',
shape: 'rect-node',
},
{
name: '圆形',
shape: 'circle-node',
},
{
name: '椭圆',
shape: 'ellipse-node',
},
{
name: '菱形',
shape: 'diamond-node',
},
],
};
},
props: {},
computed: {},
methods: {
createGraphic () {
const vm = this;
// 实例化 Grid 插件 这个插件是用来绘制网格
const grid = new G6.Grid();
// Menu 用于配置节点上的右键菜单
const menu = new G6.Menu({
offsetX: -20,
offsetY: -50,
itemTypes: ['node', 'edge'],
getContent(e) {
const outDiv = document.createElement('div');
outDiv.style.width = '80px';
outDiv.style.cursor = 'pointer';
outDiv.innerHTML = '<p id="deleteNode">删除节点</p>';
return outDiv;
},
handleMenuClick(target, item) {
const { id } = target;
if(id) {
vm[id](item);
}
},
});
// Minimap 是用于快速预览和探索图的工具
const minimap = new G6.Minimap({
size: [200, 100],
});
// 配置
const cfg = registerFactory(G6, {
width: window.innerWidth,
height: window.innerHeight,
// renderer: 'svg',
layout: {
type: 'xxx', // 位置将固定
},
// 所有节点默认配置
defaultNode: {
type: 'rect-node',
style: {
radius: 10,
width: 100,
height: 50,
cursor: 'move',
fill: '#ecf3ff',
},
labelCfg: {
fontSize: 20,
style: {
cursor: 'move',
},
},
},
// 所有边的默认配置
defaultEdge: {
type: 'polyline-edge', // 扩展了内置边, 有边的事件
style: {
radius: 5,
offset: 15,
stroke: '#aab7c3',
lineAppendWidth: 10, // 防止线太细没法点中
endArrow: true,
},
},
// 覆盖全局样式
nodeStateStyles: {
'nodeState:default': {
opacity: 1,
},
'nodeState:hover': {
opacity: 0.8,
},
'nodeState:selected': {
opacity: 0.9,
},
},
// 默认边不同状态下的样式集合
edgeStateStyles: {
'edgeState:default': {
stroke: '#aab7c3',
},
'edgeState:selected': {
stroke: '#1890FF',
},
'edgeState:hover': {
animate: true,
animationType: 'dash',
stroke: '#1890FF',
},
},
modes: {
// 支持的 behavior
default: ['drag-canvas', 'drag-shadow-node', 'canvas-event', 'delete-item', 'select-node', 'hover-node', 'active-edge'],
originDrag: ['drag-canvas', 'drag-node', 'canvas-event', 'delete-item', 'select-node', 'hover-node', 'active-edge'],
},
plugins: [menu, minimap, grid],
// ... 其他G6原生入参
});
// Graph是实例化图表
this.graph = new G6.Graph(cfg);
// this.graph.read(data); // 读取数据
// this.graph.paint(); // 渲染到页面
// this.graph.get('canvas').set('localRefresh', false); // 关闭局部渲染
// this.graph.fitView();
},
// 初始化图事件
initGraphEvent() {
// 拖拽事件
this.graph.on('drop', e => {
const { originalEvent } = e;
if(originalEvent.dataTransfer) {
// 这里获取到侧边栏拖拽的节点的数据
const transferData = originalEvent.dataTransfer.getData('dragComponent');
console.log(transferData, 'transferData')
// 添加节点
if(transferData) {
this.addNode(transferData, e);
}
}
});
// 在节点上释放事件
this.graph.on('node:drop', e => {
console.log(e, 'e1')
e.item.getOutEdges().forEach(edge => {
edge.clearStates('edgeState');
});
});
// 选中节点后的事件
this.graph.on('after-node-selected', e => {
console.log(e, '选中节点')
this.configVisible = !!e;
if (e && e.item) {
const model = e.item.get('model');
console.log(model, '获取到model')
this.config = model;
this.configData = {
node: {
id: model.id,
label: model.label,
lineDash: 'none',
type: model.type,
style: model.style,
labelCfg: model.labelCfg,
},
}
}
});
// 添加边之前的事件
this.graph.on('before-edge-add', ({ source, target, sourceAnchor, targetAnchor }) => {
setTimeout(() => {
this.graph.addItem('edge', {
id: `${+new Date() + (Math.random()*10000).toFixed(0)}`, // edge id
source: source.get('id'),
target: target.get('id'),
sourceAnchor,
targetAnchor,
// label: 'edge label',
});
}, 100);
});
},
// 添加节点
addNode (transferData, { x, y }) {
const { label, shape, fill } = JSON.parse(transferData);
const model = {
label,
// id: Util.uniqueId(),
// 形状
type: shape,
style: {
fill: fill || '#ecf3ff',
},
// 坐标
x,
y,
};
this.graph.addItem('node', model);
},
deleteNode(item) {
this.graph.removeItem(item);
},
save() {
// this.graph.addItem('node', model);
console.log(this.configData, 'this.configData')
this.graph.updateItem(this.config.id, {
label: this.configData.node.label,
type: this.configData.node.type,
labelCfg: this.configData.node.labelCfg,
style: {
stroke: this.configData.node.style.stroke,
width: Number(this.configData.node.style.width),
height: Number(this.configData.node.style.height),
}
});
this.graph.refreshItem(this.config.id)
this.graph.refreshPositions()
// window.alert('我觉得就算我不写你也会了');
}
},
mounted() {
// 创建画布
this.$nextTick(() => {
this.createGraphic();
this.initGraphEvent();
});
},
created() {
},
};
</script>
<style scoped>
.header {
height: 55px;
line-height: 55px;
position: relative;
box-shadow: 1px 2px 3px #cccccc;
z-index: 11;
background: #f5f5f5;
text-align: center;
font-weight: bolder;
font-size: 20px;
margin-bottom: 20px;
}
#configPanel{
margin-top: 20px;
color: #902a8f;
}
</style>
ItemPanel.vue
<template>
<div
id="itemPanel"
ref="itemPanel"
:class="{'hidden': itemVisible}"
>
<i class="iconfont icon-h-drag" />
<div class="icon-tool">
<i
draggable="true"
data-label="方形节点"
data-shape="rect-node"
class="node iconfont icon-rect"
/>
<i
draggable="true"
data-label="椭圆形节点"
data-shape="ellipse-node"
class="node iconfont icon-ellipse"
/>
<i
draggable="true"
data-label="菱形节点"
data-shape="diamond-node"
class="node iconfont icon-diamond"
/>
<i class="split" />
</div>
</div>
</template>
<script>
export default {
name: 'ItemPanel',
data () {
return {
itemVisible: false,
};
},
mounted () {
const icons = [...this.$refs.itemPanel.querySelector('.icon-tool').querySelectorAll('.node')];
icons.forEach(icon => {
icon.addEventListener('dragstart', event => {
const shape = icon.getAttribute('data-shape');
const label = icon.getAttribute('data-label');
const fill = icon.getAttribute('fill');
/* 设置拖拽传输数据 */
event.dataTransfer.setData('dragComponent',
JSON.stringify({
label,
shape,
fill,
}),
);
});
});
// 阻止默认动作
document.addEventListener('drop', e => {
e.preventDefault();
}, false);
},
};
</script>
<style lang="scss">
#itemPanel {
position : absolute;
top : 0;
left : 0;
bottom : 0;
z-index : 10;
width : 100px;
background : #fff;
padding-top: 65px;
transition : transform .3s ease-in-out;
box-shadow : 0 0 2px 0 rgba(0, 0, 0, .1);
&.hidden {
transform: translate(-100%, 0);
}
.icon-h-drag {
position : absolute;
top : 40px;
left : 0;
width : 100%;
height : 20px;
line-height: 20px;
font-size : 18px;
background : #f5f5f5;
text-align : center;
cursor : move;
&:hover {
background: #f1f1f1;
}
}
.gb-toggle-btn {
width : 10px;
height : 20px;
top : 50%;
left : 100%;
border-radius: 0 10px 10px 0;
box-shadow : 2px 0 2px 0 rgba(0, 0, 0, .1);
transform : translate(0, -50%);
}
.split {
height : 1px;
display : block;
background: #e0e0e0;
margin : 5px 0;
}
.icon-tool {
padding:10px;
text-align : center;
.iconfont {
display : block;
width : 40px;
height : 40px;
line-height : 40px;
font-size : 30px;
cursor : move;
border : 1px solid transparent;
margin: 0 auto;
&:hover {
border-color: #ccc;
}
}
.node{
display: block;
margin-bottom: 10px;
cursor : move;
}
.circle{
height: 80px;
line-height: 80px;
border-radius: 50%;
border: 1px solid #ccc;
background:#eef5fe;
}
.warning{
height: 40px;
line-height: 40px;
border-left: 4px solid #E6A23C;
background:#f8ecda;
}
.end{
height: 40px;
line-height: 40px;
border-radius: 10px;
background:#f9e3e2;
}
}
}
</style>
其它配置项
由于相关代码过多,这里就先不作展示
4. 数据存储
前端编辑出想要的图以后,有两种方式进行保存
- graph.save(),以数据的形式保存,优点是重新获取数据以后,可以再次编辑;缺点是成本较高,数据结构较复杂;
- graph.downloadFullImage()/graph.toFullDataURL(),以图片的形式保存,优点是简单方面,拿到图片直接展示;缺点是不可编辑,不够灵活。