3 changed files with 988 additions and 6 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,182 @@ |
|||
<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: 160px" |
|||
> |
|||
<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-select> |
|||
|
|||
<el-button |
|||
type="primary" |
|||
size="small" |
|||
style="margin-left: 10px" |
|||
@click="executeSql" |
|||
> |
|||
执行 |
|||
</el-button> |
|||
</div> |
|||
|
|||
<!-- SQL 编辑器 --> |
|||
<SQLCodeMirror |
|||
v-if="activeColumnTab === 'proc'" |
|||
: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 } from '@/api/meta/metaInfo' |
|||
import BloodRelation from '@/views/meta/metaInfo/bloodRelationSql.vue' |
|||
import SQLCodeMirror from '@/components/codemirror/SQLCodeMirrorSqlFlow.vue' |
|||
|
|||
// ========================= 数据定义 ========================= |
|||
const activeColumnTab = ref('proc') |
|||
const procStr = ref('SELECT * FROM users LIMIT 100;') |
|||
const dbType = ref('MYSQL') |
|||
const containerRef = ref(null) |
|||
|
|||
// 当前元数据信息 |
|||
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 containerLeft = containerRef.value?.getBoundingClientRect().left || 0 |
|||
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 |
|||
}) |
|||
} |
|||
|
|||
const executeSql = () => { |
|||
console.log('执行 SQL:', procStr.value) |
|||
console.log('数据库类型:', dbType.value) |
|||
ElMessage.success(`已在 ${dbType.value} 数据库中执行 SQL`) |
|||
} |
|||
|
|||
// ========================= 生命周期 ========================= |
|||
onMounted(() => { |
|||
changeBloodOption() |
|||
}) |
|||
|
|||
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