< template>
< div class = "content-main" >
< div class = "tool-container" >
< div @ click = "undo" class = "command" title= "后退" >
< Icon icon= "ant-design:undo-outlined" / >
< / div>
< div @ click = "redo" class = "command" title= "前进" >
< Icon icon= "ant-design:redo-outlined" / >
< / div>
< el- divider direction= "vertical" / >
< div @ click = "copy" class = "command" title= "复制" >
< Icon icon= "ant-design:copy-filled" / >
< / div>
< div @ click = "paste" class = "command" title= "粘贴" >
< Icon icon= "fa-solid:paste" / >
< / div>
< div @ click = "del" class = "command" title= "删除" >
< Icon icon= "ant-design:delete-filled" / >
< / div>
< el- divider direction= "vertical" / >
< div @ click = "save" class = "command" title= "保存" >
< Icon icon= "ant-design:save-filled" / >
< / div>
< el- divider direction= "vertical" / >
< div @ click = "exportPng" class = "command" title= "导出PNG" >
< Icon icon= "ant-design:file-image-filled" / >
< / div>
< / div>
< div class = "content-container" id= "" >
< div class = "content" >
< div class = "stencil" ref= "stencilContainer" > < / div>
< div class = "graph-content" id= "graphContainer" ref= "graphContainer" > < / div>
< div class = "editor-sidebar" >
< div class = "edit-panel" >
< el- card shadow= "never" >
< template #header>
< div class = "card-header" >
< span> { { cellFrom. title } } < / span>
< / div>
< / template>
< el- form : model= "nodeFrom" label- width= "50px" v- if = "nodeFrom.show" >
< el- form- item label= "label" >
< el- input v- model= "nodeFrom.label" @ blur = "changeLabel" / >
< / el- form- item>
< el- form- item label= "desc" >
< el- input type= "textarea" v- model= "nodeFrom.desc" @ blur = "changeDesc" / >
< / el- form- item>
< / el- form>
< el- form : model= "cellFrom" label- width= "50px" v- if = "cellFrom.show" >
< el- form- item label= "label" >
< el- input v- model= "cellFrom.label" @ blur = "changeEdgeLabel" / >
< / el- form- item>
< ! -- < el- form- item label= "连线方式" >
< el- select v- model= "cellFrom.edgeType" class = "m-2" placeholder= "Select" @ change = "changeEdgeType" >
< el- option
v- for = "item in EDGE_TYPE_LIST"
: key= "item.type"
: label= "item.name"
: value= "item.type"
/ >
< / el- select>
< / el- form- item> -- >
< / el- form>
< / el- card>
< / div>
< div>
< el- card shadow= "never" >
< template #header>
< div class = "card-header" >
< span> Minimap< / span>
< / div>
< / template>
< div class = "minimap" ref= "miniMapContainer" > < / div>
< / el- card>
< / div>
< / div>
< / div>
< / div>
< div v- if = "showMenu" class = "node-menu" ref= "nodeMenu" >
< div
class = "menu-item"
v- for = "(item, index) in PROCESSING_TYPE_LIST"
: key= "index"
@ click = "addNodeTool(item)"
>
< el- image : src= "item.image" style= "width: 16px; height: 16px" fit= "fill" / >
< span> { { item. name } } < / span>
< / div>
< / div>
< / div>
< / template>
< script setup lang= "ts" >
import { Graph, Path, Edge, StringExt, Node, Cell, Model, DataUri } from '@antv/x6'
import { Transform } from '@antv/x6-plugin-transform'
import { Selection } from '@antv/x6-plugin-selection'
import { Snapline } from '@antv/x6-plugin-snapline'
import { Keyboard } from '@antv/x6-plugin-keyboard'
import { Clipboard } from '@antv/x6-plugin-clipboard'
import { History } from '@antv/x6-plugin-history'
import { MiniMap } from '@antv/x6-plugin-minimap'
import { Stencil } from '@antv/x6-plugin-stencil'
import { Export } from '@antv/x6-plugin-export'
import { ref, onMounted, reactive, toRefs, nextTick, onUnmounted } from 'vue'
import '@/styles/animation.less'
import { ElMessage, ElCard, ElForm, ElFormItem, ElInput, ElImage, ElDivider } from 'element-plus'
const stencilContainer = ref ( )
const graphContainer = ref ( )
const miniMapContainer = ref ( )
let graph: any = null
const state = reactive ( {
cellFrom: {
title: 'Canvas' ,
label: '' ,
desc: '' ,
show: false ,
id: '' ,
edgeType: 'topBottom'
} ,
nodeFrom: {
title: 'Canvas' ,
label: '' ,
desc: '' ,
show: false ,
id: ''
} ,
showMenu: false ,
data: {
nodes: [
{
id: 'ac51fb2f-2753-4852-8239-53672a29bb14' ,
position: {
x: - 340 ,
y: - 160
} ,
data: {
name: '诗名' ,
type: 'OUTPUT' ,
desc: '春望'
}
} ,
{
id: '81004c2f-0413-4cc6-8622-127004b3befa' ,
position: {
x: - 340 ,
y: - 10
} ,
data: {
name: '第一句' ,
type: 'SYNC' ,
desc: '国破山河在'
}
} ,
{
id: '7505da25-1308-4d7a-98fd-e6d5c917d35d' ,
position: {
x: - 140 ,
y: 180
} ,
data: {
name: '结束' ,
type: 'INPUT' ,
desc: '城春草木胜'
}
}
] ,
edges: [
{
id: '6eea5dc9-4e15-4e78-959f-ee13ec59d11c' ,
shape: 'processing-curve' ,
source: { cell: 'ac51fb2f-2753-4852-8239-53672a29bb14' , port: '-out' } ,
target: { cell: '81004c2f-0413-4cc6-8622-127004b3befa' , port: '-in' } ,
zIndex: - 1 ,
data: {
source: 'ac51fb2f-2753-4852-8239-53672a29bb14' ,
target: '81004c2f-0413-4cc6-8622-127004b3befa'
}
} ,
{
id: '8cbce713-54be-4c07-8efa-59c505f74ad7' ,
labels: [ '下半句' ] ,
shape: 'processing-curve' ,
source: { cell: '81004c2f-0413-4cc6-8622-127004b3befa' , port: '-out' } ,
target: { cell: '7505da25-1308-4d7a-98fd-e6d5c917d35d' , port: '-in' } ,
data: {
source: '81004c2f-0413-4cc6-8622-127004b3befa' ,
target: '7505da25-1308-4d7a-98fd-e6d5c917d35d'
}
}
]
} ,
nodeStatusList: [
{
id: 'ac51fb2f-2753-4852-8239-53672a29bb14' ,
status: 'success'
} ,
{
id: '81004c2f-0413-4cc6-8622-127004b3befa' ,
status: 'success'
}
] ,
edgeStatusList: [
{
id: '6eea5dc9-4e15-4e78-959f-ee13ec59d11c' ,
status: 'success'
} ,
{
id: '8cbce713-54be-4c07-8efa-59c505f74ad7' ,
status: 'executing'
}
] ,
PROCESSING_TYPE_LIST : [
{
type: 'SYNC' ,
name: '数据同步' ,
image: new URL ( '@/assets/imgs/persimmon.png' , import . meta. url) . href
} ,
{
type: 'INPUT' ,
name: '结束' ,
image: new URL ( '@/assets/imgs/lime.png' , import . meta. url) . href
}
] ,
EDGE_TYPE_LIST : [
{
type: 'topBottom' ,
name: '上下'
} ,
{
type: 'leftRight' ,
name: '左右'
}
]
} )
const { cellFrom, nodeFrom, showMenu, PROCESSING_TYPE_LIST } = toRefs ( state)
let nodeMenu = ref ( )
enum NodeType {
INPUT = 'INPUT' ,
FILTER = 'FILTER' ,
JOIN = 'JOIN' ,
UNION = 'UNION' ,
AGG = 'AGG' ,
OUTPUT = 'OUTPUT' ,
SYNC = 'SYNC'
}
interface Position {
x: number
y: number
}
function init ( ) {
graph = new Graph ( {
container: graphContainer. value,
grid: true ,
panning: {
enabled: true ,
eventTypes: [ 'leftMouseDown' , 'mouseWheel' ]
} ,
mousewheel: {
enabled: true ,
modifiers: 'ctrl' ,
factor: 1.1 ,
maxScale: 1.5 ,
minScale: 0.5
} ,
highlighting: {
magnetAdsorbed: {
name: 'stroke' ,
args: {
attrs: {
fill: '#fff' ,
stroke: '#31d0c6' ,
strokeWidth: 4
}
}
}
} ,
connecting: {
snap: true ,
allowBlank: false ,
allowLoop: false ,
highlight: true ,
createEdge ( ) {
return graph. createEdge ( {
shape: 'processing-curve' ,
attrs: {
line: {
strokeDasharray: '5 5'
}
} ,
zIndex: - 1
} )
} ,
validateConnection ( { sourceMagnet, targetMagnet } ) {
if ( ! sourceMagnet || sourceMagnet. getAttribute ( 'port-group' ) === 'in' ) {
return false
}
if ( ! targetMagnet || targetMagnet. getAttribute ( 'port-group' ) === 'out' ) {
return false
}
return true
}
}
} )
graph. centerContent ( )
graph
. use (
new Transform ( {
resizing: true ,
rotating: true
} )
)
. use (
new Selection ( {
rubberband: true ,
showNodeSelectionBox: true
} )
)
. use (
new MiniMap ( {
container: miniMapContainer. value,
width: 200 ,
height: 260 ,
padding: 10
} )
)
. use ( new Snapline ( ) )
. use ( new Keyboard ( ) )
. use ( new Clipboard ( ) )
. use ( new History ( ) )
. use ( new Export ( ) )
const ports = {
groups: {
in : {
position: 'top' ,
attrs: {
circle: {
r: 4 ,
magnet: true ,
stroke: '#5F95FF' ,
strokeWidth: 1 ,
fill: '#fff' ,
style: {
visibility: 'hidden'
}
}
}
} ,
out: {
position: 'bottom' ,
attrs: {
circle: {
r: 4 ,
magnet: true ,
stroke: '#31d0c6' ,
strokeWidth: 1 ,
fill: '#fff' ,
style: {
visibility: 'hidden'
}
}
}
} ,
left: {
position: 'left' ,
attrs: {
circle: {
r: 4 ,
magnet: true ,
stroke: '#5F95FF' ,
strokeWidth: 1 ,
fill: '#fff' ,
style: {
visibility: 'hidden'
}
}
}
} ,
right: {
position: 'right' ,
attrs: {
circle: {
r: 4 ,
magnet: true ,
stroke: '#5F95FF' ,
strokeWidth: 1 ,
fill: '#fff' ,
style: {
visibility: 'hidden'
}
}
}
}
}
}
Graph. registerNode (
'custom-node' ,
{
inherit: 'rect' ,
width: 140 ,
height: 76 ,
attrs: {
body: {
strokeWidth: 1
} ,
image: {
width: 16 ,
height: 16 ,
x: 12 ,
y: 6
} ,
text: {
refX: 40 ,
refY: 15 ,
fontSize: 15 ,
'text-anchor' : 'start'
} ,
label: {
text: 'Please nominate this node' ,
refX: 10 ,
refY: 30 ,
fontSize: 12 ,
fill: 'rgba(0,0,0,0.6)' ,
'text-anchor' : 'start' ,
textWrap: {
width: - 10 ,
height: '70%' ,
ellipsis: true ,
breakWord: true
}
}
} ,
markup: [
{
tagName: 'rect' ,
selector: 'body'
} ,
{
tagName: 'image' ,
selector: 'image'
} ,
{
tagName: 'text' ,
selector: 'text'
} ,
{
tagName: 'text' ,
selector: 'label'
}
] ,
data: { } ,
relation: { } ,
ports: { ... ports }
} ,
true
)
const stencil = new Stencil ( {
title: '数据集成' ,
target: graph,
search: false ,
collapsable: true ,
stencilGraphWidth: 300 ,
stencilGraphHeight: 600 ,
groups: [
{
name: 'processLibrary' ,
title: 'dataSource'
}
] ,
layoutOptions: {
dx: 30 ,
dy: 20 ,
columns: 1 ,
columnWidth: 130 ,
rowHeight: 100
}
} )
stencilContainer. value. appendChild ( stencil. container)
const showPorts = ( ports: NodeListOf< SVGElement> , show: boolean ) => {
for ( let i = 0 , len = ports. length; i < len; i += 1 ) {
ports[ i] . style. visibility = show ? 'visible' : 'hidden'
}
}
graph. on ( 'node:mouseenter' , ( ) => {
const container = graphContainer. value
const ports = container. querySelectorAll ( '.x6-port-body' )
showPorts ( ports, true )
} )
graph. on ( 'node:mouseleave' , ( ) => {
const container = graphContainer. value
const ports = container. querySelectorAll (
'.x6-port-body'
) as NodeListOf< SVGElement>
showPorts ( ports, false )
} )
graph. bindKey ( [ 'meta+c' , 'ctrl+c' ] , ( ) => {
copy ( )
} )
graph. bindKey ( [ 'meta+x' , 'ctrl+x' ] , ( ) => {
const cells = graph. getSelectedCells ( )
if ( cells. length) {
graph. cut ( cells)
}
return false
} )
graph. bindKey ( [ 'meta+v' , 'ctrl+v' ] , ( ) => {
paste ( )
} )
graph. bindKey ( [ 'meta+z' , 'ctrl+z' ] , ( ) => {
undo ( )
} )
graph. bindKey ( [ 'meta+y' , 'ctrl+y' ] , ( ) => {
redo ( )
} )
graph. bindKey ( [ 'meta+a' , 'ctrl+a' ] , ( ) => {
const nodes = graph. getNodes ( )
if ( nodes) {
graph. select ( nodes)
}
} )
graph. bindKey ( 'backspace' , ( ) => {
del ( )
} )
graph. bindKey ( [ 'ctrl+1' , 'meta+1' ] , ( ) => {
const zoom = graph. zoom ( )
if ( zoom < 1.5 ) {
graph. zoom ( 0.1 )
}
} )
graph. bindKey ( [ 'ctrl+2' , 'meta+2' ] , ( ) => {
const zoom = graph. zoom ( )
if ( zoom > 0.5 ) {
graph. zoom ( - 0.1 )
}
} )
graph. on ( 'node:added' , ( { node } : any ) => {
addNodeInfo ( node)
} )
graph. on ( 'node:click' , ( { node } : any ) => {
addNodeInfo ( node)
} )
graph. on ( 'node:selected' , ( args: { cell: Cell; node: Node; options: Model. SetOptions } ) => {
if ( NodeType. INPUT != args. node. data. type) {
args. node. removeTools ( )
args. node. addTools ( {
name: 'button' ,
args: {
x: 0 ,
y: 0 ,
offset: { x: 160 , y: 40 } ,
markup: [
{
tagName: 'circle' ,
selector: 'button' ,
attrs: {
r: 8 ,
stroke: 'rgba(0,0,0,.25)' ,
strokeWidth: 1 ,
fill: 'rgba(255, 255, 255, 1)' ,
cursor: 'pointer'
}
} ,
{
tagName: 'text' ,
textContent: '+' ,
selector: 'icon' ,
attrs: {
fill: 'rgba(0,0,0,.25)' ,
fontSize: 15 ,
textAnchor: 'middle' ,
pointerEvents: 'none' ,
y: '0.3em' ,
stroke: 'rgba(0,0,0,.25)'
}
}
] ,
onClick ( { e, view } : any ) {
showNodeTool ( e, view)
}
}
} )
}
} )
graph. on ( 'node:unselected' , ( args: { cell: Cell; node: Node; options: Model. SetOptions } ) => {
args. node. removeTools ( )
} )
graph. on ( 'edge:added' , ( { edge } : any ) => {
addEdgeInfo ( edge)
edge. data = {
source: edge. source. cell,
target: edge. target. cell
}
} )
graph. on ( 'edge:click' , ( { edge } : any ) => {
addEdgeInfo ( edge)
} )
graph. on ( 'edge:selected' , ( args: { cell: Cell; edge: Edge; options: Model. SetOptions } ) => {
args. edge. attr ( 'line/strokeWidth' , 3 )
} )
graph. on ( 'edge:unselected' , ( args: { cell: Cell; edge: Edge; options: Model. SetOptions } ) => {
args. edge. attr ( 'line/strokeWidth' , 1 )
} )
const nodeShapes = [
{
label: '开始' ,
nodeType: 'OUTPUT' as NodeType
} ,
{
label: '数据同步' ,
nodeType: 'SYNC' as NodeType
} ,
{
label: '结束' ,
nodeType: 'INPUT' as NodeType
}
]
const nodes = nodeShapes. map ( ( item) => {
const id = StringExt. uuid ( )
const node = {
id: id,
shape: 'custom-node' ,
ports: getPortsByType ( item. nodeType, id) ,
data: {
name: ` ${ item. label} ` ,
type: item. nodeType
} ,
attrs: getNodeAttrs ( item. nodeType)
}
const newNode = graph. addNode ( node)
return newNode
} )
stencil. load ( nodes, 'processLibrary' )
}
const getPortsByType = ( type: NodeType, nodeId: string ) => {
let ports = [ ] as any
switch ( type) {
case NodeType. INPUT :
ports = [
{
id: ` ${ nodeId} -in ` ,
group: 'in'
} ,
{
id: ` ${ nodeId} -left ` ,
group: 'left'
} ,
{
id: ` ${ nodeId} -right ` ,
group: 'right'
}
]
break
case NodeType. OUTPUT :
ports = [
{
id: ` ${ nodeId} -out ` ,
group: 'out'
} ,
{
id: ` ${ nodeId} -left ` ,
group: 'left'
} ,
{
id: ` ${ nodeId} -right ` ,
group: 'right'
}
]
break
default :
ports = [
{
id: ` ${ nodeId} -in ` ,
group: 'in'
} ,
{
id: ` ${ nodeId} -out ` ,
group: 'out'
} ,
{
id: ` ${ nodeId} -left ` ,
group: 'left'
} ,
{
id: ` ${ nodeId} -right ` ,
group: 'right'
}
]
break
}
return ports
}
Graph. registerConnector (
'curveConnectorTB' ,
( s, e) => {
const offset = 4
const deltaY = Math. abs ( e. y - s. y)
const control = Math. floor ( ( deltaY / 3 ) * 2 )
const v1 = { x: s. x, y: s. y + offset + control }
const v2 = { x: e. x, y: e. y - offset - control }
return Path. normalize (
` M ${ s. x} ${ s. y}
L ${ s. x} ${ s. y + offset}
C ${ v1. x} ${ v1. y} ${ v2. x} ${ v2. y} ${ e. x} ${ e. y - offset}
L ${ e. x} ${ e. y}
`
)
} ,
true
)
Graph. registerConnector (
'curveConnectorLR' ,
( sourcePoint, targetPoint) => {
const hgap = Math. abs ( targetPoint. x - sourcePoint. x)
const path = new Path ( )
path. appendSegment ( Path. createSegment ( 'M' , sourcePoint. x - 4 , sourcePoint. y) )
path. appendSegment ( Path. createSegment ( 'L' , sourcePoint. x + 12 , sourcePoint. y) )
path. appendSegment (
Path. createSegment (
'C' ,
sourcePoint. x < targetPoint. x ? sourcePoint. x + hgap / 2 : sourcePoint. x - hgap / 2 ,
sourcePoint. y,
sourcePoint. x < targetPoint. x ? targetPoint. x - hgap / 2 : targetPoint. x + hgap / 2 ,
targetPoint. y,
targetPoint. x - 6 ,
targetPoint. y
)
)
path. appendSegment ( Path. createSegment ( 'L' , targetPoint. x + 2 , targetPoint. y) )
return path. serialize ( )
} ,
true
)
Graph. registerEdge (
'processing-curve' ,
{
inherit: 'edge' ,
markup: [
{
tagName: 'path' ,
selector: 'wrap' ,
attrs: {
fill: 'none' ,
cursor: 'pointer' ,
stroke: 'transparent' ,
strokeLinecap: 'round'
}
} ,
{
tagName: 'path' ,
selector: 'line' ,
attrs: {
fill: 'none' ,
pointerEvents: 'none'
}
}
] ,
connector: { name: 'smooth' } ,
attrs: {
wrap: {
connection: true ,
strokeWidth: 10 ,
strokeLinejoin: 'round'
} ,
line: {
connection: true ,
stroke: '#A2B1C3' ,
strokeWidth: 1 ,
targetMarker: {
name: 'classic' ,
size: 6
}
}
}
} ,
true
)
function save ( ) {
console . log ( 'save' )
const graphData = graph. toJSON ( )
console . log ( graphData)
}
function undo ( ) {
if ( graph. canUndo ( ) ) {
graph. undo ( )
}
return false
}
function redo ( ) {
if ( graph. canRedo ( ) ) {
graph. redo ( )
}
return false
}
function copy ( ) {
const cells = graph. getSelectedCells ( )
if ( cells. length) {
graph. copy ( cells)
}
return false
}
function paste ( ) {
if ( ! graph. isClipboardEmpty ( ) ) {
const cells = graph. paste ( { offset: 32 } )
graph. cleanSelection ( )
graph. select ( cells)
}
return false
}
function del ( ) {
const cells = graph. getSelectedCells ( )
if ( cells. length) {
graph. removeCells ( cells)
}
}
function exportPng ( ) {
graph. toPNG (
( dataUri: string ) => {
DataUri. downloadDataUri ( dataUri, 'chart.png' )
} ,
{
padding: {
top: 20 ,
right: 20 ,
bottom: 20 ,
left: 20
}
}
)
}
function addNodeInfo ( node: any ) {
state. nodeFrom. title = 'Node'
state. nodeFrom. label = node. label
state. nodeFrom. desc = node. attrs. label. text
state. nodeFrom. show = true
state. nodeFrom. id = node. id
state. cellFrom. show = false
}
function addEdgeInfo ( edge: any ) {
state. nodeFrom. show = false
state. cellFrom. title = 'Edge'
if ( edge. labels[ 0 ] ) {
state. cellFrom. label = edge. labels[ 0 ] . attrs. label. text
} else {
state. cellFrom. label = ''
}
state. cellFrom. edgeType = edge. data ? edge. data. edgeType : ''
state. cellFrom. show = true
state. cellFrom. id = edge. id
}
function changeLabel ( ) {
const nodes = graph. getNodes ( )
nodes. forEach ( ( node: any ) => {
if ( state. nodeFrom. id == node. id) {
node. label = state. nodeFrom. label
}
} )
}
function changeDesc ( ) {
const nodes = graph. getNodes ( )
nodes. forEach ( ( node: any ) => {
if ( state. nodeFrom. id == node. id) {
node. attr ( 'label/text' , state. nodeFrom. desc)
}
} )
}
function changeEdgeLabel ( ) {
const edges = graph. getEdges ( )
edges. forEach ( ( edge: any ) => {
if ( state. cellFrom. id == edge. id) {
edge. setLabels ( state. cellFrom. label)
console . log ( edge)
}
} )
}
const getNodeAttrs = ( nodeType: string ) => {
let attr = { } as any
switch ( nodeType) {
case NodeType. INPUT :
attr = {
image: {
'xlink:href' : new URL ( '@/assets/imgs/lime.png' , import . meta. url) . href
} ,
body: {
fill: '#b9dec9' ,
stroke: '#229453'
} ,
text: {
text: '结束' ,
fill: '#229453'
}
}
break
case NodeType. SYNC :
attr = {
image: {
'xlink:href' : new URL ( '@/assets/imgs/persimmon.png' , import . meta. url) . href
} ,
body: {
fill: '#edc3ae' ,
stroke: '#f9723d'
} ,
text: {
text: '数据同步' ,
fill: '#f9723d'
}
}
break
case NodeType. OUTPUT :
attr = {
image: {
'xlink:href' : new URL ( '@/assets/imgs/rice.png' , import . meta. url) . href
} ,
body: {
fill: '#EFF4FF' ,
stroke: '#5F95FF'
} ,
text: {
text: '开始' ,
fill: '#5F95FF'
}
}
break
}
return attr
}
function getData ( ) {
let cells = [ ] as any
const location = state. data
location. nodes. map ( ( node) => {
let attr = getNodeAttrs ( node. data. type)
if ( node. data. desc) {
attr. label = { text: node. data. desc }
}
if ( node. data. name) {
let temp = attr. text
if ( temp) {
temp. text = node. data. name
}
}
cells. push (
graph. addNode ( {
id: node. id,
x: node. position. x,
y: node. position. y,
shape: 'custom-node' ,
attrs: attr,
ports: getPortsByType ( node. data. type as NodeType, node. id) ,
data: node. data
} )
)
} )
location. edges. map ( ( edge) => {
cells. push (
graph. addEdge ( {
id: edge. id,
source: edge. source,
target: edge. target,
zIndex: edge. zIndex,
shape: 'processing-curve' ,
labels: edge. labels,
attrs: { line: { strokeDasharray: '5 5' } } ,
data: edge. data
} )
)
} )
graph. resetCells ( cells)
}
const excuteAnimate = ( edge: any ) => {
edge. attr ( {
line: {
stroke: '#3471F9'
}
} )
edge. attr ( 'line/strokeDasharray' , 5 )
edge. attr ( 'line/style/animation' , 'running-line 30s infinite linear' )
}
const showEdgeStatus = ( ) => {
state. edgeStatusList. forEach ( ( item) => {
const edge = graph. getCellById ( item. id)
if ( item. status == 'success' ) {
edge. attr ( 'line/strokeDasharray' , 0 )
edge. attr ( 'line/stroke' , '#52c41a' )
} else if ( 'error' == item. status) {
edge. attr ( 'line/stroke' , '#ff4d4f' )
} else if ( 'executing' == item. status) {
excuteAnimate ( edge)
}
} )
}
function showNodeTool ( e: any , _view: any ) {
state. showMenu = true
nextTick ( ( ) => {
nodeMenu. value. style. top = e. offsetY + 60 + 'px'
nodeMenu. value. style. left = e. offsetX + 210 + 'px'
} )
}
function addNodeTool ( item: any ) {
createDownstream ( item. type)
state. showMenu = false
}
const getDownstreamNodePosition = ( node: Node, graph: Graph, dx = 250 , dy = 100 ) => {
const downstreamNodeIdList: string [ ] = [ ]
graph. getEdges ( ) . forEach ( ( edge) => {
const originEdge = edge. toJSON ( ) ?. data
console . log ( node)
if ( originEdge. source === node. id) {
downstreamNodeIdList. push ( originEdge. target)
}
} )
const position = node. getPosition ( )
let minX = Infinity
let maxY = - Infinity
graph. getNodes ( ) . forEach ( ( graphNode) => {
if ( downstreamNodeIdList. indexOf ( graphNode. id) > - 1 ) {
const nodePosition = graphNode. getPosition ( )
if ( nodePosition. x < minX) {
minX = nodePosition. x
}
if ( nodePosition. y > maxY) {
maxY = nodePosition. y
}
}
} )
return {
x: minX !== Infinity ? minX : position. x + dx,
y: maxY !== - Infinity ? maxY + dy : position. y
}
}
const createDownstream = ( type: NodeType) => {
const cells = graph. getSelectedCells ( )
if ( cells. length == 1 ) {
const node = cells[ 0 ]
if ( graph) {
const position = getDownstreamNodePosition ( node, graph)
const newNode = createNode ( type, graph, position)
const source = node. id
const target = newNode. id
createEdge ( source, target, graph)
}
} else {
ElMessage ( {
message: '请选择一个节点' ,
type: 'warning'
} )
}
}
const createNode = ( type: NodeType, graph: Graph, position? : Position) : Node => {
let newNode = { } as Node
const typeName = state. PROCESSING_TYPE_LIST ?. find ( ( item) => item. type === type) ?. name
const id = StringExt. uuid ( )
const node = {
id,
shape: 'custom-node' ,
x: position?. x,
y: position?. y,
ports: getPortsByType ( type, id) ,
data: {
name: ` ${ typeName} ` ,
type
} ,
attrs: getNodeAttrs ( type)
}
newNode = graph. addNode ( node)
return newNode
}
const createEdge = ( source: string , target: string , graph: Graph) => {
const edge = {
id: StringExt. uuid ( ) ,
shape: 'processing-curve' ,
source: {
cell: source
} ,
target: {
cell: target
} ,
zIndex: - 1 ,
data: {
source,
target
} ,
attrs: { line: { strokeDasharray: '5 5' } }
}
if ( graph) {
graph. addEdge ( edge)
}
}
onMounted ( ( ) => {
init ( )
getData ( )
showEdgeStatus ( )
} )
onUnmounted ( ( ) => {
graph. dispose ( )
} )
< / script>
< style lang= "less" scoped>
. content- main {
display: flex;
width: 100 % ;
flex- direction: column;
height: calc ( 100vh - 85px - 40px) ;
background- color: #ffffff;
position: relative;
. tool- container {
padding: 8px;
display: flex;
align- items: center;
color: rgba ( 0 , 0 , 0 , 0.45 ) ;
. command {
display: inline- block;
width: 27px;
height: 27px;
margin: 0 6px;
padding- top: 6px;
text- align: center;
cursor: pointer;
}
}
}
. content- container {
position: relative;
width: 100 % ;
height: 100 % ;
. content {
width: 100 % ;
height: 100 % ;
position: relative;
min- width: 400px;
min- height: 600px;
display: flex;
border: 1px solid #dfe3e8;
flex- direction: row;
flex: 1 1 ;
. stencil {
width: 250px;
height: 100 % ;
border- right: 1px solid #dfe3e8;
position: relative;
: deep ( . x6- widget- stencil) {
background- color: #fff;
}
: deep ( . x6- widget- stencil- title) {
background- color: #fff;
}
: deep ( . x6- widget- stencil- group- title) {
background- color: #fff ! important;
}
}
. graph- content {
width: calc ( 100 % - 180px) ;
height: 100 % ;
}
. editor- sidebar {
display: flex;
flex- direction: column;
border- left: 1px solid #e6f7ff;
background: #fafafa;
z- index: 9 ;
. el- card {
border: none;
}
. edit- panel {
flex: 1 1 ;
background- color: #fff;
}
: deep ( . x6- widget- minimap- viewport) {
border: 1px solid #8f8f8f;
}
: deep ( . x6- widget- minimap- viewport- zoom) {
border: 1px solid #8f8f8f;
}
}
}
}
: deep ( . x6- widget- transform) {
margin: - 1px 0 0 - 1px;
padding: 0px;
border: 1px solid #239edd;
}
: deep ( . x6- widget- transform > div) {
border: 1px solid #239edd;
}
: deep ( . x6- widget- transform > div: hover) {
background- color: #3dafe4;
}
: deep ( . x6- widget- transform- active- handle) {
background- color: #3dafe4;
}
: deep ( . x6- widget- transform- resize) {
border- radius: 0 ;
}
: deep ( . x6- widget- selection- inner) {
border: 1px solid #239edd;
}
: deep ( . x6- widget- selection- box) {
opacity: 0 ;
}
. topic- image {
visibility: hidden;
cursor: pointer;
}
. x6- node: hover . topic- image {
visibility: visible;
}
. x6- node- selected rect {
stroke- width: 2px;
}
. node- menu {
position: absolute;
box- shadow: var ( -- el- box- shadow- light) ;
background: var ( -- el- bg- color- overlay) ;
border: 1px solid var ( -- el- border- color- light) ;
padding: 5px 0px;
. menu- item {
display: flex;
align- items: center;
white- space: nowrap;
list- style: none;
line- height: 22px;
padding: 5px 16px;
margin: 0 ;
font- size: var ( -- el- font- size- base) ;
color: var ( -- el- text- color- regular) ;
cursor: pointer;
outline: none;
box- sizing: border- box;
}
. menu- item . el- image {
margin- right: 5px;
}
. menu- item: hover {
background- color: var ( -- el- color- primary- light- 9 ) ;
color: var ( -- el- color- primary) ;
}
}
< / style>
< template>
< div class = "content-main" >
< div class = "content-container" id= "" >
< div class = "content" >
< div class = "graph-content" id= "graphContainer" ref= "graphContainer" > < / div>
< / div>
< / div>
< / div>
< / template>
< script setup lang= "ts" >
import { Graph, Path, Edge } from '@antv/x6'
import { ref, onMounted, reactive } from 'vue'
import '@/styles/animation.less'
const graphContainer = ref ( )
let graph: any = null
const state = reactive ( {
data: {
nodes: [
{
id: 'ac51fb2f-2753-4852-8239-53672a29bb14' ,
x: - 340 ,
y: - 160 ,
ports: [
{
id: 'ac51fb2f-2753-4852-8239-53672a29bb14_out' ,
group: 'out'
}
] ,
data: {
name: '数据输入_1' ,
type: 'OUTPUT' ,
checkStatus: 'sucess'
} ,
attrs: {
body: {
fill: '#EFF4FF' ,
stroke: '#5F95FF'
} ,
image: {
'xlink:href' : 'http://localhost:20002/src/assets/imgs/rice.png'
} ,
label: {
text: '春望'
} ,
text: {
fill: '#5F95FF' ,
text: '开始'
}
}
} ,
{
id: '81004c2f-0413-4cc6-8622-127004b3befa' ,
x: - 340 ,
y: - 10 ,
ports: [
{
id: '81004c2f-0413-4cc6-8622-127004b3befa_in' ,
group: 'in'
} ,
{
id: '81004c2f-0413-4cc6-8622-127004b3befa_out' ,
group: 'out'
}
] ,
data: {
name: '数据输入_1' ,
type: 'SYAN' ,
checkStatus: 'sucess'
} ,
attrs: {
body: {
fill: '#edc3ae' ,
stroke: '#f9723d'
} ,
image: {
'xlink:href' : 'http://localhost:20002/src/assets/imgs/persimmon.png'
} ,
label: {
text: '国破山河在'
} ,
text: {
fill: '#f9723d' ,
text: '数据同步'
}
}
} ,
{
id: '7505da25-1308-4d7a-98fd-e6d5c917d35d' ,
x: - 140 ,
y: 180 ,
ports: [
{
id: '7505da25-1308-4d7a-98fd-e6d5c917d35d_in' ,
group: 'in'
}
] ,
data: {
name: '数据输入_1' ,
type: 'INPUT' ,
checkStatus: 'sucess'
} ,
attrs: {
body: {
fill: '#b9dec9' ,
stroke: '#229453'
} ,
image: {
'xlink:href' : 'http://localhost:20002/src/assets/imgs/lime.png'
} ,
label: {
text: '城春草木胜'
} ,
text: {
fill: '#229453' ,
text: '结束'
}
}
}
] ,
edges: [
{
attrs: { line: { strokeDasharray: '5 5' } } ,
connector: { name: 'curveConnector' } ,
id: '6eea5dc9-4e15-4e78-959f-ee13ec59d11c' ,
shape: 'data-processing-curve' ,
source: { cell: 'ac51fb2f-2753-4852-8239-53672a29bb14' , port: '_out' } ,
target: { cell: '81004c2f-0413-4cc6-8622-127004b3befa' , port: '_in' } ,
zIndex: - 1
} ,
{
attrs: { line: { strokeDasharray: '5 5' } } ,
connector: { name: 'curveConnector' } ,
id: '8cbce713-54be-4c07-8efa-59c505f74ad7' ,
labels: [ '下半句' ] ,
shape: 'data-processing-curve' ,
source: { cell: '81004c2f-0413-4cc6-8622-127004b3befa' , port: '_out' } ,
target: { cell: '7505da25-1308-4d7a-98fd-e6d5c917d35d' , port: '_in' }
}
]
} ,
nodeStatusList: [
{
id: 'ac51fb2f-2753-4852-8239-53672a29bb14' ,
status: 'success'
} ,
{
id: '81004c2f-0413-4cc6-8622-127004b3befa' ,
status: 'success'
}
] ,
edgeStatusList: [
{
id: '6eea5dc9-4e15-4e78-959f-ee13ec59d11c' ,
status: 'success'
} ,
{
id: '8cbce713-54be-4c07-8efa-59c505f74ad7' ,
status: 'executing'
}
]
} )
function init ( ) {
graph = new Graph ( {
container: graphContainer. value,
interacting : function ( ) {
return { nodeMovable: false }
} ,
grid: true ,
panning: {
enabled: false ,
eventTypes: [ 'leftMouseDown' , 'mouseWheel' ]
} ,
mousewheel: {
enabled: true ,
modifiers: 'ctrl' ,
factor: 1.1 ,
maxScale: 1.5 ,
minScale: 0.5
} ,
highlighting: {
magnetAdsorbed: {
name: 'stroke' ,
args: {
attrs: {
fill: '#fff' ,
stroke: '#31d0c6' ,
strokeWidth: 4
}
}
}
} ,
connecting: {
snap: true ,
allowBlank: false ,
allowLoop: false ,
highlight: true ,
sourceAnchor: {
name: 'bottom' ,
args: {
dx: 0
}
} ,
targetAnchor: {
name: 'top' ,
args: {
dx: 0
}
} ,
createEdge ( ) {
return graph. createEdge ( {
shape: 'data-processing-curve' ,
attrs: {
line: {
strokeDasharray: '5 5'
}
} ,
zIndex: - 1
} )
} ,
validateConnection ( { sourceMagnet, targetMagnet } ) {
if ( ! sourceMagnet || sourceMagnet. getAttribute ( 'port-group' ) === 'in' ) {
return false
}
if ( ! targetMagnet || targetMagnet. getAttribute ( 'port-group' ) === 'out' ) {
return false
}
return true
}
}
} )
graph. centerContent ( )
const ports = {
groups: {
in : {
position: 'top' ,
attrs: {
circle: {
r: 4 ,
magnet: true ,
stroke: '#5F95FF' ,
strokeWidth: 1 ,
fill: '#fff' ,
style: {
visibility: 'hidden'
}
}
}
} ,
out: {
position: 'bottom' ,
attrs: {
circle: {
r: 4 ,
magnet: true ,
stroke: '#31d0c6' ,
strokeWidth: 1 ,
fill: '#fff' ,
style: {
visibility: 'hidden'
}
}
}
} ,
left: {
position: 'left' ,
attrs: {
circle: {
r: 4 ,
magnet: true ,
stroke: '#5F95FF' ,
strokeWidth: 1 ,
fill: '#fff' ,
style: {
visibility: 'hidden'
}
}
}
} ,
right: {
position: 'right' ,
attrs: {
circle: {
r: 4 ,
magnet: true ,
stroke: '#5F95FF' ,
strokeWidth: 1 ,
fill: '#fff' ,
style: {
visibility: 'hidden'
}
}
}
}
}
}
Graph. registerNode (
'custom-node' ,
{
inherit: 'rect' ,
width: 140 ,
height: 76 ,
attrs: {
body: {
strokeWidth: 1
} ,
image: {
width: 16 ,
height: 16 ,
x: 12 ,
y: 6
} ,
text: {
refX: 40 ,
refY: 15 ,
fontSize: 15 ,
'text-anchor' : 'start'
} ,
label: {
text: 'Please nominate this node' ,
refX: 10 ,
refY: 30 ,
fontSize: 12 ,
fill: 'rgba(0,0,0,0.6)' ,
'text-anchor' : 'start' ,
textWrap: {
width: - 10 ,
height: '70%' ,
ellipsis: true ,
breakWord: true
}
}
} ,
markup: [
{
tagName: 'rect' ,
selector: 'body'
} ,
{
tagName: 'image' ,
selector: 'image'
} ,
{
tagName: 'text' ,
selector: 'text'
} ,
{
tagName: 'text' ,
selector: 'label'
}
] ,
data: { } ,
relation: { } ,
ports: { ... ports }
} ,
true
)
Graph. registerConnector (
'curveConnector' ,
( s, e) => {
const offset = 4
const deltaY = Math. abs ( e. y - s. y)
const control = Math. floor ( ( deltaY / 3 ) * 2 )
const v1 = { x: s. x, y: s. y + offset + control }
const v2 = { x: e. x, y: e. y - offset - control }
return Path. normalize (
` M ${ s. x} ${ s. y}
L ${ s. x} ${ s. y + offset}
C ${ v1. x} ${ v1. y} ${ v2. x} ${ v2. y} ${ e. x} ${ e. y - offset}
L ${ e. x} ${ e. y}
`
)
} ,
true
)
}
Edge. config ( {
markup: [
{
tagName: 'path' ,
selector: 'wrap' ,
attrs: {
fill: 'none' ,
cursor: 'pointer' ,
stroke: 'transparent' ,
strokeLinecap: 'round'
}
} ,
{
tagName: 'path' ,
selector: 'line' ,
attrs: {
fill: 'none' ,
pointerEvents: 'none'
}
}
] ,
connector: { name: 'curveConnector' } ,
attrs: {
wrap: {
connection: true ,
strokeWidth: 10 ,
strokeLinejoin: 'round'
} ,
line: {
connection: true ,
stroke: '#A2B1C3' ,
strokeWidth: 1 ,
targetMarker: {
name: 'classic' ,
size: 6
}
}
}
} )
Graph. registerEdge ( 'data-processing-curve' , Edge, true )
function getData ( ) {
let cells = [ ] as any
const location = state. data
location. nodes. map ( ( node) => {
cells. push (
graph. addNode ( {
id: node. id,
x: node. x,
y: node. y,
shape: 'custom-node' ,
attrs: node. attrs,
ports: node. ports,
data: node. data
} )
)
} )
location. edges. map ( ( edge) => {
cells. push (
graph. addEdge ( {
id: edge. id,
source: edge. source,
target: edge. target,
zIndex: edge. zIndex,
shape: 'data-processing-curve' ,
connector: { name: 'curveConnector' } ,
labels: edge. labels,
attrs: edge. attrs
} )
)
} )
graph. resetCells ( cells)
}
const excuteAnimate = ( edge: any ) => {
edge. attr ( {
line: {
stroke: '#3471F9'
}
} )
edge. attr ( 'line/strokeDasharray' , 5 )
edge. attr ( 'line/style/animation' , 'running-line 30s infinite linear' )
}
const showEdgeStatus = ( ) => {
state. edgeStatusList. forEach ( ( item) => {
const edge = graph. getCellById ( item. id)
if ( item. status == 'success' ) {
edge. attr ( 'line/strokeDasharray' , 0 )
edge. attr ( 'line/stroke' , '#52c41a' )
} else if ( 'error' == item. status) {
edge. attr ( 'line/stroke' , '#ff4d4f' )
} else if ( 'executing' == item. status) {
excuteAnimate ( edge)
}
} )
}
onMounted ( ( ) => {
init ( )
getData ( )
showEdgeStatus ( )
} )
< / script>
< style lang= "less" scoped>
. content- main {
display: flex;
width: 100 % ;
flex- direction: column;
height: calc ( 100vh - 85px - 40px) ;
background- color: #ffffff;
position: relative;
}
. content- container {
position: relative;
width: 100 % ;
height: 100 % ;
. content {
width: 100 % ;
height: 100 % ;
position: relative;
min- width: 400px;
min- height: 600px;
display: flex;
border: 1px solid #dfe3e8;
flex- direction: row;
flex: 1 1 ;
. graph- content {
width: calc ( 100 % ) ;
height: 100 % ;
}
}
}
: deep ( . x6- widget- transform) {
margin: - 1px 0 0 - 1px;
padding: 0px;
border: 1px solid #239edd;
}
: deep ( . x6- widget- transform > div) {
border: 1px solid #239edd;
}
: deep ( . x6- widget- transform > div: hover) {
background- color: #3dafe4;
}
: deep ( . x6- widget- transform- active- handle) {
background- color: #3dafe4;
}
: deep ( . x6- widget- transform- resize) {
border- radius: 0 ;
}
: deep ( . x6- widget- selection- inner) {
border: 1px solid #239edd;
}
: deep ( . x6- widget- selection- box) {
opacity: 0 ;
}
. topic- image {
visibility: hidden;
cursor: pointer;
}
. x6- node: hover . topic- image {
visibility: visible;
}
. x6- node- selected rect {
stroke- width: 2px;
}
< / style>