├── 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 | 
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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
12 |
13 |
14 |
54 |
55 |
75 |
--------------------------------------------------------------------------------
/src/components/ContextMenu/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
47 |
48 |
--------------------------------------------------------------------------------
/src/components/Page/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
80 |
81 |
--------------------------------------------------------------------------------
/src/components/G6Editor/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
2 |
3 |
4 |
5 |
模型详情
6 |
7 |
8 | 名称
9 |
10 |
11 |
12 | 任意属性
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
画布
21 |
22 | 网格对齐
23 |
24 |
25 |
39 |
40 |
41 |
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 |
2 |
3 | -
14 |
15 | {{item.name}}
16 |
17 |
18 |
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 |
2 |
3 |
8 |
14 |
20 |
21 |
23 |
30 |
31 |
37 |
43 |
49 |
55 |
56 |
63 |
70 |
71 |
72 |
79 |
86 |
87 | 控制台输出数据
88 |
89 |
90 |
91 |
306 |
307 |
308 |
--------------------------------------------------------------------------------