8 changed files with 1150 additions and 42 deletions
			
			
		| @ -0,0 +1,78 @@ | |||
| <template> | |||
|   <codemirror v-model:value="value" :options="sqlOptions" :dbType="dbType" /> | |||
| </template> | |||
| 
 | |||
| <script setup> | |||
| // sql 编辑器 | |||
| import * as sqlFormatter from "sql-formatter"; | |||
| import Codemirror from 'codemirror-editor-vue3'; | |||
| import 'codemirror/mode/sql/sql.js'; | |||
| import "codemirror/mode/javascript/javascript.js"; | |||
| // language | |||
| import 'codemirror/mode/javascript/javascript.js'; | |||
| // theme 主题 | |||
| import 'codemirror/theme/monokai.css'; | |||
| // 折叠功能 | |||
| import 'codemirror/addon/fold/foldcode.js'; | |||
| import 'codemirror/addon/fold/foldgutter.js'; | |||
| import 'codemirror/addon/fold/foldgutter.css'; | |||
| import 'codemirror/addon/fold/brace-fold.js'; | |||
| // 自动提示 | |||
| import 'codemirror/addon/hint/show-hint.js'; | |||
| import 'codemirror/addon/hint/show-hint.css'; | |||
| import 'codemirror/addon/hint/javascript-hint.js'; | |||
| // 代码校验 lint | |||
| import 'codemirror/addon/lint/lint.js'; | |||
| import 'codemirror/addon/lint/lint.css'; | |||
| import 'codemirror/addon/lint/json-lint'; | |||
| 
 | |||
| // 其他 | |||
| import 'codemirror/addon/edit/matchbrackets.js'; | |||
| import 'codemirror/addon/edit/closebrackets.js'; | |||
| import "codemirror/addon/lint/json-lint.js"; | |||
| import {watch,ref} from "vue"; | |||
| 
 | |||
| const props = defineProps({ | |||
|   data: String, | |||
|   dbType: String, | |||
| }) | |||
| const sqlOptions = { | |||
|   autorefresh: true, // 是否自动刷新 | |||
|   smartIndent: true, // 自动缩进 | |||
|   tabSize: 4, // 缩进单元格为 4 个空格 | |||
|   mode: "text/x-sql", //编辑器的编程语言 | |||
|   line: true, // 是否显示行数 | |||
|   viewportMargin: Infinity, // 高度自适应 | |||
|   highlightDifferences: true, | |||
|   autofocus: false, | |||
|   indentUnit: 2, | |||
|   readOnly: false, // 只读 | |||
|   showCursorWhenSelecting: true, | |||
|   firstLineNumber: 1, | |||
|   matchBrackets: true,//括号匹配 | |||
|   lineWrapping: true, //是否折叠 | |||
|   gutters: [ | |||
|     "CodeMirror-linenumbers", | |||
|     "CodeMirror-foldgutter", | |||
|     "CodeMirror-lint-markers", | |||
|   ], | |||
|   lineNumbers: true, //是否显示左边换行数字 | |||
|   lint: true,  // 打开json校验 | |||
| } | |||
| const value = ref("") | |||
| watch(() => value.value, | |||
|     (val) =>{ | |||
|       console.log(props.dbType) | |||
|       value.value = sqlFormatter.format(val,{ language: props.dbType?.toLowerCase() || "sql" }) | |||
|     } | |||
| ) | |||
| onMounted(()=>{ | |||
|   value.value = props.data | |||
| } | |||
| ) | |||
| 
 | |||
| </script> | |||
| 
 | |||
| <style scoped lang="scss"> | |||
| 
 | |||
| </style> | |||
| @ -0,0 +1,744 @@ | |||
| <template> | |||
|   <div id="bloodRelationG6" style="width: 100%;height: 100%;overflow: auto;position: relative"></div> | |||
|   <div id="bloodmini-container"></div> | |||
| </template> | |||
| <script setup> | |||
| import G6 from '@antv/g6'; | |||
| import {watch} from "vue"; | |||
| const props = defineProps({ | |||
|   data: { | |||
|     type: Object, | |||
|     default: () => {} | |||
|   }, | |||
|   currentTable: { | |||
|     type:Object, | |||
|     default:()=>{} | |||
|   } | |||
| }) | |||
| const g6data = ref([]) | |||
| 
 | |||
| function initG6() { | |||
|   const { | |||
|     Util, | |||
|     registerBehavior, | |||
|     registerEdge, | |||
|     registerNode | |||
|   } = G6; | |||
|   const isInBBox = (point, bbox) => { | |||
|     const { | |||
|       x, | |||
|       y | |||
|     } = point; | |||
|     const { | |||
|       minX, | |||
|       minY, | |||
|       maxX, | |||
|       maxY | |||
|     } = bbox; | |||
| 
 | |||
|     return x < maxX && x > minX && y > minY && y < maxY; | |||
|   }; | |||
|   const itemHeight = 20; | |||
|   registerBehavior("dice-er-scroll", { | |||
|     getDefaultCfg() { | |||
|       return { | |||
|         multiple: true, | |||
|       }; | |||
|     }, | |||
|     getEvents() { | |||
|       return { | |||
|         itemHeight, | |||
|         wheel: "scorll", | |||
|         click: "click", | |||
|         "node:mousemove": "move", | |||
|         "node:mousedown": "mousedown", | |||
|         "node:mouseup": "mouseup" | |||
|       }; | |||
|     }, | |||
|     scorll(e) { | |||
|       // e.preventDefault(); | |||
|       const { | |||
|         graph | |||
|       } = this; | |||
|       const nodes = graph.getNodes().filter((n) => { | |||
|         const bbox = n.getBBox(); | |||
| 
 | |||
|         return isInBBox(graph.getPointByClient(e.clientX, e.clientY), bbox); | |||
|       }); | |||
| 
 | |||
|       const x = e.deltaX || e.movementX; | |||
|       let y = e.deltaY || e.movementY; | |||
|       if (!y && navigator.userAgent.indexOf('Firefox') > -1) y = (-e.wheelDelta * 125) / 3 | |||
| 
 | |||
|       if (nodes) { | |||
|         const edgesToUpdate = new Set(); | |||
|         nodes.forEach((node) => { | |||
|           return; | |||
|           const model = node.getModel(); | |||
|           if (model.attrs.length < 2) { | |||
|             return; | |||
|           } | |||
|           const idx = model.startIndex || 0; | |||
|           let startX = model.startX || 0.5; | |||
|           let startIndex = idx + y * 0.02; | |||
|           startX -= x; | |||
|           if (startIndex < 0) { | |||
|             startIndex = 0; | |||
|           } | |||
|           if (startX > 0) { | |||
|             startX = 0; | |||
|           } | |||
|           if (startIndex > model.attrs.length - 1) { | |||
|             startIndex = model.attrs.length - 1; | |||
|           } | |||
|           graph.updateItem(node, { | |||
|             startIndex, | |||
|             startX, | |||
|           }); | |||
|           node.getEdges().forEach(edge => edgesToUpdate.add(edge)) | |||
|         }); | |||
|         // G6 update the related edges when graph.updateItem with a node according to the new properties | |||
|         // here you need to update the related edges manualy since the new properties { startIndex, startX } for the nodes are custom, and cannot be recognized by G6 | |||
|         edgesToUpdate.forEach(edge => edge.refresh()) | |||
|       } | |||
|     }, | |||
|     click(e) { | |||
|       const { | |||
|         graph | |||
|       } = this; | |||
|       const item = e.item; | |||
|       const shape = e.shape; | |||
|       if (!item) { | |||
|         return; | |||
|       } | |||
| if (shape.get("name") === "collapse") { | |||
|   graph.updateItem(item, { | |||
|     collapsed: true, | |||
|     size: [300, 50], | |||
|   }); | |||
|   setTimeout(() => { | |||
|     graph.layout(); | |||
|   }, 50); | |||
| } else if (shape.get("name") === "expand") { | |||
|   graph.updateItem(item, { | |||
|     collapsed: false, | |||
|     size: [300, 120], | |||
|   }); | |||
|   setTimeout(() => { | |||
|     graph.layout(); | |||
|   }, 50); | |||
| } | |||
| 
 | |||
| else if (shape.get("name") && shape.get("name").startsWith("item")) { | |||
|         let edges = graph.getEdges() | |||
|         let columnName = '' | |||
|         g6data.value.forEach(data => { | |||
|           if (data.id === item.getModel().id) { | |||
|             columnName = data.attrs[shape.get("name").split("-")[1]].key | |||
|           } | |||
|         }) | |||
|         edges.forEach(edg => { | |||
|           if (edg.getModel().sourceKey === columnName && edg.getModel().source === item.getModel().id) { | |||
|             edg.show() | |||
|           } else if(edg.getModel().targetKey === columnName && edg.getModel().target === item.getModel().id){ | |||
|             edg.show() | |||
|           } else{ | |||
|             edg.hide() | |||
|           } | |||
|         }) | |||
|         let nodes = graph.getNodes() | |||
|         let index = Number(shape.get("name").split("-")[1]) | |||
|         nodes.forEach(node => { | |||
|           let model = node.getModel() | |||
|           model.selectedIndex = NaN | |||
|           graph.updateItem(node, model) | |||
|         }) | |||
|         this.graph.updateItem(item, { | |||
|           selectedIndex: index, | |||
|         }); | |||
|       } else { | |||
|         graph.updateItem(item, { | |||
|           selectedIndex: NaN, | |||
|         }); | |||
|         let edges = graph.getEdges() | |||
|         edges.forEach(edg => { | |||
|           edg.show() | |||
|         }) | |||
|       } | |||
|     }, | |||
|     mousedown(e) { | |||
|       this.isMousedown = true; | |||
|     }, | |||
|     mouseup(e) { | |||
|       this.isMousedown = false; | |||
|     }, | |||
|     move(e) { | |||
|       if (this.isMousedown) return; | |||
|       // const name = e.shape.get("name"); | |||
|       // const item = e.item; | |||
|       // | |||
|       // if (name && name.startsWith("item")) { | |||
|       //   this.graph.updateItem(item, { | |||
|       //     selectedIndex: Number(name.split("-")[1]), | |||
|       //   }); | |||
|       // } else { | |||
|       //   this.graph.updateItem(item, { | |||
|       //     selectedIndex: NaN, | |||
|       //   }); | |||
|       // } | |||
|     }, | |||
|   }); | |||
| 
 | |||
|   registerEdge("dice-er-edge", { | |||
|     draw(cfg, group) { | |||
|       const edge = group.cfg.item; | |||
|       const sourceNode = edge.getSource().getModel(); | |||
|       const targetNode = edge.getTarget().getModel(); | |||
| 
 | |||
|       const sourceIndex = sourceNode.attrs.findIndex( | |||
|           (e) => e.key === cfg.sourceKey | |||
|       ); | |||
| 
 | |||
|       const sourceStartIndex = sourceNode.startIndex || 0; | |||
| 
 | |||
|       let sourceY = 15; | |||
| 
 | |||
|       if (!sourceNode.collapsed && sourceIndex > sourceStartIndex - 1) { | |||
|         sourceY = 30 + (sourceIndex - sourceStartIndex + 0.5) * itemHeight; | |||
|       } | |||
| 
 | |||
|       const targetIndex = targetNode.attrs.findIndex( | |||
|           (e) => e.key === cfg.targetKey | |||
|       ); | |||
| 
 | |||
|       const targetStartIndex = targetNode.startIndex || 0; | |||
| 
 | |||
|       let targetY = 15; | |||
| 
 | |||
|       if (!targetNode.collapsed && targetIndex > targetStartIndex - 1) { | |||
|         targetY = (targetIndex - targetStartIndex + 0.5) * itemHeight + 30; | |||
|       } | |||
| 
 | |||
|       const startPoint = { | |||
|         ...cfg.startPoint | |||
|       }; | |||
|       const endPoint = { | |||
|         ...cfg.endPoint | |||
|       }; | |||
| 
 | |||
|       startPoint.y = startPoint.y + sourceY; | |||
|       endPoint.y = endPoint.y + targetY; | |||
| 
 | |||
|       let shape; | |||
|       if (sourceNode.id !== targetNode.id) { | |||
|         shape = group.addShape("path", { | |||
|           attrs: { | |||
|             stroke: "#5B8FF9", | |||
|             path: [ | |||
|               ["M", startPoint.x, startPoint.y], | |||
|               [ | |||
|                 "C", | |||
|                 endPoint.x / 3 + (2 / 3) * startPoint.x, | |||
|                 startPoint.y, | |||
|                 endPoint.x / 3 + (2 / 3) * startPoint.x, | |||
|                 endPoint.y, | |||
|                 endPoint.x, | |||
|                 endPoint.y, | |||
|               ], | |||
|             ], | |||
|             endArrow: true, | |||
|           }, | |||
|           // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type | |||
|           name: "path-shape", | |||
|         }); | |||
|       } else { | |||
|         let gap = Math.abs((startPoint.y - endPoint.y) / 3); | |||
|         if (startPoint["index"] === 1) { | |||
|           gap = -gap; | |||
|         } | |||
|         shape = group.addShape("path", { | |||
|           attrs: { | |||
|             stroke: "#5B8FF9", | |||
|             path: [ | |||
|               ["M", startPoint.x, startPoint.y], | |||
|               [ | |||
|                 "C", | |||
|                 startPoint.x - gap, | |||
|                 startPoint.y, | |||
|                 startPoint.x - gap, | |||
|                 endPoint.y, | |||
|                 startPoint.x, | |||
|                 endPoint.y, | |||
|               ], | |||
|             ], | |||
|             endArrow: props.type === 'er'? {path: G6.Arrow.triangle(10,-10,6),fill:'#5B8FF9'} : false//cfg.endArrow, | |||
|           }, | |||
|           // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type | |||
|           name: "path-shape", | |||
|         }); | |||
|       } | |||
|       return shape; | |||
|     }, | |||
|     afterDraw(cfg, group) { | |||
|       const labelCfg = cfg.labelCfg || {}; | |||
|       const edge = group.cfg.item; | |||
|       const sourceNode = edge.getSource().getModel(); | |||
|       const targetNode = edge.getTarget().getModel(); | |||
|       if (sourceNode.collapsed && targetNode.collapsed) { | |||
|         return; | |||
|       } | |||
|       const path = group.find( | |||
|           (element) => element.get("name") === "path-shape" | |||
|       ); | |||
| 
 | |||
|       const labelStyle = Util.getLabelPosition(path, 0.5, 0, 0, true); | |||
|       const label = group.addShape("text", { | |||
|         attrs: { | |||
|           ...labelStyle, | |||
|           text: cfg.label || '', | |||
|           fill: "#000", | |||
|           textAlign: "center", | |||
|           stroke: "#fff", | |||
|           lineWidth: 1, | |||
|         }, | |||
|       }); | |||
|       label.rotateAtStart(labelStyle.rotate); | |||
|     }, | |||
|   }); | |||
| 
 | |||
|   registerNode("dice-er-box", { | |||
|     draw(cfg, group) { | |||
|       const width = 300; | |||
|       const { | |||
|         attrs = [], | |||
|         startIndex = 0, | |||
|         selectedIndex, | |||
|         collapsed, | |||
|         icon, | |||
|       } = cfg; | |||
|       let currentTableLabel = props.currentTable.tabEngName | |||
|       if (props.currentTable.tabCnName && props.currentTable.tabCnName.length>0){ | |||
|         currentTableLabel += "("+props.currentTable.tabCnName+")" | |||
|       }else if (props.currentTable.tabCrrctName && props.currentTable.tabCnName.tabCrrctName>0){ | |||
|         currentTableLabel += "("+props.currentTable.tabCrrctName+")" | |||
|       } | |||
|       const list = attrs; | |||
|       const itemCount = list.length; | |||
|       const boxStyle = { | |||
|         stroke: currentTableLabel === cfg.label?"#67C23A":"#096DD9", | |||
|         radius: 4, | |||
|       }; | |||
|       const afterList = list.slice( | |||
|           Math.floor(startIndex), | |||
|           Math.floor(startIndex + itemCount) | |||
|       ); | |||
|       const offsetY = (0.5 - (startIndex % 1)) * itemHeight + 30; | |||
|       //表头盒子 | |||
|       group.addShape("rect", { | |||
|         attrs: { | |||
|           fill: boxStyle.stroke, | |||
|           height: 30, | |||
|           width, | |||
|           radius: [boxStyle.radius, boxStyle.radius, 0, 0], | |||
|         }, | |||
|         draggable: true, | |||
|       }); | |||
| 
 | |||
|       let fontLeft = 12; | |||
|       //图标 | |||
|       if (icon && icon.show !== false) { | |||
|         group.addShape("image", { | |||
|           attrs: { | |||
|             x: 8, | |||
|             y: 8, | |||
|             height: 16, | |||
|             width: 16, | |||
|             ...icon, | |||
|           }, | |||
|         }); | |||
|         fontLeft += 18; | |||
|       } | |||
|       //表名 | |||
|       group.addShape("text", { | |||
|         attrs: { | |||
|           y: 22, | |||
|           x: fontLeft, | |||
|           fill: "#fff", | |||
|           text: cfg.label, | |||
|           fontSize: 12, | |||
|           fontWeight: 500, | |||
|         }, | |||
|       }); | |||
| 
 | |||
|       //展开div | |||
|       group.addShape("rect", { | |||
|         attrs: { | |||
|           x: 0, | |||
|           y: collapsed ? 30 : (list.length + 1) * itemHeight + 15, | |||
|           height: 15, | |||
|           width, | |||
|           fill: "#eee", | |||
|           radius: [0, 0, boxStyle.radius, boxStyle.radius], | |||
|           cursor: "pointer", | |||
|         }, | |||
|         // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type | |||
|         name: collapsed ? "expand" : "collapse", | |||
|       }); | |||
| 
 | |||
|       //展开图标 | |||
|       group.addShape("text", { | |||
|         attrs: { | |||
|           x: width / 2 - 6, | |||
|           y: (collapsed ? 30 : (list.length + 1) * itemHeight + 15) + 12, | |||
|           text: collapsed ? "+" : "-", | |||
|           width, | |||
|           fill: "#000", | |||
|           radius: [0, 0, boxStyle.radius, boxStyle.radius], | |||
|           cursor: "pointer", | |||
|         }, | |||
|         // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type | |||
|         name: collapsed ? "expand" : "collapse", | |||
|       }); | |||
|       //表格外框 | |||
|       const keyshape = group.addShape("rect", { | |||
|         attrs: { | |||
|           x: 0, | |||
|           y: 0, | |||
|           width, | |||
|           height: collapsed ? 45 : (list.length + 1) * itemHeight + 15 + 15, | |||
|           ...boxStyle, | |||
|         }, | |||
|         draggable: true, | |||
|       }); | |||
| 
 | |||
|       if (collapsed) { | |||
|         return keyshape; | |||
|       } | |||
| 
 | |||
|       const listContainer = group.addGroup({}); | |||
|       //字段内容 | |||
|       listContainer.setClip({ | |||
|         type: "rect", | |||
|         attrs: { | |||
|           x: -8, | |||
|           y: 30, | |||
|           width: width + 16, | |||
|           height: (list.length + 1) * itemHeight + 15 - 30, | |||
|         }, | |||
|       }); | |||
|       //字段背景框 | |||
|       listContainer.addShape({ | |||
|         type: "rect", | |||
|         attrs: { | |||
|           x: 1, | |||
|           y: 30, | |||
|           width: width - 2, | |||
|           height: (list.length + 1) * itemHeight + 15 - 30, | |||
|           fill: "#fff", | |||
|         }, | |||
|         draggable: true, | |||
|       }); | |||
| 
 | |||
|       if (list.length + 1 > itemCount) { | |||
|         const barStyle = { | |||
|           width: 4, | |||
|           padding: 0, | |||
|           boxStyle: { | |||
|             stroke: "#00000022", | |||
|           }, | |||
|           innerStyle: { | |||
|             fill: "#00000022", | |||
|           }, | |||
|         }; | |||
|         //滚动轴 | |||
|         listContainer.addShape("rect", { | |||
|           attrs: { | |||
|             y: 30, | |||
|             x: width - barStyle.padding - barStyle.width, | |||
|             width: barStyle.width, | |||
|             height: (list.length + 1) * itemHeight - 30, | |||
|             ...barStyle.boxStyle, | |||
|           }, | |||
|         }); | |||
| 
 | |||
|         const indexHeight = | |||
|             afterList.length > itemCount ? | |||
|                 (afterList.length / list.length + 1) * (list.length + 1) * itemHeight : | |||
|                 10; | |||
|         //滚动轴框 | |||
|         listContainer.addShape("rect", { | |||
|           attrs: { | |||
|             y: 30 + | |||
|                 barStyle.padding + | |||
|                 (startIndex / list.length + 1) * ((list.length + 1) * itemHeight - 30), | |||
|             x: width - barStyle.padding - barStyle.width, | |||
|             width: barStyle.width, | |||
|             height: Math.min((list.length + 1) * itemHeight, indexHeight), | |||
|             ...barStyle.innerStyle, | |||
|           }, | |||
|         }); | |||
|       } | |||
|       if (afterList) { | |||
|         afterList.forEach((e, i) => { | |||
|           const isSelected = | |||
|               Math.floor(startIndex) + i === Number(selectedIndex); | |||
|           let { | |||
|             key = "", type | |||
|           } = e; | |||
|           if (type) { | |||
|             key += " - " + type; | |||
|           } | |||
|           const label = key; | |||
|           //字段框 | |||
|           listContainer.addShape("rect", { | |||
|             attrs: { | |||
|               x: 1, | |||
|               y: i * itemHeight - itemHeight / 2 + offsetY, | |||
|               width: width - 4, | |||
|               height: itemHeight, | |||
|               radius: 2, | |||
|               lineWidth: 1, | |||
|               cursor: "pointer", | |||
|             }, | |||
|             // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type | |||
|             name: `item-${Math.floor(startIndex) + i}-content`, | |||
|             draggable: true, | |||
|           }); | |||
| 
 | |||
|           if (!cfg.hideDot) { | |||
|             //左边连接桩 | |||
|             listContainer.addShape("circle", { | |||
|               attrs: { | |||
|                 x: 0, | |||
|                 y: i * itemHeight + offsetY, | |||
|                 r: 3, | |||
|                 stroke: boxStyle.stroke, | |||
|                 fill: "white", | |||
|                 radius: 2, | |||
|                 lineWidth: 1, | |||
|                 cursor: "pointer", | |||
|               }, | |||
|             }); | |||
|             //右边连接桩 | |||
|             listContainer.addShape("circle", { | |||
|               attrs: { | |||
|                 x: width, | |||
|                 y: i * itemHeight + offsetY, | |||
|                 r: 3, | |||
|                 stroke: boxStyle.stroke, | |||
|                 fill: "white", | |||
|                 radius: 2, | |||
|                 lineWidth: 1, | |||
|                 cursor: "pointer", | |||
|               }, | |||
|             }); | |||
|           } | |||
|           //字段文字 | |||
|           listContainer.addShape("text", { | |||
|             attrs: { | |||
|               x: 12, | |||
|               y: i * itemHeight + offsetY + 6, | |||
|               text: label, | |||
|               fontSize: 12, | |||
|               fill: "#000", | |||
|               fontFamily: "Avenir,-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol", | |||
|               full: e, | |||
|               fontWeight: isSelected ? 500 : 100, | |||
|               cursor: "pointer", | |||
|             }, | |||
|             // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type | |||
|             name: `item-${Math.floor(startIndex) + i}`, | |||
|           }); | |||
|         }); | |||
|       } | |||
| 
 | |||
| 
 | |||
|       return keyshape; | |||
|     }, | |||
|     getAnchorPoints() { | |||
|       return [ | |||
|         [0, 0], | |||
|         [1, 0], | |||
|       ]; | |||
|     }, | |||
|   }); | |||
| 
 | |||
|   const dataTransform = (data) => { | |||
|     const nodes = []; | |||
|     let edges = []; | |||
|     data.map((node) => { | |||
|       nodes.push({ | |||
|         ...node | |||
|       }); | |||
|       if (node.attrs) { | |||
|         node.attrs.forEach((attr) => { | |||
|           if (attr.relation) { | |||
|             attr.relation.forEach((relation) => { | |||
|               edges.push({ | |||
|                 source: node.id, | |||
|                 target: relation.nodeId, | |||
|                 sourceKey: attr.key, | |||
|                 endArrow: relation.endArrow, | |||
|                 targetKey: relation.key, | |||
|                 label: relation.label, | |||
|               }); | |||
|             }); | |||
|           } | |||
| 
 | |||
|         }); | |||
|       } | |||
|     }); | |||
|     return { | |||
|       nodes, | |||
|       edges, | |||
|     }; | |||
|   } | |||
| 
 | |||
|   const container = document.getElementById("bloodRelationG6"); | |||
|   const mini_container = document.getElementById("bloodmini-container"); | |||
| const width = container.scrollWidth; | |||
| const height = container.scrollHeight || container.offsetHeight || 500; | |||
| 
 | |||
|   // 实例化 minimap 插件 | |||
|   const minimap = new G6.Minimap({ | |||
|     size: [200, 200], | |||
|     container: mini_container, | |||
|     type: 'delegate', | |||
|   }); | |||
|   const graph = new G6.Graph({ | |||
|     container: container, | |||
|     plugins: [minimap], | |||
|     width, | |||
|     height, | |||
|     defaultNode: { | |||
|       size: [350, 200], | |||
|       type: 'dice-er-box', | |||
|       color: '#5B8FF9', | |||
|       style: { | |||
|         fill: '#9EC9FF', | |||
|         lineWidth: 3, | |||
|       }, | |||
|       labelCfg: { | |||
|         style: { | |||
|           fill: 'black', | |||
|           fontSize: 20, | |||
|         }, | |||
|       }, | |||
|     }, | |||
|     defaultEdge: { | |||
|       type: 'dice-er-edge', | |||
|       style: { | |||
|         stroke: '#e2e2e2', | |||
|         lineWidth: 4, | |||
|         endArrow: {path: G6.Arrow.triangle(10,-10,6),fill:'#5B8FF9'}, | |||
|       }, | |||
|     }, | |||
|     modes: { | |||
|       default: ['dice-er-scroll', 'drag-node', 'drag-canvas', 'zoom-canvas'], | |||
|     }, | |||
|     layout: { | |||
|       type: 'dagre', | |||
|       rankdir: 'LR', | |||
|       align: 'UL', | |||
|       controlPoints: true, | |||
|       nodesep: 1, | |||
|       ranksep: 1 | |||
|     }, | |||
|     animate: true, | |||
|     // fitView: true | |||
|   }) | |||
|   graph.data(dataTransform(g6data.value)) | |||
|   graph.render(); | |||
| } | |||
| 
 | |||
| 
 | |||
| watch( | |||
|     () => props.data, | |||
|     (val) => { | |||
|       g6data.value = [] | |||
|       if (props.data.tableList && props.data.tableList.length>0){ | |||
|         for (let i = 0; i < props.data.tableList.length; i++) { | |||
|           let table = props.data.tableList[i] | |||
|           let g6Tab = { | |||
|             id: table.ssys_id+"-"+table.mdl_name+"-"+table.tab_eng_name, | |||
|             label: table.tab_eng_name + ((table.tab_cn_name && table.tab_cn_name.length>0)?"("+table.tab_cn_name+")":""), | |||
|             attrs:[], | |||
|             collapsed:true | |||
|             } | |||
|           for (let j = 0; j < table.column.length; j++) { | |||
|             g6Tab.attrs.push({ | |||
|               key: table.column[j].fldEngName, | |||
|               type: table.column[j].fldType | |||
|             }) | |||
|           } | |||
|           g6data.value.push(g6Tab) | |||
|         } | |||
|         if (props.data.relation && props.data.relation.length>0){ | |||
|           for (let i = 0; i < props.data.relation.length; i++) { | |||
|             let relation = props.data.relation[i] | |||
|             let key = relation.targetColName.toLowerCase() | |||
|             let tableKey = relation.sourceSysId+"-"+relation.sourceMdlName.toLowerCase()+"-"+relation.sourceTableName.toLowerCase() | |||
|             let nodeId = relation.targetSysId+"-"+relation.targetMdlName.toLowerCase()+"-"+relation.targetTableName.toLowerCase() | |||
|             if (g6data.value.length > 0){ | |||
|               for (let j = 0; j < g6data.value.length; j++) { | |||
|                 if (g6data.value[j].id === tableKey){ | |||
|                   for (let k = 0; k < g6data.value[j].attrs.length; k++) { | |||
|                     if (g6data.value[j].attrs[k].key === relation.sourceColName.toLowerCase()){ | |||
|                       if (g6data.value[j].attrs[k].relation && g6data.value[j].attrs[k].relation.length>0){ | |||
|                         let hasRelation = false | |||
|                         for (let l = 0; l < g6data.value[j].attrs[k].relation.length; l++) { | |||
|                           if (g6data.value[j].attrs[k].relation[l].key === key && g6data.value[j].attrs[k].relation[l].nodeId === nodeId){ | |||
|                             hasRelation = true | |||
|                           } | |||
|                         } | |||
|                         if (!hasRelation){ | |||
|                           g6data.value[j].attrs[k].relation.push({ | |||
|                             key: key, | |||
|                             nodeId: nodeId, | |||
|                             endArrow: true | |||
|                           }) | |||
|                         } | |||
|                       }else { | |||
|                         g6data.value[j].attrs[k].relation = [{ | |||
|                           key: key, | |||
|                           nodeId: nodeId, | |||
|                           endArrow: true | |||
|                         }] | |||
|                       } | |||
|                     } | |||
|                   } | |||
|                 } | |||
|               } | |||
|             } | |||
|           } | |||
|         } | |||
| 
 | |||
|         let g6 = document.getElementById("bloodRelationG6") | |||
|         const mini_container = document.getElementById("bloodmini-container"); | |||
|         if (mini_container){ | |||
|           mini_container.innerHTML='' | |||
|         } | |||
|         if (g6){ | |||
|           g6.innerHTML='' | |||
|           initG6() | |||
|         } | |||
|       } | |||
| 
 | |||
|     }, | |||
|     { deep: true,immediate: true } | |||
| ) | |||
| 
 | |||
| </script> | |||
| 
 | |||
| <style scoped lang="scss"> | |||
| #bloodmini-container{ | |||
|   position: absolute !important; | |||
|   right: 20px; | |||
|   bottom: 20px; | |||
|   border: 1px solid #e2e2e2; | |||
|   border-radius: 4px; | |||
|   background-color: rgba(255, 255, 255, 0.9); | |||
|   z-index: 10; | |||
| } | |||
| 
 | |||
| </style> | |||
| @ -1,22 +1,250 @@ | |||
| <template> | |||
|   <div class="sql-container"> | |||
|     <SQLCodeMirror v-if="activeColumnTab === 'proc'" :data="procStr" :dbType="dbType" /> | |||
|   <div class="app-container" ref="containerRef"> | |||
|     <!-- 左侧 SQL 编辑区 --> | |||
|     <div class="sql-container" :style="{ width: leftWidth + 'px' }"> | |||
|       <!-- 工具栏 --> | |||
|       <div class="toolbar"> | |||
| 
 | |||
| 
 | |||
|         <!-- 数据库类型 --> | |||
|         <el-select | |||
|           v-model="dbType" | |||
|           placeholder="选择数据库类型" | |||
|           size="small" | |||
|           style="width: 140px; margin-left: 10px;" | |||
|         > | |||
|           <el-option label="MySQL" value="MYSQL" /> | |||
|           <el-option label="PostgreSQL" value="PG" /> | |||
|           <el-option label="SQL Server" value="MSSQL" /> | |||
|           <el-option label="Oracle" value="ORACLE" /> | |||
|           <el-option label="DB2" value="DB2" /> | |||
|         </el-select> | |||
|         <!-- 系统选择 --> | |||
|         <el-select | |||
|           v-model="selectedSystem" | |||
|           placeholder="选择系统" | |||
|           size="small" | |||
|           style="width: 160px" | |||
|         > | |||
|           <el-option | |||
|             v-for="sys in dsSysList" | |||
|             :key="sys.id" | |||
|             :label="sys.name" | |||
|             :value="sys.id" | |||
|           /> | |||
|         </el-select> | |||
|         <!-- 模式 --> | |||
|         <el-input | |||
|           v-model="defaultModel" | |||
|           placeholder="输入模式名" | |||
|           size="small" | |||
|           style="width: 140px; margin-left: 10px;" | |||
|           clearable | |||
|         /> | |||
| 
 | |||
|         <!-- 执行按钮 --> | |||
|         <el-button | |||
|           type="primary" | |||
|           size="small" | |||
|           style="margin-left: 10px" | |||
|           @click="executeSql" | |||
|         > | |||
|           执行 | |||
|         </el-button> | |||
|       </div> | |||
| 
 | |||
|       <!-- SQL 编辑器 --> | |||
|       <SQLCodeMirror | |||
|         v-if="activeColumnTab === 'proc'" | |||
|         v-model:data="procStr" | |||
|         :dbType="dbType" | |||
|       /> | |||
|     </div> | |||
| 
 | |||
|     <!-- 分隔条 --> | |||
|     <div class="divider" @mousedown="startDragging"></div> | |||
| 
 | |||
|     <!-- 右侧 血缘关系图 --> | |||
|     <div class="relation-container"> | |||
|       <BloodRelation :currentTable="currentMetaData" :data="bloodRelation" /> | |||
|     </div> | |||
|   </div> | |||
| </template> | |||
| 
 | |||
| <script setup> | |||
| import { ref } from 'vue' | |||
| import SQLCodeMirror from '@/components/codemirror/SQLCodeMirror.vue' | |||
| import { ref, reactive, onMounted, onBeforeUnmount } from 'vue' | |||
| import { ElMessage } from 'element-plus' | |||
| import { getMetaDataBloodRelship, runBloodAnalysisBySql } from '@/api/meta/metaInfo' | |||
| import BloodRelation from '@/views/meta/metaInfo/bloodRelationSql.vue' | |||
| import SQLCodeMirror from '@/components/codemirror/SQLCodeMirrorSqlFlow.vue' | |||
| import useUserStore from '@/store/modules/user' | |||
| import cache from "@/plugins/cache"; | |||
| 
 | |||
| const userStore = useUserStore() | |||
| const dsSysList = userStore.dsSysList | |||
| 
 | |||
| // ========================= 数据定义 ========================= | |||
| const activeColumnTab = ref('proc') | |||
| const procStr = ref('SELECT * FROM users LIMIT 100;') | |||
| const dbType = ref('MYSQL') | |||
| const containerRef = ref(null) | |||
| 
 | |||
| // 当前选中系统 | |||
| const selectedSystem = ref(dsSysList?.[0]?.id || null) | |||
| // 模式(可手动输入) | |||
| const defaultModel = ref('') | |||
| 
 | |||
| // 当前元数据信息 | |||
| const currentMetaData = reactive({ | |||
|   tabEngName: 't_dim_comp', | |||
|   tabCnName: '公司维度表', | |||
|   ssysCd: 'PG_CONN', | |||
|   ssysId: 1, | |||
|   mdlName: 'public', | |||
|   tabCrrctName: '', | |||
|   tabDesc: '', | |||
|   govFlag: null, | |||
|   pic: '', | |||
|   tags: [] | |||
| }) | |||
| 
 | |||
| // 血缘图数据 | |||
| const bloodRelation = ref([]) | |||
| 
 | |||
| // ========================= 拖拽逻辑 ========================= | |||
| const leftWidth = ref(600) | |||
| const isDragging = ref(false) | |||
| let startX = 0 | |||
| let startWidth = 0 | |||
| 
 | |||
| const startDragging = (e) => { | |||
|   isDragging.value = true | |||
|   startX = e.clientX | |||
|   startWidth = leftWidth.value | |||
|   document.body.style.userSelect = 'none' | |||
|   document.addEventListener('mousemove', handleDragging) | |||
|   document.addEventListener('mouseup', stopDragging) | |||
| } | |||
| 
 | |||
| const handleDragging = (e) => { | |||
|   if (!isDragging.value) return | |||
|   const delta = e.clientX - startX | |||
|   const newWidth = startWidth + delta | |||
|   const minWidth = 300 | |||
|   const maxWidth = window.innerWidth - 400 | |||
|   leftWidth.value = Math.min(Math.max(newWidth, minWidth), maxWidth) | |||
| } | |||
| 
 | |||
| const stopDragging = () => { | |||
|   isDragging.value = false | |||
|   document.body.style.userSelect = '' | |||
|   document.removeEventListener('mousemove', handleDragging) | |||
|   document.removeEventListener('mouseup', stopDragging) | |||
| } | |||
| 
 | |||
| // ========================= 方法 ========================= | |||
| const changeBloodOption = () => { | |||
|   getMetaDataBloodRelship(currentMetaData.ssysId).then((res) => { | |||
|     bloodRelation.value = res.data | |||
|   }) | |||
| } | |||
| 
 | |||
| /** | |||
|  * 执行 SQL 并生成血缘分析图 | |||
|  */ | |||
| const executeSql = async () => { | |||
|   if (!selectedSystem.value) { | |||
|     ElMessage.warning('请选择系统') | |||
|     return | |||
|   } | |||
| 
 | |||
|   if (!procStr.value.trim()) { | |||
|     ElMessage.warning('请输入 SQL 语句') | |||
|     return | |||
|   } | |||
| 
 | |||
|   const params = { | |||
|     sqlType: dbType.value, | |||
|     defaultSystem: selectedSystem.value, | |||
|     defaultModel: defaultModel.value || '', | |||
|     sql: procStr.value, | |||
|     userName: cache.local.get("username"), | |||
|     password: cache.local.get("password") | |||
|   } | |||
| console.log(params) | |||
|   try { | |||
|     ElMessage.info('正在执行血缘分析,请稍候...') | |||
|     const res = await runBloodAnalysisBySql(params) | |||
|     if (res?.data) { | |||
|       bloodRelation.value = res.data | |||
|       ElMessage.success('血缘分析执行成功') | |||
|     } else { | |||
|       ElMessage.warning('未返回血缘数据') | |||
|     } | |||
|   } catch (err) { | |||
|     console.error(err) | |||
|     ElMessage.error('执行血缘分析失败') | |||
|   } | |||
| } | |||
| 
 | |||
| // ========================= 生命周期 ========================= | |||
| onMounted(() => { | |||
|   changeBloodOption() // 初始化时仅加载血缘,不执行SQL | |||
| }) | |||
| 
 | |||
| onBeforeUnmount(() => { | |||
|   stopDragging() | |||
| }) | |||
| </script> | |||
| 
 | |||
| <style scoped> | |||
| .app-container { | |||
|   display: flex; | |||
|   flex-direction: row; | |||
|   height: 100vh; | |||
|   width: 100%; | |||
|   overflow: hidden; | |||
| } | |||
| 
 | |||
| /* 左侧 SQL 编辑区 */ | |||
| .sql-container { | |||
|   padding: 16px; | |||
|   height: 100%; | |||
|   background: #fafafa; | |||
|   height: 100vh; | |||
|   border-right: 1px solid #e0e0e0; | |||
|   overflow: auto; | |||
|   transition: width 0.1s ease-out; | |||
|   display: flex; | |||
|   flex-direction: column; | |||
| } | |||
| 
 | |||
| /* 工具栏 */ | |||
| .toolbar { | |||
|   display: flex; | |||
|   align-items: center; | |||
|   background: #f5f7fa; | |||
|   border-bottom: 1px solid #e0e0e0; | |||
|   padding: 8px 12px; | |||
|   height: 45px; | |||
| } | |||
| 
 | |||
| /* 分隔条 */ | |||
| .divider { | |||
|   width: 6px; | |||
|   cursor: col-resize; | |||
|   background-color: #dcdcdc; | |||
|   transition: background-color 0.2s; | |||
|   flex-shrink: 0; | |||
| } | |||
| .divider:hover { | |||
|   background-color: #aaa; | |||
| } | |||
| 
 | |||
| /* 右侧 血缘关系图 */ | |||
| .relation-container { | |||
|   flex: 1; | |||
|   height: 100%; | |||
|   overflow: hidden; | |||
|   padding: 8px; | |||
|   background: #fff; | |||
| } | |||
| </style> | |||
|  | |||
					Loading…
					
					
				
		Reference in new issue