├── src ├── views │ ├── flowChart │ │ ├── Editor │ │ │ ├── utils │ │ │ │ ├── eventBus.js │ │ │ │ ├── customStack.js │ │ │ │ └── commonCaculate.js │ │ │ ├── behavior │ │ │ │ ├── index.js │ │ │ │ ├── add-edge.js │ │ │ │ └── edit-control.js │ │ │ ├── custom │ │ │ │ ├── edge │ │ │ │ │ ├── customLinkEdge.js │ │ │ │ │ ├── polylineFinding.js │ │ │ │ │ └── customPolyEdge.js │ │ │ │ ├── index.js │ │ │ │ └── node │ │ │ │ │ ├── customEndNode.js │ │ │ │ │ ├── customStartNode.js │ │ │ │ │ ├── customBranchNode.js │ │ │ │ │ ├── customParallelNode.js │ │ │ │ │ └── customToolNode.js │ │ │ ├── config │ │ │ │ └── commonConfig.js │ │ │ └── containers │ │ │ │ ├── PanelLeft.vue │ │ │ │ ├── ContextMenu.vue │ │ │ │ └── ToolBar.vue │ │ ├── assets │ │ │ └── images │ │ │ │ ├── end.png │ │ │ │ ├── branch.png │ │ │ │ ├── start.png │ │ │ │ ├── tool.png │ │ │ │ └── parallel.png │ │ └── DemoFroTest.vue │ ├── treetopo │ │ ├── index.vue │ │ └── component │ │ │ └── subtopo.vue │ └── topo │ │ ├── component │ │ └── subTopo.vue │ │ └── index.vue ├── assets │ ├── 1.png │ ├── 3.png │ ├── 10.png │ ├── 15.png │ ├── logo.png │ ├── defend.png │ ├── database.png │ ├── service.png │ └── wancheng.png ├── main.js ├── router │ └── router.js ├── App.vue └── jsondata │ ├── subdata.js │ ├── data.js │ ├── treetopo.js │ └── togo.js ├── babel.config.js ├── public ├── favicon.ico └── index.html ├── vue.config.js ├── .gitignore ├── .github └── workflows │ └── cl.yml └── package.json /src/views/flowChart/Editor/utils/eventBus.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | export default new Vue(); -------------------------------------------------------------------------------- /src/assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/assets/1.png -------------------------------------------------------------------------------- /src/assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/assets/3.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/assets/10.png -------------------------------------------------------------------------------- /src/assets/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/assets/15.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/defend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/assets/defend.png -------------------------------------------------------------------------------- /src/assets/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/assets/database.png -------------------------------------------------------------------------------- /src/assets/service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/assets/service.png -------------------------------------------------------------------------------- /src/assets/wancheng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/assets/wancheng.png -------------------------------------------------------------------------------- /src/views/flowChart/assets/images/end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/views/flowChart/assets/images/end.png -------------------------------------------------------------------------------- /src/views/flowChart/assets/images/branch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/views/flowChart/assets/images/branch.png -------------------------------------------------------------------------------- /src/views/flowChart/assets/images/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/views/flowChart/assets/images/start.png -------------------------------------------------------------------------------- /src/views/flowChart/assets/images/tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/views/flowChart/assets/images/tool.png -------------------------------------------------------------------------------- /src/views/flowChart/assets/images/parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jason-chen-coder/D3-EasyFlowRender/HEAD/src/views/flowChart/assets/images/parallel.png -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: process.env.NODE_ENV === 'production' 3 | ? '/D3-EasyFlowRender/' 4 | : '/', 5 | outputDir: 'dist', 6 | lintOnSave: true, 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /.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 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router/router' 4 | // 导入element插件 5 | import ElementUI from 'element-ui'; 6 | import 'element-ui/lib/theme-chalk/index.css'; 7 | Vue.config.productionTip = false 8 | Vue.use(ElementUI); 9 | 10 | 11 | new Vue({ 12 | router, 13 | render: h => h(App), 14 | }).$mount('#app') 15 | -------------------------------------------------------------------------------- /src/router/router.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import VueRouter from "vue-router" 3 | import topoindex from "../views/topo/index.vue" 4 | import treetopo from "../views/treetopo/index.vue" 5 | Vue.use(VueRouter) 6 | export default new VueRouter({ 7 | routes: [ 8 | { 9 | path: '/topoindex', 10 | name: "topoindex", 11 | component: topoindex 12 | }, 13 | { 14 | path: '/treetopo', 15 | name: "treetopo", 16 | component: treetopo 17 | }, 18 | ] 19 | }) -------------------------------------------------------------------------------- /src/views/flowChart/Editor/behavior/index.js: -------------------------------------------------------------------------------- 1 | import G6 from "@antv/g6/build/g6"; 2 | 3 | import addLine from './add-edge'; 4 | import editControl from './edit-control'; 5 | 6 | const behavors = { 7 | 8 | 'edit-control': editControl, 9 | 'add-edge': addLine, 10 | }; 11 | 12 | export function initBehavors() { 13 | for (let key in behavors) { 14 | G6.registerBehavior(key, behavors[key]) 15 | } 16 | } 17 | export function clearTempState(graph) { 18 | for (let key in behavors) { 19 | behavors[key].clearTempState(graph); 20 | } 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/utils/customStack.js: -------------------------------------------------------------------------------- 1 | const items = new WeakMap(); 2 | 3 | export default class Stack { 4 | constructor () { 5 | items.set(this, []); 6 | } 7 | 8 | push(element) { 9 | let s = items.get(this); 10 | s.push(element); 11 | } 12 | 13 | pop() { 14 | let s = items.get(this); 15 | return s.pop(); 16 | } 17 | 18 | peek() { 19 | let s = items.get(this); 20 | return s[s.length - 1]; 21 | } 22 | 23 | isEmpty() { 24 | return items.get(this).length === 0; 25 | } 26 | 27 | size() { 28 | return items.get(this).length; 29 | } 30 | 31 | clear() { 32 | items.set(this, []); 33 | } 34 | 35 | print() { 36 | console.log(items.get(this).toString()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/custom/edge/customLinkEdge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 修改自 https://github.com/caoyu48/vue-g6-editor 3 | * 用于连线过程中的虚线 4 | */ 5 | 6 | import G6 from "@antv/g6/build/g6"; 7 | 8 | const customLinkEdge = { 9 | init(editor) { 10 | G6.registerEdge('customLinkEdge', { 11 | draw(cfg, group) { 12 | let path = [] 13 | path = [ 14 | ['M', cfg.source.x, cfg.source.y], 15 | ['L', cfg.target.x, cfg.target.y] 16 | ] 17 | const keyShape = group.addShape('path', { 18 | attrs: { 19 | path: path, 20 | stroke: '#1890FF', 21 | strokeOpacity: 0.9, 22 | lineDash: [5, 5] 23 | } 24 | }); 25 | return keyShape 26 | }, 27 | }); 28 | } 29 | } 30 | 31 | export default customLinkEdge 32 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/custom/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 注册自定义节点和边 3 | */ 4 | 5 | import customStartNode from './node/customStartNode' 6 | import customEndNode from './node/customEndNode' 7 | 8 | import customParallelNode from './node/customParallelNode'//并行网关 9 | import customBranchlNode from './node/customBranchNode'//分支网关mpo 10 | import customToolNode from './node/customToolNode'//工具 11 | 12 | import customPolyEdge from './edge/customPolyEdge' 13 | import customLinkEdge from './edge/customLinkEdge' 14 | 15 | const obj = { 16 | 17 | customStartNode, 18 | customEndNode, 19 | customParallelNode, 20 | customBranchlNode, 21 | customToolNode, 22 | customPolyEdge, 23 | customLinkEdge, 24 | } 25 | 26 | export default function (editor) { 27 | Object.values(obj).map(item => { 28 | item.init(editor); 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/config/commonConfig.js: -------------------------------------------------------------------------------- 1 | export default { 2 | node :{ 3 | anchorPoints:[[0.5, 0],[0.5, 1],[0, 0.5],[1, 0.5]],//上,下,左,右 4 | anchorR:5, 5 | defaultDeleteR:8, 6 | 7 | anchorColor_blue:'#3688ed',//锚点颜色 8 | anchorColor_grey:'#e1e4e8',//锚点颜色 9 | anchorBiggerOpacity:'0.3',//锚点外面大的圈圈的透明度 10 | 11 | strokeColor_grey:'#b5b9c0',//边的灰色 12 | strokeColor_blue:'#64a1ec',//边的蓝色 13 | 14 | centerColor_blue:'#53699d',//内部的颜色 15 | centerColor_white:'#fff',//内部的颜色 16 | 17 | lineWidth_thin:1, 18 | lineWidth_bolder:2, 19 | 20 | deleteByIcon:false, 21 | 22 | }, 23 | edge:{ 24 | strokeColor_grey:'#b8c3ce',//线的颜色 25 | strokeColor_blue:'#64a1ec',//线的颜色 26 | 27 | strokeWidth_thin: 2, 28 | strokeWidth_bolder: 2, 29 | 30 | deleteByIcon:false 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 28 | 29 | 47 | -------------------------------------------------------------------------------- /.github/workflows/cl.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the V1.0-beat branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # jobs 表示要执行的一项或者多项任务 17 | jobs: 18 | # 任务名,可自定义 19 | build-and-deploy: 20 | # runs-on字段指定运行所需要的虚拟机环境。它是必填字段。目前可用的虚拟机如下。 21 | runs-on: ubuntu-latest 22 | # steps表示执行步骤 23 | steps: 24 | # 检出代码,这里用了 actions/checkout@master 库来完成 25 | - name: Checkout 26 | uses: actions/checkout@master 27 | # 这里展示了如何执行多条命令 28 | - name: Install and Build 29 | run: | 30 | yarn 31 | yarn build 32 | # 这里引用了别人写好的发布库,具体参数信息可以查阅上面的链接 33 | - name: Deploy to GitHub Pages 34 | uses: JamesIves/github-pages-deploy-action@4.0.0 35 | with: 36 | branch: gh-pages 37 | folder: dist 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "D3-EasyFlowRender", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "d3": "^5.16.0", 13 | "dagre-d3": "^0.6.4", 14 | "element-ui": "^2.13.2", 15 | "less-loader": "^6.1.0", 16 | "vue": "^2.6.11", 17 | "vue-router": "^3.3.2" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "~4.4.0", 21 | "@vue/cli-plugin-eslint": "~4.4.0", 22 | "@vue/cli-service": "~4.4.0", 23 | "babel-eslint": "^10.1.0", 24 | "eslint": "^6.7.2", 25 | "eslint-plugin-vue": "^6.2.2", 26 | "jquery": "^3.6.0", 27 | "vue-template-compiler": "^2.6.11" 28 | }, 29 | "eslintConfig": { 30 | "root": true, 31 | "env": { 32 | "node": true 33 | }, 34 | "extends": [ 35 | "plugin:vue/essential", 36 | "eslint:recommended" 37 | ], 38 | "parserOptions": { 39 | "parser": "babel-eslint" 40 | }, 41 | "rules": {} 42 | }, 43 | "browserslist": [ 44 | "> 1%", 45 | "last 2 versions", 46 | "not dead" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/containers/PanelLeft.vue: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by OXOYO on 2019/7/2. 3 | * 4 | * PanelLeft 左侧面板 5 | */ 6 | 7 | 8 | 20 | 21 | 59 | -------------------------------------------------------------------------------- /src/jsondata/subdata.js: -------------------------------------------------------------------------------- 1 | let __options = { 2 | data: [ 3 | { 4 | type: 'database', 5 | name: 'web-server', 6 | time: 30, 7 | rpm: 40, 8 | epm: 50, 9 | active: 3, 10 | total: 5, 11 | code: 'java', 12 | health: 1, 13 | lineProtocol: 'http', 14 | lineTime: 12, 15 | lineRpm: 34 16 | }, 17 | { 18 | type: 'database', 19 | name: 'Mysql', 20 | time: 30, 21 | rpm: 40, 22 | epm: 50, 23 | active: 3, 24 | total: 5, 25 | code: 'java', 26 | health: 2, 27 | lineProtocol: 'http', 28 | lineTime: 12, 29 | lineRpm: 34 30 | }, 31 | { 32 | type: 'app', 33 | name: 'Redis', 34 | time: 30, 35 | rpm: 40, 36 | epm: 50, 37 | active: 3, 38 | total: 5, 39 | code: 'java', 40 | health: 3, 41 | lineProtocol: 'http', 42 | lineTime: 12, 43 | lineRpm: 34 44 | }, 45 | { 46 | type: 'earth', 47 | name: 'ES', 48 | time: 30, 49 | rpm: 40, 50 | epm: 50, 51 | active: 3, 52 | total: 5, 53 | code: 'java', 54 | health: 1, 55 | lineProtocol: 'http', 56 | lineTime: 12, 57 | lineRpm: 34, 58 | value: 100 59 | }, 60 | { 61 | type: 'docker', 62 | name: 'ES', 63 | time: 30, 64 | rpm: 40, 65 | epm: 50, 66 | active: 3, 67 | total: 5, 68 | code: 'java', 69 | health: 1, 70 | lineProtocol: 'http', 71 | lineTime: 12, 72 | lineRpm: 34, 73 | value: 100 74 | }, 75 | { 76 | type: 'cloud', 77 | name: 'cloud', 78 | time: 30, 79 | rpm: 40, 80 | epm: 50, 81 | active: 3, 82 | total: 5, 83 | code: 'java', 84 | health: 1, 85 | lineProtocol: 'http', 86 | lineTime: 12, 87 | lineRpm: 34, 88 | value: 100 89 | } 90 | ] 91 | } 92 | export default __options -------------------------------------------------------------------------------- /src/jsondata/data.js: -------------------------------------------------------------------------------- 1 | let options = { 2 | data: [ 3 | { 4 | type: 'app', // 节点类型 5 | name: 'monitor-app', // 节点名称 6 | active: 9, // 已完成数 7 | total: 10, // 总数 8 | health: 1, // 监控健康程度 9 | underText: 'underText', // 节点之间连接线下方的文字 10 | upwardText: "upwardText", // 节点之间连接线上方的文字 11 | isConfig: "true" // 节点配置状态 12 | }, 13 | { 14 | type: 'app', 15 | name: 'monitor-app2', 16 | active: 1, 17 | total: 10, 18 | health: 3, 19 | underText: 'underText', 20 | upwardText: "upwardText", 21 | isConfig: "true" 22 | }, 23 | { 24 | type: 'database', 25 | name: 'Mysql', 26 | active: 3, 27 | total: 5, 28 | health: 2, 29 | underText: 'underText', 30 | upwardText: "upwardText", 31 | isConfig: "false" 32 | }, 33 | { 34 | type: 'cloud', 35 | name: 'Redis', 36 | active: 3, 37 | total: 5, 38 | health: 3, 39 | underText: 'underText', 40 | upwardText: "upwardText", 41 | isConfig: "false" 42 | }, 43 | { 44 | type: 'earth', 45 | name: 'ES', 46 | active: 3, 47 | total: 5, 48 | health: 1, 49 | underText: 'underText', 50 | upwardText: "upwardText", 51 | value: 100, 52 | isConfig: "false" 53 | }, 54 | { 55 | type: 'earth', 56 | name: 'Redis', 57 | active: 3, 58 | total: 5, 59 | health: 3, 60 | underText: 'underText', 61 | upwardText: "upwardText", 62 | isConfig: "true" 63 | }, 64 | { 65 | type: 'myapp', 66 | name: 'AKA', 67 | active: 3, 68 | total: 5, 69 | code: 'Python', 70 | health: 1, 71 | underText: 'underText', 72 | upwardText: "upwardText", 73 | value: 10, 74 | isConfig: "true" 75 | } 76 | ], 77 | edges: [ 78 | { 79 | source: 0, // 连接线起点(数值对应data数组中的元素) 80 | target: 3, // 连接线终点(数值对应data数组中的元素) 81 | }, 82 | { 83 | source: 1, 84 | target: 2, 85 | }, 86 | { 87 | source: 1, 88 | target: 6, 89 | }, 90 | { 91 | source: 1, 92 | target: 3, 93 | }, 94 | { 95 | source: 0, 96 | target: 1, 97 | }, 98 | { 99 | source: 4, 100 | target: 5, 101 | }, 102 | { 103 | source: 1, 104 | target: 2, 105 | }, 106 | { 107 | source: 5, 108 | target: 2, 109 | } 110 | ] 111 | } 112 | export default options -------------------------------------------------------------------------------- /src/views/flowChart/Editor/containers/ContextMenu.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 63 | 64 | 89 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/utils/commonCaculate.js: -------------------------------------------------------------------------------- 1 | import commonConfig from "../config/commonConfig"; 2 | 3 | const nodeTitleLengthLimit = 8; 4 | 5 | export const getShowNodeTitle = function (title){ 6 | let result = title; 7 | if(title==null){ 8 | result = ''; 9 | } 10 | if(result.length>nodeTitleLengthLimit){ 11 | result = result.substring(0,nodeTitleLengthLimit-1)+'...'; 12 | } 13 | return result; 14 | } 15 | 16 | export const getNodeToolTip = function (title){ 17 | if(title && title.length>nodeTitleLengthLimit){ 18 | return title; 19 | } 20 | return null; 21 | } 22 | 23 | /* 24 | 计算连接点相对节点的偏移量 25 | 预置的连线 :pointOffset为null,nodeAnchorIndex不应为空,为空就默认选一个点 26 | 拖动生成的连线:pointOffset不为null,nodeAnchorIndex为空 27 | */ 28 | export const getPointOffset = function (pointOffset, node, nodeAnchorIndex){ 29 | let nodeModel = node.getModel(); 30 | if(!pointOffset){ 31 | 32 | let nodeAnchorPoint = commonConfig.node.anchorPoints[0]; 33 | if(nodeAnchorIndex!=null){ 34 | nodeAnchorPoint = commonConfig.node.anchorPoints[nodeAnchorIndex]; 35 | } 36 | //计算出锚点的具体坐标 37 | let nodeWidth = nodeModel.size[0]; 38 | let nodeHeight = nodeModel.size[1]; 39 | if (nodeModel.size.length == 1) { 40 | nodeHeight = nodeWidth; 41 | } 42 | pointOffset = {x:nodeWidth * nodeAnchorPoint[0] - nodeWidth / 2, y:nodeHeight * nodeAnchorPoint[1] - nodeHeight / 2}; 43 | } 44 | return pointOffset; 45 | } 46 | 47 | export const getEdgeStartOrEndPoint = function (pointOffset, node, anchorIndex){ 48 | let offset = getPointOffset(pointOffset, node, anchorIndex); 49 | return { 50 | x: Math.round(node.getModel().x + offset.x), 51 | y: Math.round(node.getModel().y + offset.y) 52 | } 53 | } 54 | 55 | //深拷贝 56 | export const deepCopy = function (obj){ 57 | let tmp = JSON.stringify(obj); 58 | return JSON.parse(tmp); 59 | 60 | // var copy = Object.create(Object.getPrototypeOf(obj)); 61 | // var propNames = Object.getOwnPropertyNames(obj); 62 | // propNames.forEach(function(name) { 63 | // var desc = Object.getOwnPropertyDescriptor(obj, name); 64 | // Object.defineProperty(copy, name, desc); 65 | // }); 66 | // return copy; 67 | } 68 | 69 | /** 70 | * [generateUUID 返回一串序列码] 71 | * @return {String} [uuid] 72 | */ 73 | export const generateUUID = function () { 74 | var d = new Date().getTime(); 75 | var uuid = 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 76 | var r = (d + Math.random() * 16) % 16 | 0; 77 | d = Math.floor(d / 16); 78 | return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16); 79 | }); 80 | return uuid; 81 | } 82 | 83 | 84 | /** 85 | * 判断两个数组是否相同 86 | * @return boolean 87 | */ 88 | export const isArrayEqual = function (arr1 = [],arr2 = []){ 89 | if(arr1 instanceof Array && arr2 instanceof Array && arr1.length === arr2.length){ 90 | return JSON.stringify(arr1.sort()) == JSON.stringify(arr2.sort()) 91 | }else{ 92 | return false; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/jsondata/treetopo.js: -------------------------------------------------------------------------------- 1 | let list = { 2 | nodeInfos: [ 3 | { 4 | id: "node1", 5 | label: "组件1", 6 | type: "service", 7 | isFinished: false 8 | }, { 9 | id: "node2", 10 | label: "组件2", 11 | type: "defend", 12 | isFinished: true 13 | }, { 14 | id: "node3", 15 | label: "组件3", 16 | type: "defend", 17 | isFinished: false 18 | }, { 19 | id: "node4", 20 | label: "组件4", 21 | type: "service", 22 | isFinished: true 23 | }, { 24 | id: "node5", 25 | label: "组件5", 26 | type: "service", 27 | isFinished: false 28 | }, { 29 | id: "node6", 30 | label: "组件6", 31 | type: "defend", 32 | isFinished: true 33 | }, { 34 | id: "node7", 35 | label: "组件7", 36 | type: "database", 37 | isFinished: false 38 | }, { 39 | id: "node8", 40 | label: "组件8", 41 | type: "database", 42 | isFinished: false 43 | }, { 44 | id: "node9", 45 | label: "组件9", 46 | type: "service", 47 | isFinished: false 48 | }, { 49 | id: "node10", 50 | label: "组件10", 51 | type: "service", 52 | isFinished: true 53 | }, { 54 | id: "node11", 55 | label: "组件11", 56 | type: "defend", 57 | isFinished: false 58 | }, { 59 | id: "node12", 60 | label: "组件12", 61 | type: "service", 62 | isFinished: false 63 | }, { 64 | id: "node13", 65 | label: "组件13", 66 | type: "service", 67 | isFinished: true 68 | }, { 69 | id: "node14", 70 | label: "组件14", 71 | type: "service", 72 | isFinished: false 73 | }, 74 | ], 75 | edges: [ 76 | { 77 | source: "node1", 78 | target: "node2", 79 | }, 80 | { 81 | source: "node1", 82 | target: "node5", 83 | }, 84 | { 85 | source: "node1", 86 | target: "node6", 87 | }, 88 | { 89 | source: "node2", 90 | target: "node2", 91 | }, 92 | { 93 | source: "node2", 94 | target: "node3", 95 | }, 96 | { 97 | source: "node2", 98 | target: "node4", 99 | }, 100 | { 101 | source: "node2", 102 | target: "node4", 103 | }, 104 | { 105 | source: "node1", 106 | target: "node4", 107 | }, 108 | { 109 | source: "node7", 110 | target: "node1", 111 | }, 112 | { 113 | source: "node8", 114 | target: "node8", 115 | }, 116 | { 117 | source: "node1", 118 | target: "node9", 119 | }, 120 | { 121 | source: "node4", 122 | target: "node11", 123 | }, 124 | { 125 | source: "node1", 126 | target: "node10", 127 | }, 128 | { 129 | source: "node1", 130 | target: "node12", 131 | }, 132 | { 133 | source: "node10", 134 | target: "node13", 135 | }, 136 | { 137 | source: "node4", 138 | target: "node14", 139 | }, 140 | { 141 | source: "node8", 142 | target: "node10", 143 | }, { 144 | source: "node14", 145 | target: "node7", 146 | } 147 | ], 148 | dataFlow: { 149 | 0: '配置完成', 150 | 1: '待配置', 151 | 2: '配置完成', 152 | 3: '待配置', 153 | 4: '待配置', 154 | 5: '待配置', 155 | 6: '配置完成', 156 | 7: '配置完成', 157 | 8: '待配置', 158 | 9: '待配置', 159 | 10: '待配置', 160 | 11: '待配置', 161 | 12: '配置完成', 162 | 13: '待配置', 163 | 14: '配置完成', 164 | 15: '待配置', 165 | 16: '配置完成', 166 | 17: '待配置', 167 | } 168 | } 169 | export default list -------------------------------------------------------------------------------- /src/views/treetopo/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 141 | 142 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/custom/node/customEndNode.js: -------------------------------------------------------------------------------- 1 | import G6 from "@antv/g6/build/g6"; 2 | import commonConfig from "../../config/commonConfig" 3 | const config = { 4 | defaultSize:[80], 5 | title:'结束', 6 | ...commonConfig.node, 7 | deleteEnabled:false 8 | }; 9 | 10 | const customEndNode = { 11 | init(editor) { 12 | G6.registerNode("customEndNode", { 13 | draw(cfg, group) { 14 | let currentMode = editor.getCurrentMode(); 15 | 16 | let r = parseInt(config.defaultSize[0]/2); 17 | cfg.size = [2*r]; 18 | 19 | 20 | const shape = group.addShape("circle", {//这个是主图形 21 | attrs: { 22 | id: cfg.id, 23 | x: 0, 24 | y: 0, 25 | r: r, 26 | isMain:true, 27 | stroke: config.strokeColor_grey, 28 | fill: '#fff',//此处必须有fill 不然不能触发事件 29 | lineDash: [4, 4],//虚线 30 | lineWidth: config.lineWidth_thin, 31 | } 32 | }); 33 | group.addShape("circle", { 34 | attrs: { 35 | x: 0, 36 | y: 0, 37 | r: parseInt(r * 0.8), 38 | fill: config.centerColor_blue, 39 | } 40 | }); 41 | group.addShape("text", { 42 | attrs: { 43 | x: 0, 44 | y: 0, 45 | textAlign: "center", 46 | textBaseline: "middle", 47 | text: config.title, 48 | fill: config.centerColor_white, 49 | fontSize: 18, 50 | fontWeight:'bolder'//最粗的字体 51 | } 52 | }); 53 | group.addShape("circle", {//加一个透明的图形解决点击有的地方不好拖动的问题 54 | attrs: { 55 | x: 0, 56 | y: 0, 57 | r: r, 58 | fill: '#fff',//此处必须有fill 不然不能触发事件 59 | opacity: 0, 60 | cursor:'move', 61 | } 62 | }); 63 | if(config.deleteByIcon && config.deleteEnabled && currentMode === 'edit') { 64 | const offset_ = parseInt(0.8 * r); 65 | group.addShape("circle", {//删除的按钮 66 | attrs: { 67 | x: offset_, 68 | y: -offset_, 69 | r: config.defaultDeleteR, 70 | isDeleteButton: true, 71 | enabled: false, 72 | stroke: '#fff', 73 | lineWidth: 2, 74 | fill: '#FF111A', 75 | cursor: 'pointer', 76 | opacity: 0, 77 | } 78 | }); 79 | group.addShape("text", {//删除的按钮的叉叉 80 | attrs: { 81 | x: offset_, 82 | y: -offset_ + 0.95, 83 | textAlign: "center", 84 | textBaseline: "middle", 85 | text: '×', 86 | fontSize: 18, 87 | cursor: 'pointer', 88 | isDeleteButton: true, 89 | enabled: false, 90 | fill: '#fff', 91 | opacity: 0, 92 | fontWeight: 'bolder'//最粗的字体 93 | } 94 | }); 95 | } 96 | if(currentMode === 'edit') { 97 | for (let i = 0; i < config.anchorPoints.length; i++) {//画上下左右的4个点 98 | group.addShape("circle", { 99 | attrs: { 100 | x: parseInt(config.anchorPoints[i][0] * 2 * r - r), 101 | y: parseInt(config.anchorPoints[i][1] * 2 * r - r), 102 | r: 0, 103 | isAnchorPointBigger:true, 104 | anchorPointIndex:i, 105 | opacity: config.anchorBiggerOpacity, 106 | fill: config.anchorColor_blue, 107 | cursor:'crosshair', 108 | } 109 | }); 110 | group.addShape("circle", { 111 | attrs: { 112 | x: parseInt(config.anchorPoints[i][0] * 2 * r - r), 113 | y: parseInt(config.anchorPoints[i][1] * 2 * r - r), 114 | r: 0, 115 | isAnchorPoint:true, 116 | anchorPointIndex:i, 117 | opacity: 1, 118 | stroke: config.anchorColor_blue, 119 | fill: config.anchorColor_grey, 120 | cursor:'crosshair', 121 | } 122 | }); 123 | } 124 | } 125 | return shape; 126 | }, 127 | //设置状态 128 | setState(name, value, item) { 129 | const group = item.getContainer(); 130 | const shape = group.findAll(point => { 131 | return point._attrs.isMain; 132 | })[0]; 133 | const points = group.findAll(point => { 134 | return point._attrs.isAnchorPoint; 135 | }); 136 | const pointBiggers = group.findAll(point => { 137 | return point._attrs.isAnchorPointBigger; 138 | }); 139 | const deleteButtons = group.findAll(point => { 140 | return point._attrs.isDeleteButton; 141 | }); 142 | const selectStyles = () => { 143 | shape.attr("stroke", config.strokeColor_blue); 144 | shape.attr("lineWidth", config.lineWidth_bolder); 145 | 146 | deleteButtons.forEach(button => { 147 | button.attr('opacity', 1); 148 | button.attr('enabled',true); 149 | }); 150 | }; 151 | const defaultStyles = () => { 152 | shape.attr("stroke", config.strokeColor_grey); 153 | shape.attr("lineWidth", config.lineWidth_thin); 154 | points.forEach(point => { 155 | point.attr('r', 0); 156 | }); 157 | pointBiggers.forEach(point => { 158 | point.attr('r', 0); 159 | }); 160 | deleteButtons.forEach(button => { 161 | button.attr('opacity', 0); 162 | button.attr('enabled',false); 163 | }); 164 | }; 165 | let hover_addedge = () => { 166 | points.forEach(point => { 167 | point.attr('r', config.anchorR); 168 | point.attr('fill', config.anchorColor_grey); 169 | }); 170 | pointBiggers.forEach(point => { 171 | point.attr('r', config.anchorR*3); 172 | }); 173 | }; 174 | switch (name) { 175 | case "selected": 176 | case "hover": 177 | if (value) { 178 | selectStyles() 179 | } else { 180 | defaultStyles() 181 | } 182 | break; 183 | case "hover_addedge": 184 | if (value) { 185 | hover_addedge() 186 | } else { 187 | defaultStyles() 188 | } 189 | break; 190 | } 191 | } 192 | }); 193 | } 194 | } 195 | export default customEndNode 196 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/custom/node/customStartNode.js: -------------------------------------------------------------------------------- 1 | import G6 from "@antv/g6/build/g6"; 2 | import commonConfig from "../../config/commonConfig" 3 | const config = { 4 | defaultSize:[80], 5 | title:'开始', 6 | ...commonConfig.node, 7 | deleteEnabled:false, 8 | outEdgeNum:1, 9 | }; 10 | 11 | const customStartNode = { 12 | init(editor) { 13 | G6.registerNode("customStartNode", { 14 | draw(cfg, group) { 15 | let currentMode = editor.getCurrentMode(); 16 | let r = parseInt(config.defaultSize[0]/2); 17 | cfg.size = [2*r]; 18 | 19 | 20 | const shape = group.addShape("circle", {//这个是主图形 21 | attrs: { 22 | id: cfg.id, 23 | x: 0, 24 | y: 0, 25 | r: r, 26 | isMain:true, 27 | stroke: config.strokeColor_grey, 28 | fill: '#fff',//此处必须有fill 不然不能触发事件 29 | lineDash: [4, 4],//虚线 30 | lineWidth: config.lineWidth_thin, 31 | } 32 | }); 33 | group.addShape("circle", { 34 | attrs: { 35 | x: 0, 36 | y: 0, 37 | r: parseInt(r * 0.8), 38 | fill: config.centerColor_blue, 39 | } 40 | }); 41 | group.addShape("text", { 42 | attrs: { 43 | x: 0, 44 | y: 0, 45 | textAlign: "center", 46 | textBaseline: "middle", 47 | text: config.title, 48 | fill: config.centerColor_white, 49 | fontSize: 18, 50 | fontWeight:'bolder'//最粗的字体 51 | } 52 | }); 53 | group.addShape("circle", {//加一个透明的图形解决点击有的地方不好拖动的问题 54 | attrs: { 55 | x: 0, 56 | y: 0, 57 | r: r, 58 | fill: '#fff',//此处必须有fill 不然不能触发事件 59 | opacity: 0, 60 | cursor:'move', 61 | } 62 | }); 63 | if(config.deleteByIcon && config.deleteEnabled && currentMode == 'edit') { 64 | const offset_ = parseInt(0.8 * r); 65 | group.addShape("circle", {//删除的按钮 66 | attrs: { 67 | x: offset_, 68 | y: -offset_, 69 | r: config.defaultDeleteR, 70 | isDeleteButton: true, 71 | enabled: false, 72 | stroke: '#fff', 73 | lineWidth: 2, 74 | fill: '#FF111A', 75 | cursor: 'pointer', 76 | opacity: 0, 77 | } 78 | }); 79 | group.addShape("text", {//删除的按钮的叉叉 80 | attrs: { 81 | x: offset_, 82 | y: -offset_ + 0.95, 83 | textAlign: "center", 84 | textBaseline: "middle", 85 | text: '×', 86 | fontSize: 18, 87 | cursor: 'pointer', 88 | isDeleteButton: true, 89 | enabled: false, 90 | fill: '#fff', 91 | opacity: 0, 92 | fontWeight: 'bolder'//最粗的字体 93 | } 94 | }); 95 | } 96 | if(currentMode === 'edit') { 97 | for (let i = 0; i < config.anchorPoints.length; i++) {//画上下左右的4个点 98 | 99 | group.addShape("circle", { 100 | attrs: { 101 | x: parseInt(config.anchorPoints[i][0] * 2 * r - r), 102 | y: parseInt(config.anchorPoints[i][1] * 2 * r - r), 103 | r: 0, 104 | isAnchorPointBigger:true, 105 | anchorPointIndex:i, 106 | opacity: config.anchorBiggerOpacity, 107 | fill: config.anchorColor_blue, 108 | cursor:'crosshair', 109 | } 110 | }); 111 | 112 | group.addShape("circle", { 113 | attrs: { 114 | x: parseInt(config.anchorPoints[i][0] * 2 * r - r), 115 | y: parseInt(config.anchorPoints[i][1] * 2 * r - r), 116 | r: 0, 117 | isAnchorPoint:true, 118 | anchorPointIndex:i, 119 | opacity: 1, 120 | stroke: config.anchorColor_blue, 121 | fill: config.anchorColor_grey, 122 | cursor:'crosshair', 123 | } 124 | }); 125 | } 126 | } 127 | return shape; 128 | }, 129 | //设置状态 130 | setState(name, value, item) { 131 | let showOutAnchor = false; 132 | if((name === 'selected' || name === 'hover' || name === 'hover_addedge') && item._cfg.edges){ 133 | let existOutEdges = 0; 134 | item._cfg.edges.forEach(edge => { 135 | if (edge._cfg.sourceNode._cfg.id == item._cfg.id) { 136 | existOutEdges++; 137 | } 138 | }); 139 | if(existOutEdges { 145 | return point._attrs.isMain; 146 | })[0]; 147 | const points = group.findAll(point => { 148 | return point._attrs.isAnchorPoint; 149 | }); 150 | const pointBiggers = group.findAll(point => { 151 | return point._attrs.isAnchorPointBigger; 152 | }); 153 | const deleteButtons = group.findAll(point => { 154 | return point._attrs.isDeleteButton; 155 | }); 156 | 157 | 158 | const selectStyles = () => { 159 | shape.attr("stroke", config.strokeColor_blue); 160 | shape.attr("lineWidth", config.lineWidth_bolder); 161 | if(showOutAnchor){ 162 | points.forEach(point => { 163 | point.attr('r', config.anchorR); 164 | point.attr('fill', config.anchorColor_grey); 165 | }); 166 | }else{ 167 | points.forEach(point => { 168 | point.attr('r', 0); 169 | }); 170 | } 171 | deleteButtons.forEach(button => { 172 | button.attr('opacity', 1); 173 | button.attr('enabled',true); 174 | }); 175 | }; 176 | const defaultStyles = () => { 177 | shape.attr("stroke", config.strokeColor_grey); 178 | shape.attr("lineWidth", config.lineWidth_thin); 179 | 180 | points.forEach(point => { 181 | point.attr('r', 0); 182 | }); 183 | pointBiggers.forEach(point => { 184 | point.attr('r', 0); 185 | }); 186 | deleteButtons.forEach(button => { 187 | button.attr('r', 0); 188 | button.attr('enabled',false); 189 | }); 190 | }; 191 | const hover_changeedgesource = () => { 192 | //只是改个锚点,肯定满足出入的规则 193 | points.forEach(point => { 194 | point.attr('r', config.anchorR); 195 | point.attr('fill', config.anchorColor_grey); 196 | }); 197 | pointBiggers.forEach(point => { 198 | point.attr('r', config.anchorR*3); 199 | }); 200 | }; 201 | switch (name) { 202 | case "selected": 203 | case "hover": 204 | if (value) { 205 | selectStyles() 206 | } else { 207 | defaultStyles() 208 | } 209 | break; 210 | case "hover_addedge": 211 | break; 212 | case "hover_changeedgesource": 213 | if (value) { 214 | hover_changeedgesource() 215 | } else { 216 | defaultStyles() 217 | } 218 | break; 219 | } 220 | } 221 | }); 222 | } 223 | } 224 | 225 | export default customStartNode 226 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/behavior/add-edge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 修改自 https://github.com/caoyu48/vue-g6-editor 3 | */ 4 | import eventBus from "../utils/eventBus"; 5 | import {getEdgeStartOrEndPoint} from "../utils/commonCaculate"; 6 | 7 | 8 | let startItem = null; 9 | let startPointOffset = null; 10 | let sourceAnchorIndex = null; 11 | let edgeAdding = null; 12 | 13 | let changeEdgeId = null; 14 | let changeSource = null; 15 | 16 | export default { 17 | getEvents() { 18 | return { 19 | mousemove: 'onMousemove', 20 | mouseup: 'onMouseup', 21 | 'node:mouseover': 'onNodeHover', 22 | 'node:mouseout': 'onNodeOut', 23 | 'editor:addEdge': 'startAddEdge', 24 | 'editor:moveEdge': 'startMoveEdge', 25 | }; 26 | }, 27 | onNodeOut(event) { 28 | let _t = this; 29 | _t.log('add-edge onNodeOut'); 30 | if(startItem && event.item._cfg.id !== startItem._cfg.id && !changeEdgeId){ 31 | _t.graph.setItemState(event.item, 'hover_addedge', false);//因为直接设置true的话,原来就是true,就不会执行 32 | _t.graph.setItemState(event.item, 'hover_addedge', true); 33 | } 34 | }, 35 | onMouseup(event) { 36 | let _t = this; 37 | _t.log('add-edge onMouseup'); 38 | 39 | const item = event.item; 40 | if (startItem 41 | && item 42 | && item.getType() === 'node' 43 | && item._cfg.id !== startItem._cfg.id 44 | && edgeAdding 45 | &&(event.target._attrs.isAnchorPoint || event.target._attrs.isAnchorPointBigger)) { 46 | let targetItem = item; 47 | let endPointOffset = {x:parseInt(event.target._attrs.x), y:parseInt(event.target._attrs.y)}; 48 | _t.graph.removeItem(edgeAdding); 49 | _t.graph.setMode('edit');//这里先改模式,否则可能会出现画线的时候,mode还是addEdge 50 | if(!changeEdgeId){ 51 | const model = { 52 | source: startItem._cfg.id,//必传 53 | target: targetItem._cfg.id,//必传 54 | startPointOffset: startPointOffset, 55 | sourceAnchorIndex: sourceAnchorIndex, 56 | endPointOffset: endPointOffset, 57 | targetAnchorIndex:event.target._attrs.anchorPointIndex, 58 | }; 59 | eventBus.$emit('add_edge', model); 60 | }else{ 61 | const model = { 62 | id: changeEdgeId 63 | }; 64 | if (changeSource) { 65 | model.sourceAnchorIndex = event.target._attrs.anchorPointIndex; 66 | } else { 67 | model.targetAnchorIndex = event.target._attrs.anchorPointIndex; 68 | } 69 | eventBus.$emit('change_edge', model); 70 | } 71 | }else{ 72 | if(changeEdgeId){ 73 | eventBus.$emit('delete_item', {ids:[changeEdgeId],type:'edge'}); 74 | } 75 | } 76 | if (edgeAdding){ 77 | _t.graph.removeItem(edgeAdding); 78 | } 79 | _t.graph.paint(); 80 | 81 | startPointOffset = null; 82 | sourceAnchorIndex = null; 83 | startItem = null; 84 | edgeAdding = null; 85 | changeEdgeId = null; 86 | changeSource = null; 87 | _t.doClearAllStates(); 88 | _t.graph.setMode('edit'); 89 | }, 90 | onMousemove(event) { 91 | let _t = this; 92 | _t.log('add-edge onMousemove'); 93 | if (startItem) { 94 | const point = {x: event.x, y: event.y}; 95 | if (edgeAdding) { 96 | // 增加边的过程中,移动时边跟着移动 97 | _t.graph.updateItem(edgeAdding, { 98 | target: point 99 | }); 100 | } 101 | } 102 | }, 103 | startAddEdge(event){ 104 | let _t = this; 105 | _t.log('add-edge startAddEdge'); 106 | const item = event.item; 107 | if(!item){ 108 | _t.clearAddEdge(); 109 | return; 110 | } 111 | let model = item.getModel(); 112 | startPointOffset = {x:parseInt(event.target._attrs.x), y:parseInt(event.target._attrs.y)}; 113 | 114 | sourceAnchorIndex = event.target._attrs.anchorPointIndex; 115 | let startPoint = {x: startPointOffset.x+model.x, y: startPointOffset.y+model.y}; 116 | startItem = item; 117 | edgeAdding = _t.graph.addItem('edge', { 118 | id:'tempLinkLine', 119 | source: startPoint, 120 | target: startPoint, 121 | type:'linkLine', 122 | shape: 'customLinkEdge' 123 | }); 124 | 125 | //把其他的节点都设置为hover_addedge true 126 | _t.graph.getNodes().forEach(node => { 127 | if(node._cfg.id !== startItem._cfg.id){ 128 | _t.graph.setItemState(node, 'hover_addedge', true); 129 | } 130 | }); 131 | }, 132 | 133 | //线只能改锚点,不能换节点(因为有的线上还有设置参数,不好处理) 134 | startMoveEdge(event){ 135 | let _t = this; 136 | _t.log('add-edge startMoveEdge'); 137 | let targetPoint = {x: event.x, y: event.y}; 138 | let startPoint = null; 139 | let targetNode = null; 140 | if (event.target._attrs.sourceMoveBtn) { 141 | startPoint = getEdgeStartOrEndPoint(null, event.item._cfg.target, event.item._cfg.targetAnchorIndex); 142 | sourceAnchorIndex = event.item._cfg.model.targetAnchorIndex; 143 | startItem = event.item._cfg.target; 144 | targetNode = event.item._cfg.source; 145 | changeSource = true; 146 | }else{ 147 | startPoint = getEdgeStartOrEndPoint(null, event.item._cfg.source, event.item._cfg.sourceAnchorIndex); 148 | sourceAnchorIndex = event.item._cfg.model.sourceAnchorIndex; 149 | startItem = event.item._cfg.source; 150 | targetNode = event.item._cfg.target; 151 | changeSource = false; 152 | } 153 | 154 | let state = 'hover_addedge'; 155 | if (event.target._attrs.sourceMoveBtn) { 156 | state = 'hover_changeedgesource';//因为只是改个锚点,所以肯定满足节点的出入线的规则 157 | } 158 | changeEdgeId = event.item._cfg.id; 159 | _t.graph.removeItem(event.item);//这里先不急着让Index里面删掉这根线,等改完(即鼠标mouseup的时候,该删就删,该改就改) 160 | edgeAdding = _t.graph.addItem('edge', { 161 | id:'tempLinkLine', 162 | source: startPoint, 163 | target: targetPoint, 164 | type:'linkLine', 165 | shape: 'customLinkEdge' 166 | }); 167 | 168 | //把节点设置为hover_changeEdgeSource true 169 | _t.graph.setItemState(targetNode, state, true); 170 | 171 | }, 172 | 173 | clearAddEdge(){ 174 | let _t = this; 175 | _t.graph.setMode('edit'); 176 | }, 177 | // 清除所有状态 178 | doClearAllStates() { 179 | let _t = this; 180 | if (!_t.graph) { 181 | return 182 | } 183 | // 批量操作时关闭自动重绘,以提升性能 184 | _t.graph.setAutoPaint(false); 185 | _t.graph.getNodes().forEach(function (node) { 186 | _t.graph.clearItemStates(node, ['hover', 'hover_addedge', 'hover_changeedgesource', 'selected']); 187 | }); 188 | _t.graph.getEdges().forEach(function (edge) { 189 | _t.graph.clearItemStates(edge, ['hover', 'hover_addedge', 'hover_changeedgesource', 'selected']); 190 | }); 191 | _t.graph.paint(); 192 | _t.graph.setAutoPaint(true); 193 | }, 194 | onNodeHover(event) { 195 | let _t = this; 196 | _t.log('add-edge onNodeHover'); 197 | if (startItem && event.item._cfg.id !== startItem._cfg.id) { 198 | if(!changeEdgeId){ 199 | _t.graph.setItemState(event.item, 'hover_addedge', true); 200 | } 201 | if (event.target._attrs.isAnchorPoint || event.target._attrs.isAnchorPointBigger) {//鼠标的目标是锚点 202 | event.item.getContainer().findAll(point => { 203 | if(point._attrs.isAnchorPoint) { 204 | if (event.target._attrs.anchorPointIndex === point._attrs.anchorPointIndex) { 205 | point.attr("fill", "#3688ed"); 206 | } else { 207 | point.attr("fill", "#e1e4e8"); 208 | } 209 | } 210 | }); 211 | } 212 | } 213 | }, 214 | clearTempState(graph){ 215 | if (edgeAdding){ 216 | graph.removeItem(edgeAdding); 217 | } 218 | startItem = null; 219 | startPointOffset = null; 220 | sourceAnchorIndex = null; 221 | edgeAdding = null; 222 | 223 | }, 224 | log(message) { 225 | if (false) { 226 | console.log(message); 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/containers/ToolBar.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 199 | 200 | 201 | 238 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/custom/edge/polylineFinding.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by OXOYO on 2019/8/7. 3 | * 4 | * 折线寻径 5 | * 6 | * 文档:https://www.yuque.com/antv/blog/eyi70n 7 | * 鸣谢:@guozhaolong https://github.com/guozhaolong/wfd 8 | * 参考:https://github.com/guozhaolong/wfd/blob/master/src/item/edge.js 9 | */ 10 | 11 | // 折线寻径 12 | export const polylineFinding = function (sNode, tNode, sPort, tPort, offset = 10) { 13 | const sourceBBox = sNode && sNode.getBBox ? sNode.getBBox() : getPointBBox(sPort) 14 | const targetBBox = tNode && tNode.getBBox ? tNode.getBBox() : getPointBBox(tPort) 15 | // 获取节点带 offset 的区域(扩展区域) 16 | const sBBox = getExpandedBBox(sourceBBox, offset) 17 | const tBBox = getExpandedBBox(targetBBox, offset) 18 | // 获取扩展区域上的起始和终止连接点 19 | const sPoint = getExpandedPort(sBBox, sPort) 20 | const tPoint = getExpandedPort(tBBox, tPort) 21 | // 获取合法折点集 22 | let points = getConnectablePoints(sBBox, tBBox, sPoint, tPoint) 23 | // 过滤合法点集,预处理、剪枝等 24 | filterConnectablePoints(points, sBBox) 25 | // 过滤合法点集,预处理、剪枝等 26 | filterConnectablePoints(points, tBBox) 27 | // 用 A-Star 算法寻径 28 | let polylinePoints = AStar(points, sPoint, tPoint, sBBox, tBBox) 29 | return polylinePoints 30 | } 31 | 32 | const getPointBBox = function (t) { 33 | return { 34 | centerX: t.x, 35 | centerY: t.y, 36 | minX: t.x, 37 | minY: t.y, 38 | maxX: t.x, 39 | maxY: t.y, 40 | height: 0, 41 | width: 0 42 | } 43 | } 44 | 45 | // 获取扩展区域 46 | const getExpandedBBox = function (bbox, offset) { 47 | if (bbox.width === 0 && bbox.height === 0) { 48 | return bbox 49 | } 50 | return { 51 | centerX: bbox.centerX, 52 | centerY: bbox.centerY, 53 | minX: bbox.minX - offset, 54 | minY: bbox.minY - offset, 55 | maxX: bbox.maxX + offset, 56 | maxY: bbox.maxY + offset, 57 | height: bbox.height + 2 * offset, 58 | width: bbox.width + 2 * offset 59 | } 60 | } 61 | 62 | // 获取扩展区域上的连接点 63 | const getExpandedPort = function (bbox, point) { 64 | // 判断连接点在上下左右哪个区域,相应地给 x 或 y 加上或者减去 offset 65 | if (Math.abs(point.x - bbox.centerX) / bbox.width > Math.abs(point.y - bbox.centerY) / bbox.height) { 66 | return { 67 | x: point.x > bbox.centerX ? bbox.maxX : bbox.minX, 68 | y: point.y 69 | } 70 | } 71 | return { 72 | x: point.x, 73 | y: point.y > bbox.centerY ? bbox.maxY : bbox.minY 74 | } 75 | } 76 | 77 | // 获取合法折点集合 78 | const getConnectablePoints = function (sBBox, tBBox, sPoint, tPoint) { 79 | let lineBBox = getBBoxFromVertexes(sPoint, tPoint) 80 | let outerBBox = combineBBoxes(sBBox, tBBox) 81 | let sLineBBox = combineBBoxes(sBBox, lineBBox) 82 | let tLineBBox = combineBBoxes(tBBox, lineBBox) 83 | let points = [ 84 | ...vertexOfBBox(sLineBBox), 85 | ...vertexOfBBox(tLineBBox), 86 | ...vertexOfBBox(outerBBox) 87 | ] 88 | const centerPoint = { x: outerBBox.centerX, y: outerBBox.centerY } 89 | let bboxes = [ outerBBox, sLineBBox, tLineBBox, lineBBox ] 90 | bboxes.forEach(bbox => { 91 | // 包含 bbox 延长线和线段的相交线 92 | points = [ 93 | ...points, 94 | ...crossPointsByLineAndBBox(bbox, centerPoint) 95 | ] 96 | }) 97 | points.push({ x: sPoint.x, y: tPoint.y }) 98 | points.push({ x: tPoint.x, y: sPoint.y }) 99 | return points 100 | } 101 | 102 | const getBBoxFromVertexes = function (sPoint, tPoint) { 103 | const minX = Math.min(sPoint.x, tPoint.x) 104 | const maxX = Math.max(sPoint.x, tPoint.x) 105 | const minY = Math.min(sPoint.y, tPoint.y) 106 | const maxY = Math.max(sPoint.y, tPoint.y) 107 | 108 | return { 109 | centerX: (minX + maxX) / 2, 110 | centerY: (minY + maxY) / 2, 111 | maxX: maxX, 112 | maxY: maxY, 113 | minX: minX, 114 | minY: minY, 115 | height: maxY - minY, 116 | width: maxX - minX 117 | } 118 | } 119 | 120 | const combineBBoxes = function (sBBox, tBBox) { 121 | const minX = Math.min(sBBox.minX, tBBox.minX) 122 | const minY = Math.min(sBBox.minY, tBBox.minY) 123 | const maxX = Math.max(sBBox.maxX, tBBox.maxX) 124 | const maxY = Math.max(sBBox.maxY, tBBox.maxY) 125 | 126 | return { 127 | centerX: (minX + maxX) / 2, 128 | centerY: (minY + maxY) / 2, 129 | minX: minX, 130 | minY: minY, 131 | maxX: maxX, 132 | maxY: maxY, 133 | height: maxY - minY, 134 | width: maxX - minX 135 | } 136 | } 137 | 138 | const vertexOfBBox = function (bbox) { 139 | return [ 140 | { x: bbox.minX, y: bbox.minY }, 141 | { x: bbox.maxX, y: bbox.minY }, 142 | { x: bbox.maxX, y: bbox.maxY }, 143 | { x: bbox.minX, y: bbox.maxY } 144 | ] 145 | } 146 | 147 | const crossPointsByLineAndBBox = function (bbox, centerPoint) { 148 | let crossPoints = [] 149 | if (!(centerPoint.x < bbox.minX || centerPoint.x > bbox.maxX)) { 150 | crossPoints = [ 151 | ...crossPoints, 152 | { x: centerPoint.x, y: bbox.minY }, 153 | { x: centerPoint.x, y: bbox.maxY } 154 | ] 155 | } 156 | if (!(centerPoint.y < bbox.minY || centerPoint.y > bbox.maxY)) { 157 | crossPoints = [ 158 | ...crossPoints, 159 | { x: bbox.minX, y: centerPoint.y }, 160 | { x: bbox.maxX, y: centerPoint.y } 161 | ] 162 | } 163 | 164 | return crossPoints 165 | } 166 | 167 | // 过滤连接点 168 | const filterConnectablePoints = function (points, bbox) { 169 | return points.filter(point => { 170 | return point.x <= bbox.minX || point.x >= bbox.maxX || point.y <= bbox.minY || point.y >= bbox.maxY 171 | }) 172 | } 173 | 174 | const crossBBox = function (bboxes, p1, p2) { 175 | for (let i = 0; i < bboxes.length; i++) { 176 | const bbox = bboxes[i] 177 | if (p1.x === p2.x && bbox.minX < p1.x && bbox.maxX > p1.x) { 178 | if ((p1.y < bbox.maxY && p2.y >= bbox.maxY) || (p2.y < bbox.maxY && p1.y >= bbox.maxY)) { 179 | return true 180 | } 181 | } else if (p1.y === p2.y && bbox.minY < p1.y && bbox.maxY > p1.y) { 182 | if ((p1.x < bbox.maxX && p2.x >= bbox.maxX) || (p2.x < bbox.maxX && p1.x >= bbox.maxX)) { 183 | return true 184 | } 185 | } 186 | } 187 | return false 188 | } 189 | 190 | const getCost = function (p1, p2) { 191 | return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y) 192 | } 193 | 194 | // aStar 寻径 195 | const AStar = function (points, sPoint, tPoint, sBBox, tBBox) { 196 | const openList = [sPoint] 197 | const closeList = [] 198 | points.forEach(item => { 199 | item.id = item.x + '-' + item.y 200 | }) 201 | let tmpArr = [] 202 | points.forEach(item => { 203 | if (!tmpArr.includes(target => target.id === item.id)) { 204 | tmpArr.push(item) 205 | } 206 | }) 207 | points = [ 208 | ...tmpArr, 209 | tPoint 210 | ] 211 | let endPoint 212 | while (openList.length > 0) { 213 | let minCostPoint 214 | openList.forEach((p, i) => { 215 | if (!p.parent) { 216 | p.f = 0 217 | } 218 | if (!minCostPoint) { 219 | minCostPoint = p 220 | } 221 | if (p.f < minCostPoint.f) { 222 | minCostPoint = p 223 | } 224 | }) 225 | if (minCostPoint.x === tPoint.x && minCostPoint.y === tPoint.y) { 226 | endPoint = minCostPoint 227 | break 228 | } 229 | openList.splice(openList.findIndex(o => o.x === minCostPoint.x && o.y === minCostPoint.y), 1) 230 | closeList.push(minCostPoint) 231 | const neighbor = points.filter(p => { 232 | return (p.x === minCostPoint.x || p.y === minCostPoint.y) && 233 | !(p.x === minCostPoint.x && p.y === minCostPoint.y) && 234 | !crossBBox([sBBox, tBBox], minCostPoint, p) 235 | } 236 | ) 237 | neighbor.forEach(p => { 238 | const inOpen = openList.find(o => o.x === p.x && o.y === p.y) 239 | const currentG = getCost(p, minCostPoint) 240 | if (!closeList.find(o => o.x === p.x && o.y === p.y)) { 241 | if (inOpen) { 242 | if (p.g > currentG) { 243 | p.parent = minCostPoint 244 | p.g = currentG 245 | p.f = p.g + p.h 246 | } 247 | } else { 248 | p.parent = minCostPoint 249 | p.g = currentG 250 | let h = getCost(p, tPoint) 251 | if (crossBBox([tBBox], p, tPoint)) { 252 | // 如果穿过bbox则增加该点的预估代价为bbox周长的一半 253 | h += (tBBox.width / 2 + tBBox.height / 2) 254 | } 255 | p.h = h 256 | p.f = p.g + p.h 257 | openList.push(p) 258 | } 259 | } 260 | }) 261 | } 262 | if (endPoint) { 263 | const result = [] 264 | result.push({ 265 | x: endPoint.x, 266 | y: endPoint.y 267 | }) 268 | while (endPoint.parent) { 269 | endPoint = endPoint.parent 270 | result.push({ 271 | x: endPoint.x, 272 | y: endPoint.y 273 | }) 274 | } 275 | return result.reverse() 276 | } 277 | return [] 278 | } 279 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/custom/node/customBranchNode.js: -------------------------------------------------------------------------------- 1 | import G6 from "@antv/g6/build/g6"; 2 | import commonConfig from "../../config/commonConfig" 3 | const config = { 4 | defaultSize:[80], 5 | title:'x', 6 | ...commonConfig.node, 7 | inEdgeNum:1, 8 | outEdgeNum:10, 9 | }; 10 | 11 | 12 | const customBranchNode = { 13 | init(editor) { 14 | G6.registerNode("customBranchNode", { 15 | draw(cfg, group) { 16 | let currentMode = editor.getCurrentMode(); 17 | let r = parseInt(config.defaultSize[0]/2); 18 | 19 | cfg.size = [2*r]; 20 | 21 | 22 | const shape = group.addShape("circle", {//这个是主图形 23 | attrs: { 24 | id: cfg.id, 25 | x: 0, 26 | y: 0, 27 | r: r, 28 | isMain:true, 29 | stroke: config.strokeColor_grey, 30 | fill: '#fff',//此处必须有fill 不然不能触发事件 31 | lineDash: [4, 4],//虚线 32 | cursor:'move', 33 | lineWidth: config.lineWidth_thin, 34 | } 35 | }); 36 | 37 | 38 | let diamondR = parseInt(r * 0.6); 39 | group.addShape("polygon", {//多边形做成菱形 40 | attrs: { 41 | x: 0, 42 | y: 0, 43 | width: 2*diamondR, 44 | height: 2*diamondR, 45 | points:[[0,-diamondR],[-diamondR,0],[0,diamondR],[diamondR,0]], 46 | stroke: config.centerColor_blue, 47 | lineWidth:2, 48 | } 49 | }); 50 | group.addShape("text", { 51 | attrs: { 52 | isText:true, 53 | x: 0, 54 | y: 0, 55 | textAlign: "center", 56 | textBaseline: "middle", 57 | text: config.title, 58 | fill: config.centerColor_blue, 59 | fontSize: 30, 60 | cursor:'move', 61 | fontWeight:'bolder'//最粗的字体 62 | } 63 | }); 64 | group.addShape("circle", {//加一个透明的图形解决点击有的地方不好拖动的问题 65 | attrs: { 66 | x: 0, 67 | y: 0, 68 | r: r, 69 | fill: '#fff',//此处必须有fill 不然不能触发事件 70 | opacity: 0, 71 | cursor:'move', 72 | } 73 | }); 74 | if(config.deleteByIcon && currentMode === 'edit') { 75 | const offset_ = parseInt(0.8 * r); 76 | group.addShape("circle", {//删除的按钮 77 | attrs: { 78 | x: offset_, 79 | y: -offset_, 80 | r: config.defaultDeleteR, 81 | isDeleteButton: true, 82 | enabled: false, 83 | stroke: '#fff', 84 | lineWidth: 2, 85 | fill: '#FF111A', 86 | cursor: 'pointer', 87 | opacity: 0, 88 | } 89 | }); 90 | group.addShape("text", {//删除的按钮的叉叉 91 | attrs: { 92 | x: offset_, 93 | y: -offset_ + 0.95, 94 | textAlign: "center", 95 | textBaseline: "middle", 96 | text: '×', 97 | fontSize: 18, 98 | cursor: 'pointer', 99 | isDeleteButton: true, 100 | enabled: false, 101 | fill: '#fff', 102 | opacity: 0, 103 | fontWeight: 'bolder'//最粗的字体 104 | } 105 | }); 106 | } 107 | if(currentMode === 'edit') { 108 | for (let i = 0; i < config.anchorPoints.length; i++) {//画上下左右的4个点 109 | group.addShape("circle", { 110 | attrs: { 111 | x: parseInt(config.anchorPoints[i][0] * 2 * r - r), 112 | y: parseInt(config.anchorPoints[i][1] * 2 * r - r), 113 | r: 0, 114 | isAnchorPointBigger:true, 115 | anchorPointIndex:i, 116 | opacity: config.anchorBiggerOpacity, 117 | fill: config.anchorColor_blue, 118 | cursor:'crosshair', 119 | } 120 | }); 121 | group.addShape("circle", { 122 | attrs: { 123 | x: parseInt(config.anchorPoints[i][0] * 2 * r - r), 124 | y: parseInt(config.anchorPoints[i][1] * 2 * r - r), 125 | r: 0, 126 | isAnchorPoint:true, 127 | anchorPointIndex:i, 128 | opacity: 1, 129 | stroke: config.anchorColor_blue, 130 | fill: config.anchorColor_grey, 131 | cursor:'crosshair', 132 | } 133 | }); 134 | } 135 | } 136 | return shape; 137 | }, 138 | //设置状态 139 | setState(name, value, item) { 140 | let showOutAnchor = false; 141 | let showInAnchor = false; 142 | if((name === 'selected' || name === 'hover' || name === 'hover_addedge') && item._cfg.edges){ 143 | let existOutEdges = 0; 144 | let existInEdges = 0; 145 | item._cfg.edges.forEach(edge => { 146 | if (edge._cfg.sourceNode._cfg.id == item._cfg.id) { 147 | existOutEdges++; 148 | } 149 | if(edge._cfg.targetNode._cfg.id == item._cfg.id){ 150 | existInEdges++; 151 | } 152 | }); 153 | if(existOutEdges { 162 | return point._attrs.isMain; 163 | })[0]; 164 | const points = group.findAll(point => { 165 | return point._attrs.isAnchorPoint; 166 | }); 167 | const pointBiggers = group.findAll(point => { 168 | return point._attrs.isAnchorPointBigger; 169 | }); 170 | const deleteButtons = group.findAll(point => { 171 | return point._attrs.isDeleteButton; 172 | }); 173 | 174 | 175 | const selectStyles = () => { 176 | shape.attr("stroke", config.strokeColor_blue); 177 | shape.attr("lineWidth", config.lineWidth_bolder); 178 | 179 | if(showOutAnchor){ 180 | points.forEach(point => { 181 | point.attr('r', config.anchorR); 182 | point.attr('fill', config.anchorColor_grey); 183 | }); 184 | }else{ 185 | points.forEach(point => { 186 | point.attr('r', 0); 187 | }); 188 | } 189 | deleteButtons.forEach(button => { 190 | button.attr('opacity', 1); 191 | button.attr('enabled',true); 192 | }); 193 | }; 194 | const defaultStyles = () => { 195 | shape.attr("stroke", config.strokeColor_grey); 196 | shape.attr("lineWidth", config.lineWidth_thin); 197 | 198 | points.forEach(point => { 199 | point.attr('r', 0); 200 | }); 201 | pointBiggers.forEach(point => { 202 | point.attr('r', 0); 203 | }); 204 | deleteButtons.forEach(button => { 205 | button.attr('opacity', 0); 206 | button.attr('enabled',false); 207 | }); 208 | }; 209 | let hover_addedge = () => { 210 | if(showInAnchor){ 211 | points.forEach(point => { 212 | point.attr('r', config.anchorR); 213 | point.attr('fill', config.anchorColor_grey); 214 | }); 215 | pointBiggers.forEach(point => { 216 | point.attr('r', config.anchorR*3); 217 | }); 218 | }else{ 219 | points.forEach(point => { 220 | point.attr('r', 0); 221 | }); 222 | pointBiggers.forEach(point => { 223 | point.attr('r', 0); 224 | }); 225 | } 226 | }; 227 | const hover_changeedgesource = () => { 228 | //只是改个锚点,肯定满足出入的规则 229 | points.forEach(point => { 230 | point.attr('r', config.anchorR); 231 | point.attr('fill', config.anchorColor_grey); 232 | }); 233 | pointBiggers.forEach(point => { 234 | point.attr('r', config.anchorR*3); 235 | }); 236 | }; 237 | switch (name) { 238 | case "selected": 239 | case "hover": 240 | if (value) { 241 | selectStyles() 242 | } else { 243 | defaultStyles() 244 | } 245 | break; 246 | case "hover_addedge": 247 | if (value) { 248 | hover_addedge() 249 | } else { 250 | defaultStyles() 251 | } 252 | break; 253 | case "hover_changeedgesource": 254 | if (value) { 255 | hover_changeedgesource() 256 | } else { 257 | defaultStyles() 258 | } 259 | break; 260 | } 261 | } 262 | }); 263 | } 264 | } 265 | 266 | export default customBranchNode 267 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/custom/node/customParallelNode.js: -------------------------------------------------------------------------------- 1 | import G6 from "@antv/g6/build/g6"; 2 | import commonConfig from "../../config/commonConfig" 3 | const config = { 4 | defaultSize:[80], 5 | title:'=', 6 | ...commonConfig.node, 7 | inEdgeNum:10, 8 | outEdgeNum:1, 9 | }; 10 | 11 | const customParallelNode = { 12 | init(editor) { 13 | G6.registerNode("customParallelNode", { 14 | draw(cfg, group) { 15 | let currentMode = editor.getCurrentMode(); 16 | 17 | let r = parseInt(config.defaultSize[0]/2); 18 | cfg.size = [2*r]; 19 | 20 | 21 | const shape = group.addShape("circle", {//这个是主图形 22 | attrs: { 23 | id: cfg.id, 24 | x: 0, 25 | y: 0, 26 | r: r, 27 | isMain:true, 28 | stroke: config.strokeColor_grey, 29 | fill: '#fff',//此处必须有fill 不然不能触发事件 30 | lineDash: [4, 4],//虚线 31 | cursor:'move', 32 | lineWidth: config.lineWidth_thin, 33 | } 34 | }); 35 | 36 | let diamondR = parseInt(r * 0.6); 37 | group.addShape("polygon", {//多边形做成菱形 38 | attrs: { 39 | x: 0, 40 | y: 0, 41 | width: 2*diamondR, 42 | height: 2*diamondR, 43 | points:[[0,-diamondR],[-diamondR,0],[0,diamondR],[diamondR,0]], 44 | stroke: config.centerColor_blue, 45 | lineWidth:2, 46 | } 47 | }); 48 | group.addShape("text", { 49 | attrs: { 50 | isText:true, 51 | x: 0, 52 | y: 0, 53 | textAlign: "center", 54 | textBaseline: "middle", 55 | text: config.title, 56 | fill: config.centerColor_blue, 57 | fontSize: 30, 58 | cursor:'move', 59 | fontWeight:'bolder'//最粗的字体 60 | } 61 | }); 62 | group.addShape("circle", {//加一个透明的图形解决点击有的地方不好拖动的问题 63 | attrs: { 64 | x: 0, 65 | y: 0, 66 | r: r, 67 | fill: '#fff',//此处必须有fill 不然不能触发事件 68 | opacity: 0, 69 | cursor:'move', 70 | } 71 | }); 72 | if(config.deleteByIcon && currentMode === 'edit') { 73 | const offset_ = parseInt(0.8 * r); 74 | group.addShape("circle", {//删除的按钮 75 | attrs: { 76 | x: offset_, 77 | y: -offset_, 78 | r: config.defaultDeleteR, 79 | isDeleteButton: true, 80 | enabled: false, 81 | stroke: '#fff', 82 | lineWidth: 2, 83 | fill: '#FF111A', 84 | cursor: 'pointer', 85 | opacity: 0, 86 | } 87 | }); 88 | group.addShape("text", {//删除的按钮的叉叉 89 | attrs: { 90 | x: offset_, 91 | y: -offset_ + 0.95, 92 | textAlign: "center", 93 | textBaseline: "middle", 94 | text: '×', 95 | fontSize: 18, 96 | cursor: 'pointer', 97 | isDeleteButton: true, 98 | enabled: false, 99 | fill: '#fff', 100 | opacity: 0, 101 | fontWeight: 'bolder'//最粗的字体 102 | } 103 | }); 104 | if(currentMode === 'edit') { 105 | for (let i = 0; i < config.anchorPoints.length; i++) {//画上下左右的4个点 106 | group.addShape("circle", { 107 | attrs: { 108 | x: parseInt(config.anchorPoints[i][0] * 2 * r - r), 109 | y: parseInt(config.anchorPoints[i][1] * 2 * r - r), 110 | r: 0, 111 | isAnchorPointBigger:true, 112 | anchorPointIndex:i, 113 | opacity: config.anchorBiggerOpacity, 114 | fill: config.anchorColor_blue, 115 | cursor:'crosshair', 116 | } 117 | }); 118 | group.addShape("circle", { 119 | attrs: { 120 | x: parseInt(config.anchorPoints[i][0] * 2 * r - r), 121 | y: parseInt(config.anchorPoints[i][1] * 2 * r - r), 122 | r: 0, 123 | isAnchorPoint:true, 124 | anchorPointIndex:i, 125 | opacity: 1, 126 | stroke: config.anchorColor_blue, 127 | fill: config.anchorColor_grey, 128 | cursor:'crosshair', 129 | } 130 | }); 131 | } 132 | } 133 | } 134 | return shape; 135 | }, 136 | //设置状态 137 | setState(name, value, item) { 138 | let showOutAnchor = false; 139 | let showInAnchor = false; 140 | if((name === 'selected' || name === 'hover' || name === 'hover_addedge') && item._cfg.edges){ 141 | let existOutEdges = 0; 142 | let existInEdges = 0; 143 | item._cfg.edges.forEach(edge => { 144 | if (edge._cfg.sourceNode._cfg.id == item._cfg.id) { 145 | existOutEdges++; 146 | } 147 | if(edge._cfg.targetNode._cfg.id == item._cfg.id){ 148 | existInEdges++; 149 | } 150 | }); 151 | if(existOutEdges { 160 | return point._attrs.isMain; 161 | })[0]; 162 | const points = group.findAll(point => { 163 | return point._attrs.isAnchorPoint; 164 | }); 165 | const pointBiggers = group.findAll(point => { 166 | return point._attrs.isAnchorPointBigger; 167 | }); 168 | const deleteButtons = group.findAll(point => { 169 | return point._attrs.isDeleteButton; 170 | }); 171 | 172 | 173 | const selectStyles = () => { 174 | shape.attr("stroke", config.strokeColor_blue); 175 | shape.attr("lineWidth", config.lineWidth_bolder); 176 | 177 | if(showOutAnchor){ 178 | points.forEach(point => { 179 | point.attr('r', config.anchorR); 180 | point.attr('fill', config.anchorColor_grey); 181 | }); 182 | }else{ 183 | points.forEach(point => { 184 | point.attr('r', 0); 185 | }); 186 | } 187 | deleteButtons.forEach(button => { 188 | button.attr('opacity', 1); 189 | button.attr('enabled',true); 190 | }); 191 | }; 192 | const defaultStyles = () => { 193 | shape.attr("stroke", config.strokeColor_grey); 194 | shape.attr("lineWidth", config.lineWidth_thin); 195 | 196 | points.forEach(point => { 197 | point.attr('r', 0); 198 | }); 199 | pointBiggers.forEach(point => { 200 | point.attr('r', 0); 201 | }); 202 | deleteButtons.forEach(button => { 203 | button.attr('opacity', 0); 204 | button.attr('enabled',false); 205 | }); 206 | }; 207 | let hover_addedge = () => { 208 | if(showInAnchor){ 209 | points.forEach(point => { 210 | point.attr('r', config.anchorR); 211 | point.attr('fill', config.anchorColor_grey); 212 | }); 213 | pointBiggers.forEach(point => { 214 | point.attr('r', config.anchorR*3); 215 | }); 216 | }else{ 217 | points.forEach(point => { 218 | point.attr('r', 0); 219 | }); 220 | pointBiggers.forEach(point => { 221 | point.attr('r', 0); 222 | }); 223 | } 224 | }; 225 | const hover_changeedgesource = () => { 226 | //只是改个锚点,肯定满足出入的规则 227 | points.forEach(point => { 228 | point.attr('r', config.anchorR); 229 | point.attr('fill', config.anchorColor_grey); 230 | }); 231 | pointBiggers.forEach(point => { 232 | point.attr('r', config.anchorR*3); 233 | }); 234 | }; 235 | switch (name) { 236 | case "selected": 237 | case "hover": 238 | if (value) { 239 | selectStyles() 240 | } else { 241 | defaultStyles() 242 | } 243 | break; 244 | case "hover_addedge": 245 | if (value) { 246 | hover_addedge() 247 | } else { 248 | defaultStyles() 249 | } 250 | break; 251 | case "hover_changeedgesource": 252 | if (value) { 253 | hover_changeedgesource() 254 | } else { 255 | defaultStyles() 256 | } 257 | break; 258 | } 259 | } 260 | }); 261 | 262 | } 263 | } 264 | 265 | export default customParallelNode 266 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/custom/node/customToolNode.js: -------------------------------------------------------------------------------- 1 | import G6 from "@antv/g6/build/g6"; 2 | import commonConfig from "../../config/commonConfig" 3 | import {getShowNodeTitle} from "../../utils/commonCaculate" 4 | 5 | const config = { 6 | defaultSize:[130,70], 7 | title:'工具', 8 | ...commonConfig.node, 9 | inEdgeNum:10, 10 | outEdgeNum:1, 11 | unConfiguredColor:'#ABB8CC', 12 | }; 13 | 14 | const customToolNode = { 15 | init(editor) { 16 | G6.registerNode("customToolNode", { 17 | draw(cfg, group) { 18 | let currentMode = editor.getCurrentMode(); 19 | let width = config.defaultSize[0]; 20 | let height = config.defaultSize[1]; 21 | let title = getShowNodeTitle(cfg.title); 22 | 23 | cfg.size = [width,height]; 24 | 25 | let centerColor = config.centerColor_blue; 26 | if(cfg.unConfigured){ 27 | centerColor = config.unConfiguredColor; 28 | } 29 | 30 | const shape = group.addShape("rect", {//这个是主图形 31 | attrs: { 32 | id: cfg.id, 33 | x: -parseInt(width/2), 34 | y: -parseInt(height/2), 35 | width: width, 36 | height: height, 37 | isMain:true, 38 | stroke: config.strokeColor_grey, 39 | fill: config.centerColor_white,//此处必须有fill 不然不能触发事件 40 | lineWidth: config.lineWidth_thin, 41 | 42 | } 43 | }); 44 | 45 | group.addShape("rect", { 46 | attrs: { 47 | x: -parseInt(width/2), 48 | y: parseInt(height*0.2), 49 | isCenterShape:true, 50 | width: width, 51 | height: parseInt(height*0.3), 52 | fill: centerColor,//此处必须有fill 不然不能触发事件 53 | } 54 | }); 55 | 56 | group.addShape("text", { 57 | attrs: { 58 | x: 0, 59 | y: -parseInt(height*0.1), 60 | textAlign: "center", 61 | textBaseline: "middle", 62 | text: title, 63 | fill: config.centerColor_blue, 64 | fontSize: 16, 65 | // fontWeight:'bolder'//最粗的字体 66 | } 67 | }); 68 | group.addShape("rect", {//加一个透明的图形解决点击有的地方不好拖动的问题 69 | attrs: { 70 | x: -parseInt(width/2), 71 | y: -parseInt(height/2), 72 | width: width, 73 | height: height, 74 | 75 | fill: '#fff',//此处必须有fill 不然不能触发事件 76 | cursor:'move', 77 | opacity: 0, 78 | } 79 | }); 80 | if(config.deleteByIcon && currentMode === 'edit') { 81 | group.addShape("circle", {//删除的按钮 82 | attrs: { 83 | x: parseInt(width / 2), 84 | y: -parseInt(height / 2), 85 | r: parseInt(config.defaultDeleteR), 86 | isDeleteButton: true, 87 | enabled: false, 88 | stroke: '#fff', 89 | lineWidth: 2, 90 | fill: '#FF111A', 91 | cursor: 'pointer', 92 | opacity: 0, 93 | } 94 | }); 95 | group.addShape("text", {//删除的按钮的叉叉 96 | attrs: { 97 | x: parseInt(width / 2), 98 | y: -parseInt(height / 2) + 0.95, 99 | textAlign: "center", 100 | textBaseline: "middle", 101 | text: '×', 102 | fontSize: 18, 103 | cursor: 'pointer', 104 | isDeleteButton: true, 105 | enabled: false, 106 | fill: '#fff', 107 | opacity: 0, 108 | fontWeight: 'bolder'//最粗的字体 109 | } 110 | }); 111 | } 112 | 113 | 114 | if(currentMode === 'edit') { 115 | for (let i = 0; i < config.anchorPoints.length; i++) {//画上下左右的4个点 116 | group.addShape("circle", { 117 | attrs: { 118 | x: parseInt(config.anchorPoints[i][0] * width - 0.5 * width), 119 | y: parseInt(config.anchorPoints[i][1] * height - 0.5 * height), 120 | r: 0, 121 | isAnchorPointBigger:true, 122 | anchorPointIndex:i, 123 | opacity: config.anchorBiggerOpacity, 124 | fill: config.anchorColor_blue, 125 | cursor:'crosshair', 126 | } 127 | }); 128 | group.addShape("circle", { 129 | attrs: { 130 | x: parseInt(config.anchorPoints[i][0] * width - 0.5 * width), 131 | y: parseInt(config.anchorPoints[i][1] * height - 0.5 * height), 132 | r: 0, 133 | isAnchorPoint:true, 134 | anchorPointIndex:i, 135 | opacity: 1, 136 | stroke: config.anchorColor_blue, 137 | fill: config.anchorColor_grey, 138 | cursor:'crosshair', 139 | } 140 | }); 141 | 142 | } 143 | } 144 | return shape; 145 | }, 146 | 147 | //设置状态 148 | setState(name, value, item) { 149 | let showOutAnchor = false; 150 | let showInAnchor = false; 151 | if((name === 'selected' || name === 'hover' || name === 'hover_addedge') && item._cfg.edges){ 152 | let existOutEdges = 0; 153 | let existInEdges = 0; 154 | item._cfg.edges.forEach(edge => { 155 | if (edge._cfg.sourceNode._cfg.id === item._cfg.id) { 156 | existOutEdges++; 157 | } 158 | if(edge._cfg.targetNode._cfg.id === item._cfg.id){ 159 | existInEdges++; 160 | } 161 | }); 162 | if(existOutEdges { 171 | return point._attrs.isMain; 172 | })[0]; 173 | const centerShapes = group.findAll(center => { 174 | return center._attrs.isCenterShape; 175 | }); 176 | const points = group.findAll(point => { 177 | return point._attrs.isAnchorPoint; 178 | }); 179 | const pointBiggers = group.findAll(point => { 180 | return point._attrs.isAnchorPointBigger; 181 | }); 182 | const deleteButtons = group.findAll(point => { 183 | return point._attrs.isDeleteButton; 184 | }); 185 | 186 | 187 | const selectStyles = () => { 188 | shape.attr("stroke", config.strokeColor_blue); 189 | shape.attr("lineWidth", config.lineWidth_bolder); 190 | 191 | if(showOutAnchor){ 192 | points.forEach(point => { 193 | point.attr('r', config.anchorR); 194 | point.attr('fill', config.anchorColor_grey); 195 | }); 196 | }else{ 197 | points.forEach(point => { 198 | point.attr('r', 0); 199 | }); 200 | } 201 | deleteButtons.forEach(button => { 202 | button.attr('opacity', 1); 203 | button.attr('enabled',true); 204 | }); 205 | }; 206 | let hover_addedge = () => { 207 | if(showInAnchor){ 208 | points.forEach(point => { 209 | point.attr('r', config.anchorR); 210 | point.attr('fill', config.anchorColor_grey); 211 | }); 212 | pointBiggers.forEach(point => { 213 | point.attr('r', config.anchorR*3); 214 | }); 215 | }else{ 216 | points.forEach(point => { 217 | point.attr('r', 0); 218 | }); 219 | pointBiggers.forEach(point => { 220 | point.attr('r', 0); 221 | }); 222 | } 223 | }; 224 | const hover_changeedgesource = () => { 225 | //只是改个锚点,肯定满足出入的规则 226 | points.forEach(point => { 227 | point.attr('r', config.anchorR); 228 | point.attr('fill', config.anchorColor_grey); 229 | }); 230 | pointBiggers.forEach(point => { 231 | point.attr('r', config.anchorR*3); 232 | }); 233 | }; 234 | const defaultStyles = () => { 235 | shape.attr("stroke", config.strokeColor_grey); 236 | shape.attr("lineWidth", config.lineWidth_thin); 237 | points.forEach(point => { 238 | point.attr('r', 0); 239 | }); 240 | pointBiggers.forEach(point => { 241 | point.attr('r', 0); 242 | }); 243 | 244 | 245 | deleteButtons.forEach(button => { 246 | button.attr('opacity', 0); 247 | button.attr('enabled',false); 248 | }); 249 | 250 | }; 251 | 252 | switch (name) { 253 | case "selected": 254 | case "hover": 255 | if (value) { 256 | selectStyles() 257 | } else { 258 | defaultStyles() 259 | } 260 | break; 261 | case "hover_addedge": 262 | if (value) { 263 | hover_addedge() 264 | } else { 265 | defaultStyles() 266 | } 267 | break; 268 | case "hover_changeedgesource": 269 | if (value) { 270 | hover_changeedgesource() 271 | } else { 272 | defaultStyles() 273 | } 274 | break; 275 | 276 | } 277 | } 278 | }); 279 | } 280 | } 281 | 282 | export default customToolNode 283 | -------------------------------------------------------------------------------- /src/views/flowChart/Editor/custom/edge/customPolyEdge.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 修改自 https://github.com/OXOYO/X-Flowchart-Vue 3 | */ 4 | 5 | import G6 from "@antv/g6/build/g6"; 6 | import { polylineFinding } from './polylineFinding' 7 | import { customPolylineFinding } from './customPolylineFinding' 8 | import { getPointOffset, getEdgeStartOrEndPoint } from '../../utils/commonCaculate' 9 | import commonConfig from "../../config/commonConfig" 10 | 11 | const config = { 12 | ...commonConfig.edge, 13 | }; 14 | 15 | const selectStyle = { 16 | strokeColor:'#64a1ec',//线的颜色 17 | strokeWidth: 2, 18 | }; 19 | 20 | 21 | const customPolyEdge = { 22 | init(editor) { 23 | G6.registerEdge('customPolyEdge', { 24 | draw (cfg, group) { 25 | let currentMode = editor.getCurrentMode(); 26 | let source = cfg.source; 27 | let target = cfg.target; 28 | let title = cfg.title; 29 | if(!source._cfg && source.x == null){//source传的是id 30 | source = editor.findById(cfg.source); 31 | } 32 | if(!target._cfg && target.x == null){ 33 | target = editor.findById(cfg.target); 34 | } 35 | if(cfg.sourceAnchorIndex == null){ 36 | cfg.sourceAnchorIndex = 1; 37 | } 38 | if(cfg.targetAnchorIndex == null){ 39 | cfg.targetAnchorIndex = 0; 40 | } 41 | 42 | const startPoint = getEdgeStartOrEndPoint(cfg.startPointOffset, source, cfg.sourceAnchorIndex); 43 | const endPoint = getEdgeStartOrEndPoint(cfg.endPointOffset, target, cfg.targetAnchorIndex); 44 | 45 | cfg.source = source;//预置的连线没有这个参数,但是这个参数是必须的 46 | cfg.target = target; 47 | 48 | if((title == null || title === '') && source._cfg.model.type === 'exclusiveGateway'){ 49 | title = '默认'; 50 | } 51 | 52 | // const controlPoints = this.getControlPoints(cfg.controlPoints,source, target, startPoint, endPoint); 53 | // let points = [startPoint]; 54 | // if (controlPoints) { 55 | // points.push(...controlPoints) 56 | // } 57 | // points.push(endPoint); 58 | let points = customPolylineFinding(startPoint, endPoint, cfg.sourceAnchorIndex, cfg.targetAnchorIndex); 59 | 60 | let path = this.getPath(points); 61 | const keyShape = group.addShape('path', { 62 | className: 'edge-shape', 63 | attrs: { 64 | path: path, 65 | isEdge: true, 66 | stroke: config.strokeColor_grey, 67 | lineAppendWidth: 10,//边响应鼠标事件时的检测宽度, 68 | lineWidth: config.strokeWidth_thin,//边的宽度 69 | endArrow: { 70 | path: 'M 8,0 L -6,-6 L -6,6 Z', 71 | d: 8, 72 | }, 73 | } 74 | }); 75 | 76 | 77 | 78 | 79 | let first = 0; 80 | let second = 1; 81 | //取points的最中间的2个点的下标 82 | if (points.length > 3) { 83 | first = parseInt(points.length / 2) -1; 84 | second = first + 1; 85 | } 86 | 87 | let middlePoint = this.getMiddlePoint(points);//因为获取到的points有问题,所以根据距离来算不行 88 | const iconX = middlePoint.x; 89 | const iconY = middlePoint.y; 90 | 91 | if(title != null && title !==''){ 92 | group.addShape("text", {//线上的文字 93 | attrs: { 94 | x: iconX, 95 | y: iconY-5, 96 | textAlign: "center", 97 | textBaseline: "middle", 98 | text: title, 99 | fontSize: 18, 100 | fill: '#000000', 101 | } 102 | }); 103 | } 104 | if (currentMode === 'edit') { 105 | 106 | //增加透明的可以用来拖动线的按钮 107 | let sourceParam= this.getMoveBtnSizeAndOffset(cfg.sourceAnchorIndex); 108 | let targetParam = this.getMoveBtnSizeAndOffset(cfg.targetAnchorIndex); 109 | group.addShape("rect", {//线两头的可拖动的透明按钮 110 | attrs: { 111 | x: points[0].x+sourceParam.offsetX, 112 | y: points[0].y+sourceParam.offsetY, 113 | fill: '#ff1a21', 114 | width: sourceParam.width, 115 | height: sourceParam.height, 116 | sourceMoveBtn:true, 117 | opacity: 0, 118 | cursor:'move', 119 | } 120 | }); 121 | group.addShape("rect", {//线两头的可拖动的透明按钮 122 | attrs: { 123 | x: points[points.length-1].x+targetParam.offsetX, 124 | y: points[points.length-1].y+targetParam.offsetY, 125 | fill: '#18ff40', 126 | width: targetParam.width, 127 | height: targetParam.height, 128 | targetMoveBtn:true, 129 | opacity: 0, 130 | cursor:'move', 131 | } 132 | }); 133 | 134 | if(config.deleteByIcon){ 135 | group.addShape("circle", {//删除的按钮 136 | attrs: { 137 | x: iconX, 138 | y: iconY, 139 | r: 8, 140 | isDeleteButton: true, 141 | enabled: false, 142 | stroke: '#fff', 143 | lineWidth: 2, 144 | fill: '#FF111A', 145 | cursor: 'pointer', 146 | opacity: 0, 147 | } 148 | }); 149 | 150 | group.addShape("text", {//删除的按钮的叉叉 151 | attrs: { 152 | x: iconX, 153 | y: iconY + 0.95, 154 | textAlign: "center", 155 | textBaseline: "middle", 156 | text: '×', 157 | fontSize: 18, 158 | cursor: 'pointer', 159 | isDeleteButton: true, 160 | enabled: false, 161 | fill: '#fff', 162 | opacity: 0, 163 | fontWeight: 'bolder'//最粗的字体 164 | } 165 | }); 166 | } 167 | } 168 | 169 | return keyShape 170 | }, 171 | 172 | getMoveBtnSizeAndOffset(anchorIndex){ 173 | let width = 10; 174 | let height = 20; 175 | let offsetX = 0; 176 | let offsetY = 0; 177 | switch(anchorIndex){ 178 | case 0: 179 | width = 10; 180 | height = 20; 181 | offsetX = -5; 182 | offsetY = -20; 183 | break; 184 | case 1: 185 | width = 10; 186 | height = 20; 187 | offsetX = -5; 188 | offsetY = 0; 189 | break; 190 | case 2: 191 | width = 20; 192 | height = 10; 193 | offsetX = -20; 194 | offsetY = -5; 195 | break; 196 | case 3: 197 | width = 20; 198 | height = 10; 199 | offsetX = 0; 200 | offsetY = -5; 201 | break; 202 | default: 203 | width = 20; 204 | height = 10; 205 | offsetX = 0; 206 | offsetY = -5; 207 | break 208 | } 209 | return {width:width,height:height,offsetX:offsetX,offsetY:offsetY} 210 | }, 211 | 212 | getPath (points) { 213 | const path = [] 214 | for (let i = 0; i < points.length; i++) { 215 | const point = points[i] 216 | if (i === 0) { 217 | path.push([ 'M', point.x, point.y ]) 218 | } else if (i === points.length - 1) { 219 | path.push([ 'L', point.x, point.y ]) 220 | } else { 221 | const prevPoint = points[i - 1] 222 | let nextPoint = points[i + 1] 223 | let cornerLen = 5 224 | if (Math.abs(point.y - prevPoint.y) > cornerLen || Math.abs(point.x - prevPoint.x) > cornerLen) { 225 | if (prevPoint.x === point.x) { 226 | path.push(['L', point.x, point.y > prevPoint.y ? point.y - cornerLen : point.y + cornerLen]) 227 | } else if (prevPoint.y === point.y) { 228 | path.push(['L', point.x > prevPoint.x ? point.x - cornerLen : point.x + cornerLen, point.y]) 229 | } 230 | } 231 | const yLen = Math.abs(point.y - nextPoint.y) 232 | const xLen = Math.abs(point.x - nextPoint.x) 233 | if (yLen > 0 && yLen < cornerLen) { 234 | cornerLen = yLen 235 | } else if (xLen > 0 && xLen < cornerLen) { 236 | cornerLen = xLen 237 | } 238 | if (prevPoint.x !== nextPoint.x && nextPoint.x === point.x) { 239 | path.push(['Q', point.x, point.y, point.x, point.y > nextPoint.y ? point.y - cornerLen : point.y + cornerLen]) 240 | } else if (prevPoint.y !== nextPoint.y && nextPoint.y === point.y) { 241 | path.push(['Q', point.x, point.y, point.x > nextPoint.x ? point.x - cornerLen : point.x + cornerLen, point.y]) 242 | } 243 | } 244 | } 245 | return path 246 | }, 247 | getControlPoints (controlPoints, source, target, startPoint, endPoint) { 248 | if (!source) { 249 | return controlPoints; 250 | } 251 | return polylineFinding(source, target, startPoint, endPoint, 30); 252 | }, 253 | 254 | getMiddlePoint(points){ 255 | let distance = 0; 256 | let length = points.length; 257 | let array = [];//相邻两个点之间的距离 258 | for(let i = 0; i < length-1; i++) { 259 | let temp = 0; 260 | if(Math.abs(points[i].x - points[i+1].x) < 1){ 261 | temp = Math.abs(points[i].y - points[i+1].y) 262 | }else{ 263 | temp = Math.abs(points[i].x - points[i+1].x) 264 | } 265 | array.push(temp); 266 | distance += temp; 267 | } 268 | distance = parseInt(distance/2); 269 | let point = {x:points[0].x,y:points[0].y}; 270 | for(let j = 0; j < length-1; j++){ 271 | if(distance - array[j] < 0){//说明中间点在第i和第i+1个点中间 272 | if(Math.abs(points[j].x - points[j+1].x) < 1){//x坐标相同 273 | if(points[j].y>points[j+1].y){ 274 | point.y = points[j].y-distance; 275 | }else{ 276 | point.y = points[j].y+distance; 277 | } 278 | point.x = points[j].x; 279 | }else{ 280 | if(points[j].x>points[j+1].x){ 281 | point.x = points[j].x-distance; 282 | }else{ 283 | point.x = points[j].x+distance; 284 | } 285 | point.y = points[j].y; 286 | } 287 | break; 288 | }else{ 289 | distance -= array[j] 290 | } 291 | } 292 | return point; 293 | }, 294 | setState(name, value, item) { 295 | const group = item.getContainer(); 296 | const shape = group.findAll(point => { 297 | return point._attrs.isEdge; 298 | })[0]; 299 | const deleteButtons = group.findAll(point => { 300 | return point._attrs.isDeleteButton; 301 | }); 302 | const selectStyles = () => { 303 | shape.attr("stroke", config.strokeColor_blue); 304 | shape.attr("lineWidth", config.strokeWidth_bolder); 305 | 306 | deleteButtons.forEach(button => { 307 | button.attr('opacity', 1); 308 | button.attr('enabled',true); 309 | }); 310 | }; 311 | const unSelectStyles = () => { 312 | shape.attr("stroke", config.strokeColor_grey); 313 | shape.attr("lineWidth", config.strokeWidth_thin); 314 | deleteButtons.forEach(button => { 315 | button.attr('opacity', 0); 316 | button.attr('enabled',false); 317 | }); 318 | }; 319 | 320 | switch (name) { 321 | case "selected": 322 | case "hover": 323 | if (value) { 324 | selectStyles() 325 | } else { 326 | unSelectStyles() 327 | } 328 | break; 329 | } 330 | } 331 | }); 332 | } 333 | } 334 | 335 | export default customPolyEdge 336 | -------------------------------------------------------------------------------- /src/views/treetopo/component/subtopo.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 261 | -------------------------------------------------------------------------------- /src/jsondata/togo.js: -------------------------------------------------------------------------------- 1 | import d3 from 'd3' 2 | export class Togo { 3 | /**/ 4 | constructor(svg, option) { 5 | this.data = option.data; 6 | this.edges = option.edges; 7 | this.svg = d3.select(svg); 8 | 9 | } 10 | 11 | //初始化节点位置 12 | initPosition () { 13 | let width = this.svg.attr('width'); 14 | let points = this.getVertices(this.data.length); 15 | this.data.forEach((item, i) => { 16 | // 加上width/2是为了让所有节点的位置在中间 17 | item.x = points[i].x + width / 2; 18 | item.y = points[i].y; 19 | }) 20 | } 21 | 22 | //根据节点的个数,生成矩形阵列中所有节点的定位坐标[{x:..,y:...},...] 23 | getVertices (n) { 24 | if (typeof n !== 'number') return; 25 | var i = 0; 26 | var j = 0; 27 | var k = 0 28 | var points = []; 29 | while (k < n) { 30 | points.push({ 31 | x: 100 + 200 * i, 32 | y: 100 + 200 * j, 33 | }); 34 | if (i < 2) { 35 | i++; 36 | } else { 37 | i = 0 38 | j++ 39 | } 40 | k++ 41 | } 42 | return points; 43 | } 44 | 45 | // // 计算两点的中心点 46 | // getCenter (x1, y1, x2, y2) { 47 | // return [(x1 + x2) / 2, (y1 + y2) / 2] 48 | // } 49 | 50 | // // 计算两点的距离 51 | // getDistance (x1, y1, x2, y2) { 52 | // return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); 53 | // } 54 | 55 | // // 计算两点角度 56 | // getAngle (x1, y1, x2, y2) { 57 | // var x = Math.abs(x1 - x2); 58 | // var y = Math.abs(y1 - y2); 59 | // var z = Math.sqrt(x * x + y * y); 60 | // return Math.round((Math.asin(y / z) / Math.PI * 180)); 61 | // } 62 | 63 | // 初始化缩放器 64 | initZoom () { 65 | let self = this; 66 | let zoom = d3.zoom() 67 | .scaleExtent([0.7, 3]) 68 | .on('zoom', function () { 69 | self.onZoom(this) 70 | }); 71 | this.svg.call(zoom) 72 | } 73 | 74 | // 初始化图标库 75 | initDefineSymbol () { 76 | // defs用于预定义一个元素使其能够在SVG图像中重复使用,我们defs标签中的g元素必须在元素上设置一个ID,通过ID来引用它。 77 | let defs = this.container.append('svg:defs'); 78 | // 向defs中添加箭头元素 79 | defs 80 | .selectAll('marker') 81 | .data(this.edges) 82 | .enter() 83 | .append('svg:marker') 84 | .attr('id', (link, i) => 'marker-' + i) 85 | .attr('markerUnits', 'userSpaceOnUse') 86 | .attr('viewBox', '0 -5 10 10') 87 | .attr('refX', symbolSize / 2 + padding) 88 | .attr('refY', 0) 89 | .attr('markerWidth', 14) 90 | .attr('markerHeight', 14) 91 | .attr('orient', 'auto') 92 | .attr('stroke-width', 2) 93 | .append('svg:path') 94 | .attr('d', 'M2,0 L0,-3 L9,0 L0,3 M2,0 L0,-3') 95 | .attr('class', 'arrow') 96 | 97 | // console.log(marker) 98 | // 向defs中添加数据库 99 | defs.append('g') 100 | .attr('id', 'database') 101 | .attr('transform', 'scale(0.042)').append('path') 102 | .attr('d', 'M512 800c-247.42 0-448-71.63-448-160v160c0 88.37 200.58 160 448 160s448-71.63 448-160V640c0 88.37-200.58 160-448 160z M512 608c-247.42 0-448-71.63-448-160v160c0 88.37 200.58 160 448 160s448-71.63 448-160V448c0 88.37-200.58 160-448 160z M512 416c-247.42 0-448-71.63-448-160v160c0 88.37 200.58 160 448 160s448-71.63 448-160V256c0 88.37-200.58 160-448 160z M64 224a448 160 0 1 0 896 0 448 160 0 1 0-896 0Z') 103 | .attr('style', "fill:#297aff") 104 | 105 | // 向defs中添加云 106 | defs.append('g') 107 | .attr('id', 'cloud') 108 | .attr('transform', 'scale(0.042)') 109 | .append('path') 110 | .attr('d', 'M709.3 285.8C668.3 202.7 583 145.4 484 145.4c-132.6 0-241 102.8-250.4 233-97.5 27.8-168.5 113-168.5 213.8 0 118.9 98.8 216.6 223.4 223.4h418.9c138.7 0 251.3-118.8 251.3-265.3 0-141.2-110.3-256.2-249.4-264.5z') 111 | 112 | // 向defs中添加地球 113 | let earth = defs.append('g') 114 | .attr('id', "earth") 115 | .attr('transform', 'scale(0.042)'); 116 | 117 | earth.append("path") 118 | .attr("d", 'm 973.70505,457.95556 c -9.82626,-86.10909 -43.70101,-167.56364 -97.74545,-235.57172 -12.54142,6.07677 -24.69495,12.15353 -36.7192,17.71313 3.23233,14.48081 6.46465,29.34949 9.30909,45.12323 10.73132,58.69899 18.61819,131.7495 17.71314,218.76364 36.71919,-13.83434 72.53333,-28.70303 107.44242,-46.02828 z M 224.32323,247.59596 c -2.84444,8.40404 -6.07677,17.19596 -8.79192,26.50505 -17.71313,59.08687 -30.25454,134.46465 -26.50505,227.55556 39.04647,15.38585 84.29899,30.25454 134.98182,41.8909 57.66465,13.05859 126.57778,22.75556 204.8,22.36768 V 310.9495 h -8.40404 c -101.49495,0 -202.47273,-21.46263 -296.08081,-63.35354 z M 550.14141,48.355556 V 289.87475 c 80.03233,-3.23233 168.98586,-20.94546 264.40405,-61.41414 -7.88687,-31.67677 -17.71314,-62.3192 -28.83233,-92.57374 C 716.41212,85.20404 635.34546,54.949495 550.14141,48.355556 Z M 52.622222,432.87273 c 21.850505,13.96363 61.414138,37.23636 115.846468,59.99192 -2.84445,-92.18586 10.21414,-167.04647 27.92727,-226.26263 2.84444,-9.82626 6.07677,-19.13535 9.30909,-27.92727 -20.42828,-9.82627 -37.23636,-19.52324 -49.77778,-27.02223 C 102.4,275.13535 66.973738,351.41818 52.622222,432.87273 Z') 119 | 120 | earth.append("path").attr('d', 'm 845.31717,511.09495 c 1.42222,-104.72727 -9.82626,-192.25859 -25.6,-262.46465 -96.8404,40.98586 -187.60404,58.69899 -269.05858,61.93132 v 255.09495 c 100.07272,-2.97374 199.2404,-21.59192 294.65858,-54.56162 z') 121 | 122 | earth.append("path").attr('d', 'M 845.70505,727.53131 C 882.94142,708.00808 918.75556,685.6404 952.7596,660.0404 969.0505,612.07273 976.93737,562.29495 976.93737,512 c 0,-10.73131 -0.51717,-21.46263 -1.42222,-32.06465 -37.23636,18.10101 -73.95555,33.09899 -110.28687,46.02829 -1.8101,67.49091 -8.27474,134.98181 -19.52323,201.56767 z M 169.50303,516.13737 C 128.12929,499.32929 88.048485,478.90101 49.389899,455.62828 41.890909,516.65455 46.545455,578.45657 62.836364,637.67273 107.05455,667.4101 153.08283,694.04444 201.56768,716.8 182.9495,642.84444 172.73535,576.25859 169.50303,516.13737 Z m 360.2101,283.5394 V 586.47273 h -3.23232 c -114.55354,0 -228.5899,-20.94546 -335.64445,-61.93132 4.13738,60.50909 14.86869,128.51718 35.42627,202.9899 35.81414,15.77374 74.9899,30.25455 117.26868,42.40808 60.50909,17.58384 122.82829,27.41011 186.18182,29.73738 z M 208.93737,742.4 C 162.00404,720.93737 116.88081,696.7596 73.050505,668.83232 108.86465,768.38788 177.26061,852.68687 267.11919,908.02424 244.36364,854.10909 224.8404,798.77172 208.93737,742.4 Z m 320.77576,76.8 c -65.16364,-1.8101 -129.8101,-11.63636 -192.25859,-29.73737 -35.42626,-10.21415 -69.81818,-22.36768 -103.30505,-36.33132 15.77374,53.52728 36.7192,111.19192 63.35354,171.70101 65.68081,34.90909 139.63636,52.62222 214.10909,52.62222 6.07677,0 12.15354,-0.51717 18.10101,-0.51717 z M 213.20404,219.28081 c 15.77374,-40.46869 34.39192,-74.9899 52.62222,-102.91717 -35.42626,21.8505 -67.49091,48.87272 -95.93535,79.12727 11.24848,6.98182 25.6,15.38586 43.31313,23.7899 z m 628.36364,532.94545 c -6.46465,35.42627 -13.96364,71.62829 -23.27273,109.89899 52.10505,-46.02828 93.60808,-102.91717 121.01818,-166.65858 -30.77172,20.81616 -63.35353,39.95151 -97.74545,56.75959 z') 123 | 124 | earth.append("path").attr('d', 'm 550.14141,585.95556 v 213.59191 c 95.41819,0 186.69899,-20.42828 272.80809,-61.02626 13.05858,-74.47272 19.52323,-142.86869 21.46262,-205.70505 -95.0303,32.71111 -194.19798,50.42424 -294.27071,53.1394 z') 125 | 126 | earth.append("path").attr('d', "m 818.81212,762.82828 c -84.81616,37.23637 -175.96768,56.88889 -268.67071,56.88889 v 155.92727 c 87.53132,-6.98181 171.31314,-39.04646 241.5192,-92.18585 10.73131,-41.50303 20.0404,-81.97172 27.15151,-120.63031 z m 15.25657,-543.15959 c 9.30909,-4.13738 18.61818,-8.79192 27.92727,-13.44647 -13.96364,-16.29091 -29.34949,-31.15959 -45.12323,-45.12323 6.07677,17.58384 11.63636,37.23636 17.19596,58.5697 z m -602.24647,8.40404 c 93.60808,42.40808 194.97374,63.35353 297.37374,62.31919 V 47.062626 C 453.30101,43.830303 377.40606,59.60404 309.0101,92.70303 c -25.08283,31.54748 -53.91515,76.67071 -77.18788,135.3697 z") 127 | // .attr('style', "fill:#297aff") 128 | earth.selectAll("path").attr('style', "fill:#297aff") 129 | } 130 | 131 | // 132 | //初始化链接线 133 | initLink () { 134 | this.drawLinkLine(); 135 | // this.drawLinkText(); 136 | } 137 | 138 | //初始化节点 139 | initNode () { 140 | var self = this; 141 | //节点容器 142 | this.nodes = this.container.selectAll(".node") 143 | .data(this.data) 144 | .enter() 145 | .append("g") 146 | .attr("transform", function (d) { 147 | return "translate(" + d.x + "," + d.y + ")"; 148 | }) 149 | .call(d3.drag() 150 | // 给每一个节点添加拖拽事件 151 | .on("drag", function (d) { 152 | console.log("111") 153 | self.onDrag(this, d) 154 | }) 155 | ) 156 | // 给每一个节点添加点击事件 157 | .on('click', function (d) { 158 | console.log(d.name) 159 | that.dialogVisible = true 160 | }) 161 | //节点背景默认背景层 162 | this.nodes.append('circle') 163 | .attr('r', symbolSize / 2 + padding) 164 | .attr('class', 'node-bg').attr("opacity", "1"); 165 | 166 | //节点图标 167 | this.drawNodeSymbol(); 168 | //节点标题 169 | this.drawNodeTitle(); 170 | //节点其他说明 171 | // this.drawNodeOther(); 172 | this.drawNodeCode("database"); 173 | this.drawNodeCode("cloud"); 174 | this.drawNodeCode("earth"); 175 | 176 | // console.log(d3) 177 | // var force = d3.layout.force() 178 | // .nodes(this.nodes) //指定节点数组 179 | // .links(this.data) //指定连线数组 180 | // .size([1000, 800]) //指定作用域范围 181 | // .linkDistance(150) //指定连线长度 182 | // .charge([-400]); //相互之间的作用力 183 | // force.start(); //开始作用 184 | } 185 | // 画节点语言标识 186 | drawNodeCode (code) { 187 | this.nodeCodes = this.nodes.filter(item => item.type == code) 188 | .append('g') 189 | .attr('class', 'node-code') 190 | .attr('transform', 'translate(' + -symbolSize / 2 + ',' + symbolSize / 3 + ')') 191 | 192 | this.nodeCodes 193 | .append('circle') 194 | .attr('r', () => fontSize * 1.5) 195 | 196 | this.nodeCodes 197 | .append('text') 198 | .attr('dy', fontSize / 1.5) 199 | // .text(item => item.code); 200 | .attr("style", "font-size:18px;line-height:27px") 201 | .text("√"); 202 | 203 | } 204 | 205 | //画节点图标 206 | drawNodeSymbol () { 207 | //绘制圆点图标 208 | this.nodes.filter(item => item.type == 'app') 209 | .append("circle") 210 | .attr("r", symbolSize / 2) 211 | .attr("fill", '#fff') 212 | .attr('class', function (d) { 213 | return 'health' + d.health; 214 | }) 215 | .attr('stroke-width', '5px') 216 | 217 | // 在元素中定义的图形不会直接显示在SVG图像上。要显示它们需要使用元素来引入它们 218 | // 元素通过xlink:href属性来引入元素。注意在ID前面要添加一个#。 219 | //绘制数据库图标 220 | this.nodes.filter(item => item.type == 'database') 221 | .append('use') 222 | .attr('xlink:href', '#database') 223 | .attr('x', function () { 224 | return -this.getBBox().width / 2 225 | }) 226 | .attr('y', function () { 227 | return -this.getBBox().height / 2 228 | }) 229 | 230 | //绘制云图标 231 | this.nodes.filter(item => item.type == 'cloud') 232 | .append('use') 233 | .attr('xlink:href', '#cloud') 234 | .attr('x', function () { 235 | return -this.getBBox().width / 2 236 | }) 237 | .attr('y', function () { 238 | return -this.getBBox().height / 2 239 | }) 240 | // 绘制地球图标 241 | this.nodes.filter(item => item.type == 'earth') 242 | .append('use') 243 | .attr('xlink:href', '#earth') 244 | .attr('x', function () { 245 | return -this.getBBox().width / 2 246 | }) 247 | .attr('y', function () { 248 | return -this.getBBox().height / 2 249 | }) 250 | 251 | } 252 | 253 | // 填写节点右侧信息 254 | // drawNodeOther() { 255 | // //如果是应用的时候 256 | // this.nodeOthers = this.nodes.filter(item => item.type == 'app') 257 | // .append("text") 258 | // .attr("x", symbolSize / 2 + padding) 259 | // .attr("y", -5) 260 | // .attr('class', 'node-other') 261 | 262 | // this.nodeOthers.append('tspan') 263 | // .text(d => d.time + 'ms'); 264 | 265 | // this.nodeOthers.append('tspan') 266 | // .text(d => d.rpm + 'rpm') 267 | // .attr('x', symbolSize / 2 + padding) 268 | // .attr('dy', '1em'); 269 | 270 | // this.nodeOthers.append('tspan') 271 | // .text(d => d.epm + 'epm') 272 | // .attr('x', symbolSize / 2 + padding) 273 | // .attr('dy', '1em') 274 | // } 275 | 276 | //画节点标题 277 | drawNodeTitle () { 278 | console.log(this.nodes) 279 | //节点标题 280 | this.nodes.append("text") 281 | .attr('class', 'node-title') 282 | .text(function (d) { 283 | return d.name; 284 | }) 285 | .attr("dy", symbolSize) 286 | // 处理节点图标中的百分比 287 | this.nodes.filter(item => item.type == 'app').append("text") 288 | .text(function (d) { 289 | return (d.active / d.total) * 100 + "%"; 290 | }) 291 | .attr('dy', fontSize / 2) 292 | .attr('class', 'node-call') 293 | 294 | } 295 | 296 | // 画节点链接线 297 | drawLinkLine () { 298 | let data = this.data; 299 | if (this.lineGroup) { 300 | this.lineGroup.selectAll('.link') 301 | .attr( 302 | 'd', link => genLinkPath(link), 303 | ) 304 | } else { 305 | this.lineGroup = this.container.append('g') 306 | this.lineGroup.selectAll('.link') 307 | .data(this.edges) 308 | .enter() 309 | .append('path') 310 | .attr('class', 'link') 311 | .attr( 312 | 'marker-end', (link, i) => 'url(#' + 'marker-' + i + ')' 313 | ).attr( 314 | 'd', link => genLinkPath(link), 315 | ).attr( 316 | 'id', (link, i) => 'link-' + i 317 | ) 318 | } 319 | 320 | // 连接线路的路径 321 | function genLinkPath (d) { 322 | let sx = data[d.source].x; 323 | let tx = data[d.target].x; 324 | let sy = data[d.source].y; 325 | let ty = data[d.target].y; 326 | return 'M' + sx + ',' + sy + 327 | ' L' + tx + ',' + ty 328 | 329 | } 330 | } 331 | 332 | //画节点链接线文字 333 | // drawLinkText () { 334 | // let data = this.data; 335 | // let self = this; 336 | // if (this.lineTextGroup) { 337 | // this.lineTexts 338 | // .attr('transform', getTransform) 339 | 340 | // } else { 341 | // this.lineTextGroup = this.container.append('g') 342 | 343 | // this.lineTexts = this.lineTextGroup 344 | // .selectAll('.linetext') 345 | // .data(this.edges) 346 | // .enter() 347 | // .append('text') 348 | // .attr('dy', -2) 349 | // .attr('transform', getTransform) 350 | // .on('click', () => { alert() }) 351 | 352 | // this.lineTexts 353 | // .append('tspan') 354 | // .text((d) => this.data[d.source].lineTime + 'ms,' + this.data[d.source].lineRpm + 'rpm'); 355 | 356 | // this.lineTexts 357 | // .append('tspan') 358 | // .text((d) => this.data[d.source].lineProtocol) 359 | // .attr('dy', '1em') 360 | // .attr('dx', function () { 361 | // return -this.getBBox().width / 2 362 | // }) 363 | // } 364 | 365 | // function getTransform (link) { 366 | // let s = data[link.source]; 367 | // let t = data[link.target]; 368 | // let p = self.getCenter(s.x, s.y, t.x, t.y); 369 | // let angle = self.getAngle(s.x, s.y, t.x, t.y); 370 | // if (s.x > t.x && s.y < t.y || s.x < t.x && s.y > t.y) { 371 | // angle = -angle 372 | // } 373 | // return 'translate(' + p[0] + ',' + p[1] + ') rotate(' + angle + ')' 374 | // } 375 | // } 376 | 377 | // 更新视图(图标位置和连接线) 378 | update (d) { 379 | this.drawLinkLine(); 380 | // this.drawLinkText(); 381 | console.log(d) 382 | } 383 | 384 | //拖拽方法 385 | onDrag (ele, d) { 386 | console.log("触发拖拽onDrag") 387 | d.x = d3.event.x; 388 | d.y = d3.event.y; 389 | d3.select(ele) 390 | .attr('transform', "translate(" + d3.event.x + "," + d3.event.y + ")") 391 | this.update(d); 392 | } 393 | 394 | //缩放方法 395 | onZoom (ele) { 396 | 397 | this.width = this.svg.attr('width'); 398 | var transform = d3.zoomTransform(ele); 399 | 400 | this.scale = transform.k; 401 | // this.scale>1则为放大, <1为缩小 402 | this.container.attr('transform', "translate(" + transform.x + "," + transform.y + ")scale(" + transform.k + ")") 403 | } 404 | 405 | //主渲染方法 406 | render () { 407 | this.scale = 1; 408 | // 准备svg画布 409 | this.width = this.svg.attr('width'); 410 | this.height = this.svg.attr('height'); 411 | this.container = this.svg.append('g') 412 | .attr('transform', 'scale(' + this.scale + ')') 413 | 414 | 415 | 416 | // 执行类中定义的方法 417 | // 1.获取所有节点位置数据 418 | this.initPosition(); 419 | // 2.初始化图标数据 420 | this.initDefineSymbol(); 421 | // 3. 初始化连接线的信息 422 | this.initLink(); 423 | // 4. 初始化节点 424 | this.initNode(); 425 | this.initZoom(); 426 | } 427 | } -------------------------------------------------------------------------------- /src/views/flowChart/Editor/behavior/edit-control.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 修改自 https://github.com/OXOYO/X-Flowchart-Vue 3 | * 4 | */ 5 | import eventBus from "../utils/eventBus"; 6 | import {deepCopy} from "../utils/commonCaculate" 7 | 8 | 9 | let itemCfg = null; 10 | let status = null;//'ToAdd','ToGrag' 11 | let timestamp = 0;//用于微调 12 | let alignLine = { 13 | enable: true, 14 | style: { 15 | stroke: '#FA8C16', 16 | lineWidth: 1 17 | }, 18 | lineList: [], 19 | // 最大距离 20 | maxDistance: 1, 21 | }; 22 | let controlDown = false; 23 | let copyTargetIds = []; 24 | 25 | export default { 26 | getEvents() { 27 | return { 28 | mousemove: 'onMousemove', 29 | 'customMouseUp': 'customMouseUp',//必须转一下,直接在这里监听会和多选的冲突 30 | 'customAddNode': 'customAddNode', 31 | 'canvas:mousedown': 'onCanvasMousedown', 32 | 'node:mouseover': 'onNodeHover', 33 | 'node:mouseout': 'onNodeOut', 34 | 'node:mousedown': 'onNodeMouseDown', 35 | 'node:drag': 'onNodeDrag', 36 | 'node:dragend': 'onNodeDragEnd', 37 | 'edge:mouseenter': 'onEdgeMouseenter', 38 | 'edge:mouseout': 'onEdgeMouseout', 39 | 'edge:mousemove': 'onEdgeMousemove', 40 | 'edge:mousedown': 'onEdgeMouseDown', 41 | 'keydown': 'onKeyDown', 42 | 'keyup': 'onKeyUp', 43 | 'node:contextmenu': 'onNodeContextmenu', 44 | 'edge:contextmenu': 'onEdgeContextmenu', 45 | }; 46 | }, 47 | 48 | onCanvasMousedown(event) { 49 | 50 | let _t = this; 51 | _t.log('nodeControl onCanvasMousedown'); 52 | _t.doClearAllStates(); 53 | }, 54 | onNodeHover(event) { 55 | let _t = this; 56 | _t.log('nodeControl onNodeHover'); 57 | if (!event.item.hasState('selected')) { 58 | _t.graph.setItemState(event.item, 'hover', true); 59 | } 60 | if (event.target._attrs.isAnchorPoint) {//鼠标的目标是锚点 61 | event.item.getContainer().findAll(point => { 62 | if(point._attrs.isAnchorPoint){ 63 | if(event.target._attrs.anchorPointIndex === point._attrs.anchorPointIndex){ 64 | point.attr("fill", "#3688ed"); 65 | }else{ 66 | point.attr("fill", "#e1e4e8"); 67 | } 68 | } 69 | }); 70 | _t.graph.paint();//修改了fill属性,必须绘制一下,不然不生效 71 | } 72 | }, 73 | 74 | onNodeOut(event) { 75 | let _t = this; 76 | _t.log('nodeControl onNodeOut'); 77 | if (!event.item.hasState('selected')) { 78 | _t.graph.setItemState(event.item, 'hover', false) 79 | } 80 | }, 81 | onNodeMouseDown(event) { 82 | let _t = this;//鼠标在节点上按下 83 | _t.log('nodeControl onNodeMouseDown'); 84 | 85 | 86 | if (event.target._attrs.isDeleteButton && event.target._attrs.enabled) {//鼠标的目标是删除按钮 87 | eventBus.$emit('delete_item',{ids:[event.item._cfg.id],type:'node'}); 88 | } else if (event.target._attrs.isAnchorPoint) { 89 | _t.graph.setMode('addEdge'); 90 | _t.graph.emit('editor:addEdge', event); 91 | } 92 | }, 93 | 94 | onEdgeMouseenter(event) { 95 | let _t = this;//鼠标移入线 96 | _t.log('nodeControl onEdgeMouseenter'); 97 | _t.graph.setItemState(event.item, 'hover', true); 98 | }, 99 | onEdgeMouseout(event) { 100 | let _t = this;//鼠标移除线后 101 | _t.log('nodeControl onEdgeMouseout'); 102 | _t.graph.setItemState(event.item, 'hover', false); 103 | }, 104 | onEdgeMousemove(event) {//不加这个,想选中线上的删除按钮很难 105 | let _t = this;//鼠标在线上移动不断触发 106 | _t.log('nodeControl onEdgeMousemove'); 107 | _t.graph.setItemState(event.item, 'hover', true); 108 | }, 109 | onEdgeMouseDown(event) { 110 | let _t = this;//鼠标在线上按下 111 | _t.log('nodeControl onEdgeMouseDown'); 112 | if (event.target._attrs.isDeleteButton && event.target._attrs.enabled) {//鼠标的目标是删除按钮 113 | // _t.graph.removeItem(event.item);//删除这个线 114 | eventBus.$emit('delete_item',{ids:[event.item._cfg.id],type:'edge'}); 115 | }else if(event.target._attrs.sourceMoveBtn || event.target._attrs.targetMoveBtn){ 116 | _t.graph.setMode('addEdge'); 117 | _t.graph.emit('editor:moveEdge', event); 118 | } 119 | }, 120 | onKeyUp(event) { 121 | let _t = this; 122 | _t.log('弹起键盘'+event.code); 123 | if(event.code &&(event.code === 'ControlLeft' || event.code === 'ControlRight')){ 124 | controlDown = false; 125 | } 126 | }, 127 | onKeyDown(event) { 128 | let _t = this; 129 | _t.log('按下键盘' + event.code); 130 | if (event.code && event.timeStamp && timestamp !== event.timeStamp) { 131 | timestamp = event.timeStamp; 132 | if (event.code === 'ControlLeft' || event.code === 'ControlRight') { 133 | controlDown = true 134 | } else if (controlDown && event.code === 'KeyZ') { 135 | eventBus.$emit('undo_operation', null); 136 | } else if (controlDown && event.code === 'KeyY') { 137 | eventBus.$emit('redo_operation', null); 138 | }else if(controlDown && event.code === 'KeyC'){ 139 | copyTargetIds = []; 140 | _t.graph.findAllByState('node', 'selected').forEach(node => { 141 | copyTargetIds.push(node._cfg.id); 142 | }); 143 | }else if(controlDown && event.code === 'KeyV' && copyTargetIds.length>0){ 144 | eventBus.$emit('copyAndPasteNodes', copyTargetIds); 145 | }else if (event.code === 'Delete') { 146 | let ids = []; 147 | _t.graph.findAllByState('node', 'selected').forEach(node => { 148 | ids.push(node._cfg.id); 149 | }); 150 | eventBus.$emit('delete_item', {ids: ids, type:'node'}); 151 | }else if (event.code === 'ArrowLeft' 152 | || event.code === 'ArrowRight' 153 | || event.code === 'ArrowUp' 154 | || event.code === 'ArrowDown') { 155 | event.preventDefault(); 156 | let ids = []; 157 | let coordinates = []; 158 | let nodes = []; 159 | _t.graph.findAllByState('node', 'selected').forEach(node => { 160 | ids.push(node._cfg.id); 161 | coordinates.push({x: parseInt(node._cfg.model.x), y: parseInt(node._cfg.model.y)}); 162 | nodes.push(node); 163 | }); 164 | if(nodes.length<=0){ 165 | return; 166 | } 167 | if (event.code === 'ArrowLeft') { 168 | for(let i=0; i { 288 | ids.push(node._cfg.id); 289 | coordinates.push({x:parseInt(node._cfg.model.x),y:parseInt(node._cfg.model.y)}); 290 | }); 291 | } 292 | eventBus.$emit('move_item',{ids:ids,coordinates:coordinates}); 293 | }, 294 | 295 | //对齐线 296 | clearAlign() { 297 | let _t = this; 298 | alignLine.lineList.forEach(line => { 299 | line.remove(); 300 | }); 301 | alignLine.lineList = []; 302 | _t.graph.paint(); 303 | }, 304 | drawAlign(item) { 305 | if (!alignLine.enable) { 306 | return; 307 | } 308 | let _t = this; 309 | // 先清空已有对齐线 310 | _t.clearAlign(); 311 | const bbox = item.getBBox(); 312 | // FIXME bbox 中x、y坐标为图形左上角坐标 313 | // 中上 314 | const ct = {x: bbox.x + bbox.width / 2, y: bbox.y}; 315 | // 中心 316 | const cc = {x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height / 2}; 317 | // 中下 318 | const cb = {x: bbox.x + bbox.width / 2, y: bbox.y + bbox.height}; 319 | // 左中 320 | const lc = {x: bbox.x, y: bbox.y + bbox.height / 2}; 321 | // 右中 322 | const rc = {x: bbox.x + bbox.width, y: bbox.y + bbox.height / 2}; 323 | // 计算距离 324 | const getDistance = function (line, point) { 325 | // 归一向量 326 | function normalize(out, a) { 327 | let x = a[0]; 328 | let y = a[1]; 329 | let len = x * x + y * y; 330 | if (len > 0) { 331 | len = 1 / Math.sqrt(len); 332 | out[0] = a[0] * len; 333 | out[1] = a[1] * len; 334 | } 335 | return out; 336 | } 337 | 338 | function dot(a, b) { 339 | return a[0] * b[0] + a[1] * b[1]; 340 | } 341 | 342 | const pointLineDistance = function (lineX1, lineY1, lineX2, lineY2, pointX, pointY) { 343 | const lineLength = [lineX2 - lineX1, lineY2 - lineY1]; 344 | if (lineLength[0] === 0 && lineLength[1] === 0) { 345 | return NaN; 346 | } 347 | let s = [-lineLength[1], lineLength[0]]; 348 | normalize(s, s); 349 | return Math.abs(dot([pointX - lineX1, pointY - lineY1], s)); 350 | }; 351 | return { 352 | line, 353 | point, 354 | dis: pointLineDistance(line[0], line[1], line[2], line[3], point.x, point.y) 355 | } 356 | }; 357 | // 遍历节点 358 | const nodes = _t.graph.getNodes(); 359 | nodes.forEach(node => { 360 | let horizontalLines = []; 361 | let verticalLines = []; 362 | // 对齐线信息 363 | let info = { 364 | horizontals: [], 365 | verticals: [] 366 | } 367 | const bbox1 = node.getBBox(); 368 | // 水平线 369 | let horizontalInfo = [ 370 | // 左上 右上 tltr 371 | [bbox1.minX, bbox1.minY, bbox1.maxX, bbox1.minY], 372 | // 左中 右中 lcrc 373 | [bbox1.minX, bbox1.centerY, bbox1.maxX, bbox1.centerY], 374 | // 左下 右下 blbr 375 | [bbox1.minX, bbox1.maxY, bbox1.maxX, bbox1.maxY] 376 | ] 377 | // 垂直线 378 | let verticalInfo = [ 379 | // 左上 左下 tlbl 380 | [bbox1.minX, bbox1.minY, bbox1.minX, bbox1.maxY], 381 | // 上中 下中 tcbc 382 | [bbox1.centerX, bbox1.minY, bbox1.centerX, bbox1.maxY], 383 | // 上右 下右 trbr 384 | [bbox1.maxX, bbox1.minY, bbox1.maxX, bbox1.maxY] 385 | ] 386 | horizontalInfo.forEach(line => { 387 | horizontalLines.push(getDistance(line, ct)); 388 | horizontalLines.push(getDistance(line, cc)); 389 | horizontalLines.push(getDistance(line, cb)); 390 | }) 391 | verticalInfo.forEach(line => { 392 | verticalLines.push(getDistance(line, lc)); 393 | verticalLines.push(getDistance(line, cc)); 394 | verticalLines.push(getDistance(line, rc)); 395 | }) 396 | horizontalLines.sort((a, b) => a.dis - b.dis); 397 | verticalLines.sort((a, b) => a.dis - b.dis); 398 | // 过滤掉距离为0的线条 399 | horizontalLines = horizontalLines.filter(item => item.dis !== 0); 400 | if (horizontalLines.length && horizontalLines[0].dis < alignLine.maxDistance) { 401 | // 取前3个距离相等的线条 402 | for (let i = 0; i < 3; i++) { 403 | if (horizontalLines[0].dis === horizontalLines[i].dis) { 404 | info.horizontals.push(horizontalLines[i]); 405 | } 406 | } 407 | } 408 | // 过滤掉距离为0的线条 409 | verticalLines = verticalLines.filter(item => item.dis !== 0); 410 | if (verticalLines.length && verticalLines[0].dis < alignLine.maxDistance) { 411 | // 取前3个距离相等的线条 412 | for (let i = 0; i < 3; i++) { 413 | if (verticalLines[0].dis === verticalLines[i].dis) { 414 | info.verticals.push(verticalLines[i]); 415 | } 416 | } 417 | } 418 | // 添加对齐线 419 | const group = _t.graph.get('group'); 420 | // 对齐线样式 421 | const lineStyle = alignLine.style; 422 | // 处理水平线 423 | if (info.horizontals.length) { 424 | info.horizontals.forEach(lineObj => { 425 | let line = lineObj.line; 426 | let point = lineObj.point; 427 | let lineHalf = (line[0] + line[2]) / 2; 428 | let x1; 429 | let x2; 430 | if (point.x < lineHalf) { 431 | x1 = point.x - bbox.width / 2; 432 | x2 = Math.max(line[0], line[2]); 433 | } else { 434 | x1 = point.x + bbox.width / 2; 435 | x2 = Math.min(line[0], line[2]); 436 | } 437 | let shape = group.addShape('line', { 438 | attrs: { 439 | x1, 440 | y1: line[1], 441 | x2, 442 | y2: line[1], 443 | ...lineStyle 444 | }, 445 | // 是否拾取及触发该元素的交互事件 446 | capture: false 447 | }); 448 | alignLine.lineList.push(shape); 449 | }) 450 | } 451 | // 处理垂直线 452 | if (info.verticals.length) { 453 | info.verticals.forEach(lineObj => { 454 | let line = lineObj.line; 455 | let point = lineObj.point; 456 | let lineHalf = (line[1] + line[3]) / 2; 457 | let y1; 458 | let y2; 459 | if (point.y < lineHalf) { 460 | y1 = point.y - bbox.height / 2; 461 | y2 = Math.max(line[1], line[3]); 462 | } else { 463 | y1 = point.y + bbox.height / 2; 464 | y2 = Math.min(line[1], line[3]); 465 | } 466 | let shape = group.addShape('line', { 467 | attrs: { 468 | x1: line[0], 469 | y1, 470 | x2: line[0], 471 | y2, 472 | ...lineStyle 473 | }, 474 | capture: false 475 | }); 476 | alignLine.lineList.push(shape); 477 | }) 478 | } 479 | }) 480 | }, 481 | clearTempState(graph){ 482 | itemCfg = null; 483 | status = null; 484 | controlDown = false; 485 | copyTargetIds = []; 486 | }, 487 | log(message) { 488 | if (false) { 489 | console.log(message); 490 | } 491 | }, 492 | } 493 | -------------------------------------------------------------------------------- /src/views/topo/component/subTopo.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 326 | -------------------------------------------------------------------------------- /src/views/flowChart/DemoFroTest.vue: -------------------------------------------------------------------------------- 1 | 225 | 226 | 502 | 509 | -------------------------------------------------------------------------------- /src/views/topo/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 538 | 539 | 597 | --------------------------------------------------------------------------------