新建流程图
< 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- optionv- 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" > < divclass = "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. valueconst ports = container. querySelectorAll ( '.x6-port-body' ) showPorts ( ports, true ) } ) graph. on ( 'node:mouseleave' , ( ) => { const container = graphContainer. valueconst 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. labelstate. nodeFrom. desc = node. attrs. label. textstate. nodeFrom. show = true state. nodeFrom. id = node. idstate. 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. datalocation. 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. textif ( 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 ( ) ?. dataconsole . 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. idconst target = newNode. idcreateEdge ( source, target, graph) } } else { ElMessage ( { message: '请选择一个节点' , type: 'warning' } ) }
} const createNode = ( type: NodeType, graph: Graph, position? : Position) : Node => { let newNode = { } as Nodeconst typeName = state. PROCESSING_TYPE_LIST ?. find ( ( item) => item. type === type) ?. nameconst 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. datalocation. 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>