CzRger месяцев назад: 3
Родитель
Сommit
e40e94c232

+ 2 - 0
package.json

@@ -10,6 +10,8 @@
     "serve": "vite --host 0.0.0.0"
   },
   "dependencies": {
+    "@antv/x6": "^2.18.1",
+    "@antv/x6-vue-shape": "^2.1.2",
     "@types/node": "^20.17.11",
     "axios": "^1.7.9",
     "default-passive-events": "^2.0.0",

+ 9 - 1
src/router/index.ts

@@ -12,7 +12,7 @@ const routes = [
         name: 'root',
         path: '/',
         component: Layout,
-        redirect: '/draw',
+        redirect: '/workflow',
         children: [
         ]
     },
@@ -23,6 +23,14 @@ const routes = [
         meta: {
             noLoading: true
         }
+    },
+    {
+        name: 'workflow',
+        path: '/workflow',
+        component: () => import('@/views/workflow/index.vue'),
+        meta: {
+            noLoading: true
+        }
     }
 ]
 

+ 0 - 182
src/views/draw/FlowChart.vue

@@ -1,182 +0,0 @@
-<template>
-  <div class="flowchart-container" ref="container">
-    <div class="flowchart" :style="flowchartStyle">
-      <FlowNode
-        v-for="node in nodes"
-        :key="node.id"
-        :node="node"
-        @add-child="handleAddChild"
-        @node-drag="handleNodeDrag"
-      />
-      <svg class="connectors">
-        <path
-          v-for="connector in connectors"
-          :key="connector.id"
-          :d="connector.path"
-          stroke="#999"
-          stroke-width="2"
-          fill="none"
-          marker-end="url(#arrowhead)"
-        />
-      </svg>
-    </div>
-    <defs>
-      <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
-        <polygon points="0 0, 10 3.5, 0 7" fill="#999" />
-      </marker>
-    </defs>
-  </div>
-</template>
-
-<script setup>
-import { ref, computed, onMounted, watch } from 'vue'
-import { useDraggable } from '@vueuse/core'
-import FlowNode from './FlowNode.vue'
-
-// 节点类型定义
-const nodeTypes = {
-  start: { name: '开始', color: '#4CAF50' },
-  process: { name: '处理', color: '#2196F3' },
-  decision: { name: '判断', color: '#FFC107' },
-  end: { name: '结束', color: '#F44336' }
-}
-
-// 初始节点数据
-const nodes = ref([
-  {
-    id: '1',
-    type: 'start',
-    x: 300,
-    y: 50,
-    children: ['2']
-  },
-  {
-    id: '2',
-    type: 'process',
-    x: 300,
-    y: 150,
-    parent: '1'
-  }
-])
-
-// 容器拖拽
-const container = ref(null)
-// 修改容器拖拽初始化
-const { x: containerX, y: containerY } = useDraggable(container, {
-  onStart: (e) => {
-    // 只有当点击的不是节点时才允许拖拽容器
-    return !e.target.closest('.flow-node')
-  }
-})
-
-// 修改节点拖拽处理
-const handleNodeDrag = (nodeId, newX, newY) => {
-  const nodeIndex = nodes.value.findIndex(n => n.id === nodeId)
-  if (nodeIndex !== -1) {
-    // 创建新数组确保响应性
-    const newNodes = [...nodes.value]
-    newNodes[nodeIndex] = {
-      ...newNodes[nodeIndex],
-      x: newX,
-      y: newY
-    }
-    nodes.value = newNodes
-  }
-}
-const flowchartStyle = computed(() => ({
-  transform: `translate(${containerX.value}px, ${containerY.value}px)`
-}))
-
-// 计算连接线
-const connectors = computed(() => {
-  const result = []
-  nodes.value.forEach(node => {
-    if (node.children) {
-      node.children.forEach(childId => {
-        const childNode = nodes.value.find(n => n.id === childId)
-        if (childNode) {
-          result.push({
-            id: `${node.id}-${childId}`,
-            path: calculatePath(node, childNode)
-          })
-        }
-      })
-    }
-  })
-  return result
-})
-
-function calculatePath(startNode, endNode) {
-  const startX = startNode.x + 100
-  const startY = startNode.y + 40
-  const endX = endNode.x + 100
-  const endY = endNode.y
-
-  // 简单的贝塞尔曲线路径
-  const controlY = (startY + endY) / 2
-  return `M${startX},${startY} C${startX},${controlY} ${endX},${controlY} ${endX},${endY}`
-}
-
-// 添加子节点
-function handleAddChild(parentId, nodeType) {
-  const parentNode = nodes.value.find(n => n.id === parentId)
-  if (!parentNode) return
-
-  const newNodeId = Date.now().toString()
-  const newNode = {
-    id: newNodeId,
-    type: nodeType,
-    x: parentNode.x,
-    y: parentNode.y + 120,
-    parent: parentId
-  }
-
-  if (!parentNode.children) {
-    parentNode.children = []
-  }
-  parentNode.children.push(newNodeId)
-
-  nodes.value.push(newNode)
-}
-// 自动排列
-function autoLayout() {
-  // 这里可以实现自动排列算法,如树状布局等
-  // 简化版:简单垂直排列
-  nodes.value.forEach((node, index) => {
-    if (index > 0) {
-      node.x = 300
-      node.y = 50 + index * 120
-    }
-  })
-}
-
-// 初始化时自动排列
-onMounted(() => {
-  autoLayout()
-})
-</script>
-
-<style>
-.flowchart-container {
-  width: 100%;
-  height: 100vh;
-  overflow: hidden;
-  cursor: grab;
-}
-
-.flowchart {
-  position: relative;
-  width: max-content;
-  height: max-content;
-}
-
-.connectors {
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  pointer-events: none;
-  z-index: 0;
-}
-</style>

+ 0 - 158
src/views/draw/FlowNode.vue

@@ -1,158 +0,0 @@
-<template>
-  <div
-    class="flow-node"
-    :style="nodeStyle"
-    ref="nodeElement"
-    @mousedown="startDrag"
-  >
-    <div class="node-header" :style="headerStyle">
-      {{ nodeData.name }}
-    </div>
-    <div class="node-content">
-      <slot></slot>
-    </div>
-    <div class="node-footer">
-      <button
-        v-for="type in availableChildTypes"
-        :key="type"
-        @click.stop="addChild(type)"
-        class="add-child-btn"
-      >
-        添加{{ nodeTypes[type].name }}节点
-      </button>
-    </div>
-  </div>
-</template>
-
-<script setup>
-import { computed, ref } from 'vue'
-
-const props = defineProps({
-  node: {
-    type: Object,
-    required: true
-  }
-})
-
-const emit = defineEmits(['add-child', 'node-drag'])
-
-const nodeTypes = {
-  start: { name: '开始', color: '#4CAF50' },
-  process: { name: '处理', color: '#2196F3' },
-  decision: { name: '判断', color: '#FFC107' },
-  end: { name: '结束', color: '#F44336' }
-}
-
-const nodeElement = ref(null)
-const isDragging = ref(false)
-const dragStartPos = ref({ x: 0, y: 0 })
-const nodeStartPos = ref({ x: 0, y: 0 })
-
-const nodeData = computed(() => ({
-  ...props.node,
-  ...nodeTypes[props.node.type]
-}))
-
-const nodeStyle = computed(() => ({
-  left: `${props.node.x}px`,
-  top: `${props.node.y}px`,
-  borderColor: nodeData.value.color
-}))
-
-const headerStyle = computed(() => ({
-  backgroundColor: nodeData.value.color
-}))
-
-const availableChildTypes = computed(() => {
-  if (props.node.type === 'start') return ['process', 'decision']
-  if (props.node.type === 'process') return ['process', 'decision', 'end']
-  if (props.node.type === 'decision') return ['process', 'end']
-  return []
-})
-
-function addChild(type) {
-  emit('add-child', props.node.id, type)
-}
-
-function startDrag(e) {
-  if (e.target.closest('.add-child-btn')) return
-
-  isDragging.value = true
-  dragStartPos.value = {
-    x: e.clientX,
-    y: e.clientY
-  }
-  nodeStartPos.value = {
-    x: props.node.x,
-    y: props.node.y
-  }
-
-  document.addEventListener('mousemove', handleDrag)
-  document.addEventListener('mouseup', stopDrag)
-  e.preventDefault()
-}
-
-function handleDrag(e) {
-  if (!isDragging.value) return
-
-  const dx = e.clientX - dragStartPos.value.x
-  const dy = e.clientY - dragStartPos.value.y
-
-  const newX = nodeStartPos.value.x + dx
-  const newY = nodeStartPos.value.y + dy
-
-  emit('node-drag', props.node.id, newX, newY)
-}
-
-function stopDrag() {
-  isDragging.value = false
-  document.removeEventListener('mousemove', handleDrag)
-  document.removeEventListener('mouseup', stopDrag)
-}
-</script>
-
-<style>
-.flow-node {
-  position: absolute;
-  width: 200px;
-  background: white;
-  border: 2px solid;
-  border-radius: 4px;
-  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
-  z-index: 1;
-  cursor: move;
-  user-select: none;
-}
-
-.node-header {
-  padding: 8px 12px;
-  color: white;
-  font-weight: bold;
-  border-top-left-radius: 2px;
-  border-top-right-radius: 2px;
-}
-
-.node-content {
-  padding: 12px;
-}
-
-.node-footer {
-  padding: 8px;
-  display: flex;
-  flex-direction: column;
-  gap: 4px;
-}
-
-.add-child-btn {
-  padding: 4px 8px;
-  background: #f5f5f5;
-  border: 1px solid #ddd;
-  border-radius: 3px;
-  cursor: pointer;
-  font-size: 12px;
-}
-
-.add-child-btn:hover {
-  background: #e0e0e0;
-}
-</style>

+ 32 - 0
src/views/draw/node/component/node-operation.vue

@@ -0,0 +1,32 @@
+<template>
+  <div class="node-operation" :style="{top: top + 'px'}">
+    <SvgIcon name="czr_add" color="#ffffff" size="12"/>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {getCurrentInstance, reactive, ref} from "vue";
+
+const emits = defineEmits([])
+const props = defineProps({
+  top: {default: 0},
+  addRadius: {default: 0}
+})
+const addRadiusPx = props.addRadius + 'px'
+const {proxy}: any = getCurrentInstance()
+const state: any = reactive({})
+</script>
+
+<style lang="scss" scoped>
+.node-operation {
+  background-color: #286bfb;
+  width: calc(v-bind(addRadiusPx) * 2);
+  height: calc(v-bind(addRadiusPx) * 2);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: absolute;
+  right: calc(v-bind(addRadiusPx) * -1);
+}
+</style>

+ 4 - 19
src/views/draw/node/index.vue

@@ -27,15 +27,11 @@
     <template v-if="state.active || state.hover">
       <template v-if="item.data?.levels?.length > 0">
         <template v-for="level in item.data.levels">
-          <div class="line-operation" :style="{top: getLevelTop(level)}">
-            <SvgIcon name="czr_add" color="#ffffff" size="12"/>
-          </div>
+          <nodeOperation :top="getLevelTop(level)" :add-radius="addRadius"/>
         </template>
       </template>
       <template v-else>
-        <div class="line-operation" :style="{top: (nodeStyleConfig.titleHeight / 2 - addRadius) + 'px'}">
-          <SvgIcon name="czr_add" color="#ffffff" :size="addRadius + 2"/>
-        </div>
+        <nodeOperation :top="nodeStyleConfig.titleHeight / 2 - addRadius" :add-radius="addRadius"/>
       </template>
     </template>
   </div>
@@ -45,6 +41,7 @@
 import {getCurrentInstance, reactive, ref} from "vue";
 import {nodeStyleConfig, nodeTypeConfig} from '@/views/draw/config/config'
 import nodeIf from '@/views/draw/node/component/if/index.vue'
+import nodeOperation from '@/views/draw/node/component/node-operation.vue'
 import {IfLevel} from "@/views/draw/node/component/if/type";
 
 const emits = defineEmits(['update:x', 'update:y', 'update:w', 'update:h'])
@@ -61,7 +58,6 @@ const state: any = reactive({
   hover: false,
 })
 const addRadius = nodeStyleConfig.lineOperationRadius
-const addRadiusPx = addRadius + 'px'
 const ref_node = ref()
 const moveStart = (e) => {
   state.moving = true
@@ -83,7 +79,7 @@ const getLevelTop = (level: IfLevel) => {
     const dom = node.querySelector(`#LEVEL_${level.id}`)
     top = dom.offsetTop + 2
   }
-  return top + 'px'
+  return top
 }
 </script>
 
@@ -113,16 +109,5 @@ const getLevelTop = (level: IfLevel) => {
     padding: 0px 12px;
     margin-bottom: 8px;
   }
-  .line-operation {
-    background-color: #286bfb;
-    width: calc(v-bind(addRadiusPx) * 2);
-    height: calc(v-bind(addRadiusPx) * 2);
-    border-radius: 50%;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    position: absolute;
-    right: calc(v-bind(addRadiusPx) * -1);
-  }
 }
 </style>

+ 89 - 0
src/views/workflow/chart/index.vue

@@ -0,0 +1,89 @@
+<template>
+  <div class="chart">
+    <div class="chart-block">
+      <div class="chart-ref" ref="ref_chart"></div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {getCurrentInstance, inject, onMounted, reactive, ref, watch} from "vue";
+import { Graph } from '@antv/x6'
+import {WorkflowFunc} from "@/views/workflow/types";
+
+const emits = defineEmits([])
+const props = defineProps({
+  data: <any>{}
+})
+const {proxy}: any = getCurrentInstance()
+const state: any = reactive({
+  graph: null
+})
+const ref_chart = ref()
+const workflowFuncInject = inject('workflowFunc', {} as WorkflowFunc)
+const initChart = () => {
+  const graph = new Graph({
+    container: ref_chart.value,
+    autoResize: true,
+    panning: true,
+    mousewheel: true,
+    grid: {
+      visible: true,
+      type: 'doubleMesh',
+      args: [
+        {
+          color: '#eee', // 主网格线颜色
+          thickness: 1, // 主网格线宽度
+        },
+        {
+          color: '#ddd', // 次网格线颜色
+          thickness: 1, // 次网格线宽度
+          factor: 4, // 主次网格线间隔
+        },
+      ],
+    },
+    connecting: {
+      anchor: {
+        name: 'topLeft',
+        args: {
+          dx: 0,
+          dy: 25
+        },
+      },
+    },
+  })
+  graph.fromJSON(props.data)
+  graph.zoomToFit({ maxScale: 1 })
+  graph.centerContent() // 居中显示
+  graph.on('node:click', ({ e, x, y, node, view }) => {
+    workflowFuncInject.nodeClick(node)
+  })
+  state.graph = graph
+}
+watch(() => props.data, (n) => {
+  if (n) {
+    initChart()
+  }
+}, {immediate: true})
+onMounted(() => {
+})
+defineExpose({
+  toJSON: () => state.graph.toJSON()
+})
+</script>
+
+<style lang="scss" scoped>
+.chart {
+  width: 100%;
+  height: 100%;
+  background-color: #f2f4f7;
+  .chart-block {
+    width: 100%;
+    height: 100%;
+    .chart-ref {
+      width: 100%;
+      height: 100%;
+    }
+  }
+}
+</style>

+ 110 - 0
src/views/workflow/index.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="workflow">
+    <TeleportContainer/>
+    <el-button style="position: absolute; top: 0;left: 0; z-index: 2" type="primary" @click="getJsonData">保存配置</el-button>
+    <div class="workflow-chart">
+      <workflowChart :data="state.workflowData" ref="ref_workflow"/>
+    </div>
+    <workflowPanel v-model:show="state.showPanel" :node="state.currentNode"/>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {getCurrentInstance, onMounted, provide, reactive, ref} from "vue";
+import workflowChart from './chart/index.vue'
+import workflowPanel from './panel/index.vue'
+import { getTeleport, register } from '@antv/x6-vue-shape'
+import {data} from './mockJson'
+import {WorkflowFunc} from "@/views/workflow/types";
+import WorkflowNode from "@/views/workflow/nodes/index.vue";
+const TeleportContainer = getTeleport()
+register({
+  shape: 'workflow-node',
+  component: WorkflowNode,
+})
+const emits = defineEmits([])
+const props = defineProps({})
+const {proxy}: any = getCurrentInstance()
+const state: any = reactive({
+  workflowData: null,
+  showPanel: false,
+  currentNode: null,
+})
+const ref_workflow = ref()
+provide('workflowFunc', <WorkflowFunc>{
+  nodeClick: (node) => {
+    state.currentNode = node
+    state.showPanel = true
+  }
+})
+const getJsonData = () => {
+  const data = ref_workflow.value.toJSON()
+  console.log(data)
+}
+const initData = () => {
+  const formatData = (data) => {
+    const res: any = {
+      nodes: [],
+      edges: [],
+    }
+    data.nodes.forEach((v) => {
+      const node = {
+        ...v,
+        shape: 'workflow-node',
+      }
+      res.nodes.push(node)
+    })
+    data.edges.forEach((v) => {
+      const edge = {
+        ...v,
+        shape: 'edge',
+        attrs: {
+          line: {
+            stroke: '#8f8f8f',
+            strokeWidth: 1,
+          },
+        },
+        router: {
+          name: 'manhattan',
+          args: {
+            startDirections: ['right'],
+            endDirections: ['left'],
+          },
+        },
+      }
+      edge.source = {
+        cell: v.source,
+        anchor: {
+          name: 'topRight',
+          args: {
+            dx: 0,
+            dy: 25
+          }
+        },
+      }
+      res.edges.push(edge)
+    })
+    return res
+  }
+  state.workflowData = formatData(data)
+}
+onMounted(() => {
+  initData()
+})
+</script>
+
+<style lang="scss" scoped>
+.workflow {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+  .workflow-chart {
+    width: 100%;
+    height: 100%;
+    z-index: 1;
+  }
+}
+</style>

+ 81 - 0
src/views/workflow/mockJson.ts

@@ -0,0 +1,81 @@
+// export const data = {
+//   nodes: [
+//     {
+//       id: 'node1',
+//       shape: 'workflow-node',
+//       x: 40,
+//       y: 40,
+//       width: 100,
+//       height: 100,
+//       data: {
+//         title: '开始节点xx',
+//         type: 'root',
+//         p1: '啥的哈哈哈哈哈'
+//       }
+//     },
+//     {
+//       id: 'node2',
+//       shape: 'workflow-node',
+//       x: 160,
+//       y: 180,
+//       width: 100,
+//       height: 100,
+//       data: {
+//         title: '条件判断节点fff',
+//         type: 'if-else',
+//         p1: 'asfasfa飞洒'
+//       }
+//     },
+//   ],
+//   edges: [
+//     {
+//       shape: 'edge',
+//       source: {
+//         cell: 'node1',
+//         anchor: {
+//           name: 'topRight'
+//         }
+//       },
+//       target: 'node2',
+//       attrs: {
+//         // line 是选择器名称,选中的边的 path 元素
+//         line: {
+//           stroke: '#8f8f8f',
+//           strokeWidth: 1,
+//         },
+//       },
+//     },
+//   ],
+// }
+
+export const data = {
+  nodes: [
+    {
+      id: 'node1',
+      x: 40,
+      y: 40,
+      data: {
+        title: '开始节点xx',
+        type: 'root',
+        p1: '啥的哈哈哈哈哈'
+      }
+    },
+    {
+      id: 'node2',
+      x: 160,
+      y: 180,
+      data: {
+        title: '条件判断节点fff',
+        type: 'if-else',
+        p1: 'asfasfa飞洒'
+      }
+    },
+  ],
+  edges: [
+    {
+      source: 'node1',
+      target: 'node2',
+    },
+  ],
+}
+

+ 26 - 0
src/views/workflow/nodes/if-else/index.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="node-if-else">
+    <div>p1:{{state.nodeData?.p1}}</div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {computed, getCurrentInstance, inject, onMounted, reactive, ref} from "vue";
+
+const emits = defineEmits([])
+const props = defineProps({
+  node: <any>{}
+})
+const {proxy}: any = getCurrentInstance()
+const state: any = reactive({
+  nodeData: {}
+})
+onMounted(() => {
+  state.nodeData = props.node.data
+})
+</script>
+
+<style lang="scss" scoped>
+.node-root {
+}
+</style>

+ 65 - 0
src/views/workflow/nodes/index.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="node" ref="ref_node">
+    <div class="node-title">{{ state.nodeData?.title }}</div>
+    <div class="node-content">
+      <template v-if="state.nodeData?.type === 'root'">
+        <rootNode :node="state.node"/>
+      </template>
+      <template v-else-if="state.nodeData?.type === 'if-else'">
+        <ifElseNode :node="state.node"/>
+      </template>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {computed, getCurrentInstance, inject, onMounted, reactive, ref} from "vue";
+import rootNode from './root/index.vue'
+import ifElseNode from './if-else/index.vue'
+
+const getNode: any = inject('getNode')
+const emits = defineEmits([])
+const props = defineProps({})
+const {proxy}: any = getCurrentInstance()
+const state: any = reactive({
+  node: null,
+  nodeData: {}
+})
+const ref_node = ref()
+onMounted(() => {
+  state.node = getNode()
+  state.nodeData = state.node.data
+  state.node.size(ref_node.value.clientWidth, ref_node.value.clientHeight)
+})
+</script>
+
+<style lang="scss" scoped>
+.node {
+  z-index: 10;
+  border-style: solid;
+  border-color: rgba(0, 0, 0, 0.1);
+  border-radius: 10px;
+  background-color: #ffffff;
+  position: absolute;
+  min-width: 240px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  &:hover {
+    border-color: #286bfb;
+    cursor: pointer;
+  }
+  .node-title {
+    height: 50px;
+    width: 100%;
+    display: flex;
+    align-items: center;
+    padding: 8px 12px;
+    font-size: 14px;
+    font-weight: bold;
+  }
+  .node-content {
+    padding: 0px 12px;
+    margin-bottom: 8px;
+    border-top: 1px solid rgba(0, 0, 0, 0.1);
+  }
+}
+</style>

+ 26 - 0
src/views/workflow/nodes/root/index.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="node-root">
+    <div>p1:{{state.nodeData?.p1}}</div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {computed, getCurrentInstance, inject, onMounted, reactive, ref} from "vue";
+
+const emits = defineEmits([])
+const props = defineProps({
+  node: <any>{}
+})
+const {proxy}: any = getCurrentInstance()
+const state: any = reactive({
+  nodeData: {}
+})
+onMounted(() => {
+  state.nodeData = props.node.data
+})
+</script>
+
+<style lang="scss" scoped>
+.node-root {
+}
+</style>

+ 70 - 0
src/views/workflow/panel/index.vue

@@ -0,0 +1,70 @@
+<template>
+  <transition name="slide">
+    <div class="panel" v-if="show">
+      <div class="panel-header">
+        <h3>{{ nodeDataCpt.title }}</h3>
+        <button @click="$emit('update:show', false)">×</button>
+      </div>
+      <div class="panel-content">
+        <template v-if="nodeDataCpt.type === 'root'">
+          <rootPanel :node="node"/>
+        </template>
+        <template v-if="nodeDataCpt.type === 'if-else'">
+          <ifElsePanel :node="node"/>
+        </template>
+      </div>
+    </div>
+  </transition>
+</template>
+
+<script setup lang="ts">
+import {computed, getCurrentInstance, reactive, ref} from "vue";
+import rootPanel from './root/index.vue'
+import ifElsePanel from './is-else/index.vue'
+
+const emits = defineEmits([])
+const props = defineProps({
+  show: {},
+  node: {}
+})
+const {proxy}: any = getCurrentInstance()
+const state: any = reactive({})
+const nodeDataCpt = computed(() => props.node?.data || {})
+</script>
+
+<style lang="scss" scoped>
+.panel {
+  position: fixed;
+  top: 0;
+  right: 0;
+  width: 400px;
+  height: 100%;
+  background-color: white;
+  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  display: flex;
+  flex-direction: column;
+  .panel-header {
+    padding: 16px;
+    border-bottom: 1px solid #eee;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+  .panel-content {
+    padding: 16px;
+    flex: 1;
+    overflow-y: auto;
+  }
+}
+
+/* 过渡动画 */
+.slide-enter-active,
+.slide-leave-active {
+  transition: transform 0.3s ease;
+}
+.slide-enter-from,
+.slide-leave-to {
+  transform: translateX(100%);
+}
+</style>

+ 26 - 0
src/views/workflow/panel/is-else/index.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="root-panel" v-if="state.nodeData">
+    <el-input v-model="state.nodeData.p1"/>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {getCurrentInstance, reactive, ref, watch} from "vue";
+
+const emits = defineEmits([])
+const props = defineProps({
+  node: <any>{}
+})
+const {proxy}: any = getCurrentInstance()
+const state: any = reactive({
+  nodeData: null,
+})
+watch(() => props.node, (n) => {
+  if (n) {
+    state.nodeData = n.data
+  }
+}, {immediate: true})
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 26 - 0
src/views/workflow/panel/root/index.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="root-panel" v-if="state.nodeData">
+    <el-input v-model="state.nodeData.p1"/>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {getCurrentInstance, reactive, ref, watch} from "vue";
+
+const emits = defineEmits([])
+const props = defineProps({
+  node: <any>{}
+})
+const {proxy}: any = getCurrentInstance()
+const state: any = reactive({
+  nodeData: null,
+})
+watch(() => props.node, (n) => {
+  if (n) {
+    state.nodeData = n.data
+  }
+}, {immediate: true})
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 3 - 0
src/views/workflow/types.ts

@@ -0,0 +1,3 @@
+export type WorkflowFunc = {
+  nodeClick: (node) => void
+}

+ 40 - 1
yarn.lock

@@ -2,6 +2,35 @@
 # yarn lockfile v1
 
 
+"@antv/x6-common@^2.0.16":
+  version "2.0.17"
+  resolved "https://registry.npmmirror.com/@antv/x6-common/-/x6-common-2.0.17.tgz#a40a0920859faadf567452dea81132783a19aaa4"
+  integrity sha512-37g7vmRkNdYzZPdwjaMSZEGv/MMH0S4r70/Jwoab1mioycmuIBN73iyziX8m56BvJSDucZ3J/6DU07otWqzS6A==
+  dependencies:
+    lodash-es "^4.17.15"
+    utility-types "^3.10.0"
+
+"@antv/x6-geometry@^2.0.5":
+  version "2.0.5"
+  resolved "https://registry.npmmirror.com/@antv/x6-geometry/-/x6-geometry-2.0.5.tgz#c158317d74135bedd78c2fdeb76f9c7cfa0ef0aa"
+  integrity sha512-MId6riEQkxphBpVeTcL4ZNXL4lScyvDEPLyIafvWMcWNTGK0jgkK7N20XSzqt8ltJb0mGUso5s56mrk8ysHu2A==
+
+"@antv/x6-vue-shape@^2.1.2":
+  version "2.1.2"
+  resolved "https://registry.npmmirror.com/@antv/x6-vue-shape/-/x6-vue-shape-2.1.2.tgz#880abc2842f47f668c3de97cbacd1618bee1427a"
+  integrity sha512-lfLNJ2ztK8NP2JBAWTD6m5Wol0u6tOqj2KdOhWZoT8EtEw9rMmAdxsr8uTi9MRJO9pDMM0nbsR3cidnMh7VeDQ==
+  dependencies:
+    vue-demi latest
+
+"@antv/x6@^2.18.1":
+  version "2.18.1"
+  resolved "https://registry.npmmirror.com/@antv/x6/-/x6-2.18.1.tgz#4f3fab8c24119ca5f83583188657fc243cc5a0bc"
+  integrity sha512-FkWdbLOpN9J7dfJ+kiBxzowSx2N6syBily13NMVdMs+wqC6Eo5sLXWCZjQHateTFWgFw7ZGi2y9o3Pmdov1sXw==
+  dependencies:
+    "@antv/x6-common" "^2.0.16"
+    "@antv/x6-geometry" "^2.0.5"
+    utility-types "^3.10.0"
+
 "@babel/helper-string-parser@^7.25.9":
   version "7.25.9"
   resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz"
@@ -2867,7 +2896,7 @@ loader-utils@^1.1.0:
     emojis-list "^3.0.0"
     json5 "^1.0.1"
 
-lodash-es@^4.17.21:
+lodash-es@^4.17.15, lodash-es@^4.17.21:
   version "4.17.21"
   resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz"
   integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
@@ -4565,6 +4594,11 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2:
   resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz"
   integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
 
+utility-types@^3.10.0:
+  version "3.11.0"
+  resolved "https://registry.npmmirror.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c"
+  integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==
+
 uuid@^10.0.0:
   version "10.0.0"
   resolved "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz"
@@ -4654,6 +4688,11 @@ vue-demi@*:
   resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz"
   integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
 
+vue-demi@latest:
+  version "0.14.10"
+  resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
+  integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
+
 vue-router@^4.5.0:
   version "4.5.0"
   resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz"