├── .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 |
2 |
3 |
4 |
5 |
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 |
--------------------------------------------------------------------------------
/src/assets/svg/json.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/svg/clear.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/assets/svg/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 | 
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 |
2 |
3 |
4 |
5 | {{ node.name }}
6 |
7 |
8 |
9 |
10 |
16 | {{ field.name }}
17 |
18 |
19 |
20 |
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 |
2 |
29 |
30 |
31 |
179 |
180 |
259 |
260 |
270 |
--------------------------------------------------------------------------------