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> | <template> | ||||
|   <div class="sql-container"> |   <div class="app-container" ref="containerRef"> | ||||
|     <SQLCodeMirror v-if="activeColumnTab === 'proc'" :data="procStr" :dbType="dbType" /> |     <!-- 左侧 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> |   </div> | ||||
| </template> | </template> | ||||
| 
 | 
 | ||||
| <script setup> | <script setup> | ||||
| import { ref } from 'vue' | import { ref, reactive, onMounted, onBeforeUnmount } from 'vue' | ||||
| import SQLCodeMirror from '@/components/codemirror/SQLCodeMirror.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 activeColumnTab = ref('proc') | ||||
| const procStr = ref('SELECT * FROM users LIMIT 100;') | const procStr = ref('SELECT * FROM users LIMIT 100;') | ||||
| const dbType = ref('MYSQL') | 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> | </script> | ||||
| 
 | 
 | ||||
| <style scoped> | <style scoped> | ||||
|  | .app-container { | ||||
|  |   display: flex; | ||||
|  |   flex-direction: row; | ||||
|  |   height: 100vh; | ||||
|  |   width: 100%; | ||||
|  |   overflow: hidden; | ||||
|  | } | ||||
|  | 
 | ||||
|  | /* 左侧 SQL 编辑区 */ | ||||
| .sql-container { | .sql-container { | ||||
|   padding: 16px; |   height: 100%; | ||||
|   background: #fafafa; |   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> | </style> | ||||
|  | |||||
					Loading…
					
					
				
		Reference in new issue