组件:
基于vue@3.5.18开发,封装成workflow-view组件
使用@antv/x6-vue-shape插件,将vue组件作为节点渲染
横向布局转为纵向布局(主要是连节点位置调整)
使用@antv/x6-plugin-dnd插件,实现拖拽插入节点,移除了下拉选择添加下个节点
调整节点状态样式,节点布局调整,
v-tooltip指令
点击节点中'配置'图标触发自定义事件node:config事件,用于节点属性配置
组件属性:interacting:是否可交互;initData:场景初始数据
workflow-view/index.vue
vue
<template>
<div ref="container" style="width: 100%; height: 100%; overflow: hidden" />
</template>
<script lang="ts" setup>
import { ref, useTemplateRef, onMounted, watchEffect, defineExpose, nextTick } from 'vue'
import { Graph, Node, Path, Edge, Platform, StringExt } from '@antv/x6'
import { Selection } from '@antv/x6-plugin-selection'
import { register } from '@antv/x6-vue-shape'
import DataProcessingDagNode from './components/DataProcessingDagNode.vue'
import { Dnd } from '@antv/x6-plugin-dnd'
let graph: Graph
let dnd: any
const { interacting = true, initData } = defineProps<{
interacting?: boolean
initData?: Record<string, any>[]
}>()
const emits = defineEmits(['node:config'])
const container = useTemplateRef('container')
watchEffect(
async () => {
if (initData.length) {
await nextTick()
setTimeout(() => {
renderScene(initData)
}, 200)
}
},
{ flush: 'post' },
)
register({
shape: 'data-processing-dag-node',
width: 180,
height: 48,
component: DataProcessingDagNode,
// port默认不可见
ports: {
groups: {
in: {
position: {
name: 'top',
args: {
// dx: -14,
},
},
attrs: {
circle: {
r: 4,
magnet: true,
stroke: 'transparent',
strokeWidth: 1,
fill: 'transparent',
},
},
},
out: {
position: {
name: 'bottom',
args: {
// dx: -14,
},
},
attrs: {
circle: {
r: 4,
magnet: true,
stroke: 'transparent',
strokeWidth: 1,
fill: 'transparent',
},
},
},
},
},
})
// 注册连接器
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)
onMounted(() => {
init()
})
function init() {
graph = new Graph({
container: container.value!,
interacting: interacting, // 禁止元素拖拽
autoResize: 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,
sourceAnchor: {
name: 'top',
args: {
dy: Platform.IS_SAFARI ? 4 : 8,
},
},
targetAnchor: {
name: 'bottom',
args: {
dy: Platform.IS_SAFARI ? 4 : -8,
},
},
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') !== 'in') {
return false
}
return true
},
},
})
if (interacting) {
dnd = new Dnd({
target: graph,
})
graph.use(
new Selection({
multiple: true,
rubberEdge: true,
rubberNode: true,
modifiers: 'shift',
rubberband: true,
}),
)
graph.on('node:mouseenter', ({ node }) => {
node.addTools({
name: 'button-remove',
args: {
x: '100%',
y: 0,
offset: { x: -10, y: 10 },
},
})
})
graph.on('node:mouseleave', ({ node }) => {
node.removeTools()
})
graph.on('edge:mouseenter', ({ cell }) => {
cell.addTools([
{ name: 'vertices' },
{
name: 'button-remove',
args: { distance: '50%' },
},
])
})
// 节点单击事件
graph.on('node:config', async (val) => {
emits('node:config', val)
// RightPanelRef.value.show(val)
})
graph.on('edge:mouseleave', ({ cell }) => {
if (cell.hasTool('button-remove')) {
cell.removeTool('button-remove')
}
})
}
}
function renderScene(json) {
graph?.fromJSON(json)
// 居中显示
graph.zoomToFit({
maxScale: 1,
padding: {
left: 10,
right: 10,
top: 30,
bottom: 30,
},
})
}
function startDrag(e: any, item) {
if (!interacting) return
// 该 node 为拖拽的节点,默认也是放置到画布上的节点,可以自定义任何属性
const id = Date.now() + Math.random().toString(36).slice(-5)
const node = graph.createNode({
// custom_id: id, // 没用
shape: 'data-processing-dag-node',
ports: getPortsByType(item.componentType, id),
data: {
componentDefId: item.componentDefId,
name: item.name,
nodeName: item.name,
componentType: item.componentType,
},
})
dnd?.start(node, e)
}
enum NodeType {
RECEIVE = 'RECEIVE', // 数据输入
PROCESS = 'PROCESS', // 数据过滤
// JOIN = 'JOIN', // 数据连接
// UNION = 'UNION', // 数据合并
// AGG = 'AGG', // 数据聚合
SEND = 'SEND', // 数据输出
}
function getPortsByType(type: NodeType, nodeId: string) {
let ports = []
switch (type) {
case NodeType.RECEIVE:
ports = [
{
id: `${nodeId}-out`,
group: 'out',
},
]
break
case NodeType.SEND:
ports = [
{
id: `${nodeId}-in`,
group: 'in',
},
]
break
default:
ports = [
{
id: `${nodeId}-in`,
group: 'in',
},
{
id: `${nodeId}-out`,
group: 'out',
},
]
break
}
return ports
}
function getData() {
return graph.toJSON().cells
}
// '清除'按钮
function clear() {
graph.fromJSON([])
graph.zoomTo(1)
}
// 显示节点状态
function showNodeStatus(
edgeStatusList: {
id: string
status: 'success' | 'error'
statusMsg?: string
}[],
) {
nodeStatusList.forEach((item) => {
const { id, status, statusMsg } = item
const node = graph.getCellById(id)
const data = node.getData()
node.setData({
...data,
status,
statusMsg,
})
})
}
// 开启边的运行动画
function excuteAnimate() {
graph.getEdges().forEach((edge) => {
edge.attr({
line: {
stroke: '#3471F9',
},
})
edge.attr('line/strokeDasharray', 5)
edge.attr('line/style/animation', 'running-line 30s infinite linear')
})
}
// 关闭边的动画
function stopAnimate(
edgeStatusList: {
id: string
status: 'success' | 'error'
statusMsg?: string
}[],
) {
graph.getEdges().forEach((edge) => {
edge.attr('line/strokeDasharray', 0)
edge.attr('line/style/animation', '')
})
edgeStatusList.forEach((item) => {
const { id, status } = item
const edge = graph.getCellById(id)
if (status === 'success') {
edge.attr('line/stroke', '#52c41a')
}
if (status === 'error') {
edge.attr('line/stroke', '#ff4d4f')
}
})
}
defineExpose({
renderScene,
showNodeStatus,
excuteAnimate,
stopAnimate,
startDrag,
getData,
clear,
})
</script>
workflow-view/components/DataProcessingDagNode.vue
vue
<template>
<div style="width: 100%; height: 100%">
<div class="data-processing-dag-node">
<div
class="main-area"
:class="{
success: nodeData?.status === CellStatus.SUCCESS,
error: nodeData?.status === CellStatus.ERROR,
}"
@mouseenter="onMainMouseEnter"
>
<div class="flex-vcenter" style="gap: 6px">
<!-- 节点类型icon -->
<i
class="node-logo"
:style="{ backgroundImage: `url(${NODE_TYPE_LOGO?.[nodeData?.componentType] as any})` }"
/>
<div class="flex-1 text-ellipsis" v-tooltip:top="nodeData?.nodeName">
{{ nodeData?.nodeName }}
</div>
<!-- 字体图标自行替换 -->
<i
class="more-action iconfont icon-peizhi pointer"
v-tooltip="'配置'"
@click="handleEdit()"
/>
</div>
</div>
<!-- 添加下游节点 -->
<!-- <div v-if="nodeData?.componentType !== NodeType.SEND">
<el-dropdown trigger="click" popper-class="processing-node-menu" class="plus-dag">
<i class="plus-action" :class="{ 'plus-action-selected': plusActionSelected }" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in PROCESSING_TYPE_LIST" class="each-sub-menu">
<a @click="clickPlusDragMenu(item.type)">
<i
class="node-mini-logo"
:style="{ backgroundImage: `url(${NODE_TYPE_LOGO[item.type]})` }"
/>
<span>{{ item.name }}</span>
</a>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div> -->
</div>
</div>
</template>
<script lang="ts" setup>
import { defineComponent, ref, shallowRef, onMounted, inject, defineExpose } from 'vue'
import { Graph, Node, Path, Edge, Platform, StringExt } from '@antv/x6'
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
import { Tooltip } from '@/directives/tooltip.ts'
const vTooltip = Tooltip // 指令
enum NodeType {
RECEIVE = 'RECEIVE', // 数据输入
PROCESS = 'PROCESS', // 数据过滤
// JOIN = 'JOIN', // 数据连接
// UNION = 'UNION', // 数据合并
// AGG = 'AGG', // 数据聚合
SEND = 'SEND', // 数据输出
}
// 元素校验状态
enum CellStatus {
DEFAULT = 'default',
SUCCESS = 'success',
ERROR = 'error',
}
// 节点位置信息
interface Position {
x: number
y: number
}
// 加工类型列表
const PROCESSING_TYPE_LIST: { type: NodeType; name: string } = [
{
type: 'PROCESS',
name: '数据筛选',
},
// {
// type: 'JOIN',
// name: '数据连接',
// },
// {
// type: 'UNION',
// name: '数据合并',
// },
// {
// type: 'AGG',
// name: '数据聚合',
// },
// {
// type: 'SEND',
// name: '数据输出',
// },
]
// 不同节点类型的icon
const NODE_TYPE_LOGO: Record<string, string> = {
RECEIVE:
'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*RXnuTpQ22xkAAAAAAAAAAAAADtOHAQ/original', // 数据输入
PROCESS:
'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*ZJ6qToit8P4AAAAAAAAAAAAADtOHAQ/original', // 数据筛选
JOIN: 'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*EHqyQoDeBvIAAAAAAAAAAAAADtOHAQ/original', // 数据连接
UNION:
'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*k4eyRaXv8gsAAAAAAAAAAAAADtOHAQ/original', // 数据合并
AGG: 'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*TKG8R6nfYiAAAAAAAAAAAAAADtOHAQ/original', // 数据聚合
SEND: 'https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*zUgORbGg1HIAAAAAAAAAAAAADtOHAQ/original', // 数据输出
}
const plusActionSelected = ref(false)
const node = shallowRef<Node>({})
const nodeData = shallowRef<Record<string, any>>({})
const getNode: any = inject('getNode')
node.value = getNode() as Node
nodeData.value = node.value?.getData() as Record<string, any>
onMounted(() => {
node.value.on('change:data', ({ current }) => {
nodeData.value = current
})
})
function createDownstream(type: NodeType) {
const { graph } = node.value?.model || {}
if (graph) {
// 获取下游节点的初始位置信息
const position = getDownstreamNodePosition(node.value, graph)
// 创建下游节点
const newNode: any = createNode(type, graph, position)
const source: string = node.value.id
const target: string = newNode.id
// 创建该节点出发到下游节点的边
createEdge(source, target, graph)
}
}
function createNode(type: NodeType, graph: Graph, position?: Position) {
if (!graph) {
return {}
}
let newNode = {}
const sameTypeNodes = graph.getNodes().filter((item) => item.getData()?.type === type)
const typeName = PROCESSING_TYPE_LIST?.find((item) => item.type === type)?.name
const id = StringExt.uuid()
const node = {
id,
shape: 'data-processing-dag-node',
x: position?.x,
y: position?.y,
ports: getPortsByType(type, id),
data: {
name: `${typeName}_${sameTypeNodes.length + 1}`,
type,
},
}
newNode = graph.addNode(node)
return newNode
}
function createEdge(source: string, target: string, graph: Graph) {
const edge = {
id: StringExt.uuid(),
shape: 'data-processing-curve',
source: {
cell: source,
port: `${source}-out`,
},
target: {
cell: target,
port: `${target}-in`,
},
zIndex: -1,
data: {
source,
target,
},
}
if (graph) {
graph.addEdge(edge)
}
}
// 根据节点的类型获取ports
function getPortsByType(type: NodeType, nodeId: string) {
let ports = []
switch (type) {
case NodeType.RECEIVE:
ports = [
{
id: `${nodeId}-out`,
group: 'out',
},
]
break
case NodeType.SEND:
ports = [
{
id: `${nodeId}-in`,
group: 'in',
},
]
break
default:
ports = [
{
id: `${nodeId}-in`,
group: 'in',
},
{
id: `${nodeId}-out`,
group: 'out',
},
]
break
}
return ports
}
function getDownstreamNodePosition(node: Node, graph: Graph, dx = 220, dy = 100) {
// 找出画布中以该起始节点为起点的相关边的终点id集合
const downstreamNodeIdList: string[] = []
graph.getEdges().forEach((edge: any) => {
const originEdge = edge.toJSON()?.data || {}
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()
// 找到所有节点中最左侧的节点的x坐标
if (nodePosition.x < minX) {
minX = nodePosition.x
}
// 找到所有节点中最x下方的节点的y坐标
if (nodePosition.y > maxY) {
maxY = nodePosition.y
}
}
})
return {
x: minX !== Infinity ? minX + dx : position.x,
y: maxY !== -Infinity ? maxY : position.y + dy,
}
}
function clickPlusDragMenu(type: NodeType) {
createDownstream(type)
plusActionSelected.value = false
}
function onPlusDropdownOpenChange(value: boolean) {
plusActionSelected.value = value
}
function onMainMouseEnter() {
const ports = node.value?.getPorts() || []
ports.forEach((port) => {
node.value.setPortProp(port.id, 'attrs/circle', {
fill: '#fff',
stroke: '#85A5FF',
})
})
}
function onMainMouseLeave() {
// 获取该节点下的所有连接桩
const ports = node.value?.getPorts() || []
ports.forEach((port) => {
node.value.setPortProp(port.id, 'attrs/circle', {
fill: 'transparent',
stroke: 'transparent',
})
})
}
function handleEdit() {
const { graph } = node.value?.model || {}
if (graph) {
graph.emit('node:config', { data: { ...nodeData.value }, node: node.value })
}
}
defineExpose({ getPortsByType })
</script>
<style lang="scss" scoped>
.flex-vcenter {
display: flex;
align-items: center;
}
.flex-1 {
flex: 1;
}
.text-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.data-processing-dag-node {
display: flex;
flex-direction: row;
align-items: center;
}
.main-area {
padding: 12px;
width: 180px;
height: 48px;
color: rgba(0, 0, 0, 65%);
font-size: 12px;
font-family: PingFangSC;
line-height: 24px;
background-color: #fff;
box-shadow:
0 -1px 4px 0 rgba(209, 209, 209, 50%),
1px 1px 4px 0 rgba(217, 217, 217, 50%);
border-radius: 2px;
border: 1px solid transparent;
border-radius: 4px;
overflow: hidden;
}
.main-area:hover {
border: 1px solid rgba(0, 0, 0, 10%);
box-shadow:
0 -2px 4px 0 rgba(209, 209, 209, 50%),
2px 2px 4px 0 rgba(217, 217, 217, 50%);
}
.node-logo {
display: inline-block;
width: 24px;
height: 24px;
background-repeat: no-repeat;
background-position: center;
background-size: 100%;
}
.node-name {
overflow: hidden;
display: inline-block;
width: 95px;
margin-left: 6px;
color: rgba(0, 0, 0, 65%);
font-size: 12px;
font-family: PingFangSC;
white-space: nowrap;
text-overflow: ellipsis;
vertical-align: top;
}
.status-action {
display: flex;
flex-direction: row;
align-items: center;
}
.status-icon {
display: inline-block;
width: 24px;
height: 24px;
}
.status-icon-error {
background: url('@/assets/icon-error.png') no-repeat center center / 100% 100%;
}
.status-icon-success {
background: url('@/assets/icon-success.png') no-repeat center center / 100% 100%;
}
.more-action-container {
width: 15px;
height: 15px;
text-align: center;
cursor: pointer;
}
.more-action {
visibility: hidden;
pointer-events: none;
}
.x6-node-selected .more-action {
visibility: visible;
pointer-events: all;
}
.plus-dag {
visibility: hidden;
position: absolute;
bottom: -6px;
left: calc(50% - 8px);
}
.plus-action {
position: absolute;
width: 16px;
height: 16px;
background: url('https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*ScX2R4ODfokAAAAAAAAAAAAADtOHAQ/original')
no-repeat center center / 100% 100%;
cursor: pointer;
}
.plus-action:hover {
background-image: url('https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*tRaoS5XhsuQAAAAAAAAAAAAADtOHAQ/original');
}
.plus-action:active,
.plus-action-selected {
background-image: url('https://mdn.alipayobjects.com/huamei_f4t1bn/afts/img/A*k9cnSaSmlw4AAAAAAAAAAAAADtOHAQ/original');
}
.x6-node-selected .main-area {
border-color: #3471f9;
}
.x6-node-selected .plus-dag {
visibility: visible;
}
.processing-node-menu {
padding: 2px 0;
width: 105px;
background-color: #fff;
box-shadow:
0 9px 28px 8px rgba(0, 0, 0, 5%),
0 6px 16px 0 rgba(0, 0, 0, 8%),
0 3px 6px -4px rgba(0, 0, 0, 12%);
border-radius: 2px;
}
.processing-node-menu ul {
margin: 0;
padding: 0;
}
.processing-node-menu li {
list-style: none;
}
.each-sub-menu {
padding: 6px 12px;
width: 100%;
}
.each-sub-menu:hover {
background-color: rgba(0, 0, 0, 4%);
}
.each-sub-menu a {
display: inline-block;
width: 100%;
height: 16px;
font-family: PingFangSC;
font-weight: 400;
font-size: 12px;
color: rgba(0, 0, 0, 65%);
}
.each-sub-menu span {
margin-left: 8px;
vertical-align: top;
}
.each-disabled-sub-menu a {
cursor: not-allowed;
color: rgba(0, 0, 0, 35%);
}
.node-mini-logo {
display: inline-block;
width: 16px;
height: 16px;
background-repeat: no-repeat;
background-position: center;
background-size: 100%;
vertical-align: top;
}
@keyframes running-line {
to {
stroke-dashoffset: -1000;
}
}
.main-area.success {
border-color: #52c41a;
border-left-width: 4px;
}
.main-area.error {
border-color: #ff4d4f;
border-left-width: 4px;
}
</style>
@/directives/tooltip.ts
typescript
import type { App } from 'vue'
import { ElTooltip } from 'element-plus'
import { useDirectiveComponent } from '@/hooks/useDirectiveComponent'
export const Tooltip = useDirectiveComponent(ElTooltip, (el: any, binding: Record<string, any>) => {
return {
content: typeof binding.value === 'boolean' ? undefined : binding.value,
placement: binding.arg,
enterable: false,
hideAfter: 0,
'virtual-triggering': true,
'virtual-ref': el,
}
})
export default (app: App) => {
app.directive('tooltip', Tooltip)
}
@/hooks/useDirectiveComponent
typescript
import { h, mergeProps, render, resolveComponent } from 'vue'
export function useDirectiveComponent(component: any, props: any) {
const concreteComponent = typeof component === 'string' ? resolveComponent(component) : component
const hook = mountComponent(concreteComponent, props)
return {
mounted: hook,
updated: hook,
unmounted(el: any) {
render(null, el)
},
}
}
function mountComponent(component: any, props: any) {
return function (el: any, binding: any) {
const _props = typeof props === 'function' ? props(el, binding) : props
const text = binding.value?.text ?? binding.value ?? _props?.text
const value = binding.value && typeof binding.value === 'object' ? binding.value : {}
const children = () => text ?? el.textContent
const node = h(component, mergeProps(_props, value), children)
render(node, el)
}
}
用法:
vue
<template>
<div style="display: flex">
<!-- 拖拽列表 -->
<div style="width: 200px">
<div
v-for="item in componentList"
class="node pointer text-ellipsis"
@mousedown="($event) => workflowViewRef.startDrag($event, item)"
>
{{ item.name }}
</div>
</div>
<div style="height: 300px; flex:1; overflow:hidden">
<workflowView
ref="workflowViewRef"
:initData="data"
@node:config="handleNodeConfig"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, useTemplateRef, onMounted, nextTick, defineExpose } from 'vue'
const workflowViewRef = useTemplateRef('workflowViewRef')
// 拖拽节点列表
const componentList= Ref([
{
"componentDefId": "1964962263787741185",
"name": "接收组件",
"componentType": "RECEIVE",
},
{
"componentDefId": "1964962263787741186",
"name": "过程组件",
"componentType": "PROCESS",
},
{
"componentDefId": "1964962263787741187",
"name": "发送组件",
"componentType": "SEND",
},
])
// 演示数据
const data = ref([
{
"shape": "data-processing-curve",
"connector": {
"name": "curveConnector"
},
"id": "434e0975-a09a-4b51-b265-03e1fc594ec4",
"zIndex": -1,
"source": {
"cell": "6c827f2f-7861-4cd3-9621-c97b1f41d4e0",
"port": "1757412449213blq1o-out"
},
"target": {
"cell": "ec51f09f-a546-4112-a463-c3ce6e123273",
"port": "1757412016182wgqto-in"
},
"tools": {
"items": [
{
"name": "vertices"
},
{
"name": "vertices"
},
{
"name": "vertices"
}
],
"name": null
}
},
{
"shape": "data-processing-curve",
"connector": {
"name": "curveConnector"
},
"id": "70c881ea-3165-40e2-a940-5c90d6e9e260",
"zIndex": -1,
"source": {
"cell": "ec51f09f-a546-4112-a463-c3ce6e123273",
"port": "1757412016182wgqto-out"
},
"target": {
"cell": "7fe9aec4-999a-4dfb-a99d-b209df2e2a58",
"port": "1757412446647i7yfn-in"
},
"tools": {
"items": [
{
"name": "vertices"
},
{
"name": "vertices"
},
{
"name": "vertices"
},
{
"name": "vertices"
},
{
"name": "vertices"
},
{
"name": "vertices"
},
{
"name": "vertices"
}
],
"name": null
}
},
{
"position": {
"x": 380,
"y": 170
},
"size": {
"width": 180,
"height": 48
},
"view": "vue-shape-view",
"shape": "data-processing-dag-node",
"ports": {
"groups": {
"in": {
"position": {
"name": "top",
"args": {}
},
"attrs": {
"circle": {
"r": 4,
"magnet": true,
"stroke": "transparent",
"strokeWidth": 1,
"fill": "transparent"
}
}
},
"out": {
"position": {
"name": "bottom",
"args": {}
},
"attrs": {
"circle": {
"r": 4,
"magnet": true,
"stroke": "transparent",
"strokeWidth": 1,
"fill": "transparent"
}
}
}
},
"items": [
{
"id": "1757412016182wgqto-in",
"group": "in",
"attrs": {
"circle": {
"fill": "#fff",
"stroke": "#85A5FF"
}
}
},
{
"id": "1757412016182wgqto-out",
"group": "out",
"attrs": {
"circle": {
"fill": "#fff",
"stroke": "#85A5FF"
}
}
}
]
},
"id": "ec51f09f-a546-4112-a463-c3ce6e123273",
"data": {
"componentDefId": "1964957852445286401",
"name": "mapping-test",
"nodeName": "mapping-test",
"componentType": "PROCESS"
},
"zIndex": 1
},
{
"position": {
"x": 380,
"y": 270
},
"size": {
"width": 180,
"height": 48
},
"view": "vue-shape-view",
"shape": "data-processing-dag-node",
"ports": {
"groups": {
"in": {
"position": {
"name": "top",
"args": {}
},
"attrs": {
"circle": {
"r": 4,
"magnet": true,
"stroke": "transparent",
"strokeWidth": 1,
"fill": "transparent"
}
}
},
"out": {
"position": {
"name": "bottom",
"args": {}
},
"attrs": {
"circle": {
"r": 4,
"magnet": true,
"stroke": "transparent",
"strokeWidth": 1,
"fill": "transparent"
}
}
}
},
"items": [
{
"id": "1757412446647i7yfn-in",
"group": "in",
"attrs": {
"circle": {
"fill": "#fff",
"stroke": "#85A5FF"
}
}
}
]
},
"id": "7fe9aec4-999a-4dfb-a99d-b209df2e2a58",
"data": {
"componentDefId": "1964853143181004801",
"name": "SFTP发送",
"nodeName": "SFTP发送",
"componentType": "SEND"
},
"zIndex": 2
},
{
"position": {
"x": 380,
"y": 70
},
"size": {
"width": 180,
"height": 48
},
"view": "vue-shape-view",
"shape": "data-processing-dag-node",
"ports": {
"groups": {
"in": {
"position": {
"name": "top",
"args": {}
},
"attrs": {
"circle": {
"r": 4,
"magnet": true,
"stroke": "transparent",
"strokeWidth": 1,
"fill": "transparent"
}
}
},
"out": {
"position": {
"name": "bottom",
"args": {}
},
"attrs": {
"circle": {
"r": 4,
"magnet": true,
"stroke": "transparent",
"strokeWidth": 1,
"fill": "transparent"
}
}
}
},
"items": [
{
"id": "1757412449213blq1o-out",
"group": "out",
"attrs": {
"circle": {
"fill": "#fff",
"stroke": "#85A5FF"
}
}
}
]
},
"id": "6c827f2f-7861-4cd3-9621-c97b1f41d4e0",
"data": {
"componentDefId": "1964852291938619394",
"name": "SFTP接收",
"nodeName": "SFTP接收",
"componentType": "RECEIVE"
},
"zIndex": 3
}
])
function handleNodeConfig(data) {
console.log(data)
}
</script>
<style lang="scss" scoped>
.node {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 10px;
cursor: move;
}
</style>