一、说明
本例使用V4.1.12
版本。最新版本4.2.6
版本有问题,节点间连线数据获取不到,建议不要轻易升级。
1. 长啥样
2. 目录说明
├─public
│ └─data
├─src
| ├─assets # 样式
| │ └─style
| ├─behavior # g6行为定义
| ├─components # 组件
| │ ├─menu # 左侧菜单
| │ ├─props # 右侧属性配置
| │ ├─svg-icon # svg组件
| │ └─toolbar-panel
| ├─libs # 库(发布订阅模式)
| ├─plugins # toolbar命令注册
| ├─services # axios封装
| ├─shape # g6图形定义
| │ ├─edges # g6边相关定义
| │ └─nodes # g6节点相关定义
| └─utils # 工具函数
└─Index.vue # 入口
复制代码
3. 功能点
1. 拖拽组件到画布区域
2. 画布区节点hover,selected
3. 画布区节点拖拽
4. 锚点绘制
5. 节点连线
6. 连线hover,selected
7. toolbar操作
8. 画布拖拽
9. minimap
复制代码
二、功能实现
1. 初始化G6实例
G6的渲染数据是有权重的:
使用 graph.node(nodeFn) 配置 > 数据中动态配置 > 实例化图时全局配置
// 注册自定义节点、边
registerFactory(G6);
// 画布
const canvasMap = this.$refs['canvasRef'];
this.graph = new G6.Graph({
container: canvasMap,
width: 600,
height: 400,
// 编辑模式下行为
modes: {},
// 默认节点配置
defaultNode: {},
// 默认边配置
defaultEdge: {},
// 全局样式
nodeStateStyles: {},
edgeStateStyles: {}
});
// 设置graph模式
this.graph.setMode(this.mode);
// 设置自适应比例
this.graph.fitView = true;
// 初始化canvas事件
this.initEvents();
复制代码
2. 节点和边的交互行为
节点和边的行为,分为点击选中、hover、双击等。以节点的交互事件为例:
// hover-node.js
export default G6 => {
G6.registerBehavior('hover-node', {
getDefaultCfg() {
return {
multiple: false // 多选
}
},
getEvents() {
return {
'node:mouseenter': 'onNodeMouseEnter',
'node:mousemove': 'onNodeMouseMove',
'node:mouseleave': 'onNodeMouseLeave'
}
},
onNodeMouseEnter(e) {
const graph = this.graph;
const item = e.item;
if (!item.hasState('nodeState:selected')) {
graph.setItemState(item, 'nodeState', 'hover');
}
// 锚点显示
graph.setItemState(item, 'anchorShow', true); // 二值状态
// mouseenter回调
graph.emit('on-node-mouseenter', e);
},
onNodeMouseMove(e) {
this.graph.emit('on-node-mousemove', e);
},
onNodeMouseLeave(e) {
const graph = this.graph;
const item = e.item;
if (item.hasState('nodeState:hover')) {
graph.setItemState(item, 'nodeState', 'default');
}
// 锚点显示
graph.setItemState(item, 'anchorShow', false);
// mouseleave回调
graph.emit('on-node-mouseleave', e);
}
})
}
复制代码
// select-node.js
export default G6 => {
G6.registerBehavior('select-node', {
getDefaultCfg() {
return {
multiple: false // 多选
}
},
getEvents() {
return {
'node:click': 'onNodeClick',
'node:dblclick': 'onNodeDblClick',
'node:contextmenu': 'onNodeContextmenu'
}
},
onNodeClick(e) {
const graph = this.graph;
const item = e.item;
this._clearSelected();
graph.setItemState(item, 'nodeState', 'selected');
graph.emit('after-node-selected', e);
},
onNodeDblClick(e) {
const graph = this.graph;
const item = e.item;
this._clearSelected();
// item.toFront(); // 提高层级
graph.setItemState(item, 'nodeState', 'selected');
graph.emit('after-node-dblclick', e);
},
onNodeContextmenu(e) {
this.graph.emit('on-editor-contextmenu', {
type: 'node',
event: e
})
}, // 清除选中的节点和边样式
_clearSelected() {
let graph = this.graph;
const selectNodes = this.graph.findAllByState('node', 'nodeState:selected');
selectNodes.forEach(node => {
graph.setItemState(node, 'nodeState', 'default');
})
const selectEdges = this.graph.findAllByState('edge', 'edgeState:selected');
selectEdges.forEach(edge => {
graph.setItemState(edge, 'edgeState', 'default');
})
}
})
}
复制代码
'node:click': 'onNodeClick'
是G6提供的事件交互方式。
我们想要点击节点的时候更改节点的样式怎么做?
- 定义节点不同状态时的样式
nodeStateStyles: {
'nodeState:default': {
lineWidth: 1,
fill: '#fff',
stroke: '#1890FF',
opacity: 1
},
'nodeState:hover': {
lineWidth: 2,
fill: '#d5f1fd',
opacity: 0.8
},
'nodeState:selected': {
fill: '#caebf9',
stroke: '#1890FF',
opacity: 0.9
}
}
复制代码
- 更改状态
graph.setItemState(item, 'nodeState', 'selected');
复制代码
作用机制是:
setItemState
时,触发节点setState
钩子函数,setState会根据节点状态应用对应的样式。
3. 节点的绘制
// base-node.js
// 基础节点,其他节点在此基础上扩展
import itemEvents from './item-event';
import anchorEvent from './anchor-event';
export default G6 => {
G6.registerNode('base-node', {
// 绘制图标
drawIcon(cfg, group, attrs) {
},
// 初始化锚点
initAnchor(cfg, group) {
// 锚点图形
group.anchorShapes = [];
// 显示锚点
group.showAnchor = group => {
this.drawAnchor(cfg, group);
}
// 隐藏锚点
group.clearAnchor = group => {
if (group.anchorShapes) {
group.anchorShapes.forEach(anchor => anchor.remove());
}
group.anchorShapes = [];
}
},
// 绘制锚点
drawAnchor(cfg, group) {
const keyShape = group.getFirst();
const { type, direction, anchorPointStyles } = keyShape.attr();
const node = group.get('item');
const bBox = keyShape.getBBox();
// 获取配置锚点
const anchors = this.getAnchorPoints(cfg) || [];
if (anchors.length) {
anchors.forEach((anchorCfg, i) => {
const x = bBox.width * (anchorCfg[0] - 0.5);
const y = bBox.height * (anchorCfg[1] - 0.5);
// 绘制三层锚点
// 最底层:锚点背景
// 中间层:锚点
// 最顶层:锚点group,用于事件触发
const anchor = group.addShape('circle', {
attrs: {
x,
y,
...anchorPointStyles
},
zIndex: 1,
className: 'node-anchor',
nodeId: node.get('id'),
draggable: true,
isAnchor: true,
index: i // 方便查找锚点
})
const anchorGroup = group.addShape('circle', {
attrs: {
x,
y,
r: 11,
fill: '#000',
opacity: 0
},
zIndex: 2,
className: 'node-anchor-group',
nodeId: node.get('id'),
draggable: true,
isAnchor: true,
index: i
})
// 添加绑定锚点事件,给最上层的group添加事件
anchorEvent(anchorGroup, group, anchorCfg);
// 锚点放到组中
group.anchorShapes.push(anchor);
group.anchorShapes.push(anchorGroup);
})
// 查找所有锚点
group.getAllAnchors = () => {
return group.anchorShapes.filter(c => c.get('isAnchor') === true);
}
// 查找指定锚点
group.getAnchor = i => {
return group.anchorShapes.filter(c => c.get('className') === 'node-anchor' && c.get('index') === i);
}
// 查找所有锚点背景
group.getAllAnchorBg = () => {
return group.anchorShapes.filter(c => c.get('className') === 'node-anchor-bg');
};
}
},
// 绘制label文本
drawLabel(cfg, group, attrs) {
const { label, labelCfg, labels } = attrs;
// 多行文本
if (labels) {
labels.forEach(item => {
const { label, labelCfg: { maxLength }, className } = item;
// 文本长度
let text = maxLength ? label.substr(0, maxLength) : label || '';
if (label.length > maxLength) {
text = `${text}...`;
}
group.addShape('text', {
attrs: {
text,
...item,
...item.labelCfg
},
className: `node-text ${className}`,
draggable: true
})
})
// 单行文本
} else if (label) {
const { maxLength } = labelCfg;
// 超出显示...
let text = maxLength ? label.substr(0, maxLength) : label || '';
if (label.length > maxLength) {
text = `${text}...`;
}
group.addShape('text', {
attrs: {
text,
x: 0,
y: 0,
...label,
...labelCfg
},
className: 'node-text',
draggable: true
})
}
},
// 绘制节点和文本
draw(cfg, group) {
// 获取图形样式
const attrs = this.getShapeStyle(cfg, group);
const keyShape = group.addShape(this.shapeType, {
className: `${this.shapeType}-shape`, // rect-shape
draggable: true,
attrs
})
// 通过class查找元素
group.getItem = className => {
return group.get('children').find(item => item.get('className') === className);
}
// 绘制文本节点
this.drawLabel(cfg, group, attrs);
// 添加锚点
this.initAnchor(cfg, group);
return keyShape;
},
// 更新节点和文本
update(cfg, node) {
},
// 设置节点状态,主要是交互状态,业务状态在draw方法中实现(定义此方法,实例中配置的状态会失效)
setState(name, value, item) { // name =>'nodeState hover' , value => true , item => Node
const activeEvents = [
'anchorShow', // 锚点显示隐藏
'anchorActive', // 鼠标按下锚点,让所有node的锚点处于激活状态
'nodeState', // nodeState:xxx都会触发nodeState事件
'nodeState:default',
'nodeState:hover',
'nodeState:selected'
];
const group = item.getContainer(); // 获取容器
if (group.get('destroyed')) return; // 组已经卸载
if (activeEvents.includes(name)) {
// this指向当前实例
// 调用对应更新状态的方法
itemEvents[name].call(this, value, group);
} else {
console.warn(`warning: ${name} 事件回调未注册`);
}
},
// 锚点配置
getAnchorPoints(cfg) {
return cfg.anchorPoints || [
[0.5, 0],
[1, 0.5],
[0.5, 1],
[0, 0.5]
]
}
}, 'single-node');
}
复制代码
继承base-node
注册rect-node
和circle-node
节点。
// builtIn-node.js
export default G6 => {
G6.registerNode('rect-node', {
// 图形类型
shapeType: 'rect',
// shape样式
getShapeStyle(cfg) {
const width = cfg.style.width || 80;
const height = cfg.style.height || 40;
return getStyle.call(this, {
width,
height,
radius: 5,
x: -width / 2,
y: -height / 2
}, cfg);
}
}, 'base-node');
// 扩展圆形节点
G6.registerNode('circle-node', {
shapeType: 'circle',
getShapeStyle(cfg) {
const r = cfg.style.r || 30;
return getStyle.call(this, {
r, // 半径
// 将图形中心坐标移动到图形中心, 用于方便鼠标位置计算
x: 0,
y: 0
}, cfg);
}
}, 'base-node');
}
复制代码
4. toolbar操作栏
注册命令,待用户点击toolbar按钮是触发对应方法。
// command.js
import { mix, clone, isString } from '@antv/util';
class Command {
constructor() {
this._cfgs = this.getDefaultCfg();
this.list = []; // 注册的命令集合
this.queue = []; // 用户撤销重做
}
getDefaultCfg() {
return { _command: { zoomDelta: 0.1, queue: [], current: 0, clipboard: [] } };
}
get(key) {
return this._cfgs[key];
}
set(key, val) {
this._cfgs[key] = val;
}
// 初始化插件
initPlugin(graph) {
this.initCommands();
// 获取命令队列
graph.getCommands = () => {
return this.get('_command').queue;
};
// 获取当前命令
graph.getCurrentCommand = () => {
const c = this.get('_command');
return c.queue[c.current - 1];
};
// 执行命令
graph.executeCommand = (name, cfg) => {
this.execute(name, graph, cfg);
};
// 命令是否可以被执行
graph.commandEnable = name => {
return this.enable(name, graph);
};
}
// 注册命令
registerCommand(name, cfg) {
if (this[name]) {
mix(this[name], cfg);
} else {
const cmd = mix(
{},
{
name: name, // add、update、autoFit、redo、undo
shortcutCodes: [], // 键盘命令
queue: true, // 是否入队,支持撤销重做
executeTimes: 1, // 撤销次数
init() { // 初始化调用函数
console.log('init');
},
enable() { // 是否可操作
return true;
},
execute(graph) { // 命令执行
this.snapShot = graph.save(); // 获取图数据
this.selectedItems = graph.get('selectedItems'); // 获取当前选中的元素
// 执行命令
if (this.method) {
if (isString(this.method)) {
graph[this.method]();
} else {
this.method(graph);
}
}
},
back(graph) { // 命令撤销
graph.read(this.snapShot);
graph.set('selectedItems', this.selectedItems);
}
},
cfg
);
// 将命令挂载到Command实例上
this[name] = cmd;
// 将命令push到list中
this.list.push(cmd);
}
}
// 执行命令
execute(name, graph, cfg) {
const cmd = mix({}, this[name], cfg);
const manager = this.get('_command');
// 如果可以点击操作
if (cmd.enable(graph)) {
// 调用初始化函数
cmd.init();
if (cmd.queue) {
manager.queue.splice(manager.current, manager.queue.length - manager.current, cmd);
manager.current++;
}
}
// 执行命令
graph.emit('beforecommandexecute', { command: cmd });
cmd.execute(graph);
graph.emit('aftercommandexecute', { command: cmd });
return cmd;
}
// 是否可点击
enable(name, graph) {
return this[name]?.enable(graph);
}
// 销毁插件
destroyPlugin() {
this._events = null;
this._cfgs = null;
this.list = [];
this.queue = [];
this.destroyed = true;
}
// 删除子流程节点
_deleteSubProcessNode(graph, itemId) {
const subProcess = graph.find('node', node => {
if (node.get('model')) {
const clazz = node.get('model').clazz;
if (clazz === 'subProcess') {
const containerGroup = node.getContainer();
const subGroup = containerGroup.subGroup;
const item = subGroup.findById(itemId);
return subGroup.contain(item);
} else {
return false;
}
} else {
return false;
}
});
if (subProcess) {
const group = subProcess.getContainer();
const resultModel = group.removeItem(subProcess, itemId);
graph.updateItem(subProcess, resultModel);
}
}
// 初始化命令-注册命令
initCommands() {
const cmdPlugin = this;
// 删除
cmdPlugin.registerCommand('delete', {
enable: function (graph) {
const mode = graph.getCurrentMode();
const selectedItems = graph.get('selectedItems');
return mode === 'edit' && selectedItems && selectedItems.length > 0;
},
method: function (graph) {
const selectedItems = graph.get('selectedItems');
graph.emit('beforedelete', { items: selectedItems });
if (selectedItems && selectedItems.length > 0) {
selectedItems.forEach(i => {
const node = graph.findById(i);
if (node) {
graph.remove(i);
} else {
cmdPlugin._deleteSubProcessNode(graph, i);
}
});
}
graph.emit('afterdelete', { items: selectedItems });
},
shortcutCodes: ['Delete', 'Backspace']
});
// 重做
cmdPlugin.registerCommand('redo', {
queue: false,
enable: function (graph) {
const mode = graph.getCurrentMode();
const manager = cmdPlugin.get('_command');
return mode === 'edit' && manager.current < manager.queue.length;
},
execute: function (graph) {
const manager = cmdPlugin.get('_command');
const cmd = manager.queue[manager.current];
if (cmd) {
cmd.execute(graph);
}
manager.current++;
},
shortcutCodes: [
['metaKey', 'shiftKey', 'z'],
['ctrlKey', 'shiftKey', 'z']
]
});
// 撤销
cmdPlugin.registerCommand('undo', {
queue: false,
enable: function (graph) {
const mode = graph.getCurrentMode();
return mode === 'edit' && cmdPlugin.get('_command').current > 0;
},
execute: function (graph) {
const manager = cmdPlugin.get('_command');
const cmd = manager.queue[manager.current - 1];
if (cmd) {
cmd.executeTimes++;
cmd.back(graph);
}
manager.current--;
},
shortcutCodes: [
['metaKey', 'z'],
['ctrlKey', 'z']
]
});
// 拷贝
cmdPlugin.registerCommand('copy', {
queue: false,
enable: function (graph) {
const mode = graph.getCurrentMode();
const items = graph.get('selectedItems');
return mode === 'edit' && items && items.length > 0;
},
method: function (graph) {
const manager = cmdPlugin.get('_command');
manager.clipboard = [];
const items = graph.get('selectedItems');
if (items && items.length > 0) {
const item = graph.findById(items[0]);
if (item) {
manager.clipboard.push({ type: item.get('type'), model: item.getModel() });
}
}
}
});
// 粘贴
cmdPlugin.registerCommand('paste', {
enable: function (graph) {
const mode = graph.getCurrentMode();
return mode === 'edit' && cmdPlugin.get('_command').clipboard.length > 0;
},
method: function (graph) {
const manager = cmdPlugin.get('_command');
this.pasteData = clone(manager.clipboard[0]);
const addModel = this.pasteData.model;
if (addModel.x) {
addModel.x += 10;
}
if (addModel.y) {
addModel.y += 10;
}
const { clazz = 'userTask' } = addModel;
const timestamp = new Date().getTime();
const id = clazz + timestamp;
addModel.id = id;
const item = graph.add(this.pasteData.type, addModel);
item.toFront();
}
});
// 放大
cmdPlugin.registerCommand('zoomIn', {
queue: false,
enable: function (graph) {
const zoom = graph.getZoom();
const maxZoom = graph.get('maxZoom');
const minZoom = graph.get('minZoom');
return zoom <= maxZoom && zoom >= minZoom;
},
execute: function (graph) {
const manager = cmdPlugin.get('_command');
const maxZoom = graph.get('maxZoom');
const zoom = graph.getZoom();
this.originZoom = zoom;
let currentZoom = zoom + manager.zoomDelta;
if (currentZoom > maxZoom) currentZoom = maxZoom;
graph.zoomTo(currentZoom);
},
back: function (graph) {
graph.zoomTo(this.originZoom);
},
shortcutCodes: [
['metaKey', '='],
['ctrlKey', '=']
]
});
// 缩小
cmdPlugin.registerCommand('zoomOut', {
queue: false,
enable: function (graph) {
const zoom = graph.getZoom();
const maxZoom = graph.get('maxZoom');
const minZoom = graph.get('minZoom');
return zoom <= maxZoom && zoom >= minZoom;
},
execute: function (graph) {
const manager = cmdPlugin.get('_command');
const minZoom = graph.get('minZoom');
const zoom = graph.getZoom();
this.originZoom = zoom;
let currentZoom = zoom - manager.zoomDelta;
if (currentZoom < minZoom) currentZoom = minZoom;
graph.zoomTo(currentZoom);
},
back: function (graph) {
graph.zoomTo(this.originZoom);
},
shortcutCodes: [
['metaKey', '-'],
['ctrlKey', '-']
]
});
// 重置缩放(自适应)
cmdPlugin.registerCommand('resetZoom', {
queue: false,
execute: function (graph) {
const zoom = graph.getZoom();
this.originZoom = zoom;
graph.zoomTo(1);
},
back: function (graph) {
graph.zoomTo(this.originZoom);
}
});
// 1:1
cmdPlugin.registerCommand('autoFit', {
queue: false,
execute: function (graph) {
const zoom = graph.getZoom();
this.originZoom = zoom;
graph.fitView = true;
},
back: function (graph) {
graph.zoomTo(this.originZoom);
}
});
// 置顶
cmdPlugin.registerCommand('toFront', {
queue: false,
enable: function (graph) {
const items = graph.get('selectedItems');
return items && items.length > 0;
},
execute: function (graph) {
const items = graph.get('selectedItems');
if (items && items.length > 0) {
const item = graph.findById(items[0]);
item.toFront();
graph.paint();
}
},
back: function (graph) {
console.log(graph);
}
});
// 返回
cmdPlugin.registerCommand('toBack', {
queue: false,
enable: function (graph) {
const items = graph.get('selectedItems');
return items && items.length > 0;
},
execute: function (graph) {
const items = graph.get('selectedItems');
if (items && items.length > 0) {
const item = graph.findById(items[0]);
item.toBack();
graph.paint();
}
},
back: function (graph) {
console.log(graph);
}
});
}
}
export default Command;
复制代码
如果操作可点击,调用注册好的命令
// toolbar.js
import { deepMix, each, wrapBehavior } from '@antv/util';
import { modifyCSS } from '@antv/dom-util';
class Toolbar {
constructor(cfgs) {
this._cfgs = deepMix(this.getDefaultCfg(), cfgs);
}
getDefaultCfg() {
return { container: null };
}
get(key) {
return this._cfgs[key];
}
set(key, val) {
this._cfgs[key] = val;
}
// 初始化插件
initPlugin(graph) {
const self = this;
this.set('graph', graph);
const events = self.getEvents();
const bindEvents = {};
each(events, (v, k) => {
const event = wrapBehavior(self, v);
bindEvents[k] = event;
graph.on(k, event); // 事件监听,更新toolbar
});
// 挂载到实例上
this._events = bindEvents;
this.initEvents();
this.updateToolbar();
}
// 点击节点,命令执行后,更新toolbar
getEvents() {
return { 'after-node-selected': 'updateToolbar', aftercommandexecute: 'updateToolbar' };
}
// toolbar按钮点击,调用事件
initEvents() {
const graph = this.get('graph');
const parentNode = this.get('container');
const children = parentNode.querySelectorAll('.flow-toolbar > div[data-command]');
each(children, child => {
const cmdName = child.getAttribute('data-command');
child.addEventListener('click', () => {
// 如果此toolbar是启用状态
if (graph.commandEnable(cmdName)) {
graph.executeCommand(cmdName);
}
});
});
}
// 点击节点,命令执行后,更新toolbar
updateToolbar() {
const graph = this.get('graph');
const parentNode = this.get('container');
const children = parentNode.querySelectorAll('.flow-toolbar > div[data-command]');
each(children, child => {
const cmdName = child.getAttribute('data-command');
// 按钮可被点击
if (graph.commandEnable(cmdName)) {
modifyCSS(child, {
cursor: 'pointer'
});
modifyCSS(child.children[0], {
fill: '#333',
cursor: 'pointer'
});
child.children[0].setAttribute('fill', '#333');
// 按钮不可被点击
} else {
modifyCSS(child, {
cursor: 'default'
});
modifyCSS(child.children[0], {
fill: 'rgba(0, 0, 0, 0.25)',
cursor: 'default'
});
child.children[0].setAttribute('fill', 'rgba(0, 0, 0, 0.25)');
}
});
}
// 销毁插件
destroyPlugin() {
const graph = this.get('graph');
graph.get('canvas').destroy();
const container = graph.getContainer();
container.parentNode.removeChild(container);
}
}
export default Toolbar;
复制代码
三、G6文档
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END