├── .browserslistrc ├── babel.config.js ├── public ├── hlgalgame.ico ├── vue.config.js └── index.html ├── src ├── assets │ ├── point.png │ ├── sample.png │ └── svg │ │ ├── API.svg │ │ ├── json.svg │ │ ├── clear.svg │ │ ├── local.svg │ │ └── link.svg ├── common │ └── until.js ├── router │ └── index.js ├── App.vue ├── main.js └── views │ ├── config │ ├── buttonGroup.js │ ├── tableTypeMappingColor.js │ ├── jsplumbConfig.js │ └── sampleData.json │ ├── components │ └── TableNode.vue │ ├── methods │ ├── buttonMethods.js │ └── comm.js │ └── Index.vue ├── vue.config.js ├── .gitignore ├── .eslintrc.js ├── LICENSE ├── package.json └── README.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /public/hlgalgame.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizuhokaga/jsplumb-dataLineage-vue/HEAD/public/hlgalgame.ico -------------------------------------------------------------------------------- /src/assets/point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizuhokaga/jsplumb-dataLineage-vue/HEAD/src/assets/point.png -------------------------------------------------------------------------------- /src/assets/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizuhokaga/jsplumb-dataLineage-vue/HEAD/src/assets/sample.png -------------------------------------------------------------------------------- /src/common/until.js: -------------------------------------------------------------------------------- 1 | //生成指定长度的唯一ID 2 | export function GenNonDuplicateID(randomLength) { 3 | return Number( 4 | Math.random() 5 | .toString() 6 | .substr(3, randomLength) + Date.now() 7 | ).toString(36); 8 | } -------------------------------------------------------------------------------- /public/vue.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | chainWebpack: config => { 4 | config 5 | .plugin('html') 6 | .tap(args => { 7 | args[0].title= 'jsplumb绘制流程图' 8 | return args 9 | }) 10 | } 11 | } -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const resolve = dir => { 3 | return path.join(__dirname, dir) 4 | } 5 | module.exports = { 6 | publicPath: './', 7 | chainWebpack: config => { 8 | config.resolve.alias.set('@', resolve('src')) 9 | }, 10 | productionSourceMap: false 11 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Index from '../views/Index' 4 | 5 | Vue.use(VueRouter) 6 | 7 | const routes = [ 8 | { 9 | path: '/', 10 | name: 'Index', 11 | component: Index 12 | } 13 | ] 14 | 15 | const router = new VueRouter({ 16 | mode:'history', 17 | routes 18 | }) 19 | 20 | export default router 21 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import ElementUI from 'element-ui' 4 | import 'element-ui/lib/theme-chalk/index.css' 5 | import router from './router' 6 | 7 | Vue.config.productionTip = false 8 | 9 | Vue.use(ElementUI); 10 | import Contextmenu from 'vue-contextmenujs' 11 | Vue.use(Contextmenu);//自定义右键菜单,暂时没用上 12 | new Vue({ 13 | router, 14 | render: h => h(App) 15 | }).$mount('#app') 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | // jquery:true 6 | }, 7 | 'extends': [ 8 | 'plugin:vue/essential', 9 | 'eslint:recommended' 10 | ], 11 | parserOptions: { 12 | parser: 'babel-eslint' 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 17 | "no-unused-vars": "off", 18 | 'no-multiple-empty-lines': 0 19 | } 20 | } -------------------------------------------------------------------------------- /src/views/config/buttonGroup.js: -------------------------------------------------------------------------------- 1 | //配置左侧按钮组 ,请确保 type的值 与 src/views/methods/buttonMetohds的函数名保持一致,否则调用失败 2 | const buttonGroup=[{ 3 | label:'下载JSON', 4 | icon:require('@/assets/svg/json.svg'), 5 | type:'downloadJSON' 6 | },{ 7 | label:'下载为图片', 8 | icon:require('@/assets/svg/link.svg'), 9 | type:'downloadIMG' 10 | },{ 11 | label:'渲染本地数据', 12 | icon:require('@/assets/svg/local.svg'), 13 | type:'renderLocal' 14 | },{ 15 | label:'清空画布', 16 | icon:require('@/assets/svg/clear.svg'), 17 | type:'clear' 18 | }] 19 | 20 | export default buttonGroup 21 | -------------------------------------------------------------------------------- /src/views/config/tableTypeMappingColor.js: -------------------------------------------------------------------------------- 1 | // 不同类型的表的表头颜色不一样,定义全局常量 2 | const tableTypeMappingColor = [ 3 | { //源表 早苗绿 4 | color: "#91c051", 5 | type: "Origin" 6 | }, 7 | { //中间表 ミク葱绿 8 | color: "#39c5bb", 9 | type: "Middle" 10 | }, 11 | { //union表 天依蓝 12 | color: "#66ccff", 13 | type: "Union" 14 | }, 15 | { //结果表 小菊橙 16 | color: "#ef8014", 17 | type: "RS" 18 | }, 19 | { 20 | color:"#ffed4a", 21 | type: "HighLight" 22 | }, 23 | { 24 | color:"#fff", 25 | type: "NormalLight" 26 | } 27 | ] 28 | 29 | export default tableTypeMappingColor 30 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/svg/API.svg: -------------------------------------------------------------------------------- 1 | 14描述统计 -------------------------------------------------------------------------------- /src/assets/svg/json.svg: -------------------------------------------------------------------------------- 1 | 6数据校验 -------------------------------------------------------------------------------- /src/assets/svg/clear.svg: -------------------------------------------------------------------------------- 1 | 15清洗 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Code-RoadFly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/assets/svg/local.svg: -------------------------------------------------------------------------------- 1 | 13测试报告 -------------------------------------------------------------------------------- /src/assets/svg/link.svg: -------------------------------------------------------------------------------- 1 | 7单键查询 -------------------------------------------------------------------------------- /src/views/config/jsplumbConfig.js: -------------------------------------------------------------------------------- 1 | import colorFields from "./tableTypeMappingColor"; 2 | 3 | const commConfig = { 4 | grid: [10, 10], 5 | Container: "flow", 6 | //四种样式:Bezier/Straight/Flowchart/StateMachine 7 | Connector: ["Bezier", {curviness: 10}], 8 | // Connector: ["Straight", {stub: [20, 50], gap: 0}], 9 | // Connector: ["Flowchart", { stub: [20, 10], gap: 10, cornerRadius: 5, alwaysRespectStubs: true }], 10 | // Connector: ["StateMachine"], 11 | // 连线的端点 12 | Endpoint: ["Dot", {radius: 1}], 13 | // 端点的样式 14 | EndpointStyle: { 15 | fill: "#c4c4c4", 16 | outlineWidth: 1 17 | }, 18 | // 通常连线的样式 19 | PaintStyle: { 20 | stroke: colorFields[2].color, 21 | strokeWidth: 2 22 | }, 23 | //hover激活连线的样式 24 | HoverPaintStyle: { 25 | stroke: colorFields[3].color, 26 | strokeWidth: 2 27 | }, 28 | maxConnections: -1, // 设置连接点最多可以连接几条线 -1不限 29 | // 绘制箭头 30 | Overlays: [ 31 | [ 32 | "Arrow", 33 | { 34 | width: 8, 35 | length: 10, 36 | location: 1 37 | } 38 | ] 39 | ], 40 | LogEnabled: false, //是否打开jsPlumb的内部日志记录 41 | } 42 | 43 | export default commConfig 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsplumb-dataLineage-vue", 3 | "version": "2.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve --port 8620", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.25.0", 12 | "canvas": "^2.9.0", 13 | "canvg": "^1.5.3", 14 | "core-js": "^3.6.5", 15 | "element-ui": "^2.15.12", 16 | "html2canvas": "^1.4.1", 17 | "jquery": "^3.6.0", 18 | "jsdom": "^13.2.0", 19 | "jsplumb": "^2.15.5", 20 | "panzoom": "^9.4.1", 21 | "view-design": "^4.5.0-beta.3", 22 | "vue": "^2.6.11", 23 | "vue-click-outside": "^1.1.0", 24 | "vue-contextmenujs": "^1.3.13", 25 | "vue-router": "^3.2.0", 26 | "vuex": "^3.4.0", 27 | "xmldom": "^0.6.0" 28 | }, 29 | "devDependencies": { 30 | "@vue/cli-plugin-babel": "~4.5.0", 31 | "@vue/cli-plugin-eslint": "~4.5.0", 32 | "@vue/cli-plugin-router": "~4.5.0", 33 | "@vue/cli-plugin-vuex": "~4.5.0", 34 | "@vue/cli-service": "~4.5.0", 35 | "babel-eslint": "^10.1.0", 36 | "eslint": "^6.7.2", 37 | "eslint-plugin-vue": "^6.2.2", 38 | "less": "^3.0.4", 39 | "less-loader": "^5.0.0", 40 | "vue-template-compiler": "^2.6.11" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsplumb-dataLineage-vue 2 | 3 | 基于Vue和jsPlumb的、模仿sqlFlow前端的数据血缘前端展示页面 4 | - [main 分支](https://github.com/mizuhokaga/jsplumb-dataLineage-vue/tree/main) 为 vue2 版本 5 | - [vue3 分支](https://github.com/mizuhokaga/jsplumb-dataLineage-vue/tree/vue3) 为 vue3 版本,当然由于我不大会vue3所以写得很烂 6 | - [jsplumb2.x https://github.com/jsplumb/jsplumb](https://github.com/jsplumb/jsplumb) 7 | - jsplumb [中文文档参考](https://github.com/wangduanduan/jsplumb-chinese-tutorial) 8 | ## 1.效果 9 | 10 | ![图片](https://github.com/mizuhokaga/jsplumb-dataLineage-vue/blob/main/src/assets/sample.png) 11 | 12 | - 表级关联:data1 到 middle1 13 | ``` 14 | 表级JSON: 15 | "edges": 16 | [ 17 | { 18 | "from": { 19 | "field": "", 20 | "name": "data1" 21 | }, 22 | "to": { 23 | "field": "", 24 | "name": "middle1" 25 | } 26 | }, 27 | …… 28 | ] 29 | ``` 30 | - 字段级关联:middle1的age字段 到 middle3的age字段 31 | ``` 32 | 字段JSON: 33 | "edges": 34 | [ 35 | { 36 | "from": { 37 | "field": "age", 38 | "name": "middle1" 39 | }, 40 | "to": { 41 | "field": "age", 42 | "name": "middle3" 43 | } 44 | }, 45 | …… 46 | ] 47 | ``` 48 | ## 2.如何启动? 49 | ### 2.1安装依赖 50 | 51 | ``` 52 | npm install 53 | ``` 54 | 55 | ### 2.2本地运行 56 | 57 | ``` 58 | npm run serve 59 | ``` 60 | 浏览器访问 http://localhost:8620 61 | ## 3.功能: 62 | - 根据json渲染血缘图,每个节点可自由拖动; 63 | - ~~移动到连线上==高亮==相关**列和线**~~,因为失灵时不灵连线高亮暂时注释掉了 64 | - 画布支持缩放 (鼠标中键滚轮缩放) 65 | - 画布的整体无限平移 66 | - 导出血缘为JSON 或 PNG图片 67 | 68 | 待实现功能: 69 | * minimap 70 | 71 | 72 | ## 4.其他 73 | - 功能详情参考文章:[【已开源】基于Vue2和jsPlumb.js的模仿sqlFlow数据血缘图的前端页面【源代码已更新】](https://blog.csdn.net/qq_44831907/article/details/122923483) 74 | - JS版本在这里: https://github.com/mizuhokaga/jsplumb-dataLineage 75 | - 现在可以用[G6的官方demoER图](https://antv-g6.gitee.io/zh/examples/case/simpleCase#ER) 编写更好 76 | - [Vue3版本请切换分支](https://github.com/mizuhokaga/jsplumb-dataLineage-vue/tree/vue3) 77 | - 后端坐标生成博文存档:[后端 绘制有向无环图(DAG图)](https://blog.csdn.net/qq_44831907/article/details/128370539) 78 | -------------------------------------------------------------------------------- /src/views/components/TableNode.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 70 | 71 | 104 | -------------------------------------------------------------------------------- /src/views/methods/buttonMethods.js: -------------------------------------------------------------------------------- 1 | import {GenNonDuplicateID} from "@/common/until" 2 | import html2canvas from "html2canvas" 3 | import canvg from "canvg" 4 | 5 | const buttonMethods = { 6 | click(e) {//根据点击类型的不同分流到指定函数 7 | this[e](); 8 | }, 9 | 10 | downloadJSON() { 11 | if (this.json == null) { 12 | this.$message.error("JSON IS NULL!!!"); 13 | return; 14 | } 15 | if ( 16 | Object.keys(this.json.nodes).length !== 0 && 17 | Object.keys(this.json.edges).length !== 0 18 | ) { 19 | let datastr = 20 | "data:text/json;charset=utf-8," + 21 | encodeURIComponent(JSON.stringify(this.json)); 22 | let a = document.createElement("a"); 23 | a.setAttribute("href", datastr); 24 | a.setAttribute( 25 | "download", 26 | new Date().getTime() + GenNonDuplicateID(8) + ".json" 27 | ); 28 | a.click(); 29 | a.remove(); 30 | } 31 | }, 32 | downloadIMG() { 33 | if (this.json == null) { 34 | this.$message.error("JSON IS NULL!!!"); 35 | return; 36 | } 37 | //将边(svg)转化为canvas的形式 38 | const flow = this.$refs.flowWrap 39 | //svg转换canvas会导致svg无法拖动,使用临时元素 40 | const tmpFlow = flow.cloneNode(true) 41 | flow.appendChild(tmpFlow) 42 | this.svgToCanvas(tmpFlow) 43 | html2canvas(tmpFlow, { 44 | taintTest: false, 45 | scale: window.devicePixelRatio < 2 ? 2 : window.devicePixelRatio, //scala属性解决生成的canvas模糊问题 46 | }).then((canvas) => { 47 | let blob = this.base64ToFile(canvas.toDataURL('image/png')); 48 | let a = document.createElement('a'); 49 | a.setAttribute('href', URL.createObjectURL(blob)); 50 | a.setAttribute('download', new Date().getTime() + GenNonDuplicateID(8) + '.png'); 51 | URL.revokeObjectURL(blob); 52 | a.click(); 53 | a.remove(); 54 | flow.removeChild(tmpFlow) // 移除临时tmpFlow 55 | }).catch((error) => { 56 | flow.removeChild(tmpFlow) // 移除临时tmpFlow 57 | this.$message.error('血缘图生成失败,请查看控制台错误') 58 | console.log(error); 59 | }); 60 | }, 61 | // 下载图片的辅助方法:将base64转换成file文件 62 | base64ToFile(dataurl) { 63 | let arr = dataurl.split(','); 64 | let mime = arr[0].match(/:(.*?);/)[1]; 65 | let bstr = atob(arr[1]); 66 | let n = bstr.length; 67 | let u8arr = new Uint8Array(n); 68 | while (n--) { 69 | u8arr[n] = bstr.charCodeAt(n); 70 | } 71 | return new Blob([u8arr], { 72 | type: mime 73 | }); 74 | }, 75 | //下载图片的辅助方法: canvg将svg转canvas 76 | svgToCanvas(element) { 77 | const svgElems = element.querySelectorAll('svg'); 78 | svgElems.forEach(node => { 79 | let parentNode = node.parentNode; 80 | let svg = node.outerHTML.trim(); 81 | let canvas = document.createElement("canvas"); 82 | canvg(canvas, svg); 83 | canvas.style.zIndex = 9 84 | if (node.style.position) { 85 | canvas.style.position += node.style.position; 86 | canvas.style.left += node.style.left; 87 | canvas.style.top += node.style.top; 88 | } 89 | parentNode.removeChild(node); 90 | parentNode.appendChild(canvas); 91 | }); 92 | }, 93 | 94 | renderLocal() { 95 | if (this.json.nodes.length == 0) { 96 | this.renderDefaultLineage() 97 | this.$message.success("render success!"); 98 | } else this.$message.warning("render duplication!"); 99 | }, 100 | clear() { 101 | if (Object.keys(this.json.nodes).length) { 102 | this.jsplumbInstance.deleteEveryConnection(); 103 | this.jsplumbInstance.deleteEveryEndpoint(); 104 | this.json.nodes.forEach((node) => { 105 | this.jsplumbInstance.remove(node.name); 106 | }); 107 | } 108 | this.json = { 109 | nodes: [], 110 | edges: [] 111 | } 112 | } 113 | } 114 | export default buttonMethods 115 | -------------------------------------------------------------------------------- /src/views/config/sampleData.json: -------------------------------------------------------------------------------- 1 | { 2 | "edges": [ 3 | { 4 | "from": { 5 | "field": "", 6 | "name": "data1" 7 | 8 | }, 9 | "to": { 10 | "field": "", 11 | "name": "middle1" 12 | } 13 | 14 | }, 15 | { 16 | "from": { 17 | "field": "name", 18 | "name": "data2" 19 | 20 | }, 21 | "to": { 22 | "field": "name", 23 | "name": "middle2" 24 | } 25 | 26 | }, 27 | { 28 | "from": { 29 | "field": "age", 30 | "name": "data2" 31 | }, 32 | "to": { 33 | "field": "age", 34 | "name": "middle2" 35 | } 36 | 37 | }, 38 | { 39 | "from": { 40 | "field": "grade", 41 | "name": "data2" 42 | }, 43 | "to": { 44 | "field": "grade", 45 | "name": "middle2" 46 | } 47 | 48 | }, 49 | { 50 | "from": { 51 | "field": "age", 52 | "name": "middle1" 53 | }, 54 | "to": { 55 | "field": "age", 56 | "name": "middle3" 57 | } 58 | 59 | }, 60 | { 61 | "from": { 62 | "field": "name", 63 | "name": "middle2" 64 | }, 65 | "to": { 66 | "field": "name", 67 | "name": "middle3" 68 | } 69 | 70 | }, 71 | { 72 | "from": { 73 | "field": "class", 74 | "name": "middle2" 75 | }, 76 | "to": { 77 | "field": "class", 78 | "name": "middle3" 79 | } 80 | }, 81 | { 82 | "from": { 83 | "field": "grade", 84 | "name": "middle2" 85 | }, 86 | "to": { 87 | "field": "grade", 88 | "name": "middle3" 89 | } 90 | }, 91 | { 92 | "from": { 93 | "field": "name", 94 | "name": "middle3" 95 | }, 96 | "to": { 97 | "field": "name", 98 | "name": "RS" 99 | } 100 | }, 101 | { 102 | "from": { 103 | "field": "age", 104 | "name": "middle3" 105 | }, 106 | "to": { 107 | "field": "age", 108 | "name": "RS" 109 | } 110 | }, 111 | { 112 | "from": { 113 | "field": "grade", 114 | "name": "middle3" 115 | }, 116 | "to": { 117 | "field": "grade", 118 | "name": "RS" 119 | } 120 | }, 121 | { 122 | "from": { 123 | "field": "class", 124 | "name": "middle3" 125 | }, 126 | "to": { 127 | "field": "class", 128 | "name": "RS" 129 | } 130 | } 131 | ], 132 | "nodes": [{ 133 | "name": "data1", 134 | "type": "Origin", 135 | "fields": [{ 136 | "name": "age" 137 | }, { 138 | "name": "name" 139 | }, { 140 | "name": "class" 141 | }], 142 | "top": 135, 143 | "left": 10 144 | }, 145 | { 146 | "name": "data2", 147 | "type": "Origin", 148 | "fields": [{ 149 | "name": "age" 150 | }, 151 | { 152 | "name": "name" 153 | }, { 154 | "name": "grade" 155 | } 156 | ], 157 | "top": 255, 158 | "left": 50 159 | }, 160 | { 161 | "name": "middle1", 162 | "type": "Middle", 163 | "fields": [{ 164 | "name": "age" 165 | }, { 166 | "name": "name" 167 | }], 168 | "top": 139, 169 | "left": 233 170 | }, 171 | { 172 | "name": "middle2", 173 | "type": "Middle", 174 | "fields": [{ 175 | "name": "age" 176 | }, { 177 | "name": "name" 178 | }, 179 | { 180 | "name": "class" 181 | }, 182 | { 183 | "name": "grade" 184 | } 185 | ], 186 | "top": 309, 187 | "left": 231 188 | }, 189 | { 190 | "name": "middle3", 191 | "type": "Middle", 192 | "fields": [{ 193 | "name": "age" 194 | }, { 195 | "name": "name" 196 | }, 197 | { 198 | "name": "class" 199 | }, 200 | { 201 | "name": "grade" 202 | } 203 | ], 204 | "top": 222, 205 | "left": 388 206 | }, 207 | { 208 | "name": "RS", 209 | "type": "RS", 210 | "fields": [{ 211 | "name": "age" 212 | }, { 213 | "name": "name" 214 | }, { 215 | "name": "class" 216 | }, { 217 | "name": "grade" 218 | }], 219 | "top": 280, 220 | "left": 571 221 | } 222 | ] 223 | } 224 | -------------------------------------------------------------------------------- /src/views/methods/comm.js: -------------------------------------------------------------------------------- 1 | import panzoom from "panzoom"; 2 | // 封装的jsplumb的通用方法 3 | const comm = { 4 | //添加端点 5 | addEndpoint(elID, anchorArr) { 6 | //AnchorArr 可能有多个锚点需要添加 7 | anchorArr.forEach(anchor => { 8 | this.jsplumbInstance.addEndpoint(elID, { 9 | anchors: anchor, 10 | uuid: elID.concat(this.minus, anchor) 11 | }, this.commConfig) 12 | }) 13 | }, 14 | //将端点连线 15 | connectEndpoint(from, to) { 16 | this.jsplumbInstance.connect({ 17 | uuids: [from, to] 18 | }, this.commConfig); 19 | }, 20 | //封装拖动,添加辅助对齐线功能 21 | draggableNode(nodeID) { 22 | this.jsplumbInstance.draggable(nodeID, { 23 | grid: this.commGrid, 24 | drag: (params) => { 25 | this.alignForLine(nodeID, params.pos) 26 | }, 27 | start: () => { 28 | }, 29 | stop: (params) => { 30 | this.auxiliaryLine.isShowXLine = false 31 | this.auxiliaryLine.isShowYLine = false 32 | this.changeNodePosition(nodeID, params.pos) 33 | } 34 | }) 35 | }, 36 | //移动节点时,动态显示对齐线 37 | alignForLine(nodeID, position) { 38 | let showXLine = false, showYLine = false 39 | this.json.nodes.some(el => { 40 | if (el.name !== nodeID && el.left === position[0]) { 41 | this.auxiliaryLinePos.x = position[0]; 42 | showYLine = true 43 | } 44 | if (el.name !== nodeID && el.top === position[1]) { 45 | this.auxiliaryLinePos.y = position[1]; 46 | showXLine = true 47 | } 48 | }) 49 | this.auxiliaryLine.isShowYLine = showYLine 50 | this.auxiliaryLine.isShowXLine = showXLine 51 | }, 52 | changeNodePosition(nodeID, pos) { 53 | this.json.nodes.some(v => { 54 | if (nodeID === v.name) { 55 | v.left = pos[0] 56 | v.top = pos[1] 57 | return true 58 | } else { 59 | return false 60 | } 61 | }) 62 | }, 63 | //初始化缩放功能 64 | initPanZoom() { 65 | const mainContainer = this.jsplumbInstance.getContainer(); 66 | const mainContainerWrap = mainContainer.parentNode; 67 | const pan = panzoom(mainContainer, { 68 | smoothScroll: false, 69 | bounds: true, 70 | // autocenter: true, 71 | zoomDoubleClickSpeed: 1, 72 | minZoom: 0.5, 73 | maxZoom: 2, 74 | //设置滚动缩放的组合键,默认不需要组合键 75 | beforeWheel: (e) => { 76 | // console.log(e) 77 | // let shouldIgnore = !e.ctrlKey 78 | // return shouldIgnore 79 | }, 80 | beforeMouseDown: function (e) { 81 | // allow mouse-down panning only if altKey is down. Otherwise - ignore 82 | let shouldIgnore = e.ctrlKey; 83 | return shouldIgnore; 84 | } 85 | }); 86 | this.jsplumbInstance.mainContainerWrap = mainContainerWrap; 87 | this.jsplumbInstance.pan = pan; 88 | // 缩放时设置jsPlumb的缩放比率 89 | pan.on("zoom", e => { 90 | const {x, y, scale} = e.getTransform(); 91 | this.jsplumbInstance.setZoom(scale); 92 | //根据缩放比例,缩放对齐辅助线长度和位置 93 | this.auxiliaryLinePos.width = (1 / scale) * 100 + '%' 94 | this.auxiliaryLinePos.height = (1 / scale) * 100 + '%' 95 | this.auxiliaryLinePos.offsetX = -(x / scale) 96 | this.auxiliaryLinePos.offsetY = -(y / scale) 97 | }); 98 | pan.on("panend", (e) => { 99 | const {x, y, scale} = e.getTransform(); 100 | this.auxiliaryLinePos.width = (1 / scale) * 100 + '%' 101 | this.auxiliaryLinePos.height = (1 / scale) * 100 + '%' 102 | this.auxiliaryLinePos.offsetX = -(x / scale) 103 | this.auxiliaryLinePos.offsetY = -(y / scale) 104 | }) 105 | // 平移时设置鼠标样式 106 | mainContainerWrap.style.cursor = "move"; 107 | mainContainerWrap.addEventListener("mousedown", function wrapMousedown() { 108 | this.style.cursor = "grabbing"; 109 | mainContainerWrap.addEventListener("mouseout", function wrapMouseout() { 110 | this.style.cursor = "move"; 111 | }); 112 | }); 113 | mainContainerWrap.addEventListener("mouseup", function wrapMouseup() { 114 | this.style.cursor = "move"; 115 | }); 116 | }, 117 | //初始化节点位置 (以便对齐,居中) 118 | fixNodesPosition() { 119 | if (this.json.nodes && this.$refs.flowWrap) { 120 | const nodeWidth = 120 121 | const nodeHeight = 40 122 | let wrapInfo = this.$refs.flowWrap.getBoundingClientRect() 123 | let maxLeft = 0, 124 | minLeft = wrapInfo.width, 125 | maxTop = 0, 126 | minTop = wrapInfo.height; 127 | let nodePoint = { 128 | left: 0, 129 | right: 0, 130 | top: 0, 131 | bottom: 0 132 | } 133 | let fixTop = 0, 134 | fixLeft = 0; 135 | this.json.nodes.forEach(el => { 136 | // let top = Number(el.top.substring(0, el.top.length -2)) 137 | // let left = Number(el.left.substring(0, el.left.length -2)) 138 | let top = el.top 139 | let left = el.left 140 | maxLeft = left > maxLeft ? left : maxLeft 141 | minLeft = left < minLeft ? left : minLeft 142 | maxTop = top > maxTop ? top : maxTop 143 | minTop = top < minTop ? top : minTop 144 | }) 145 | nodePoint.left = minLeft 146 | nodePoint.right = wrapInfo.width - maxLeft - nodeWidth 147 | nodePoint.top = minTop 148 | nodePoint.bottom = wrapInfo.height - maxTop - nodeHeight; 149 | 150 | fixTop = nodePoint.top !== nodePoint.bottom ? (nodePoint.bottom - nodePoint.top) / 2 : 0; 151 | fixLeft = nodePoint.left !== nodePoint.right ? (nodePoint.right - nodePoint.left) / 2 : 0; 152 | 153 | this.json.nodes.map(el => { 154 | let top = Number(el.top) + fixTop; 155 | let left = Number(el.left) + fixLeft; 156 | el.top = (Math.round(top / 20)) * 20 157 | el.left = (Math.round(left / 20)) * 20 158 | }) 159 | } 160 | }, 161 | 162 | //鼠标移动到连线上时候 163 | findActiveNode(edges, tbName, column) { 164 | let up = this.findUpstreamNode(edges, tbName, column) 165 | //up数组首个元素重复 需要跳过 166 | return up.slice(1).reverse() 167 | .concat(this.findDownstreamNode(edges, tbName, column)) 168 | }, 169 | //找下游的子节点 170 | findDownstreamNode(edges, tbName, column) { 171 | let downstreamNodes = [{ 172 | tbName, 173 | column 174 | }] 175 | edges.forEach(edge => { 176 | if (edge.from.tbName === tbName && edge.from.column === column) { 177 | downstreamNodes = downstreamNodes.concat(this.findDownstreamNode(this.json.edges, edge.to.tbName, edge.to.column)) 178 | } 179 | }) 180 | return downstreamNodes 181 | }, 182 | //找上游的父节点 183 | findUpstreamNode(edges, tbName, column) { 184 | let upstreamNodes = [{ 185 | tbName, 186 | column 187 | }] 188 | edges.forEach(edge => { 189 | if (edge.to.tbName === tbName && edge.to.column === column) { 190 | upstreamNodes = upstreamNodes.concat(this.findUpstreamNode(this.json.edges, edge.from.tbName, edge.from.column)) 191 | } 192 | }) 193 | return upstreamNodes 194 | } 195 | 196 | } 197 | 198 | export default comm 199 | -------------------------------------------------------------------------------- /src/views/Index.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 179 | 180 | 259 | 260 | 270 | --------------------------------------------------------------------------------