< 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 . toLowerCase ( ) + "-" + table . tab_eng_name . toLowerCase ( ) ,
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 : 20 px ;
bottom : 20 px ;
border : 1 px solid # e2e2e2 ;
border - radius : 4 px ;
background - color : rgba ( 255 , 255 , 255 , 0.9 ) ;
z - index : 10 ;
}
< / style >