├── 1.gif ├── src ├── utils │ ├── eventBus.js │ └── index.js ├── assets │ ├── bg.jpg │ ├── logo.png │ └── icons │ │ ├── close.svg │ │ ├── open.svg │ │ └── ok.svg ├── index.js ├── main.js ├── components │ ├── Base │ │ ├── Node.js │ │ ├── Edge.js │ │ └── Editor.js │ ├── Flow │ │ ├── index.vue │ │ ├── teamNode.js │ │ ├── customEdge.js │ │ └── customNode.js │ ├── ItemPanel │ │ ├── index.vue │ │ └── item.vue │ ├── Minimap │ │ └── index.vue │ ├── ContextMenu │ │ └── index.vue │ ├── Page │ │ └── index.vue │ ├── G6Editor │ │ └── index.vue │ ├── DetailPanel │ │ └── index.vue │ └── Toolbar │ │ └── index.vue ├── App.vue ├── behavior │ ├── add-menu.js │ ├── keyboard.js │ ├── index.js │ ├── hover-node.js │ ├── hover-edge.js │ ├── mulit-select.js │ ├── select-node.js │ ├── add-edge.js │ └── drag-item.js ├── global.js └── command │ └── index.js ├── babel.config.js ├── public ├── favicon.ico └── index.html ├── .gitignore ├── README.md ├── LICENSE ├── package.json └── vue.config.js /1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caoyu48/vue-g6-editor/HEAD/1.gif -------------------------------------------------------------------------------- /src/utils/eventBus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | export default new Vue(); -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caoyu48/vue-g6-editor/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caoyu48/vue-g6-editor/HEAD/src/assets/bg.jpg -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caoyu48/vue-g6-editor/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import G6Editor from './components/G6Editor' 2 | G6Editor.install = function (Vue) { 3 | Vue.component(G6Editor.name, G6Editor) 4 | } 5 | 6 | export default G6Editor -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App' 2 | import Vue from 'vue' 3 | import ElementUI from 'element-ui'; 4 | import 'element-ui/lib/theme-chalk/index.css'; 5 | Vue.config.productionTip = false 6 | Vue.use(ElementUI,{size:'mini'}) 7 | 8 | new Vue({ 9 | render: h => h(App), 10 | }).$mount('#app') 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | node_modules 23 | .npmignore 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![效果图](https://github.com/caoyu48/vue-g6-editor/blob/master/1.gif) 2 | # vue-g6-editor 3 | 4 | demo地址:http://62.234.69.136/ 5 | G6文档 https://www.yuque.com/antv/g6 6 | 7 | 这个是个基于阿里G6制作的modelFlow组件 g6版本为3.0,UI部分用了elementUI。 8 | 由于公司需要,需要一个模型流程图编辑器,本来g6-editor是个不错的选择,但是调研之后发现 9 | g6-editor不开源,不得商用。嗝屁,只能自己尝试着用g6实现一个editor。代码写的比较丑,仅做参考使用,不喜勿喷~。 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/Base/Node.js: -------------------------------------------------------------------------------- 1 | class Node extends Object{ 2 | constructor(params) { 3 | super() 4 | this.id = params.id 5 | 6 | for (let key in params) { 7 | this[key] = params[key]||0 8 | } 9 | this.size = params.size.split('*') 10 | this.parent = params.parent // 所属组 11 | this.index = params.index // 渲染层级 12 | } 13 | } 14 | export default Node; -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | 17 | 28 | -------------------------------------------------------------------------------- /src/components/Base/Edge.js: -------------------------------------------------------------------------------- 1 | class Edge { 2 | constructor(id, source, target, controlPoints, sourceAnchor, targetAnchor, shape,style,label) { 3 | this.id = id 4 | this.source = source 5 | this.target = target 6 | this.controlPoints = controlPoints 7 | this.sourceAnchor = sourceAnchor 8 | this.targetAnchor = targetAnchor 9 | this.shape = shape 10 | this.style=style 11 | this.label=label 12 | } 13 | } 14 | export default Edge; -------------------------------------------------------------------------------- /src/components/Flow/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/behavior/add-menu.js: -------------------------------------------------------------------------------- 1 | import eventBus from "@/utils/eventBus"; 2 | export default { 3 | getEvents() { 4 | return { 5 | 'node:contextmenu': 'onContextmenu', 6 | 'mousedown': 'onMousedown', 7 | 'canvas:click':'onCanvasClick' 8 | }; 9 | }, 10 | onContextmenu(e) { 11 | eventBus.$emit('contextmenuClick',e) 12 | }, 13 | onMousedown(e) { 14 | eventBus.$emit('mousedown',e) 15 | }, 16 | onCanvasClick(e){ 17 | eventBus.$emit('canvasClick',e) 18 | } 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | g6-editor_vue 10 | 11 | 12 | 13 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/behavior/keyboard.js: -------------------------------------------------------------------------------- 1 | import eventBus from "@/utils/eventBus"; 2 | export default { 3 | getDefaultCfg() { 4 | return { 5 | backKeyCode: 8, 6 | deleteKeyCode: 46 7 | }; 8 | }, 9 | getEvents() { 10 | return { 11 | keyup: 'onKeyUp', 12 | keydown: 'onKeyDown' 13 | }; 14 | }, 15 | 16 | onKeyDown(e) { 17 | const code = e.keyCode || e.which; 18 | switch (code) { 19 | case this.deleteKeyCode: 20 | case this.backKeyCode: 21 | eventBus.$emit('deleteItem') 22 | break 23 | } 24 | }, 25 | onKeyUp() { 26 | this.keydown = false; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/behavior/index.js: -------------------------------------------------------------------------------- 1 | import G6 from "@antv/g6/build/g6"; 2 | import hoverNode from './hover-node' 3 | import addLine from './add-edge' 4 | import dragItem from './drag-item' 5 | import selectNode from './select-node' 6 | import hoverEdge from "./hover-edge"; 7 | import keyboard from './keyboard' 8 | import mulitSelect from './mulit-select' 9 | import addMenu from './add-menu' 10 | 11 | const behavors = { 12 | 'hover-node': hoverNode, 13 | 'add-edge': addLine, 14 | 'drag-item': dragItem, 15 | 'select-node': selectNode, 16 | 'hover-edge': hoverEdge, 17 | 'keyboard':keyboard, 18 | 'mulit-select':mulitSelect, 19 | 'add-menu':addMenu 20 | } 21 | 22 | export function initBehavors() { 23 | for (let key in behavors) { 24 | G6.registerBehavior(key, behavors[key]) 25 | } 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/Base/Editor.js: -------------------------------------------------------------------------------- 1 | import { uniqueId } from '@/utils'; 2 | import eventBus from "@/utils/eventBus"; 3 | 4 | export default class Editor { 5 | constructor() { 6 | this.id = uniqueId(); 7 | } 8 | getGrpah() { 9 | return this.graph 10 | } 11 | emit(event, params) { 12 | if (event === 'afterAddPage') { 13 | this.graph = params.graph 14 | } 15 | eventBus.$emit(event, params) 16 | } 17 | on(event) { 18 | switch (event) { 19 | case 'changeNodeData': 20 | this.graph.refresh() 21 | break 22 | } 23 | } 24 | add(type, item) { 25 | this.graph.add(type, item) 26 | } 27 | update(item, model) { 28 | this.graph.update(item, model) 29 | } 30 | remove(item) { 31 | const node = this.graph.findById(item.id) 32 | this.graph.remove(node) 33 | } 34 | } -------------------------------------------------------------------------------- /src/assets/icons/open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | import pick from 'lodash/pick'; 3 | import uniqueId from 'lodash/uniqueId'; 4 | import upperFirst from 'lodash/upperFirst'; 5 | 6 | const toQueryString = obj => Object.keys(obj).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`).join('&'); 7 | 8 | const addListener = (target, eventName, handler) => { 9 | if (typeof handler === 'function') target.on(eventName, handler); 10 | }; 11 | 12 | const getBox=(x, y, width, height)=> { 13 | const x1 = (x + width) < x ? (x + width) : x 14 | const x2 = (x + width) > x ? (x + width) : x 15 | const y1 = (y + height) < y ? (y + height) : y 16 | const y2 = (y + height) > y ? (y + height) : y 17 | return { 18 | x1, x2, y1, y2 19 | } 20 | } 21 | 22 | export { 23 | merge, 24 | pick, 25 | toQueryString, 26 | uniqueId, 27 | upperFirst, 28 | addListener, 29 | getBox 30 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-present guozhaolong (guozhaolong@gmail.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/global.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview global config 3 | */ 4 | 5 | export default { 6 | version: '0.0.1-beat', 7 | rootContainerClassName: 'root-container', 8 | nodeContainerClassName: 'node-container', 9 | edgeContainerClassName: 'edge-container', 10 | groupContainerClassName:'group-container', 11 | defaultNode: { 12 | shape: 'circle', 13 | style: { 14 | fill: '#fff' 15 | }, 16 | size: 40, 17 | color: '#333' 18 | }, 19 | defaultEdge: { 20 | shape: 'line', 21 | style: {}, 22 | size: 1, 23 | color: '#333' 24 | }, 25 | nodeLabel: { 26 | style: { 27 | fill: '#595959', 28 | textAlign: 'center', 29 | textBaseline: 'middle' 30 | }, 31 | offset: 5 // 节点的默认文本不居中时的偏移量 32 | }, 33 | edgeLabel: { 34 | style: { 35 | fill: '#595959', 36 | textAlign: 'center', 37 | textBaseline: 'middle' 38 | } 39 | }, 40 | // 节点应用状态后的样式,默认仅提供 active 和 selected 用户可以自己扩展 41 | nodeStateStyle: { 42 | active: { 43 | fillOpacity: 0.8 44 | }, 45 | selected: { 46 | lineWidth: 2 47 | } 48 | }, 49 | edgeStateStyle: { 50 | active: { 51 | strokeOpacity: 0.8 52 | }, 53 | selected: { 54 | lineWidth: 2 55 | } 56 | }, 57 | loopPosition: 'top', 58 | delegateStyle: { 59 | fill: '#F3F9FF', 60 | fillOpacity: 0.5, 61 | stroke: '#1890FF', 62 | strokeOpacity: 0.9, 63 | lineDash: [5, 5] 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/components/ItemPanel/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-g6-editor", 3 | "version": "0.0.1", 4 | "description": "vue+g6+element可视化编辑器", 5 | "keyword": "vue g6 g6-editor", 6 | "author": "280196641@qq.com", 7 | "private": false, 8 | "license": "MIT", 9 | "scripts": { 10 | "dev": "npm run serve", 11 | "serve": "vue-cli-service serve", 12 | "build": "vue-cli-service build", 13 | "lint": "vue-cli-service lint" 14 | }, 15 | "dependencies": { 16 | "@antv/g6": "^3.0.5-beta.9", 17 | "core-js": "^2.6.5", 18 | "element-ui": "^2.11.1", 19 | "html-webpack-plugin": "^4.0.0-beta.8", 20 | "lodash": "^4.17.15", 21 | "script-ext-html-webpack-plugin": "^2.1.4", 22 | "vue": "^2.6.10" 23 | }, 24 | "devDependencies": { 25 | "@vue/cli-plugin-babel": "^3.10.0", 26 | "@vue/cli-plugin-eslint": "^3.10.0", 27 | "@vue/cli-service": "^3.10.0", 28 | "babel-eslint": "^10.0.1", 29 | "eslint": "^5.16.0", 30 | "eslint-plugin-vue": "^5.0.0", 31 | "vue-template-compiler": "^2.6.10" 32 | }, 33 | "eslintConfig": { 34 | "root": true, 35 | "env": { 36 | "node": true 37 | }, 38 | "extends": [ 39 | "plugin:vue/essential", 40 | "eslint:recommended" 41 | ], 42 | "rules": {}, 43 | "parserOptions": { 44 | "parser": "babel-eslint" 45 | } 46 | }, 47 | "postcss": { 48 | "plugins": { 49 | "autoprefixer": {} 50 | } 51 | }, 52 | "browserslist": [ 53 | "> 1%", 54 | "last 2 versions" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /src/assets/icons/ok.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Minimap/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 54 | 55 | 75 | -------------------------------------------------------------------------------- /src/components/ContextMenu/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 47 | 48 | -------------------------------------------------------------------------------- /src/components/Page/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 80 | 81 | -------------------------------------------------------------------------------- /src/components/G6Editor/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 75 | 76 | -------------------------------------------------------------------------------- /src/components/Flow/teamNode.js: -------------------------------------------------------------------------------- 1 | import G6 from "@antv/g6/build/g6"; 2 | import { uniqueId } from '@/utils' 3 | import openSvg from '@/assets/icons/open.svg' 4 | import closeSvg from '@/assets/icons/close.svg' 5 | const teamNode = { 6 | init() { 7 | G6.registerNode("teamNode", { 8 | draw(cfg, group) { 9 | const padding = 10 10 | const top=20 11 | // 此处必须是NUMBER 不然bbox不正常 12 | const width = cfg.width + padding * 2 13 | const height = cfg.height + padding * 2 14 | // 此处必须有偏移 不然drag-node错位 15 | const offsetX = -width / 2 16 | const offsetY = -height / 2-top 17 | const mainId = 'rect' + uniqueId() 18 | const shape = group.addShape("rect", { 19 | attrs: { 20 | id: mainId, 21 | x: offsetX, 22 | y: offsetY, 23 | width: width, 24 | height: height+top, 25 | stroke: "#ced4d9", 26 | fill: '#f2f4f5',//此处必须有fill 不然不能触发事件 27 | radius: 4 28 | } 29 | }); 30 | 31 | group.addShape("text", { 32 | attrs: { 33 | id: 'label' + uniqueId(), 34 | x: offsetX+padding, 35 | y: offsetY+padding, 36 | textBaseline:'top', 37 | text: cfg.label || '新建分组', 38 | parent: mainId, 39 | fill: "#565758" 40 | } 41 | }); 42 | group.addShape("image", { 43 | attrs: { 44 | x: offsetX + width - 26, 45 | y: offsetY + 8, 46 | width: 16, 47 | height: 16, 48 | img: closeSvg 49 | } 50 | }); 51 | // 添加文本、更多图形 52 | return shape; 53 | }, 54 | //设置状态 55 | setState(name, value, item) { 56 | const group = item.getContainer(); 57 | const shape = group.get("children")[0]; // 顺序根据 draw 时确定 58 | const selectStyles = () => { 59 | shape.attr("stroke", "#6ab7ff"); 60 | shape.attr("cursor", "move"); 61 | }; 62 | const unSelectStyles = () => { 63 | shape.attr("stroke", "#ced4d9"); 64 | }; 65 | 66 | switch (name) { 67 | case "selected": 68 | case "hover": 69 | if (value) { 70 | selectStyles() 71 | } else { 72 | unSelectStyles() 73 | } 74 | break; 75 | } 76 | } 77 | }); 78 | 79 | } 80 | } 81 | 82 | export default teamNode 83 | -------------------------------------------------------------------------------- /src/behavior/hover-node.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getEvents() { 3 | return { 4 | 'node:mouseover': 'onMouseover', 5 | 'node:mouseleave': 'onMouseleave', 6 | "node:mousedown": "onMousedown" 7 | }; 8 | }, 9 | onMouseover(e) { 10 | const self = this; 11 | const item = e.item; 12 | const graph = self.graph; 13 | const group = item.getContainer() 14 | if (e.target._attrs.isOutPointOut || e.target._attrs.isOutPoint) { 15 | group.find(g => { 16 | if (g._attrs.isInPoint || g._attrs.isOutPoint) { 17 | g.attr("fill", "#fff") 18 | } 19 | if (g._attrs.isOutPoint) { 20 | if (g._attrs.id === e.target._attrs.parent) { 21 | group.find(gr => { 22 | if (gr._attrs.id === g._attrs.id) { 23 | gr.attr('fill', "#1890ff") 24 | gr.attr('opacity',1) 25 | } 26 | }) 27 | } 28 | if (g._attrs.id === e.target._attrs.id) { 29 | g.attr("fill", "#1890ff") 30 | g.attr('opacity',1) 31 | } 32 | 33 | } 34 | }); 35 | e.target.attr("cursor", "crosshair"); 36 | this.graph.paint(); 37 | } 38 | if (item.hasState('selected')) { 39 | return 40 | } else { 41 | if (self.shouldUpdate.call(self, e)) { 42 | graph.setItemState(item, 'hover', true); 43 | } 44 | } 45 | graph.paint(); 46 | }, 47 | onMouseleave(e) { 48 | const self = this; 49 | const item = e.item; 50 | const graph = self.graph; 51 | const group = item.getContainer() 52 | group.find(g => { 53 | if (g._attrs.isInPoint || g._attrs.isOutPoint) { 54 | g.attr("fill", "#fff") 55 | } 56 | }); 57 | if (self.shouldUpdate.call(self, e)) { 58 | if(!item.hasState('selected')) 59 | graph.setItemState(item, 'hover', false); 60 | } 61 | graph.paint(); 62 | }, 63 | onMousedown(e) { 64 | if(e.target._attrs.isOutPoint ||e.target._attrs.isOutPointOut){ 65 | this.graph.setMode('addEdge') 66 | }else{ 67 | this.graph.setMode('moveNode') 68 | } 69 | }, 70 | 71 | }; 72 | -------------------------------------------------------------------------------- /src/behavior/hover-edge.js: -------------------------------------------------------------------------------- 1 | 2 | import Util from '@antv/g6/src/util' 3 | import eventBus from "@/utils/eventBus"; 4 | export default { 5 | getEvents() { 6 | return { 7 | 'edge:mouseover': 'onMouseover', 8 | 'edge:mouseleave': 'onMouseleave', 9 | "edge:click": "onClick", 10 | }; 11 | }, 12 | onMouseover(e) { 13 | const self = this; 14 | const item = e.item; 15 | const graph = self.graph; 16 | if (item.hasState('selected')) { 17 | return 18 | } else { 19 | if (self.shouldUpdate.call(self, e)) { 20 | graph.setItemState(item, 'hover', true); 21 | } 22 | } 23 | graph.paint(); 24 | }, 25 | onMouseleave(e) { 26 | const self = this; 27 | const item = e.item; 28 | const graph = self.graph; 29 | const group = item.getContainer() 30 | group.find(g => { 31 | if (g._attrs.isInPoint || g._attrs.isOutPoint) { 32 | g.attr("fill", "#fff") 33 | } 34 | }); 35 | if (self.shouldUpdate.call(self, e)) { 36 | if (!item.hasState('selected')) 37 | graph.setItemState(item, 'hover', false); 38 | } 39 | graph.paint(); 40 | }, 41 | onClick(e) { 42 | const self = this; 43 | const item = e.item; 44 | const graph = self.graph; 45 | const autoPaint = graph.get('autoPaint'); 46 | graph.setAutoPaint(false); 47 | const selectedNodes = graph.findAllByState('node', 'selected'); 48 | Util.each(selectedNodes, node => { 49 | graph.setItemState(node, 'selected', false); 50 | }); 51 | if (!self.keydown || !self.multiple) { 52 | const selected = graph.findAllByState('edge', 'selected'); 53 | Util.each(selected, edge => { 54 | if (edge !== item) { 55 | graph.setItemState(edge, 'selected', false); 56 | } 57 | }); 58 | } 59 | if (item.hasState('selected')) { 60 | if (self.shouldUpdate.call(self, e)) { 61 | graph.setItemState(item, 'selected', false); 62 | } 63 | eventBus.$emit('nodeselectchange', { target: item, select: false }); 64 | } else { 65 | if (self.shouldUpdate.call(self, e)) { 66 | graph.setItemState(item, 'selected', true); 67 | } 68 | eventBus.$emit('nodeselectchange', { target: item, select: true }); 69 | } 70 | graph.setAutoPaint(autoPaint); 71 | graph.paint(); 72 | }, 73 | 74 | }; 75 | -------------------------------------------------------------------------------- /src/behavior/mulit-select.js: -------------------------------------------------------------------------------- 1 | import Util from '@antv/g6/src/util' 2 | import eventBus from "@/utils/eventBus"; 3 | import { uniqueId,getBox } from '@/utils' 4 | import config from '../global' 5 | export default { 6 | getDefaultCfg() { 7 | return { 8 | }; 9 | }, 10 | getEvents() { 11 | return { 12 | 'canvas:mouseenter': 'onCanvasMouseenter', 13 | 'canvas:mousedown': 'onCanvasMousedown', 14 | mousemove: 'onMousemove', 15 | mouseup: 'onMouseup' 16 | }; 17 | }, 18 | onCanvasMouseenter() { 19 | // console.log(this.graph.get('canvas')); 20 | const canvas = document.getElementById('graph-container').children[0] 21 | canvas.style.cursor = 'crosshair' 22 | // this.graph.paint(); 23 | }, 24 | 25 | onCanvasMousedown(e) { 26 | const attrs = config.delegateStyle 27 | const width = 0, height = 0, x = e.x, y = e.y 28 | const parent = this.graph.get('group'); 29 | this.shape = parent.addShape('rect', { 30 | attrs: { 31 | id: 'rect' + uniqueId(), 32 | width, 33 | height, 34 | x, 35 | y, 36 | ...attrs 37 | } 38 | }) 39 | }, 40 | onMousemove(e) { 41 | if (this.shape) { 42 | const width = e.x - this.shape._attrs.x 43 | const height = e.y - this.shape._attrs.y 44 | this.shape.attr({ 45 | width, 46 | height 47 | }) 48 | this.graph.paint() 49 | } 50 | }, 51 | onMouseup() { 52 | const canvas = document.getElementById('graph-container').children[0] 53 | canvas.style.cursor = 'default' 54 | const selected = this.graph.findAllByState('node', 'selected'); 55 | Util.each(selected, node => { 56 | this.graph.setItemState(node, 'selected', false); 57 | eventBus.$emit('nodeselectchange', { target: node, select: false }); 58 | }); 59 | if (this.shape) { 60 | this.addTeam() 61 | this.shape.remove(); 62 | this.shape = null 63 | } 64 | this.graph.paint() 65 | eventBus.$emit('muliteSelectEnd') 66 | this.graph.setMode('default') 67 | }, 68 | addTeam() { 69 | const { x, y, width, height } = this.shape._attrs 70 | const { x1, y1, x2, y2 } = getBox(x, y, width, height) 71 | this.graph.findAll('node', node => { 72 | const { x: nodeX, y: nodeY, width: nodeWidth, height: nodeHeight } = node.getBBox() 73 | const nodeBox = getBox(nodeX, nodeY, nodeWidth, nodeHeight) 74 | if ((x2 >= nodeBox.x1 && nodeBox.x1 >= x1) && 75 | (x2 >= nodeBox.x2 && nodeBox.x2 >= x1) && 76 | (y2 >= nodeBox.y1 && nodeBox.y1 >= y1) && 77 | (y2 >= nodeBox.y2 && nodeBox.y2 >= y1)) { 78 | this.graph.setItemState(node, 'selected', true); 79 | } 80 | }) 81 | 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /src/behavior/select-node.js: -------------------------------------------------------------------------------- 1 | 2 | import Util from '@antv/g6/src/util' 3 | import eventBus from "@/utils/eventBus"; 4 | export default { 5 | getDefaultCfg() { 6 | return { 7 | multiple: true, 8 | keyCode: 16 9 | }; 10 | }, 11 | getEvents() { 12 | return { 13 | 'node:click': 'onClick', 14 | 'canvas:click': 'onCanvasClick', 15 | 'canvas:mouseover': 'onCanvasMouseover', 16 | keyup: 'onKeyUp', 17 | keydown: 'onKeyDown' 18 | }; 19 | }, 20 | onClick(e) { 21 | const self = this; 22 | const item = e.item; 23 | const graph = self.graph; 24 | const autoPaint = graph.get('autoPaint'); 25 | graph.setAutoPaint(false); 26 | const selectedEdges = graph.findAllByState('edge', 'selected'); 27 | Util.each(selectedEdges, edge => { 28 | graph.setItemState(edge, 'selected', false); 29 | }); 30 | if (!self.keydown || !self.multiple) { 31 | const selected = graph.findAllByState('node', 'selected'); 32 | Util.each(selected, node => { 33 | if (node !== item) { 34 | graph.setItemState(node, 'selected', false); 35 | } 36 | }); 37 | } 38 | if (item.hasState('selected')) { 39 | if (self.shouldUpdate.call(self, e)) { 40 | graph.setItemState(item, 'selected', false); 41 | } 42 | 43 | eventBus.$emit('nodeselectchange', { target: item, select: false }); 44 | } else { 45 | if (self.shouldUpdate.call(self, e)) { 46 | graph.setItemState(item, 'selected', true); 47 | } 48 | eventBus.$emit('nodeselectchange', { target: item, select: true }); 49 | } 50 | graph.setAutoPaint(autoPaint); 51 | graph.paint(); 52 | }, 53 | onCanvasClick() { 54 | const graph = this.graph; 55 | const autoPaint = graph.get('autoPaint'); 56 | graph.setAutoPaint(false); 57 | const selected = graph.findAllByState('node', 'selected'); 58 | Util.each(selected, node => { 59 | graph.setItemState(node, 'selected', false); 60 | eventBus.$emit('nodeselectchange', { target: node, select: false }); 61 | }); 62 | 63 | const selectedEdges = graph.findAllByState('edge', 'selected'); 64 | Util.each(selectedEdges, edge => { 65 | graph.setItemState(edge, 'selected', false); 66 | eventBus.$emit('nodeselectchange', { target: edge, select: false }); 67 | }) 68 | 69 | graph.paint(); 70 | graph.setAutoPaint(autoPaint); 71 | }, 72 | onCanvasMouseover() { 73 | const graph = this.graph; 74 | graph.paint(); 75 | }, 76 | onKeyDown(e) { 77 | const code = e.keyCode || e.which; 78 | if (code === this.keyCode) { 79 | this.keydown = true; 80 | } else { 81 | this.keydown = false; 82 | } 83 | }, 84 | onKeyUp() { 85 | this.keydown = false; 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /src/components/DetailPanel/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 100 | 101 | 130 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | 4 | 5 | function resolve(dir) { 6 | return path.join(__dirname, dir) 7 | } 8 | 9 | //const name = 'test' // page title 10 | // If your port is set to 80, 11 | // use administrator privileges to execute the command line. 12 | // For example, Mac: sudo npm run 13 | const port = 9528 // dev port 14 | 15 | // All configuration item explanations can be find in https://cli.vuejs.org/config/ 16 | module.exports = { 17 | 18 | /** 19 | * You will need to set publicPath if you plan to deploy your site under a sub path, 20 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/, 21 | * then publicPath should be set to "/bar/". 22 | * In most cases please use '/' !!! 23 | * Detail: https://cli.vuejs.org/config/#publicpath 24 | */ 25 | publicPath: '/', 26 | outputDir: 'dist', 27 | assetsDir: 'static', 28 | lintOnSave: process.env.NODE_ENV === 'development', 29 | productionSourceMap: false, 30 | devServer: { 31 | port: port, 32 | open: true, 33 | overlay: { 34 | warnings: false, 35 | errors: true 36 | }, 37 | proxy: null 38 | }, 39 | configureWebpack: { 40 | 41 | // provide the app's title in webpack's name field, so that 42 | // it can be accessed in index.html to inject the correct title. 43 | // name: name, 44 | resolve: { 45 | alias: { 46 | '@': resolve('src') 47 | } 48 | }, 49 | output: { 50 | libraryExport: 'default' 51 | } 52 | // externals: { 53 | // 'vue': 'Vue', 54 | // 'element-ui': 'ELEMENT', 55 | // }, 56 | }, 57 | chainWebpack(config) { 58 | config.plugins.delete('preload') // TODO: need test 59 | config.plugins.delete('prefetch') // TODO: need test 60 | // config 61 | // // 插件名 62 | // .plugin('extract-css') 63 | // // 修改规则 64 | // .tap(args => { 65 | // args[0].filename = 'css/styles.css' 66 | // args[0].chunkFilename = 'css/[name].css' 67 | // return args 68 | // }) 69 | // set svg-sprite-loader 70 | config.module 71 | .rule('svg') 72 | .exclude.add(resolve('src/icons')) 73 | .end() 74 | config.module 75 | .rule('icons') 76 | .test(/\.svg$/) 77 | .include.add(resolve('src/icons')) 78 | .end() 79 | .use('svg-sprite-loader') 80 | .loader('svg-sprite-loader') 81 | .options({ 82 | symbolId: 'icon-[name]' 83 | }) 84 | .end() 85 | // set preserveWhitespace 86 | config.module 87 | .rule('vue') 88 | .use('vue-loader') 89 | .loader('vue-loader') 90 | .tap(options => { 91 | options.compilerOptions.preserveWhitespace = true 92 | return options 93 | }) 94 | .end() 95 | 96 | config 97 | // https://webpack.js.org/configuration/devtool/#development 98 | .when(process.env.NODE_ENV === 'development', 99 | config => config.devtool('cheap-source-map') 100 | ) 101 | 102 | // config 103 | // .when(process.env.NODE_ENV !== 'development', 104 | // config => { 105 | // config 106 | // .plugin('ScriptExtHtmlWebpackPlugin') 107 | // .after('html') 108 | // .use('script-ext-html-webpack-plugin', [{ 109 | // // `runtime` must same as runtimeChunk name. default is `runtime` 110 | // inline: /runtime\..*\.js$/ 111 | // }]) 112 | // .end() 113 | // config 114 | // .optimization.splitChunks({ 115 | // chunks: 'all', 116 | // cacheGroups: { 117 | // libs: { 118 | // name: 'chunk-libs', 119 | // test: /[\\/]node_modules[\\/]/, 120 | // priority: 10, 121 | // chunks: 'initial' // only package third parties that are initially dependent 122 | // }, 123 | // elementUI: { 124 | // name: 'chunk-elementUI', // split elementUI into a single package 125 | // priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app 126 | // test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm 127 | // }, 128 | // commons: { 129 | // name: 'chunk-commons', 130 | // test: resolve('src/components'), // can customize your rules 131 | // minChunks: 3, // minimum common number 132 | // priority: 5, 133 | // reuseExistingChunk: true 134 | // } 135 | // } 136 | // }) 137 | // config.optimization.runtimeChunk('single') 138 | // } 139 | // ) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/command/index.js: -------------------------------------------------------------------------------- 1 | import { uniqueId } from '@/utils' 2 | class command { 3 | editor = null; 4 | undoList = [] 5 | redoList = [] 6 | constructor(editor) { 7 | this.editor = editor; 8 | } 9 | executeCommand(key, datas) { 10 | const list = [] 11 | datas.map(data => { 12 | let model = data 13 | if (key === 'add') { 14 | model.id = data.type + uniqueId() 15 | } 16 | if (key === 'delete') { 17 | if (data.getType() === 'node') { 18 | const edges = data.getEdges() 19 | model = data.getModel() 20 | model.type = data.getType() 21 | model.id = data.get('id') 22 | edges.forEach(edge => { 23 | let edgeModel = edge.getModel() 24 | edgeModel.type = 'edge' 25 | edgeModel.id = edge.get('id') 26 | list.push(edgeModel) 27 | }) 28 | } else if (data.getType() === 'edge') { 29 | model = data.getModel() 30 | model.type = data.getType() 31 | model.id = data.get('id') 32 | } 33 | } 34 | list.push(model) 35 | 36 | this.doCommand(key, model) 37 | 38 | }); 39 | this.undoList.push({ key, datas: list }) 40 | if(key==='delete'){ 41 | this.redoList =[] 42 | } 43 | this.editor.emit(key, { undoList: this.undoList, redoList: this.redoList }) 44 | } 45 | doCommand(key, data) { 46 | switch (key) { 47 | case 'add': 48 | this.add(data.type, data) 49 | break; 50 | case "update": 51 | this.update(data.item, data.newModel) 52 | break 53 | case "delete": 54 | this.remove(data) 55 | break 56 | } 57 | } 58 | add(type, item) { 59 | this.editor.add(type, item) 60 | } 61 | update(item, model) { 62 | this.editor.update(item, model) 63 | } 64 | remove(item) { 65 | this.editor.remove(item) 66 | } 67 | undo() { 68 | const undoData = this.undoList.pop() 69 | const edgeList = [] 70 | const list = [] 71 | for (let i = 0; i < undoData.datas.length; i++) { 72 | const data = undoData.datas[i] 73 | if (data.type === 'edge') { 74 | edgeList.push(data) 75 | continue 76 | } 77 | list.push(data) 78 | this.doundo(undoData.key, data) 79 | } 80 | for (let i = 0; i < edgeList.length; i++) { 81 | const edge = edgeList[i] 82 | if (edge.source.destroyed) { 83 | edge.source = edge.sourceId 84 | 85 | } 86 | if (edge.target.destroyed) { 87 | edge.target = edge.targetId 88 | } 89 | list.push(edge) 90 | this.doundo(undoData.key, edge) 91 | } 92 | this.redoList.push({ key: undoData.key, datas: list }) 93 | this.editor.emit(undoData.key, { undoList: this.undoList, redoList: this.redoList }) 94 | } 95 | doundo(key, data) { 96 | switch (key) { 97 | case 'add': 98 | this.remove(data) 99 | break; 100 | case "update": 101 | this.update(data.item, data.oldModel) 102 | break 103 | case "delete": 104 | this.add(data.type, data) 105 | break 106 | } 107 | } 108 | redo() { 109 | const redoData = this.redoList.pop() 110 | const list = [] 111 | const edgeList = [] 112 | for (let i = 0; i < redoData.datas.length; i++) { 113 | const data = redoData.datas[i] 114 | if (data.type === 'edge') { 115 | edgeList.push(data) 116 | continue 117 | } 118 | list.push(data) 119 | this.doredo(redoData.key, data) 120 | } 121 | for (let i = 0; i < edgeList.length; i++) { 122 | const edge = edgeList[i] 123 | if (edge.source.destroyed) { 124 | edge.source = edge.sourceId 125 | 126 | } 127 | if (edge.target.destroyed) { 128 | edge.target = edge.targetId 129 | } 130 | list.push(edge) 131 | this.doredo(redoData.key, edge) 132 | } 133 | this.undoList.push({ key: redoData.key, datas: list }) 134 | 135 | this.editor.emit(redoData.key, { undoList: this.undoList, redoList: this.redoList }) 136 | } 137 | doredo(key, data) { 138 | switch (key) { 139 | case 'add': 140 | this.add(data.type, data) 141 | break; 142 | case "update": 143 | this.update(data.item, data.newModel) 144 | break 145 | case "delete": 146 | this.remove(data) 147 | break 148 | } 149 | } 150 | delete(item) { 151 | this.executeCommand('delete', [item]) 152 | } 153 | } 154 | 155 | export default command; -------------------------------------------------------------------------------- /src/components/ItemPanel/item.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 162 | 163 | -------------------------------------------------------------------------------- /src/behavior/add-edge.js: -------------------------------------------------------------------------------- 1 | 2 | import eventBus from "@/utils/eventBus"; 3 | import { uniqueId } from '@/utils' 4 | let startPoint = null 5 | let startItem = null 6 | let endPoint = {} 7 | let activeItem = null 8 | let curInPoint = null 9 | export default { 10 | getEvents() { 11 | return { 12 | mousemove: 'onMousemove', 13 | mouseup: 'onMouseup', 14 | 'node:mouseover': 'onMouseover', 15 | 'node:mouseleave': 'onMouseleave' 16 | }; 17 | }, 18 | onMouseup(e) { 19 | const item = e.item 20 | if (item && item.getType() === 'node') { 21 | const group = item.getContainer() 22 | if (e.target._attrs.isInPoint) { 23 | const children = group._cfg.children 24 | children.map(child => { 25 | if (child._attrs.isInPointOut && child._attrs.parent === e.target._attrs.id) { 26 | activeItem = child 27 | } 28 | }) 29 | curInPoint = e.target 30 | } else if (e.target._attrs.isInPointOut) { 31 | activeItem = e.target 32 | const children = group._cfg.children 33 | children.map(child => { 34 | if (child._attrs.isInPoint && child._attrs.id === e.target._attrs.parent) { 35 | curInPoint = child 36 | } 37 | }) 38 | } 39 | if (activeItem) { 40 | const endX = parseInt(curInPoint._attrs.x) 41 | const endY = parseInt(curInPoint._attrs.y) 42 | endPoint = { x: endX, y: endY }; 43 | if (this.edge) { 44 | this.graph.removeItem(this.edge); 45 | const model = { 46 | id: 'edge' + uniqueId(), 47 | source: startItem, 48 | target: item, 49 | sourceId: startItem._cfg.id, 50 | targetId: item._cfg.id, 51 | start: startPoint, 52 | end: endPoint, 53 | shape: 'customEdge', 54 | type: 'edge' 55 | } 56 | eventBus.$emit('addItem', model) 57 | } 58 | } else { 59 | if (this.edge) 60 | this.graph.removeItem(this.edge); 61 | } 62 | } else { 63 | if (this.edge) 64 | this.graph.removeItem(this.edge); 65 | } 66 | this.graph.find("node", node => { 67 | const group = node.get('group') 68 | const children = group._cfg.children 69 | children.map(child => { 70 | if (child._attrs.isInPointOut) { 71 | child.attr("opacity", "0") 72 | } 73 | if (child._attrs.isInPoint) { 74 | child.attr("opacity", "0") 75 | } 76 | if (child._attrs.isOutPoint) { 77 | child.attr("opacity", "0") 78 | child.attr("fill", "#fff") 79 | } 80 | }) 81 | }) 82 | if (startItem) { 83 | this.graph.setItemState(startItem, 'hover', false); 84 | } 85 | 86 | this.graph.paint() 87 | startPoint = null 88 | startItem = null 89 | endPoint = {} 90 | activeItem = null 91 | curInPoint = null 92 | this.graph.setMode('default') 93 | }, 94 | onMousemove(e) { 95 | const item = e.item 96 | if (!startPoint) { 97 | this.graph.find("node", node => { 98 | const group = node.get('group') 99 | const children = group._cfg.children 100 | children.map(child => { 101 | if (child._attrs.isInPointOut) { 102 | child.attr("opacity", "0.3") 103 | } 104 | if (child._attrs.isInPoint) { 105 | child.attr("opacity", "1") 106 | } 107 | }) 108 | }) 109 | const startX = parseInt(e.target._attrs.x) 110 | const startY = parseInt(e.target._attrs.y) 111 | startPoint = { x: startX, y: startY }; 112 | startItem = item 113 | this.edge = this.graph.addItem('edge', { 114 | source: item, 115 | target: item, 116 | start: startPoint, 117 | end: startPoint, 118 | shape: 'link-edge' 119 | }); 120 | } else { 121 | const point = { x: e.x, y: e.y }; 122 | if (this.edge) { 123 | // 增加边的过程中,移动时边跟着移动 124 | this.graph.updateItem(this.edge, { 125 | // start: startPoint, 126 | target: point 127 | }); 128 | } 129 | } 130 | }, 131 | onMouseover(e) { 132 | const item = e.item 133 | if (item && item.getType() === 'node') { 134 | if (e.target._attrs.isInPointOut && !this.hasTran) { 135 | this.hasTran = true 136 | e.target.transform([ 137 | ['t', 0, 3], 138 | ['s', 1.2, 1.2], 139 | ]) 140 | } 141 | this.graph.paint() 142 | } 143 | }, 144 | onMouseleave() { 145 | this.graph.find("node", node => { 146 | const group = node.get('group') 147 | const children = group._cfg.children 148 | children.map(child => { 149 | if (child._attrs.isInPointOut) { 150 | child.resetMatrix() 151 | } 152 | }) 153 | }) 154 | this.hasTran = false 155 | this.graph.paint() 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/components/Flow/customEdge.js: -------------------------------------------------------------------------------- 1 | import G6 from "@antv/g6/build/g6"; 2 | import { uniqueId } from '@/utils' 3 | const MIN_ARROW_SIZE = 3 4 | 5 | const customEdge = { 6 | init() { 7 | const dashArray = [ 8 | [0, 1], 9 | [0, 2], 10 | [1, 2], 11 | [0, 1, 1, 2], 12 | [0, 2, 1, 2], 13 | [1, 2, 1, 2], 14 | [2, 2, 1, 2], 15 | [3, 2, 1, 2], 16 | [4, 2, 1, 2] 17 | ]; 18 | 19 | const lineDash = [4,2,1,2]; 20 | const interval = 9; 21 | G6.registerEdge('customEdge', { 22 | draw(cfg, group) { 23 | let sourceNode, targetNode, start, end 24 | if (typeof (cfg.source) === 'string') { 25 | cfg.source = cfg.sourceNode 26 | } 27 | if(!cfg.start){ 28 | cfg.start={ 29 | x:0, 30 | y:17 31 | } 32 | } 33 | if(!cfg.end){ 34 | cfg.end={ 35 | x:0, 36 | y:-17 37 | } 38 | } 39 | if (!cfg.source.x) { 40 | sourceNode = cfg.source.getModel() 41 | start = { x: sourceNode.x + cfg.start.x, y: sourceNode.y + cfg.start.y } 42 | } else { 43 | start = cfg.source 44 | } 45 | if (typeof (cfg.target) === 'string') { 46 | cfg.target = cfg.targetNode 47 | } 48 | if (!cfg.target.x) { 49 | 50 | targetNode = cfg.target.getModel() 51 | end = { x: targetNode.x + cfg.end.x, y: targetNode.y + cfg.end.y } 52 | } else { 53 | end = cfg.target 54 | } 55 | 56 | let path = [] 57 | let hgap = Math.abs(end.x - start.x) 58 | if (end.x > start.x) { 59 | path = [ 60 | ['M', start.x, start.y], 61 | [ 62 | 'C', 63 | start.x, 64 | start.y + hgap / (hgap / 50), 65 | end.x, 66 | end.y - hgap / (hgap / 50), 67 | end.x, 68 | end.y - 4 69 | ], 70 | [ 71 | 'L', 72 | end.x, 73 | end.y 74 | ] 75 | ] 76 | } else { 77 | path = [ 78 | ['M', start.x, start.y], 79 | [ 80 | 'C', 81 | start.x, 82 | start.y + hgap / (hgap / 50), 83 | end.x, 84 | end.y - hgap / (hgap / 50), 85 | end.x, 86 | end.y - 4 87 | ], 88 | [ 89 | 'L', 90 | end.x, 91 | end.y 92 | ] 93 | ] 94 | } 95 | let lineWidth = 1; 96 | lineWidth = lineWidth > MIN_ARROW_SIZE ? lineWidth : MIN_ARROW_SIZE; 97 | const width = lineWidth * 10 / 3; 98 | const halfHeight = lineWidth * 4 / 3; 99 | const radius = lineWidth * 4; 100 | const endArrowPath = [ 101 | ['M', -width, halfHeight], 102 | ['L', 0, 0], 103 | ['L', -width, -halfHeight], 104 | ['A', radius, radius, 0, 0, 1, -width, halfHeight], 105 | ['Z'] 106 | ]; 107 | const keyShape = group.addShape('path', { 108 | attrs: { 109 | id: 'edge' + uniqueId(), 110 | path: path, 111 | stroke: '#b8c3ce', 112 | lineAppendWidth: 10, 113 | endArrow: { 114 | path: endArrowPath, 115 | } 116 | } 117 | }); 118 | return keyShape 119 | }, 120 | afterDraw(cfg, group) { 121 | if (cfg.source.getModel().isDoingStart && cfg.target.getModel().isDoingEnd) { 122 | const shape = group.get('children')[0]; 123 | const length = shape.getTotalLength(); // G 增加了 totalLength 的接口 124 | let totalArray = []; 125 | for (var i = 0; i < length; i += interval) { 126 | totalArray = totalArray.concat(lineDash); 127 | } 128 | let index = 0; 129 | shape.animate({ 130 | onFrame() { 131 | const cfg = { 132 | lineDash: dashArray[index].concat(totalArray) 133 | }; 134 | index = (index + 1) % interval; 135 | return cfg; 136 | }, 137 | repeat: true 138 | }, 3000); 139 | } 140 | }, 141 | setState(name, value, item) { 142 | const group = item.getContainer(); 143 | const shape = group.get("children")[0]; 144 | const selectStyles = () => { 145 | shape.attr("stroke", "#6ab7ff"); 146 | }; 147 | const unSelectStyles = () => { 148 | shape.attr("stroke", "#b8c3ce"); 149 | }; 150 | 151 | switch (name) { 152 | case "selected": 153 | case "hover": 154 | if (value) { 155 | selectStyles() 156 | } else { 157 | unSelectStyles() 158 | } 159 | break; 160 | } 161 | } 162 | }); 163 | G6.registerEdge('link-edge', { 164 | draw(cfg, group) { 165 | let sourceNode, targetNode, start, end 166 | if (!cfg.source.x) { 167 | sourceNode = cfg.source.getModel() 168 | start = { x: sourceNode.x + cfg.start.x, y: sourceNode.y + cfg.start.y } 169 | } else { 170 | start = cfg.source 171 | } 172 | if (!cfg.target.x) { 173 | targetNode = cfg.target.getModel() 174 | end = { x: targetNode.x + cfg.end.x, y: targetNode.y + cfg.end.y } 175 | } else { 176 | end = cfg.target 177 | } 178 | 179 | let path = [] 180 | path = [ 181 | ['M', start.x, start.y], 182 | ['L', end.x, end.y] 183 | ] 184 | const keyShape = group.addShape('path', { 185 | attrs: { 186 | id: 'edge' + uniqueId(), 187 | path: path, 188 | stroke: '#1890FF', 189 | strokeOpacity: 0.9, 190 | lineDash: [5, 5] 191 | } 192 | }); 193 | return keyShape 194 | }, 195 | }); 196 | } 197 | } 198 | 199 | export default customEdge 200 | -------------------------------------------------------------------------------- /src/components/Flow/customNode.js: -------------------------------------------------------------------------------- 1 | import G6 from "@antv/g6/build/g6"; 2 | import { uniqueId } from '@/utils' 3 | import Shape from '@antv/g/src/shapes' 4 | const customNode = { 5 | init() { 6 | G6.registerNode("customNode", { 7 | draw(cfg, group) { 8 | let size = cfg.size; 9 | if(!size){ 10 | size=[170,34] 11 | } 12 | // 此处必须是NUMBER 不然bbox不正常 13 | const width = parseInt(size[0]); 14 | const height = parseInt(size[1]); 15 | const color = cfg.color; 16 | // 此处必须有偏移 不然drag-node错位 17 | const offsetX = -width / 2 18 | const offsetY = -height / 2 19 | const mainId = 'rect' + uniqueId() 20 | const shape = group.addShape("rect", { 21 | attrs: { 22 | id: mainId, 23 | x: offsetX, 24 | y: offsetY, 25 | width: width, 26 | height: height, 27 | stroke: "#ced4d9", 28 | fill: '#fff',//此处必须有fill 不然不能触发事件 29 | radius: 4 30 | } 31 | }); 32 | group.addShape("rect", { 33 | attrs: { 34 | x: offsetX, 35 | y: offsetY, 36 | width: 4, 37 | height: height, 38 | fill: color, 39 | parent: mainId, 40 | radius: [4, 0, 0, 4] 41 | } 42 | }); 43 | group.addShape("image", { 44 | attrs: { 45 | x: offsetX + 16, 46 | y: offsetY + 8, 47 | width: 20, 48 | height: 16, 49 | img: cfg.image, 50 | parent: mainId 51 | } 52 | }); 53 | group.addShape("image", { 54 | attrs: { 55 | x: offsetX + width - 32, 56 | y: offsetY + 8, 57 | width: 16, 58 | height: 16, 59 | parent: mainId, 60 | img: cfg.stateImage 61 | } 62 | }); 63 | if(cfg.backImage){ 64 | const clip = new Shape.Rect({ 65 | attrs: { 66 | x: offsetX, 67 | y: offsetY, 68 | width: width, 69 | height: height, 70 | fill:'#fff', 71 | radius: 4 72 | } 73 | }); 74 | group.addShape("image", { 75 | attrs: { 76 | x: offsetX, 77 | y: offsetY, 78 | width: width, 79 | height: height, 80 | img: cfg.backImage, 81 | clip: clip 82 | } 83 | }); 84 | } 85 | if (cfg.label) { 86 | group.addShape("text", { 87 | attrs: { 88 | id: 'label' + uniqueId(), 89 | x: offsetX + width / 2, 90 | y: offsetY + height / 2, 91 | textAlign: "center", 92 | textBaseline: "middle", 93 | text: cfg.label, 94 | parent: mainId, 95 | fill: "#565758" 96 | } 97 | }); 98 | } 99 | if (cfg.inPoints) { 100 | for (let i = 0; i < cfg.inPoints.length; i++) { 101 | let x, 102 | y = 0; 103 | //0为顶 1为底 104 | if (cfg.inPoints[i][0] === 0) { 105 | y = 0; 106 | } else { 107 | y = height; 108 | } 109 | x = width * cfg.inPoints[i][1]; 110 | const id = 'circle' + uniqueId() 111 | group.addShape("circle", { 112 | attrs: { 113 | id: 'circle' + uniqueId(), 114 | parent: id, 115 | x: x + offsetX, 116 | y: y + offsetY, 117 | r: 10, 118 | isInPointOut: true, 119 | fill: "#1890ff", 120 | opacity: 0 121 | } 122 | }); 123 | group.addShape("circle", { 124 | attrs: { 125 | id: id, 126 | x: x + offsetX, 127 | y: y + offsetY, 128 | r: 3, 129 | isInPoint: true, 130 | fill: "#fff", 131 | stroke: "#1890ff", 132 | opacity: 0 133 | } 134 | }); 135 | } 136 | } 137 | if (cfg.outPoints) { 138 | for (let i = 0; i < cfg.outPoints.length; i++) { 139 | let x, 140 | y = 0; 141 | //0为顶 1为底 142 | if (cfg.outPoints[i][0] === 0) { 143 | y = 0; 144 | } else { 145 | y = height; 146 | } 147 | x = width * cfg.outPoints[i][1]; 148 | const id = 'circle' + uniqueId() 149 | group.addShape("circle", { 150 | attrs: { 151 | id: 'circle' + uniqueId(), 152 | parent: id, 153 | x: x + offsetX, 154 | y: y + offsetY, 155 | r: 10, 156 | isOutPointOut: true, 157 | fill: "#1890ff", 158 | opacity: 0//默認0 需要時改成0.3 159 | } 160 | }); 161 | group.addShape("circle", { 162 | attrs: { 163 | id: id, 164 | x: x + offsetX, 165 | y: y + offsetY, 166 | r: 3, 167 | isOutPoint: true, 168 | fill: "#fff", 169 | stroke: "#1890ff", 170 | opacity: 0 171 | } 172 | }); 173 | } 174 | } 175 | //group.sort() 176 | // 添加文本、更多图形 177 | return shape; 178 | }, 179 | //设置状态 180 | setState(name, value, item) { 181 | const group = item.getContainer(); 182 | const shape = group.get("children")[0]; // 顺序根据 draw 时确定 183 | 184 | const children = group.findAll(g => { 185 | return g._attrs.parent === shape._attrs.id 186 | }); 187 | const circles = group.findAll(circle => { 188 | return circle._attrs.isInPoint || circle._attrs.isOutPoint; 189 | }); 190 | const selectStyles = () => { 191 | shape.attr("fill", "#f3f9ff"); 192 | shape.attr("stroke", "#6ab7ff"); 193 | shape.attr("cursor", "move"); 194 | children.forEach(child => { 195 | child.attr("cursor", "move"); 196 | }); 197 | circles.forEach(circle => { 198 | circle.attr('opacity', 1) 199 | }) 200 | }; 201 | const unSelectStyles = () => { 202 | shape.attr("fill", "#fff"); 203 | shape.attr("stroke", "#ced4d9"); 204 | circles.forEach(circle => { 205 | circle.attr('opacity', 0) 206 | }) 207 | }; 208 | switch (name) { 209 | case "selected": 210 | case "hover": 211 | if (value) { 212 | selectStyles() 213 | } else { 214 | unSelectStyles() 215 | } 216 | break; 217 | } 218 | } 219 | }); 220 | } 221 | } 222 | 223 | export default customNode 224 | -------------------------------------------------------------------------------- /src/behavior/drag-item.js: -------------------------------------------------------------------------------- 1 | import { merge, isString } from 'lodash'; 2 | import eventBus from "@/utils/eventBus"; 3 | const delegateStyle = { 4 | fill: '#F3F9FF', 5 | fillOpacity: 0.5, 6 | stroke: '#1890FF', 7 | strokeOpacity: 0.9, 8 | lineDash: [5, 5] 9 | } 10 | const body = document.body; 11 | 12 | export default { 13 | isDrag: false, 14 | nodeEvent: null, 15 | getDefaultCfg() { 16 | return { 17 | updateEdge: true, 18 | delegate: true, 19 | delegateStyle: {} 20 | }; 21 | }, 22 | getEvents() { 23 | return { 24 | 'node:mousedown': 'onMousedown', 25 | 'mousemove': 'onMousemove', 26 | 'mouseup': 'onMouseup', 27 | // 'node:dragstart': 'onDragStart', 28 | // 'node:drag': 'onDrag', 29 | // 'node:dragend': 'onDragEnd', 30 | 'canvas:mouseleave': 'onOutOfRange' 31 | }; 32 | }, 33 | getNode(e) { 34 | if (!this.shouldBegin.call(this, e)) { 35 | return; 36 | } 37 | this.isDrag = true 38 | this.nodeEvent = e 39 | const { item } = e; 40 | const graph = this.graph; 41 | 42 | this.targets = []; 43 | 44 | // 获取所有选中的元素 45 | const nodes = graph.findAllByState('node', 'selected'); 46 | 47 | const currentNodeId = item.get('id'); 48 | 49 | // 当前拖动的节点是否是选中的节点 50 | const dragNodes = nodes.filter(node => { 51 | const nodeId = node.get('id'); 52 | return currentNodeId === nodeId; 53 | }); 54 | 55 | // 只拖动当前节点 56 | if (dragNodes.length === 0) { 57 | this.target = item; 58 | } else { 59 | // 拖动多个节点 60 | if (nodes.length > 1) { 61 | nodes.forEach(node => { 62 | this.targets.push(node); 63 | }); 64 | } else { 65 | this.targets.push(item); 66 | } 67 | } 68 | 69 | this.origin = { 70 | x: e.x, 71 | y: e.y 72 | }; 73 | 74 | this.point = {}; 75 | this.originPoint = {}; 76 | }, 77 | onMousemove(e) { 78 | 79 | if (!this.origin) { 80 | this.getNode(e) 81 | } 82 | if(!this.isDrag){ 83 | return 84 | } 85 | if (!this.get('shouldUpdate').call(this, e)) { 86 | return; 87 | } 88 | // 当targets中元素时,则说明拖动的是多个选中的元素 89 | if (this.targets&&this.targets.length > 0) { 90 | this._updateDelegate(e, this.nodeEvent); 91 | } else { 92 | // 只拖动单个元素 93 | this._update(this.target, e, this.nodeEvent, true); 94 | } 95 | }, 96 | onMouseup(e) { 97 | if (this.shape) { 98 | this.shape.remove(); 99 | this.shape = null; 100 | } 101 | 102 | if (this.target) { 103 | const delegateShape = this.target.get('delegateShape'); 104 | if (delegateShape) { 105 | delegateShape.remove(); 106 | this.target.set('delegateShape', null); 107 | } 108 | } 109 | 110 | if (this.targets&&this.targets.length > 0) { 111 | // 获取所有已经选中的节点 112 | this.targets.forEach(node => this._update(node, e)); 113 | } else if (this.target) { 114 | // this._update(this.target, e); 115 | const origin = this.origin; 116 | const model = this.target.get('model'); 117 | const nodeId = this.target.get('id'); 118 | if (!this.point[nodeId]) { 119 | this.point[nodeId] = { 120 | x: model.x, 121 | y: model.y 122 | }; 123 | } 124 | 125 | const x = e.x - origin.x + this.point[nodeId].x; 126 | const y = e.y - origin.y + this.point[nodeId].y; 127 | const data = {} 128 | data.item = this.target 129 | data.oldModel = this.origin 130 | data.newModel = { x, y } 131 | eventBus.$emit('updateItem', data) 132 | } 133 | this.point = {}; 134 | this.origin = null; 135 | this.originPoint = {}; 136 | if(this.targets)this.targets.length = 0; 137 | this.target = null; 138 | // 终止时需要判断此时是否在监听画布外的 mouseup 事件,若有则解绑 139 | const fn = this.fn; 140 | if (fn) { 141 | body.removeEventListener('mouseup', fn, false); 142 | this.fn = null; 143 | } 144 | this.isDrag = false 145 | this.nodeEvent = null 146 | this.graph.setMode('default') 147 | }, 148 | // 若在拖拽时,鼠标移出画布区域,此时放开鼠标无法终止 drag 行为。在画布外监听 mouseup 事件,放开则终止 149 | onOutOfRange(e) { 150 | const self = this; 151 | if (this.origin) { 152 | const canvasElement = self.graph.get('canvas').get('el'); 153 | const fn = ev => { 154 | if (ev.target !== canvasElement) { 155 | self.onDragEnd(e); 156 | } 157 | }; 158 | this.fn = fn; 159 | body.addEventListener('mouseup', fn, false); 160 | } 161 | }, 162 | _update(item, e, nodeEvent, force) { 163 | const origin = this.origin; 164 | const model = item.get('model'); 165 | const nodeId = item.get('id'); 166 | if (!this.point[nodeId]) { 167 | this.point[nodeId] = { 168 | x: model.x, 169 | y: model.y 170 | }; 171 | } 172 | 173 | const x = e.x - origin.x + this.point[nodeId].x; 174 | const y = e.y - origin.y + this.point[nodeId].y; 175 | // 拖动单个未选中元素 176 | if (force) { 177 | this._updateDelegate(e, nodeEvent, x, y); 178 | return; 179 | } 180 | 181 | const pos = { x, y }; 182 | 183 | if (this.get('updateEdge')) { 184 | this.graph.updateItem(item, pos); 185 | } else { 186 | item.updatePosition(pos); 187 | this.graph.paint(); 188 | } 189 | }, 190 | 191 | /** 192 | * 更新拖动元素时的delegate 193 | * @param {Event} e 事件句柄 194 | * @param {number} x 拖动单个元素时候的x坐标 195 | * @param {number} y 拖动单个元素时候的y坐标 196 | */ 197 | _updateDelegate(e, nodeEvent, x, y) { 198 | const bbox = nodeEvent.item.get('keyShape').getBBox(); 199 | if (!this.shape) { 200 | // 拖动多个 201 | const parent = this.graph.get('group'); 202 | const attrs = merge({}, delegateStyle, this.delegateStyle); 203 | if (this.targets.length > 0) { 204 | const { x, y, width, height, minX, minY } = this.calculationGroupPosition(); 205 | this.originPoint = { x, y, width, height, minX, minY }; 206 | // model上的x, y是相对于图形中心的,delegateShape是g实例,x,y是绝对坐标 207 | this.shape = parent.addShape('rect', { 208 | attrs: { 209 | width, 210 | height, 211 | x, 212 | y, 213 | ...attrs 214 | } 215 | }); 216 | } else if (this.target) { 217 | this.shape = parent.addShape('rect', { 218 | attrs: { 219 | width: bbox.width, 220 | height: bbox.height, 221 | x: x - bbox.width / 2, 222 | y: y - bbox.height / 2, 223 | ...attrs 224 | } 225 | }); 226 | this.target.set('delegateShape', this.shape); 227 | } 228 | this.shape.set('capture', false); 229 | } 230 | 231 | if (this.targets.length > 0) { 232 | const clientX = e.x - this.origin.x + this.originPoint.minX; 233 | const clientY = e.y - this.origin.y + this.originPoint.minY; 234 | this.shape.attr({ 235 | x: clientX, 236 | y: clientY 237 | }); 238 | } else if (this.target) { 239 | this.shape.attr({ 240 | x: x - bbox.width / 2, 241 | y: y - bbox.height / 2 242 | }); 243 | } 244 | this.graph.paint(); 245 | }, 246 | /** 247 | * 计算delegate位置,包括左上角左边及宽度和高度 248 | * @memberof ItemGroup 249 | * @return {object} 计算出来的delegate坐标信息及宽高 250 | */ 251 | calculationGroupPosition() { 252 | const graph = this.graph; 253 | 254 | const nodes = graph.findAllByState('node', 'selected'); 255 | const minx = []; 256 | const maxx = []; 257 | const miny = []; 258 | const maxy = []; 259 | 260 | // 获取已节点的所有最大最小x y值 261 | for (const id of nodes) { 262 | const element = isString(id) ? graph.findById(id) : id; 263 | const bbox = element.getBBox(); 264 | const { minX, minY, maxX, maxY } = bbox; 265 | minx.push(minX); 266 | miny.push(minY); 267 | maxx.push(maxX); 268 | maxy.push(maxY); 269 | } 270 | 271 | // 从上一步获取的数组中,筛选出最小和最大值 272 | const minX = Math.floor(Math.min(...minx)); 273 | const maxX = Math.floor(Math.max(...maxx)); 274 | const minY = Math.floor(Math.min(...miny)); 275 | const maxY = Math.floor(Math.max(...maxy)); 276 | 277 | const x = minX - 20; 278 | const y = minY + 10; 279 | const width = maxX - minX; 280 | const height = maxY - minY; 281 | 282 | return { 283 | x, 284 | y, 285 | width, 286 | height, 287 | minX, 288 | minY 289 | }; 290 | } 291 | }; 292 | -------------------------------------------------------------------------------- /src/components/Toolbar/index.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 306 | 307 | 308 | --------------------------------------------------------------------------------